WebsitesActive
yotamfromm.dev (this website)
The site you are reading: a Next.js app with a developer side in English and a philosopher side in Hebrew, a content pipeline that checks numbers and quotes in code, and gates that fail the build. Three things broke along the way, and each one became a rule or a test.

A page of mine failed a check on the day I published it, and the check that said it was ready was the one I trusted. The page was a developer post, and the command I used to ask "is this ready?" said yes. The full test suite said no, on two files. That is the story of this site in small: most of what is on it exists because something I built first did not hold.
This page is about yotamfromm.dev, the site itself. It is a Next.js 16 app, created on 2026-10-21, with 323 commits by 2026-10-29. It has three modes: a developer side in English, left to right, a philosopher side in Hebrew, right to left, and a shared side called together. Below I say why I built it, what I looked at first, three moments where something broke and what I changed, how I check that it works, and how you can copy the parts that fit your own site.
I work in two fields that rarely share a room: building software with coding agents, and Jewish philosophy. I wanted one address where a reader could pick the side they came for, and where a post about a plugin and an essay with quotations were both held to the same standard: every claim checked, every number traceable.
So the site has three modes. The developer side holds posts and project pages like this one. The philosopher side holds essays with quotations. Together is where the two meet. The newsletter is one, whichever side you read.
The standard is the real reason for the site. A personal site is a thing I can build the checks for, and then run them on my own writing before anyone else sees it.
I was not starting from nothing. The site had an earlier life as an Astro build with two modes, which the README calls v2 and keeps in git history and on a branch. Version 3 is a rebuild, and it began with reading, not code.
The docs/ folder shows what I read and drew first: a spec, a plan, four research notes (on an accessible mode toggle, on Trove content, on Trove design and on the web), and three mockups, one built around the toggle, one around the entrances and one around a page of Talmud laid out as a daf. How that design process ran, step by step, has its own post: How I designed this site. I will not retell it here. Hosting and measurement are in Vercel and GA4.
What I took from the first round is that the design needed a contract, not a mood. DESIGN.md at the root holds the tokens, the colours, spacing and type, and a script reads it. If a stylesheet uses a colour or a length that is not a token, the build stops.
The app lives in web/, which is the Vercel root directory. The stack, from the README and package.json, is Next.js 16, React 19, next-intl for the text catalogues, NextAuth for the admin sign-in, a Neon Postgres database for the few things that need one, and Resend for the newsletter. Content is plain Markdown files, one per item, under web/content/items/<mode>/<slug>.md, with a front matter block that a schema validates.
Content gets written through a pipeline of eleven steps that run as commands in Claude Code: ideate, sources, thesis, evidence, outline, draft, cite-check, image, label, publish-check and revise. Each mode has its own voice file. The thesis step takes my words verbatim, since I never want the model to write what I believe. The outline step waits for me to approve each claim. The pipeline's output is always a draft, and publishing happens only when I click publish in the admin.
The checks differ by mode. For the philosopher side, a script compares every quotation word for word against its source. For the developer side, the check is on numbers: any number in a draft has to appear in a repo file or an approved fact, and a rewrite may drop a number but never invent one. That check lives in numbers-trace.mjs, plain code and no model.
Saving in the admin on the live site turns into one commit on main, and Vercel redeploys on that commit. The publishing guide says the change is live in about 1-2 minutes.
For the first days the developer side was Hebrew like the rest, with English tool names inside Hebrew sentences. On 2026-10-25 I made a commit titled to keep English tech terms in English across developer posts. It patched the symptom, mixed-language sentences, and left the Hebrew posts about English-language tools as they were.
Two days later I wrote the r12 plan, and its decisions section is blunt: developer blogs and projects are English, only the philosopher side is Hebrew, and together stays Hebrew. The plan says "The main content is the problem." I read that as the posts themselves needing a rewrite, not another round of term fixes.
The change had to be more than translation, because every shared component read one fixed locale. The commit that landed it, adaeae5 on 2026-10-27, made locale a function of the mode. routing.ts now has two locales, he and en, and localeOfMode returns English for the developer mode and Hebrew for everything else. The root layout sets the lang and dir attributes from that. The header, footer, breadcrumbs, lists, cards and the subscribe form all read the mode's catalogue.
Two details made this safer than it sounds. The English catalogue is a partial of the Hebrew one, and check-messages.mjs demands key-for-key parity inside its scope. If an English string is missing, the code throws at build time, so a gap fails the static build instead of showing a raw key name on a page. And the old Hebrew developer tag URLs got 308 redirects to their English slugs, so no old link broke.
The plan also called for rewriting 22 developer items at their existing URLs, and it started with a pilot of three pages before the rest. It turned out that English did not make the posts better by itself. The rewrite needed its own rules, which I come back to in the checks section.
Every page on the site has a plain-text twin: add .txt to the address and an agent, or a person with curl, gets the text without the chrome. I added it on 2026-10-25 in commit 8f300ee, alongside a menu of agent actions on each page.
It did not work. The twins came back as Markdown, with the wrong content type. The cause was in how I had written the rewrite: the .txt address was rewritten to an API route, but the handler never saw the query string that said which format to serve.
That took three fix commits on the same day, and they read like a short debugging diary. The first, d192b42, passed the format as a request header, because the handler never saw the rewrite's query. The second, 8fc6bf2, built the rewrite target as a plain URL so the trailing slash and the ?as=txt part survived. The third, 3bef54e, pointed the twins at a trailing-slash API path so they were served as text/plain. A fourth commit the same day, a87f873, fixed a neighbouring problem: an empty content directory variable now counts as unset, so a server that blanks its environment still finds the content.
I would not call this one a clean win. The log shows it took three attempts to get the served response right, and each commit message names a different part of the rewrite that was wrong. The lesson I take from it is where to look: at the response a reader gets, its content type included, and not only at the handler I wrote.
On 2026-10-29 I had two developer drafts, and the pipeline's publish-check listed nothing blocking them. I asked whether they were ready. They were not. When I ran the whole unit suite, prose-gates.test.ts failed on both posts, and a second test failed over image text in the proposed covers. A third rule, a required aiAssisted flag on every post, was missing because the draft writer does not add it.
The lesson is a plain one, and I wrote it into my own notes the same day: the narrow check and the full suite are different questions. publish-check answers "does the pipeline have what it needs?" The suite answers "does the site still hold together with this item in it?" Only the second one decides ready.
The shape rules behind prose-gates.test.ts are worth copying because they are small and mechanical. For an English developer page, prose-shape.mjs allows at most 10 headings, 12 list items and 10 bold labels, wants at least three quarters of the words to be prose and not lists or tables, and keeps the page between 1,200 and 3,000 words. It allows no em dashes and fails on a short list of stock AI phrases. Fenced code is exempt, so real commands do not count against the page.
I did not loosen a limit when the posts failed. I rewrote the posts, turning bullet lists of sources into linked paragraphs, which is why the posts you read here carry their sources as sentences.
The checks come in layers, and each layer has one job.
The cheapest is the build. npm run build runs check-messages.mjs and check-tokens.mjs first, through the prebuild hook, then next build. Because the build loads every content item through its schema while it prerenders, a malformed item fails the deploy and Vercel keeps serving the last good version.
Then there are the unit tests: 153 files in web/tests/unit, run with vitest. They cover the schema, the redirects, the admin guards, the pipeline gates and the corpus itself, so adding a post means adding a thing the suite reads.
Then Playwright: 45 spec files in web/tests/e2e, with axe from @axe-core/playwright for accessibility. The config runs separate projects, among them bare, phone, desktop and stub, so the same pages are checked at 375 pixels wide and at 1280. Some admin axe sweeps are slow under full-suite load, and a commit on 2026-10-25 marks them test.slow after one passed alone in 43 seconds. I would rather have a slow test than a skipped one.
A safety scan runs before any publish. It looks for host names, addresses, home paths and tokens in public content, and test-gates.sh tests the scan itself with cases that must pass and cases that must fail; its last line reads gates ok: 7 cases.
What these checks do not show: I have no measured accuracy for the pipeline's fact check, and no count of how many drafts it has stopped. The site was created on 2026-10-21, so the honest summary is that the gates exist and run, not that they have a long record.
Fix the cause, not the symptom. The English tech terms patch and the English section are the same problem at two sizes, and the smaller fix cost me two days and fixed nothing.
Put the rule where it can fail a build. A line in a guide gets forgotten; a script that exits with code 1 does not. Every gate on this site began as something I caught by hand and did not want to catch twice.
Test the thing the reader gets. The twins bug sat between my handler and the served response. And ask the wide question before the narrow one: "does the whole suite pass?" before "does the checker say ready?"
You do not need my stack. You need three habits: a contract for your design, gates that exit nonzero, and one command that runs everything before you call a page done.
To run the site from a clone, from the web folder:
npm ci
npm run dev # next dev
npm test # vitest
npm run test:e2e # playwright
npm run build # checks messages and tokens, then next build
To run my two content gates on any Markdown file, point them at the file. The first checks the shape, the second checks that every number in the new file appears in a reference file:
node scripts/content/prose-shape.mjs <item.md>
node scripts/content/numbers-trace.mjs <new.md> <reference.md>
Both exit 0 on a pass and 1 on a failure, so they drop straight into a pre-commit hook or a CI step. If you build something similar, here is the prompt I would give an agent to start it:
My site is a Next.js app with Markdown content. Add three gates that exit nonzero: (1) a token check that fails when a stylesheet uses a colour or length not defined in DESIGN.md; (2) a prose-shape check that counts headings, list items and bold labels outside fenced code and fails above set limits; (3) a number check that fails when a number in a new draft is absent from its reference files. Then add one command that runs the build, the unit tests and the end-to-end tests, and treat only that command's result as "ready".
The page you are reading went through those gates before it was published, and the next one will too.
