Next.js and Tumblr as a CMS Part 3: Sitemap and RSS
Tags: javascript, nextjs
Published: 2022-06-02 22:06:05

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](https://blog.laurenashpole.com/post/685261044028981248/nextjs-tumblr-cms-data-fetching). 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:

```js
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](https://www.tumblr.com/docs/en/api/v2) 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`. 

```js
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 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
      <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](https://github.com/jpmonette/feed) package to handle the data formatting. You can install it with:

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

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