diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
new file mode 100644
index 0000000..c6491f0
--- /dev/null
+++ b/src/layouts/BaseLayout.astro
@@ -0,0 +1,101 @@
+---
+import '../styles/tokens.css';
+import '../styles/fonts.css';
+
+interface Props {
+ title: string;
+ description?: string;
+}
+
+const { title, description } = Astro.props;
+
+const siteTitle = title === 'Crash Test Dev' ? title : `${title} — Crash Test Dev`;
+const base = import.meta.env.BASE_URL;
+---
+
+
+
+
+
+
+ {description && }
+ {siteTitle}
+
+
+
+ Skip to content
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 8c648df..46403cd 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,14 +1,15 @@
---
+import BaseLayout from '../layouts/BaseLayout.astro';
---
-
-
-
-
- Crash Test Dev
-
-
- Crash Test Dev
- Blog by Ken Chau
-
-
\ No newline at end of file
+
+
+ Crash Test Dev
+
+
+ Blog by Ken Chau
+
+
+ Tokens and fonts are working.
+
+
diff --git a/tests/check-base-layout.mjs b/tests/check-base-layout.mjs
new file mode 100644
index 0000000..56729ac
--- /dev/null
+++ b/tests/check-base-layout.mjs
@@ -0,0 +1,138 @@
+/**
+ * Build-output test for BaseLayout.astro
+ * Runs after `npm run build` and checks the dist HTML for expected structure.
+ */
+import { readFileSync, existsSync } from 'fs';
+import { resolve } from 'path';
+
+const distDir = resolve('dist');
+const indexPath = resolve(distDir, 'index.html');
+
+let failures = 0;
+let passes = 0;
+
+function assert(condition, label) {
+ if (condition) {
+ console.log(` ✓ ${label}`);
+ passes++;
+ } else {
+ console.error(` ✗ FAIL: ${label}`);
+ failures++;
+ }
+}
+
+// ── Source file existence checks ──
+console.log('\n--- Source file checks ---');
+
+assert(
+ existsSync(resolve('src/layouts/BaseLayout.astro')),
+ 'src/layouts/BaseLayout.astro exists'
+);
+
+assert(
+ existsSync(resolve('src/pages/index.astro')),
+ 'src/pages/index.astro exists'
+);
+
+// ── Source content checks ──
+console.log('\n--- Source content checks ---');
+
+if (existsSync(resolve('src/layouts/BaseLayout.astro'))) {
+ const layoutSrc = readFileSync(resolve('src/layouts/BaseLayout.astro'), 'utf-8');
+
+ assert(
+ layoutSrc.includes("import '../styles/tokens.css'") || layoutSrc.includes("import '../styles/tokens.css';"),
+ 'BaseLayout imports tokens.css'
+ );
+ assert(
+ layoutSrc.includes("import '../styles/fonts.css'") || layoutSrc.includes("import '../styles/fonts.css';"),
+ 'BaseLayout imports fonts.css'
+ );
+ assert(
+ /title:\s*string/.test(layoutSrc),
+ 'Props interface has title: string'
+ );
+ assert(
+ /description\?:\s*string/.test(layoutSrc),
+ 'Props interface has description?: string'
+ );
+ assert(
+ layoutSrc.includes('skip-link'),
+ 'BaseLayout has skip-link class'
+ );
+ assert(
+ layoutSrc.includes("skip-to-content") || layoutSrc.includes("Skip to content"),
+ 'BaseLayout has skip-to-content link text'
+ );
+ assert(
+ layoutSrc.includes('') || layoutSrc.includes(""),
+ 'BaseLayout has named nav slot'
+ );
+ assert(
+ layoutSrc.includes('id="main-content"') || layoutSrc.includes("id='main-content'"),
+ 'BaseLayout has main with id=main-content'
+ );
+ assert(
+ layoutSrc.includes('Crash Test Dev'),
+ 'BaseLayout references site name for title computation'
+ );
+ assert(
+ layoutSrc.includes('import.meta.env.BASE_URL'),
+ 'BaseLayout reads import.meta.env.BASE_URL'
+ );
+}
+
+if (existsSync(resolve('src/pages/index.astro'))) {
+ const indexSrc = readFileSync(resolve('src/pages/index.astro'), 'utf-8');
+
+ assert(
+ indexSrc.includes('BaseLayout'),
+ 'index.astro uses BaseLayout'
+ );
+}
+
+// ── Build output checks ──
+console.log('\n--- Build output checks ---');
+
+assert(existsSync(indexPath), 'dist/index.html exists');
+
+if (existsSync(indexPath)) {
+ const html = readFileSync(indexPath, 'utf-8');
+
+ assert(html.includes('') || html.includes(''), 'Has doctype');
+ assert(html.includes('lang="en"') || html.includes("lang='en'"), 'html has lang=en');
+ assert(html.includes('charset'), 'Has charset meta');
+ assert(html.includes('viewport'), 'Has viewport meta');
+ // Astro extracts CSS into separate files; check both HTML and linked CSS bundles
+ const { readdirSync } = await import('fs');
+ const astroDir = resolve(distDir, '_astro');
+ let distCss = '';
+ if (existsSync(astroDir)) {
+ for (const f of readdirSync(astroDir)) {
+ if (f.endsWith('.css')) {
+ distCss += readFileSync(resolve(astroDir, f), 'utf-8');
+ }
+ }
+ }
+ assert(
+ html.includes('Source Serif') || distCss.includes('Source Serif'),
+ 'dist output (HTML or CSS) contains Source Serif reference'
+ );
+ assert(html.includes('skip-link') || html.includes('Skip to content'), 'Has skip link');
+ assert(html.includes('main-content'), 'Has main-content id');
+ assert(
+ html.includes('Crash Test Dev'),
+ 'Title contains Crash Test Dev'
+ );
+ assert(
+ html.includes('application/rss+xml') || html.includes('rss'),
+ 'Has RSS link'
+ );
+}
+
+// ── Summary ──
+console.log(`\n--- Results: ${passes} passed, ${failures} failed ---`);
+
+if (failures > 0) {
+ process.exit(1);
+}
\ No newline at end of file