app/routes.ts
181 lines and 5 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| <anonymous:75> | 75 to 80 | 1 | 100% | OkCRAP 1 | No data: no mutants in this code |
| <anonymous:82> | 82 to 82 | 1 | 100% | OkCRAP 1 | No data: no mutants in this code |
| <anonymous:82> | 82 to 90 | 1 | 100% | OkCRAP 1 | No data: no mutants in this code |
| <anonymous:93> | 93 to 93 | 2 | 100% | OkCRAP 2 | No data: no mutants in this code |
| <anonymous:93> | 93 to 94 | 1 | 100% | OkCRAP 1 | No data: no mutants in this code |
Imports
It imports app/kb/bases.mjs, app/kb/libraries.mjs, app/kb/procedures/taxonomy.mjs, app/lib/content-pages.mjs. No module of this repo imports it.
Source
import { type RouteConfig, index, layout, route } from "@react-router/dev/routes";
import { CONTENT_PAGE_PATHS, CONTENT_PAGES_FROM_DATA } from "./lib/content-pages.mjs";
import { BASES } from "./kb/bases.mjs";
import { LIBRARY_BASES } from "./kb/libraries.mjs";
import { LIBRARY_PATH } from "./kb/procedures/taxonomy.mjs";
export default [
// TEMPORARY: everything a visitor can see sits under one layout, which owns the old public CSS. The public
// rebuild on Capsomer deletes it (routes/legacy-public.tsx says what goes with it).
layout("routes/legacy-public.tsx", [
index("routes/home.tsx"),
// Writing: the posts, at /writing since the 2026-09-27 site structure. The module files keep their
// blog.* names; only the addresses moved.
route("writing", "routes/blog._index.tsx"),
// Ordered before the :slug routes so the feed is not read as a post slug.
route("writing/rss.xml", "routes/blog.rss[.xml].ts"),
route("writing/feed.json", "routes/blog.feed[.json].ts"),
route("writing/atom.xml", "routes/blog.atom[.xml].ts"),
route("writing/tags/:tag/rss.xml", "routes/blog.tags.$tag.rss[.xml].ts"),
route("writing/tags/:tag/feed.json", "routes/blog.tags.$tag.feed[.json].ts"),
route("writing/tags/:tag", "routes/blog.tags.$tag.tsx"),
route("writing/series/:series/rss.xml", "routes/blog.series.$series.rss[.xml].ts"),
route("writing/series/:series/feed.json", "routes/blog.series.$series.feed[.json].ts"),
route("writing/series/:series", "routes/blog.series.$series.tsx"),
route("writing/:slug.md", "routes/blog.$slug[.md].ts"),
route("writing/:slug", "routes/blog.$slug.tsx"),
// The old feed addresses keep answering with the feed itself, not a redirect: a feed reader is
// told the address once and may never follow a 301. Same modules, their own route ids.
route("blog/rss.xml", "routes/blog.rss[.xml].ts", { id: "legacy-blog-rss" }),
route("blog/feed.json", "routes/blog.feed[.json].ts", { id: "legacy-blog-feed" }),
route("blog/atom.xml", "routes/blog.atom[.xml].ts", { id: "legacy-blog-atom" }),
route("blog/tags/:tag/rss.xml", "routes/blog.tags.$tag.rss[.xml].ts", { id: "legacy-tag-rss" }),
route("blog/tags/:tag/feed.json", "routes/blog.tags.$tag.feed[.json].ts", { id: "legacy-tag-feed" }),
route("blog/series/:series/rss.xml", "routes/blog.series.$series.rss[.xml].ts", { id: "legacy-series-rss" }),
route("blog/series/:series/feed.json", "routes/blog.series.$series.feed[.json].ts", { id: "legacy-series-feed" }),
// Top level, never under the post route: that route sends public cache headers, Workers Cache
// does not key on cookies and a preview link is cookieless, so sharing it would cache a draft.
route("preview/:token", "routes/preview.$token.tsx"),
// Under Research since the 2026-09-27 site structure; the old /publications addresses 301.
route("research/publications", "routes/publications.tsx"),
// The citation exports precede the page routes so a slug ending in `.bib` cannot collide. Paper pages
// take a trailing slash: page and PDF in one directory is Scholar's condition for citation_pdf_url.
route("research/publications.bib", "routes/publications[.bib].ts"),
route("research/publications.ris", "routes/publications[.ris].ts"),
route("research/publications.json", "routes/publications[.json].ts"),
route("research/publications/:slug.bib", "routes/publications.$slug[.bib].ts"),
route("research/publications/:slug.ris", "routes/publications.$slug[.ris].ts"),
route("research/publications/:slug.md", "routes/publications.$slug[.md].ts"),
route("research/publications/:slug", "routes/publications.$slug.tsx"),
// The Research pages (app/lib/content-pages.mjs), drawn from D1 (docs/PAGES.md). A splat, so the
// more specific research/publications routes above still win; a path with no page answers 404.
// The phage page: its prose and sections from the page row, its table as the catalog over the phage rows.
route("research/phages", "routes/phages.tsx"),
// The family of systems (app/lib/systems.mjs): one page, a chosen system kept in its query
// (?system=<id>), and its twin.
route("systems.md", "routes/systems[.md].ts", { id: "systems-md" }),
route("systems", "routes/systems.tsx"),
// The lab registry (docs/REGISTRY.md): the master inventory, a kind's own page and one item, each with its twin.
// The twins come first so `.md` is not read as part of a kind or an id; the item's id is the kind's optional segment.
route("research/lab", "routes/lab.tsx"),
route("research/lab.md", "routes/lab[.md].ts", { id: "lab-md" }),
route("research/lab/:kind.md", "routes/lab[.md].ts", { id: "lab-kind-md" }),
route("research/lab/:kind/:id.md", "routes/lab[.md].ts", { id: "lab-item-md" }),
route("research/lab/:kind/:id?", "routes/lab.kind.tsx"),
// Procedures (docs/PROCEDURES.md): drawn from D1 at request time, so they precede the splat. Each
// profile's root takes the page, its printable sheet and its markdown twin; the twin precedes the
// page so `.md` is not read as part of a slug.
// The protocol library: the page, its twin and its downloads, drawn from the procedure rows (library.mjs).
route("research/protocols", "routes/protocols.tsx"),
route("research/protocols.md", "routes/protocols[.md].ts"),
route("research/protocols.json", "routes/protocols[.json].ts"),
route("research/protocols.csv", "routes/protocols[.csv].ts"),
// The other knowledge bases' libraries (app/kb/libraries.mjs): the page, its twin and its downloads, one module for all.
...LIBRARY_BASES.flatMap((base) => [
route(base.library.slice(1), "routes/kb-library.tsx", { id: `library-${base.id}` }),
route(`${base.library.slice(1)}.md`, "routes/kb-library[.md].ts", { id: `library-${base.id}-md` }),
route(`${base.library.slice(1)}.json`, "routes/kb-library[.json].ts", { id: `library-${base.id}-json` }),
route(`${base.library.slice(1)}.csv`, "routes/kb-library[.csv].ts", { id: `library-${base.id}-csv` }),
]),
// One set per knowledge base (app/kb/bases.mjs), at its entry root.
...BASES.map((base) => [base.entryRoot.slice(1, -1), base.profile]).flatMap(([root, id]) => [
route(`${root}/:slug.md`, "routes/procedure[.md].ts", { id: `procedure-${id}-md` }),
// A frozen version (drizzle/0027_procedure_versions.sql): its page and its twin, the twin first as above.
route(`${root}/:slug/v/:version.md`, "routes/procedure.version[.md].ts", { id: `procedure-${id}-version-md` }),
route(`${root}/:slug/v/:version`, "routes/procedure.version.tsx", { id: `procedure-${id}-version` }),
route(`${root}/:slug/v/:version/sheet`, "routes/procedure.version.sheet.tsx", { id: `procedure-${id}-version-sheet` }),
route(`${root}/:slug/sheet`, "routes/procedure.sheet.tsx", { id: `procedure-${id}-sheet` }),
route(`${root}/:slug`, "routes/procedure.tsx", { id: `procedure-${id}` }),
]),
// Each page's markdown twin, drawn from D1 like the page. One route per listed path, because a splat
// cannot end in `.md`; a path with no page answers through the splats below. /cv's twin has its own route, below.
...CONTENT_PAGE_PATHS.filter((path) => !CONTENT_PAGES_FROM_DATA.includes(path) && path !== LIBRARY_PATH).map((path) =>
route(`${path.slice(1)}.md`, "routes/content-page[.md].ts", { id: `page-twin-${path.slice(1).replaceAll("/", "-")}` }),
),
route("research/*", "routes/content-page.tsx"),
route("teaching/*", "routes/teaching.tsx"),
// Same markdown pages as Research and Teaching. The module is its own file so headers() is this
// route's, which check:headers reads per file.
route("software/*", "routes/software.tsx"),
route("about", "routes/about.tsx"),
// The CV, one of the markdown pages; the old 2019 CV PDF address 301s here on the apex host.
route("cv", "routes/cv.tsx"),
// The CV twin, drawn from the same D1 rows as the page (docs/CV.md).
route("cv.md", "routes/cv[.md].ts"),
// The CV's charts for a filter state, which app/enhance/cv.ts swaps in so the page ships no renderer.
route("cv/charts.json", "routes/cv.charts[.json].ts"),
// The CV's PDF: one R2 object the Worker renders after each CV save (docs/CV.md). A static file at this
// address would win over the route, so there is none.
route("dustin-edwards-cv.pdf", "routes/cv-pdf.ts"),
// `/colophon` is the IndieWeb convention tooling expects; the page title carries the legibility.
route("colophon", "routes/colophon.tsx"),
route("privacy", "routes/privacy.tsx"),
route("terms", "routes/terms.tsx"),
// The accessibility statement, a markdown page like privacy and terms (docs/PAGES.md).
route("accessibility", "routes/accessibility.tsx"),
route("contact", "routes/contact.tsx"),
// 410 Gone for removed WordPress addresses; the gateway renders it in their place on the apex host.
route("gone", "routes/gone.tsx"),
// Before /search, and a resource route so it can stream a raw Response.
route("search/ask", "routes/search.ask.ts"),
route("search", "routes/search.tsx"),
route("sitemap.xml", "routes/sitemap.ts"),
// The site map for people, from the same URL list as sitemap.xml (app/lib/site-urls.server.ts).
route("site-map", "routes/site-map.tsx"),
route("robots.txt", "routes/robots.ts"),
route(".well-known/security.txt", "routes/security-txt.ts"),
route("llms.txt", "routes/llms.ts"),
route("llms-full.txt", "routes/llms-full[.txt].ts"),
route("media/*", "routes/media.$.ts"),
route("theme", "routes/theme.ts"),
// Unauthenticated because senders have no credential to offer; bounded in the route file. At the
// root, not under `/api`, because it is advertised in a `<link>` and is published surface.
route("webmention", "routes/webmention.ts"),
// The old sign-in page. Sign-in is Cloudflare Access on /admin, so this only forwards there.
route("login", "routes/login.ts"),
// Last, so a real route always wins; it only gives the 404 page the public layout.
route("*", "routes/not-found.tsx"),
]),
// Private: the admin layout gates every child via middleware.
route("admin", "routes/admin.tsx", [
index("routes/admin._index.tsx"),
route("mentions", "routes/admin.mentions.tsx"),
// The Knowledge Base (docs/KNOWLEDGE-BASE.md): read-only, a tab per base and Needs info.
route("kb", "routes/admin.kb.tsx"),
// One file of it, edited and saved through the save the operator API uses (step 3).
route("kb/entry/:slug", "routes/admin.kb.entry.tsx"),
route("kb/item/:kind/:id", "routes/admin.kb.item.tsx"),
// The editor's live preview: the entry the form describes, drawn by the public page's component. Read only.
route("kb/preview/:slug", "routes/admin.kb.preview.tsx"),
route("tools", "routes/admin.tools.tsx"),
route("logout", "routes/admin.logout.tsx"),
route("media", "routes/admin.media._index.tsx"),
route("media/upload", "routes/admin.media.upload.ts"),
// Read-only. POST because a whole post body does not belong in a query string, not because it writes.
route("preview", "routes/admin.preview.ts"),
route("posts", "routes/admin.posts._index.tsx"),
route("posts/new", "routes/admin.posts.new.tsx"),
route("posts/:slug/edit", "routes/admin.posts.$slug.edit.tsx"),
route("posts/:slug/history", "routes/admin.posts.$slug.history.tsx"),
// Loader only, no action, so restoring a revision cannot write.
route("posts/:slug/revisions", "routes/admin.posts.$slug.revisions.tsx"),
]),
// The lab workspace (docs/KNOWLEDGE-BASE.md step 5): lab members, through their own Access application. The layout's
// middleware admits a person only with an active row in `members`, read on every request. Not /admin, which stays
// the Primary Owner's alone. Module files are workspace.* because routes/lab.tsx is the public /research/lab.
route("lab", "routes/workspace.tsx", [
index("routes/workspace._index.tsx"),
route("members", "routes/workspace.members.tsx"),
// Step 7: lots and run records. Append-only runs; every lot change writes its kb_audit row.
route("inventory", "routes/workspace.inventory.tsx"),
route("inventory/:id", "routes/workspace.inventory.lot.tsx"),
route("runs", "routes/workspace.runs.tsx"),
route("runs/:id", "routes/workspace.runs.run.tsx"),
]),
route("api/health", "routes/api.health.ts"),
// Unauthenticated because browsers send reports without credentials; capped and rate limited there.
route("api/csp-report", "routes/api.csp-report.ts"),
// Bearer token, not the session, so it sits outside the /admin subtree the middleware gates.
route("api/operator", "routes/api.operator.ts"),
// Carrel's key, not the session, and only this prefix: the site-api package guards it.
route("api/carrel/v1/*", "routes/api.carrel.v1.$.ts"),
] satisfies RouteConfig;