@anandhunadesh/next-wp-sitemap-proxy
v0.2.1
Published
Proxy and rewrite Yoast WordPress sitemaps for headless Next.js frontends
Maintainers
Readme
@anandhunadesh/next-wp-sitemap-proxy
Proxy Yoast SEO sitemaps from WordPress through a headless Next.js frontend.
In a typical headless setup, Yoast emits sitemap URLs that point at the CMS domain. This package:
- Serves those sitemaps from your Next.js site (
/sitemap.xml,/post-sitemap.xml, …) - Fetches the XML from WordPress
- Rewrites page
<loc>URLs to your frontend domain - Keeps image / upload URLs on the CMS domain
- Only allows known Yoast sitemap filenames (path traversal safe)
- Serves a styled browser view via bundled
main-sitemap.xsl(like Yoast)
Requirements
- Node.js 18+
- Next.js (Pages Router or App Router)
- WordPress with Yoast SEO sitemaps enabled
TypeScript is supported via bundled .d.ts files (no extra install). Works in JS and TS projects.
Install
npm install @anandhunadesh/next-wp-sitemap-proxyEnvironment variables
Use full origins (no trailing path):
| Variable | Example | Purpose |
| --- | --- | --- |
| WP_URL / CMS_ORIGIN | https://cms.example.com | WordPress / Yoast source |
| NEXT_PUBLIC_SITE_URL / FRONTEND_ORIGIN | https://www.example.com | Public Next.js site |
You can name them whatever you like — pass the values into the handlers as cmsOrigin and frontendOrigin.
Setup overview
You need two pieces in every project:
- A route that fetches + returns the transformed XML
- Rewrites in
next.configso/sitemap.xmlhits that route
Use the same allowlist config in both places.
Pages Router
1. Create the sitemap route
pages/sitemaps/[...sitemap].js:
import SitemapProxyPage, {
createPagesRouterSitemapHandler,
} from "@anandhunadesh/next-wp-sitemap-proxy/next/pages-router";
const sitemapConfig = {
cmsOrigin: process.env.WP_URL,
frontendOrigin: process.env.NEXT_PUBLIC_SITE_URL,
fetchTimeoutMs: 10000,
allowlistOptions: {
staticSitemapFiles: [
"sitemap.xml",
"post-sitemap.xml",
"post-sitemap2.xml",
"page-sitemap.xml",
// Add custom post type sitemaps as needed:
// "people-sitemap.xml",
// "press-sitemap.xml",
],
paginationRules: [
{ prefix: "post-sitemap", minNumber: 3 },
{ prefix: "page-sitemap", minNumber: 2 },
],
},
};
export const getServerSideProps = createPagesRouterSitemapHandler(sitemapConfig);
export default SitemapProxyPage;2. Add rewrites
next.config.mjs (or next.config.js):
import { buildSitemapRewrites } from "@anandhunadesh/next-wp-sitemap-proxy";
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return buildSitemapRewrites({
staticSitemapFiles: [
"sitemap.xml",
"post-sitemap.xml",
"post-sitemap2.xml",
"page-sitemap.xml",
],
paginationRules: [
{ prefix: "post-sitemap", minNumber: 3 },
{ prefix: "page-sitemap", minNumber: 2 },
],
routePrefix: "/sitemaps",
});
},
};
export default nextConfig;This maps:
/main-sitemap.xsl→/sitemaps/main-sitemap.xsl/sitemap.xml→/sitemaps/sitemap.xml/post-sitemap3.xml→/sitemaps/post-sitemap3.xml- etc.
App Router
1. Create the route handler
app/sitemaps/[...sitemap]/route.js:
import { createAppRouterSitemapHandler } from "@anandhunadesh/next-wp-sitemap-proxy/next/app-router";
export const GET = createAppRouterSitemapHandler({
cmsOrigin: process.env.WP_URL,
frontendOrigin: process.env.NEXT_PUBLIC_SITE_URL,
fetchTimeoutMs: 10000,
allowlistOptions: {
staticSitemapFiles: [
"sitemap.xml",
"post-sitemap.xml",
"post-sitemap2.xml",
"page-sitemap.xml",
],
paginationRules: [
{ prefix: "post-sitemap", minNumber: 3 },
{ prefix: "page-sitemap", minNumber: 2 },
],
},
});2. Add the same rewrites
Use buildSitemapRewrites() in next.config exactly as in the Pages Router section above.
Configuration reference
Handler options (createPagesRouterSitemapHandler / createAppRouterSitemapHandler)
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| cmsOrigin | string | required | WordPress origin, e.g. https://cms.example.com |
| frontendOrigin | string | required | Next.js public origin |
| fetchTimeoutMs | number | 10000 | CMS fetch timeout |
| allowlistOptions | object | package defaults | Which sitemap files are allowed |
| stylesheetHref | string \| false \| null | /main-sitemap.xsl | XSL href injected into XML. Pass false/null to disable |
| fetchImpl | function | fetch | Optional custom fetch (tests / proxies) |
Allowlist options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| staticSitemapFiles | string[] | sitemap.xml, post-sitemap.xml, post-sitemap2.xml, page-sitemap.xml | Exact filenames to allow |
| paginationRules | { prefix, minNumber }[] | post ≥ 3, page ≥ 2 | Pattern allowlist for paginated Yoast files |
| routePrefix | string | /sitemaps | Used only by buildSitemapRewrites |
| stylesheetHref | string \| false \| null | /main-sitemap.xsl | Also rewritten into the sitemap route. Pass false/null to skip |
Tip: List every Yoast sitemap your site actually emits (including CPT sitemaps like people-sitemap.xml). Files not on the allowlist return 404.
Browser styling (XSL)
By default the package:
- Injects
<?xml-stylesheet type="text/xsl" href="/main-sitemap.xsl"?>into sitemap XML - Rewrites
/main-sitemap.xsl→/sitemaps/main-sitemap.xsl - Serves the bundled XSL from the same catch-all sitemap route
Open /sitemap.xml in a browser to see the styled table view. Search engines ignore the XSL and read the raw XML.
To disable styling:
createPagesRouterSitemapHandler({
cmsOrigin: process.env.WP_URL,
frontendOrigin: process.env.NEXT_PUBLIC_SITE_URL,
stylesheetHref: false,
});
buildSitemapRewrites({
// ...
stylesheetHref: false,
});Verify it works
- Confirm Yoast works on CMS:
https://cms.example.com/sitemap.xml - Open frontend:
https://www.example.com/sitemap.xml - In a browser you should see a styled HTML table (XSL)
- Check that
<loc>URLs use the frontend domain (View Source) - Check that
<image:loc>(and/wp-content/uploads/URLs) still use the CMS domain - Confirm
https://www.example.com/main-sitemap.xslloads
Behavior details
- Yoast CMS stylesheet PIs are replaced with
/main-sitemap.xslon the frontend (or removed if disabled) <loc>URLs are rewritten from CMS → frontend (except/wp-content/uploads/paths)<image:loc>URLs are forced back to CMS if they leaked to the frontend domain- Missing / unknown sitemaps →
404 - CMS timeout →
504 - Other CMS fetch failures →
502
Low-level API
If you need custom handlers:
import {
isAllowedSitemapPath,
buildSitemapRewrites,
transformYoastSitemapXml,
fetchYoastSitemap,
} from "@anandhunadesh/next-wp-sitemap-proxy";
const result = await fetchYoastSitemap("sitemap.xml", {
cmsOrigin: "https://cms.example.com",
frontendOrigin: "https://www.example.com",
});
if (result.ok) {
// result.xml — transformed sitemap
}License
MIT
