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:
Emir Karabeg
2026-04-15 19:56:16 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent ba6bc91681
commit 23ccd4a50c
12 changed files with 63 additions and 16 deletions
@@ -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(
@@ -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',
@@ -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()
}