Sitemap

If you've never worked with sitemaps, this starts from zero: what a sitemap is, why you need one, and how to use it in swift-web (how static and dynamic pages each get into the sitemap). By the end you'll be able to get your site fully indexed by search engines.

1. What is a sitemap (the basics)

A sitemap is a file that lists all the page URLs on your site that you want search engines to index. It's usually called sitemap.xml and lives at your site root (https://your-domain/sitemap.xml). Think of it as a "page inventory" you hand to Google / Bing, telling crawlers: here are my pages, where they are, and when they last changed.

It looks like this (a minimal XML):

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
    <lastmod>2026-06-23</lastmod>
  </url>
  <url>
    <loc>https://example.com/series/123</loc>
  </url>
</urlset>

Why content sites especially need one

Search engines mainly discover pages by crawling links from page to page. That creates two pain points for content sites:

  • Too many pages to crawl fully: a video / comic site can have tens of thousands of detail pages, and crawlers may not reach all of them by following links from the homepage. A page that's never discovered earns zero organic traffic.
  • New pages discovered slowly: a freshly published detail page might only be found days later via links.

A sitemap hands "here are my pages" directly to search engines, bypassing the slow link-crawling, so indexing is more complete and faster. For content sites that monetize search traffic via ads, this is SEO infrastructure, not optional.

A sitemap helps discovery — it does not guarantee indexing or ranking. Search engines still decide for themselves whether to index each page and how to rank it. But a page not in the sitemap is far less likely to be discovered.

2. How swift-web implements it: one unified @nuxtjs/sitemap

