Next.js Sitemap for Static Export: The force-static Solution

TLDR: Add export const dynamic = 'force-static' to your app/sitemap.ts file. This tells Next.js to generate the sitemap at build time instead of requiring a server.


You deployed your Next.js site as a static export to Cloudflare Pages, Netlify, or GitHub Pages. Everything works great.

Then you realize: search engines can't find your pages because you don't have a sitemap.

The problem? Most Next.js sitemap guides assume you're running a Node.js server. But static exports don't have a server.

Here's how to generate a sitemap that actually works with static exports.

The problem with Next.js static exports

When you use output: 'export' in Next.js, the framework builds your entire site as static HTML files. No server-side rendering, no API routes, no dynamic generation.

This is great for performance and hosting simplicity, but it breaks Next.js's built-in sitemap generation which expects dynamic route handlers.

The typical Next.js sitemap approach looks like this:

// app/sitemap.ts - doesn't work with static export
export default function sitemap() {
return [
  {
    url: 'https://example.com',
    lastModified: new Date(),
  },
]
}

When you build with output: 'export', the dynamic route handler can't execute without a server. Your sitemap never generates.

The solution: force-static mode

Next.js has a configuration option that forces route handlers to generate at build time: force-static.

This tells Next.js to pre-render the sitemap during the build process instead of generating it on-demand.

Here's how it works:

// src/app/sitemap.ts
import { MetadataRoute } from 'next'

export const dynamic = 'force-static'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
return [
  {
    url: 'https://keninkujovic.com',
    lastModified: new Date(),
    changeFrequency: 'weekly',
    priority: 1,
  },
  {
    url: 'https://keninkujovic.com/blog',
    lastModified: new Date(),
    changeFrequency: 'weekly',
    priority: 0.8,
  },
]
}

The key line is export const dynamic = 'force-static'. This tells Next.js to generate sitemap.xml at build time as a static file.

Adding dynamic content from MDX files

For a blog or documentation site, you want your sitemap to include all your posts automatically.

Here's how to read your content files and add them to the sitemap:

// src/utils/mdx.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const blogDirectory = path.join(process.cwd(), 'src/content/blog')

export async function getAllPosts() {
const fileNames = fs.readdirSync(blogDirectory)
  .filter(fileName => fileName.endsWith('.mdx'))

const posts = fileNames
  .map((fileName) => {
    const slug = fileName.replace(/\.mdx$/, '')
    const fullPath = path.join(blogDirectory, fileName)
    const fileContents = fs.readFileSync(fullPath, 'utf8')
    const { data: frontmatter } = matter(fileContents)

    return {
      slug,
      frontmatter
    }
  })
  .filter((post) => !post.frontmatter.draft)

return posts.sort((a, b) => {
  return new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()
})
}

Now update your sitemap to include all blog posts:

// src/app/sitemap.ts
import { MetadataRoute } from 'next'
import { getAllPosts } from '@/utils/mdx'

export const dynamic = 'force-static'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts()

const blogPosts = posts.map((post) => ({
  url: `https://keninkujovic.com/blog/${post.slug}`,
  lastModified: new Date(post.frontmatter.date),
  changeFrequency: 'monthly' as const,
  priority: 0.7,
}))

return [
  {
    url: 'https://keninkujovic.com',
    lastModified: new Date(),
    changeFrequency: 'weekly',
    priority: 1,
  },
  {
    url: 'https://keninkujovic.com/blog',
    lastModified: new Date(),
    changeFrequency: 'weekly',
    priority: 0.8,
  },
  ...blogPosts,
]
}

Verify the sitemap generates correctly

Build your site and check that the sitemap was created:

bun run build

# Check the output directory
ls out/sitemap.xml

# View the generated sitemap
cat out/sitemap.xml

You should see a properly formatted XML file:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
  <loc>https://keninkujovic.com</loc>
  <lastmod>2026-01-09T00:00:00.000Z</lastmod>
  <changefreq>weekly</changefreq>
  <priority>1</priority>
</url>
<url>
  <loc>https://keninkujovic.com/blog</loc>
  <lastmod>2026-01-09T00:00:00.000Z</lastmod>
  <changefreq>weekly</changefreq>
  <priority>0.8</priority>
