1---
2import FontPreloads from "../components/FontPreloads.astro";
3import "../styles/tokens.css";
4import "../styles/marketing.css";
5
6import Nav from "../components/marketing/Nav.astro";
7import Footer from "../components/marketing/Footer.astro";
8import PostHog from "../components/analytics/PostHog.astro";
9import ThemeProvider from "../components/ThemeProvider.astro";
10import BrandIcons from "../brand/Icons.astro";
11import StructuredData from "../components/StructuredData.astro";
12
13interface Props {
14 title?: string;
15 description?: string;
16 /**
17 * Path to the page's OG image, relative to the site root. Defaults to
18 * the build-time generated card for the current page (slug derived from
19 * the URL pathname). The OG endpoint at `/og/[...slug].webp` produces
20 * these during `astro build`.
21 */
22 image?: string;
23 /**
24 * MDX/Markdown layouts receive their YAML frontmatter under this key
25 * rather than as flat props. We accept both shapes so an .astro page
26 * can pass `title=…` directly while an .mdx page's frontmatter just
27 * works.
28 */
29 frontmatter?: { title?: string; description?: string; image?: string };
30}
31
32const props = Astro.props;
33const fm = props.frontmatter ?? {};
34const title = props.title ?? fm.title ?? "Alchemy";
35const description = props.description ?? fm.description;
36const image = props.image ?? fm.image;
37
38// Derive an OG image slug from the URL pathname when not explicitly set:
39// "/" → /og/index.webp
40// "/some/page" → /og/some/page.webp (currently unused, but future-proof)
41const slug = (() => {
42 const p = Astro.url.pathname.replace(/^\/+|\/+$/g, "");
43 return p === "" ? "index" : p;
44})();
45const ogImage = new URL(image ?? `/og/${slug}.webp`, Astro.site);
46const canonical = new URL(Astro.url.pathname, Astro.site);
47---
48
49<!doctype html>
50<html lang="en">
51 <head>
52 <meta charset="utf-8" />
53 <meta name="viewport" content="width=device-width, initial-scale=1" />
54 <FontPreloads />
55 {/* Pre-paint theme script + color-scheme/theme-color metas. Must
56 run before any paint so navigating from a docs page (where the
57 Starlight head includes the same partial) doesn't drop the
58 user's theme on a marketing page. */}
59 <ThemeProvider />
60 <title>{title}</title>
61 {description && <meta name="description" content={description} />}
62 <link rel="canonical" href={canonical.href} />
63
64 <BrandIcons />
65
66 {/* Open Graph */}
67 <meta property="og:type" content="website" />
68 <meta property="og:site_name" content="alchemy" />
69 <meta property="og:title" content={title} />
70 {description && <meta property="og:description" content={description} />}
71 <meta property="og:url" content={canonical.href} />
72 <meta property="og:image" content={ogImage.href} />
73 <meta property="og:image:width" content="1200" />
74 <meta property="og:image:height" content="630" />
75
76 {/* Twitter / X */}
77 <meta name="twitter:card" content="summary_large_image" />
78 <meta name="twitter:site" content="@alchemy_run" />
79 <meta name="twitter:title" content={title} />
80 {description && <meta name="twitter:description" content={description} />}
81 <meta name="twitter:image" content={ogImage.href} />
82
83 <StructuredData />
84
85 <PostHog />
86 </head>
87 <body class="alc-root">
88 <header class="alc-marketing-header">
89 <Nav marketing />
90 </header>
91 <main>
92 <slot />
93 </main>
94 <Footer />
95 </body>
96</html>
97
98<style is:global>
99 html,
100 body {
101 margin: 0;
102 padding: 0;
103 background: var(--alc-bg);
104 color: var(--alc-fg-2);
105 font-family: var(--alc-font-sans);
106 -webkit-font-smoothing: antialiased;
107 overflow-x: clip;
108 }
109 /* In-page links glide, and land clear of the 64px sticky header.
110 tokens.css turns this off for reduced motion. */
111 html {
112 scroll-behavior: smooth;
113 scroll-padding-top: 80px;
114 }
115 a:focus-visible {
116 outline: 2px solid var(--alc-accent);
117 outline-offset: 2px;
118 }
119 .alc-marketing-header {
120 position: sticky;
121 top: 0;
122 z-index: 10;
123 height: 64px;
124 padding: 0 32px;
125 background: color-mix(in srgb, var(--alc-bg-nav) 92%, transparent);
126 backdrop-filter: saturate(140%) blur(8px);
127 border-bottom: 1px solid var(--alc-hairline);
128 }
129 @media (max-width: 640px) {
130 .alc-marketing-header { padding: 0 20px; }
131 }
132 @media (max-width: 480px) {
133 .alc-marketing-header { padding: 0 16px; }
134 }
135</style>