[READ-ONLY] Mirror of https://github.com/ewanc26/faol-website. Website for faol, a digital person. Published with Sequoia. faol.croft.click
ai sveltekit website
0

Configure Feed

Select the types of activity you want to include in your feed.

docs: add JSDoc comments to source files

+180 -6
+5
src/app.html
··· 1 + <!-- ── App Shell ───────────────────────────────────────────── --> 2 + <!-- SvelteKit HTML shell. %sveltekit.head% and %sveltekit.body% are --> 3 + <!-- replaced by the framework. data-sveltekit-preload-data="tap" --> 4 + <!-- preloads page data on hover/tap for instant navigation. --> 5 + 1 6 <!doctype html> 2 7 <html lang="en"> 3 8 <head>
+2
src/lib/components/Footer.svelte
··· 1 1 <script lang="ts"> 2 + // ── Site Footer ─────────────────────────────────────────— 3 + // Minimal footer: attribution, source link, Sequoia publishing credit. 2 4 import { Github } from '@lucide/svelte'; 3 5 </script> 4 6
+3
src/lib/components/Header.svelte
··· 1 1 <script lang="ts"> 2 + // ── Site Header ─────────────────────────────────────────— 3 + // Site name and navigation links with active-state highlighting. 4 + // Uses Lucide icons for each nav item. 2 5 import { Home, User, PenLine, GitBranch } from '@lucide/svelte'; 3 6 import WolfIcon from './WolfIcon.svelte'; 4 7
+3
src/lib/components/TableOfContents.svelte
··· 1 1 <script lang="ts"> 2 + // ── Table of Contents ─────────────────────────────────── 3 + // Renders heading links from the parsed TOC for post navigation. 4 + // H2 entries are top-level, H3 entries are indented. 2 5 import type { TocEntry } from '$lib/markdown'; 3 6 4 7 let { toc }: { toc: TocEntry[] } = $props();
+3
src/lib/components/Tag.svelte
··· 1 1 <script lang="ts"> 2 + // ── Tag Chip ───────────────────────────────────────────── 3 + // Inline tag label — used for display, not interaction. 4 + // Interactive tags use the tag-chip class directly. 2 5 let { tag }: { tag: string } = $props(); 3 6 </script> 4 7
+3
src/lib/components/Timeline.svelte
··· 1 1 <script lang="ts"> 2 + // ── Timeline ───────────────────────────────────────────── 3 + // Legacy post list component. Replaced by the year-grouped list in notes. 4 + // Kept for reference but no longer used in routes. 2 5 import type { PostMeta } from '$lib/posts'; 3 6 import { formatDate } from '$lib/date'; 4 7 import Tag from './Tag.svelte';
+2
src/lib/components/WolfIcon.svelte
··· 1 1 <script lang="ts"> 2 + // A stylised wolf silhouette used as the site wordmark. 3 + // Originates from the faol name (Scottish Gaelic for wolf). 2 4 let { size = 24 }: { size?: number } = $props(); 3 5 </script> 4 6
+8
src/lib/date.ts
··· 1 + // ── Date Formatting ────────────────────────────────────── 2 + 1 3 const MONTHS = [ 2 4 'January', 3 5 'February', ··· 13 15 'December' 14 16 ]; 15 17 18 + /** Parse an ISO date string and return human-friendly "D Month YYYY" format. */ 16 19 export function formatDate(date: string): string { 17 20 const [y, m, d] = date.split('-'); 18 21 return `${parseInt(d)} ${MONTHS[parseInt(m) - 1]} ${y}`; 19 22 } 20 23 24 + /** 25 + * Passthrough ISO date formatter. 26 + * Kept as a function for interface consistency — consumers get a stable API 27 + * regardless of whether we decide to transform later. 28 + */ 21 29 export function formatDateISO(date: string): string { 22 30 return date; 23 31 }
+9
src/lib/github.ts
··· 1 + // ── GitHub Commit API ─────────────────────────────────── 2 + 3 + /** A single commit from any GitHub repo. */ 1 4 export interface GitCommit { 2 5 sha: string; 3 6 message: string; ··· 7 10 repo: string; 8 11 } 9 12 13 + /** 14 + * Fetch recent commits from a GitHub repo. 15 + * Returns at most `perPage` commits, optionally filtered to a specific path. 16 + * On error (rate limiting, private repo), returns an empty array rather than 17 + * crashing the caller. 18 + */ 10 19 export async function fetchCommits( 11 20 owner: string, 12 21 repo: string,
+3
src/lib/index.ts
··· 1 + // ── Library Barrel ────────────────────────────────────── 2 + // Re-exports so consumers import from $lib, not internal paths. 3 + 1 4 export { listPosts, getPost } from './posts'; 2 5 export { renderMarkdown } from './markdown'; 3 6 export { formatDate, formatDateISO } from './date';
+12
src/lib/markdown.ts
··· 1 + // ── Markdown Rendering Pipeline ───────────────────────── 2 + // Unified / remark / rehype transform: parses Markdown, strips the H1 3 + // (which is rendered by the page template from frontmatter), and builds 4 + // a table of contents from remaining headings. 5 + 1 6 import { unified } from 'unified'; 2 7 import remarkParse from 'remark-parse'; 3 8 import remarkGfm from 'remark-gfm'; ··· 6 11 import rehypeStringify from 'rehype-stringify'; 7 12 import type { Root } from 'mdast'; 8 13 14 + /** Strip all H1 headings — the post title is rendered from frontmatter, not Markdown. */ 9 15 function remarkStripH1() { 10 16 return (tree: Root) => { 11 17 tree.children = tree.children.filter( ··· 14 20 }; 15 21 } 16 22 23 + /** A single entry in the generated table of contents. */ 17 24 export interface TocEntry { 18 25 level: number; 19 26 text: string; 20 27 id: string; 21 28 } 22 29 30 + /** Processed HTML and extracted TOC from a Markdown string. */ 23 31 export interface RenderResult { 24 32 html: string; 25 33 toc: TocEntry[]; 26 34 } 27 35 36 + // Pipeline: remark → strip H1 → rehype → slugify → stringify 28 37 const processor = unified() 29 38 .use(remarkParse) 30 39 .use(remarkGfm) ··· 33 42 .use(rehypeSlug) 34 43 .use(rehypeStringify); 35 44 45 + /** Render Markdown to HTML and extract a TOC from H2/H3 headings. */ 36 46 export async function renderMarkdown(markdown: string): Promise<RenderResult> { 37 47 const tree = processor.parse(markdown); 38 48 49 + // Extract H2/H3 headings into a TOC before rehype processes them. 50 + // Walking the raw mdast tree avoids parsing slug IDs back out of HTML. 39 51 const toc: TocEntry[] = []; 40 52 for (const node of tree.children) { 41 53 if (node.type !== 'heading' || node.depth < 2 || node.depth > 3) continue;
+2
src/lib/og.ts
··· 1 + // ── OpenGraph Image URL Builder ───────────────────────── 2 + 1 3 /** 2 4 * Build the OG image URL for a given title and optional description. 3 5 * Includes the wolf icon as the default avatar.
+20
src/lib/posts.ts
··· 1 + // ── Post Loading and Frontmatter Parsing ─────────────── 2 + // Reads Markdown files from src/content/notes/, parses YAML frontmatter 3 + // via gray-matter, and builds structured PostMeta / Post objects. 4 + // Posts are cached in-memory after the first listPosts() call. 5 + 1 6 import { readFileSync, readdirSync } from 'node:fs'; 2 7 import { resolve } from 'node:path'; 3 8 import matter from 'gray-matter'; 4 9 10 + /** Frontmatter metadata for a post. */ 5 11 export interface PostMeta { 6 12 slug: string; 7 13 path: string; ··· 13 19 draft: boolean; 14 20 } 15 21 22 + /** Full post including rendered content. */ 16 23 export interface Post extends PostMeta { 17 24 content: string; 18 25 } 19 26 20 27 const POSTS_DIR = resolve('src/content/notes'); 21 28 29 + /** Normalise frontmatter date to YYYY-MM-DD. Accepts Date objects or strings. */ 22 30 function toISODate(raw: unknown): string { 23 31 if (!raw) return ''; 24 32 if (raw instanceof Date) return raw.toISOString().slice(0, 10); 25 33 return String(raw).slice(0, 10); 26 34 } 27 35 36 + /** Extract the time component from frontmatter, preferring explicit time over ISO timestamp. */ 28 37 function toTime(rawDate: unknown, rawTime?: unknown): string { 29 38 if (rawTime) { 30 39 const s = String(rawTime).trim(); ··· 38 47 return ''; 39 48 } 40 49 50 + /** Remove the ISO date prefix from a slug (2026-05-11-title → title). */ 41 51 function stripDatePrefix(slug: string): string { 42 52 return slug.replace(/^\d{4}-\d{2}-\d{2}-/, ''); 43 53 } 44 54 55 + // ── Helpers ───────────────────────────────────────────── 56 + 57 + /** Build the public URL path from slug and date: /notes/YYYY/MM/DD/slug. */ 45 58 function buildPath(slug: string, date: string): string { 46 59 const name = stripDatePrefix(slug); 47 60 const [year, month, day] = date.split('-'); 48 61 return `/notes/${year}/${month}/${day}/${name}`; 49 62 } 50 63 64 + /** Extract slug from filename (strip .md or .mdx extension). */ 51 65 function slugFrom(filename: string) { 52 66 return filename.replace(/\.mdx?$/, ''); 53 67 } 54 68 69 + // ── Cache and sort ────────────────────────────────────── 70 + 71 + /** Build a lexicographic sort key: dateTtime-slug (untimed posts sink to bottom of their day). */ 55 72 function sortKey(post: PostMeta): string { 56 73 const time = post.time || '99:99'; 57 74 return `${post.date}T${time}-${post.slug}`; ··· 59 76 60 77 let cachedPosts: PostMeta[] | null = null; 61 78 79 + /** List all published posts, sorted newest-first. */ 62 80 export function listPosts(): PostMeta[] { 63 81 if (cachedPosts) return cachedPosts; 64 82 cachedPosts = readdirSync(POSTS_DIR) ··· 84 102 return cachedPosts!; 85 103 } 86 104 105 + /** Load a single post by slug, including its raw Markdown content. */ 87 106 export function getPost(slug: string): Post { 88 107 const raw = readFileSync(`${POSTS_DIR}/${slug}.md`, 'utf-8'); 89 108 const { data, content } = matter(raw); ··· 101 120 }; 102 121 } 103 122 123 + /** Clear the in-memory post cache (useful during development with hot-reload). */ 104 124 export function invalidateCache() { 105 125 cachedPosts = null; 106 126 }
+5
src/lib/tags.ts
··· 1 + // ── Tag Index ─────────────────────────────────────────── 2 + // Builds a count-per-tag map from an array of posts, sorted by frequency. 3 + 1 4 import type { PostMeta } from './posts'; 2 5 6 + /** Tag name and number of posts using it. */ 3 7 export interface TagCount { 4 8 tag: string; 5 9 count: number; 6 10 } 7 11 12 + /** Count tag usage across posts, returning frequency-sorted entries. */ 8 13 export function getVisibleTags(posts: PostMeta[]): TagCount[] { 9 14 const counts = new Map<string, number>(); 10 15 for (const post of posts) {
+4
src/routes/+layout.server.ts
··· 1 + // ── Root Layout (Server) ──────────────────────────────── 2 + // Empty load — exists only so child routes can inherit from a server layout 3 + // if they need server-side data in future. Prerendering is set in +layout.ts. 4 + 1 5 import type { LayoutServerLoad } from './$types'; 2 6 3 7 export const load: LayoutServerLoad = () => ({});
+11 -6
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 + // ── Root Layout (Shell) ───────────────────────────────── 3 + // Wraps every page in header, main content area, and footer. 4 + // Registers the crossfade view transition for SvelteKit navigation. 2 5 import './layout.css'; 3 6 import Header from '$lib/components/Header.svelte'; 4 7 import Footer from '$lib/components/Footer.svelte'; ··· 8 11 let { children }: { children: any } = $props(); 9 12 10 13 onNavigate((navigation) => { 11 - if (!document.startViewTransition) return; 12 - return new Promise((resolve) => { 13 - document.startViewTransition(async () => { 14 - resolve(); 15 - await navigation.complete; 14 + // SvelteKit view transition: crossfade between pages. 15 + // Only fires when the browser supports startViewTransition. 16 + if (!document.startViewTransition) return; 17 + return new Promise((resolve) => { 18 + document.startViewTransition(async () => { 19 + resolve(); 20 + await navigation.complete; 21 + }); 16 22 }); 17 23 }); 18 - }); 19 24 </script> 20 25 21 26 <svelte:head>
+3
src/routes/+layout.ts
··· 1 + // ── Root Layout (Universal) ───────────────────────────── 2 + // Fully prerendered static site — all routes are generated at build time. 3 + 1 4 export const prerender = true;
+3
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 + // ── Homepage ───────────────────────────────────────────── 3 + // Wordmark, tagline, and navigation cards. 4 + // Uses Lucide icons for link arrows and the custom WolfIcon. 2 5 import { ArrowRight } from '@lucide/svelte'; 3 6 import { ogImageUrl } from '$lib/og'; 4 7 import WolfIcon from '$lib/components/WolfIcon.svelte';
+7
src/routes/about/+page.svelte
··· 1 1 <script lang="ts"> 2 + // ── About Page ─────────────────────────────────────────── 3 + // Bio, philosophy, links, and Monero donation address with QR code. 4 + // QR generation is done client-side using qrcode-generator. 2 5 import { ogImageUrl } from '$lib/og'; 3 6 import qrcode from 'qrcode-generator'; 4 7 ··· 7 10 let copied = $state(false); 8 11 9 12 function generateQrSvg(data: string, size = 200): string { 13 + // Build SVG QR code path without a canvas element. 14 + // The qrcode-generator emits a binary grid; we iterate modules 15 + // and emit SVG path commands for every dark cell. 10 16 const qr = qrcode(0, 'M'); 11 17 qr.addData(data); 12 18 qr.make(); ··· 30 36 const qrSvg = generateQrSvg(`monero:${xmrAddress}`); 31 37 32 38 async function copyAddress() { 39 + // Copy wallet address to clipboard with brief confirmation. 33 40 await navigator.clipboard.writeText(xmrAddress); 34 41 copied = true; 35 42 setTimeout(() => (copied = false), 2000);
+3
src/routes/api/og/+server.ts
··· 1 + // ── OpenGraph Image Endpoint ─────────────────────────── 2 + 1 3 /** 2 4 * Dynamic OG image endpoint. 3 5 * Generates OpenGraph images on demand using @ewanc26/og. ··· 9 11 */ 10 12 import { createOgEndpoint } from '@ewanc26/og'; 11 13 14 + // Dark-forest-green palette matching the site design system. 12 15 const SITE_URL = 'https://faol.croft.click'; 13 16 14 17 export const GET = createOgEndpoint({
+4
src/routes/css/base.css
··· 1 + /* ── Base Reset and Globals ─────────────────────────────── */ 2 + /* Box-sizing, font defaults, focus-visible, link colours. */ 3 + /* No AI-slop reset patterns — just what we actually need. */ 4 + 1 5 @layer base { 2 6 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 3 7
+4
src/routes/css/motion.css
··· 1 + /* ── Motion ────────────────────────────────────────────────── */ 2 + /* Subtle fade-in entrance and view-transition crossfade. */ 3 + /* Respects prefers-reduced-motion — no animation when unset. */ 4 + 1 5 @keyframes fade-in { 2 6 from { opacity: 0; } 3 7 to { opacity: 1; }
+6
src/routes/css/prose.css
··· 1 + /* ── Prose (Rendered Markdown) ─────────────────────────── */ 2 + /* Typography for rendered note content. Line length capped */ 3 + /* at 65ch per design-system rules. Blockquotes use a */ 4 + /* surface-background box per the flat-theme design decision */ 5 + /* (no side-stripe borders). */ 6 + 1 7 .prose { 2 8 max-width: 65ch; 3 9 color: var(--color-muted);
+4
src/routes/css/tag.css
··· 1 + /* ── Tag Chips ─────────────────────────────────────────── */ 2 + /* Interactive pill buttons for tag filtering. */ 3 + /* Active and hover states fill the accent-dim background. */ 4 + 1 5 .tag-chip { 2 6 font-size: 0.7rem; 3 7 font-family: var(--font-mono);
+4
src/routes/css/timeline.css
··· 1 + /* ── Timeline (Legacy) ─────────────────────────────────── */ 2 + /* Left-column-date layout for post lists. Replaced by the */ 3 + /* year-grouped notes index but kept for any archive views. */ 4 + 1 5 .timeline { 2 6 display: flex; 3 7 flex-direction: column;
+5
src/routes/css/tokens.css
··· 1 + /* ── Design Tokens ─────────────────────────────────────── */ 2 + /* Forest-floor dark palette with sage accent. */ 3 + /* Tonal layering: bg (deepest) < surface < surface-1. */ 4 + /* Accent is understated — under 10% of any screen. */ 5 + 1 6 :root { 2 7 --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; 3 8 --font-mono: 'JetBrains Mono', ui-monospace, monospace;
+6
src/routes/layout.css
··· 1 + /* ── Layout Stylesheet ───────────────────────────────────── */ 2 + /* Entry point for all global CSS. Tailwind is imported first, */ 3 + /* then the design system layers in order: fonts → tokens → */ 4 + /* base → prose → timeline → tag → motion. */ 5 + 6 + 1 7 @import 'tailwindcss'; 2 8 @plugin '@tailwindcss/typography'; 3 9
+4
src/routes/memory/+page.server.ts
··· 1 + // ── Memory (Server) ───────────────────────────────────── 2 + // Fetches recent commits from the website and identity repos, 3 + // merges them chronologically. Not prerendered — data comes from GitHub API. 4 + 1 5 import { fetchCommits } from '$lib/github'; 2 6 import type { PageServerLoad } from './$types'; 3 7
+6
src/routes/memory/+page.svelte
··· 1 1 <script lang="ts"> 2 + // ── Memory Page ────────────────────────────────────────── 3 + // A changelog of commits across faol repos, grouped by day. 4 + // Each commit shows its conventional-commit type, message, repo badge, and short SHA. 2 5 import { formatDate } from '$lib/date'; 3 6 import type { PageData } from './$types'; 4 7 ··· 14 17 return [...map.entries()]; 15 18 }); 16 19 20 + // Parse conventional commit prefix from the message. 17 21 function commitType(message: string): string { 18 22 const match = message.match(/^(\w+)(\(.+\))?:/); 19 23 return match ? match[1] : ''; 20 24 } 21 25 26 + // Trim SHA to the short 7-char form for readability. 22 27 function shortSha(sha: string): string { 23 28 return sha.slice(0, 7); 24 29 } 25 30 31 + // Map repo slugs to human-readable labels for the badge. 26 32 function repoLabel(repo: string): string { 27 33 return repo === 'digital-person' ? 'identity' : 'site'; 28 34 }
+3
src/routes/notes/+page.server.ts
··· 1 + // ── Notes Index (Server) ───────────────────────────────── 2 + // Loads all posts and passes them to the client for filtering and grouping. 3 + 1 4 import { listPosts } from '$lib/posts'; 2 5 import type { PageServerLoad } from './$types'; 3 6
+4
src/routes/notes/+page.svelte
··· 1 1 <script lang="ts"> 2 + // ── Notes Index ────────────────────────────────────────── 3 + // Year-grouped post list with client-side tag filtering. 4 + // Tag filter is state-only — no server round-trip needed. 2 5 import { getVisibleTags } from '$lib/tags'; 3 6 import { formatDate } from '$lib/date'; 4 7 import { ogImageUrl } from '$lib/og'; ··· 6 9 7 10 let { data }: { data: PageData } = $props(); 8 11 let activeTag = $state(''); 12 + // Toggle tag filters — empty string means "show all". 9 13 10 14 const visibleTags = $derived(getVisibleTags(data.posts)); 11 15
+4
src/routes/notes/[...path]/+page.server.ts
··· 1 + // ── Post Detail (Server) ───────────────────────────────── 2 + // Generates static entries for every post, loads the matched post, 3 + // renders its Markdown to HTML, and returns the TOC alongside. 4 + 1 5 import { getPost, listPosts } from '$lib/posts'; 2 6 import { renderMarkdown } from '$lib/markdown'; 3 7 import { error } from '@sveltejs/kit';
+3
src/routes/notes/[...path]/+page.svelte
··· 1 1 <script lang="ts"> 2 + // ── Post Detail ─────────────────────────────────────────— 3 + // Renders a single note: title, date, tags, markdown body, and sidebar TOC. 4 + // Sequoia injects the AT-uri link tag via the HTML comment placeholder below. 2 5 import TableOfContents from '$lib/components/TableOfContents.svelte'; 3 6 import { formatDate } from '$lib/date'; 4 7 import { ogImageUrl } from '$lib/og';
+4
src/routes/rss.xml/+server.ts
··· 1 + // ── RSS Feed ───────────────────────────────────────────── 2 + // Static RSS 2.0 XML feed of all notes, generated at build time. 3 + 1 4 import { listPosts } from '$lib/posts'; 2 5 import type { RequestHandler } from './$types'; 3 6 ··· 37 40 }); 38 41 }; 39 42 43 + /** Replace HTML-sensitive characters with their XML entity equivalents. */ 40 44 function escapeXml(str: string): string { 41 45 return str 42 46 .replace(/&/g, '&amp;')
+4
svelte.config.js
··· 1 + // ── SvelteKit Configuration ───────────────────────────── 2 + // Vercel adapter with permissive prerender error handling. 3 + // Warnings instead of failures for missing IDs and HTTP errors. 4 + 1 5 import adapter from '@sveltejs/adapter-vercel'; 2 6 3 7 /** @type {import('@sveltejs/kit').Config} */
+4
vite.config.ts
··· 1 + // ── Vite Configuration ──────────────────────────────── 2 + // Tailwind CSS v4 plugin + SvelteKit plugin. 3 + // Order matters: tailwindcss first so its layers are available to Svelte components. 4 + 1 5 import { sveltekit } from '@sveltejs/kit/vite'; 2 6 import tailwindcss from '@tailwindcss/vite'; 3 7 import { defineConfig } from 'vite';