Technouz

SEO Hacking with Next.js

By Zahid Mahmood / 21 October 2020

I wanted to build a really straight-forward website using technologies I already knew to deliver a responsive experience to users, but also a laser-focus on SEO. After considering Nuxt.js, Gatsby.js and Jekyll; I settled on Next.js by Vercel because of it's support for SSR (server-side rendering) and SG (static generation) through a very pleasant developer experience.

I was sold the moment I learnt that Next.js only requires a function called getStaticProps() to be exported to support both SSR and SG.

So, I fired up VS Code on Friday evening and created by my first Next.js application with the command npx create-next-app. The website was live on Monday.

The Objective

The goal was to build a super-simple, SEO-focused website for Halal Joints as an exposure and marketing tool. Most food bloggers in the Halal food scene get much of their traffic for terms like "is Taco Bell in Colindale Halal", so I decided to build a simple site that would target terms like this.

It was important that I could use the same backend API the React Native mobile app was already using. I wanted to build something that could extract all 750 restaurants (in October 2020) from the API, build static pages and then publish it to either Heroku, Netlify, Render or AWS. I wanted to easily leverage meta tags, SEO schemas and sitemaps so that every time the project was re-deployed, it would automatically pick up new restaurants and publish them to the website along with an updated sitemap.

A bonus would have been a sprinkle of traditional React magic to the website so I could also build a simple Discover screen whereby users can search for restaurants too.

TLDR: Next.js is awesome but disappointingly lacks support for schemas and sitemaps

In case you came to this article for a quick answer, here it is…

Next.js let me do everything I wanted to above, but the biggest difficulty was automatically generating sitemaps on the fly. My primary aim for this project was to leverage SEO, so I was somewhat disappointed I had to roll my own solution for sitemaps.

In addition, there was no out-of-the-box support for robots.txt or JSON schemas.

One more thing bubbling in the back of my mind is how many SG pages Next.js will be able to handle. At approximately 760 total pages, the framework handles my website with ease. But if Halal Joints ever does cover multiple cities, it will need to generate up to 600,000 static pages per deployment. This is something I'll need to read in to a little more - perhaps incremental static generation?

Creating pages in Next.js

Next.js brings some of the framework rigidity developers experienced in Angular may enjoy. For example, the framework guides you to create React components within the /pages directory, which are then automagically available at the corresponding route. This made it pretty easy to setup initial pages for the website without having to think too much about file structure and scaling the website.

Creating dynamic pages in Next.js

Dynamic pages are also equally as joyous to work with. For the restaurant pages I created a file called /pages/restaurant/[slug].tsx where the slug is a route parameter that can be accessed in the code.

One disappointment I experienced here was that I couldn't mix the slug with some static text. For example, I wanted to follow the pattern JustEat uses with its restaurant listings and have /pages/restaurant-[slug].tsx. Not the biggest issue in the world, but it would have been cool if the framework handled it like a boss.

Statically Generating dynamic pages

This bit was easy. Inside the /pages/restaurant/[slug].tsx file, I had to ensure I exported a function called getStaticPaths() as well as the React component that rendered the page. As described in the documentation, method must return an object that describes all paths this particular page must be pre-rendered for.

export async function getStaticPaths() {
    return {
        paths: [
            ...
            { params: { slug: "uniqueid-restaurant-name-and-location" } }
            ...
        ],
        fallback: false
  };
}

Since we're generating pages beforehand, the next export command runs through all the routes provided by getStaticPaths() and creates a HTML output for each and every page.

When the page is eventually served through either SG or SSR, Next.js also requires a function called getStaticProps() to be exported from the same file. This function is executed to resolve any static properties required for the main component which renders the page.

In this function, I simply extracted the slug generated in getStaticPaths() and used the unique to query the API to get details about each restaurant. These details were then passed in to the main component as a React prop.

export async function getStaticProps(context) {
    const { params } = context;
    const { slug } = params;
    const unique = slug.split("-")[0];

    const { data } = await api.get(`/restaurants/${unique}`);

    return {
        props: {
            restaurant: data
        },
    }
}

With the restaurant data now available as a prop to the component which renders the page, I can design the screen like any other component in React.

Page title and meta

Another super cool feature of Next.js is the ability to overwrite the <title> element on a page-by-page basis. With the restaurant details now available as a React prop in the component code, it was simple to construct appropriate page titles as shown below:

const Restaurant = ({ restaurant }) => {
    return (
        <>
            <Head>
                <title>{`${restaurant.name} ${restaurant.city} | Halal Joints</title>
                <meta name="description" content={restaurant.description}`} />
                <meta name="robots" content="index, follow" />
            </Head>
            ...

JSON schema

JSON schemas are completely new to me and I am using them for the first time. Since Halal Joints is a discovery platform, I figured it would be useful to ensure I translate the content of a page in to something search engine crawlers can understand. When I inspected the source code for OpenTable, Zomato, JustEat and Deliveroo I found them also to be using schemas - great minds think alike!

My implementation for schemas certainly leaves plenty of room for improvement. But looking at Google's Webmaster console it seems to be working well thus far.

To implement the schema I simply created a function which returned a JSX element containing the data to be printed within the <Head> tags:

return (
    <script type='application/ld+json'>
        {`{
            "@context": "http://www.schema.org",
            "@type": "Restaurant",
            "name": "${restaurant.name}",
            "url": "${slug}",
            "sameAs": [
                "${restaurant.website}"
            ],
            "image": "${restaurant.thumbUrl}",
            "description": "${restaurant.description}",
        }`}
    </script>
);

Sitemaps

The hardest part! I decided to deploy to Netlify which required two commands to be run upon deployment: npm run build && npm run export. I then configured my Netlify project to serve from the /out directory which is where the export command creates the SG code.

Next, I created a new script within my package.json file called postexport. Apparently prefixing a command with 'post' makes it automatically run after the command is executed with npm. I did not know this - neat!

...
"postexport": "node scripts/post-export.script.js"
...

And within the post-export-script.js file I wrote a super hacky script which creates a sitemap.xml file based on data from the API. It essentially looks like this:

axios.get(`${API_SOURCE}/restaurants`)
.then(res => {
    let { results } = res.data;

    results.forEach((restaurant, index) => {
        const slug = generateRestaurantSlug(restaurant);
        const modDate = new Date(restaurant.updatedAt);
        const lastMod = `${modDate.getFullYear()}-${("0" + (modDate.getMonth() + 1)).slice(-2)}-${("0" + modDate.getDate()).slice(-2)}`;

        xml += "";
        xml += `${SITE_ROOT}/restaurant/${slug}`;
        xml += `daily0.9${lastMod}`;
    });

    xml += "";

    fs.writeFileSync("out/sitemap.xml", xml);

    return xml;
})
.catch(error => {
    console.log(error.message, error.name);
});

It's still a bit hacky and messy, so I'll come back and update this post once I've cleaned it up.

Thanks for reading!

My name is Zahid Mahmood, and I'm one of the founders of Anterior. I started this technology blog when I was in high school and grew it to over 100,000 readers before becoming occupied with other projects. I've recently started writing again and will be posting more frequently.