Case Study: Migrating a high-traffic WordPress website to a Headless Architecture using Nuxt
Tags: nuxt, vue, wordpress, headless
Published: 2024-02-16 18:29:25

In this case study, I'll share a small sneak peek of my experience of migrating a traditional WordPress site to a redesigned front-end and high-performance headless architecture using Nuxt. 

I’ll highlight our challenges, focus on what we think to be the most important and pivotal moments of our decision-making process, and the ultimate positive impact our move to Nuxt made on our lead generation website.

## A little bit of history

I have been working for my client for almost 4 years, for the first 2 years all I was doing was marketing microsites and small experiments but that changed in the second quarter of  2022.



A  highly visited website was around 12 years old and with the last redesign circa 2015, the whole website was showing its age. We use WordPress and a custom theme.

The problems we had in 2022 were the same problems everybody running a business with WordPress might encounter:

- **Technical Debt in the Form of Plugin Updates:** Our website was burdened by outdated plugins. which threatened to break our site with every attempt at an update.

- **Oversized Network Payload:** probably again because of out-of-use plugins that had to inject many dependencies into our website

- Unnecessary Markup

- Slow Website Load Times (sometimes)

We did try to solve the technical debt situation. Despite our best efforts to optimize the site, even disabling all tracking scripts in incognito mode to get a cleaner performance read, many pages stubbornly remained in the low 70s according to Lighthouse scores. For those unfamiliar, Lighthouse is a tool by Google that measures website quality across several metrics, including performance. Scores in the 70s indicate significant room for improvement, especially in terms of speed and user experience.

Around April that’s when the Tech Director said **“Ey, why don’t we go headless?”**. 

I will try to be as faithful to the story as possible but the story (more or less) was this way.

## Choosing the Tech Stack.

We were a small team back then:


- A front-end developer

- Me. Another front-end developer with back then around 6 years of experience and a couple of years of experience using React and VueJs.  
     
3 devs counting my boss. 

After the project started we were joined by an SEO specialist that had experience in these kinds of projects. He played a crucial role in providing UX quality assurance and in maintaining and reviewing the integrity of the information so SEO-wise everything ended up as close as possible to the back-then live site.

We had never done anything similar with WordPress before: **A website using WordPress as a headless CMS?**

I had done some freelancing using React where I used Contentul, Strapi, or Builder.io to control the text, toggle components, site-wide announcements, and navigation for SPAs but not with an SEO, SSR, or SSG focus but anyway pitched the idea.


> Well, why don't we go with NextJs or Gatsby? I have plenty of experience with React and many articles cover what we are trying to achieve.


The conversation went somewhat like this:


> Boss: Maybe later, let’s stick with VueJs. All 3 of us know it and there are others in the company that know it in case we need people to chime in.


> Me: Alright, I guess we are going with Nuxt then.



> Boss: No, we are going to use Gridsome, we are taking the Static Site Generation route.



## The Short-lived Gridsome Era
I was excited. I had been reading about SSG for the past 12 months and was eager for a new challenge. The prospect of serving our pages with less latency and the best performance for the client and achieving above the 80s in the lighthouse sounded promising.

When we researched about it, we found that it was a favorite tool in the Vue community for headless implementation because of how well it interacted with WordPress

It had most of what was provided by a modern front-end framework plus nice interfaces to generate dynamic pages/templates from WordPress.

It generated the graphQL data from WordPress without having to install plugins in our backend. Had good documentation on how to do middleware or load custom data when the server started.

It did not matter however how much we liked Gridsome, reality soon set in with two major hurdles:

**Maintenance and Popularity:** The last update to Gridsome was in November 2020, leaving it behind with Vue 2's limitations, lacking the Composition API and Vite support. Its niche adoption meant solutions to our challenges were scarce.

**Data Handling:** Our decade-old site, boasting over published 9,000 posts, strained Gridsome's capabilities. Server start times and build processes became painstakingly long, with frequent crashes due to the overwhelming demand on our WordPress backend. 

Long story short, we began development around mid-April, and after shedding blood and tears, and facing the hurdles of the typical learning curve and the technical issues described above plus many more (that could make its list called “The Headless Wordpress with VueJS Cheat sheet”) we launched in September of 2022.

A couple of months after we went live and even after many optimizations, just starting the server could take from 5 to 10 minutes. By the time we went live with Gridsome, our build times ended up being 4 to 6 minutes long after much optimization and content pruning a couple of months after launching. 

That meant that on every build, we generated almost 9k pages. And every page/post update on our WordPress backend triggered a new build.  And we had to run those builds to show the latest posts or content updates. More than once our builds would crash because our (now) WordPress backend couldn’t handle that many calls and requests back to back.

You might ask “Why are you talking about WordPress to Gridsome if in the title you refer to **‘Moving an old high-traffic WordPress website to Nuxt’?**”

Because the first part of the tale on _**how the coconut gets its water:**_  was the gruesome rebuild we did.

All the lessons learned there, the limitations, and the issues became the foundations and gave enough experience & proof to convince everybody that Nuxt is the way to go for the next batch of websites.

Yes, this was supposed to be the first of many websites to transition to SSG, it was the prototype to benchmark to create the template on how to work the next step: the website’s redesign. 

**Spoiler alert**, the other websites were migrated to Nuxt.

## Redesigning a WordPress Website using Nuxt

Almost one year later after the Gridsome launch, we got the task of doing the facelift.

Only this time, nobody was against using Nuxt.

The first step we took was to list all the non-dynamic pages we were going to use and what was going to be dynamic.



![Nuxt + Wordpress Folder Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sh4uvybismew7lj52581.png)



The second step was installing the WPGraphQL plugin, which allows us to fetch data using GraphQL.


