mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
166 lines
6.7 KiB
Plaintext
166 lines
6.7 KiB
Plaintext
---
|
|
/**
|
|
* BaseLayout.astro
|
|
* ---------------------------------------------------------------------------
|
|
* The shared shell for every page: <html>, <head> (fonts, meta, theme-init
|
|
* inline script), the fixed NavBar, the main content slot, and the Footer.
|
|
*
|
|
* Theming axis #1 (light/dark) is set on <html data-theme> by the inline
|
|
* theme-init script before paint (no FOUC). Theming axis #2 (section tone) is
|
|
* set on the <body data-surface> via the `surface` prop so each page can opt
|
|
* into neutral / professional / interest styling.
|
|
*
|
|
* Page transitions use Astro's <ClientRouter /> (View Transitions API).
|
|
*/
|
|
import "@/styles/global.css";
|
|
|
|
// Self-hosted fonts (woff2, variable). Avoids external requests for perf + privacy.
|
|
import "@fontsource-variable/inter";
|
|
import "@fontsource-variable/plus-jakarta-sans";
|
|
import "@fontsource-variable/jetbrains-mono";
|
|
|
|
import { ClientRouter } from "astro:transitions";
|
|
import NavBar from "@components/nav/NavBar.astro";
|
|
import StoreNav from "@components/store/StoreNav.astro";
|
|
import Footer from "@components/layout/Footer.astro";
|
|
import { themeInitScript } from "@/scripts/theme-init";
|
|
import { siteIdentity } from "@lib/site";
|
|
// Dev-only react-refresh preamble (see astro.config.mjs for why). Imported as a
|
|
// raw string so we can emit it as the FIRST module script in <head>, ahead of
|
|
// any React island, so $RefreshReg$/$RefreshSig$ exist before islands eval.
|
|
import { REACT_REFRESH_PREAMBLE } from "../../astro.config.mjs";
|
|
|
|
// Only emit the preamble in dev (import.meta.env.DEV). Production builds compile
|
|
// islands to jsx/jsxs and never touch the refresh runtime.
|
|
const isDev = import.meta.env.DEV;
|
|
|
|
type Surface = "neutral" | "professional" | "interest";
|
|
|
|
interface Props {
|
|
title?: string;
|
|
description?: string;
|
|
/** Which section tone to apply. Defaults to neutral (Home/About/Contact). */
|
|
surface?: Surface;
|
|
/** Override default OG image path. */
|
|
ogImage?: string;
|
|
/** When true, emit <meta name="robots" content="noindex, nofollow">. Use on
|
|
pages that shouldn't be indexed (e.g. /contact, to reduce scraping). */
|
|
noindex?: boolean;
|
|
/** When true, render the store-only nav (StoreNav) instead of the main NavBar.
|
|
Used by store pages, which intentionally share no links with the main site. */
|
|
storeNav?: boolean;
|
|
}
|
|
|
|
const {
|
|
title,
|
|
description = "Greg Kilmer is a software developer, maker, and tabletop gamemaster based in Philadelphia. This is where his professional and personal worlds meet.",
|
|
surface = "neutral",
|
|
ogImage,
|
|
noindex = false,
|
|
storeNav = false,
|
|
} = Astro.props;
|
|
|
|
// True on the Home page. Used to switch the NavBar's view-transition-name so
|
|
// the Home hero can morph into / out of the nav bar (expand/collapse effect).
|
|
const isHome = Astro.url.pathname === "/";
|
|
|
|
const fullTitle = title
|
|
? `${title} | ${siteIdentity.name}`
|
|
: `${siteIdentity.name} — Developer. Maker. Storyteller.`;
|
|
|
|
const canonical = new URL(
|
|
Astro.url.pathname,
|
|
Astro.site ?? "https://gregkilmer.com",
|
|
);
|
|
const ogImageUrl = ogImage
|
|
? new URL(ogImage, Astro.site ?? "https://gregkilmer.com").toString()
|
|
: new URL(
|
|
"/images/hero-portrait.jpg",
|
|
Astro.site ?? "https://gregkilmer.com",
|
|
).toString();
|
|
---
|
|
|
|
<!doctype html>
|
|
<!-- no-js guard: removed by reveal-init.ts once JS runs; while present, the
|
|
global.css `.no-js .reveal { opacity:1 }` rule keeps content visible even
|
|
if the reveal script fails. Theme model: data-theme on <html>, data-surface
|
|
on <body>. DARK MODE IS CURRENTLY FORCED — light mode is deferred (the
|
|
site is designed for dark first). theme-init.ts pins data-theme="dark" and
|
|
ignores any stored/OS preference until light mode is reintroduced. -->
|
|
<html lang="en" class="no-js" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<meta name="generator" content={Astro.generator} />
|
|
|
|
{/* Dev-only react-refresh preamble. MUST be the first module script so the
|
|
refresh globals exist before any React island module evaluates. Without
|
|
it, islands throw at eval time and never hydrate (see astro.config.mjs). */}
|
|
{isDev && (
|
|
<script type="module" set:html={REACT_REFRESH_PREAMBLE} />
|
|
)}
|
|
|
|
<title>{fullTitle}</title>
|
|
<meta name="description" content={description} />
|
|
<link rel="canonical" href={canonical} />
|
|
{noindex && <meta name="robots" content="noindex, nofollow" />}
|
|
<meta name="theme-color" content="#0b1026" />
|
|
|
|
<!-- Open Graph / social cards -->
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:title" content={fullTitle} />
|
|
<meta property="og:description" content={description} />
|
|
<meta property="og:url" content={canonical} />
|
|
<meta property="og:site_name" content={siteIdentity.name} />
|
|
{ogImageUrl && <meta property="og:image" content={ogImageUrl} />}
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
|
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
|
|
<!-- Theme boot: blocking inline script prevents flash of the wrong theme.
|
|
Dark mode is currently forced (light mode deferred), so this just pins
|
|
data-theme="dark" before paint. No data-astro-rerun needed since the
|
|
theme never changes across navigations. -->
|
|
<script is:inline set:html={themeInitScript} />
|
|
|
|
<!-- Reveal-on-scroll observer. Pairs with Reveal.astro / [data-reveal].
|
|
Loaded as a module so it self-initializes on load + on View Transitions
|
|
(it listens for astro:page-load internally). -->
|
|
<script>
|
|
import "@/scripts/reveal-init";
|
|
</script>
|
|
|
|
<!-- View Transitions flash-killer. The incoming page is snapshotted with its
|
|
`.reveal` elements still at opacity:0 (the observer hasn't fired yet),
|
|
which makes content vanish during the morph. This toggles a class on
|
|
<html> around the swap so global.css forces reveals visible mid-flight,
|
|
then lets the observer take over for scroll reveals on the new page. -->
|
|
<script is:inline>
|
|
document.addEventListener("astro:before-swap", () => {
|
|
document.documentElement.classList.add("vt-transitioning");
|
|
});
|
|
document.addEventListener("astro:after-swap", () => {
|
|
document.documentElement.classList.remove("vt-transitioning");
|
|
});
|
|
</script>
|
|
|
|
<ClientRouter />
|
|
</head>
|
|
<body data-surface={surface} data-page={isHome ? "home" : "other"}>
|
|
<a
|
|
href="#main"
|
|
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:rounded-lg focus:bg-(--color-amber) focus:px-4 focus:py-2 focus:text-(--color-ink)"
|
|
>
|
|
Skip to content
|
|
</a>
|
|
|
|
{storeNav ? <StoreNav /> : <NavBar isHome={isHome} />}
|
|
|
|
<main id="main">
|
|
<slot />
|
|
</main>
|
|
|
|
<Footer />
|
|
</body>
|
|
</html>
|