Moving the blog off a domain nobody could find
The old site shipped every article with no title tag, description or canonical URL — and LaTeX that only appeared after JavaScript ran. Here is the move, and the three bugs that almost broke it.
It started with one line of source code
I was sending an old article to a colleague, so I grabbed the page source, and stopped.
No lang on <html>. No <title>. No description, no Open Graph, no canonical, not a single line of JSON-LD. The math was worse: the source contained the literal string \operatorname{Attention}, and only became typeset once JavaScript finished running.
Which means: to a search engine, that article never existed.
How it got that way
Not carelessness — we used the tool harder than it was designed to be used.
To make formulas render, we wrapped LaTeX in <div class="latex-block"> and replaced its textContent with KaTeX output inside onMounted. It looked perfect in the browser, so nobody questioned it.
The problem is that this couples two things: the content only exists after JavaScript runs, while SEO needs content that exists before it does.
Three decisions
Move the content, not the layout. All 12 articles were converted to $...$ and $$...$$, handled by remark-math and rehype-katex at build time. Math is now part of the HTML.
Let data decide the URL, not the filename. The old filenames looked like 2604.04921v1.md, and the resulting URL was /en/blog/04921v1 — the numeric prefix silently vanished. Now a slug field in frontmatter owns the URL.
Not a single old URL may break. 28 redirects, including trailing-slash variants.
Three bugs that almost broke the launch
The eaten slug. Discovered by fetching the live page, not by reading the code. Had we skipped that, we would have assumed a move is just copying files.
hreflang pointing at 404s. The i18n module generates alternates for every locale, but a Chinese-only article has no English version. nuxt generate failed with "Article not found, linked from /blog/xxx". The fix was not to disable the check, but to let each page declare which locales actually exist.
/sitemap.xml was HTML. The build succeeded, the file existed, it had content — but it was a <meta http-equiv="refresh"> shim, not XML. Google would reject it outright. A post-build step now writes the sitemap index into that path.
Where we are
- Math is typeset server-side, with MathML in the markup
- Every page has title, description, canonical, hreflang, OG; articles carry
BlogPostingstructured data - RSS,
llms.txt, sitemap and redirects all in place - An
auditscript checks 25 pages for SEO regressions and broken links on every build: currently 0 errors, 0 warnings
The one-line takeaway
Most of the work in a migration is not moving things — it is proving the things arrived.
So we turned "should be there" into a check that runs on every build. People forget; scripts do not.