| 1 | /** |
| 2 | * /sitemap.xml: the public pages, then the public projects Explore lists, |
| 3 | * newest first. Explore being unreachable leaves just the pages. |
| 4 | */ |
| 5 | import type { Route } from "./+types/sitemap-xml"; |
| 6 | import { search } from "../lib/services.server"; |
| 7 | |
| 8 | /** Pages anyone can read, signed in or not. */ |
| 9 | const PAGES = ["/", "/pricing", "/explore", "/support", "/security", "/policies", "/policies/privacy"]; |
| 10 | |
| 11 | /** How many of Explore's pages to list; each holds one page of projects. */ |
| 12 | const EXPLORE_PAGES = 10; |
| 13 | |
| 14 | function escape(text: string): string { |
| 15 | return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); |
| 16 | } |
| 17 | |
| 18 | export async function loader({ request }: Route.LoaderArgs) { |
| 19 | const site = new URL(request.url).origin; |
| 20 | const entries: { loc: string; lastmod?: string }[] = PAGES.map((path) => ({ loc: `${site}${path}` })); |
| 21 | for (let pageNumber = 1; pageNumber <= EXPLORE_PAGES; pageNumber++) { |
| 22 | const explore = await search.explore(null, { sort: "new", page: pageNumber }).catch(() => null); |
| 23 | if (!explore) break; |
| 24 | for (const repo of explore.repos) { |
| 25 | const lastmod = (repo.pushedAt ?? repo.createdAt)?.slice(0, 10); |
| 26 | entries.push({ loc: `${site}/${repo.namespace}/${repo.name}`, lastmod }); |
| 27 | } |
| 28 | if (!explore.more) break; |
| 29 | } |
| 30 | const body = [ |
| 31 | '<?xml version="1.0" encoding="UTF-8"?>', |
| 32 | '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', |
| 33 | ...entries.map( |
| 34 | (entry) => ` <url><loc>${escape(entry.loc)}</loc>${entry.lastmod ? `<lastmod>${entry.lastmod}</lastmod>` : ""}</url>`, |
| 35 | ), |
| 36 | "</urlset>", |
| 37 | "", |
| 38 | ].join("\n"); |
| 39 | return new Response(body, { |
| 40 | headers: { |
| 41 | "content-type": "application/xml; charset=utf-8", |
| 42 | "cache-control": "public, max-age=3600", |
| 43 | }, |
| 44 | }); |
| 45 | } |