- Fresh structure: Next.js App Router + Tailwind v4 at the repo root, exported
as static files (output: export) and served by static-web-server
- Remove the Strapi CMS, Astro app, Pagefind search, comments, old scripts,
Lighthouse config and compose files; content in content/ is now plain
markdown with YAML frontmatter
- Pages: About, Writing, Reading (3D bookshelf) and Portfolio, styled after
adammaj.com
- SEO: metadata and canonicals, JSON-LD, generated OG images, sitemap, robots,
RSS and JSON feeds, llms.txt
- Umami via PUBLIC_UMAMI_SCRIPT_URL / PUBLIC_UMAMI_WEBSITE_ID (build args)
- Dockerfile builds the export and serves it with sws.toml (security headers,
clean URLs, .well-known, OG image content type, immutable asset caching)
- Prettier and cspell for content with a husky/lint-staged pre-commit hook
- Old redirect list dropped to start from a clean slate
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
- Added a Docker entrypoint script to manage content repository cloning and updates.
- Updated server configuration to include public URL and proxy settings.
- Enhanced content file utilities to return file paths for written entries.
- Introduced GitPublisher class for handling Git operations and deployments.
- Added tests for Git publishing functionality.
- Updated Dockerfile and docker-compose for production setup.
- Added new public PGP key and security.txt for contact information.
- Created new About and Archive pages in the site.
- Updated site configuration to include new navigation links.
Shiki pulls in a WASM oniguruma engine and bundled theme/grammar JSON
for two full themes; highlight.js's core build plus only the 11
languages content actually uses is much thinner for the same job here.
Token colors now come straight from the design tokens (--accent,
--text-muted, ...) instead of shiki's dual-theme CSS vars, so they
already switch with the site's theme with no extra [data-theme] rules.
Converts the note/tip/warning/danger `{% <tag> %}...{% </tag> %}` blocks
the posts→writings migration script left untranslated into styled
.callout markup (toml-content-loader.ts's renderCallouts(), styled in
global.css to match the new design system).
Also wires shiki into the same markdown pipeline for fenced code block
highlighting (github-light/github-dark, switched by the same
[data-theme]/prefers-color-scheme pattern tokens.css already uses).
Also records the Three.js centerpiece's perf budget check in TODO.md:
scene is trivially light (well under 10k triangles, 100+fps uncapped);
the ~130KB gzip payload is three.js core itself, not a tree-shaking gap
(verified named imports change nothing - sideEffects:false already lets
Rollup shake fully), and the script is a deferred module that doesn't
block first paint. No code changes needed there.
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.
Root "dev" script uses concurrently to run both apps with labeled,
color-coded output ([site]/[cms]) instead of needing two terminals.
The per-app dev:site/dev:cms scripts stay for when you only want one.
References: pixperk.tech (dark, warm off-white text, serif-italic +
mono mixed typography, personal dashboard modules) and mcyoung.xyz
(meta line with reading time, subtle accent underline on titles).
- src/styles/tokens.css: dark-first color palette with a warm-paper
light mode (data-theme override + prefers-color-scheme, toggle wins
either direction), fluid clamp() type scale, spacing/radius scale.
- Fonts: Fraunces (display/headings, italic), Newsreader (body,
long-form readability), JetBrains Mono (nav/meta/code) — all
self-hosted via @fontsource, continuing the mono choice already
used in the legacy OG-image pipeline.
- Base.astro rewritten: real header/nav with active-link state, a
theme toggle (persisted to localStorage, no-flash inline script),
footer with social links (ported from zola.toml), replacing the
scaffold placeholder.
- Reading time: toml-content-loader.ts computes wordCount/
readingMinutes from the raw body at load time, surfaced in the
writings list and detail meta line.
- Bug fix: the marked heading renderer was using raw token.text for
both the visible heading and the TOC, so a heading like
"**Registers**" rendered literal asterisks instead of bold — now
renders inline tokens properly and strips markdown for the
plain-text TOC label/slug.
Verified in-browser (light + dark) across home, a migrated writing
with TOC/headings, bookshelf, and 404.
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.
- src/site.config.ts: minimal site metadata (title, description,
base URL, nav, tag descriptions) ported from the legacy zola.toml
as a placeholder pending real design/copy.
- RSS (@astrojs/rss) and JSON Feed endpoints, sitemap
(@astrojs/sitemap), robots.txt.
- Base.astro: meta description (site-wide default or per-page
override via llms_description/description), RSS/JSON feed
<link rel=alternate>, and a webmention endpoint link (plumbing
only — needs the domain registered on webmention.io to do
anything).
- toml-content-loader.ts: marked renderer now assigns heading ids
and collects them into rendered.metadata.headings, so
render(entry) exposes headings the same way a built-in Markdown
loader would.
- Writing detail page: table of contents, prev/next navigation,
series-nav (uses the existing extra.series/series_index fields),
and tag-overlap related posts.
- Tags index + per-tag pages, tag links wired in from writing cards.
- Pagefind: indexes the build output as part of `pnpm build`
(astro build && pagefind --site dist), plus a /search/ page using
Pagefind's default UI.
Verified: astro check clean, full build succeeds (feeds/sitemap/
pagefind all generate correctly), and manually exercised /search/
(real query, highlighted results) and /tags/ in a browser against
the built site.
Adds slug (uid) fields to all four content types so Strapi documents
can be matched to files deterministically, plus:
- apps/cms/src/utils/content-file.ts: read/write/delete the +++ TOML
frontmatter files, using smol-toml's TomlDate so dates round-trip as
bare local dates (matching the legacy Zola format), not quoted
datetime strings.
- apps/cms/src/utils/content-sync.ts: per-type frontmatter <-> Strapi
field mappings, a boot-time importer that upserts every content/
file into Strapi by slug, and a Document Service middleware that
writes any create/update/delete made through Strapi straight back
to the same files.
Strapi's SQLite DB is now genuinely a disposable cache: content/
stays buildable and readable with Strapi entirely absent, and
restarting Strapi rebuilds its DB from content/ with no duplicates
(matched by slug).
Verified end-to-end: fresh boot imports all seed files once; a second
boot re-imports idempotently (0 created, all matched by slug); create/
update/delete through the content-manager API all correctly write to
and remove the corresponding file on disk.
Adds a custom Astro loader that reads the Zola-style TOML frontmatter
(+++ ... +++) directly, so content/ stays the single source of truth
with no YAML conversion step. Defines writings/portfolio/bookshelf/
blogs collections and basic list/detail pages for each, rendering the
seed entries end-to-end (verified in-browser via `pnpm --filter site
dev`). This is the first runnable MVP: home, writings list + post
detail, portfolio, bookshelf (books and papers), and curated blogs
all render from real content/ files.
Set up a pnpm workspace with apps/site (Astro) and apps/cms (Strapi,
SQLite-backed) as the foundation for the new digital-legacy site.
Switches package management to pnpm and adds Biome for linting/
formatting, replacing npm/no-linter setup. Existing Zola site stays
in place for now and will be migrated/removed progressively.
- new paper/ink palette with single red accent, replacing catppuccin
- self-hosted IBM Plex Serif (display) + Source Serif 4 (body), drop Outfit
- rebuilt homepage masthead, numbered post index, navbar with active states
- switch syntax highlighting + utterances to gruvbox, synced via postMessage
- remove terminal/404 easter egg chrome, clean up remaining inline styles
- fix stray syntax.css link causing a 404