Skip to the content

Capsid menu

app/routes/content-page.tsx

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

Functions

The functions of content-page.tsx
FunctionLinesComplexityCoveredCRAPMutation
headers42 to 441No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
loader46 to 668No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:61>61 to 611No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
cardUrl69 to 722No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
meta74 to 873No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
ContentPageRoute89 to 1288No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:95>95 to 1011No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code

Imports

It imports app/components/breadcrumb.tsx, app/components/dictionary-entry.tsx, app/components/page-shell.tsx, app/components/phage-roster.tsx, app/components/phage-tool.tsx, app/db/dictionary.ts, app/db/pages.ts, app/db/roster.ts, app/lib/access.server.ts, app/lib/content-pages.mjs, app/lib/context.ts, app/lib/json-ld.mjs, app/lib/long-date.mjs, app/lib/pages/compile.mjs, app/lib/pages/page-json-ld.ts, app/lib/pages/route.ts, app/lib/phage-tools.mjs, app/lib/roster/compile.mjs, app/lib/seo.ts. Imported by app/routes/accessibility.tsx, app/routes/privacy.tsx, app/routes/software.tsx, app/routes/teaching.tsx, app/routes/terms.tsx.

Source

import { data } from "react-router";

import { Breadcrumb } from "~/components/breadcrumb";
import { DictionaryEntry } from "~/components/dictionary-entry";
import { PageShell } from "~/components/page-shell";
import { PhageRoster } from "~/components/phage-roster";
import { PhageTools } from "~/components/phage-tool";
import { getPublishedEntryByPath } from "~/db/dictionary";
import { getPageByPath, getPublishedPageTitles } from "~/db/pages";
import { listRoster } from "~/db/roster";
import { isAdminViewer } from "~/lib/access.server";
import { getEnv } from "~/lib/context";
import { longDateUTC } from "~/lib/long-date.mjs";
import {
  contentPageCardPath,
  contentPageMarkdownPath,
  contentPageTrail,
} from "~/lib/content-pages.mjs";
import type { PageRecord } from "~/lib/pages/compile.mjs";
import { contentPageJsonLd } from "~/lib/pages/page-json-ld";
import { jsonLd as serializeJsonLd } from "~/lib/json-ld.mjs";
import { CONTENT_PAGE_HTML_TAGS } from "~/lib/pages/route";
import { toolsOnPage } from "~/lib/phage-tools.mjs";
import { ROSTER_PAGE_PATH } from "~/lib/roster/compile.mjs";
import { SITE_ORIGIN, pageMeta, publicHtmlHeaders } from "~/lib/seo";

import type { Route } from "./+types/content-page";

// prose.css is route-scoped: a page using `.prose` without importing it renders unstyled.
import "~/styles/enarratio.css";
import "~/styles/prose.css";

/**
 * Every Research, Teaching and Software page (app/lib/content-pages.mjs): the research/* splat, and
 * /teaching and /software through their own modules. Drawn from the page's D1 row at request time
 * (docs/PAGES.md), so an edit saved through the operator API is live without a deploy; the HTML in the
 * row was rendered by the site's pipeline with the URL allowlist already applied, so the Worker carries
 * no markdown renderer and nothing third-party reaches the page. A draft is served only to the signed-in
 * admin; that request carries a cookie, and the Worker never stores a cookie-bearing response.
 * The markdown twin is the sibling route in content-page[.md].ts.
 */
export function headers() {
  return new Headers(publicHtmlHeaders(CONTENT_PAGE_HTML_TAGS));
}

export async function loader({ request, context }: Route.LoaderArgs) {
  const env = getEnv(context);
  const url = new URL(request.url);
  const pathname = url.pathname.replace(/\/+$/, "") || "/";
  const row = await getPageByPath(env, pathname);
  // A path with no row: not a page, or a page the content sync has not written yet.
  if (!row) throw data(null, { status: 404 });
  if (row.status === "draft" && !(await isAdminViewer(env, request))) throw data(null, { status: 404 });
  const page = row.record;
  // The page it sits under, by title, for the trail above it (the course above its FAQ).
  const parent = page.path.slice(0, page.path.lastIndexOf("/"));
  const titles = await getPublishedPageTitles(env, parent ? [parent] : []);
  // The lead a named Software page opens with, drawn from D1 beside the page (docs/DICTIONARY.md): an entry
  // edit is live at the next request, and a page with no published entry has none.
  const entry = await getPublishedEntryByPath(env, page.path);
  const trail = contentPageTrail(page, (path) => titles.get(path));
  // The program page carries the roster, drawn from D1 beside the page (docs/ROSTER.md); no other page reads it.
  const roster = page.path === ROSTER_PAGE_PATH ? await listRoster(env) : null;
  // A calculator page computes from its query string, so Calculate works as a plain GET with script off.
  return { page, trail, entry, roster, draft: row.status === "draft", search: toolsOnPage(page.path).length > 0 ? url.search : "" };
}

/** The page's own social card where build:og draws one (CARDED_PAGE_ROOTS), else the site card. */
function cardUrl(page: PageRecord) {
  const path = contentPageCardPath(page);
  return path ? `${SITE_ORIGIN}${path}` : undefined;
}

export function meta({ loaderData }: Route.MetaArgs) {
  if (!loaderData) return [];
  const { page, draft } = loaderData;
  return [
    ...pageMeta({ title: page.seoTitle, description: page.description, path: page.path, image: cardUrl(page) }),
    {
      tagName: "link",
      rel: "alternate",
      type: "text/markdown",
      href: `${SITE_ORIGIN}${contentPageMarkdownPath(page.path)}`,
    },
    ...(draft ? [{ name: "robots", content: "noindex" }] : []),
  ];
}

export default function ContentPageRoute({ loaderData }: Route.ComponentProps) {
  const { page, trail, entry, roster, draft, search } = loaderData;
  return (
    <PageShell
      trail={
        <>
          {contentPageJsonLd(page, trail, entry).map((block, i) => (
            <script
              key={i}
              type="application/ld+json"
              dangerouslySetInnerHTML={{ __html: serializeJsonLd(block) }}
            />
          ))}
        </>
      }
    >
      <Breadcrumb trail={trail} />
      <h1 className="page-title">{page.title}</h1>
      {draft ? <p>Draft: only you can see this page.</p> : null}
      {page.banner || page.lastUpdated ? (
        <div className="legal-status">
          {page.banner ? <p className="legal-banner">{page.banner}</p> : null}
          {page.lastUpdated ? (
            <p className="legal-updated">
              Last updated <time dateTime={page.lastUpdated}>{longDateUTC(page.lastUpdated)}</time>
            </p>
          ) : null}
        </div>
      ) : null}
      {entry ? <DictionaryEntry entry={entry} /> : null}
      <PhageTools path={page.path} search={search} />
      <div className="prose" dangerouslySetInnerHTML={{ __html: page.html }} />
      {roster ? (
        <div className="prose">
          <PhageRoster cohorts={roster} />
        </div>
      ) : null}
    </PageShell>
  );
}