import { Err, Try, TryAsync } from "@/modules/utility/result"
import type { Types } from "./types"
type SiteMapper = Map<string, DynamicMap | StaticMap>
export class Cartographer {
#sitemap: SiteMapper = new Map()
#config: Types.ConcertConfig
constructor(config: Types.Config) {
defaultChangeFrequency: "weekly",
defaultLastMod: new Date(),
dynamic(key: string, { fetcher, counter }: Types.DynamicInput) {
this.#sitemap.set(key, new DynamicMap(key, fetcher, counter, this.#config))
static(key: string, input: Types.StaticInput) {
this.#sitemap.set(`${key}.xml`, new StaticMap(key, input, this.#config))
return new CartographerToolbelt(this.#sitemap, this.#config)
class CartographerToolbelt {
#config: Types.ConcertConfig
constructor(sitemap: SiteMapper, config: Types.ConcertConfig) {
...args: Parameters<Types.ConcertConfig["logger"]["error"]>
this.#config.logger.error(args[0], args[1])
return new Response("Not Found", {
"Content-Type": "text/plain",
#format(entries: Types.SitemapEntry[]): string[] {
return entries.map((entry) => {
const slug = new URL(url).pathname
const priority = entry.priority || this.#config.priorityFn(slug)
let lastModified = entry.lastModified || this.#config.defaultLastMod
typeof lastModified === "string"
: lastModified.toISOString()
`<loc>${entry.url}</loc>`,
`<priority>${priority}</priority>`,
`<changefreq>${entry.changeFrequency || this.#config.defaultChangeFrequency}</changefreq>`,
`<lastmod>${lastModified}</lastmod>`,
return `<url>${root.join("")}</url>`
async $survey(): Promise<Response> {
const promises: Promise<string[]>[] = []
for (const map of this.#sitemap.values()) {
promises.push(map.$list())
const sitemaps = await Promise.all(promises)
'<?xml version="1.0" encoding="UTF-8"?>',
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
...sitemaps.flat().map((url) => `<sitemap><loc>${url}</loc></sitemap>`),
return new Response(xml, {
"Content-Type": "application/xml",
async $draw(_: unknown, data: Types.Segment): Promise<Response> {
const params = await data.params
const map = this.#sitemap.get(params.key)
return this.#abort(`${params.key} is not a registered sitemap`)
const entries = map instanceof StaticMap
? Try(() => map.$draw())()
: await TryAsync(() => map.$draw(params))()
return this.#abort(`Unable to generate entries for ${params.key}`, {
const body = this.#format(entries)
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">',
return new Response(xml, {
"Content-Type": "application/xml",
#fetcher: Types.DynamicFetcher
#config: Types.ConcertConfig
#counter: Types.DynamicCounter
fetcher: Types.DynamicFetcher,
counter: Types.DynamicCounter,
config: Types.ConcertConfig,
extractPageId(id: string) {
const withoutExtension = id.replace(/\.xml$/, "")
const asNumber = Number(withoutExtension)
if (Number.isNaN(asNumber)) {
throw new Error("Invalid page id, must be a number")
return Math.floor(asNumber)
params: Awaited<Types.Segment["params"]>,
): Promise<Types.SitemapEntry[]> {
if (!params.page || params.page.length !== 1) {
throw new Error("Dynamic sitemaps require a page parameter")
const page = this.extractPageId(params.page[0])
const count = await this.#counter()
const result = await TryAsync(this.#fetcher)(
this.#config.urlsPerPage,
if (Err(result)) throw result
return result.map((value) => {
value.url = new URL(value.url, this.#config.domain).href
async $list(): Promise<string[]> {
const count = await this.#counter()
if (count === 0) return []
const pages = Math.ceil(count / this.#config.urlsPerPage)
return Array.from({ length: pages }).map((_, index) => {
return `${this.#config.domain}/sitemap/${this.#key}/${index}.xml`
#input: Types.StaticInput
#config: Types.ConcertConfig
input: Types.StaticInput,
config: Types.ConcertConfig,
$draw(): Types.SitemapEntry[] {
return this.#input.map((value) => {
return typeof value === "string"
? { url: new URL(value, this.#config.domain).href }
async $list(): Promise<string[]> {
return [`${this.#config.domain}/sitemap/${this.#key}.xml`]