Next.js and Tumblr as a CMS Part 3: Sitemap and RSS

If you’ve been following along with my series on using Tumblr as a Next.js CMS, last time we looked at fetching data and re-creating all the pages you’d expect to find on your average blog. That tutorial skipped two items that Tumblr normally handles: the sitemap and RSS feed. They require a little extra attention so in this post we’ll build on the earlier code to generate those files.

Sitemap

The basic Tumblr sitemap contains links to your homepage and each individual post. You can also add any other URLs you want to include (I like to throw in a list of featured tags) but for now we’ll focus on posts.

In the tumblr.js file from the last entry, reuse the CLIENT and getPosts code to create a method to return all posts:

export async function findAll (limit = 50) {
  const client = tumblr.createClient(CLIENT);
  const initialResponse = await getPosts(client, limit, 0);
  const totalPages = Math.floor(initialResponse.total_posts / limit);

  const posts = await [...Array(totalPages).keys()].reduce(async (arr, i) => {
    const response = await getPosts(client, limit * (i + 1));
    return [ ...(await arr), ...response.posts ];
  }, []);

  return { ...initialResponse, posts: [ ...initialResponse.posts, ...posts ] };
}

The Tumblr API documentation isn’t super clear when it comes to the number of posts you can retrieve in one request. It implies that 20 is the maximum but I haven’t found that to be the case in practice so, to make updating easier just in case, the findAll method accepts limit as a parameter.

Next, in the pages directory, add a new file called sitemap.xml.js. The file will return an empty React component and most of the action takes place in getServerSideProps where we’ll get the posts, use them to generate an XML string, and then switch the Content-Type to XML with setHeader.

import { findAll } from '../utils/tumblr';

const Sitemap = () => {};

export async function getServerSideProps ({ res }) {
  const response = await findAll();

  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 <a href="http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd</a>">
      <url>
        <loc>${process.env.NEXT_PUBLIC_BASE_URL}</loc>
        <changefreq>weekly</changefreq>
        <lastmod>${new Date().toISOString().substring(0, 10)}</lastmod>
      </url>

      ${response.posts.map((post) => `
        <url>
          <loc>${process.env.NEXT_PUBLIC_BASE_URL}${new URL(post.post_url).pathname}</loc>
          <lastmod>${new Date(post.date).toISOString().substring(0, 10)}</lastmod>
        </url>
      `).join('')}
    </urlset>
  `;

  res.setHeader('Content-Type', 'text/xml');
  res.write(sitemap);
  res.end();

  return {
    props: {}
  };
}

One thing to note, I store my blog’s base URL as an environmental variable so you’ll need to either replace NEXT_PUBLIC_BASE_URL above or add it to your .env files to get this working.

RSS

For the RSS feed, we’ll be using the Feed package to handle the data formatting. You can install it with:

yarn add feed

Create a directory called rss inside pages and then add an index.js file inside of it. By default, the Tumblr feed shows the ten most recent posts so we can use the same find method we used before for pages.

The file for the RSS feed will look pretty similar to the sitemap. The main difference is that instead of using string interpolation to generate XML we’ll follow the example from the Feed docs:

import { Feed } from 'feed';
import { find } from '../../utils/tumblr';

const Rss = () => {};

export async function getServerSideProps ({ res }) {
  const response = await find();

  const feed = new Feed({
    title: 'Your Tumblr Title',
    description: 'Your Tumblr description.',
    link: process.env.NEXT_PUBLIC_BASE_URL
  });

  response.posts.forEach((post) => {
    feed.addItem({
      title: post.title || post.summary,
      description: `${post.type === 'photo' ? post.photos.map((photo) => `<img src=${photo.original_size.url} /><br /><br />`) : ''}${post.type === 'video' ? `<iframe width="700" height="383" src="https://www.youtube.com/embed/${post.video.youtube.video_id}" frameborder="0" /><br /><br />` : ''}${post.type === 'link' ? `<a href=${post.url}>${post.title}</a>: ` : ''}${post.type === 'answer' ? `${post.question}: ` : ''}${post.trail[0].content_raw}`,
      link: `${process.env.NEXT_PUBLIC_BASE_URL}${new URL(post.post_url).pathname}`,
      pubDate: new Date(post.date).toISOString().substring(0, 10),
      category: post.tags.map((tag) => { return { name: tag }; })
    });
  });

  res.setHeader('Content-Type', 'text/xml');
  res.write(feed.rss2());
  res.end();

  return {
    props: {}
  };
}

export default Rss;

To match the Tumblr provided feed, I added some post type specific intros before the raw content in the description text.


That should cover sitemaps and RSS. My next post will dig into some of the bells and whistles that inspired the move to a separate app in the first place like code syntax highlighting and custom social sharing images.

Comments