
Visit the GitHub Repo
Leveraging Netlify’s static-hosting speed and security doesn’t mean sacrificing agility. This guide shows how to add a lightweight serverless function that renders sitemap.xml on demand, ensuring crawlers always receive an up-to-the-minute view of your site. No plugins are required and there are no additional build steps beyond those described here.
Prerequisites
Everything that follows assumes your project meets two basic conditions:
- Netlify deploys from Git: the site’s source sits in a Git repository connected to Netlify.
- Node 18 or later: Netlify’s current default runtime already provides this.
Why Build the Sitemap Dynamically?
A sitemap written at build time works for many sites, but it brings two drawbacks:
- Each content change forces a full rebuild, which can be slow on large projects.
- Content added outside Git (for example through a CMS webhook) is not listed until the next deploy.
By generating sitemap.xml only when it is requested, you avoid extra build minutes and ensure the file mirrors the current release exactly. It is a genuine set-and-forget solution: once the function is in place, every deploy automatically scans the HTML in the site root and in /posts/, converts the paths to clean, canonical URLs, and serves them to crawlers. The result is an up-to-date index of every page on each visit, with no manual upkeep.
1/ Create the Function
Add a file at netlify/functions/sitemap.js:
import fs from "fs";
import path from "path";
const CANONICAL_BASE =
process.env.URL || process.env.DEPLOY_PRIME_URL;
// __dirname points to the root of the function bundle
const PUBLISH_DIR = path.join(__dirname);
function walk(dir, list = []) {
for (const entry of fs.readdirSync(dir)) {
const full = path.join(dir, entry);
fs.statSync(full).isDirectory()
? walk(full, list)
: list.push(full);
}
return list;
}
export const handler = async () => {
try {
const pages = walk(PUBLISH_DIR)
.filter(f => f.endsWith(".html"))
.map(f =>
f
.replace(PUBLISH_DIR, "")
.replace(/index\.html$/, "/")
.replace(/\.html$/, "")
);
const xml = `\n\n${pages
.map((p) => ` ${CANONICAL_BASE}${p} `)
.join("\n")}\n `;
return {
statusCode: 200,
headers: {
"Content-Type": "application/xml",
"Cache-Control": "public, max-age=0, must-revalidate",
}
body: xml,
};
} catch (err) {
return { statusCode: 500, body: err.message };
}
};
2/ Create netlify.toml at the Repo Root
[build]
publish = "."
functions = "netlify/functions"
[functions."sitemap"]
included_files = [
"*.html",
"posts/**/*.html"
]
[[redirects]]
from = "/sitemap.xml"
to = "/.netlify/functions/sitemap"
status = 200
force = true
3/ Commit and Deploy
git add netlify.toml netlify/functions/sitemap.js
git commit -m "feat: dynamic sitemap"
git push origin main
4/ Test It
- /.netlify/functions/sitemap → should return the XML sitemap.
- /sitemap.xml → same XML served via a redirect.
Going Further
Once the core function described above is up and running you can extend it for scale, automation and dynamic data.
Schedule Regeneration
Netlify functions support cron-style triggers. Add a schedule property to your .toml file so the sitemap is refreshed automatically, even when there is no deploy:
[functions."sitemap"]
included_files = [
"*.html",
"posts/**/*.html"
]
schedule = "0 3 * * *" # run every day at 03:00 UTC
Very Large Sites
Google limits a single sitemap to fifty thousand URLs or 50 MB. If your pages array exceeds that, slice it into 50,000-item chunks, write each slice to its own function (for example sitemap-1, sitemap-2) and have the root function return a sitemapindex that points to them.
Dynamic Content Sources
If your site publishes pages between deploys—through a headless CMS, database or API, you can fetch those URLs inside the function instead of reading the packaged HTML. You can add to the function to combine the external list with the on-disk pages, de-duplicate and return the merged set so crawlers always see everything that is live.
Conclusion
A handful of lines is all it takes to turn Netlify’s serverless runtime into a self-maintaining sitemap service. You create a single function, list your HTML files with included_files, add an optional schedule, and deploy as normal. From that point:
- Every request for sitemap.xml returns a fresh index of the HTML shipped in the latest release.
- Optional cron settings keep the file refreshed without a full site build.
- The pattern scales to many tens of thousands of pages and can grow to include CMS or database URLs when needed.
The outcome is a search-friendly, always-current sitemap that runs itself while you focus on content. Give it a try, adapt it to your workflow, and share your results.