Applied React SEO on a Next.js App [Live Demo]

June 02, 2018 0 Comments

Applied React SEO on a Next.js App [Live Demo]

 

 

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

Not long ago, a developer friend of mine was telling me about a new e-commerce project for a client.

"I would have loved to use a React.js app if only it wasn't for all these SEO issues".

Sigh

I thought I'd already convinced him with my earlier post on Vue.js that SEO with JS frameworks was manageable.

I guess not! So I started from scratch, and explained to my friend how to handle SEO with a React SPA.

Today, I'm putting my answer to him in words, using Next.js to craft a crawler-friendly e-commerce SPA.

In this tutorial, I'll:

  • Create a Next.js project.
  • Use the React/Redux bindings.
  • Write and render products.
  • Generate static Files for SEO.
  • Deploy my static assets with Netlify.

Before getting practical, let's review a bit of theory.

What is Next.js?

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

next.js

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 also has a cool set of features including automatic code splitting, simple client-side routing, webpack-based dev environment and any Node.js server implementation.

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

Handling React.js SEO

What's the matter with SEO in 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.

Search engine optimization is now a very competitive field and small mistakes can cost your online business a whole lot of traffic.

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?

As I've already found out with Vue.js, 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 normal 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 applicable to React.js.

Next.js Tutorial: Handling SEO on a React.js SPA

Pre-requisite

1. Creating the project's structure

Let's start from scratch here. Create a new folder where it pleases you and run the following commands:

npm init npm install --save next npm install --save react npm install --save react-dom npm install --save redux npm install --save react-redux 

Now that I have the required dependencies, let's write the actual file structure.

root ├───components ├───lib └───pages 

2. Mocking a real project's architecture with Redux

I want to make this demo as real as possible. So even though it will feel a bit contrived for our use case, I decided to use Redux with the React/Redux bindings.

Later I'll declare the store with products as the initial state but not any actions & reducers. This is only done so you get as close as possible to a real-life architecture.

Hop in the pages folder and create a _app.js file, which is a brand new addition to Next—it'll only work if you are using version 6 and higher.

It enables page transitions, error boundaries, and more. In my case, I'll use it to write a new App format that uses the React/Redux provider so that it injects the Redux store in my components.

Disclaimer: this architecture is highly inspired by this Next with Redux demo.

Here's the content of my file:

import App, {Container} from 'next/app' import React from 'react' import withReduxStore from '../lib/with-redux-store' import { Provider } from 'react-redux' class MyApp extends App { render () { const {Component, pageProps, reduxStore} = this.props return ( <Container> <Provider store={reduxStore}> <div id="main"> <h1 className="title">Peaky Blinders’ Store</h1> <Component {...pageProps} /> <div> <p> SEO-friendly Next.js app with a <a href="https://snipcart.com/">Snipcart</a> powered store. <a href="https://github.com/snipcart/next-snipcart">[See the code]</a> <a href="https://snipcart.com/blog/react-seo-nextjs-tutorial">[Read full tutorial]</a> </p> </div> </div> </Provider> </Container> ) } } export default withReduxStore(MyApp) 

As you can see, I provide the store to my Provider and relay the current page props to the current component.

I don't export the component directly but I call withReduxStore with MyApp as a parameter instead. You'll have to craft this function as it doesn't exist at the moment.

This is definitely the most complicated function. For the purpose of this post, I won't explain it thoroughly as it's a little bit more advanced, and the complex section only serves if you were to use server-side rendering. As I'll generate static assets, it should all be okay.

So, hop in your /lib folder and create a with-redux-store.js file with the following content:

