Repository navigation
v1.4.0
Related content. A case study and the posts that go deep on the same work
now link to each other, and a series holds together regardless of what
gets published between its parts.
Run supabase/blog_series.sql in the Supabase SQL Editor BEFORE
deploying. It adds three columns to posts; without them PostgREST
rejects the whole select and the blog index renders empty rather than
degrading. Idempotent, safe to re-run.
series + series_order decide reading order. Previous/next stays
chronological on purpose — it answers "what else is there", which is a
different question from "where am I in this set". Before this, the only
thing linking Part 1 to Part 2 was publish date, so anything published
between them would have stranded the series.
The database enforces the invariant rather than the application
remembering to:
- a series with no position is rejected (posts_series_needs_order)
- two parts claiming one position is rejected (posts_series_order_key)
Both verified against the live database, not just locally.
Unpublished parts are absent rather than greyed out. RLS already hides
them, and a link a reader cannot follow is worse than not knowing the
part is coming.
A post can name a project. The post shows what it is about; the case
study grows a "build log" listing the posts. That direction did not
exist at all.
No foreign key, deliberately: projects.slug is unique only through a
partial index — blank slugs are allowed for projects without a case
study — and Postgres cannot back a foreign key with one. The slug
resolves at render time through getProjectBySlug(), which returns null
unless the project actually has a case study page, so a stale slug
renders no link rather than a dead one.
Series name with a part number, disabled until a series is named, and a
project dropdown listing only projects that have a case study page — a
link that goes nowhere should be impossible to create, not merely
discouraged.
- The active tag chip on the blog index uses accentBright behind white
text and measures 3.93:1, below AA. Predates the accentSolid token. - Body images carry no width/height or loading="lazy", so every figure
shifts the layout as it loads and all of them load eagerly. - Both posts share a cover image, so the index shows the same artwork
twice. - Production still requires a manual "Promote to Production" in Vercel.
- Callout blocks (> [!NOTE]) are not rendered.
- The admin blog editor cannot reference an existing Cloudinary asset,
only upload a new one, so re-using an image duplicates it.