1---
2/**
3 * Starlight `Head` override that wraps the default head and appends our
4 * per-page Open Graph image and Twitter card metadata. The image URL is
5 * derived from the Starlight route slug and points at the static
6 * `/og/<slug>.webp` artifact emitted at build time by
7 * `src/pages/og/[...slug].webp.ts`.
8 *
9 * We intentionally append (rather than replace) so all of Starlight's
10 * default `<title>`, description, canonical, RSS, and theme-color tags
11 * still ship.
12 */
13import Default from "@astrojs/starlight/components/Head.astro";
14import FontPreloads from "../FontPreloads.astro";
15import PostHog from "../analytics/PostHog.astro";
16import BrandIcons from "../../brand/Icons.astro";
17import StructuredData from "../StructuredData.astro";
18
19// Starlight injects route data via Astro.locals at runtime; the public
20// types aren't ambient-declared in this project, so we cast through any.
21// Starlight ≥0.39 exposes the slug as `id` (the old `slug` field is gone
22// — reading it yields undefined and produced `/og/undefined.webp`).
23const { id } = (Astro.locals as { starlightRoute: { id: string } })
24 .starlightRoute;
25// Starlight normalizes the index entry's id to an empty string.
26const ogSlug = id === "" ? "index" : id;
27const ogImage = new URL(`/og/${ogSlug}.webp`, Astro.site);
28---
29
30<Default><slot /></Default>
31
32<FontPreloads />
33
34{/* Brand mark. Starlight already emits the SVG from its own `favicon`
35 config, so skip it here and add only the raster/apple-touch candidates. */}
36<BrandIcons svg={false} />
37
38{/* Open Graph image (Starlight's default Head emits og:title/description). */}
39<meta property="og:image" content={ogImage.href} />
40<meta property="og:image:width" content="1200" />
41<meta property="og:image:height" content="630" />
42
43{/* Twitter / X */}
44<meta name="twitter:card" content="summary_large_image" />
45<meta name="twitter:site" content="@alchemy_run" />
46<meta name="twitter:image" content={ogImage.href} />
47
48{/* Organization (sameAs → social profiles) + WebSite JSON-LD. */}
49<StructuredData />
50
51<PostHog />
52
53<script>
54 // Copy the heading link to the clipboard whenever a user clicks one of
55 // Starlight's auto-generated anchor buttons. Navigation still happens
56 // normally — we just piggyback the copy on top of it.
57 document.addEventListener("click", (event) => {
58 const target = event.target as Element | null;
59 const link = target?.closest?.("a.sl-anchor-link") as HTMLAnchorElement | null;
60 if (!link) return;
61 // `link.href` resolves to the absolute URL including the hash.
62 void navigator.clipboard?.writeText(link.href).catch(() => {});
63 });
64
65 // Sidebar entries are CSS-truncated with an ellipsis (see custom.css). Give
66 // each link a `title` so hovering reveals the full name in a native tooltip.
67 // Runs on first paint and after every Starlight client-side navigation.
68 const addSidebarTitles = () => {
69 document
70 .querySelectorAll<HTMLAnchorElement>(".sidebar-content a")
71 .forEach((link) => {
72 const text = link.textContent?.trim();
73 if (text && !link.title) link.title = text;
74 });
75 };
76 if (document.readyState === "loading") {
77 document.addEventListener("DOMContentLoaded", addSidebarTitles);
78 } else {
79 addSidebarTitles();
80 }
81 document.addEventListener("astro:page-load", addSidebarTitles);
82</script>