Creating a dynamic sitemap with Ghost & Next.js for ultimate SEO benefits
Tags: ghost, nextjs, react, javascript
Published: 2019-03-09 13:50:40

---
title: Creating a dynamic sitemap with Ghost & Next.js for ultimate SEO benefits
published: true
description: Sitemaps are imperative for adequate SEO. I'm going to show you how to create a dynamic sitemap for ghost that's integrated with Next.js
tags: ghost, nextjs, react, javascript
cover_image: https://i.imgur.com/Bjm6ryB.png
---

There are 2 titans in the blogging platform world. Wordpress owns the majority of the market, but Ghost ([https://ghost.org/](https://ghost.org/)) is just beautiful. Traditionally, most create themes for these platforms using their internal rendering engines, but we went a different route.

We use React for everything at Creators Never Die, and wanted to continue that pattern for our own site. Obviously, running a blog requires great SEO practices - which out of the box, React doesn't do well. Most search engine bots just scrape HTML, although I've heard Google is able to render React sites properly. Instead of taking that chance, there is a great framework called Next.js. Without explaining the nuances this wonderful framework brings, their main selling point is that they handle rendering React on the server.

After finishing our site, an issue arisen. We needed a dynamic sitemap! Most blogging platforms offer this solution, but only if we use their templating language. Since we are using Next.js, we had to handle creating our sitemap ourselves. I'm going to show you how we did this.

Next.js offers the ability to customize server routes using any node backend framework you like. For this example, we're going to use express, but you can use whatever you like.

We're going to assume you have Next.js installed. Install express & the official Ghost Javascript SDK:

    npm install --save express @tryghost/content-api

Next, create a generateSitemap.js file. We're going to run this script whenver the /sitemap.xml route is hit. Well get to routes later in this post. 

Inside the file, we're first going to initiate the Ghost SDK. To do this, well need to supply the URL to your Ghost blog & the API token you'll get from your admin panel. Goto the Integrations tab, and create a new custom Integration. This is where you'll find your API Key.

![](https://i.imgur.com/Bjm6ryB.png)

Copy the Content API key, and add this to your new generateSitemap.js file (It's recommended to use a .env file):

```javascript
    const GhostContentAPI = require('@tryghost/content-api')
    const api = new GhostContentAPI({
      host: http://ghostblogurl.com,
      key: abcdefghijklmnopqrstuvwxyz,
      version: 'v2'
    });
```

Now we're going to create a function that returns a Promise of all the posts in your Ghost backend:
```javascript
    const getPosts = () => new Promise((resolve, reject) => {
      api.posts.browse().then((data) => {
        resolve(data)
      })
    })
```

And finally, an async function that will actually create the XML structure. Notice the line that supplies the URL:
```javascript
    const createSitemap = async() => {
    
      let xml = ''
      xml += '<?xml version="1.0" encoding="UTF-8"?>'
      xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
    
    
      await getPosts().then((_newData) => {
        _newData.map((_post) => {
          xml += '<url>'
          xml += `<loc>${SITE_ROOT}/blog/item?q=${_post.slug}</loc>`
          xml += `<lastmod>${_post.updated_at}</lastmod>`
          xml += `<changefreq>always</changefreq>`
          xml += `<priority>0.5</priority>`
          xml += '</url>'
        })
      })
    
      xml += '</urlset>'
    
      console.log(`Wrote Sitemap`);
      return xml;
    
    }
    
    module.exports = createSitemap
```
Make sure the url follows how you have Next.js setup. In our case, we have blog folder within the pages directory. `pages > blog > item.js`
```javascript
    xml += `<loc>${SITE_ROOT}/blog/item?q=${_post.slug}</loc>`
```
Wont get into detail in this post, but we basically are using the same concept in the getPosts() function above, but supply the slug parsed from the url. Here is an example:
```javascript
    const posts = await api.posts.read({slug: `${query.q}`}, {include: 'tags,authors'}, {formats: ['html']});
```
The complete generateSitemap.js file should look like this (I've added dotenv package to handle parsing the .env file):
```javascript
    require('dotenv').config()
    
    const GhostContentAPI = require('@tryghost/content-api')
    const api = new GhostContentAPI({
      host: process.env.GHOST_API,
      key: process.env.GHOST_TOKEN,
      version: 'v2'
    });
    
    
    
    const SITE_ROOT = process.env.SITE_ROOT || 'https://creatorsneverdie.com'
    
    
    const getPosts = () => new Promise((resolve, reject) => {
      api.posts.browse().then((data) => {
        resolve(data)
      })
    })
    
    
    const createSitemap = async() => {
    
      let xml = ''
      xml += '<?xml version="1.0" encoding="UTF-8"?>'
      xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
    
    
      await getPosts().then((_newData) => {
        _newData.map((_post) => {
          xml += '<url>'
          xml += `<loc>${SITE_ROOT}/blog/item?q=${_post.slug}</loc>`
          xml += `<lastmod>${_post.updated_at}</lastmod>`
          xml += `<changefreq>always</changefreq>`
          xml += `<priority>0.5</priority>`
          xml += '</url>'
        })
      })
    
      xml += '</urlset>'
    
      console.log(`Wrote Sitemap`);
      return xml;
    
    
    }
    
    
    module.exports = createSitemap
```
All that's left is creating the custom routes. Create a server.js file in the root of your directory. We're going to require all the necessary packages, and create a SITEMAP variable to store the XML content within the session:
```javascript
    const express = require('express');
    const next = require('next');
    const port = parseInt(process.env.PORT, 10) || 3000;
    const dev = process.env.NODE_ENV !== 'production';
    const app = next({ dev });
    const handle = app.getRequestHandler();
    
    
    const genSitemap = require('./lib/generateSitemap')
    let SITEMAP = null
```
Well then prepare Next.js and initiate the express server:
```javascript
    app.prepare()
      .then(() => {
        const server = express();
        
        server.get('*', (req, res) => handle(req, res));
    
        server.listen(port, (err) => {
          if (err) throw err;
          console.log(`> Ready on http://localhost:${port}`);
        });
      });
```
We need 2 routes. One to view the sitemap, and one to generate the sitemap whenever a new post is made, edited or deleted. To accomplish this, Ghost allows you to create a Webhook. First let's create the Webhook in the Ghost backend. Navigate to the same location you found your Content API Key, press "Add Webhook" and supply the following values (replacing our domain with yours):

![](https://i.imgur.com/5NcZ79c.png)

Now back to the server.js file, well add the 2 routes. A GET route & POST route:
```javascript
    server.get('/sitemap.xml', async (req,res) => {
    	 if(!SITEMAP) {
    		 SITEMAP = await genSitemap();
       } 
    
       res.set('Content-Type', 'text/xml');
       res.send(SITEMAP);
    })
    
    server.post('/createSitemap', async (req, res, next) => {
      SITEMAP = await genSitemap()
    	res.status(200).send(SITEMAP)
    })
```

In the GET request, we check if the SITEMAP variable is empty. If it's empty, we call the genSitemap() function we created in the generateSitemap.js file. This will return the XML file and store in the SITEMAP variable. Same concept applies to the POST request, which gets called whenever a post is created or modified. Your server.js file should look like this:
```javascript
    const express = require('express');
    const next = require('next');
    const port = parseInt(process.env.PORT, 10) || 3000;
    const dev = process.env.NODE_ENV !== 'production';
    const app = next({ dev });
    const handle = app.getRequestHandler();
    
    
    const genSitemap = require('./lib/generateSitemap')
    let SITEMAP = null
    
    
    app.prepare()
      .then(() => {
        const server = express();
        
        server.get('/sitemap.xml', async (req,res) => {
          if(!SITEMAP) {
            SITEMAP = await genSitemap();
          } 
    
          res.set('Content-Type', 'text/xml');
          res.send(SITEMAP);
        })
    
        server.post('/createSitemap', async (req, res, next) => {
          SITEMAP = await genSitemap()
          res.status(200).send(SITEMAP)
        })
    
    
        server.get('*', (req, res) => handle(req, res));
    
        server.listen(port, (err) => {
          if (err) throw err;
          console.log(`> Ready on http://localhost:${port}`);
        });
      });
```
And now if you goto /sitemap.xml you'll see the following:

![](https://i.imgur.com/eD8rLph.png)

Try creating a new post, and watch the /sitemap.xml automatically update!

If you would be so kind and help me build my clout on twitter [@dillonraphael](https://twitter.com/dillonraphael). Feel free to ask me questions.