diff --git a/public/images/og.png b/public/images/og.png new file mode 100644 index 0000000..9f37af5 Binary files /dev/null and b/public/images/og.png differ diff --git a/public/textures/earth-black-marble.jpg b/public/textures/earth-black-marble.jpg index 6889180..8e46853 100644 Binary files a/public/textures/earth-black-marble.jpg and b/public/textures/earth-black-marble.jpg differ diff --git a/skills/context/progress-tracker.md b/skills/context/progress-tracker.md index c61ca93..e706699 100644 --- a/skills/context/progress-tracker.md +++ b/skills/context/progress-tracker.md @@ -85,10 +85,9 @@ All seven homepage sections are built, wired, and animated (scroll-driven entran ## Next Steps -1. Performance review and optimization. -2. og image and description. -3. SEO, tracking validation, and launch metadata. -4. Staking page (remove `TEMP(staking-page)` comments when shipping). +1. og image and description. +2. launch metadata. +3. Staking page (remove `TEMP(staking-page)` comments when shipping). ## Latest Handoff diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 238f5db..0ddb9eb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -11,6 +11,15 @@ import { SITE_HEADER_SCROLL_BOOTSTRAP_SCRIPT, SITE_HEADER_SCROLL_CRITICAL_CSS, } from "@/lib/layout/site-header-scroll"; +import { + OG_IMAGE, + OG_PAGE_URL, + SITE_DESCRIPTION, + SITE_NAME, + SITE_TAGLINE, + SITE_URL, + TWITTER_HANDLE, +} from "@/lib/site"; import "./globals.css"; const monaSans = Mona_Sans({ @@ -19,16 +28,47 @@ const monaSans = Mona_Sans({ display: "swap", }); +// `optional` avoids the late font-swap repaint that made the hero subtitle's +// LCP land at font-arrival time; the preloaded font still wins on fast loads. const openSans = Open_Sans({ variable: "--font-open-sans", subsets: ["latin"], - display: "swap", + display: "optional", }); +const openGraphTitle = `${SITE_NAME} — ${SITE_TAGLINE}`; + export const metadata: Metadata = { - title: "Cortex Global", - description: - "A global network of local hubs, education, events, services, and real projects around emerging technology.", + metadataBase: new URL(SITE_URL), + title: { + default: SITE_NAME, + template: `%s | ${SITE_NAME}`, + }, + description: SITE_DESCRIPTION, + applicationName: SITE_NAME, + openGraph: { + type: "website", + locale: "en_US", + url: OG_PAGE_URL, + siteName: SITE_NAME, + title: openGraphTitle, + description: SITE_DESCRIPTION, + images: [ + { + url: OG_IMAGE.url, + width: OG_IMAGE.width, + height: OG_IMAGE.height, + alt: OG_IMAGE.alt, + }, + ], + }, + twitter: { + card: "summary_large_image", + site: `@${TWITTER_HANDLE}`, + title: openGraphTitle, + description: SITE_DESCRIPTION, + images: [OG_IMAGE.url], + }, }; // HeaderUpcomingEvent filters by UTC date at render; hourly ISR keeps the promo fresh. diff --git a/src/components/sections/mission/mission-cards-client.tsx b/src/components/sections/mission/mission-cards-client.tsx index cd7f66f..acf3dc3 100644 --- a/src/components/sections/mission/mission-cards-client.tsx +++ b/src/components/sections/mission/mission-cards-client.tsx @@ -6,6 +6,7 @@ import { MissionIllustrationFallback, } from "@/components/illustrations/mission-illustration"; import { DESKTOP_MQL, useIsDesktop } from "@/hooks/use-is-desktop"; +import { useInView } from "@/hooks/use-in-view"; import { useReducedMotion } from "@/hooks/use-reduced-motion"; import { gsap, ScrollTrigger, useGSAP } from "@/lib/gsap-setup"; import { cn } from "@/lib/utils"; @@ -140,6 +141,7 @@ export function MissionDesktopStack({ const [activeIndex, setActiveIndex] = useState(0); const [entranceComplete, setEntranceComplete] = useState(false); const reducedMotion = useReducedMotion(); + const { ref: inViewRef, inView } = useInView(); const markEntranceComplete = useCallback(() => { entranceCompleteRef.current = true; @@ -265,7 +267,10 @@ export function MissionDesktopStack({ return (
-
+
) : ( @@ -333,6 +338,7 @@ export function MissionMobileCards({ cards }: MissionCardsProps) { const carouselRef = useRef(null); const scrollRafRef = useRef(null); const isDesktop = useIsDesktop(); + const { ref: inViewRef, inView } = useInView(); const setCarouselRef = useCallback((node: HTMLDivElement | null) => { if (node === null && scrollRafRef.current !== null) { @@ -368,7 +374,7 @@ export function MissionMobileCards({ cards }: MissionCardsProps) { }, []); return ( -
+
))} diff --git a/src/components/webgl/globe/globe-canvas.tsx b/src/components/webgl/globe/globe-canvas.tsx index 69a4d1b..7541c31 100644 --- a/src/components/webgl/globe/globe-canvas.tsx +++ b/src/components/webgl/globe/globe-canvas.tsx @@ -83,16 +83,19 @@ export function GlobeCanvas({ reducedMotion, rotationY, onReady, + active = true, }: { reducedMotion: boolean; rotationY: number; onReady?: () => void; + /** When false (hero off-screen), the frameloop idles to save CPU/GPU. */ + active?: boolean; }) { return ( setReady(true)} + active={active} /> ); @@ -150,12 +158,14 @@ export const HeroWebglBackground = memo(function HeroWebglBackground() { const reducedMotion = useReducedMotion(); const isLargeScreen = useIsLargeScreen(); const webgl = useWebGLSupport(); + const { ref: inViewRef, inView } = useInView(); const showLiveGlobe = isLargeScreen && webgl; const showStaticGlobe = !showLiveGlobe; return (