</url>
<url>
  <loc>https://keninkujovic.com/blog/nextjs-static-sitemap</loc>
  <lastmod>2026-01-09T00:00:00.000Z</lastmod>
  <changefreq>monthly</changefreq>
  <priority>0.7</priority>
</url>
</urlset>

Deploy and test

Deploy your site to your static host. The sitemap should be accessible at:

https://your-domain.com/sitemap.xml

Test it in your browser or use curl:

curl https://keninkujovic.com/sitemap.xml

Submit to search engines

Once your sitemap is live, submit it to search engines:

Google Search Console:

  1. Go to https://search.google.com/search-console
  2. Select your property
  3. Navigate to Sitemaps
  4. Enter sitemap.xml and submit

Bing Webmaster Tools:

  1. Go to https://www.bing.com/webmasters
  2. Add your site
  3. Submit your sitemap URL

You can also add it to your robots.txt:

# public/robots.txt
User-agent: *
Allow: /

Sitemap: https://keninkujovic.com/sitemap.xml

Tip: You can also generate robots.txt dynamically using the same force-static approach with app/robots.ts.

Common issues and fixes

Issue: Sitemap not generating

Problem: The out directory doesn't contain sitemap.xml after building.

Solution: Make sure you're using force-static:

export const dynamic = 'force-static'

Issue: Build fails with "dynamic server usage"

Problem: Next.js complains about dynamic functions in your sitemap.

Solution: Don't use dynamic Next.js functions like cookies(), headers(), or searchParams in your sitemap. These require a server.

Adding projects or other content types

If you have multiple content directories (blog, projects, docs), follow the same pattern:

// src/utils/mdx.ts
const projectsDirectory = path.join(process.cwd(), 'src/content/projects')

export async function getAllProjects() {
const fileNames = fs.readdirSync(projectsDirectory)
  .filter(fileName => fileName.endsWith('.mdx'))

return fileNames.map((fileName) => {
  const slug = fileName.replace(/\.mdx$/, '')
  const fullPath = path.join(projectsDirectory, fileName)
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const { data: frontmatter } = matter(fileContents)

  return { slug, frontmatter }
})
}

Then add to your sitemap:

// src/app/sitemap.ts
import { MetadataRoute } from 'next'
import { getAllPosts, getAllProjects } from '@/utils/mdx'

export const dynamic = 'force-static'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts()
const projects = await getAllProjects()

const blogUrls = posts.map((post) => ({
  url: `https://keninkujovic.com/blog/${post.slug}`,
  lastModified: new Date(post.frontmatter.date),
  changeFrequency: 'monthly' as const,
  priority: 0.7,
}))

const projectUrls = projects.map((project) => ({
  url: `https://keninkujovic.com/projects/${project.slug}`,
  lastModified: new Date(project.frontmatter.date),
  changeFrequency: 'monthly' as const,
  priority: 0.6,
}))

return [
  {
    url: 'https://keninkujovic.com',
    lastModified: new Date(),
    changeFrequency: 'weekly',
    priority: 1,
  },
  ...blogUrls,
  ...projectUrls,
]
}

Performance considerations

Sitemap generation happens at build time, not runtime. This means:

  1. No performance impact - The sitemap is a static file served directly by your CDN
  2. Rebuild to update - You need to rebuild and redeploy when you add new content
  3. No caching issues - The sitemap updates every time you build

For static sites, this is perfect. You're already rebuilding when you publish new content.

Takeaways

  1. Use force-static - This is the key to making sitemaps work with static Next.js exports

  2. Read content at build time - Use Node.js fs module to scan your MDX/MD files during the build

  3. Filter drafts - Make sure to exclude draft posts from your sitemap

  4. Test after building - Always verify the sitemap exists in your out directory

  5. Submit to search engines - Don't forget to submit your sitemap to Google Search Console and Bing

  6. Rebuild on content changes - Your sitemap only updates when you rebuild the site

Why this matters

Without a sitemap, search engines have to discover your pages by crawling links. This is slow and unreliable.

With a sitemap, you're explicitly telling search engines:

  • Every page on your site
  • When each page was last updated
  • Which pages are most important

This helps your content get indexed faster and rank better in search results.

For a static blog or portfolio site, implementing a proper sitemap is one of the easiest SEO wins you can get.

·share on