From 7864f5c3cf3b72c95c729c1dd6d4d8ecf84c8782 Mon Sep 17 00:00:00 2001 From: Piotr Nowacki Date: Thu, 16 Oct 2025 18:20:01 +0200 Subject: [PATCH] Add /sitemap endpoint for generating sitemap.xml Summary: Ref T3045 Reviewers: #testers, kuba-orlik Reviewed By: #testers, kuba-orlik Subscribers: kuba-orlik, jenkins-user Maniphest Tasks: T3045 Differential Revision: https://hub.sealcode.org/D1616 --- src/back/routes/index.ts | 6 +++++ src/back/sitemap.xml.tsx | 29 +++++++++++++++++++++ tests/sitemap.test.ts | 54 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 src/back/sitemap.xml.tsx create mode 100644 tests/sitemap.test.ts diff --git a/src/back/routes/index.ts b/src/back/routes/index.ts index 1135d9e..3642812 100644 --- a/src/back/routes/index.ts +++ b/src/back/routes/index.ts @@ -8,6 +8,7 @@ import { imageRouter, RESPONSIVE_IMAGES_URL_PATH } from "../image-router.js"; import { customUrlView } from "./middlewares/customUrlView.js"; import mountAutoRoutes from "./routes.js"; import _locreq from "locreq"; +import sitemapPage from "../sitemap.xml.js"; const locreq = _locreq(new URL("./", import.meta.url).pathname); export const mainRouter = (app: TheApp, koa_app: Koa, router: Router): void => { @@ -41,6 +42,11 @@ export const mainRouter = (app: TheApp, koa_app: Koa, router: Router): void => { ctx.body = { status: ctx.$app.status, started_at }; }); + router.get("/sitemap.xml", async (ctx) => { + ctx.set("Content-Type", "application/xml"); + ctx.body = await sitemapPage.render(ctx); + }); + router.use(RESPONSIVE_IMAGES_URL_PATH, imageRouter.getRoutes()); mountAutoRoutes(router); diff --git a/src/back/sitemap.xml.tsx b/src/back/sitemap.xml.tsx new file mode 100644 index 0000000..482982c --- /dev/null +++ b/src/back/sitemap.xml.tsx @@ -0,0 +1,29 @@ +import type { Context } from "koa"; +import { Page } from "@sealcode/sealgen"; + +export const actionName = "Sitemap"; + +async function generateSitemap(ctx: Context) { + const pages = await ctx.$app.collections.pages.list(ctx.$context).fetch(); + + let xml = `\n`; + xml += `\n`; + + for (const page of pages.items) { + xml += ` \n ${ctx.origin.replace(/\/$/, "")}${page.get("url")}\n \n`; + } + + xml += `\n`; + return xml; +} + +export default new (class SitemapPage extends Page { + async canAccess() { + return { canAccess: true, message: "" }; + } + + async render(ctx: Context) { + ctx.set("Content-Type", "application/xml"); + return generateSitemap(ctx); + } +})(); diff --git a/tests/sitemap.test.ts b/tests/sitemap.test.ts new file mode 100644 index 0000000..114ca72 --- /dev/null +++ b/tests/sitemap.test.ts @@ -0,0 +1,54 @@ +import * as assert from "node:assert"; +import { test, expect } from "./backend-fixture"; + +test("sitemap returns expected XML for empty DB", async ({ backend, request }) => { + const response = await request.get(`${backend.url}/sitemap.xml`); + assert.strictEqual(response.status(), 200); + + const actual = await response.text(); + const expected = ` + + + ${backend.url}/ + + +`; + assert.strictEqual(actual, expected); +}); + +test("sitemap returns expected XML after creating a page via UI", async ({ + page, + backend, + request, +}) => { + await page.goto(backend.url); + await page.getByRole("link", { name: "Go to Admin" }).click(); + await page.getByPlaceholder("text").fill("admin"); + await page.getByPlaceholder("text").press("Tab"); + await page.getByPlaceholder("password").fill("adminadmin"); + await page.getByRole("button", { name: "Wyślij" }).click(); + + await page.getByRole("link", { name: "Edit Pages" }).click(); + await page.getByRole("link", { name: "Create" }).click(); + await page.getByLabel("url").fill("/my-page/"); + await page.getByLabel("title").fill("My Page"); + await page.getByLabel("heading").fill("My Heading"); + await page.getByLabel("description").fill("My Description"); + await page.getByRole("button", { name: "Wyślij" }).click(); + + const response = await request.get(`${backend.url}/sitemap.xml`); + assert.strictEqual(response.status(), 200); + + const actual = await response.text(); + const expected = ` + + + ${backend.url}/ + + + ${backend.url}/my-page/ + + +`; + assert.strictEqual(actual, expected); +});