Skip to content

Commit 718dfd1

Browse files
committed
Filter the sitemap on noindex rather than on directory name
The exclusion list was growing one framework route at a time. #123 added 404 after it appeared in the sitemap; this branch added _not-found for the same reason; and the versioned-docs work in progress adds a third hand-written skip for the archived version directories. Each is the same rule discovered again: do not advertise a page that tells crawlers not to index it. That property is readable from the page itself, so collectPages now reads each index.html and skips the ones carrying a noindex robots meta. The name list keeps only the directories that hold no pages at all - _next, deb, rpm, images - where it is a traversal concern rather than an indexing decision. The tag is matched in either attribute order and tolerates content lists such as "noindex,nofollow", since Next.js and hand-written metadata do not agree on either. The final log line now reports how many pages were skipped, so a filter that starts matching too much is visible in the build output instead of silently shrinking the sitemap. Verified against a synthetic export carrying an indexable root, /docs, /docs/versions, /packages and /samples, plus three noindex pages written in three different tag forms and an excluded _next directory: five URLs emitted, three skipped, and out/404.html left alone as the file it is. Note that the archived-version case is now covered by this rule, so the docs/versions skip on the versioned-docs branch can go when it lands.
1 parent 3913fae commit 718dfd1

1 file changed

Lines changed: 41 additions & 17 deletions

File tree

scripts/generate-sitemap.mjs

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,41 @@ import path from 'node:path';
1111
const siteUrl = 'https://documentdb.io';
1212
const outDir = path.join(process.cwd(), 'out');
1313

14-
// Top-level build outputs that are not HTML pages: Next.js assets, the APT/RPM
15-
// package repositories, images, and the two forms the not-found page takes
16-
// (with trailingSlash the export emits out/404/index.html alongside
17-
// out/404.html, plus out/_not-found/index.html for the App Router's not-found
18-
// route - both serve noindex, so listing either one submits a URL that tells
19-
// crawlers not to index it). The packages workflow adds deb/ and rpm/ after
20-
// this script runs in the deploy job, but they are excluded here too so local
21-
// full builds behave identically. Note that out/packages/ is NOT excluded: it
22-
// is the exported /packages download page; the workflow only adds
23-
// release-info.json (not a page) next to it.
14+
// Top-level build outputs that are not pages at all: Next.js assets, the
15+
// APT/RPM package repositories, and images. The packages workflow adds deb/
16+
// and rpm/ after this script runs in the deploy job, but they are excluded
17+
// here too so local full builds behave identically. Note that out/packages/ is
18+
// NOT excluded: it is the exported /packages download page; the workflow only
19+
// adds release-info.json (not a page) next to it.
20+
//
21+
// Pages that exist but must not be advertised are handled by isNoindex()
22+
// rather than by name. That covers both forms the not-found route takes - with
23+
// trailingSlash the export writes out/404/index.html alongside out/404.html,
24+
// and the App Router adds out/_not-found/index.html - and anything else the
25+
// framework starts emitting later. Listing a noindex URL is what earns the
26+
// "submitted URL marked noindex" warning in Search Console, and a page already
27+
// states that about itself, so there is no second list to keep in sync.
2428
const excludedTopLevelDirectories = new Set([
2529
'_next',
2630
'deb',
2731
'rpm',
2832
'images',
29-
'404',
30-
'_not-found',
3133
]);
3234

35+
let noindexPagesSkipped = 0;
36+
37+
/**
38+
* True when the page asks crawlers not to index it. Reads the meta tag in
39+
* either attribute order and tolerates content lists such as "noindex,nofollow".
40+
*/
41+
function isNoindex(html) {
42+
return (html.match(/<meta\b[^>]*>/gi) ?? []).some(
43+
(tag) =>
44+
/\bname=["']?robots["']?/i.test(tag) &&
45+
/\bcontent=["'][^"']*\bnoindex\b/i.test(tag),
46+
);
47+
}
48+
3349
function xmlEscape(value) {
3450
return value
3551
.replace(/&/g, '&amp;')
@@ -49,10 +65,14 @@ function collectPages(directory, relativePath = '') {
4965
const indexFile = path.join(directory, 'index.html');
5066

5167
if (fs.existsSync(indexFile)) {
52-
pages.push({
53-
url: relativePath === '' ? '/' : `/${relativePath}/`,
54-
lastModified: fs.statSync(indexFile).mtime,
55-
});
68+
if (isNoindex(fs.readFileSync(indexFile, 'utf8'))) {
69+
noindexPagesSkipped += 1;
70+
} else {
71+
pages.push({
72+
url: relativePath === '' ? '/' : `/${relativePath}/`,
73+
lastModified: fs.statSync(indexFile).mtime,
74+
});
75+
}
5676
}
5777

5878
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
@@ -117,4 +137,8 @@ if (fs.existsSync(robotsPath)) {
117137
fs.writeFileSync(robotsPath, `User-agent: *\nAllow: /\n\n${sitemapLine}\n`);
118138
}
119139

120-
console.log(`Wrote out/sitemap.xml with ${pages.length} URLs and advertised it in out/robots.txt.`);
140+
console.log(
141+
`Wrote out/sitemap.xml with ${pages.length} URLs (skipped ${noindexPagesSkipped} noindex ${
142+
noindexPagesSkipped === 1 ? 'page' : 'pages'
143+
}) and advertised it in out/robots.txt.`,
144+
);

0 commit comments

Comments
 (0)