We used rinkata to migrate our site to Astro so that we could make this post

Bryan Schwab

The site you are reading moved from Next.js to Astro this week. If you visited before and after, you saw the same pages. That was the goal. The migration reproduced the old site closely enough that a diff of the rendered HTML would still mean something.

So it shipped with nothing to show for it. No new page, and no load time anyone would notice. For a company site of roughly 1,300 lines, that is a strange thing to spend a day on.

The reason was this post.

What a blog costs

We wanted posts written in Markdown and kept in the repo alongside the code. There were two honest ways to get there.

The first was to stay on Next.js and hand-roll it: MDX, a frontmatter parser, our own validation, our own typed queries over the result. That is a pile of code we would own forever, and none of it is code about anything we do.

The second was a bolt-on. Keep the marketing site where it was and run a separate Astro build under /blog. That sounds cheap until you count what gets duplicated: a second navbar, and a second copy of the document head, both kept in step by hand. Chrome that exists in two places drifts, and nobody notices until a customer does.

We moved the whole site instead. One set of chrome, and a content pipeline that validates every post against a schema before it builds:

schema: z.object({
  title: z.string(),
  description: z.string(),
  pubDate: z.coerce.date(),
  draft: z.boolean().default(false),
})

A post missing a description now fails the build instead of rendering a page with an empty <meta name="description"> that nobody catches for three months.

What else fell out

The deploy pipeline lost its worst habit. It used to empty the bucket and re-upload everything, which meant every deploy had a real window where the origin was empty and the site returned 404s to real visitors. It also forced text/html onto every file it touched, so /.well-known/security.txt went out to security researchers as HTML. Both of those are gone.

A class of bug stopped being possible. The contact page had shipped a React hydration mismatch, where the client router disagreed with the prerendered HTML about a trailing slash and React threw at load. We fixed it once. Nothing on this site hydrates now, so nobody can reintroduce it later without noticing.

Neither of those was the reason we did the work.

We ran it through rinkata

We build rinkata, so we used it on this.

The whole thing started as one line typed into it: “Migrate quirence.ai to Astro, then add a Markdown blog.” That became a goal with sections, and the three that described work each got a spec: the deploy rewrite, the migration, and the blog. Nineteen tickets came out of those specs. Each one carried its own acceptance criteria and had to close with evidence rather than a checkbox.

It stopped us once, and that is the part worth reporting.

Partway through the migration we edited a spec that tickets had already shipped against. rinkata refused the write. The contract was bound to finished work, so changing it needed either a successor spec or a decision on the record. It also flagged that a ticket we had already marked done might now be wrong, because the text it was built from had moved underneath it.

Both became decisions, raised as proposals that could not be self-approved. A human ratified them twelve seconds apart.

Four decisions ended up on the record for this project. Two were design calls: Astro sources could not live in src/ while Next.js was still building, and the Astro build adds an og:image to sub-pages that production had lost. The other two were the drift above.

From that first line to this post merging was five hours and forty-five minutes.

The bet

If we never write a second post, this was wasted work. That was the actual argument for doing it, and it is worth saying plainly instead of dressing it up after the fact. The migration had no justification of its own. It was a bet that the writing was coming, and the only thing that settles it is whether more of these turn up.

So, hello world. Now we owe you a second one.

All posts