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
This commit is contained in:
Piotr Nowacki 2025-10-16 18:20:01 +02:00
parent 30c6a2f881
commit 7864f5c3cf
3 changed files with 89 additions and 0 deletions

View File

@ -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);

29
src/back/sitemap.xml.tsx Normal file
View File

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>\n`;
xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`;
for (const page of pages.items) {
xml += ` <url>\n <loc>${ctx.origin.replace(/\/$/, "")}${page.get("url")}</loc>\n </url>\n`;
}
xml += `</urlset>\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);
}
})();

54
tests/sitemap.test.ts Normal file
View File

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>${backend.url}/</loc>
</url>
</urlset>
`;
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 = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>${backend.url}/</loc>
</url>
<url>
<loc>${backend.url}/my-page/</loc>
</url>
</urlset>
`;
assert.strictEqual(actual, expected);
});