Ctrl K
ComunidadGuías…

src/features/sitemap.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/** The /sitemap.xml document: the site's public, indexable URLs in one
 *  sitemaps.org 0.9 urlset. It lists the home page, the fixed pages that
 *  serve, every content page, and every news post. It also lists every
 *  listing and group whose /ticket page serves.
 *
 *  Every entry passes the same checks its public GET handler applies to a
 *  buyer, so the document names only pages that answer 200. The document
 *  carries no <lastmod>: no public page kind holds a timestamp that changes
 *  when the page's content changes. The static path serves before the
 *  request's settings load, so the handler reads its own keys, exactly like
 *  /robots.txt. */

import { getNonStandaloneChildIds } from "#db/listing-parents.ts";
import { getAllListings } from "#db/listings/records.ts";
import { isMissingSettingsTableError } from "#db/migrations/errors.ts";
import { getNewsPostSummaries } from "#db/news-posts.ts";
import { settings } from "#db/settings.ts";
import { sitePages } from "#db/site-pages.ts";
import { loadDecidedGroups } from "#routes/public/group-liveness.ts";
import {
  loadPublicDomain,
  publicCacheResponse,
} from "#routes/public-origin.ts";
import { isPublicListing } from "#shared/listing-visibility.ts";
import { SLUG_PARAM_SERVES } from "#shared/route-pattern.ts";
import { CONFIG_KEYS } from "#shared/settings/keys.ts";
import { escapeXml } from "#shared/xml.ts";

const SITEMAP_CONTENT_TYPE = "application/xml";

/** The settings keys the sitemap's reads need: the nav flags behind the fixed
 *  pages, and LISTING_DEFAULTS behind the catalog's inherited hidden flags.
 *  The static route serves before the request's settings load, so the handler
 *  declares them itself, exactly like /robots.txt. */
const SITEMAP_SETTINGS_KEYS = [
  CONFIG_KEYS.ENABLED_FEATURES,
  CONFIG_KEYS.BUSINESS_EMAIL,
  CONFIG_KEYS.CONTACT_FORM_ENABLED,
  CONFIG_KEYS.CONTACT_PAGE_TEXT,
  CONFIG_KEYS.LISTING_DEFAULTS,
  CONFIG_KEYS.ORDER_ENABLED,
  CONFIG_KEYS.TERMS_AND_CONDITIONS,
];

/** The fixed pages a visitor can load, with the check each public GET handler
 *  applies. `/` and `/listings` serve behind the public-site gate the whole
 *  sitemap sits behind. The rest answer 404 when their handler's content
 *  check fails. Each is listed only while the check passes, and the flags
 *  are the ones the public nav renders its links from. /news joins them
 *  when the news list has a post to show. */
const fixedPublicPaths = (
  flags: { hasContact: boolean; hasOrder: boolean; hasTerms: boolean },
  hasNews: boolean,
): string[] => {
  const served: [string, boolean][] = [
    ["/", true],
    ["/listings", true],
    ["/terms", flags.hasTerms],
    ["/contact", flags.hasContact],
    ["/order", flags.hasOrder],
    ["/news", hasNews],
  ];
  return served.filter(([, loads]) => loads).map(([path]) => path);
};

const urlEntry = (path: string, domain: string): string => {
  const loc = escapeXml(`https://${domain}${path}`);
  return `  <url><loc>${loc}</loc></url>`;
};

/** Every public URL the site serves, one <loc> per line. The slugs are stored
 *  sealed, so the reads go through the entity caches that decrypt them. Each
 *  catalog entry passes the checks its own page applies. A listing is
 *  active, not hidden, and bookable on its own. A child that sells only
 *  through its parent has no /ticket page. A group is not hidden and its
 *  page serves. A package keeps its bookable bundle. A regular group keeps
 *  one visible active member that is not a child. A content page is not
 *  hidden. Every slug passes the
 *  public route's own slug pattern, so a slug the router rejects — the
 *  empty slug with it — stays out. */
const publicPaths = async (): Promise<string[]> => {
  const [listings, posts, pages, groups] = await Promise.all([
    getAllListings(),
    getNewsPostSummaries(),
    sitePages.getAll(),
    loadDecidedGroups("served"),
  ]);
  const nonStandaloneChildIds = await getNonStandaloneChildIds(
    listings.map((listing) => listing.id),
  );
  // shared.tsx renders the public pages and must stay off the cold-start
  // path. The static sitemap route loads its nav flags only when it serves,
  // like every other public page defers it.
  const { navFlags } = await import("#templates/public/shared.tsx");
  return [
    ...fixedPublicPaths(navFlags(), posts.length > 0),
    ...pages
      .filter((page) => !page.hidden && SLUG_PARAM_SERVES.test(page.slug))
      .map((page) => `/page/${page.slug}`),
    ...posts
      .filter((post) => SLUG_PARAM_SERVES.test(post.slug))
      .map((post) => `/news/${post.slug}`),
    ...listings
      .filter(
        (listing) =>
          isPublicListing(listing) &&
          SLUG_PARAM_SERVES.test(listing.slug) &&
          !nonStandaloneChildIds.has(listing.id),
      )
      .map((listing) => `/ticket/${listing.slug}`),
    ...groups
      .filter(({ group }) => SLUG_PARAM_SERVES.test(group.slug))
      .map(({ group }) => `/ticket/${group.slug}`),
  ];
};

export const handleSitemapXml = async (request: Request): Promise<Response> => {
  // A site before setup has no settings table, so the feature cannot be on
  // and the 404 is the answer. Every other failure stays loud.
  try {
    await settings.loadKeys(SITEMAP_SETTINGS_KEYS);
  } catch (error) {
    if (!isMissingSettingsTableError(error)) throw error;
    return new Response("Not found", { status: 404 });
  }
  if (!settings.features.site) {
    return new Response("Not found", { status: 404 });
  }

  const domain = await loadPublicDomain(new URL(request.url).hostname);
  const paths = await publicPaths();
  const body = [
    '<?xml version="1.0" encoding="UTF-8"?>',
    '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
    ...paths.map((path) => urlEntry(path, domain)),
    "</urlset>",
    "",
  ].join("\n");
  return publicCacheResponse(body, SITEMAP_CONTENT_TYPE);
};