React & SEO: Crafting Next.js SPAs Optimized for Google

In a rush? Skip to tutorial steps or live demo.

We’ve often promoted the use of single-page applications.

We’ll continue to do so because they’re great. I mean, they’re fast, they offer incredible UX, and are fun to develop.

However, we’ve also always been very aware of things to be cautious with when it comes to dealing with SPAs—crucial things, like search engine optimization (SEO).

It’s not to say that you can’t get great SEO results with SPAs, because you can. You simply need to handle it the right way, whether you’re working with Angular, Vue or React.

In this post, I’ll present Next.js, and how it can help you create SEO-friendly React SPAs.

All of this, in a few easy steps:

  • Creating a Next.js project
  • Generating components
  • Optimizing the Next.js app for SEO
    • Making it crawlable with prerendering
    • Creating a sitemap
    • Adding metadata
  • Hosting the SPA with Netlify

By the end of this tutorial, your project should be safe under the good grace of our Lord Almighty a.k.a. Google.

Let’s do this.

What is Next.js?

In a nutshell, Next.js is a lightweight framework for static and server-rendered React applications.

next-js-seo

Don't confuse it with Nuxt, which is a framework for Universal Vue.js apps—actually inspired by Next. They share very similar purposes.

By now, you must at least have heard about React, but for the sake of clarity, we'll define it as a component-based JavaScript library for building interfaces.

And what do we mean by Universal JavaScript? Well, it refers to apps where JavaScript runs on both client and server. This is great both for performance in first-page load and SEO purposes, as we'll see in a moment.

Next takes all the good parts of React and makes it even easier to get an app running. It does this thanks to multiple built-in configurations—automatic code-splitting, file-system routing, server-side rendering, static files exporting, and styling solutions. It can also be used as a static site generator.

It currently sits on top of the list of the most popular tools of its kind and is still growing in popularity. Not surprising, considering the rising adoption of the React framework.

No wonder big companies such as Netflix, Uber, and GitHub are already using it.

The problem with single-page applications and SEO

I first want to let you know that we’ve got a full guide on how to optimize your SPA for SEO. You can find it right here. Stick to this post for the (very) condensed version.

houston-we-have-a-problem

SPAs’ ability to run entirely in the browser is powered by a whole lot of JavaScript. SEO-wise it acts as a double-edged sword.

On one side, it brings both great performances & UX, which are important factors in good SEO results. The equation is simple:

Better UX = Longer average time-on-page & lower bounce rate = better Google rankings.

However, with React SPAs, like many frontend frameworks, rendering is done dynamically with JavaScript. Search engine bots then have a hard time crawling the asynchronous content of our pages resulting in lower SEO performances.

Google has claimed numerous times that it’s getting better at crawling JS, but we’ve never got proof that it does it 100%. Plus, remember that there are other search engines out there, that certainly don’t successfully crawl your JavaScript code.

Considering how competitive the field of search engine optimization has become, any small mistake can cost your online business a whole lot of traffic (and, consequently, money).

Let's see how we can fix this!

How can I verify if my SPA content is correctly crawled?

I suggest you run Fetch as Google from Google's Search Console on every key page of your website.

fetch-as-google

The name is pretty self-explanatory, but you can use this tool to ensure that bots are finding your content. It'll tell you if it can indeed access the page, how it renders it and whether any of the page resources (images or scripts) are blocked to Googlebot.

If you find out that JS dynamic rendering is causing any obstruction to search engine crawls, you can quickly act on it.

How do I make sure my content is crawled?

react-seo

There are a few solutions to this problem. In this case, the answer will come from Next.js.

All you have to determine is the right approach for your specific needs:

→ Server-side rendering. In an SSR setup, you're offloading the rendering process to the backend. What is then returned to the client are fully-rendered HTML views, easing the logic on the frontend. For this reason, this approach is great for time-sensitive apps.

→ Generating static files. This lightweight process performs the action of loading all your assets into a static HTML for the crawlers to enjoy. It only executes for pages that are requested by bots, so they aren't blocked by all the JavaScript, otherwise (for regular users) everything is loaded as usual.

→ Third-party tools like Prerender SPA Plugin & Prerender.io also do kind of the same process as the latter, with great results.

For this demo, I decided to go with static files generation because it doesn't require a server, which directly fits with the JAMstack logic.

To learn more about these rendering approaches, watch this thorough video tutorial. It was done for Vue.js, but the concepts are also applicable to React.js.

Other SEO considerations

Obviously, there’s more to SEO than having your content rendered and indexed correctly. There are plenty of ways to optimize that content to be more engaging and of top quality. Always remember that in SEO in 2019, user engagement is king.

