How to Create a Sitemap for a JavaScript Site or Single-Page App

JavaScript sites and SPAs face a discovery problem before they face a canonical one. Here is how to make your URLs visible through the sitemap.

Victor Ijomah
By
Victor Ijomah
Victor Ijomah
Technical SEO Specialist
Victor Afamefuna Ijomah is a UK-based Technical SEO Specialist focused on how Google and AI engines like ChatGPT, Perplexity, and AI Overviews decide what gets discovered,...
- Technical SEO Specialist
Highlights
  • JavaScript sites face a discovery problem (do engines see URLs) rather than the canonical question that defines e-commerce sitemaps.
  • Server-rendered, static-generated, hybrid, and pure client-side rendered sites have different sitemap implications; identify your category first.
  • Dynamic routes require explicit enumeration through your framework's path generation API or a custom data query at sitemap build time.
  • Lastmod should reflect data update dates for dynamic content, with build date acceptable only for genuinely static pages.
  • Pure SPAs without server rendering rely on the sitemap as the primary URL discovery signal because URLs may not exist in static HTML at all.

Part of the SiteMap Series

In the previous lesson, you saw how e-commerce sitemaps require decisions about which URLs are canonical landing pages out of a much larger set of URLs the platform generates. JavaScript sites and single-page apps face a different question. The problem is not selecting which URLs to include; it is making sure the engines see your URLs at all.

On a traditional server-rendered site, every URL exists as static HTML the engines can crawl and parse directly. The sitemap is a convenience that confirms what the engines could discover anyway through internal links. On a JavaScript-heavy site, that assumption breaks down. URLs may only exist after the JavaScript executes. Client-side routers create URL transitions that never produce a new HTML document.

Dynamic routes generate URLs from data that the engines have no visibility into. The sitemap stops being a convenience and becomes a primary signal: it tells the engines what URLs exist, regardless of whether they could find those URLs by crawling alone.

This lesson covers how to think about your specific JavaScript setup (the categories of JS site behave differently), how to enumerate dynamic routes for the sitemap, how to set lastmod when “when did this page change” is harder to answer, and how to implement sitemap generation on the main frameworks (Next.js, Astro, Gatsby, and pure SPAs).

Why JavaScript sites have a sitemap problem traditional sites do not

Three properties of JavaScript sites create the discovery friction that this lesson addresses.

URLs may not exist in static HTML. On a server-rendered site, every URL produces a unique HTML document with content that the engines can read directly. On a pure client-side rendered site, every URL might serve the same HTML shell, with the actual content loaded by JavaScript after the page loads. The engines that do not execute JavaScript see only the empty shell. The engines that do execute JavaScript see the content but pay a higher cost (rendering is more expensive than parsing).

Client-side routing makes URLs invisible to traditional crawlers. When a user clicks a link on a single-page app, the URL changes (via the History API) but no new HTML document loads. The router intercepts the navigation, updates the visible content, and changes the URL in the browser. A crawler that only follows traditional HTTP requests never sees these client-side navigations. The URLs exist from the user’s perspective but not from the crawler’s perspective.

Dynamic routes are generated from data sources. A blog with /posts/[slug]/ routes generates a unique URL for every post. A directory site with /companies/[id]/ routes generates one per company. These URLs exist only because the data source contains the corresponding records. The crawler has no direct access to that data source, so it cannot enumerate the URLs unless they are explicitly listed somewhere (links in HTML, sitemap entries).

In all three cases, the sitemap becomes the explicit URL list that does not require any rendering or data access to read. The engines see the URL list as plain XML and use it to know what URLs exist on your site. The crawling that follows still has to deal with the JavaScript execution problem, but the discovery problem is solved at the sitemap level.

How to think about your specific JavaScript setup

JavaScript sites fall into three categories with different sitemap implications. Identifying which category your site falls into is the first step.

Server-rendered (SSR) or static-generated (SSG) sites.

Next.js with SSR or SSG, Nuxt, Astro, Remix, SvelteKit in SSR mode, Gatsby. The framework executes the JavaScript at build time or on the server and produces HTML that crawlers can parse without executing anything. From a sitemap perspective, these sites behave almost like traditional server-rendered sites. The URLs exist in the HTML; the sitemap is straightforward. The main complication is enumerating dynamic routes at build time.

Hybrid (SSR plus client-side hydration)

Most modern frameworks default to this. The initial HTML is server-rendered or pre-rendered, then JavaScript hydrates the page for interactivity. Crawlers see the initial HTML directly. The sitemap considerations are mostly the same as fully server-rendered sites, with some nuance around content that loads after hydration.

Pure client-side rendered (CSR) single-page apps

