Skip to the content

Capsid menu

app/lib/site-map.mjs

80 lines and 12 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.

Functions

The functions of site-map.mjs
FunctionLinesComplexityCoveredCRAPMutation
under43 to 432100%OkCRAP 2No data: no mutants in this code
sectionOf46 to 492100%OkCRAP 2No data: no mutants in this code
<anonymous:47>47 to 471100%OkCRAP 1No data: no mutants in this code
<anonymous:47>47 to 471100%OkCRAP 1No data: no mutants in this code
siteMapSections58 to 828100%WatchCRAP 8No data: no mutants in this code
<anonymous:73>73 to 733100%OkCRAP 3No data: no mutants in this code
<anonymous:77>77 to 811100%OkCRAP 1No data: no mutants in this code
<anonymous:78>78 to 781100%OkCRAP 1No data: no mutants in this code
isHub79 to 792100%OkCRAP 2No data: no mutants in this code
<anonymous:80>80 to 801100%OkCRAP 1No data: no mutants in this code
<anonymous:81>81 to 811100%OkCRAP 1No data: no mutants in this code
lineText85 to 872100%OkCRAP 2No data: no mutants in this code

Imports

It imports no other module of this repo. Imported by app/lib/site-urls.server.ts, app/routes/site-map.tsx.

Source

/**
 * The site map page (/site-map), drawn from the one list of public URLs that sitemap.xml is also drawn from
 * (app/lib/site-urls.server.ts), so the two cannot drift. Pure, and relative imports only, so
 * test/site-map.test.mjs runs it under plain node.
 *
 * Grouped by the header menu's five sections, then Site. Every page is one line by its title, except the
 * kinds of page there are many of (papers, registry items, procedures, posts and tags): those are one line,
 * the count, linking to their index.
 */

/**
 * @typedef {{ key: string, label: string, to: string, merge: boolean }} Tally
 *   An item counted rather than listed. `to` is its index; with `merge`, the count goes on the index's own
 *   line (Publications (36)). Without it, the count is a line of its own in `to`'s section, as plain text
 *   (Tags (34)): the kind has no index of its own, and a second link to `to` under another name would make
 *   two links with different text go to one place.
 * @typedef {{ path: string, lastmod: Date | null, title: string, tally?: Tally, images?: string[] }} SiteUrl
 *   `images`: the site paths of the images the page shows, for sitemap.xml's image entries.
 * @typedef {{ to: string, label: string, count?: number, plain?: boolean }} SiteMapLine
 *   `plain`: shown as text, not a link; `to` then only places it in its section.
 * @typedef {{ id: string, heading: string, to: string | null, lines: SiteMapLine[] }} SiteMapSection
 */

/**
 * The sections, in the header's order (app/lib/nav.ts, NAV), then Site. `roots` are the address prefixes a
 * section owns; the first match wins, and a path no section owns is Site's. The System map sits under
 * Software, where the header's Software panel links it; the CV and the recipes sit under About, where the
 * About panel links them.
 */
const SITE_MAP_SECTIONS = /** @type {const} */ ([
  { id: "research", heading: "Research", to: "/research", roots: ["/research"] },
  { id: "teaching", heading: "Teaching", to: "/teaching", roots: ["/teaching"] },
  { id: "software", heading: "Software", to: "/software", roots: ["/software", "/systems"] },
  { id: "writing", heading: "Writing", to: "/writing", roots: ["/writing"] },
  { id: "about", heading: "About", to: "/about", roots: ["/about", "/cv", "/recipes"] },
  { id: "site", heading: "Site", to: null, roots: [] },
]);

/** The page's own address. */
export const SITE_MAP_PATH = "/site-map";

/** @param {string} path @param {string} root */
const under = (path, root) => path === root || path.startsWith(`${root}/`);

/** The id of the section a path belongs to. @param {string} path */
export function sectionOf(path) {
  const found = SITE_MAP_SECTIONS.find((section) => section.roots.some((root) => under(path, root)));
  return found ? found.id : "site";
}

/**
 * The page's sections, from the URL list. Lines keep the list's order, except that a section's own hub
 * comes first. A section with no line is left out.
 *
 * @param {SiteUrl[]} urls
 * @returns {SiteMapSection[]}
 */
export function siteMapSections(urls) {
  /** @type {Map<string, { tally: Tally, count: number }>} */
  const tallies = new Map();
  /** @type {SiteMapLine[]} */
  const lines = [];
  for (const url of urls) {
    if (url.tally) {
      const seen = tallies.get(url.tally.key);
      if (seen) seen.count += 1;
      else tallies.set(url.tally.key, { tally: url.tally, count: 1 });
      continue;
    }
    lines.push({ to: url.path, label: url.title });
  }
  for (const { tally, count } of tallies.values()) {
    const index = tally.merge ? lines.find((line) => line.to === tally.to && line.count === undefined && !line.plain) : undefined;
    if (index) index.count = count;
    else lines.push({ to: tally.to, label: tally.label, count, ...(tally.merge ? {} : { plain: true }) });
  }
  return SITE_MAP_SECTIONS.map(({ id, heading, to }) => {
    const own = lines.filter((line) => sectionOf(line.to) === id);
    const isHub = (/** @type {SiteMapLine} */ line) => line.to === to && !line.plain;
    return { id, heading, to, lines: [...own.filter(isHub), ...own.filter((line) => !isHub(line))] };
  }).filter((section) => section.lines.length > 0);
}

/** A line's text: its label, and its count in brackets when it has one. @param {SiteMapLine} line */
export function lineText(line) {
  return line.count === undefined ? line.label : `${line.label} (${line.count})`;
}