This part mostly falls under the responsibility of content creators you might say. You would be half-right to think so. Search engine optimization is a team affair and should always be on top of the developers’ minds.

Many other technical considerations will have a HUGE impact on SEO (some that I’ll cover in the tutorial below):

  • Website speed 🏎
  • Mobile optimization (not an option)📱
  • Structured metadata 📋
  • HTTPS certification for security 🔒
  • Sitemaps 🗺

Time to see how you can cover pretty much all of this with Next.js.

Next.js tutorial: crafting an SEO-friendly React SPA

next-js

The use case is a small online store because e-commerce websites are excellent business examples for which SEO is vital. Know that you don’t have to go through the shopping cart integration to understand the Next.js SEO features. This demo is relevant for any other use case.

Prerequisites

  • A basic understanding of single-page applications (SPAs)
  • A Snipcart account (forever free in test mode)

1. Creating a Next.js project

For this demonstration, we'll be starting from scratch. Create a new folder for your project and initialize it as an NPM project with the following command.

npm init -y

Once this is done, you can install Next.js as well as dependencies, react and react-dom.

npm i --save react react-dom next

I'll also be using Sass for this demonstration. If you wish to do so as well, you can install the following packages.

npm i --save @zeit/next-sass node-sass

In that case, you'll also need to specify to Next.js that you want to use this tool. Create a new file at the root of your project named next.config.js and add the following snippet.

const withSass = require('@zeit/next-sass')
module.exports = withSass()

This seamlessly handles the transpiling of Sass to CSS automatically for you. Very neat!

Finally, update your package.json to add the following start scripts:

}
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "export": "next build && next export"
  },
}

This allows you to use the npm run dev command and test your website locally by inputting the following URL in your browser: localhost:3000.

2. Creating components

Just like most modern frontend frameworks, Next.js uses a component system. However, in Next.js, there is a distinction between regular components and page components. More about this in the next step.

First, create a folder named components at the root of your directory. This folder will contain the following files: Product.js, ProductList.js and ProductDefinition.js.

The ProductDefintion.js file will hold the product's buy button as described in our documentation.

const ProductDefinition = (props) => {
  return (
    <button className="snipcart-add-item"
      data-item-id={props.product.id}
      data-item-name={props.product.name}
      data-item-price={props.product.price}
      data-item-image={props.router.pathname}
      data-item-url="/">
      Add to cart ${props.product.price}
    </button>
  )
}
export default ProductDefinition;

Here, you've created an exported variable that contains a function returning an HTML element. This is all possible thanks to the JSX syntax. The props parameter of your function will eventually be passed down from our index.js page. You don't need to worry about this too much for now.

In our Product.js file, we'll output information we want users to see about the product.

import ProductDefinition from './ProductDefinition'
const Product = (props) => {
  return (
    <div className="product">
      <div className="product__information">
        <h2 className="product__title">{props.product.name}</h2>
        <p className="product__description">{props.product.description}</p>
        <ProductDefinition product={props.product} />
      </div>
      <img src={props.product.image} alt="product image" className="product__image" />
    </div>
  )
}
export default Product;

We've also imported our ProductDefintion component and added it in the return clause with an attribute called product. In the React world, this is what we call props, and it allows you to share information between components.

The ProductList component simply loops through each products using the map function and returns a Product component.

import Product from "./Product";
const ProductList = (props) => {
  return (
    <div className="products">
      {props.products.map((product, index) => <Product product={product} key={index} />)}
    </div>
  )
}
export default ProductList;

Note: as you nest more components, you might realize that passing props through each of them can get quite a bit repetitive. This is an anti-pattern known as prop-drilling and can be avoided using React's context API or other tools like Redux!

3. Creating a home page

Now that all of our "regular" components are ready to go, we'll create a new folder called pages with a file named index.js. Inside, we'll import our product list and stylesheet.

import ProductList from "../components/ProductList"
import '../assets/index.scss'
const Index = (props) => {
  return (
    <div className="app">
      <Head>
        <title>Beautiful, high quality carpets | CarpetCity</title>
        <link rel="stylesheet" href="https://cdn.snipcart.com/themes/v3.0.0-beta.3/default/snipcart.css" />
      </Head>
      <main className="main">
        <ProductList products={props.products} />
      </main>
      <script src="https://cdn.snipcart.com/themes/v3.0.0-beta.4/default/snipcart.js"></script>
      <div hidden id="snipcart" data-api-key="<your-public-api-key>"></div>
    </div>
  )
}
Index.getInitialProps = async () => {
  return {
    products: [
      { id: "nextjs-seo_carpet1", name: "Straight and Narrow", price: 25.0, image: "static/straight-and-narrow.jpg", description: "Revitalize your living room with this durable and stain hiding carpet." },
      ...
    ]
  }
}
export default Index;

