-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #347 from zackproser/post-next-js-sitemap
Add post: how to build a Next.js sitemap
- Loading branch information
Showing
3 changed files
with
129 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Binary file not shown.