Tech

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

by Dillon RaphaelAug 20 2019

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

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'
});

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

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

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>`

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']});

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

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

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}`);
    });
  });

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

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}`);
    });
  });

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

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

Recommended Posts

signature

© 2019 Creators Never Die. All Right Reserved. Made with Magic World Wide

About Creators Never Die

Creators Never Die. Is a publication that focuses on content for creators.