Nav bar & layout consistency
- Base.astro's `wide` layout now defaults on for every page (was opt-in),
fixing the nav bar wrapping differently depending on page width
- Icon buttons for search and theme toggle (magnifying glass, sun/moon),
replacing the plain "Search" text link and the "◐" glyph
- Search is now a Cmd/Ctrl+K modal over Pagefind (lazy-loaded on first
open) instead of a separate /search/ page, matching the old site
- Fixed the article page's TOC sitting with unwanted left padding relative
to the header (grid was center-justified instead of start-justified)
Smooth navigation
- Added Astro's ClientRouter for animated page transitions
- Every page-local script that only ran once at module load (three.js
hero, minimap, comments, book-spine carousel, scroll-to-top, reading
progress, theme toggle, Pagefind init) now re-initializes via
astro:page-load, with AbortController-based cleanup so window/document
listeners don't accumulate across client-side navigations
Restored from the legacy Zola site
- Reading progress bar (article pages) and scroll-to-top button
- Share buttons (X, Bluesky, LinkedIn, Reddit, HN, email) on writings
- Prev/next post navigation with the old label/title layout
- Utterances comments, wired to andwati/andwati.com with dark/light
theme sync
- YouTube embeds: any plain <iframe src="...youtube-nocookie.com...">
is now auto-wrapped in a responsive 16:9 container, gets
loading="lazy", and emits VideoObject JSON-LD — no shortcode needed
- LaTeX: $inline$ and $$block$$ math renders to static HTML via KaTeX at
build time (no client-side JS shipped for it)
- Tags page redesigned as a centered, alphabetical tag cloud
Removed the related-posts section per request.
Accessibility & contrast
- --accent (#d1502f) measured 4.0:1 on the light background, just under
WCAG AA's 4.5:1 for normal text — darkened to #a84025 (5.7:1), plus
matching light-mode-only fixes for the two hljs syntax colors and the
tip/warning callout colors that had the same problem in light mode
- Fixed heading-order violations (h1 straight to h3 with nothing at h2)
across the homepage, writings/portfolio/bookshelf/blogs lists, and tag
pages — introduced a shared .entry-title class for the h2 list titles
- Fixed inconsistent title-link underline colors: a plain <a> carries its
own default underline (in its own text color) instead of matching its
parent heading's accent-colored one — h1/h2/h3 anchors now explicitly
inherit the heading's decoration
- Bookshelf spine colors are generated per-title from a hash; the
lightness-based text-color heuristic could still produce sub-4.5:1
contrast for some hues (e.g. green) — now computes real WCAG contrast
and adjusts
Performance
- The three.js homepage hero was the single largest main-thread cost
site-wide (dragging that page's Lighthouse performance score to the low
80s while every other page scored 95+): three.js is now dynamically
imported only once the browser is idle (WebGL-support check split into
its own tiny module so unsupported browsers never fetch it at all), the
render loop is capped to ~30fps, and hover raycasting only runs on an
actual pointer move instead of every frame
SEO & structured data
- Self-referential canonical links site-wide (with override support for
posts first published elsewhere)
- Sitewide WebSite/Person JSON-LD, plus per-article BlogPosting,
BreadcrumbList, and (when applicable) VideoObject JSON-LD
- robots.txt explicitly welcomes AI/LLM crawlers (GPTBot, ClaudeBot,
PerplexityBot, Google-Extended, CCBot, etc.) and points at llms.txt
- Added an Atom feed (RSS and JSON Feed already existed)
- Sitemap now sets <lastmod> per entry from its own date/updated
frontmatter instead of leaving every URL at build time
Analytics & comments infra
- Self-hosted Umami wiring via PUBLIC_UMAMI_SCRIPT_URL/WEBSITE_ID
(apps/site/.env.example), no-ops until both are set
- sws.toml's CSP updated to allow utteranc.es and analytics.andwati.com —
without this both were silently blocked in production
Lighthouse CI
- @lhci/cli wired up against the built site (mobile + desktop configs),
`pnpm run lighthouse`, and a GitHub Actions workflow to run it on every
push/PR — reporting only for now, no failing thresholds
Replaces the legacy Zola script, which was hard-coded to the retired
"CASE FILE" branding, with scripts/generate-og-images.astro-site.mjs
built on the new design system's dark palette + Fraunces/JetBrains Mono.
Base.astro now renders og:image/twitter:image from a per-page ogImage
prop, wired into the writings and bookshelf detail pages.
Uses static-weight Fraunces/Newsreader font packages rather than the
variable fonts the site ships elsewhere, since satori's opentype.js
parser throws on their fvar table.
Data model: nodes are real content entries (writings, portfolio,
bookshelf), edges are shared-tag connections — the same relationship
"related posts" already uses, drawn instead of listed. No decorative
or fake data.
- src/lib/constellation-data.ts: builds the node/edge graph from the
actual collections at build time.
- src/lib/constellation-scene.ts: vanilla Three.js scene (no
framework) — fibonacci-sphere layout, auto-rotation, pointer
raycasting with a title tooltip, click-through to the entry's page.
Honors prefers-reduced-motion (skips rotation and the spring-eased
entrance, which reuses src/lib/spring.ts) and checks WebGL support,
falling back to a plain count for browsers/settings that lack it.
- src/components/Constellation.astro: wires it up, serializing the
graph as inline JSON for the client script to consume.
- Homepage rebuilt around it: hero constellation above a 3-column
dashboard (latest writings/portfolio/bookshelf), using a new `wide`
layout variant on Base.astro (dashboard needs more than the 42rem
article width).
Also: removes "life and beliefs" from all site copy (site.config.ts,
the llms.txt generator + its output) per request — description is
now "Computer science and mathematics."
Verified in-browser: real WebGL scene renders with correctly
positioned/colored nodes and edges, hover shows the right title via
raycasting, layout holds at the new wide container width.
"subresource integrity", "task scheduler", and "conventional commits"
had spaces, producing broken-looking /tags/<tag>/ URLs. Also fixes a
silent bug: site.config.ts's tagDescriptions already keyed
"subresource-integrity" with a hyphen, so the per-tag description
never actually matched the un-normalized tag.
Added normalizeTag() to migrate-posts-to-writings.mjs so re-running
the migration script doesn't reintroduce the spaces from the
untouched content/posts/ originals.
scripts/generate-llms.astro-site.mjs covers writings, portfolio,
bookshelf, and blogs (unblocked now that site.config.ts exists from
the feeds/meta work), wired into apps/site's build script.
Also fixes .gitignore: the bare `public` pattern (meant to ignore the
Zola build's root-level output directory) was matching at any depth,
so apps/site/public/ and apps/cms/public/ were being silently
excluded from every commit despite `git add` reporting them as
staged for directory adds (the ignore-file warning only surfaces for
an explicitly named path, not a recursive directory add — easy to
miss). Scoped to `/public` so only the root output dir is ignored.
Picks up robots.txt, the Astro scaffold's favicon, llms.txt, and
Strapi's own public/robots.txt, none of which were actually tracked
until now.
scripts/migrate-posts-to-writings.mjs converts all 20 posts under
content/posts/ into the new writings schema — normalizes dates to
bare TOML local-dates, converts the one unambiguous Zola shortcode in
use (youtube embeds) to a plain <iframe>, and strips the now-meaningless
{% raw %}/{% endraw %} escape tags. content/posts/ itself is untouched;
this only adds content/writings/*.
Also generates 301 redirects from the old /posts/<slug>/ URLs to the
new /writings/<slug>/ ones into sws.toml, inside marker comments so
re-running the script regenerates that block idempotently instead of
duplicating it.
Two posts (from-c-to-machine-code, self-hosting-gitea-and-mirroring-github)
use note/tip/warning/danger callout shortcodes that have no Astro
equivalent yet — flagged by the script rather than guessed at, since
that needs a real component once the design system exists.
Verified: astro check clean, full site builds (80 pages, all real
migrated posts render), redirect generation confirmed idempotent
across repeated runs.
Fixes the formatting deltas from the initial Astro/Strapi scaffold
and brings the legacy generate-og-images/generate-llms/validate-post
scripts in line with Biome's style. No behavioral changes.
Renders a 1200x630 social card per post (satori + resvg-js, using the
site's own self-hosted fonts) instead of reusing one generic og-image.png
for every post. Wired into npm run build via a new generate:og step;
templates/page.html falls back to the per-slug image unless extra.image
is set, and non-post pages keep the generic fallback.