Create React App, Vite-based SPAs without SSR, Vue or Angular apps without a server-rendering layer. The initial HTML is a minimal shell; all content loads after JavaScript executes. The sitemap is the primary signal the engines have for URL discovery because the URLs do not exist in the static HTML.

The right approach depends on which category you are in. Server-rendered and static-generated sites have the most reliable sitemap discovery. Hybrid sites are nearly as good. Pure CSR sites require more explicit work and may benefit from pre-rendering (covered later) on top of sitemap submission.

How to enumerate dynamic routes for the sitemap

Dynamic routes are the technical challenge for JavaScript site sitemaps. A static route (/about/, /contact/) is known at build time. A dynamic route (/products/[slug]/, /posts/[id]/) depends on data the framework needs to query.

Three patterns handle this in modern frameworks.

Pattern 1: Build-time enumeration through static path generation.

The framework provides a function that runs at build time, queries the data source, and returns the list of paths to pre-render. Next.js uses generateStaticParams (App Router) or getStaticPaths (Pages Router). Astro uses getStaticPaths. Gatsby uses GraphQL queries in gatsby-node.js. The sitemap plugin reads these paths and emits them as sitemap entries.

Pattern 2: On-demand sitemap generation through API queries.

For sites where the route set is too large or changes too frequently for build-time enumeration, the sitemap is generated by an API route or scheduled job that queries the data source and writes the sitemap file. The frequency depends on how often the route set changes (hourly for a high-volume site, daily for most).

Pattern 3: Database-driven sitemap with caching.

For very large sites, the sitemap is generated from a query that joins the URL data with the lastmod data, with the result cached and re-generated on a schedule. The cache prevents the sitemap endpoint from hitting the database on every request.

The right pattern depends on site size and content velocity. Build-time enumeration is the simplest for sites with under 10,000 dynamic routes and infrequent content updates. On-demand generation is the most flexible. Cached database-driven generation is the necessary pattern for sites at serious scale.

Whichever pattern you use, the data source needs to be the source of truth for what URLs exist. If the sitemap is built from one source and the actual URLs are served from another, the two can drift, leaving sitemap entries for URLs that no longer exist or missing entries for URLs that do exist.

Setting lastmod for JavaScript-rendered content

Lastmod on JavaScript sites is harder than on server-rendered content sites because “when did this page change” depends on what you mean by “this page”.

Three definitions of lastmod work in different contexts.

  1. The build date. Every URL in the sitemap gets the same lastmod, set to when the site was last built. This is the simplest implementation and the least informative. It works for sites that rebuild on every content change (most static-generated sites with low to medium content volume). It does not work for sites with frequently-changing content where most URLs are stable.
  2. The data update date. Each URL’s lastmod reflects when the data backing that URL last changed. A blog post’s lastmod is the date of the post’s last edit. A product page’s lastmod is when the product record was updated. This is the most accurate option and the most useful for crawler prioritisation, but it requires the data source to track update timestamps and the sitemap generator to query them.
  3. A hybrid: data update for dynamic routes, build date for static routes. Dynamic routes use their data update timestamps. Static routes (/about/, /contact/) use the build date. This balances accuracy with implementation complexity and is the pattern most modern static site generators land on by default.

The principle is the same as for any other site: lastmod should signal when the engines would benefit from re-crawling the page. Build dates that change on every deployment for unchanged content train the engines to discount your lastmod signals. Data update dates that accurately reflect content changes earn trust.

How to implement on the main JavaScript frameworks

Five framework contexts cover most JavaScript site sitemap implementations.

1. Next.js

Next.js supports sitemap generation through two main paths. The App Router (Next.js 13+) supports a sitemap.ts or sitemap.xml.ts file in the app directory that exports a default function returning the sitemap entries. The function can query data sources to enumerate dynamic routes.

For Next.js Pages Router or for sites that need more control, the next-sitemap package is the established choice. Configuration lives in next-sitemap.config.js, with transform and additionalPaths functions for customising the output and including dynamic routes from external data sources.

For sitemap-aware hreflang from Lesson 5 of Module 3: How to Use Hreflang in Sitemaps for International SEO, next-sitemap supports an alternateRefs option that handles xhtml:link generation.

2. Astro

Astro uses the @astrojs/sitemap integration. The integration scans the routes Astro is aware of (static pages, dynamic routes enumerated via getStaticPaths, content collections) and generates sitemap entries automatically. Configuration in astro.config.mjs supports filtering, customising the lastmod and changefreq, and adding i18n hreflang.

For Astro sites that fetch from a CMS at build time, the sitemap automatically includes all routes the CMS query returned. For Astro sites with on-demand rendering, the integration may need additional configuration to include those routes.

