Skip to content
45 linesCodeBlameRaw
1/**
2 * /sitemap.xml: the public pages, then the public projects Explore lists,
3 * newest first. Explore being unreachable leaves just the pages.
4 */
5import type { Route } from "./+types/sitemap-xml";
6import { search } from "../lib/services.server";
7
8/** Pages anyone can read, signed in or not. */
9const PAGES = ["/", "/pricing", "/explore", "/support", "/security", "/policies", "/policies/privacy"];
10
11/** How many of Explore's pages to list; each holds one page of projects. */
12const EXPLORE_PAGES = 10;
13
14function escape(text: string): string {
15 return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
16}
17
18export 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}