Method
No broken links, second time
When we replaced MkDocs in April, the constraint was simple — every old URL keeps working. We applied it again this week to reshape the entire IA. The rule isn't a one-time migration discipline. It is how a site matures without burning its inbound link graph.
The rule has a name now. No broken links. We named it in April, on the day we deleted MkDocs and kept every /docs/... path pointing at something useful. We applied it again this week, when we reshaped the whole information architecture and moved products to top-level routes. Same constraint, second time, larger surface.
This is the sequel to Killed MkDocs, kept the URLs. Read that one first if you want the original receipt. This post is about what happens when a one-time migration discipline turns into a repeatable rule — and what that rule costs you when it scales.
What happened in April
The April migration replaced the docs runtime. MkDocs out, Next.js in. Same Markdown, same URL paths, different render. The constraint that did most of the work was that every external citation, every PR description, every chat message that linked to /docs/whatever had to keep resolving on the day of the cutover. We held the URL interface constant and let the implementation change underneath it.
That worked because docs had a clean shape already. We were not reorganising — we were re-rendering. The migration could land in one commit.
This week's move was harder.
What just shipped
The header used to read: Ship · Lighthouse · Docs · Book · Field notes · Team · Contact. Seven items, flat. Docs was a single global section that had to host documentation for two different products, which meant either Ship docs and Lighthouse docs lived in the same shell (confusing) or Docs was a hub page that fanned out (one more click for everyone).
We dropped the global Docs entry. Docs moved per-product, under each product shell. The new header is six items. Products are now top-level routes. Case essays moved under their product. The route map looks like this:
/products/ship→/ship/products/lighthouse→/lighthouse/getting-started→/ship/quickstart/docs/navigator/overview→/ship/docs/navigator/overview/docs/lighthouse/quickstart→/lighthouse/quickstart/ship/elmundi→/ship/cases/elmundi
Across ten doc areas — orientation, setup, navigator, process, inbox, knowledge, analytics, audit, local, reference — about twenty-eight pages got a redirect each. Plus the product-shell moves. Plus the case-essay moves. Plus the quickstart.
Every old path returns a 308 to its new home. Next.js's redirects() block in next.config.ts does the work. (Next emits 308 — Permanent Redirect — rather than 301; the SEO weight is equivalent, and browsers respect the method-preserving semantics, which matters more than people think the first time a POST gets redirected.)
So if you bookmarked /ship/quickstart yesterday because somebody sent you /getting-started, the bookmark works. If you cited /ship/elmundi in a Slack thread three months ago and a colleague clicks it today, they land at /ship/cases/elmundi. The old shape is gone. The old addresses still resolve.
The trigger was one sentence
Worth saying out loud: the entire IA migration was triggered by a single user message. "I can't find /docs from /ship."
That was the symptom. The global Docs link sat in the header, but once a reader was inside the /ship shell, the docs that mattered to them were unfindable without bouncing back up. The diagnosis was that Docs did not belong at the top level — it belonged inside each product. The fix touched twenty-eight redirect rules.
We could have shipped a smaller fix. Add a Docs link inside the /ship sub-nav. Done in an hour. We chose to do the IA work instead because the navigation was already lying about the structure, and one more patch on top would have made the next migration harder, not easier.
The discipline: land all 308s in the same deploy
Here is the load-bearing rule, and the part where the second application taught us more than the first:
Half-migrated IA is worse than either state.
If you ship the new routes today and the redirects on Friday, you do not just have half a broken site for three days. You have an entire site whose URLs the reader has stopped trusting. The reader who clicks a year-old link, hits 404, and clicks the next link more carefully has not lost one page — they have lost their confidence that any link on the site is safe to follow. They start typing the URL bar more carefully. They stop forwarding the page to colleagues. A broken link is a small thing. A broken expectation about whether links work is a large thing.
So the rule is: the route move and every redirect that points at it land in the same commit, same deploy, same minute. If we cannot land the 308s with the route change, we cannot land the route change.
This rule has killed three or four "let us just rename this folder" instincts over the past year. Each time it killed one, the work that did ship later was better — because by the time we could land all the redirects in one go, we had also decided what we actually wanted the new shape to be. The constraint forces the design to finish before the move starts.
What a redirect actually pays for
Every blog post that ever linked to an old Ship URL still resolves. Every HN comment, every Reddit thread, every academic citation, every embedded screenshot's caption, every README in a downstream repo that pinned a doc page — all of those keep working. A redirect is the cheapest possible respect you can pay a reader who arrived from somewhere we forgot existed.
The arithmetic is one-way. Maintaining a 308 costs almost nothing — one row in a config, one server hop. Breaking it costs whatever the inbound link was worth, which we do not know and cannot measure, plus a small amount of trust from the person who followed it. We have no idea what the second number is. We assume it is larger than zero. The trade is obvious.
A reader who follows a year-old link and lands on the right page learns something about the site they came back to. The reader who hits 404 learns something else. Either way, the site is teaching its readers what to expect from it next time. We would rather they expect that the link will work.
The rule as a check on growth
The constraint also doubles as a brake.
If a teammate proposes "let us reorganise the case essays under /cases/<product>/<slug> instead of /<product>/cases/<slug>," the answer is not "no." The answer is "show me the redirect table." If the redirect table is twelve rows long and clean, we ship it. If the redirect table is forty rows long with overlapping patterns and one ambiguous case where the source path could match two destinations, the proposal goes back. The redirect table is the design review.
This kept us honest in April, when keeping the docs shape was easy because we were not changing it. It kept us honest this week, when twenty-eight doc redirects plus the product-shell moves plus the case-essay moves all had to fit in a single config block without contradicting each other. Two paths cannot redirect to each other. A redirect cannot point at a path that itself redirects. We caught one such loop in review — /ship/elmundi was going to land at a page that itself was being moved. We collapsed both redirects to point at the final URL.
ship.elmundi.ua is the next test. It is being consolidated onto harborgang.com — same constraint, third time. Different scale: a subdomain folding into a different domain entirely, with its own inbound link history.
We will know on the third migration whether the rule scales or breaks. The third one is already on the calendar.