Optimizing Site Score with Headless WordPress: Building a Modern Website for Texim
When building modern sites, one has to be aware of the all important site score and how exactly you will need to satisfy the scoring machines. Site scoring can be a cruel mistress and those fancy designs or cool widgets can tank your score heavily. Getting the right balance between what the client wants and what is optimal for speed and usability is key.
The project was a new banking site with no small amount of pages, with a desire for a CMS system, which the client would use to generate new content as he wished (with reasonable limitations of course). It was decided that we were going to use Headless WordPress to generate our pages, side content, translations and then serve it up via GraphQL to the front-end, where a React SPA would handle dynamically generating the large number of custom and pre-defined pages. It seemed like a good idea at the time - we would have Wordpress do all the heavy lifting and just tell the front-end how the page was structured and what were it's contents. The idea for the front-end was that we would have an assortment of components, following the Atomic design principles, formed into pages, while also giving us the flexibility to tackle any combination of existing components for a page, described by what we would receive as a payload from Wordpress.
For the pre-defined pages we set up a page and GraphQL queries map , which was meant to be used by the page components based on their template name, which was defined in Wordpress.
And in your root component you would simply call the exported function with the relevant arguments, which would return a page component...
Which then would trigger it's own relevant API call and return the current page's data.
Sparing unnecessary details, this was the backbone of front-end implementation. With the methodology and the architecture clear and in place, we began - little by little, problem by problem we tweaked, built, revamped and eventually managed to erect a rather substantial site. The clients, for the most part, were happy; aside from a hefty list of bugs and a few nitpicks, we were on the final stretch.
We knew the side would require some optimization, but we were hopeful there wouldn't be that many changes needed. But then the moment of truth came.
This was not good. All the moving parts and API calls, and dynamic bits, that made up the site were, sadly, very front-end heavy. We started frantically moving pieces about, lazy-loading components, lazily calling whatever queries we could refactor. The score wasn't budging by much, however. The main problem was the huge initial API call to Wordpress that we needed to get the metadata, the navigation data and all the relevant page data.
We started to throw ideas around - chunking the large API call? Ok, however, parts of the UI, like the large carousel on the main page, relied on heavily nested data that would be hard or impossible to carve out via Wordpress and asynchronously loading parts of the interface lead to big layout shifts. Minus points. Maybe some gimmick - like statically serving some parts of the UI on initial load, then swaping them out for more up-to-date data. Nope, that did not manage to fool the savvy scoring tools. Well, how about pre-fetching the data and then load everything else. Yes! Good! But how? That's where Next.js came in.
While the optimal solution for us was to use the Server Side Rendering (SSR) , our clients were not so keen on having a Node.js server running on their environments. That's why we settled on the next best thing - Static Site Generation (SSG). Now, since our site was currently SPA, we would need to make some notable refactoring. The first thing that went out the window was react-router. Next.jsuses it's own routing mechanisms, which are very different. To clarify, React router allows things like SSR, but does not implement it. You still need to make a server script that, at least, renders and serves it to the client. You may need to do other things, like serve static files. NextJS does that for you. More on that straight from Next.js' documentation here.
Next on the chopping block was the SPA implementation itself - Next.js works with pages. A page is a React Component file in the /pages directory. Each page is associated with a route based on its file name. I should note, that you actually, kind-of, have the option of using a single index page as your root page to run a SPA implementation. While feasible, be warned that routing problems abound. That meant that our SPA implementation would have to be split into relevant pages. And there were a lot of pages. This wasn't optimal for us at that point and we would have to account for custom, dynamic pages as well.
Enter Dynamic Routs. In Next.js you can add brackets to a page file name ([id]) to create a dynamic route (as in url slugs, prettified urls, etc.). An excerpt from the documentation states - Any route like /post/1, /post/abc, etc. will be matched by pages/post/[pid].js. The matched path parameter will be sent as a query parameter to the page, and it will be merged with the other query parameters. Awesome! Another boon from Next.js would be it's optional catch-all routes: Catch all routes can be made optional by including the parameter in double brackets ([[...slug]]).
For example, pages/post/[[...slug]].js will match /post, /post/a, /post/a/b, and so on.
The main difference between catch all and optional catch all routes is that with optional, the route without the parameter is also matched (/post in the example above).
And, as I mentioned, since we would need to take into account custom pages, that was just what we needed and, as a bonus, it was a very compact solution.
Now to make this work we would need two key functions that Next.js provides - getStaticPaths and getStaticProps. The getStaticPaths function is needed to define a list of paths to be statically generated. That would give us the proper static site structure automatically.
And if you export getStaticProps from a page, Next.js will pre-render this page at build time using the props returned by getStaticProps. Also since we rely on the page id to find it's template, we use the context of getStaticProps, passed down to us by getStaticPaths.
It is in getStaticProps where we fetch data by reusing our by-page API call, relevant to the page being generated.
Everything else from our app, like our page map and query map, for example, would be reused. Apart from some minor refactoring, reshuffling and optimization (like lazy-loading components on scroll in long pages), there was not much left to do, but hope.
Success! Although it's not on top of the food chain( we couldn't optimize things like the carousel further, which is a rather cumbersome library by itself and ate up a lot of our score) it was still a significant way off from where we started, without having to do that major of a refactor. Although our approach was probably not optimal - you could possibly use a generated sitemap for getStaticPaths instead of a potentially expensive API call, for example, but just using Next.js for our site goes to show, that the right tools for the job can always make a world of difference.