Guides · Node.js & Express · Published 2026-09-13 · 3 min read

Serving sitemap.xml, robots.txt and ads.txt From Express — Generated From Content, Cached in Memory, and Configured by Environment Variables

Three small text endpoints every content site needs. How to build sitemap.xml from the content index with lastmod and alternates, keep robots.txt honest, serve ads.txt from an environment variable so ad networks can be added without a code change, and cache all three.

A content site has three plain-text endpoints that machines read and humans never see. They are easy to get slightly wrong: a sitemap that lists draft pages, a robots file that blocks CSS, an ads.txt that is a day behind the ad network's requirement. Serving them from Express, generated from the same data as the pages, keeps them correct without maintenance.

sitemap.xml

Build it from the content index, not by hand. For each language and each published article, emit a <url> with <loc> as the absolute URL, <lastmod> from the article's updated date, and, for pages that have translations, the xhtml:link rel="alternate" entries. Include the home page and category pages. Leave out anything unlisted, redirected or paginated, and never include URLs that return anything but 200.

app.get('/sitemap.xml', (req, res) => {
  res.type('application/xml').send(buildSitemap()); // cached, see below
});

Set the content type explicitly; some crawlers reject a sitemap served as text/html. If the site grows past tens of thousands of URLs, split it into a sitemap index, but a small site should keep the single file. The alternates format is described in a multi-language Express site with hreflang.

robots.txt

Keep it short and honest:

User-agent: *
Allow: /
Disallow: /admin/
Sitemap: https://example.com/sitemap.xml

Do not block the static folder; crawlers need CSS and JavaScript to render pages. Do not try to hide pages with Disallow; that only hides them from crawlers that obey, and they may still be indexed from links. For a redirect subdomain or an admin app, Disallow: / is correct because there is nothing to index. Serve it as text/plain.

ads.txt from an environment variable

Ad networks require ads.txt at the site root listing the seller accounts authorised to sell your inventory. The content changes when you add a network, and it is different per site. Rather than committing a file, read it from an environment variable at startup:

const ADS_TXT = process.env.ADS_TXT || '';
app.get('/ads.txt', (req, res) => {
  if (!ADS_TXT) return res.status(404).type('text/plain').send('');
  res.type('text/plain').send(ADS_TXT.replace(/\\n/g, '\n'));
});

Set the variable at deploy time with the lines separated by \n. Adding a network is then a redeploy with a new value, with no code change and no chance of committing one site's ads.txt to another site's repository. Some networks instead want ads.txt to redirect to a URL they host; a 301 from the same route handles that.

Caching all three

Generating the sitemap walks the content index; cheap, but not free on every request. Cache the generated string in memory and invalidate it when the content index reloads. Send Cache-Control: public, max-age=3600 on all three so edge caches and repeated crawler fetches do not hit the app. On Cloud Run this also reduces cold-start exposure; see what a small Cloud Run site really costs.

Verifying

Fetch each with curl -i after deploying and check the status, content type and first lines. Then submit the sitemap in Search Console, as covered in Search Console domain verification with Cloudflare, and check the ad network's dashboard for its ads.txt status a day later.

Common mistakes

Summary

Generate the sitemap from the content index with lastmod and alternates, keep robots.txt to a few honest lines with the sitemap URL, serve ads.txt from an environment variable, and cache all three with explicit content types. Check them with curl after every deploy.

Related guides