Files
com.gregkilmer.www/src/layouts/BaseLayout.astro
T
2026-07-04 11:28:13 -04:00

132 lines
5.3 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 Footer from '@components/layout/Footer.astro';
import { themeInitScript } from '@/scripts/theme-init';
import { siteIdentity } from '@lib/site';
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;
}
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,
} = 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://patiun.com');
const ogImageUrl = ogImage
? new URL(ogImage, Astro.site ?? 'https://patiun.com').toString()
: new URL('/images/hero-portrait.jpg', Astro.site ?? 'https://patiun.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} />
<title>{fullTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<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/svg+xml" href="/favicon.svg" />
<!-- 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>
<NavBar isHome={isHome} />
<main id="main">
<slot />
</main>
<Footer />
</body>
</html>