npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@anandhunadesh/next-wp-sitemap-proxy

v0.2.1

Published

Proxy and rewrite Yoast WordPress sitemaps for headless Next.js frontends

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:

  1. Serves those sitemaps from your Next.js site (/sitemap.xml, /post-sitemap.xml, …)
  2. Fetches the XML from WordPress
  3. Rewrites page <loc> URLs to your frontend domain
  4. Keeps image / upload URLs on the CMS domain
  5. Only allows known Yoast sitemap filenames (path traversal safe)
  6. 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-proxy

Environment 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:

  1. A route that fetches + returns the transformed XML
  2. Rewrites in next.config so /sitemap.xml hits 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:

  1. Injects <?xml-stylesheet type="text/xsl" href="/main-sitemap.xsl"?> into sitemap XML
  2. Rewrites /main-sitemap.xsl → /sitemaps/main-sitemap.xsl
  3. 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

  1. Confirm Yoast works on CMS: https://cms.example.com/sitemap.xml
  2. Open frontend: https://www.example.com/sitemap.xml
  3. In a browser you should see a styled HTML table (XSL)
  4. Check that <loc> URLs use the frontend domain (View Source)
  5. Check that <image:loc> (and /wp-content/uploads/ URLs) still use the CMS domain
  6. Confirm https://www.example.com/main-sitemap.xsl loads

Behavior details

  • Yoast CMS stylesheet PIs are replaced with /main-sitemap.xsl on 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