AUG 22, 2026
Building zckr.dev: content as data, everything else follows
The architectural bets behind this site — flat JSON validated by zod, static everything, and client JavaScript kept to a fixed roster of deliberate islands.
This site exists so that experiments have somewhere to live. The constraint that shaped everything: adding a project must cost less than the idea is worth. Weird ideas are cheap; if publishing one costs an evening, the pipeline dies.
Content is data, the UI is a projection
Every project on this site is one flat JSON file, validated at build time:
export const projectSchema = z.object({
slug,
name: z.string().min(1),
status: z.enum(PROJECT_STATUSES),
type: z.enum(PROJECT_TYPES),
// ...lab-notebook fields drive the detail page
problem: z.string().optional(),
hypothesis: z.string().optional(),
causeOfDeath: z.string().optional(),
});No page hardcodes a project card. The homepage, lab, shipped view, and graveyard are all filters over the same records. A bad file fails the build — which sounds harsh until you realize the alternative is shipping a broken page silently.
Static by default
Every page is generated at build time. Notes are MDX compiled server-side; syntax highlighting runs at build via shiki, so code blocks cost zero client JavaScript. Client-side code is a fixed roster of deliberate islands — it started as three (the lab filter, the nav's active-link state, the g-key shortcuts) and the design-system makeover grew it to eight, adding the ⌘K palette, the easter-egg modes, and two motion micro-islands. The neutral rebuild took one back off: the pointer-tracked spotlight grid was an accent glow, and there is no accent colour any more. Then /bench earned one back — its console is the only island on the site that makes a network request, and it makes none until a reader presses RUN. Eight.
The registry layer that came after it added none at all. The graph on /graph
looks like the most obvious candidate for a client island on the site, and it
is drawn entirely at build time: hovering a node dims everything outside its
neighbourhood through generated :has() rules, two per node. The count is
asserted by a test against the files that actually carry the directive, which
is why this paragraph is the thing that had drifted and not the number.
Things decided by deleting them
- No CMS. Git is the CMS. The edit workflow is the editor I'm already in.
- No relative timestamps. A static site claiming "3 days ago" is lying within a week. Absolute dates only.
- No component library until a dialog earned it. For months the
components here were a badge, a chip, and some cards. When the ⌘K palette
became the first real dialog, the makeover adopted the shadcn
architecture — CVA variants resolving to
zk-*classes in one style file — rather than anyone's component code.
The graveyard section is load-bearing: failure documented in the same schema
as success, one causeOfDeath field away.