diff --git a/src/app/blog/how-to-next-js-sitemap/page.mdx b/src/app/blog/how-to-next-js-sitemap/page.mdx new file mode 100644 index 00000000..62d6b7c2 --- /dev/null +++ b/src/app/blog/how-to-next-js-sitemap/page.mdx @@ -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) => + +--- + +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: + +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 `` 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! 🙇 diff --git a/src/images/next-js-sitemap.webp b/src/images/next-js-sitemap.webp new file mode 100644 index 00000000..fe503156 Binary files /dev/null and b/src/images/next-js-sitemap.webp differ diff --git a/src/images/sitemap-google-search.webp b/src/images/sitemap-google-search.webp new file mode 100644 index 00000000..8734f865 Binary files /dev/null and b/src/images/sitemap-google-search.webp differ