You also need to import Snipcart's CSS and JavaScript file as described in our documentation. Since Next.js doesn't allow us to edit the index.html file as we can typically do using "vanilla" React, you can import their utility component Head and place any relevant HTML tags there instead.

Also, using Next.js, we can use the getInitalProps life cycle method on any page component to fetch data. This data is retrieved when our app gets pre-rendered or server-side rendered and passed as a prop to our page component. This is where you generally want to fetch from an API or a CMS. However, for the sake of simplicity, we'll return a plain JavaScript object here.

4. Optimizing your Next.js app for SEO

Now, the main course.

Making sure your SPA follows good SEO practices can sometimes feel like falling down the good ol' rabbit hole. Thankfully, Next.js does most of the heavy-lifting work for you. However, if this is still a new topic for you, I re-invite you to check out our resource-packed guide on optimizing your SPA's for Google. It might give you a better idea of what we're trying to achieve.

4.1 Making your website crawlable

To offer crawlable content to search engines, Next.js gives you two option: prerendering or server-side rendering.

In this guide, I'll show you how to prerender your website. Having said that, if you want to render your app server-side, you can check out this guide we made.

To prerender your app, update your next.config.js to the following and run the npm run export command.

const withSass = require('@zeit/next-sass')
module.exports = withSass({
  exportPathMap: function () {
    return {
      '/': { page: '/' },
    }
  }
});

This creates a new directory named out at the root of your project which contains all your static pages.

4.2 Creating a sitemap

Having a sitemap is always a good idea for SEO as it helps search engines to index your website appropriately. Unfortunately, creating a sitemap can be quite a tedious process. Therefore, I'll be using the nextjs-sitemap-generate package to automate the task as much as possible.

This might be a bit overkill at the moment concerning you only have one page; however, you'll be ready to go if you ever decide to grow your SPA.

npm i nextjs-sitemap-generator

Once the package is installed, add the following code to your configuration file. With the following snippet:

const sitemap = require('nextjs-sitemap-generator');  
sitemap({  
  baseUrl: '<your_website_base_url>',  
  pagesDirectory: __dirname + "/pages",  
  targetDirectory : 'static/'  
});

It generates a sitemap.xml file inside the out directory. Keep in mind; you'll need to manually provide your sitemap to the Google Search Console for it get recognized by Google.

4.3 Adding metadata

It's generally a good idea to add metadata to your website as it helps crawlers understand the content of your pages. Next.js automatically adds most of them, including the viewport and content type.

However, you should define the meta description tag by editing the Head component in your index.js file to the following:

<Head>
  <meta name="description" content="Buy beautiful, high quality carpets for your home."/>
  <title>Beautiful, high quality carpets | CarpetCity</title>
  <link rel="stylesheet" href="https://cdn.snipcart.com/themes/v3.0.0-beta.3/default/snipcart.css" />
</Head>

With all these SEO steps completed, this is what Google Lighthouse should say about your SPA:

next-js-seo-results

Not bad, eh?

5. Hosting your project using Netlify

At this stage, you'll probably want to host your SPA for others to use. Thankfully, nowadays, this step has been significantly simplified.

Note: Netlify hosts all website under HTTPS, which is a must-have in today's SEO practices!

Assuming your project is already on a service in the likes of GitHub, GitLab or BitBucket, hosting your site using Netlify is almost as easy as logging in and linking your repository.

Just make sure to input the npm run export command and the out publish directory like so:

netlify-config

Once Netlify's build has finished, your SEO optimized SPA should be open for business!

Live demo & GitHub repo

next-js-seo-demo

See the Github repo here

See the live demo here

Closing thoughts

This wasn't my first time with Next.js, and my opinion about it hasn't changed much. It's a nice layer built on top of React that helps make your life easier in many situations. Accomplishing the same thing using only React would require a lot more configuration and fiddling around.

All in all, I spent around a day building this demonstration and make sure it was optimized for SEO purposes.

To push this demonstration further, it would have been interesting to have individual pages for each product. It wouldn't be fair to expect Next.js to detect all paths for dynamic URLs—therefore, it would require us to build a function that dynamically specifies the path of each product to prerender.


If you've enjoyed this post, please take a second to share it on Twitter. Got comments, questions? Hit the section below!

Suggested posts: