Skip to content

Commit 10fa9c1

Browse files
authored
feat: create sitemap generator (#520)
1 parent e64c2cc commit 10fa9c1

File tree

9 files changed

+170
-3
lines changed

9 files changed

+170
-3
lines changed

src/generators/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import llmsTxt from './llms-txt/index.mjs';
1414
import manPage from './man-page/index.mjs';
1515
import metadata from './metadata/index.mjs';
1616
import oramaDb from './orama-db/index.mjs';
17+
import sitemap from './sitemap/index.mjs';
1718
import web from './web/index.mjs';
1819

1920
export const publicGenerators = {
@@ -27,6 +28,7 @@ export const publicGenerators = {
2728
'api-links': apiLinks,
2829
'orama-db': oramaDb,
2930
'llms-txt': llmsTxt,
31+
sitemap,
3032
web,
3133
};
3234

src/generators/llms-txt/utils/buildApiDocLink.mjs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { BASE_URL } from '../../../constants.mjs';
21
import { transformNodeToString } from '../../../utils/unist.mjs';
2+
import { buildApiDocURL } from '../../../utils/url.mjs';
33

44
/**
55
* Retrieves the description of a given API doc entry. It first checks whether
@@ -38,8 +38,7 @@ export const getEntryDescription = entry => {
3838
export const buildApiDocLink = entry => {
3939
const title = entry.heading.data.name;
4040

41-
const path = entry.api_doc_source.replace(/^doc\//, '/docs/latest/');
42-
const url = new URL(path, BASE_URL);
41+
const url = buildApiDocURL(entry);
4342

4443
const link = `[${title}](${url})`;
4544

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<url>
2+
<loc>__LOC__</loc>
3+
<lastmod>__LASTMOD__</lastmod>
4+
<changefreq>__CHANGEFREQ__</changefreq>
5+
<priority>__PRIORITY__</priority>
6+
</url>

src/generators/sitemap/index.mjs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { readFile, writeFile } from 'node:fs/promises';
2+
import { join } from 'node:path';
3+
4+
import { BASE_URL } from '../../constants.mjs';
5+
import { createPageSitemapEntry } from './utils/createPageSitemapEntry.mjs';
6+
7+
/**
8+
* This generator generates a sitemap.xml file for search engine optimization
9+
*
10+
* @typedef {Array<ApiDocMetadataEntry>} Input
11+
*
12+
* @type {GeneratorMetadata<Input, string>}
13+
*/
14+
export default {
15+
name: 'sitemap',
16+
17+
version: '1.0.0',
18+
19+
description: 'Generates a sitemap.xml file for search engine optimization',
20+
21+
dependsOn: 'metadata',
22+
23+
/**
24+
* Generates a sitemap.xml file
25+
*
26+
* @param {Input} entries
27+
* @param {Partial<GeneratorOptions>} options
28+
* @returns {Promise<string>}
29+
*/
30+
async generate(entries, { output }) {
31+
const template = await readFile(
32+
join(import.meta.dirname, 'template.xml'),
33+
'utf-8'
34+
);
35+
36+
const entryTemplate = await readFile(
37+
join(import.meta.dirname, 'entry-template.xml'),
38+
'utf-8'
39+
);
40+
41+
const lastmod = new Date().toISOString().split('T')[0];
42+
43+
const apiPages = entries
44+
.filter(entry => entry.heading.depth === 1)
45+
.map(entry => createPageSitemapEntry(entry, lastmod));
46+
47+
const { href: loc } = new URL('/docs/latest/api/', BASE_URL);
48+
49+
/**
50+
* @typedef {import('./types').SitemapEntry}
51+
*/
52+
const mainPage = {
53+
loc,
54+
lastmod,
55+
changefreq: 'daily',
56+
priority: '1.0',
57+
};
58+
59+
apiPages.push(mainPage);
60+
61+
const urlset = apiPages
62+
.map(page =>
63+
entryTemplate
64+
.replace('__LOC__', page.loc)
65+
.replace('__LASTMOD__', page.lastmod)
66+
.replace('__CHANGEFREQ__', page.changefreq)
67+
.replace('__PRIORITY__', page.priority)
68+
)
69+
.join('');
70+
71+
const sitemap = template.replace('__URLSET__', urlset);
72+
73+
if (output) {
74+
await writeFile(join(output, 'sitemap.xml'), sitemap, 'utf-8');
75+
}
76+
77+
return sitemap;
78+
},
79+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3+
__URLSET__
4+
</urlset>

src/generators/sitemap/types.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export interface SitemapEntry {
2+
loc: string;
3+
lastmod?: string;
4+
changefreq?:
5+
| 'always'
6+
| 'hourly'
7+
| 'daily'
8+
| 'weekly'
9+
| 'monthly'
10+
| 'yearly'
11+
| 'never';
12+
priority?: string;
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { buildApiDocURL } from '../../../utils/url.mjs';
2+
3+
/**
4+
* Builds an API doc sitemap url.
5+
*
6+
* @param {ApiDocMetadataEntry} entry
7+
* @param {string} lastmod
8+
* @returns {import('../types').SitemapEntry}
9+
*/
10+
export const createPageSitemapEntry = (entry, lastmod) => {
11+
const { href } = buildApiDocURL(entry, true);
12+
13+
return { loc: href, lastmod, changefreq: 'weekly', priority: '0.8' };
14+
};

src/utils/__tests__/url.test.mjs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
import { buildApiDocURL } from '../url.mjs';
5+
6+
const BASE = 'https://nodejs.org/';
7+
8+
describe('buildApiDocURL', () => {
9+
it('builds markdown doc URLs from doc/ sources', () => {
10+
const entry = { api_doc_source: 'doc/api/fs.md' };
11+
12+
const result = buildApiDocURL(entry);
13+
14+
assert.equal(result.href, `${BASE}docs/latest/api/fs.md`);
15+
});
16+
17+
it('builds html doc URLs when requested', () => {
18+
const entry = { api_doc_source: 'doc/api/path.md' };
19+
20+
const result = buildApiDocURL(entry, true);
21+
22+
assert.equal(result.href, `${BASE}docs/latest/api/path.html`);
23+
});
24+
25+
it('leaves non doc/ sources untouched', () => {
26+
const entry = { api_doc_source: 'api/crypto.md' };
27+
28+
const result = buildApiDocURL(entry);
29+
30+
assert.equal(result.href, `${BASE}api/crypto.md`);
31+
});
32+
});

src/utils/url.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { BASE_URL } from '../constants.mjs';
2+
3+
/**
4+
* Builds the url of a api doc entry.
5+
*
6+
* @param {ApiDocMetadataEntry} entry
7+
* @param {boolean} [useHtml]
8+
* @returns {URL}
9+
*/
10+
export const buildApiDocURL = (entry, useHtml = false) => {
11+
const path = entry.api_doc_source.replace(/^doc\//, '/docs/latest/');
12+
13+
if (useHtml) {
14+
return URL.parse(path.replace(/\.md$/, '.html'), BASE_URL);
15+
}
16+
17+
return URL.parse(path, BASE_URL);
18+
};

0 commit comments

Comments
 (0)