You don't need to install or configure anything — the sitemap already works. framework installs and configures @nuxtjs/sitemap as part of its SEO suite (sitemap + robots + schema-org):

  • Site domain — uses NUXT_PUBLIC_SITE_URL when set; follows the domain the visitor actually used when left empty (the same resolution as useSeo's canonical; see the SEO guide).
  • Excludes private paths by default — /login, /mine/**, /admin/** and other authenticated / admin pages are kept out of the sitemap by default (those shouldn't be indexed anyway).
  • Site-name fallback — the human-readable sitemap view (the styled page you see opening /sitemap.xml in a browser) titles itself with the nuxt-site-config site name; the framework fills it from app.head.title automatically (without it the view shows "undefined" — cosmetic only, crawlers read the raw XML).
  • Auto-references it in robots.txt — framework also installs @nuxtjs/robots, which automatically adds a Sitemap: https://your-domain/sitemap.xml line to robots.txt, no manual step needed (search engines find the sitemap when they read robots.txt).

In other words, sitemap in a swift-web project is a single pipeline, working out of the box.

::: tip Don't list @nuxtjs/sitemap again in nuxt.config modules framework already installs and configures it. If you list '@nuxtjs/sitemap' again in the modules array of nuxt.config.ts, you re-register it and may clobber framework's configured defaults (domain lock / private-path exclusion). Also don't write your own cron job / server route to generate sitemap.xml — that fights framework's pipeline (two systems competing for the same /sitemap.xml). Use framework's one pipeline. :::

3. Static routes: auto-included, zero config

Pages with a real route file (index.vue, about-us.vue, terms.vue under app/pages/) are automatically discovered and included by framework's sitemap — you do nothing.

Verifying is easy — run dev locally or after deploy, hit /sitemap.xml:

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

You'll see a <urlset> listing your static pages.

Excluding some pages

Private / admin pages are excluded by default. To additionally exclude some of your own paths (say, a temporary campaign page you don't want indexed), add to swift.config.json:

{
  "framework": {
    "seo": {
      "sitemap": { "exclude": ["/campaign-temp", "/preview/**"] }
    }
  }
}

** is a wildcard (matches everything under that prefix).

4. Dynamic routes: wire them via sources

This is the only part you need to do by hand.

The problem: dynamic detail pages generated by backend ID — e.g. /series/123, /series/456… (all from one file, app/pages/series/[id].vue) — can't be auto-discovered by framework. It only knows the [id].vue route file exists; it doesn't know which ids the backend actually has.

The fix: turn "which dynamic URLs exist" into a server endpoint (a source) for the sitemap module to fetch. Three steps:

① Write an endpoint that returns the URL list at server/routes/__sitemap__/urls.ts:

// server/routes/__sitemap__/urls.ts
export default defineSitemapEventHandler(async () => {
  const locs: string[] = []
  let page = 1
  // page through the backend ID list until it's empty
  while (true) {
    const ids = await nitroApiFetch<(string | number)[]>('/movie/sitemap', { page: String(page) })
    if (!ids?.length) break
    locs.push(...ids.map((id) => `/series/${id}`))
    page++
  }
  return locs.map((loc) => ({ loc, changefreq: 'weekly' as const, priority: 0.7 }))
})

defineSitemapEventHandler is a server helper from the sitemap module (auto-imported, no import needed); nitroApiFetch is framework's tool for calling the backend with no request context (such as this build-time / job scenario). /movie/sitemap is a backend contract endpoint returning "all IDs for a section" — adjust it to your project's backend.

⚠️ Put the endpoint under server/routes/__sitemap__/ (not under /api/) — the starter template ships a /api/** Nitro proxy (+ @nuxtjs/robots disallow /api/**); an endpoint under /api/ gets proxied to the backend instead of hitting your handler, so the module fetches no URLs.

② Wire the endpoint as a sitemap source in nuxt.config.ts:

// nuxt.config.ts
export default defineNuxtConfig({
  sitemap: {
    sources: ['/__sitemap__/urls'],
  },
})

③ Done. Hit /sitemap.xml, and those tens of thousands of /series/<id> URLs are all there.

The starter template already ships an example server/routes/__sitemap__/urls.ts (returning an empty array by default) plus the commented sitemap.sources config in nuxt.config. To go live, uncomment the fetch logic in the example and adjust it to your backend contract, then uncomment sitemap.sources in nuxt.config.

Stop generation when a mandatory source fails

seo.sitemap.mandatorySources?: string[] is disabled by default. Explicitly list sources that must enumerate completely. The framework uses the official sitemap:sources hook to read each source once through local GET without visitor cookies or Authorization, then passes inline URLs to the native sitemap XML pipeline. A missing configured source, HTTP failure, redirect, timeout, or invalid JSON raises a safe 503 with Cache-Control: no-store; it does not publish a static-only partial list or create a separate XML generator/parser. Sources must return a complete URL array or { urls: [...] } and must propagate pagination failures.

// nuxt.config.ts
export default defineNuxtConfig({
  swiftConfig: { seo: { sitemap: { mandatorySources: ['/__sitemap__/urls'] } } },
  sitemap: { sources: ['/__sitemap__/urls'] },
})

mandatorySources is an exact list of same-origin JSON endpoint paths. External URLs, query strings, fragments, directory traversal, and XML paths are unsupported. It does not verify cursor completeness, content eligibility, or indexing inside a source; the source must implement and verify those checks.

These APIs are local additions awaiting release under framework#430. Consumers must verify complete XML failure and recovery against their installed version; passing locally does not establish production deployment or indexing.

Video sitemap: entries with videos (fw#381)

For film / video sites, a detail-page entry can carry videos next to loc, producing Google's video sitemap extension (<video:video>). Zero framework config: @nuxtjs/sitemap supports it natively and the return type of defineSitemapEventHandler already includes videos, so you get type hints out of the box.

// server/routes/__sitemap__/urls.ts
export default defineSitemapEventHandler(async () => {
  const items = await nitroApiFetch<MovieRow[]>('/movie/sitemap')
  return items.map((m) => ({
    loc: `/series/${m.id}`,
    videos: [
      {
        title: m.title, // required
        description: m.summary, // required
        thumbnail_loc: m.seo.img, // required; a plain image crawlers can fetch — **never a .bnc encrypted image**
        player_loc: `https://your.site/series/${m.id}`, // at least one of content_loc / player_loc; the watch page
        duration: m.durationSeconds, // optional, in **seconds** (unlike VideoObject's ISO 8601 in JSON-LD)
        publication_date: m.publishedAt, // optional, ISO 8601
      },
    ],
  }))
})

Notes:

  • title / description / thumbnail_loc are required; give at least one of content_loc (the media file) or player_loc (the watch page). Do not put signed / expiring m3u8 URLs in content_loc — player_loc alone is fine
  • duration is in seconds; expiration_date / rating / view_count / family_friendly / restriction etc. use Google's field names verbatim, see the VideoEntry type
  • This is the same fact as useSeo({ video })'s VideoObject, in two outputs: the sitemap tells crawlers which video pages exist, the JSON-LD tells them what the video on this page is — keep title / thumbnail / duration consistent
  • One URL may carry several videos (multiple episodes on one page); entries without videos are emitted as plain <url> as before

It auto-splits when large

A single sitemap file has limits (see "Protocol cheat sheet" below: 50,000 URLs / 50MB). When exceeded, @nuxtjs/sitemap automatically splits into multiple child sitemaps plus an index file (/sitemap_index.xml) — you don't deal with chunking. For your ~10,000 scale, runtime generation by the module is more than enough.

::: tip No more legacy "generate static XML at deploy time" Some older projects do this: the deploy script calls the backend API and writes all URLs into static .xml files, which the server then serves. In swift-web you no longer need that — sitemap.sources lets the module fetch from your endpoint at request time, auto-split, and build the index, sparing you a pile of cron / chunking / index code.

Legacy: static XML at deploy timeswift-web: sitemap.sources
When generateddeploy script calls backend → writes static filesmodule calls your endpoint at request / prerender time
Who handles chunking / indexyou dothe module, automatically
Content freshnessneeds a redeploy to updateendpoint reflects the backend in real time
Amount of codea lot (cron + chunking + index + serving)a little (one endpoint + one config line)

(If you really want to save runtime cost, @nuxtjs/sitemap can also prerender to static files, but at ~10,000 scale it's unnecessary.) :::

5. Protocol cheat sheet (industry standard)

Sitemaps follow a public protocol (sitemaps.org 0.9, common to all major search engines). Day to day you rely on framework's auto-generation and never hand-write one, but knowing the rules helps you judge whether output is correct:

RuleValue / note
URLs per file50,000 max
File size50MB max (uncompressed)
Over the limitsplit into child sitemaps + a <sitemapindex> (module does it automatically)
Compressiongzip supported (.xml.gz); must still be ≤ 50MB uncompressed
<loc>the page URL, required, ≤ 2048 chars
<lastmod>last modified time (W3C date format, e.g. 2026-06-23); Google uses it, but requires it to be truly accurate
<changefreq> / <priority>change frequency / priority; Google largely ignores them — don't expect them to affect ranking

Key takeaway: priority / changefreq are "hint" fields in the protocol, but Google mostly ignores them now; what it actually uses is lastmod (and it must be accurate — faking it loses trust). So don't fuss over those two fields; getting lastmod right is more valuable.

6. Submitting to search engines

Once a sitemap exists, search engines still need to know it's there. Two channels (swift-web does the first for you):

  1. robots.txt reference (automatic) — framework's robots integration already writes Sitemap: https://your-domain/sitemap.xml into robots.txt, so search engines find it when they fetch robots.txt. Hit /robots.txt to confirm.
  2. Search engine webmaster console (manual, recommended) — submit the sitemap URL in Google Search Console / Bing Webmaster Tools. This is the most authoritative path and shows you "how many submitted / indexed / errored". Submit it manually once after launch.

7. Launch checklist

  • /sitemap.xml returns 200 + <urlset> (or /sitemap_index.xml when multi-sitemap)
  • Static pages (home / about / terms etc.) are all present
  • Private pages (login / mine / admin) are not present
  • Dynamic detail pages: if they need indexing, sitemap.sources is configured and the endpoint returns the right URLs
  • /robots.txt shows the Sitemap: reference
  • Projects that have prerendered pages set NUXT_PUBLIC_SITE_URL (no real request exists at build time, so the config value is the only source); with no prerendered pages, leaving it empty is valid — the sitemap follows the visited domain
  • Submitted once in Google Search Console after launch

See also