app/routes/home.tsx
317 lines and 22 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| headers | 39 to 41 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| meta | 43 to 45 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| homePapers | 55 to 68 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:57> | 57 to 57 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:59> | 59 to 59 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:61> | 61 to 67 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| loader | 70 to 103 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:75> | 75 to 75 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:76> | 76 to 76 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:77> | 77 to 77 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:78> | 78 to 78 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:79> | 79 to 79 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| Section | 143 to 163 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| Row | 166 to 188 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| Home | 190 to 342 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:217> | 217 to 225 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:236> | 236 to 244 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:254> | 254 to 262 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:285> | 285 to 285 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:285> | 285 to 310 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:317> | 317 to 325 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:331> | 331 to 337 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/components/enhance.tsx, app/components/home-intro.tsx, app/components/home-podcast.tsx, app/components/plate-i.tsx, app/components/shell-footer.tsx, app/components/site-header.tsx, app/db/index.ts, app/db/publications.ts, app/db/roster.ts, app/lib/context.ts, app/lib/json-ld.mjs, app/lib/long-date.mjs, app/lib/nav.ts, app/lib/podcast/podcast.server.ts, app/lib/publications/entities.mjs, app/lib/publications/types.ts, app/lib/roster/compile.mjs, app/lib/seo.ts, app/lib/timing.ts. No module of this repo imports it.
Source
import type { ReactNode } from "react";
import { Link, data } from "react-router";
import { ShellFooter } from "~/components/shell-footer";
import { HomeIntro } from "~/components/home-intro";
import { SiteHeader } from "~/components/site-header";
import { listHomeStartHere, nextScheduledPublishAt } from "~/db";
import { jsonLd as serializeJsonLd } from "~/lib/json-ld.mjs";
import { getEnv } from "~/lib/context";
import { longDateUTC } from "~/lib/long-date.mjs";
import { timed, timingsContext } from "~/lib/timing";
import {
cacheTags,
EDGE_CACHE_HEADER,
HOME_EDGE_FRESH_SECONDS,
publicHtmlHeaders,
scheduledEdgeCacheControl,
personJsonLd,
SITE,
SITE_ORIGIN,
webSiteJsonLd,
pageMeta,
} from "~/lib/seo";
import { PlateI, PlateKeyRow } from "~/components/plate-i";
import { Enhance } from "~/components/enhance";
import { HomePodcast } from "~/components/home-podcast";
import { homePodcastEpisode } from "~/lib/podcast/podcast.server";
import { listPublishedPublications } from "~/db/publications";
import { listRoster } from "~/db/roster";
import { decodeEntities } from "~/lib/publications/entities.mjs";
import { rosterFacts } from "~/lib/roster/compile.mjs";
import type { Publication } from "~/lib/publications/types";
import { RESEARCH_AREAS, SOFTWARE_PRODUCTS } from "~/lib/nav";
import type { Route } from "./+types/home";
import "~/styles/home.css";
/** Publicly cacheable; the theme is a cache-key dimension, not a Vary. The short edge policy is for the Germomics episode. */
export function headers({ loaderHeaders }: Route.HeadersArgs) {
return publicHtmlHeaders(cacheTags(), loaderHeaders.get(EDGE_CACHE_HEADER) ?? undefined);
}
export function meta() {
return pageMeta({ title: `${SITE.name}, ${SITE.role} at ${SITE.affiliation}`, description: SITE.description, path: "/" });
}
/** How many papers the Publications section shows. */
const HOME_PAPERS = 3;
/**
* The papers marked `selected` in their files (content/publications/), newest first; with none marked, the newest.
* Chosen in the loader and not the component, so the publication records (abstracts, author lists)
* stay out of the page's script.
*/
function homePapers(papers: Publication[]) {
const newest = [...papers].sort(
(a, b) => b.year - a.year || (b.publishedDate ?? "").localeCompare(a.publishedDate ?? ""),
);
const selected = newest.filter((p) => p.selected);
/* Titles and journals go through `decodeEntities`: the deposited records carry HTML entities React would print literally. */
return (selected.length > 0 ? selected : newest).slice(0, HOME_PAPERS).map((p) => ({
id: p.id,
year: p.year,
title: decodeEntities(p.title),
journal: p.journal ? decodeEntities(p.journal) : null,
href: `/research/publications/${p.slug}/`,
}));
}
export async function loader({ context }: Route.LoaderArgs) {
const env = getEnv(context);
const timings = context.get(timingsContext).timings;
const [start, podcast, nextPublishAt, papers, cohorts] = await Promise.all([
timed(timings, "home_posts", () => listHomeStartHere(env, { timings })),
timed(timings, "home_podcast", () => homePodcastEpisode(context)),
timed(timings, "home_next_scheduled", () => nextScheduledPublishAt(env)),
timed(timings, "home_papers", () => listPublishedPublications(env)),
timed(timings, "home_roster", () => listRoster(env)),
]);
return data(
{
posts: start.total,
featured: start.featured,
recent: start.recent,
papers: homePapers(papers),
paperCount: papers.length,
// Counted from the roster table, never typed: a typed number drifts when a cohort lands. The names stay out of the payload.
discovery: rosterFacts(cohorts),
podcast,
},
{
headers: {
[EDGE_CACHE_HEADER]: scheduledEdgeCacheControl(
new Date(),
nextPublishAt,
HOME_EDGE_FRESH_SECONDS,
),
},
},
);
}
/** The current retrovirus work, named beside the three areas. Title as the page titles itself. */
const AVIAN = {
to: "/research/retroviruses/avian",
label: "REV and LPDV in wild turkeys in Texas",
description: "Two bird retroviruses, and why they matter for wild turkeys and prairie chickens",
};
/** The lab's own pages. Each line is cut from that page's description. */
const LAB_RESOURCES = [
{
rail: "Protocol",
to: "/research/protocols/phage-isolation",
label: "Phage isolation and purification",
description:
"Where the Tarleton SEA-PHAGES lab runs a step differently from the Phage Discovery Guide",
},
{
rail: "Calculations",
to: "/teaching/virus-isolation/faq",
label: "Lab calculations and common questions",
description: "Titers in pfu/ml, spot titer dilutions, webbed plate volumes and lysate yields",
},
{
rail: "Calculators",
to: "/research/tools",
label: "Phage lab calculators",
description:
"Titer from plaque counts and spot titers, serial dilution plans and webbed plate volumes, with the arithmetic shown",
},
{
rail: "Protocol",
to: "/research/protocols/coi-primers",
label: "COI primers: LCO1490 and HCO2198",
description: "Primer sequences and PCR conditions for COI barcoding of invertebrates",
},
];
/** One section of the home page: a heading, a short list, and the link to the rest. */
function Section({
id,
title,
more,
children,
}: {
id: string;
title: string;
more: ReactNode;
children: ReactNode;
}) {
return (
<section className="home-section" aria-labelledby={`${id}-heading`}>
<h2 id={`${id}-heading`} className="home-section-heading">
{title}
</h2>
{children}
<p className="home-more">{more}</p>
</section>
);
}
/** A short mono label in the rail, a title that links, and one line under it. */
function Row({
rail,
to,
title,
summary,
}: {
rail: ReactNode;
to: string;
title: ReactNode;
summary?: ReactNode;
}) {
return (
<li className="home-row">
<span className="home-row-date">{rail}</span>
<span className="home-row-body">
<span className="home-row-title">
<Link to={to}>{title}</Link>
</span>
{summary ? <span className="home-row-summary">{summary}</span> : null}
</span>
</li>
);
}
export default function Home({ loaderData }: Route.ComponentProps) {
const jsonLd = [personJsonLd(SITE_ORIGIN), webSiteJsonLd(SITE_ORIGIN)];
const { posts, featured, recent, papers, paperCount, discovery, podcast } = loaderData;
return (
<>
<SiteHeader />
<main className="tracks home-tracks" id="main" tabIndex={-1}>
{/* `rel="me"` links live in the footer; only `u-url` needed a home here. No `u-photo`: the site publishes none. */}
<div className="home-hero u-wide">
<HomeIntro />
<figure className="home-plate">
<PlateI />
<figcaption className="home-plate-caption">
<span className="home-plate-num">Plate I</span>
<span className="home-plate-cap">Plaque morphology, drawn.</span>
</figcaption>
</figure>
</div>
<PlateKeyRow />
<Enhance module="plate" />
<Section id="research" title="Research" more={<Link to="/research">All research</Link>}>
<p className="home-research-line">I study viral genomics.</p>
<ul className="home-rows">
{RESEARCH_AREAS.map((area) => (
<Row
key={area.to}
rail="Area"
to={area.to}
title={area.label}
summary={area.description}
/>
))}
<Row rail="Current" to={AVIAN.to} title={AVIAN.label} summary={AVIAN.description} />
</ul>
</Section>
<Section
id="publications"
title="Publications"
more={<Link to="/research/publications">All {paperCount} papers</Link>}
>
<ol className="home-rows">
{papers.map((paper) => (
<Row
key={paper.id}
rail={paper.year}
to={paper.href}
title={paper.title}
summary={paper.journal}
/>
))}
</ol>
</Section>
<Section
id="lab"
title="Lab resources"
more={<Link to="/research/protocols">All protocols</Link>}
>
<ul className="home-rows">
{LAB_RESOURCES.map((item) => (
<Row
key={item.to}
rail={item.rail}
to={item.to}
title={item.label}
summary={item.description}
/>
))}
</ul>
</Section>
<Section id="teaching" title="Teaching" more={<Link to="/teaching">All teaching</Link>}>
<ul className="home-rows">
<Row
rail="SEA-PHAGES"
to="/teaching/phage-discovery"
title="Phage Discovery Program"
summary={
`The two-semester undergraduate research program in HHMI SEA-PHAGES: ` +
`${discovery.researchers} student researchers in ${discovery.cohorts} cohorts ` +
`since ${discovery.since}.`
}
/>
</ul>
</Section>
{featured ? (
<Section id="writing" title="Writing" more={<Link to="/writing">All {posts} posts</Link>}>
{/* The same four properties as `PostCard`; `check:machine-readable` reads both. No h-feed: a hand-picked three is not the feed. */}
<ol className="home-rows">
{[featured, ...recent.filter((post) => post.slug !== featured.slug)].map((post) => (
<li key={post.slug} className="home-row h-entry">
<span className="home-row-date">
{post.publishAt ? (
<time
className="dt-published"
dateTime={new Date(post.publishAt).toISOString()}
>
{longDateUTC(post.publishAt)}
</time>
) : null}
</span>
<span className="home-row-body">
<span className="home-row-title p-name">
<Link className="u-url" to={`/writing/${post.slug}`}>
{post.title}
</Link>
</span>
{post.description ? (
<span className="home-row-summary home-row-clamp p-summary">
{post.description}
</span>
) : null}
</span>
</li>
))}
</ol>
</Section>
) : null}
<Section id="software" title="Software" more={<Link to="/software">All software</Link>}>
<ul className="home-rows">
{SOFTWARE_PRODUCTS.map((product) => (
<Row
key={product.to}
rail="Product"
to={product.to}
title={product.label}
summary={product.description}
/>
))}
</ul>
</Section>
<HomePodcast episode={podcast} />
{jsonLd.map((data, i) => (
<script
key={i}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
/>
))}
</main>
<ShellFooter />
</>
);
}