open-design/apps/landing-page/app/_components/seo-head.astro
lefarcen 5f7d65d513
perf(landing): preconnect api.github.com + rAF-throttle scroll listener (#2666)
Two PSI-targeted wins (split from #2599 follow-up).

1. New `resource-hints.astro` mounted in every page's <head> declares
   `<link rel="preconnect" href="https://api.github.com" crossorigin>`.
   The inline enhancer script on /` issues 3 fetch() calls to
   api.github.com right after DOMContentLoaded (stars, latest release,
   contributors). Without preconnect each pays a full DNS + TCP + TLS
   handshake (~150-300ms) inline with the fetch. With preconnect those
   handshakes happen in parallel with HTML parse and all three share one
   warmed HTTP/2 connection.

2. Wrap the scroll listener's read + classList write in
   requestAnimationFrame. Trackpads and high-rate wheels fire scroll
   faster than display refresh, and every callback that hits classList
   triggers layout recalc. PSI was attributing ~700ms of "forced reflow"
   to the un-throttled version. The rAF gate collapses each burst to one
   DOM mutation per frame; `{ passive: true }` is preserved so the
   listener still doesn't block the scroll thread.

   Same throttling pattern mirrored to `header-enhancer.astro` (used by
   every sub-page) and `home-enhancer.astro` (kept in lockstep even
   though /` currently uses its own inline copy).

Expected PSI delta:
  - "Preconnect to required origins" hint: cleared
  - "Forced reflow" diagnostic 700ms → near zero
  - LCP: small bonus from earlier GH fetch warm-up (~100-300ms)
2026-05-22 14:06:39 +08:00

198 lines
6.8 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
/*
* SeoHead — single source of truth for per-page metadata.
*
* Usage in any Astro page's <head>:
*
* <SeoHead kind="website" title="..." description="..." pathname={Astro.url.pathname} />
* <SeoHead kind="article" title={post.data.title} description={post.data.summary}
* pathname={Astro.url.pathname} datePublished={post.data.date}
* category={post.data.category} />
*
* The component does NOT render <meta charset> or <meta viewport> — those
* stay in the page so they appear before any module imports. SeoHead handles
* everything else: <title>, description, canonical, OpenGraph, Twitter,
* theme-color, and (for articles) Article JSON-LD.
*/
import { ogDefaultImage } from '../image-assets';
import {
LANDING_LOCALES,
alternateLinksForPath,
getLocaleDefinition,
localeFromPath,
stripLocaleFromPath,
} from '../i18n';
import FaviconLinks from './favicon-links.astro';
import ResourceHints from './resource-hints.astro';
export interface SeoHeadProps {
/** 'website' for landing/list pages, 'article' for blog posts. */
kind: 'website' | 'article';
/** For article: raw post title (component appends " — Open Design"). For website: the full title string. */
title: string;
/** Meta description, 150160 chars recommended. */
description: string;
/** Pass `Astro.url.pathname`. */
pathname: string;
/** Optional override for the OG/Twitter card image (absolute URL). */
image?: string;
/** Required for kind: 'article'. */
datePublished?: Date;
dateModified?: Date;
/** Required for kind: 'article'. e.g. "Guides". */
category?: string;
/** Defaults to "Open Design". */
author?: string;
/**
* Optional Google Search Console verification token. When set, renders
* `<meta name="google-site-verification" content="...">`. Only needed on
* one page (typically `/`) for URL-prefix property verification; the
* Domain property type uses DNS TXT instead and doesn't need this.
*/
googleSiteVerification?: string;
}
const props = Astro.props as SeoHeadProps;
const SITE_NAME = 'Open Design';
const TAGLINE = 'Design with the agent already on your laptop.';
const isArticle = props.kind === 'article';
const locale = localeFromPath(props.pathname);
const localeDef = getLocaleDefinition(locale);
const basePath = stripLocaleFromPath(props.pathname).pathname;
const alternateLinks = alternateLinksForPath(props.pathname).map((entry) => ({
...entry,
href: new URL(entry.hrefPath, Astro.site).toString(),
}));
const xDefaultHref = new URL(alternateLinks[0]!.hrefPath, Astro.site).toString();
const canonical = new URL(props.pathname, Astro.site).toString();
const image = props.image ?? new URL(ogDefaultImage, Astro.site).toString();
const rssUrl = new URL('/blog/rss.xml', Astro.site).toString();
const fullTitle = isArticle ? `${props.title} — ${SITE_NAME}` : props.title;
const ogTitle = props.title;
const author = props.author ?? SITE_NAME;
const isoPublished =
isArticle && props.datePublished ? props.datePublished.toISOString() : null;
const isoModified =
isArticle
? (props.dateModified ?? props.datePublished)?.toISOString() ?? null
: null;
const articleJsonLd =
isArticle && isoPublished
? {
'@context': 'https://schema.org',
'@type': 'Article',
headline: props.title,
description: props.description,
inLanguage: localeDef.htmlLang,
image: [image],
datePublished: isoPublished,
dateModified: isoModified,
articleSection: props.category,
author: {
'@type': 'Organization',
name: author,
url: Astro.site?.toString() ?? 'https://open-design.ai/',
},
publisher: {
'@type': 'Organization',
name: SITE_NAME,
url: Astro.site?.toString() ?? 'https://open-design.ai/',
logo: {
'@type': 'ImageObject',
url: image,
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': canonical,
},
}
: null;
const websiteJsonLd =
props.kind === 'website' && basePath === '/'
? {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: SITE_NAME,
alternateName: TAGLINE,
url: Astro.site?.toString() ?? 'https://open-design.ai/',
inLanguage: localeDef.htmlLang,
availableLanguage: LANDING_LOCALES.map((entry) => entry.htmlLang),
}
: null;
const blogJsonLd =
props.kind === 'website' && basePath === '/blog/'
? {
'@context': 'https://schema.org',
'@type': 'Blog',
name: 'Open Design — Blog',
description: props.description,
url: canonical,
inLanguage: localeDef.htmlLang,
publisher: {
'@type': 'Organization',
name: SITE_NAME,
url: Astro.site?.toString() ?? 'https://open-design.ai/',
},
}
: null;
---
<title>{fullTitle}</title>
<meta name='description' content={props.description} />
<meta name='theme-color' content='#efe7d2' />
<link rel='canonical' href={canonical} />
{alternateLinks.map((entry) => (
<link rel='alternate' hreflang={entry.hreflang} href={entry.href} />
))}
<link rel='alternate' hreflang='x-default' href={xDefaultHref} />
<link rel='alternate' type='application/rss+xml' title='Open Design Blog' href={rssUrl} />
<FaviconLinks />
<ResourceHints />
{props.googleSiteVerification && (
<meta name='google-site-verification' content={props.googleSiteVerification} />
)}
<meta property='og:type' content={props.kind === 'article' ? 'article' : 'website'} />
<meta property='og:site_name' content={SITE_NAME} />
<meta property='og:title' content={ogTitle} />
<meta property='og:description' content={props.description} />
<meta property='og:url' content={canonical} />
<meta property='og:image' content={image} />
<meta property='og:locale' content={localeDef.ogLocale} />
{LANDING_LOCALES.filter((entry) => entry.code !== locale).map((entry) => (
<meta property='og:locale:alternate' content={entry.ogLocale} />
))}
{props.kind === 'article' && (
<>
<meta property='article:published_time' content={isoPublished!} />
<meta property='article:modified_time' content={isoModified!} />
<meta property='article:section' content={props.category} />
<meta property='article:author' content={author} />
</>
)}
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:title' content={ogTitle} />
<meta name='twitter:description' content={props.description} />
<meta name='twitter:image' content={image} />
{articleJsonLd && (
<script is:inline type='application/ld+json' set:html={JSON.stringify(articleJsonLd)} />
)}
{websiteJsonLd && (
<script is:inline type='application/ld+json' set:html={JSON.stringify(websiteJsonLd)} />
)}
{blogJsonLd && (
<script is:inline type='application/ld+json' set:html={JSON.stringify(blogJsonLd)} />
)}