Skip to content

Commit aa9afef

Browse files
committed
Add sitemapBaseUrl option
Resolves #2480
1 parent 0dd9d08 commit aa9afef

File tree

6 files changed

+112
-0
lines changed

6 files changed

+112
-0
lines changed

.config/typedoc.json

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"treatWarningsAsErrors": false,
2222
"categorizeByGroup": false,
2323
"categoryOrder": ["Reflections", "Types", "Comments", "*"],
24+
"sitemapBaseUrl": "https://typedoc.org/api/",
2425
"validation": {
2526
"notExported": true,
2627
"invalidLink": true,

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Unreleased
22

3+
## Features
4+
5+
- Added a new `--sitemapBaseUrl` option. When specified, TypeDoc will generate a `sitemap.xml` in your output folder that describes the site, #2480.
6+
37
## v0.25.7 (2024-01-08)
48

59
### Bug Fixes
+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import Path from "path";
2+
import { Component, RendererComponent } from "../components";
3+
import { RendererEvent } from "../events";
4+
import { DefaultTheme } from "../themes/default/DefaultTheme";
5+
import { Option, writeFile } from "../../utils";
6+
import { escapeHtml } from "../../utils/html";
7+
8+
@Component({ name: "sitemap" })
9+
export class SitemapPlugin extends RendererComponent {
10+
@Option("sitemapBaseUrl")
11+
accessor sitemapBaseUrl!: string;
12+
13+
override initialize() {
14+
this.listenTo(this.owner, RendererEvent.BEGIN, this.onRendererBegin);
15+
}
16+
17+
private onRendererBegin(event: RendererEvent) {
18+
if (!(this.owner.theme instanceof DefaultTheme)) {
19+
return;
20+
}
21+
if (event.isDefaultPrevented || !this.sitemapBaseUrl) {
22+
return;
23+
}
24+
25+
this.owner.preRenderAsyncJobs.push((event) => this.buildSitemap(event));
26+
}
27+
28+
private async buildSitemap(event: RendererEvent) {
29+
// cSpell:words lastmod urlset
30+
const sitemapXml = Path.join(event.outputDirectory, "sitemap.xml");
31+
const lastmod = new Date(this.owner.renderStartTime).toISOString();
32+
33+
const urls: XmlElementData[] =
34+
event.urls?.map((url) => {
35+
return {
36+
tag: "url",
37+
children: [
38+
{
39+
tag: "loc",
40+
children: new URL(
41+
url.url,
42+
this.sitemapBaseUrl,
43+
).toString(),
44+
},
45+
{
46+
tag: "lastmod",
47+
children: lastmod,
48+
},
49+
],
50+
};
51+
}) ?? [];
52+
53+
const sitemap =
54+
`<?xml version="1.0" encoding="UTF-8"?>\n` +
55+
stringifyXml({
56+
tag: "urlset",
57+
attr: { xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" },
58+
children: urls,
59+
}) +
60+
"\n";
61+
62+
await writeFile(sitemapXml, sitemap);
63+
}
64+
}
65+
66+
interface XmlElementData {
67+
attr?: Record<string, string>;
68+
tag: string;
69+
children: XmlElementData[] | string;
70+
}
71+
72+
function stringifyXml(xml: XmlElementData, indent = 0) {
73+
const parts = ["\t".repeat(indent), "<", xml.tag];
74+
75+
for (const [key, val] of Object.entries(xml.attr || {})) {
76+
parts.push(" ", key, '="', escapeHtml(val), '"');
77+
}
78+
79+
parts.push(">");
80+
81+
if (typeof xml.children === "string") {
82+
parts.push(escapeHtml(xml.children));
83+
} else {
84+
for (const child of xml.children) {
85+
parts.push("\n");
86+
parts.push(stringifyXml(child, indent + 1));
87+
}
88+
parts.push("\n", "\t".repeat(indent));
89+
}
90+
91+
parts.push("</", xml.tag, ">");
92+
93+
return parts.join("");
94+
}

src/lib/output/plugins/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { MarkedPlugin } from "../themes/MarkedPlugin";
22
export { AssetsPlugin } from "./AssetsPlugin";
33
export { JavascriptIndexPlugin } from "./JavascriptIndexPlugin";
44
export { NavigationPlugin } from "./NavigationPlugin";
5+
export { SitemapPlugin } from "./SitemapPlugin";

src/lib/utils/options/declaration.ts

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export interface TypeDocOptionMap {
131131
cname: string;
132132
htmlLang: string;
133133
githubPages: boolean;
134+
sitemapBaseUrl: string;
134135
cacheBust: boolean;
135136
gaID: string;
136137
hideGenerator: boolean;

src/lib/utils/options/sources/typedoc.ts

+11
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,17 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
372372
type: ParameterType.Boolean,
373373
defaultValue: true,
374374
});
375+
options.addDeclaration({
376+
name: "sitemapBaseUrl",
377+
help: "Specify a base URL to be used in generating a sitemap.xml in our output folder. If not specified, no sitemap will be generated.",
378+
validate(value) {
379+
if (!/https?:\/\//.test(value)) {
380+
throw new Error(
381+
"sitemapBaseUrl must start with http:// or https://",
382+
);
383+
}
384+
},
385+
});
375386
options.addDeclaration({
376387
name: "htmlLang",
377388
help: "Sets the lang attribute in the generated html tag.",

0 commit comments

Comments
 (0)