diff --git a/package.json b/package.json index 321774c4..c5b9a1d2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dev": "vite dev", "dev:port": "vite dev --port", "prebuild": "bun run generate:changelog && bun run scripts/copy-docs-images.cjs", - "build": "NODE_OPTIONS=--max-old-space-size=5120 vite build && bun run scripts/generate-static-cache.ts && bun run scripts/generate-search-index.ts", + "build": "NODE_OPTIONS=--max-old-space-size=5120 vite build && bun run scripts/generate-static-cache.ts && bun run scripts/generate-search-index.ts && bun run scripts/generate-sitemap.ts", "build:cf": "bun run build", "build:cf:staging": "CLOUDFLARE_ENV=staging bun run build", "sync:mixedbread": "mxbai vs sync $MIXEDBREAD_STORE_ID './content/docs' --ci", diff --git a/scripts/generate-sitemap.ts b/scripts/generate-sitemap.ts new file mode 100644 index 00000000..d22a5854 --- /dev/null +++ b/scripts/generate-sitemap.ts @@ -0,0 +1,269 @@ +/** + * Post-build script: generates a static `sitemap.xml` with real `` + * dates derived from git history. + * + * Cloudflare Workers have no filesystem / git at request time, so the dates are + * resolved here (Node.js, full repo) and baked into a static asset served at + * `/docs/sitemap.xml` — the same delivery path as `search-index.json`. + * + * Usage: bun run scripts/generate-sitemap.ts + * Called automatically as part of `bun run build` (after `vite build`, which + * empties `dist/`, so this must run last alongside the other generators). + */ + +import { execFile } from "node:child_process"; +import { readFileSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import { createServer } from "vite"; +import react from "@vitejs/plugin-react"; +import tsConfigPaths from "vite-tsconfig-paths"; +import mdx from "fumadocs-mdx/vite"; + +const execFileAsync = promisify(execFile); + +const DIST_CLIENT = path.join(process.cwd(), "dist/client"); +const OUTPUT_PATH = path.join(DIST_CLIENT, "docs/sitemap.xml"); + +/** + * Sentinel prefixing each commit-date line in the `git log` output so it can + * never be mistaken for a file path (no tracked path begins with it). + */ +const COMMIT_PREFIX = "@@commit@@"; + +/** + * Fumadocs `relative/path.mdx` directives. A page's rendered + * body includes these files, so an edit to a shared include must count toward + * the page's `lastmod` even though the wrapper file itself didn't change. + */ +const INCLUDE_PATTERN = /]*>([\s\S]*?)<\/include>/g; + +/** + * Extra tracked data files a page renders through a React component (not via + * ``), so an edit to the data bumps the page's `lastmod`. Keyed by the + * wrapper's repo-relative path. Extend this when a page's rendered output depends + * on a committed data file imported by a component. + */ +const SUPPLEMENTAL_SOURCES: Record = { + // /docs/changelog renders , which imports this generated, + // committed JSON — changelog regenerations don't touch the wrapper MDX. + "content/docs/changelog/index.mdx": ["src/lib/changelog-entries.json"], +}; + +/** + * One `git log` pass, newest commit first, mapping each file to the date of the + * most recent commit that touched it (YYYY-MM-DD). First occurrence wins because + * the log is in reverse-chronological order. Returns an empty map (never throws) + * so a git problem degrades to a date-less sitemap instead of failing the build. + */ +async function buildGitDateMap(): Promise> { + const dates = new Map(); + + let stdout: string; + try { + ({ stdout } = await execFileAsync( + "git", + [ + "-c", + "core.quotePath=false", + "log", + "--no-merges", + "--name-only", + `--pretty=format:${COMMIT_PREFIX}%cs`, + ], + { maxBuffer: 1024 * 1024 * 128 }, + )); + } catch (err) { + console.warn( + ` ⚠️ Could not read git history (${(err as Error).message}); ` + + "emitting sitemap without .", + ); + return dates; + } + + let currentDate: string | undefined; + for (const line of stdout.split("\n")) { + if (line.startsWith(COMMIT_PREFIX)) { + currentDate = line.slice(COMMIT_PREFIX.length).trim() || undefined; + continue; + } + const file = line.trim(); + if (!file || !currentDate) continue; + if (!dates.has(file)) dates.set(file, currentDate); + } + + return dates; +} + +/** + * Direct dependencies of a file as repo-relative POSIX paths: its `` + * targets (for `.mdx`) plus any supplemental component-data files mapped to it. + */ +const includeCache = new Map(); +function directIncludes(relPath: string): string[] { + const cached = includeCache.get(relPath); + if (cached) return cached; + + const targets: string[] = []; + if (relPath.endsWith(".mdx")) { + try { + const content = readFileSync(path.join(process.cwd(), relPath), "utf8"); + const fromDir = path.posix.dirname(relPath); + for (const match of content.matchAll(INCLUDE_PATTERN)) { + const rel = match[1].trim(); + if (rel) targets.push(path.posix.normalize(path.posix.join(fromDir, rel))); + } + } catch { + // Unreadable wrapper: fall back to whatever supplemental sources are mapped. + } + } + + targets.push(...(SUPPLEMENTAL_SOURCES[relPath] ?? [])); + + includeCache.set(relPath, targets); + return targets; +} + +/** + * Expand declared source files to include every file transitively pulled in via + * `` (cycle-safe). The wrapper plus its shared includes all contribute + * to the page's last-modified date. + */ +function expandSources(declaredPaths: string[]): string[] { + const resolved = new Set(); + const stack = [...declaredPaths]; + while (stack.length > 0) { + const current = stack.pop(); + if (!current || resolved.has(current)) continue; + resolved.add(current); + for (const included of directIncludes(current)) { + if (!resolved.has(included)) stack.push(included); + } + } + return [...resolved]; +} + +/** Most recent (max) date among the given source files, or undefined if none tracked. */ +function resolveLastModified( + sourcePaths: string[], + dateMap: Map, +): string | undefined { + let latest: string | undefined; + for (const sourcePath of sourcePaths) { + const date = dateMap.get(sourcePath); + // YYYY-MM-DD sorts lexicographically, so string comparison == date comparison. + if (date && (!latest || date > latest)) latest = date; + } + return latest; +} + +/** + * Ensure full git history is available, deepening a shallow clone if needed. + * Deploy environments like Cloudflare Workers Builds shallow-clone with no + * fetch-depth setting, so `git fetch --unshallow` is the only way to get real + * per-file dates there (anonymous fetch works because the repo is public). + * + * Returns false — so the caller omits rather than publishing clustered, + * misleading dates — whenever full history can't be *confirmed*: the depth probe + * itself fails, or a shallow clone can't be deepened. Never fails the build. + */ +async function ensureFullHistory(): Promise { + let isShallow: boolean; + try { + const { stdout } = await execFileAsync("git", ["rev-parse", "--is-shallow-repository"]); + isShallow = stdout.trim() === "true"; + } catch (err) { + console.warn( + ` ⚠️ Could not determine clone depth (${(err as Error).message}); ` + + "omitting rather than risk inaccurate dates.", + ); + return false; + } + + if (!isShallow) return true; + + console.warn(" ⚠️ Shallow clone — fetching full history with `git fetch --unshallow`…"); + try { + // A successful --unshallow converts the clone to complete history. + await execFileAsync("git", ["fetch", "--unshallow", "--quiet"], { timeout: 180_000 }); + } catch (err) { + console.warn(` ⚠️ Could not deepen history (${(err as Error).message}).`); + return false; + } + + console.log(" ✓ Fetched full git history."); + return true; +} + +async function main() { + console.log("Generating static sitemap.xml…"); + + // Lightweight Vite SSR server (no Cloudflare plugin) to resolve the fumadocs + // virtual modules, mirroring the other post-build generators. + const server = await createServer({ + configFile: false, + logLevel: "error", + server: { port: 0, host: "127.0.0.1" }, + resolve: { + alias: { "@": path.resolve(process.cwd(), "./src") }, + }, + plugins: [ + mdx(await import("../source.config")), + tsConfigPaths({ projects: ["./tsconfig.json"] }), + react(), + ], + }); + + try { + const { source } = await server.ssrLoadModule("./src/lib/source"); + const { getSitemapSourceEntries, attachLastModified, buildSitemapXml } = + await server.ssrLoadModule("./src/lib/sitemap"); + + const pages = source.getPages() as Array<{ url: string; path: string }>; + const contentPages = pages.map((page) => ({ + url: page.url, + sourcePaths: [`content/docs/${page.path}`], + })); + + const sourceEntries = getSitemapSourceEntries(contentPages).map((entry) => ({ + ...entry, + sourcePaths: expandSources(entry.sourcePaths), + })); + + // Real dates need full history. If we can't get it (e.g. an un-deepenable + // shallow clone), omit rather than publish one wrong date — and + // never fail the build over it. + let dateMap = new Map(); + if (await ensureFullHistory()) { + dateMap = await buildGitDateMap(); + } else { + console.warn(" ⚠️ Omitting : full git history unavailable."); + } + + const entries = attachLastModified(sourceEntries, (sourcePaths) => + resolveLastModified(sourcePaths, dateMap), + ); + + const xml = buildSitemapXml(entries); + + await fs.mkdir(path.dirname(OUTPUT_PATH), { recursive: true }); + await fs.writeFile(OUTPUT_PATH, xml); + + const withDates = entries.filter((entry) => entry.lastModified).length; + console.log( + ` ✓ sitemap.xml: ${entries.length} urls (${withDates} with ) → ` + + `${path.relative(process.cwd(), OUTPUT_PATH)}`, + ); + if (withDates < entries.length) { + console.log(` ℹ ${entries.length - withDates} url(s) had no git date and omit .`); + } + } finally { + await server.close(); + } +} + +main().catch((err) => { + console.error("Sitemap generation failed:", err); + process.exit(1); +}); diff --git a/src/lib/seo-routes.test.ts b/src/lib/seo-routes.test.ts index bf533766..88b4fbdf 100644 --- a/src/lib/seo-routes.test.ts +++ b/src/lib/seo-routes.test.ts @@ -1,7 +1,27 @@ import assert from "node:assert/strict"; import { describe, test } from "node:test"; import { buildRobotsTxt } from "../routes/robots[.]txt"; -import { buildSitemapXml, getSitemapEntries } from "./sitemap"; +import { + attachLastModified, + buildSitemapXml, + getSitemapSourceEntries, + type SitemapSourceEntry, +} from "./sitemap"; + +const CONTENT_PAGES = [ + { url: "/docs/ios", sourcePaths: ["content/docs/ios/index.mdx"] }, + { url: "/docs/android", sourcePaths: ["content/docs/android/index.mdx"] }, + { + url: "/docs/ios/quickstart/install", + sourcePaths: ["content/docs/ios/quickstart/install.mdx"], + }, +]; + +function entryFor(entries: SitemapSourceEntry[], url: string) { + const entry = entries.find((candidate) => candidate.url === url); + assert.ok(entry, `expected sitemap entry for ${url}`); + return entry; +} describe("seo routes", () => { test("buildRobotsTxt includes sitemap declaration", () => { @@ -12,28 +32,67 @@ describe("seo routes", () => { assert.match(robots, /^Sitemap: https:\/\/superwall\.com\/docs\/sitemap\.xml$/m); }); - test("getSitemapEntries includes docs root and generated pages", () => { - const entries = getSitemapEntries( - ["/docs/ios/quickstart/install", "/docs/android/quickstart/install"], - new Date("2026-03-02T00:00:00.000Z"), - ); + test("getSitemapSourceEntries includes the docs root and content pages", () => { + const entries = getSitemapSourceEntries(CONTENT_PAGES); const urls = entries.map((entry) => entry.url); assert.ok(urls.includes("https://superwall.com/docs/")); - assert.ok(urls.some((url) => url.startsWith("https://superwall.com/docs/ios"))); - assert.ok(urls.some((url) => url.startsWith("https://superwall.com/docs/android"))); + assert.ok(urls.includes("https://superwall.com/docs/ios")); + assert.ok(urls.includes("https://superwall.com/docs/android")); + }); + + test("docs root is backed by its route component, not a content file", () => { + const entries = getSitemapSourceEntries(CONTENT_PAGES); + const root = entryFor(entries, "https://superwall.com/docs/"); + + assert.equal(root.priority, 1.0); + assert.deepEqual(root.sourcePaths, ["src/routes/index.tsx"]); + }); + + test("landing pages keep their content source but get a bumped priority", () => { + const entries = getSitemapSourceEntries(CONTENT_PAGES); + const ios = entryFor(entries, "https://superwall.com/docs/ios"); + + // Priority bumped from the 0.8 content default to the 0.9 landing priority… + assert.equal(ios.priority, 0.9); + // …while the content file remains the single source of truth for the date. + assert.deepEqual(ios.sourcePaths, ["content/docs/ios/index.mdx"]); }); - test("buildSitemapXml renders XML urlset output", () => { - const entries = getSitemapEntries( - ["/docs/ios/quickstart/install"], - new Date("2026-03-02T00:00:00.000Z"), + test("entries are sorted by priority descending", () => { + const entries = getSitemapSourceEntries(CONTENT_PAGES); + const priorities = entries.map((entry) => entry.priority); + const sorted = [...priorities].sort((a, b) => b - a); + + assert.deepEqual(priorities, sorted); + }); + + test("attachLastModified omits the date when the source has no git history", () => { + const entries = getSitemapSourceEntries(CONTENT_PAGES); + const dated = attachLastModified(entries, (sourcePaths) => + sourcePaths.includes("content/docs/ios/index.mdx") ? "2026-03-02" : undefined, ); - const xml = buildSitemapXml(entries.slice(0, 2)); + + const ios = dated.find((entry) => entry.url === "https://superwall.com/docs/ios"); + const android = dated.find((entry) => entry.url === "https://superwall.com/docs/android"); + + assert.equal(ios?.lastModified, "2026-03-02"); + assert.equal(android?.lastModified, undefined); + }); + + test("buildSitemapXml renders urlset output and only emits known lastmod", () => { + const entries = attachLastModified(getSitemapSourceEntries(CONTENT_PAGES), (sourcePaths) => + sourcePaths.includes("content/docs/ios/index.mdx") ? "2026-03-02" : undefined, + ); + const xml = buildSitemapXml(entries); assert.match(xml, /^<\?xml version="1\.0" encoding="UTF-8"\?>/); assert.match(xml, //); - assert.match(xml, /https:\/\/superwall\.com\/docs\//); - assert.match(xml, /2026-03-02T00:00:00\.000Z<\/lastmod>/); + assert.match(xml, /https:\/\/superwall\.com\/docs\/ios<\/loc>/); + assert.match(xml, /2026-03-02<\/lastmod>/); + + // A single for ios; entries without a known date emit none. + const lastmodCount = xml.match(//g)?.length ?? 0; + assert.equal(lastmodCount, 1); }); }); diff --git a/src/lib/sitemap.ts b/src/lib/sitemap.ts index fa2ac210..6226cba9 100644 --- a/src/lib/sitemap.ts +++ b/src/lib/sitemap.ts @@ -4,17 +4,47 @@ export type SitemapEntry = { url: string; priority: number; changeFrequency: "weekly"; - lastModified: string; + /** ISO date (YYYY-MM-DD). Omitted when no source date is known. */ + lastModified?: string; }; -function toSitemapEntry(path: string, priority: number, nowIso: string): SitemapEntry { - return { - url: buildCanonicalUrl(path), - priority, - changeFrequency: "weekly", - lastModified: nowIso, - }; -} +/** A sitemap URL plus the repo-relative source file(s) that back it. */ +export type SitemapSourceEntry = { + url: string; + priority: number; + changeFrequency: "weekly"; + sourcePaths: string[]; +}; + +/** A content page discovered from the docs source, with its backing file(s). */ +export type ContentPageInput = { + url: string; + sourcePaths: string[]; +}; + +/** + * Landing pages that need a fixed priority and/or live outside `content/docs`. + * When `sourcePaths` is omitted the entry is expected to also exist as a content + * page, and only its priority is bumped — the content page remains the single + * source of truth for which file backs the URL. + */ +const STATIC_ENTRIES: ReadonlyArray<{ + path: string; + priority: number; + sourcePaths?: string[]; +}> = [ + { path: "/docs", priority: 1.0, sourcePaths: ["src/routes/index.tsx"] }, + // `/home` 301s to `/dashboard` (redirects-map.ts); date it from dashboard content. + { path: "/home", priority: 0.9, sourcePaths: ["content/docs/dashboard/index.mdx"] }, + { path: "/dashboard", priority: 0.9 }, + { path: "/web-checkout", priority: 0.9 }, + { path: "/integrations", priority: 0.9 }, + { path: "/ios", priority: 0.9 }, + { path: "/android", priority: 0.9 }, + { path: "/flutter", priority: 0.9 }, + { path: "/expo", priority: 0.9 }, + { path: "/unity", priority: 0.9 }, +]; function escapeXml(value: string) { return value @@ -25,49 +55,78 @@ function escapeXml(value: string) { .replaceAll("'", "'"); } -export function getSitemapEntries(pagePaths: string[], now = new Date()): SitemapEntry[] { - const nowIso = now.toISOString(); - - const staticEntries: SitemapEntry[] = [ - toSitemapEntry("/docs", 1.0, nowIso), - toSitemapEntry("/home", 0.9, nowIso), - toSitemapEntry("/dashboard", 0.9, nowIso), - toSitemapEntry("/web-checkout", 0.9, nowIso), - toSitemapEntry("/integrations", 0.9, nowIso), - toSitemapEntry("/ios", 0.9, nowIso), - toSitemapEntry("/android", 0.9, nowIso), - toSitemapEntry("/flutter", 0.9, nowIso), - toSitemapEntry("/expo", 0.9, nowIso), - toSitemapEntry("/unity", 0.9, nowIso), - ]; - - const pageEntries = pagePaths.map((pagePath) => toSitemapEntry(pagePath, 0.8, nowIso)); +/** + * Build the deduplicated, sorted list of sitemap URLs paired with the source + * file(s) that back each one. Pure (no git / fs) so it is unit-testable and safe + * to import anywhere; date resolution happens later via {@link attachLastModified}. + */ +export function getSitemapSourceEntries(contentPages: ContentPageInput[]): SitemapSourceEntry[] { + const entries = new Map(); - const allEntries = [...staticEntries, ...pageEntries]; - const unique = new Map(); + for (const page of contentPages) { + const url = buildCanonicalUrl(page.url); + if (!entries.has(url)) { + entries.set(url, { + url, + priority: 0.8, + changeFrequency: "weekly", + sourcePaths: page.sourcePaths, + }); + } + } - for (const entry of allEntries) { - if (!unique.has(entry.url)) unique.set(entry.url, entry); + for (const staticEntry of STATIC_ENTRIES) { + const url = buildCanonicalUrl(staticEntry.path); + const existing = entries.get(url); + if (existing) { + existing.priority = Math.max(existing.priority, staticEntry.priority); + if (staticEntry.sourcePaths) existing.sourcePaths = staticEntry.sourcePaths; + } else { + entries.set(url, { + url, + priority: staticEntry.priority, + changeFrequency: "weekly", + sourcePaths: staticEntry.sourcePaths ?? [], + }); + } } - return [...unique.values()].sort((a, b) => { + return [...entries.values()].sort((a, b) => { if (a.priority !== b.priority) return b.priority - a.priority; return a.url.localeCompare(b.url); }); } +/** + * Resolve a `lastModified` date for each entry from its source files. `resolve` + * returns the date (YYYY-MM-DD) for the most recently changed source path, or + * `undefined` when none is known — in which case `` is omitted. + */ +export function attachLastModified( + entries: SitemapSourceEntry[], + resolve: (sourcePaths: string[]) => string | undefined, +): SitemapEntry[] { + return entries.map((entry) => { + const lastModified = resolve(entry.sourcePaths); + return { + url: entry.url, + priority: entry.priority, + changeFrequency: entry.changeFrequency, + ...(lastModified ? { lastModified } : {}), + }; + }); +} + export function buildSitemapXml(entries: SitemapEntry[]) { const rows = entries - .map((entry) => - [ - "", - ` ${escapeXml(entry.url)}`, - ` ${entry.lastModified}`, - ` ${entry.changeFrequency}`, - ` ${entry.priority.toFixed(1)}`, - "", - ].join("\n"), - ) + .map((entry) => { + const lines = ["", ` ${escapeXml(entry.url)}`]; + if (entry.lastModified) lines.push(` ${entry.lastModified}`); + lines.push(` ${entry.changeFrequency}`); + lines.push(` ${entry.priority.toFixed(1)}`); + lines.push(""); + return lines.join("\n"); + }) .join("\n"); return [ diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index a8e45ada..20fda258 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -11,7 +11,6 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as Char123Char125DotmdxRouteImport } from './routes/{$}[.]mdx' import { Route as Char123Char125DotmdRouteImport } from './routes/{$}[.]md' -import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml' import { Route as SdkDotmdxRouteImport } from './routes/sdk[.]mdx' import { Route as SdkDotmdRouteImport } from './routes/sdk[.]md' import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt' @@ -62,11 +61,6 @@ const Char123Char125DotmdRoute = Char123Char125DotmdRouteImport.update({ path: '/{$}.md', getParentRoute: () => rootRouteImport, } as any) -const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({ - id: '/sitemap.xml', - path: '/sitemap.xml', - getParentRoute: () => rootRouteImport, -} as any) const SdkDotmdxRoute = SdkDotmdxRouteImport.update({ id: '/sdk.mdx', path: '/sdk.mdx', @@ -278,7 +272,6 @@ export interface FileRoutesByFullPath { '/robots.txt': typeof RobotsDottxtRoute '/sdk.md': typeof SdkDotmdRoute '/sdk.mdx': typeof SdkDotmdxRoute - '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute '/agents/llms-full.txt': typeof AgentsLlmsFullDottxtRoute @@ -322,7 +315,6 @@ export interface FileRoutesByTo { '/robots.txt': typeof RobotsDottxtRoute '/sdk.md': typeof SdkDotmdRoute '/sdk.mdx': typeof SdkDotmdxRoute - '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute '/agents/llms-full.txt': typeof AgentsLlmsFullDottxtRoute @@ -366,7 +358,6 @@ export interface FileRoutesById { '/robots.txt': typeof RobotsDottxtRoute '/sdk.md': typeof SdkDotmdRoute '/sdk.mdx': typeof SdkDotmdxRoute - '/sitemap.xml': typeof SitemapDotxmlRoute '/{$}.md': typeof Char123Char125DotmdRoute '/{$}.mdx': typeof Char123Char125DotmdxRoute '/agents/llms-full.txt': typeof AgentsLlmsFullDottxtRoute @@ -412,7 +403,6 @@ export interface FileRouteTypes { | '/robots.txt' | '/sdk.md' | '/sdk.mdx' - | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' | '/agents/llms-full.txt' @@ -456,7 +446,6 @@ export interface FileRouteTypes { | '/robots.txt' | '/sdk.md' | '/sdk.mdx' - | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' | '/agents/llms-full.txt' @@ -499,7 +488,6 @@ export interface FileRouteTypes { | '/robots.txt' | '/sdk.md' | '/sdk.mdx' - | '/sitemap.xml' | '/{$}.md' | '/{$}.mdx' | '/agents/llms-full.txt' @@ -544,7 +532,6 @@ export interface RootRouteChildren { RobotsDottxtRoute: typeof RobotsDottxtRoute SdkDotmdRoute: typeof SdkDotmdRoute SdkDotmdxRoute: typeof SdkDotmdxRoute - SitemapDotxmlRoute: typeof SitemapDotxmlRoute Char123Char125DotmdRoute: typeof Char123Char125DotmdRoute Char123Char125DotmdxRoute: typeof Char123Char125DotmdxRoute AgentsLlmsFullDottxtRoute: typeof AgentsLlmsFullDottxtRoute @@ -593,13 +580,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof Char123Char125DotmdRouteImport parentRoute: typeof rootRouteImport } - '/sitemap.xml': { - id: '/sitemap.xml' - path: '/sitemap.xml' - fullPath: '/sitemap.xml' - preLoaderRoute: typeof SitemapDotxmlRouteImport - parentRoute: typeof rootRouteImport - } '/sdk.mdx': { id: '/sdk.mdx' path: '/sdk.mdx' @@ -901,7 +881,6 @@ const rootRouteChildren: RootRouteChildren = { RobotsDottxtRoute: RobotsDottxtRoute, SdkDotmdRoute: SdkDotmdRoute, SdkDotmdxRoute: SdkDotmdxRoute, - SitemapDotxmlRoute: SitemapDotxmlRoute, Char123Char125DotmdRoute: Char123Char125DotmdRoute, Char123Char125DotmdxRoute: Char123Char125DotmdxRoute, AgentsLlmsFullDottxtRoute: AgentsLlmsFullDottxtRoute, diff --git a/src/routes/sitemap[.]xml.ts b/src/routes/sitemap[.]xml.ts deleted file mode 100644 index 7e34d448..00000000 --- a/src/routes/sitemap[.]xml.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { buildSitemapXml, getSitemapEntries } from "@/lib/sitemap"; - -export const Route = createFileRoute("/sitemap.xml")({ - server: { - handlers: { - GET: async () => { - const { source } = await import("@/lib/source"); - const pagePaths = source.getPages().map((page) => page.url); - const xml = buildSitemapXml(getSitemapEntries(pagePaths)); - - return new Response(xml, { - status: 200, - headers: { "Content-Type": "application/xml; charset=utf-8" }, - }); - }, - }, - }, -});