| 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 = [ |
| 10 | "/", |
| 11 | "/pricing", |
| 12 | "/download", |
| 13 | "/explore", |
| 14 | "/support", |
| 15 | "/security", |
| 16 | "/policies", |
| 17 | ...["terms", "privacy", "acceptable-use", "refunds", "subprocessors"].map((policy) => `/policies/${policy}`), |
| 18 | ]; |
| 19 | |
| 20 | /** How many of Explore's pages to list; each holds one page of projects. */ |
| 21 | const EXPLORE_PAGES = 10; |
| 22 | |
| 23 | function escape(text: string): string { |
| 24 | return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); |
| 25 | } |
| 26 | |
| 27 | export async function loader({ request }: Route.LoaderArgs) { |
| 28 | const site = new URL(request.url).origin; |
| 29 | const entries: { loc: string; lastmod?: string }[] = PAGES.map((path) => ({ loc: `${site}${path}` })); |
| 30 | for (let pageNumber = 1; pageNumber <= EXPLORE_PAGES; pageNumber++) { |
| 31 | const explore = await search.explore(null, { sort: "new", page: pageNumber }).catch(() => null); |
| 32 | if (!explore) break; |
| 33 | for (const repo of explore.repos) { |
| 34 | const lastmod = (repo.pushedAt ?? repo.createdAt)?.slice(0, 10); |
| 35 | entries.push({ loc: `${site}/${repo.namespace}/${repo.name}`, lastmod }); |
| 36 | } |
| 37 | if (!explore.more) break; |
| 38 | } |
| 39 | const body = [ |
| 40 | '<?xml version="1.0" encoding="UTF-8"?>', |
| 41 | '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', |
| 42 | ...entries.map( |
| 43 | (entry) => ` <url><loc>${escape(entry.loc)}</loc>${entry.lastmod ? `<lastmod>${entry.lastmod}</lastmod>` : ""}</url>`, |
| 44 | ), |
| 45 | "</urlset>", |
| 46 | "", |
| 47 | ].join("\n"); |
| 48 | return new Response(body, { |
| 49 | headers: { |
| 50 | "content-type": "application/xml; charset=utf-8", |
| 51 | "cache-control": "public, max-age=3600", |
| 52 | }, |
| 53 | }); |
| 54 | } |