# Optimizing Your Next.js Sitemap with next-sitemap: A Complete Guide

Created: 2025-06-23

Updated: 2025-06-27

13 min read

Next.js

SEO

Web Development

Performance

## Table of Contents

* [Introduction](#introduction)
* [Why Sitemaps Matter for SEO](#why-sitemaps-matter-for-seo)
* [The Problem with Default Configurations](#the-problem-with-default-configurations)
* [My Optimized Configuration](#my-optimized-configuration)
* [Key Configuration Decisions](#key-configuration-decisions)
* [1\. Comprehensive Exclusions](#1-comprehensive-exclusions)
* [2\. Data-Driven Priority System with Critical Bug Fix](#2-data-driven-priority-system-with-critical-bug-fix)
* [3\. Robust Dynamic Content Handling with Error Recovery](#3-robust-dynamic-content-handling-with-error-recovery)
* [4\. Enhanced robots.txt](#4-enhanced-robotstxt)
* [Results and Benefits](#results-and-benefits)
* [SEO Benefits](#seo-benefits)
* [Performance Benefits](#performance-benefits)
* [Common Pitfalls to Avoid](#common-pitfalls-to-avoid)
* [1\. Including Build Artifacts](#1-including-build-artifacts)
* [2\. Ignoring Dynamic Content](#2-ignoring-dynamic-content)
* [3\. Wrong Priorities](#3-wrong-priorities)
* [4\. Missing Error Handling](#4-missing-error-handling)
* [5\. CRITICAL: Root Path Matching Bug](#5-critical-root-path-matching-bug)
* [6\. Regex Issues with Apostrophes](#6-regex-issues-with-apostrophes)
* [7\. Inconsistent Slug Generation](#7-inconsistent-slug-generation)
* [Advanced Tips](#advanced-tips)
* [1\. Conditional Content Inclusion](#1-conditional-content-inclusion)
* [2\. Custom Change Frequencies](#2-custom-change-frequencies)
* [Monitoring and Maintenance](#monitoring-and-maintenance)
* [1\. Google Search Console](#1-google-search-console)
* [2\. Regular Audits](#2-regular-audits)
* [3\. Automated Testing](#3-automated-testing)
* [Debugging and Testing Your Sitemap](#debugging-and-testing-your-sitemap)
* [1\. Test Priority Assignments](#1-test-priority-assignments)
* [2\. Check for Duplicate Entries](#2-check-for-duplicate-entries)
* [3\. Validate Content Sources](#3-validate-content-sources)
* [4\. Manual Sitemap Inspection](#4-manual-sitemap-inspection)
* [5\. Unit Testing Your Configuration](#5-unit-testing-your-configuration)
* [Conclusion](#conclusion)
* [Resources](#resources)

## [Introduction](#introduction)

A well-configured sitemap is crucial for SEO success, helping search engines discover and index your content efficiently. However, many Next.js websites have poorly optimized sitemaps that include build artifacts, static assets, and other files that shouldn't be indexed.

In this post, I'll walk you through optimizing your Next.js sitemap using `next-sitemap`, sharing the exact configuration I use for this website and the reasoning behind each decision.

## [Why Sitemaps Matter for SEO](#why-sitemaps-matter-for-seo)

Before diving into the technical details, let's understand why sitemaps are essential:

* **Content Discovery**: Help search engines find all your pages, especially dynamic content
* **Crawl Efficiency**: Guide crawlers to prioritize important content
* **Metadata Communication**: Provide information about page importance, update frequency, and modification dates
* **Performance**: Reduce server load by preventing crawlers from accessing unnecessary files

## [The Problem with Default Configurations](#the-problem-with-default-configurations)

Most Next.js websites using `next-sitemap` start with a basic configuration like this:

JavaScript

Copy

```javascript
/** @type {import('next-sitemap').IConfig} */
module.exports = {
    siteUrl: 'https://example.com',
    generateRobotsTxt: true,
};
```

While this works, it often results in sitemaps that include:

* Next.js build artifacts (`/_next/static/chunks/...`)
* Image files and static assets
* API routes that shouldn't be indexed
* Component files and internal routes

## [My Optimized Configuration](#my-optimized-configuration)

Here's the complete `next-sitemap.config.js` configuration I use for this website:

JavaScript

Copy

```javascript
/** @type {import('next-sitemap').IConfig} */
module.exports = {
    siteUrl: 'https://www.yiminyang.dev',
    generateIndexSitemap: false,
    generateRobotsTxt: true,
    exclude: [
        '/blocked',
        '/blocked/*',
        '/api/*',
        '/_next/*', // Next.js build artifacts
        '/static/*', // Static assets
        '*.js', // JavaScript files
        '*.css', // CSS files
        '*.map', // Source maps
        '*.json', // JSON files (manifests, etc.)
        '*.ico', // Favicon files
        '*.png', // Image files
        '*.jpg', // Image files
        '*.jpeg', // Image files
        '*.gif', // Image files
        '*.svg', // SVG files (unless they're pages)
        '*.webp', // Image files
        '/playground/games/memory-card-game/memorycardgame', // Component file, not page
        '/playground/games/whack-a-mole/whack-a-mole', // Component file, not page
        '/playground/text-transformations/*/[A-Z]*', // Component files (capitalized)
        '/playground/tools/qr-code-generator/[content]/*', // Dynamic route internals
    ],
    robotsTxtOptions: {
        policies: [
            {
                userAgent: '*',
                disallow: ['/blocked', '/api', '/_next', '/static'],
                allow: ['/playground', '/blog', '/talks'],
            },
        ],
        additionalSitemaps: ['https://www.yiminyang.dev/sitemap.xml'],
    },
    changefreq: 'weekly',
    priority: 0.7,
    sitemapSize: 5000,
    // Custom transformation for specific pages
    transform: async (config, path) => {
        const pathPriorities = {
            main: { paths: ['/', '/about', '/blog', '/talks'], priority: 1.0, changefreq: 'weekly' },
            content: { paths: ['/blog/', '/talks/'], priority: 0.8, changefreq: 'monthly' },
            playground: { paths: ['/playground/'], priority: 0.6, changefreq: 'monthly' },
        };

        for (const [, { paths, priority, changefreq }] of Object.entries(pathPriorities)) {
            if (
                paths.some((p) => {
                    if (p === '/') {
                        // Special case: root path only matches exactly
                        return path === '/';
                    }
                    return path === p || (p.endsWith('/') && path.startsWith(p));
                })
            ) {
                return {
                    loc: path,
                    changefreq,
                    priority,
                    lastmod: new Date().toISOString(),
                };
            }
        }

        return {
            loc: path,
            changefreq: config.changefreq,
            priority: config.priority,
            lastmod: new Date().toISOString(),
        };
    },
    additionalPaths: async () => {
        const fs = require('fs');
        const path = require('path');
        const result = [];

        // Helper function to safely process files
        const safeProcess = async (description, processor) => {
            try {
                await processor();
            } catch (error) {
                console.warn(`Could not load ${description} for sitemap:`, error.message);
            }
        };

        // Add blog posts
        await safeProcess('blog posts', () => {
            const matter = require('gray-matter');
            const postsDir = path.join(process.cwd(), 'public', 'content', 'blog', 'posts');

            if (!fs.existsSync(postsDir)) return;

            fs.readdirSync(postsDir)
                .filter((file) => file.endsWith('.mdx'))
                .forEach((file) => {
                    try {
                        const filePath = path.join(postsDir, file);
                        const { data } = matter(fs.readFileSync(filePath, 'utf8'));

                        if (data.publish !== false) {
                            const slug = file.replace(/\.mdx$/, '').toLowerCase();
                            result.push({
                                loc: `/blog/${slug}`,
                                changefreq: 'monthly',
                                priority: 0.8,
                                lastmod: data.modifiedDate || data.date || new Date().toISOString(),
                            });
                        }
                    } catch (error) {
                        console.warn(`Could not process blog post ${file}:`, error.message);
                    }
                });
        });

        // Add talks with improved regex handling
        await safeProcess('talks', () => {
            const talksPath = path.join(process.cwd(), 'lib', 'data', 'talks.ts');

            if (!fs.existsSync(talksPath)) return;

            const content = fs.readFileSync(talksPath, 'utf8');
            const talkMatches = content.match(/{\s*id:\s*\d+,[\s\S]*?}/g) || [];

            talkMatches.forEach((match) => {
                try {
                    const extractField = (field) => {
                        // Improved regex to handle apostrophes within quoted strings
                        const doubleQuoteMatch = match.match(new RegExp(`${field}:\\s*"((?:[^"\\\\]|\\\\.)*)"`));
                        const singleQuoteMatch = match.match(new RegExp(`${field}:\\s*'((?:[^'\\\\]|\\\\.)*)'`));
                        const backtickMatch = match.match(new RegExp(`${field}:\\s*\`((?:[^\`\\\\]|\\\\.)*)\``));

                        return doubleQuoteMatch?.[1] || singleQuoteMatch?.[1] || backtickMatch?.[1];
                    };

                    const title = extractField('title');
                    if (!title) return;

                    const date = extractField('date') || new Date().toISOString();
                    const slug =
                        extractField('slug') ||
                        title
                            .toLowerCase()
                            .replace(/[^\w\s-]/g, '') // Remove special characters except spaces and hyphens
                            .replace(/\s+/g, '-') // Replace spaces with hyphens
                            .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
                            .trim()
                            .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens

                    result.push({
                        loc: `/talks/${slug}`,
                        changefreq: 'monthly',
                        priority: 0.8,
                        lastmod: new Date(date).toISOString(),
                    });
                } catch (error) {
                    console.warn('Could not process talk:', error.message);
                }
            });
        });

        return result;
    },
};
```

## [Key Configuration Decisions](#key-configuration-decisions)

### [1\. Comprehensive Exclusions](#1-comprehensive-exclusions)

The `exclude` array is crucial for keeping your sitemap clean:

JavaScript

Copy

```javascript
exclude: [
    '/_next/*', // Next.js build artifacts
    '*.js', // JavaScript files
    '*.css', // CSS files
    '*.png', // Image files
    // ... other static assets
];
```

**Why this matters**: Without these exclusions, your sitemap might include URLs like:

* `/_next/static/chunks/240a8089e20a3158.js`
* `/favicon.ico`
* `/apple-icon.png`

These files shouldn't be indexed by search engines as they're not content pages.

### [2\. Data-Driven Priority System with Critical Bug Fix](#2-data-driven-priority-system-with-critical-bug-fix)

I use a data-driven approach in the `transform` function that includes a critical fix for root path matching:

JavaScript

Copy

```javascript
transform: async (config, path) => {
    const pathPriorities = {
        main: { paths: ['/', '/about', '/blog', '/talks'], priority: 1.0, changefreq: 'weekly' },
        content: { paths: ['/blog/', '/talks/'], priority: 0.8, changefreq: 'monthly' },
        playground: { paths: ['/playground/'], priority: 0.6, changefreq: 'monthly' },
    };

    for (const [, { paths, priority, changefreq }] of Object.entries(pathPriorities)) {
        if (
            paths.some((p) => {
                if (p === '/') {
                    // Special case: root path only matches exactly
                    return path === '/';
                }
                return path === p || (p.endsWith('/') && path.startsWith(p));
            })
        ) {
            return {
                loc: path,
                changefreq,
                priority,
                lastmod: new Date().toISOString(),
            };
        }
    }

    return {
        loc: path,
        changefreq: config.changefreq,
        priority: config.priority,
        lastmod: new Date().toISOString(),
    };
};
```

**Critical Bug Fix**: The special handling for the root path (`/`) is essential. Without it, the root path would match ALL paths (since every path starts with `/`), causing incorrect priority assignments. This bug can significantly impact your SEO by giving wrong priorities to your pages.

This data-driven approach reduces code duplication and makes it easy to adjust priorities and change frequencies for different content types.

### [3\. Robust Dynamic Content Handling with Error Recovery](#3-robust-dynamic-content-handling-with-error-recovery)

The `additionalPaths` function uses a sophisticated approach with centralized error handling:

JavaScript

Copy

```javascript
additionalPaths: async () => {
    const fs = require('fs');
    const path = require('path');
    const result = [];

    // Helper function to safely process files
    const safeProcess = async (description, processor) => {
        try {
            await processor();
        } catch (error) {
            console.warn(`Could not load ${description} for sitemap:`, error.message);
        }
    };

    // Add blog posts
    await safeProcess('blog posts', () => {
        const matter = require('gray-matter');
        const postsDir = path.join(process.cwd(), 'public', 'content', 'blog', 'posts');

        if (!fs.existsSync(postsDir)) return;

        fs.readdirSync(postsDir)
            .filter((file) => file.endsWith('.mdx'))
            .forEach((file) => {
                try {
                    const filePath = path.join(postsDir, file);
                    const { data } = matter(fs.readFileSync(filePath, 'utf8'));

                    if (data.publish !== false) {
                        const slug = file.replace(/\.mdx$/, '').toLowerCase();
                        result.push({
                            loc: `/blog/${slug}`,
                            changefreq: 'monthly',
                            priority: 0.8,
                            lastmod: data.modifiedDate || data.date || new Date().toISOString(),
                        });
                    }
                } catch (error) {
                    console.warn(`Could not process blog post ${file}:`, error.message);
                }
            });
    });

    // Add talks with improved regex handling
    await safeProcess('talks', () => {
        const talksPath = path.join(process.cwd(), 'lib', 'data', 'talks.ts');

        if (!fs.existsSync(talksPath)) return;

        const content = fs.readFileSync(talksPath, 'utf8');
        const talkMatches = content.match(/{\s*id:\s*\d+,[\s\S]*?}/g) || [];

        talkMatches.forEach((match) => {
            try {
                const extractField = (field) => {
                    // Improved regex to handle apostrophes within quoted strings
                    const doubleQuoteMatch = match.match(new RegExp(`${field}:\\s*"((?:[^"\\\\]|\\\\.)*)"`));
                    const singleQuoteMatch = match.match(new RegExp(`${field}:\\s*'((?:[^'\\\\]|\\\\.)*)'`));
                    const backtickMatch = match.match(new RegExp(`${field}:\\s*\`((?:[^\`\\\\]|\\\\.)*)\``));

                    return doubleQuoteMatch?.[1] || singleQuoteMatch?.[1] || backtickMatch?.[1];
                };

                const title = extractField('title');
                if (!title) return;

                const date = extractField('date') || new Date().toISOString();
                const slug =
                    extractField('slug') ||
                    title
                        .toLowerCase()
                        .replace(/[^\w\s-]/g, '') // Remove special characters except spaces and hyphens
                        .replace(/\s+/g, '-') // Replace spaces with hyphens
                        .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
                        .trim()
                        .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens

                result.push({
                    loc: `/talks/${slug}`,
                    changefreq: 'monthly',
                    priority: 0.8,
                    lastmod: new Date(date).toISOString(),
                });
            } catch (error) {
                console.warn('Could not process talk:', error.message);
            }
        });
    });

    return result;
};
```

**Key improvements**:

* **`safeProcess` helper**: Centralizes error handling to prevent build failures
* **Improved regex**: Handles apostrophes within quoted strings correctly
* **Consistent slug generation**: Uses the same logic as the Next.js app to prevent duplicate entries
* **Multiple content sources**: Handles both blog posts and talks with appropriate error recovery

### [4\. Enhanced robots.txt](#4-enhanced-robotstxt)

The `robotsTxtOptions` configuration creates a comprehensive robots.txt:

Txt

Copy

```txt
User-agent: *
Allow: /playground
Allow: /blog
Allow: /talks
Disallow: /blocked
Disallow: /api
Disallow: /_next
Disallow: /static

Sitemap: https://www.yiminyang.dev/sitemap.xml
```

This explicitly tells crawlers what to index and what to avoid.

## [Results and Benefits](#results-and-benefits)

After implementing this configuration and fixing critical bugs, my website's sitemap went from including unwanted build artifacts to a clean, focused list of 59 relevant URLs:

* **4 Main pages** (priority 1.0)
* **8 Blog posts** (priority 0.8)
* **29 Talk pages** (priority 0.8)
* **13 Playground tools** (priority 0.6)
* **5 Other pages** (legal, contact, etc.)

**Critical Bug Fixes Applied**:

* Fixed root path matching that was causing incorrect priority assignments
* Resolved duplicate entries caused by inconsistent slug generation
* Improved regex handling for apostrophes in titles

### [SEO Benefits](#seo-benefits)

1. **Cleaner crawling**: Search engines focus on actual content, not build artifacts
2. **Better prioritization**: Important pages get higher priority scores (fixed the root path bug)
3. **Accurate metadata**: Real publication dates instead of build timestamps
4. **No duplicate entries**: Consistent slug generation prevents confusion
5. **Reduced server load**: Fewer unnecessary requests from crawlers

### [Performance Benefits](#performance-benefits)

1. **Smaller sitemap files**: Only relevant URLs are included
2. **Faster generation**: Efficient exclusion patterns and error handling
3. **Better caching**: Static sitemap generation during build
4. **Robust error recovery**: Build doesn't fail if content sources are unavailable

## [Common Pitfalls to Avoid](#common-pitfalls-to-avoid)

### [1\. Including Build Artifacts](#1-including-build-artifacts)

Always exclude `/_next/*` and static assets. These files change with every build and shouldn't be indexed.

### [2\. Ignoring Dynamic Content](#2-ignoring-dynamic-content)

Don't forget to handle dynamic routes like `[slug]` pages. Use `additionalPaths` to include them.

### [3\. Wrong Priorities](#3-wrong-priorities)

Avoid giving all pages the same priority. Use a tiered system that reflects your content hierarchy.

### [4\. Missing Error Handling](#4-missing-error-handling)

Always wrap dynamic content discovery in try-catch blocks to prevent build failures.

### [5\. CRITICAL: Root Path Matching Bug](#5-critical-root-path-matching-bug)

One of the most dangerous bugs in sitemap configurations is improper root path handling. Here's the complete context:

**The Problem**: Without special handling, the root path `/` will match ALL paths because every path starts with `/`. Here's what happens:

JavaScript

Copy

```javascript
// This logic has a fatal flaw
const pathPriorities = {
    main: { paths: ['/', '/about', '/blog'], priority: 1.0 },
    content: { paths: ['/blog/', '/talks/'], priority: 0.8 },
};

for (const [, { paths, priority }] of Object.entries(pathPriorities)) {
    if (paths.some((p) => path.startsWith(p))) {
        // BUG IS HERE!
        return { priority };
    }
}

// What happens:
// path = '/blog/my-post'
// '/blog/my-post'.startsWith('/') → true (WRONG!)
// Root path '/' matches everything, so ALL pages get priority 1.0
```

**The Fix**: Handle the root path as a special case that only matches exactly:

JavaScript

Copy

```javascript
// Special case for root path
for (const [, { paths, priority, changefreq }] of Object.entries(pathPriorities)) {
    if (
        paths.some((p) => {
            if (p === '/') {
                // Special case: root path only matches exactly
                return path === '/';
            }
            // For other paths, use normal prefix matching
            return path === p || (p.endsWith('/') && path.startsWith(p));
        })
    ) {
        return {
            loc: path,
            changefreq,
            priority,
            lastmod: new Date().toISOString(),
        };
    }
}

// Now it works correctly:
// path = '/' → matches '/' exactly → priority 1.0 ✓
// path = '/blog/my-post' → doesn't match '/' → continues to check '/blog/' → priority 0.8 ✓
```

**Why this matters**: Without this fix, ALL your pages would get the wrong priority (usually the first one in your list), which can severely impact SEO rankings.

### [6\. Regex Issues with Apostrophes](#6-regex-issues-with-apostrophes)

When parsing dynamic content, simple regex patterns can break on apostrophes:

JavaScript

Copy

```javascript
// Breaks on "Here's How"
const match = content.match(/title:\s*['"`](.*?)['"`]/);
```

**The fix**: Use separate patterns for each quote type:

JavaScript

Copy

```javascript
const doubleQuoteMatch = match.match(/title:\s*"((?:[^"\\]|\\.)*)"/);
const singleQuoteMatch = match.match(/title:\s*'((?:[^'\\]|\\.)*)'/);
const backtickMatch = match.match(/title:\s*`((?:[^`\\]|\\.)*)`/);
```

### [7\. Inconsistent Slug Generation](#7-inconsistent-slug-generation)

If your sitemap generates slugs differently than your Next.js app, you'll get duplicate entries:

JavaScript

Copy

```javascript
// Simple replacement
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-');

// Match your app's logic
const slug = title
    .toLowerCase()
    .replace(/[^\w\s-]/g, '') // Remove special characters except spaces and hyphens
    .replace(/\s+/g, '-') // Replace spaces with hyphens
    .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
    .trim()
    .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
```

## [Advanced Tips](#advanced-tips)

### [1\. Conditional Content Inclusion](#1-conditional-content-inclusion)

JavaScript

Copy

```javascript
if (data.publish !== false && !data.draft) {
    result.push({
        loc: `/blog/${slug}`,
        // ... rest of config
    });
}
```

### [2\. Custom Change Frequencies](#2-custom-change-frequencies)

JavaScript

Copy

```javascript
// More frequent updates for time-sensitive content
if (path.includes('/news/')) {
    return {
        changefreq: 'daily',
        priority: 0.9,
    };
}
```

## [Monitoring and Maintenance](#monitoring-and-maintenance)

### [1\. Google Search Console](#1-google-search-console)

Submit your sitemap to Google Search Console and monitor:

* Index coverage
* Crawl errors
* Sitemap processing status

### [2\. Regular Audits](#2-regular-audits)

Periodically check your sitemap for:

* Unwanted URLs
* Missing important pages
* Incorrect priorities or dates

### [3\. Automated Testing](#3-automated-testing)

Consider adding tests to verify your sitemap configuration:

JavaScript

Copy

```javascript
// Example test
test('sitemap excludes build artifacts', () => {
    const sitemap = fs.readFileSync('public/sitemap.xml', 'utf8');
    expect(sitemap).not.toContain('/_next/');
    expect(sitemap).not.toContain('.js');
});
```

## [Debugging and Testing Your Sitemap](#debugging-and-testing-your-sitemap)

Based on the critical bugs I discovered in my own configuration, here's how to properly test your sitemap:

### [1\. Test Priority Assignments](#1-test-priority-assignments)

Create a simple test to verify your transform function works correctly:

JavaScript

Copy

```javascript
// Test your transform function
const testPaths = ['/', '/blog', '/blog/test-post', '/playground/tool'];
testPaths.forEach(async (path) => {
    const result = await transform(config, path);
    console.log(`${path}: priority ${result.priority}, changefreq ${result.changefreq}`);
});
```

**Expected output**:

* `/`: priority 1.0, changefreq weekly
* `/blog`: priority 1.0, changefreq weekly
* `/blog/test-post`: priority 0.8, changefreq monthly
* `/playground/tool`: priority 0.6, changefreq monthly

### [2\. Check for Duplicate Entries](#2-check-for-duplicate-entries)

JavaScript

Copy

```javascript
// Check for duplicates in your sitemap
const sitemap = fs.readFileSync('public/sitemap.xml', 'utf8');
const urls = sitemap.match(/<loc>(.*?)<\/loc>/g) || [];
const uniqueUrls = new Set(urls);

if (urls.length !== uniqueUrls.size) {
    console.error('Duplicate URLs found in sitemap!');
    // Find duplicates
    const duplicates = urls.filter((url, index) => urls.indexOf(url) !== index);
    console.log('Duplicates:', duplicates);
}
```

### [3\. Validate Content Sources](#3-validate-content-sources)

Test that your dynamic content discovery works:

JavaScript

Copy

```javascript
// Test your additionalPaths function
const paths = await additionalPaths();
console.log(`Found ${paths.length} dynamic paths`);
paths.forEach((path) => {
    console.log(`${path.loc}: priority ${path.priority}`);
});
```

### [4\. Manual Sitemap Inspection](#4-manual-sitemap-inspection)

Always manually review your generated `public/sitemap.xml`:

1. **Check URL count**: Does it match your expectations?
2. **Verify priorities**: Are main pages getting priority 1.0?
3. **Look for unwanted URLs**: Any build artifacts or component files?
4. **Check for duplicates**: Same content with different URLs?

### [5\. Unit Testing Your Configuration](#5-unit-testing-your-configuration)

Here's a comprehensive test suite for your sitemap config:

JavaScript

Copy

```javascript
describe('Sitemap Configuration', () => {
    test('excludes build artifacts', () => {
        const sitemap = fs.readFileSync('public/sitemap.xml', 'utf8');
        expect(sitemap).not.toContain('/_next/');
        expect(sitemap).not.toContain('.js');
        expect(sitemap).not.toContain('.css');
    });

    test('includes main pages with correct priority', () => {
        const sitemap = fs.readFileSync('public/sitemap.xml', 'utf8');
        expect(sitemap).toContain('<priority>1.0</priority>');
    });

    test('no duplicate URLs', () => {
        const sitemap = fs.readFileSync('public/sitemap.xml', 'utf8');
        const urls = sitemap.match(/<loc>(.*?)<\/loc>/g) || [];
        const uniqueUrls = new Set(urls);
        expect(urls.length).toBe(uniqueUrls.size);
    });

    test('root path gets correct priority', async () => {
        const result = await transform(config, '/');
        expect(result.priority).toBe(1.0);
        expect(result.changefreq).toBe('weekly');
    });

    test('blog posts get correct priority', async () => {
        const result = await transform(config, '/blog/test-post');
        expect(result.priority).toBe(0.8);
        expect(result.changefreq).toBe('monthly');
    });
});
```

## [Conclusion](#conclusion)

A well-configured sitemap is a powerful SEO tool that helps search engines understand and index your content effectively. By excluding unwanted files, setting appropriate priorities, and handling dynamic content properly, you can significantly improve your website's search engine visibility.

The configuration I've shared has helped this website maintain clean, focused sitemaps that guide search engines to the most important content while avoiding unnecessary crawling of build artifacts and static assets.

Remember to regularly audit your sitemap and adjust the configuration as your website evolves. What works for one site might need tweaking for another, but the principles remain the same: keep it clean, prioritize correctly, and focus on content that matters to your users.

## [Resources](#resources)

* [next-sitemap Documentation](https://github.com/iamvishnusankar/next-sitemap)
* [Google Sitemap Guidelines](https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview)
* [Next.js SEO Documentation](https://nextjs.org/learn/seo/introduction-to-seo)