3. Gatsby

Gatsby uses the gatsby-plugin-sitemap plugin. Configuration lives in gatsby-config.js, with options for filtering, customising the output, and grouping URLs into multiple sitemap files.

Gatsby’s GraphQL data layer integrates naturally with sitemap generation because all the data needed to enumerate URLs and their metadata is already queryable. The plugin reads from the GraphQL layer and emits sitemap entries.

4. Vue and Nuxt

Nuxt sites use the @nuxtjs/sitemap module (Nuxt 3) or nuxt-simple-sitemap for more flexibility. Configuration lives in nuxt.config.ts. The module supports static routes, dynamic routes, and on-demand sitemap generation.

For Vue SPAs without Nuxt (vanilla Vue with Vite), sitemap generation is typically a custom build step or post-build script that queries the data source and writes the sitemap file.

5. Pure SPAs (Create React App, Vite, Angular)

Pure single-page apps without a server-rendering or static-generation layer require a custom sitemap generation step. The pattern is usually a Node.js script that runs as part of the build (or on a schedule) and writes a sitemap file alongside the built application.

For these sites, the sitemap is often the only reliable URL discovery signal the engines have. Pre-rendering through services like Prerender.io or Rendertron can supplement the sitemap by providing static HTML for crawlers that do not execute JavaScript, but the sitemap submission remains essential.

Common gotchas to avoid

Five issues come up regularly with JavaScript site sitemaps.

1. Only static routes in the sitemap

The most common JS site sitemap mistake. The sitemap includes the static routes the framework discovers automatically but misses dynamic routes that depend on data source queries. The fix is to verify that dynamic routes are being enumerated through whichever pattern your framework uses (generateStaticParams, getStaticPaths, GraphQL queries, custom scripts).

2. Hash-based URLs in the sitemap

Some older SPAs use hash-based routing (/#/about, /#/products/123). Hash fragments are never separate URLs from the engines’ perspective. Including these in the sitemap does nothing useful. The fix is to migrate to history-based routing (which uses the History API to produce real URL paths) and rebuild the sitemap accordingly.

3. Build date as lastmod for every URL

A sitemap where every URL has the same lastmod (the last build date) tells the engines nothing useful about what has actually changed. Over time, the engines discount this signal because it correlates with deployments rather than content updates. The fix is to use data update timestamps for dynamic routes, build date only for genuinely static pages.

4. URLs that return 200 but render as empty or broken

Some SPAs serve a 200 response from the server for every URL (because the same HTML shell handles all routes) but the actual content renders only after JavaScript executes. If the JavaScript fails or the route does not exist in the client-side router, the user sees a broken state but the HTTP response was 200. Engines following sitemap URLs hit these pages and get confusing signals. The fix is to either serve actual 404 responses for invalid routes (requires server-side route awareness) or pre-render real 404 states.

5. Sitemap that does not update with content changes

The sitemap is generated at build time, but content changes happen between builds through a CMS. New content appears on the site but is not in the sitemap until the next build. The fix is either to trigger a rebuild on every content change (webhook from CMS to build pipeline) or to generate the sitemap on-demand from the CMS data rather than at build time.

Where this leaves us

You can now build a sitemap for a JavaScript site or single-page app that handles dynamic routes properly, signals lastmod accurately, and works with your framework’s conventions. The friction between sitemap theory and JavaScript implementation comes down to making URLs reachable when the standard HTML-crawling approach cannot find them; the sitemap fills that gap.

Site migrations come next. They introduce a different category of sitemap friction. During a migration, the sitemap is not just a static URL list; it is a signal that has to coordinate with redirects, canonical tags, and content updates in a specific sequence. The wrong sitemap update at the wrong moment can slow down indexing of the new URLs or preserve indexing of URLs that should be retired. The next lesson covers the timing and orchestration.

Up next: How to Handle Your Sitemap During a Website Migration →


This is Module 4: Lesson 3 of The Sitemap Series, a Technical SEO series on sitemaps from first principles, built for the AI Search era.

Was this article helpful?
YesNo
Share This Article
Victor Ijomah
Technical SEO Specialist
Follow:
Victor Afamefuna Ijomah is a UK-based Technical SEO Specialist focused on how Google and AI engines like ChatGPT, Perplexity, and AI Overviews decide what gets discovered, understood, and cited. He holds an M.Sc in Digital Marketing from the University of Chester and is the editor of The Technical SEO Library, a publication on crawl systems, schema, entity SEO, AI crawler management, and the technical foundations of visibility in the AI Search era.
Leave a Comment