mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
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)
198 lines
6.8 KiB
Text
198 lines
6.8 KiB
Text
---
|
||
/*
|
||
* 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, 150–160 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)} />
|
||
)}
|