Fix: Qwik makes empty sitemap.xml

Published: (December 2, 2025 at 12:50 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Problem

When deploying a Qwik (or Qwik City) application with the Node.js adapter (nodeServerAdapter), the sitemap.xml works locally but is empty in production:

<!-- empty sitemap -->
  • Local build: ✅ works
  • Dev server: ✅ serves the sitemap correctly
  • Production build: ❌ empty sitemap, no warnings

The Node.js adapter runs Static Site Generation (SSG) during the build, even for SSR builds. During this step it:

  1. Scans routes for static‑exportable pages
  2. Generates a sitemap from the found routes
  3. Overwrites any public/sitemap.xml you placed manually

If the SSG step cannot find any exportable routes (e.g., dynamic routes, SSR‑only routes, or when using the Node.js adapter), it produces an empty sitemap.


Quick Fix – Disable SSG

For a pure SSR application you can completely turn off SSG in the adapter configuration.

adapters/node-server/vite.config.ts

import { nodeServerAdapter } from "@builder.io/qwik-city/adapters/node-server/vite";
import { extendConfig } from "@builder.io/qwik-city/vite";
import baseConfig from "../../vite.config";

export default extendConfig(baseConfig, () => ({
  build: {
    ssr: true,
    rollupOptions: {
      input: ["src/entry.express.tsx", "@qwik-city-plan"],
    },
  },
  plugins: [
    nodeServerAdapter({
      name: "express",
      // ⚠️ CRITICAL: Completely disable SSG – use a static sitemap.xml
      ssg: null,
    }),
  ],
}));

Key change: ssg: null stops the automatic sitemap generation, so the file you place in public/ is served unchanged.


Provide a Static Sitemap

Create public/sitemap.xml with the URLs you want indexed.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://yourdomain.com/</loc>
    <lastmod>2024-12-01</lastmod>
    <changefreq>weekly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://yourdomain.com/about/</loc>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

Multi‑language example

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://yourdomain.com/</loc>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://yourdomain.com/es/</loc>
    <priority>0.8</priority>
  </url>
  <url>
    <loc>https://yourdomain.com/de/</loc>
    <priority>0.8</priority>
  </url>
</urlset>

Adjust Build Scripts

If your package.json runs qwik city collect, remove it—otherwise SSG will be forced again.

{
  "scripts": {
    // Before
    // "build:deploy": "npm run build.production && qwik city collect"

    // After
    "build:deploy": "npm run build.production"
  }
}

Run the build and verify:

npm run build:deploy
ls -lh dist/sitemap.xml
cat dist/sitemap.xml

You should see the full sitemap content (not an empty file).

The Node.js server will now serve dist/sitemap.xml as a static asset:

node server.js
curl http://localhost:3000/sitemap.xml

Dynamic Sitemap (SSR Route)

If the sitemap must be generated on‑the‑fly (e.g., from a CMS), create an SSR route.

src/routes/sitemap.xml/index.ts

import type { RequestHandler } from '@builder.io/qwik-city';

const SUPPORTED_LOCALES = ['en-US', 'es', 'ru', 'de', 'fr', 'ja'];

export const onGet: RequestHandler = async ({ url }) => {
  const origin = url.origin;

  const urls = [
    // Root URL
    `  
    ${origin}/
    ${new Date().toISOString().split('T')[0]}
    weekly
    1.0
  `,
    // Language‑specific URLs
    ...SUPPORTED_LOCALES
      .filter(l => l !== 'en-US')
      .map(l => `  
    ${origin}/${l}/
    weekly
    0.8
  `),
  ].join('\n');

  const sitemap = `

${urls}
`;

  return new Response(sitemap, {
    status: 200,
    headers: {
      'Content-Type': 'application/xml; charset=utf-8',
      'Cache-Control': 'public, max-age=86400', // 1 day
    },
  });
};

Note: Keep ssg: null in the adapter config to avoid the static‑generation step overwriting this dynamic route.


Why SSG Runs with the Node.js Adapter

  • Static Site Generation (SSG) – pre‑renders pages at build time, generates a sitemap, and writes static files.
  • Server‑Side Rendering (SSR) – renders pages on demand at runtime.

The Node.js adapter is intended for SSR, but it still triggers SSG by default to produce a sitemap and pre‑render any pages that have onStaticGenerate. When routes are dynamic or lack onStaticGenerate, SSG finds zero exportable routes and writes an empty sitemap.


Common Pitfalls

IssueWhy it HappensFix
ssg: { exclude: ['*'] }Excludes files but still runs SSG, which still overwrites the sitemap.Use ssg: null.
qwik city collect in build scriptExplicitly runs SSG after the build, overwriting your static file.Remove the command.
Relying on public/ only for dynamic contentDynamic routes need an SSR handler, not a static file.Implement an SSR route as shown above.
dist/ cleared on each buildStatic files must live in public/, not copied manually to dist/.Keep public/sitemap.xml.

Checklist After Applying the Fix

  • Build finishes without “Starting Qwik City SSG…” (when ssg: null).
  • dist/sitemap.xml exists and is larger than the empty 109‑byte file.
  • cat dist/sitemap.xml shows the expected URLs.
  • Production server returns the full sitemap (curl https://yourdomain.com/sitemap.xml).
  • No SSG‑related warnings or errors appear in the build logs.

Optional: Redirect /sitemap.xml/ to /sitemap.xml

If a trailing slash is requested, add a simple redirect in your server entry:

import { createServer } from "http";

createServer((req, res) => {
  if (req.url === "/sitemap.xml/") {
    res.writeHead(301, { Location: "/sitemap.xml" });
    res.end();
    return;
  }

  // ...rest of server logic
}).listen(3000);

The redirect is harmless for search engines but can tidy up URLs.


Choosing Between a Static Sitemap and an SSR Route

Use‑caseRecommended Approach
Pure SSR app, routes known at dev time, sitemap rarely changesStatic sitemap (public/sitemap.xml + ssg: null)
Sitemap data comes from a CMS, database, or user‑generated contentSSR route (src/routes/sitemap.xml/index.ts)
Need real‑time updates or per‑locale logicSSR route
Simple deployment, no extra build stepsStatic sitemap

Implement the approach that matches your project’s needs, and the empty‑sitemap issue will be resolved.

Back to Blog

Related posts

Read more »

What Happens When You Run Python Code?

Python is a popular programming language, but have you ever wondered what happens behind the scenes when you run a Python program on your computer? In this arti...