DEV Community

Cover image for Creating a dynamic sitemap with Ghost & Next.js for ultimate SEO benefits
Dillon Raphael
Dillon Raphael

Posted on

Creating a dynamic sitemap with Ghost & Next.js for ultimate SEO benefits

There are 2 titans in the blogging platform world. Wordpress owns the majority of the market, but Ghost (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
Enter fullscreen mode Exit fullscreen mode

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.

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

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

Now we're going to create a function that returns a Promise of all the posts in your Ghost backend:

    const getPosts = () => new Promise((resolve, reject) => {
      api.posts.browse().then((data) => {
        resolve(data)
      })
    })
Enter fullscreen mode Exit fullscreen mode

And finally, an async function that will actually create the XML structure. Notice the line that supplies the URL:

    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
Enter fullscreen mode Exit fullscreen mode

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

    xml += `<loc>${SITE_ROOT}/blog/item?q=${_post.slug}</loc>`
Enter fullscreen mode Exit fullscreen mode

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:

    const posts = await api.posts.read({slug: `${query.q}`}, {include: 'tags,authors'}, {formats: ['html']});
Enter fullscreen mode Exit fullscreen mode

The complete generateSitemap.js file should look like this (I've added dotenv package to handle parsing the .env file):

    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
Enter fullscreen mode Exit fullscreen mode

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:

    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
Enter fullscreen mode Exit fullscreen mode

Well then prepare Next.js and initiate the express server:

    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}`);
        });
      });
Enter fullscreen mode Exit fullscreen mode

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):

Now back to the server.js file, well add the 2 routes. A GET route & POST route:

    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)
    })
Enter fullscreen mode Exit fullscreen mode

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:

    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}`);
        });
      });
Enter fullscreen mode Exit fullscreen mode

And now if you goto /sitemap.xml you'll see the following:

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. Feel free to ask me questions.

Top comments (1)

Collapse
 
styxlab profile image
Joost Jansky

Next.js + headless Ghost + vercel is a powerful combination that we also use for our Jamify Blog Starter. Source code can be found on Github: next-cms-ghost.