diff --git a/.config/typedoc.json b/.config/typedoc.json index d0186774a..55582c697 100644 --- a/.config/typedoc.json +++ b/.config/typedoc.json @@ -21,6 +21,7 @@ "treatWarningsAsErrors": false, "categorizeByGroup": false, "categoryOrder": ["Reflections", "Types", "Comments", "*"], + "sitemapBaseUrl": "https://typedoc.org/api/", "validation": { "notExported": true, "invalidLink": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 0318a7c82..b6d086880 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +## Features + +- Added a new `--sitemapBaseUrl` option. When specified, TypeDoc will generate a `sitemap.xml` in your output folder that describes the site, #2480. + ## v0.25.7 (2024-01-08) ### Bug Fixes diff --git a/src/lib/output/plugins/SitemapPlugin.ts b/src/lib/output/plugins/SitemapPlugin.ts new file mode 100644 index 000000000..dff37af31 --- /dev/null +++ b/src/lib/output/plugins/SitemapPlugin.ts @@ -0,0 +1,94 @@ +import Path from "path"; +import { Component, RendererComponent } from "../components"; +import { RendererEvent } from "../events"; +import { DefaultTheme } from "../themes/default/DefaultTheme"; +import { Option, writeFile } from "../../utils"; +import { escapeHtml } from "../../utils/html"; + +@Component({ name: "sitemap" }) +export class SitemapPlugin extends RendererComponent { + @Option("sitemapBaseUrl") + accessor sitemapBaseUrl!: string; + + override initialize() { + this.listenTo(this.owner, RendererEvent.BEGIN, this.onRendererBegin); + } + + private onRendererBegin(event: RendererEvent) { + if (!(this.owner.theme instanceof DefaultTheme)) { + return; + } + if (event.isDefaultPrevented || !this.sitemapBaseUrl) { + return; + } + + this.owner.preRenderAsyncJobs.push((event) => this.buildSitemap(event)); + } + + private async buildSitemap(event: RendererEvent) { + // cSpell:words lastmod urlset + const sitemapXml = Path.join(event.outputDirectory, "sitemap.xml"); + const lastmod = new Date(this.owner.renderStartTime).toISOString(); + + const urls: XmlElementData[] = + event.urls?.map((url) => { + return { + tag: "url", + children: [ + { + tag: "loc", + children: new URL( + url.url, + this.sitemapBaseUrl, + ).toString(), + }, + { + tag: "lastmod", + children: lastmod, + }, + ], + }; + }) ?? []; + + const sitemap = + `\n` + + stringifyXml({ + tag: "urlset", + attr: { xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" }, + children: urls, + }) + + "\n"; + + await writeFile(sitemapXml, sitemap); + } +} + +interface XmlElementData { + attr?: Record; + tag: string; + children: XmlElementData[] | string; +} + +function stringifyXml(xml: XmlElementData, indent = 0) { + const parts = ["\t".repeat(indent), "<", xml.tag]; + + for (const [key, val] of Object.entries(xml.attr || {})) { + parts.push(" ", key, '="', escapeHtml(val), '"'); + } + + parts.push(">"); + + if (typeof xml.children === "string") { + parts.push(escapeHtml(xml.children)); + } else { + for (const child of xml.children) { + parts.push("\n"); + parts.push(stringifyXml(child, indent + 1)); + } + parts.push("\n", "\t".repeat(indent)); + } + + parts.push(""); + + return parts.join(""); +} diff --git a/src/lib/output/plugins/index.ts b/src/lib/output/plugins/index.ts index a6fd002ba..62a995442 100644 --- a/src/lib/output/plugins/index.ts +++ b/src/lib/output/plugins/index.ts @@ -2,3 +2,4 @@ export { MarkedPlugin } from "../themes/MarkedPlugin"; export { AssetsPlugin } from "./AssetsPlugin"; export { JavascriptIndexPlugin } from "./JavascriptIndexPlugin"; export { NavigationPlugin } from "./NavigationPlugin"; +export { SitemapPlugin } from "./SitemapPlugin"; diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index 6bfb0f880..8317c2340 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -131,6 +131,7 @@ export interface TypeDocOptionMap { cname: string; htmlLang: string; githubPages: boolean; + sitemapBaseUrl: string; cacheBust: boolean; gaID: string; hideGenerator: boolean; diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index 0116d7070..90260a687 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -372,6 +372,17 @@ export function addTypeDocOptions(options: Pick) { type: ParameterType.Boolean, defaultValue: true, }); + options.addDeclaration({ + name: "sitemapBaseUrl", + 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.", + validate(value) { + if (!/https?:\/\//.test(value)) { + throw new Error( + "sitemapBaseUrl must start with http:// or https://", + ); + } + }, + }); options.addDeclaration({ name: "htmlLang", help: "Sets the lang attribute in the generated html tag.",