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_URLwhen set; follows the domain the visitor actually used when left empty (the same resolution asuseSeo'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.xmlin a browser) titles itself with the nuxt-site-config site name; the framework fills it fromapp.head.titleautomatically (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 aSitemap: https://your-domain/sitemap.xmlline torobots.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 }))
})
defineSitemapEventHandleris a server helper from the sitemap module (auto-imported, no import needed);nitroApiFetchis framework's tool for calling the backend with no request context (such as this build-time / job scenario)./movie/sitemapis 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/robotsdisallow/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 commentedsitemap.sourcesconfig in nuxt.config. To go live, uncomment the fetch logic in the example and adjust it to your backend contract, then uncommentsitemap.sourcesin 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_locare required; give at least one ofcontent_loc(the media file) orplayer_loc(the watch page). Do not put signed / expiring m3u8 URLs incontent_loc—player_localone is finedurationis in seconds;expiration_date/rating/view_count/family_friendly/restrictionetc. use Google's field names verbatim, see theVideoEntrytype- This is the same fact as
useSeo({ video })'sVideoObject, 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
videosare 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 time | swift-web: sitemap.sources | |
|---|---|---|
| When generated | deploy script calls backend → writes static files | module calls your endpoint at request / prerender time |
| Who handles chunking / index | you do | the module, automatically |
| Content freshness | needs a redeploy to update | endpoint reflects the backend in real time |
| Amount of code | a 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:
| Rule | Value / note |
|---|---|
| URLs per file | 50,000 max |
| File size | 50MB max (uncompressed) |
| Over the limit | split into child sitemaps + a <sitemapindex> (module does it automatically) |
| Compression | gzip 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/changefreqare "hint" fields in the protocol, but Google mostly ignores them now; what it actually uses islastmod(and it must be accurate — faking it loses trust). So don't fuss over those two fields; gettinglastmodright 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):
- robots.txt reference (automatic) — framework's robots integration already writes
Sitemap: https://your-domain/sitemap.xmlintorobots.txt, so search engines find it when they fetch robots.txt. Hit/robots.txtto confirm. - 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.xmlreturns 200 +<urlset>(or/sitemap_index.xmlwhen 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.sourcesis configured and the endpoint returns the right URLs -
/robots.txtshows theSitemap: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
- SEO Best Practices — useSeo / canonical / structured data / Core Web Vitals
- Rendering Strategies — which rendering strategy the indexed pages should use
- sitemaps.org protocol · Google: Build and submit a sitemap · @nuxtjs/sitemap docs