import App from 'next/app' import {initializeStore} from '../store' const isServer = typeof window === 'undefined' const NEXTREDUXSTORE = 'NEXTREDUXSTORE' function getOrCreateStore(initialState) { // Always make a new store if server, otherwise state is shared between requests if (isServer) { return initializeStore(initialState) } // Store in global variable if client if (!window[NEXTREDUXSTORE]) { window[NEXTREDUXSTORE] = initializeStore(initialState) } return window[NEXTREDUXSTORE] } export default (App) => { return class Redux extends React.Component { static async getInitialProps (appContext) { const reduxStore = getOrCreateStore() // Provide the store to getInitialProps of pages appContext.ctx.reduxStore = reduxStore let appProps = {} if (App.getInitialProps) { appProps = await App.getInitialProps(appContext) } return { ...appProps, initialReduxState: reduxStore.getState() } } constructor(props) { super(props) this.reduxStore = getOrCreateStore(props.initialReduxState) } render() { return <App {...this.props} reduxStore={this.reduxStore} /> } } } 

Basically, this checks out if the app is running on a server or in the browser, then decides whether to serve a new Redux store instance or the current one. Once it's determined, I give it to the App component as a prop.

This'll give you access to the store in each top-level component.

Import initializeStore, which is the last piece of ''data control'' needed before jumping in products. Make a store.js file directly in the root folder.

import { createStore } from 'redux' export const actionTypes = {} const initialState = { products: [ { name: 'My first product', price: 50, description: 'I like turtles', image: 'url', id: 1 },{ name: 'My second product', price: 100, description: 'I like zonks', image: 'url', id: 2 },{ name: 'My third product', price: 150, description: 'I like dragons', image: 'url', id: 3 } ] } // REDUCERS export const reducer = (state = initialState, action) => { switch (action.type) { default: return state } } export function initializeStore (initialState = initialState) { return createStore(reducer, initialState) } 

As mentioned earlier, the store is bare bones. It really just instantiates with an initial state, but doesn't provide anything else. You'll still be placing products there, as it gives a realistic feel of the way to access data in the components I'll define in the next step.

3. Writing and rendering products

For this demo, I want two different components, a products.js that will render a link to each product, and a product.js that will show each product details.

Write each of these in the components/ folder.

The products one will be a bit more complex as it needs to access the Redux store, but they both remain simple as they are functional components. They only render what they are given as props, thus they can be solely represented as a function without extending anything.

Here's the products' component:

import Link from 'next/link' const ProductLink = (props) => ( <div className="product"> <Link as={/product/${props.id}} href={/product?id=${props.id}}> <a> <img src={props.image} alt={props.name} height='250' className="thumbnail"/> <p>{ props.description }</p> <p>{props.name}</p> </a> </Link> </div>) var Products = ({ products }) => ( <div> <div className="products"> { products.map(props => ( <ProductLink key={props.id} {...props}/> )) } </div> </div>) export default Products 

And now here's product.js:

export default (props) => (f <div className="product"> <a className="product" href={props.url }> <img src={props.image} alt={props.name} className="thumbnail"/> <p>{props.name}</p> </a> <button className="snipcart-add-item" data-item-name={props.name} data-item-id={props.id} data-item-image={props.image} data-item-url='/' data-item-price={props.price}> Buy it for {props.price} $ </button> </div>) 

Now that you have these two components, you need to use them in routes so they can render data.

To do so, generate two new files in the pages folder, index.js and product.js.

The first one goes as follows:

import React from 'react' import {connect} from 'react-redux' import Products from '../components/products' import Head from 'next/head' class Index extends React.Component { render () { return ( <div> <Head> <link href="/static/main.css" rel="stylesheet" /> <meta name="title" content="Peaky Blinder's e-commerce" /> <meta name="description" content='Find the best Peaky Blinders products online.' /> </Head> <Products {...this.props}/> </div> ) } } const mapStateToProps = (state) => ({products: state.products}) export default connect(mapStateToProps)(Index) 

What React/Redux does is, instead of letting you access the Redux store directly, it gives you a connect function giving you a way to map a state part to a prop. Although not done here, it can also give you a way of mapping a dispatching function to a prop.

This isolates entirely the logic from the presentation layer. This really is an interesting way of doing things and heavily influenced by functional programming, as you can easily split all the dependencies of a component directly in its props.

If you want to read more about this there's a neat article here that introduces these concepts.

With this principle in mind, define the second file as:

import React from 'react' import {connect} from 'react-redux' import ProductComp from '../components/product' import Head from 'next/head' class Product extends React.Component { static getInitialProps = ({query}) => ({id: query.id}) getProduct = () => (this.props.products.filter(x => x.id == this.props.id)[0]) render = () => ( <div> <Head> <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script> <script src="https://cdn.snipcart.com/scripts/2.0/snipcart.js" data-api-key="YjdiNWIyOTUtZTIyMy00MWMwLTkwNDUtMzI1M2M2NTgxYjE0" id="snipcart"></script> <link href="https://cdn.snipcart.com/themes/2.0/base/snipcart.min.css" rel="stylesheet" type="text/css" /> <link href="/static/main.css" rel="stylesheet" /> <meta name="title" content={"Peaky Blinder's " + this.getProduct().name} /> <meta name="description" content={this.getProduct().description} /> </Head> <a href="/">go back to home</a> <ProductComp {...(this.getProduct())}/> </div> ); } const mapStateToProps = (state) => ({products: state.products}) export default connect(mapStateToProps)(Product) 

This way, components stay "pure", meaning that they don't handle the filtering logic nor the fetching logic. They simply get data and show it.

I also added Snipcart's necessary scripts. Next.js' head component is a simple wrapper. So everything put in there will be bundled inside the head tag of the rendered page.

I also included some meta tags here. In this case, I used "title" and "description" which you should fill with researched keywords. Even if they won't appear on-page, they'll help crawlers understand the content of your page.

For SEO, it's also important to know about meta name="robots". You'll use it in bigger projects where you have internal pages accessible after login or any other page you don't want to get indexed. Read this to learn all its attributes.

4. Generating static files for React SEO

Since pages are created dynamically by using the Redux store data, you'll need to provide some paths to Next so it knows what routes to generate when creating the static files.

To do so, generate a next.config.js file directly in your route folder:

module.exports = { exportPathMap: function () { return { '/': { page: '/' }, '/product/1': { page: '/product', query: { id: "1" } }, '/product/2': { page: '/product', query: { id: "2" } }, '/product/3': { page: '/product', query: { id: "3" } } } } } 

I really like how simply it's handled. No need to hook up to the build process; a simple JS file lets you do it.

Sure, that was only three products with easy IDs to hard code here. But it wouldn't be difficult to create the returned object dynamically with some specified format, since this is a JavaScript file where you can use any logic.

Before deploying this to any third party, let's try it locally first.

Add the following scripts section to your package.json file:

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

Now run npm start in your project's root folder. This will start Next as a server and you should be able to access everything at http://localhost:3000.

If you want to generate your static files instead, you can do so using npm run export. This'll generate an out folder where you will find these. If you want to host these locally to test the output, you can do it quickly with an npm package such as serve.

5. Deploying the static assets

You're now ready to deploy the website. Let's use Netlify.

First, you'll need to push your code to a Git repo, then hop in Netlify's dashboard.

There, choose to create a new site with the following configuration:

netlify-next-build

It's worth noting that the creators of Next also have a deploy product called Now. It lets you deploy static assets after build-up directly from your terminal, with a single command. It's really neat, however since I'm already using a Git repo to show my code, I decided to stick with Netlify.

Live demo & GitHub repo

nextjs-demo

See the live demo here.

See the GitHub repo here.

Other important general SEO considerations

  1. Mobile-first indexing is now one of the primary ranking factors. So much so that you should take care of your mobile experience as much the desktop one—if not more!
  2. If you're still not aware of the importance of an HTTPS connexion, you should look into it right away. I'm hosting this demo on Netlify, which provides free SSL certificates with all plans.
  3. To up your SEO game, you'll want to craft great content. You also want to be able to easily edit and optimize it. For content editing purposes, consider throwing one of these headless CMS into the mix!
  4. Don't forget to add the appropriate meta tags, as we've seen earlier. A sitemap of your app pages is also very relevant here. You can find a great example on how to build a sitemap for Next.js projects here.

Closing thoughts

Playing with both Next.js and React was really fun. I didn't face any major challenges as everything was answered pretty straightforwardly in their docs, which is really thorough. I like the ''quiz" approach it has, original!

It took me around two hours to build the whole thing. It took a bit more time using the react-redux binding: I wandered for a while in their examples to clearly understand what was happening.

To push all of this further, it would be fun to put together a store with more products to generate the pathMap dynamically. I also wonder to which point the build process becomes bloated if you have too many routes to export. If you have the answer to this let me know in the comments!

With creativity, great coding and a thoughtful care for SEO nothing should stand in the way of your next projects!

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


Tag cloud