From e5f245ffff7742465efbbed59157201ba71491ec Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Sat, 11 Apr 2026 12:23:38 -0700 Subject: [PATCH] Add sitemap generation --- server/src/index.ts | 9 ++++++++ worker/src/index.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++ worker/wrangler.toml | 8 +++++++ 3 files changed, 68 insertions(+) diff --git a/server/src/index.ts b/server/src/index.ts index 0babff4..2e159a7 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -26,6 +26,15 @@ app.all("/api/auth/*", async (c) => { return auth.handler(c.req.raw); }); +// Public endpoint: all memory event IDs for sitemap generation +app.get("/api/sitemap", async (c) => { + const results = await c.env.DB.prepare( + "SELECT event_id, MAX(completed_at) AS completed_at FROM memory_completions GROUP BY event_id ORDER BY completed_at DESC" + ).all<{ event_id: string; completed_at: number }>(); + + return c.json({ entries: results.results }); +}); + // Public endpoint: look up a memory completion by eventId (no auth needed) app.get("/api/memory/:eventId", async (c) => { const eventId = c.req.param("eventId"); diff --git a/worker/src/index.ts b/worker/src/index.ts index 6c0df97..4913f8b 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -162,11 +162,62 @@ async function handleScene(encoded: string, request: Request, env: Env): Promise } } +async function handleSitemap(request: Request, env: Env): Promise { + const origin = new URL(request.url).origin; + + try { + const apiRes = await fetch(`${env.API_URL}/api/sitemap`); + if (!apiRes.ok) { + return new Response('Failed to fetch sitemap data', { status: 502 }); + } + + const data = await apiRes.json() as { + entries: Array<{ event_id: string; completed_at: number }>; + }; + + const urls: string[] = [ + ` `, + ` ${escapeHtml(origin)}/`, + ` `, + ]; + + for (const entry of data.entries) { + const lastmod = new Date(entry.completed_at * 1000).toISOString().split('T')[0]; + urls.push( + ` `, + ` ${escapeHtml(origin)}/memory/${escapeHtml(entry.event_id)}`, + ` ${lastmod}`, + ` `, + ); + } + + const xml = [ + ``, + ``, + ...urls, + ``, + ].join('\n'); + + return new Response(xml, { + headers: { + 'content-type': 'application/xml; charset=utf-8', + 'cache-control': 'public, max-age=3600', + }, + }); + } catch { + return new Response('Failed to generate sitemap', { status: 500 }); + } +} + export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); const path = url.pathname; + if (path === '/sitemap.xml') { + return handleSitemap(request, env); + } + const memoryMatch = path.match(/^\/memory\/([A-Za-z0-9_-]+)$/); if (memoryMatch) { return handleMemory(memoryMatch[1], request, env); diff --git a/worker/wrangler.toml b/worker/wrangler.toml index f2b0e88..4d1cb27 100644 --- a/worker/wrangler.toml +++ b/worker/wrangler.toml @@ -29,6 +29,10 @@ zone_name = "isle.pizza" pattern = "isle.pizza/scene/*" zone_name = "isle.pizza" +[[env.production.routes]] +pattern = "isle.pizza/sitemap.xml" +zone_name = "isle.pizza" + [[env.production.routes]] pattern = "dev.isle.pizza/memory/*" zone_name = "isle.pizza" @@ -36,3 +40,7 @@ zone_name = "isle.pizza" [[env.production.routes]] pattern = "dev.isle.pizza/scene/*" zone_name = "isle.pizza" + +[[env.production.routes]] +pattern = "dev.isle.pizza/sitemap.xml" +zone_name = "isle.pizza"