![WPGraphQL plugin page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/76evj8nt3oi05u92hs2d.png)


This plugin allows us to create and test the queries visually without having lots of GraphQL knowledge.


![How does the plugin page looks like in WordPress](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xtt9s463ux24f2vutq74.png)

The data our website uses is rendered server side so we prepped our fetch function that we later are going to use on useAsyncData. This will prove beneficial later because we ended up reducing the Layout Content Shift significantly for our website, all the data that our above-the-fold components need is already available on mount.

![WordPress API Call Function Using GraphQL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ce1d8gjyvyeue63tdo6e.png)

We have different page types that need different information but a couple of them do share queries. That’s why I moved the small part of the queries to its own utility const file.

![GraphQL queries utility file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j5e6lf7dunj9x7mrgznq.png)


The main benefit of using GraphQL is _efficiency_: We can retrieve all the data required by your Nuxt.js blog in a single request, unlike REST, where we need to make multiple requests to different endpoints to fetch the same data. 

**Spoiler alert:** This *efficiency benefit would become our bane, the core of a show-stopper a couple of weeks after our successful launch.

A good idea is to make a Postman collection with all the API calls you need to do.


![Wordpress GraphQL + Rest Postman Collection Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/edhnjsnwouhu0qp8ea0j.png)


After a couple of days of work, we had wired up most of the pages and components for our blog.  All that was left was to make public our sitemap for Google to crawl once the page went live and replace inside the post content all the references to internal pages that point to our backend to our current hostname.

```javascript
<section
  ref="content"
  class="content"
  v-html="contentFormatter(post.content.rendered)"
/>

```
Remember that we are using Nuxt as a front-end layer, we are going to end up pointing our host to our production domain and then make the WordPress instance point to a different domain. 

For this story, let's say our backend is going to end up called backend.wordpress.com.  


For replacing the links, we used Cheerio. Cheerio allowed us to process our content as Nodelists and loop through our content and its tags.

```javascript
const contentFormatter = (content) => {
  const $ = load(content, {
    decodeEntities: false,
    xmlMode: false
  });

  $("a").each((_index, el) => {
    if (
      el.attribs.href &&
      el.attribs.href.includes($config.public.current_hostname)
    ) {
      el.attribs.href = el.attribs.href.replace(
        $config.public.current_hostname,
        $config.public.current_hostname.slice(0, -1)
      );
    }
  });

  const regex = new RegExp(
    "\\b" + $config.public.hostname + "(?!\\.(jpg|jpeg|png|webp|pdf|gif))\\b",
    "gi"
  );

  if (el.attribs.href && el.attribs.href.match(regex)) {
    const extracted_slug = el.attribs.href.split(".com/")[1];
    $(el).attr("href", $config.public.current_hostname + extracted_slug);
  }


```

For the sitemap, we did not find a plugin that allowed us to produce or directly download the ones provisioned by WordPress, and even if we found one, we still would have to change the references to posts using our backend URL.

For this issue, we wrote a simple script that runs before our **Nuxt** build that fetches the main sitemap from **WordPress** and then loops its content to download more sitemaps. Will all the files in hand, I loop once again to replace our references to our backend for the current hostname. The current hostname would be the URL of where we deployed the website.

```javascript
// Import required libraries
import axios from "axios";
import fs from "fs";
import path from "path";
import xml2js from "xml2js";
import dotenv from "dotenv";

dotenv.config();
const __filename = new URL(import.meta.url).pathname;
const __dirname = path.dirname(__filename);
const targetURL = process.env.HOSTNAME;
const current_hostname = process.env.CURRENT_HOSTNAME;

// Define a function to fetch and process sitemap
const fetchSitemap = async () => {
  try {
    // Get the parent directory of the script file
    const scriptParentDir = path.dirname(__dirname);

    // Fetch the sitemap.xml and robots.txt from a given HOSTNAME defined in the .env file
    const { data } = await axios.get(`${process.env.HOSTNAME}sitemap.xml`);

    // Parse the fetched sitemap.xml content
    xml2js.parseString(data, async (err, result) => {
      if (err) {
        console.error(err);
        return;
      }
      const sitemaps_arr = [];

      // Get the list of sitemaps from the parsed result
      const sitemapList = result.sitemapindex.sitemap;

      // Iterate through sitemap items and modify URLs
      for (const item of sitemapList) {
        if (item.loc[0].includes(targetURL)) {
          const { data } = await axios.get(item.loc[0]);
          let name = "";
          let modifiedLoc = "";

          if (item.loc[0].includes(targetURL)) {
            name = item.loc[0].split(targetURL)[1];
            modifiedLoc = item.loc[0].replace(targetURL, current_hostname);
          }
        }
      }
    });
  } catch (error) {
    console.error(error);
  }
};

```
With all the references updated, I proceeded to write our files to our public folder for Google to crawl.



## The Launch

In the first week of November 2023, we confidently pushed our project live, transitioning smoothly into operation without significant disruptions.

However, _<u>"without major issues"</u>_ came with a caveat. Many weeks after, the arrival of our Core Web Vitals (CWV) report revealed performance challenges that had lain dormant until the site was tested by real-world production demands and increased traffic. These insights prompted a focused effort to optimize our site’s performance post-launch and made us question [How do we improve Page Load Times and Web Core Vitals in Nuxt](https://dev.to/christonit/how-to-improve-page-load-times-and-web-core-vitals-in-nuxt-websites-j3l)



## The aftermath
3 months after launch, our work on the redesign is helping drive a 40% increase in visits and a 20% increase in session duration to the already impressive numbers.

I hope this case study sheds light on the intricacies of such a migration and serves as a valuable resource for those considering a similar transition. The journey has been full of learning opportunities.

Thank you for sticking with me to the very end.