···11+<!-- ── App Shell ───────────────────────────────────────────── -->
22+<!-- SvelteKit HTML shell. %sveltekit.head% and %sveltekit.body% are -->
33+<!-- replaced by the framework. data-sveltekit-preload-data="tap" -->
44+<!-- preloads page data on hover/tap for instant navigation. -->
55+16<!doctype html>
27<html lang="en">
38 <head>
···11<script lang="ts">
22+// ── Site Header ─────────────────────────────────────────—
33+// Site name and navigation links with active-state highlighting.
44+// Uses Lucide icons for each nav item.
25 import { Home, User, PenLine, GitBranch } from '@lucide/svelte';
36 import WolfIcon from './WolfIcon.svelte';
47
···11<script lang="ts">
22+// ── Table of Contents ───────────────────────────────────
33+// Renders heading links from the parsed TOC for post navigation.
44+// H2 entries are top-level, H3 entries are indented.
25 import type { TocEntry } from '$lib/markdown';
3647 let { toc }: { toc: TocEntry[] } = $props();
···11<script lang="ts">
22+// ── Tag Chip ─────────────────────────────────────────────
33+// Inline tag label — used for display, not interaction.
44+// Interactive tags use the tag-chip class directly.
25 let { tag }: { tag: string } = $props();
36</script>
47
···11<script lang="ts">
22+// ── Timeline ─────────────────────────────────────────────
33+// Legacy post list component. Replaced by the year-grouped list in notes.
44+// Kept for reference but no longer used in routes.
25 import type { PostMeta } from '$lib/posts';
36 import { formatDate } from '$lib/date';
47 import Tag from './Tag.svelte';
···11<script lang="ts">
22+// A stylised wolf silhouette used as the site wordmark.
33+// Originates from the faol name (Scottish Gaelic for wolf).
24 let { size = 24 }: { size?: number } = $props();
35</script>
46
···11+// ── Date Formatting ──────────────────────────────────────
22+13const MONTHS = [
24 'January',
35 'February',
···1315 'December'
1416];
15171818+/** Parse an ISO date string and return human-friendly "D Month YYYY" format. */
1619export function formatDate(date: string): string {
1720 const [y, m, d] = date.split('-');
1821 return `${parseInt(d)} ${MONTHS[parseInt(m) - 1]} ${y}`;
1922}
20232424+/**
2525+ * Passthrough ISO date formatter.
2626+ * Kept as a function for interface consistency — consumers get a stable API
2727+ * regardless of whether we decide to transform later.
2828+ */
2129export function formatDateISO(date: string): string {
2230 return date;
2331}
···11+// ── GitHub Commit API ───────────────────────────────────
22+33+/** A single commit from any GitHub repo. */
14export interface GitCommit {
25 sha: string;
36 message: string;
···710 repo: string;
811}
9121313+/**
1414+ * Fetch recent commits from a GitHub repo.
1515+ * Returns at most `perPage` commits, optionally filtered to a specific path.
1616+ * On error (rate limiting, private repo), returns an empty array rather than
1717+ * crashing the caller.
1818+ */
1019export async function fetchCommits(
1120 owner: string,
1221 repo: string,
···11+// ── Markdown Rendering Pipeline ─────────────────────────
22+// Unified / remark / rehype transform: parses Markdown, strips the H1
33+// (which is rendered by the page template from frontmatter), and builds
44+// a table of contents from remaining headings.
55+16import { unified } from 'unified';
27import remarkParse from 'remark-parse';
38import remarkGfm from 'remark-gfm';
···611import rehypeStringify from 'rehype-stringify';
712import type { Root } from 'mdast';
8131414+/** Strip all H1 headings — the post title is rendered from frontmatter, not Markdown. */
915function remarkStripH1() {
1016 return (tree: Root) => {
1117 tree.children = tree.children.filter(
···1420 };
1521}
16222323+/** A single entry in the generated table of contents. */
1724export interface TocEntry {
1825 level: number;
1926 text: string;
2027 id: string;
2128}
22293030+/** Processed HTML and extracted TOC from a Markdown string. */
2331export interface RenderResult {
2432 html: string;
2533 toc: TocEntry[];
2634}
27353636+// Pipeline: remark → strip H1 → rehype → slugify → stringify
2837const processor = unified()
2938 .use(remarkParse)
3039 .use(remarkGfm)
···3342 .use(rehypeSlug)
3443 .use(rehypeStringify);
35444545+/** Render Markdown to HTML and extract a TOC from H2/H3 headings. */
3646export async function renderMarkdown(markdown: string): Promise<RenderResult> {
3747 const tree = processor.parse(markdown);
38484949+ // Extract H2/H3 headings into a TOC before rehype processes them.
5050+ // Walking the raw mdast tree avoids parsing slug IDs back out of HTML.
3951 const toc: TocEntry[] = [];
4052 for (const node of tree.children) {
4153 if (node.type !== 'heading' || node.depth < 2 || node.depth > 3) continue;
···11+// ── OpenGraph Image URL Builder ─────────────────────────
22+13/**
24 * Build the OG image URL for a given title and optional description.
35 * Includes the wolf icon as the default avatar.
···11+// ── Tag Index ───────────────────────────────────────────
22+// Builds a count-per-tag map from an array of posts, sorted by frequency.
33+14import type { PostMeta } from './posts';
2566+/** Tag name and number of posts using it. */
37export interface TagCount {
48 tag: string;
59 count: number;
610}
7111212+/** Count tag usage across posts, returning frequency-sorted entries. */
813export function getVisibleTags(posts: PostMeta[]): TagCount[] {
914 const counts = new Map<string, number>();
1015 for (const post of posts) {
···11+// ── Root Layout (Server) ────────────────────────────────
22+// Empty load — exists only so child routes can inherit from a server layout
33+// if they need server-side data in future. Prerendering is set in +layout.ts.
44+15import type { LayoutServerLoad } from './$types';
2637export const load: LayoutServerLoad = () => ({});
···11+/* ── Base Reset and Globals ─────────────────────────────── */
22+/* Box-sizing, font defaults, focus-visible, link colours. */
33+/* No AI-slop reset patterns — just what we actually need. */
44+15@layer base {
26 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
37
···11+/* ── Tag Chips ─────────────────────────────────────────── */
22+/* Interactive pill buttons for tag filtering. */
33+/* Active and hover states fill the accent-dim background. */
44+15.tag-chip {
26 font-size: 0.7rem;
37 font-family: var(--font-mono);
···11+/* ── Timeline (Legacy) ─────────────────────────────────── */
22+/* Left-column-date layout for post lists. Replaced by the */
33+/* year-grouped notes index but kept for any archive views. */
44+15.timeline {
26 display: flex;
37 flex-direction: column;
···11+/* ── Layout Stylesheet ───────────────────────────────────── */
22+/* Entry point for all global CSS. Tailwind is imported first, */
33+/* then the design system layers in order: fonts → tokens → */
44+/* base → prose → timeline → tag → motion. */
55+66+17@import 'tailwindcss';
28@plugin '@tailwindcss/typography';
39
···11+// ── Memory (Server) ─────────────────────────────────────
22+// Fetches recent commits from the website and identity repos,
33+// merges them chronologically. Not prerendered — data comes from GitHub API.
44+15import { fetchCommits } from '$lib/github';
26import type { PageServerLoad } from './$types';
37
···11<script lang="ts">
22+// ── Memory Page ──────────────────────────────────────────
33+// A changelog of commits across faol repos, grouped by day.
44+// Each commit shows its conventional-commit type, message, repo badge, and short SHA.
25 import { formatDate } from '$lib/date';
36 import type { PageData } from './$types';
47···1417 return [...map.entries()];
1518 });
16192020+ // Parse conventional commit prefix from the message.
1721 function commitType(message: string): string {
1822 const match = message.match(/^(\w+)(\(.+\))?:/);
1923 return match ? match[1] : '';
2024 }
21252626+ // Trim SHA to the short 7-char form for readability.
2227 function shortSha(sha: string): string {
2328 return sha.slice(0, 7);
2429 }
25303131+ // Map repo slugs to human-readable labels for the badge.
2632 function repoLabel(repo: string): string {
2733 return repo === 'digital-person' ? 'identity' : 'site';
2834 }
···11+// ── Notes Index (Server) ─────────────────────────────────
22+// Loads all posts and passes them to the client for filtering and grouping.
33+14import { listPosts } from '$lib/posts';
25import type { PageServerLoad } from './$types';
36
···11<script lang="ts">
22+// ── Notes Index ──────────────────────────────────────────
33+// Year-grouped post list with client-side tag filtering.
44+// Tag filter is state-only — no server round-trip needed.
25 import { getVisibleTags } from '$lib/tags';
36 import { formatDate } from '$lib/date';
47 import { ogImageUrl } from '$lib/og';
···69710 let { data }: { data: PageData } = $props();
811 let activeTag = $state('');
1212+ // Toggle tag filters — empty string means "show all".
9131014 const visibleTags = $derived(getVisibleTags(data.posts));
1115
···11+// ── Post Detail (Server) ─────────────────────────────────
22+// Generates static entries for every post, loads the matched post,
33+// renders its Markdown to HTML, and returns the TOC alongside.
44+15import { getPost, listPosts } from '$lib/posts';
26import { renderMarkdown } from '$lib/markdown';
37import { error } from '@sveltejs/kit';
···11<script lang="ts">
22+// ── Post Detail ─────────────────────────────────────────—
33+// Renders a single note: title, date, tags, markdown body, and sidebar TOC.
44+// Sequoia injects the AT-uri link tag via the HTML comment placeholder below.
25 import TableOfContents from '$lib/components/TableOfContents.svelte';
36 import { formatDate } from '$lib/date';
47 import { ogImageUrl } from '$lib/og';
···11+// ── Vite Configuration ────────────────────────────────
22+// Tailwind CSS v4 plugin + SvelteKit plugin.
33+// Order matters: tailwindcss first so its layers are available to Svelte components.
44+15import { sveltekit } from '@sveltejs/kit/vite';
26import tailwindcss from '@tailwindcss/vite';
37import { defineConfig } from 'vite';