mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(landing): optimize core web vitals and accessibility (#4193)
* improvement(landing): optimize core web vitals and accessibility Code-split AuthModal and DemoRequestModal via next/dynamic across 7 landing components to move auth-client bundle (~150-250KB) out of the initial JS payload. Replace useSession import in navbar with direct SessionContext read to avoid pulling the entire better-auth client into the landing page bundle. Add immutable cache header for content-hashed _next/static assets. Defer PostHog session recording until user identification to avoid loading the recorder (~80KB) on anonymous visits. Fix accessibility issues flagged by Lighthouse: add missing aria-label on preview submit button, add inert to aria-hidden ReactFlow wrapper, set decorative alt on logos inside labeled links, disambiguate duplicate footer API links. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(posthog): guard startSessionRecording against repeated calls on refetch The effect fires on every session reload (e.g., subscription upgrade). Calling startSessionRecording() while already recording fragments the session in the analytics dashboard. Add sessionRecordingStarted() guard so recording only starts once per page lifecycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(config): remove redundant _next/static cache header Next.js already sets Cache-Control: public, max-age=31536000, immutable on _next/static assets natively and this cannot be overridden. The custom rule was redundant on Vercel and conflicted with the extension-based rule on self-hosted deployments due to last-match-wins ordering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ba6bc91681
commit
23ccd4a50c
@@ -1,12 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
const AuthModal = dynamic(() =>
|
||||
import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal)
|
||||
)
|
||||
|
||||
interface DotGridProps {
|
||||
className?: string
|
||||
cols: number
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Image from 'next/image'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { FeaturesPreview } from '@/app/(landing)/components/features/components/features-preview'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
const AuthModal = dynamic(() =>
|
||||
import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal)
|
||||
)
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const r = Number.parseInt(hex.slice(1, 3), 16)
|
||||
const g = Number.parseInt(hex.slice(3, 5), 16)
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { captureClientEvent } from '@/lib/posthog/client'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
|
||||
|
||||
const AuthModal = dynamic(() =>
|
||||
import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal)
|
||||
)
|
||||
|
||||
const MAX_HEIGHT = 120
|
||||
|
||||
const CTA_BUTTON =
|
||||
|
||||
@@ -38,7 +38,7 @@ const BLOCK_LINKS: FooterItem[] = [
|
||||
{ label: 'Router', href: 'https://docs.sim.ai/blocks/router', external: true },
|
||||
{ label: 'Function', href: 'https://docs.sim.ai/blocks/function', external: true },
|
||||
{ label: 'Condition', href: 'https://docs.sim.ai/blocks/condition', external: true },
|
||||
{ label: 'API', href: 'https://docs.sim.ai/blocks/api', external: true },
|
||||
{ label: 'API Block', href: 'https://docs.sim.ai/blocks/api', external: true },
|
||||
{ label: 'Workflow', href: 'https://docs.sim.ai/blocks/workflow', external: true },
|
||||
{ label: 'Parallel', href: 'https://docs.sim.ai/blocks/parallel', external: true },
|
||||
{ label: 'Guardrails', href: 'https://docs.sim.ai/blocks/guardrails', external: true },
|
||||
@@ -194,7 +194,7 @@ export default function Footer({ hideCTA }: FooterProps) {
|
||||
<Link href='/' aria-label='Sim home'>
|
||||
<Image
|
||||
src='/logo/sim-landing.svg'
|
||||
alt='Sim'
|
||||
alt=''
|
||||
width={85}
|
||||
height={26}
|
||||
className='h-[26.4px] w-auto'
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
const AuthModal = dynamic(() =>
|
||||
import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal)
|
||||
)
|
||||
|
||||
const DemoRequestModal = dynamic(() =>
|
||||
import('@/app/(landing)/components/demo-request/demo-request-modal').then(
|
||||
(m) => m.DemoRequestModal
|
||||
)
|
||||
)
|
||||
|
||||
const LandingPreview = dynamic(
|
||||
() =>
|
||||
import('@/app/(landing)/components/landing-preview/landing-preview').then(
|
||||
|
||||
+1
@@ -334,6 +334,7 @@ export const LandingPreviewHome = memo(function LandingPreviewHome({
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
aria-label='Submit message'
|
||||
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#808080' : '#e0e0e0',
|
||||
|
||||
+5
-1
@@ -3,13 +3,13 @@
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Blimp, BubbleChatPreview, ChevronDown, MoreHorizontal, Play } from '@/components/emcn'
|
||||
import { AgentIcon, HubspotIcon, OpenAIIcon, SalesforceIcon } from '@/components/icons'
|
||||
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
|
||||
import { captureClientEvent } from '@/lib/posthog/client'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import {
|
||||
EASE_OUT,
|
||||
type EditorPromptData,
|
||||
@@ -21,6 +21,10 @@ import {
|
||||
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
const AuthModal = dynamic(() =>
|
||||
import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal)
|
||||
)
|
||||
|
||||
type PanelTab = 'copilot' | 'editor'
|
||||
|
||||
const EDITOR_BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
||||
import { useCallback, useContext, useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { GithubOutlineIcon } from '@/components/icons'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { SessionContext } from '@/app/_shell/providers/session-provider'
|
||||
import {
|
||||
BlogDropdown,
|
||||
type NavBlogPost,
|
||||
@@ -17,6 +17,10 @@ import { GitHubStars } from '@/app/(landing)/components/navbar/components/github
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
const AuthModal = dynamic(() =>
|
||||
import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal)
|
||||
)
|
||||
|
||||
type DropdownId = 'docs' | 'blog' | null
|
||||
|
||||
interface NavLink {
|
||||
@@ -48,7 +52,9 @@ interface NavbarProps {
|
||||
export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps) {
|
||||
const brand = getBrandConfig()
|
||||
const searchParams = useSearchParams()
|
||||
const { data: session, isPending: isSessionPending } = useSession()
|
||||
const sessionCtx = useContext(SessionContext)
|
||||
const session = sessionCtx?.data ?? null
|
||||
const isSessionPending = sessionCtx?.isPending ?? true
|
||||
const isAuthenticated = Boolean(session?.user?.id)
|
||||
const isBrowsingHome = searchParams.has('home')
|
||||
const useHomeLinks = isAuthenticated || isBrowsingHome
|
||||
@@ -125,7 +131,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
) : (
|
||||
<Image
|
||||
src='/logo/sim-landing.svg'
|
||||
alt='Sim'
|
||||
alt=''
|
||||
width={71}
|
||||
height={22}
|
||||
className='h-[22px] w-auto'
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
const AuthModal = dynamic(() =>
|
||||
import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal)
|
||||
)
|
||||
|
||||
const DemoRequestModal = dynamic(() =>
|
||||
import('@/app/(landing)/components/demo-request/demo-request-modal').then(
|
||||
(m) => m.DemoRequestModal
|
||||
)
|
||||
)
|
||||
|
||||
interface PricingTier {
|
||||
id: string
|
||||
name: string
|
||||
|
||||
@@ -470,7 +470,7 @@ export default function Templates() {
|
||||
aria-labelledby={`template-tab-${activeIndex}`}
|
||||
className='relative hidden flex-1 lg:block'
|
||||
>
|
||||
<div aria-hidden='true' className='h-full'>
|
||||
<div aria-hidden='true' inert className='h-full'>
|
||||
<LandingPreviewWorkflow
|
||||
key={activeIndex}
|
||||
workflow={activeWorkflow}
|
||||
|
||||
@@ -35,6 +35,7 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||
capture_performance: false,
|
||||
capture_dead_clicks: false,
|
||||
enable_heatmaps: false,
|
||||
disable_session_recording: true,
|
||||
session_recording: {
|
||||
maskAllInputs: false,
|
||||
maskInputOptions: {
|
||||
|
||||
@@ -92,6 +92,12 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
email_verified: data.user.emailVerified,
|
||||
created_at: data.user.createdAt,
|
||||
})
|
||||
if (
|
||||
typeof posthog.startSessionRecording === 'function' &&
|
||||
!posthog.sessionRecordingStarted()
|
||||
) {
|
||||
posthog.startSessionRecording()
|
||||
}
|
||||
} else {
|
||||
posthog.reset()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user