What Happened After the $8 Website Rebuild
$ grep -n "^##" 2026-10-after-eight-dollar-website-rebuild.md
Last October, I reported a website rebuild costing $7.99. The receipt in that post recorded 25 minutes 25.6 seconds of API duration and 26 minutes 3.9 seconds of wall time. It was a useful demonstration of how cheaply an agent could turn a WordPress export into a working Next.js site.
I would judge an agent-built migration by the promises it preserves, including the ones a successful build cannot check.
Nearly a year later, the repository makes those promises more concrete. Its code and repair records establish what the implementation does; they are not an uptime report or a year-long maintenance bill.
A converted article can still be incomplete
One old article linked to a download called TwitterConsumer.java. The prose survived the migration. The download did not appear in the repository or its available history.
That gives you a very specific definition of incomplete. The page can render correctly, its code example can look plausible, and every TypeScript check can pass. A reader following the article still cannot obtain the file it promises.
The September repair record distinguishes recoverable links from missing material. A navmesh file was located in an existing project repository. An image path could be corrected to a file already present. The Java download and a historical earthquake dataset remained unrecovered, so the referring articles received archive notes rather than replacement links pretending to be the originals.
That last choice matters. A current earthquake API might be useful, but it is not the dataset an old article analysed. Silently substituting it would change the evidence behind the article. A plausible replacement is a different editorial act from restoring a broken link.
For a migration, I would put promised downloads and linked datasets in the acceptance criteria alongside the pages themselves. A count of converted posts tells you how many documents passed through the converter. It does not tell you whether a reader can still use them.
Publishing is a policy across several routes
The site now distinguishes drafts, scheduled previews and published articles. That sounds like a couple of frontmatter fields until you enumerate the places an article can escape.
A post appears in more than its own route: listings, category pages, feeds, a sitemap, social-preview images and a full-text export for language models. Hiding it from the homepage is only one part of hiding it.
The current contract is:
| State | Direct article route | Public discovery |
|---|---|---|
| Draft | Hidden; article shows not-found, image routes return 404 | Excluded |
| Scheduled | Preview available, marked noindex | Excluded until its publication time |
| Published | Available | Eligible for listings and feeds |
The shared publication filter gives the discovery surfaces a common rule. Tests also check less obvious routes: a draft must not disclose its title through metadata or produce an OpenGraph image. Another check ensures an article in several categories appears only once in the full-text export.
Those are product requirements. A visually convincing blog can violate every one of them.
Scheduling adds time to the contract. Cached state must not keep calling an article “scheduled” after its publication time passes. The implementation refreshes that derived state when a post is read, while route caches still have their own revalidation windows. An instant, globally synchronised release is a stronger promise than this design makes.
Modification dates required similar care. The current content system uses an explicit editorial updatedAt when supplied, otherwise the publication date. A fresh checkout or deployment does not count as an edit. The repair notes record a rejected Git-history fallback: a shallow deployment clone could lose the relevant editing commit and make the derived date move backwards.
Google’s guidance on publication dates asks for dates describing publication or substantive updates, with consistent visible and structured values. The engineering task is to preserve that meaning through the build, feed and metadata paths. Updating a timestamp because a file happens to be new on disk would make the machinery easier and the archive less truthful.
A passing check needs a target
The repository now distinguishes maintained tests from historical test reports and archived suites. Keeping an old report does not make its advertised coverage part of today’s release process.
The current CI configuration separates application checks, checks for the email Worker, browser tests against the candidate’s production build, and a smoke test against the deployed website. They answer different questions.
A browser test against the candidate can catch a rendering regression before release. A check against cutler.sg observes whichever version is deployed there. A green deployed-site check cannot validate a change still sitting on a branch, and a green branch build cannot establish that production has the right credentials or provider subscriptions.
This is the distinction I would make explicit in an agent’s completion report: what revision, in what environment, against which requirement? “All tests passed” is too compressed when those tests might be looking at different software.
The same applies to mocked integrations. A test can establish how an endpoint handles a simulated storage failure. It cannot establish that an external service is configured correctly today. Both checks are useful; the missing one must remain visible.
The site also acquired more jobs
The repository contains more than the public blog now: AI demonstrations, private automation APIs, fitness integrations and a separately deployed Cloudflare inbound-email Worker. Those additions should not all be charged to fixing the original migration. Some of the work exists because the project does more.
But each new job adds its own definition of success. The inbox endpoint, for example, returns a failure when message storage is unavailable. A healthy-looking HTTP route is not enough if a caller believes a message was saved. The Worker has its own delivery and fallback behavior, plus its own build and deployment lifecycle.
This is why the original receipt has a useful but limited scope. It reported the initial run. The estimated manual alternative in that article was not a controlled comparison, and I would not carry its multiplier forward as measured savings. There is no maintenance ledger behind this follow-up that converts the subsequent changes into a new all-in price.
What the repository supplies instead is a set of inspectable obligations. Preserve the old material. Apply publication rules consistently. Keep private operations behind their authorization boundaries. Say exactly what was verified and where.
For your next agent-built migration, pick one promise that survives beyond the screenshot: a download, a scheduled release, a private record. Follow it through the route least likely to appear in the demo. Write down the expected behavior, check it on the candidate, and verify the relevant deployment behavior separately. If an original asset is missing, record that absence before calling the migration complete.
$ subscribe --newsletter
Practical AI engineering, in your inbox
Field notes for technical leaders building agents, evaluation systems, governance, and production infrastructure.
Related
What I Need Before I Approve an Agent’s Work
A review bundle should connect the requirement to evidence for the exact candidate and make the remaining decision explicit.
When an Agent Can Change Its Own Tests
A deliberately broken discount function passes its edited test; an unchanged acceptance check shows why the owner of the verdict matters.
When Should an Agent Ask Permission?
A useful agent acts within delegated authority, prepares consequential actions for review, and asks again when the action changes.