pnpm dev was failing with "App keys are required" for anyone whose
apps/cms/.env didn't already exist (it's gitignored, so this hits
every fresh clone, not just a one-off). predev/predevelop now copy
.env.example to .env automatically when it's absent, so `pnpm dev`
and `pnpm --filter cms develop` both just work without a manual setup
step.
Interaction/layout pattern is my own implementation, inspired by
observing (not copying source from) adammaj.com/reading: a horizontal
scrollable shelf of book "spines" above a list, click-through to a
per-book detail page where the shelf re-renders with that book's
spine expanded into a cover.
- src/lib/spine-color.ts: deterministic per-title color + width hash,
so every book gets a distinct-looking spine without needing a
manually-set color field.
- src/lib/cover-exists.ts: checks the referenced cover_image file
actually exists under public/ before rendering an <img> — the ISBN/
DOI cover lookup only runs on live Strapi create/update, so
hand-seeded content (like our two placeholder books) has a
cover_image path with no cached file yet. Falls back to the
colored spine block instead of a broken image.
- src/components/BookSpines.astro: the shelf itself, reused on both
the index and detail pages; a scroll button, hover-lift, and the
active book's spine widens into its cover/fallback.
- Bookshelf index now renders full reviews inline (was previously
just a bare metadata list) with cover thumbnails.
- New src/pages/bookshelf/[...slug].astro: first individual page
route for bookshelf entries (portfolio still only has a list page).
Verified in-browser: shelf renders, hover/click navigation works,
active-book expansion on the detail page, and the cover-exists
fallback correctly avoids broken <img> tags for our two seed books.
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.
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/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.
404: "Page Fault" — a SIGSEGV/memory-fault framing distinct from the
retired terminal easter egg, still fitting the CS-flavored identity.
Plain/unstyled for now like the rest of the placeholder layout;
restyle once the design system lands.
src/lib/spring.ts: a small CSS+Web Animations API spring utility
(damped harmonic oscillator -> keyframe progress values), per the
locked "no animation library" decision. Not wired into any component
yet — this is the foundational primitive the design system's motion
work will build on, kept free of any visual/component opinions.
- 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.
apps/cms/src/utils/bookshelf-covers.ts fetches book covers from Open
Library by ISBN (real image, cached into apps/site/public/covers/)
and backfills missing title/authors for papers from Semantic Scholar
by DOI/arXiv id. Wired into the sync middleware in content-sync.ts:
runs once per entry (cover_image already set means "done"), and only
on live create/update through Strapi, not during the boot-time
import (so a restart doesn't hammer external APIs).
Also renames the bookshelf `status` field to `read_status`
everywhere (schema, content/README.md, seed content, Astro zod
schema, bookshelf page, sync mapping). Strapi's content-manager API
treats a top-level `status` key as its own reserved draft/publish
parameter even with draftAndPublish disabled, so an attribute
literally named `status` silently rejected every write — caught this
while testing the enrichment hook.
Verified end-to-end: created a book by ISBN with no cover_image,
confirmed a real cover image was downloaded and cached, cover_image
was set on the entry and written to the file, and the cover/file were
both removed on delete.
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.
apps/cms/Dockerfile.dev + root docker-compose.yml give a one-command
way to run the CMS in a container for testing (docker compose up
cms), with the shared content/ directory mounted so it matches the
host dev setup. apps/cms/Dockerfile is a production multi-stage build
for later Dokploy deployment. Uses node:24-bookworm-slim to match the
local Node version and get glibc prebuilt binaries for better-sqlite3
without needing a native build toolchain in the final image.
Verified: both images build, the dev container boots Strapi
successfully, admin panel and API routes are reachable, and the
mounted content/ volume resolves at the same relative path the future
sync provider will expect.
Adds writing, portfolio-entry, bookshelf-entry, and curated-blog
collection types with attributes mirroring the TOML frontmatter
fields in content/README.md exactly, so the future sync provider has
a direct field mapping. draftAndPublish is disabled on all four since
our own `draft` boolean (synced to the file) is the source of publish
state, not Strapi's built-in workflow. Verified Strapi boots and
registers all four REST routes (api/writings, api/portfolio-entries,
api/bookshelf-entries, api/curated-blogs).
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.
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.
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.