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.

metanextjsarchitecture

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:

packages/types/src/index.ts
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.