Moving Off WordPress: 500 Posts and a Nuxt Rebuild
In early 2020 a developer education team decided to leave WordPress. The export was HTML, not Markdown, with code blocks stored as tables by a dead plugin, so all 500 posts were checked by hand. The new site, Nuxt on Netlify with Netlify CMS, went live on 23 November 2020. Preview builds went from 30 minutes to one.
Moving off WordPress took most of 2020, for a developer education blog with over 500 posts. The plan started in a meeting in Miami early that year. The new site arrived two prototypes and one framework swap later, and it went live on 23 November 2020 on Nuxt, Netlify and Netlify CMS.
Throwback Thursday, so some of this is how it felt. I was a developer advocate at Vonage at the time, on the Developer Education team, which sat inside Developer Relations, which sat inside Product. We were not engineers. We did not run any servers.
Why we were moving off WordPress
How we made content was split across three tools. Posts were written and reviewed as Markdown, then moved into WordPress, then tracked in JIRA. External writers in our Spotlight programme couldn’t get into the git repo the posts lived in at all, so for them the process was worse.
Then there was the site itself. I had been quite public about my problems with WordPress: security, speed, bloat and the editing experience. The site was also a server that almost nobody on the team who ran our servers knew about. Vonage had bought several API businesses over the years and this WordPress install was left over from one of them.
A rebrand was coming at the same time. So instead of paying an agency to restyle the WordPress theme, we built the new site on the new brand.
The export that wasn’t Markdown
In April 2020 I wrote a small Node script using the wpapi package (the official Node client for pulling posts out over the web API). It paged through every post, 20 at a time, and wrote them to one JSON file. Around 800 posts came down, going back to 2015. The file was over 30MB, and that was without a single image.
I remember being excited before I ran it. We edited in Markdown, so WordPress must store Markdown, so a few API calls would give me a folder of Markdown files.
It stored HTML. Worse, the code blocks came from Crayon, an old plugin that coloured code in posts and had been abandoned. Crayon kept code in HTML tables: one column for line numbers, one row per line of code. The last version of Crayon before it was dropped moved to plain <pre><code> tags to make leaving easier, but our install was so old and so unmaintained that updating everything just to get the content out was not realistic.
The maintainer of Crayon, it turned out, had also had enough of WordPress and moved his own site to Jekyll.
Reviewing 500 posts by hand
So we decided to review every piece of content manually. Over 500 posts. Not the thousands Smashing Magazine had when they made the same move at the start of 2020, but a lot for a team that also had to keep publishing new content.
The rebrand made moving off WordPress less painful than it sounds. Every post needed new artwork and updated SDK versions anyway, so reviewing them one by one was work we already had to do. What we did not have was weeks to do it in. The review would take months.
The plan was to go live before the review finished. Redirect rules on the old site sent readers to the same post on the new domain, where we had already imported each post’s title, date, author and URL as a Markdown file. The old site moved to a “legacy” domain. On the new site, a post that had not been migrated yet showed a note saying we were still moving this content, with a countdown that redirected to the legacy copy. As each post got reviewed, we edited the Markdown file we had already imported, added the content, and dropped the legacy link.
We did the most read and most recent posts first, and most of those were done before launch.
Two prototypes, then a framework swap
I built two test sites. One in Jekyll, because I had used it before and it builds pages very fast. One in Nuxt.js, because Vue is terrific and I was already a big Jamstack fan (Jamstack means the site is built to plain files up front, with no server behind it).
Vonage had a design system called Volta (the brand’s shared set of colours, fonts and page parts), built on Bootstrap and shipped as a Vue library. That decided it. Jekyll’s templates were easier to write, but with Volta the Nuxt prototype already looked like the brand, and server-side rendering (pages built as finished HTML before the browser gets them) made it quick. After a few weeks of feedback and tweaks we had something close to the final site.
Two weeks after the prototype was finished, the design team dropped Volta.
We replaced it with Tailwind CSS. We got back to the same look, and we ended up with cleaner rules for how the page changes at each screen size. I had written a short post in March 2020 on adding Tailwind to a Vue app.
Netlify CMS handled the content. Its editorial workflow matched our JIRA flow closely, and it saved every post as a file in git, which matched how we already reviewed things. Each edit in the CMS opened a pull request (a proposed change someone reviews before it lands). External writers could finally use the same tool as the rest of us.
The 30 minute builds
Go-live was not the hard part. The builds were.
By October 2020 we had turned on Nuxt i18n (the translation plugin) and production deploys hit 27 minutes. A few nice-to-have Netlify plugins later and deploys were timing out at 30 minutes. Eight people worked on content. Every edit in Netlify CMS created a pull request and a preview build.
The fixes came in steps, and I wrote them all up at the time:
- Nearly 600 imported articles were generating pages for every category, tag and author, about 17,500 pages. Cleaning up post metadata got that to just over 3,000 pages, and deploys dropped from timeouts to 15 minutes.
- Skipping optional npm dependencies did nothing for us.
- Turning off Nuxt’s inline CSS and JS minification (squashing the code files smaller, which Webpack already did) saved a minute or two.
- Setting
CI=1to cut logging to errors only made a surprising difference and got us to 8 minutes. - Turning off every extra step Netlify runs after a build, because Nuxt had already squashed and built the pages, got us to 5 minutes.
Five minutes was as far as production would go without paying for Netlify enterprise. Production was never the build I cared about, though, it was the previews.
Nuxt 2.14 had a crawler that found every page URL the site could make. If I turned the crawler off and handed the page builder one route, the build finished in about a minute. The route was already in the branch name: Netlify CMS put the slug in the branch name, cms/blog/<slug>, and Netlify exposed the branch as the HEAD environment variable (a named value the build can read) and whether it was a pull request as PULL_REQUEST. A preview build could work out which single page to build from its own branch name.
// nuxt.config.js
const isPreviewBuild = () =>
process.env.PULL_REQUEST && process.env.HEAD.startsWith('cms/')
const previewRoute = () => {
const [, type, slug] = process.env.HEAD.split('/')
return [`/${type}/${slug}`]
}
module.exports = {
generate: {
crawler: !isPreviewBuild(),
routes() {
return isPreviewBuild() ? previewRoute() : []
}
}
}
The type part meant we could preview author and video pages too, as well as blog posts, and previews came in at about a minute.
I built most of that without testing it, using the BRANCH variable because I assumed it held the branch name. It does not. When nothing worked I thought I had wasted hours on a bad idea. I had got one environment variable wrong.
What it felt like
At the time I wrote that it had been almost five years since I was in a role building code that production relied on. I should not have been surprised that the default Nuxt configuration wasn’t tuned for production builds, but I was.
I was also wary of the fix. Configuration that changes how an app builds depending on the environment it runs in is the kind of thing I would normally avoid. I did it anyway because it was config, not application code, and because eight people waiting half an hour per edit was the bigger problem. At the end of that post I told myself to read the manual and be careful with code that behaves differently per environment.
Launch day, 23 November 2020, went without problems. Redirects sent people to the new site, with the legacy site behind it for anything still in review. A week later I added server-side analytics with a Netlify Function, because a site with no server of its own loses a lot of tracking to ad blockers, and page views got a lot more accurate.
Where it left me
Six years on, this blog is Markdown files in a git repo on a tool that builds the site to plain files, with a build that fails if any URL that was ever published stops working. I did not connect those two things until I went back and read the 2020 posts for this one. The countdown page pointing at a legacy domain did the same job in 2020, with less polish: a reader with an old link landed on something. I wrote up the plain _redirects file format in a TIL the following June, and I have been putting redirects in front of every site move since.
FAQ
Why did the WordPress export return HTML instead of Markdown?
WordPress stores the rendered post, not the source you typed. Our team edited in Markdown, but the database held HTML, and the Crayon syntax highlighter plugin had stored every code block as a table with a column for line numbers. The plugin was abandoned before our install could be updated, so the content had to be reviewed by hand.
How did the site go live before all the content was migrated?
Every post’s metadata was imported as a Markdown file first, so every URL existed on the new site from day one. Posts not yet reviewed showed a note saying the content was still being moved, with a countdown that redirected to the same post on a legacy domain. The most read and most recent posts were migrated before launch.
Why did Nuxt win over Jekyll?
Vonage had a Vue-based design system called Volta, so a Nuxt prototype looked like the brand from the first day, and server-side rendering kept it fast. Jekyll’s Liquid templates were easier to write, but the Nuxt prototype came together quicker. Two weeks after it was done, Volta was dropped and Tailwind CSS took its place.
How did preview builds get from 30 minutes to one minute?
Production builds went from timeouts to 5 minutes by cutting 17,500 generated pages to about 3,000, setting CI=1 to quiet the logs, and turning off duplicate minification and Netlify post-processing. Previews got to about one minute by turning off the Nuxt crawler on pull request builds and generating only the route named in the Netlify CMS branch.