Skip to content

Commit

Permalink
Merge pull request #347 from zackproser/post-next-js-sitemap
Browse files Browse the repository at this point in the history
Add post: how to build a Next.js sitemap
  • Loading branch information
zackproser authored Feb 12, 2024
2 parents f25cc43 + 9726221 commit 25ab8cf
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 0 deletions.
129 changes: 129 additions & 0 deletions src/app/blog/how-to-next-js-sitemap/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { ArticleLayout } from '@/components/ArticleLayout'
import { Button } from '@/components/Button'
import Image from 'next/image'
import Link from 'next/link'
import nextJsSitemap from '@/images/next-js-sitemap.webp'
import googleSearchConsole from '@/images/sitemap-google-search.webp'

import { createMetadata } from '@/utils/createMetadata'

export const metadata = createMetadata({
author: "Zachary Proser",
date: "2024-2-12",
title: "How to build a dynamic sitemap for your Next.js project",
description: "Steal my implementation to save yourself some time and headaches",
image: nextJsSitemap
})

export default (props) => <ArticleLayout metadata={metadata} {...props} />

---

<Image src={metadata.image} alt="How to build a dynamic sitemap for Next.js"/>

At the time of this writing, there **are** [official docs on building a sitemap for your Next.js
project](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap) that uses the newer app router pattern.

However, they're on the sparse side, and there's more than a little confusion going around about
how to pull this off based on the npm packages and GitHub issues I encountered when doing
my own research.

When you're finished this post, you'll have the knowledge and code you need to build a dynamic sitemap
for your Next.js project that includes both 'static' routes such as `/about` or `/home`
and dynamic routes such as `/blog/[id]/page.tsx` or `/posts/[slug]/page.js`.

You can also feel free to skip ahead to the implementation below. This is what I used to get a working sitemap that
included all the routes I cared about and, more importantly, was accepted by Google Search's console / robots:

<Image src={googleSearchConsole} alt="Google Search Console says our sitemap is valid!"/>

## Key things to understand

Here's the things I needed to know upfront when I wanted to build my own sitemap:

### The Next.js file to route convention is supported

You can use the file-convention approach, meaning that a file that lives at `src/app/sitemap.(js|ts)`

Your file should export a default function - conventionally named `sitemap`. I've demonstrated this in the
implementation example below.

### You can test your sitemap locally
To test your sitemap, you can use curl like `curl http://localhost:3000/sitemap.xml > /tmp/sitemap.xml` and I recommend redirecting
the output to a file as shown so that you can examine it more closely.

At a minimum, all of the URLs you want search engines and other readers of sitemaps to be aware of need to be present in this file
and wrapped in `<loc>` or location tags.

If your sitemap is missing key URLs you're looking to get indexed, there's something wrong in your logic causing your routes to be excluded.

You can do print debugging and add logging statments throughout you sitemap file so that when you're running `npm run dev` and you curl
your sitemap, you can see the output in your terminal to help you diagnose what's going on.

## Next.js dynamic and static sitemap.xml example

The following implementation creates a sitemap out of [my portfolio project](https://github.com/zackproser/portfolio).

It works by defining the root directory of the project (`src/app`), and then reading all the directories it finds there, applying the following rules:

1. If the sub-directory is named `blog` or `videos`, it is a dynamic route, which needs to be recursively read for individual posts
2. If the sub-directory is not named `blog` or `videos`, it is a static or top-level route, and processing can stop after obtaining the entry name. This will be the case
for any top-level pages such as `/photos` or `/subscribe`, for example.

Note that if you're copy-pasting this into your own file, you need to update:
1. your `baseUrl` variable, since it's currently pointing to my domain
2. the names of your dynamic directories. For my current site version, I only have `/blog` and `/videos` set up as dynamic routes
3. Your excludeDirs. I added this because I didn't want my API routes that support backend functionality in my sitemap

```javascript
import fs from 'fs';
import path from 'path';

const baseUrl = process.env.SITE_URL || 'https://zackproser.com';
const baseDir = 'src/app';
const dynamicDirs = ['blog', 'videos']
const excludeDirs = ['api'];

function getRoutes() {
const fullPath = path.join(process.cwd(), baseDir);
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
let routes = [];

entries.forEach(entry => {
if (entry.isDirectory() && !excludeDirs.includes(entry.name)) {
// Handle 'static' routes at the baseDir
routes.push(`/${entry.name}`);

// Handle dynamic routes
if (dynamicDirs.includes(entry.name)) {
const subDir = path.join(fullPath, entry.name);
const subEntries = fs.readdirSync(subDir, { withFileTypes: true });

subEntries.forEach(subEntry => {
if (subEntry.isDirectory()) {
routes.push(`/${entry.name}/${subEntry.name}`);
}
});
}
}
});

return routes.map(route => ({
url: `${baseUrl}${route}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
}));
}

function sitemap() {
return getRoutes();
}

export default sitemap;
```

That's it! Save this code to your `src/app/sitemap.js` file and test it as described above.

I hope this was helpful, and if it was, please consider
subscribing to my newsletter below! 🙇
Binary file added src/images/next-js-sitemap.webp
Binary file not shown.
Binary file added src/images/sitemap-google-search.webp
Binary file not shown.

0 comments on commit 25ab8cf

Please sign in to comment.