Skip to the content

Capsid menu

app/routes/blog.series.$series.tsx

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

Functions

The functions of blog.series.$series.tsx
FunctionLinesComplexityCoveredCRAPMutation
loader30 to 492No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
headers51 to 552No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
meta57 to 682No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
BlogSeries70 to 812No 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/taxonomy-listing.tsx, app/db/index.ts, app/lib/blog-listing.mjs, app/lib/context.ts, app/lib/seo.ts, app/lib/series-path.mjs. No module of this repo imports it.

Source

import { data } from "react-router";

import {
  TaxonomyListing,
  listingHref,
  redirectPastLastPage,
} from "~/components/taxonomy-listing";
import { getBlogSeries, listSeriesPosts, nextScheduledPublishAt } from "~/db";
import { POSTS_PER_PAGE, readPage } from "~/lib/blog-listing.mjs";
import { getEnv } from "~/lib/context";
import {
  EDGE_CACHE_HEADER,
  SITE,
  pageMeta,
  cacheTags,
  publicHtmlHeaders,
  scheduledEdgeCacheControl,
} from "~/lib/seo";
import { seriesPath } from "~/lib/series-path.mjs";
import type { Route } from "./+types/blog.series.$series";

import "~/styles/evidence-row.css";
import "~/styles/listing.css";
import "~/styles/entry-list.css";

/**
 * Ordered by part, the one listing that is not newest first. 404 rather than an empty page, which
 * would be a soft 404 and leak a series only a draft carries.
 */
export async function loader({ params, request, context }: Route.LoaderArgs) {
  const env = getEnv(context);
  const url = new URL(request.url);
  const page = readPage(url.searchParams.get("page"));

  const series = await getBlogSeries(env, params.series);
  if (!series) throw data("Not found", { status: 404 });

  const [listing, nextPublishAt] = await Promise.all([
    listSeriesPosts(env, series.name, { page, perPage: POSTS_PER_PAGE }),
    nextScheduledPublishAt(env),
  ]);

  redirectPastLastPage(page, listing.pageCount, seriesPath(series.name));

  return data(
    { ...listing, series },
    { headers: { [EDGE_CACHE_HEADER]: scheduledEdgeCacheControl(new Date(), nextPublishAt) } },
  );
}

export function headers({ loaderHeaders }: Route.HeadersArgs) {
  return new Headers(
    publicHtmlHeaders(cacheTags(), loaderHeaders.get(EDGE_CACHE_HEADER) ?? undefined),
  );
}

export function meta({ loaderData }: Route.MetaArgs) {
  if (!loaderData) {
    return [{ title: `Not found | ${SITE.name}` }];
  }
  const { series, page } = loaderData;
  const path = listingHref(seriesPath(series.name), page);
  return pageMeta({
    title: `${series.name} | ${SITE.name}`,
    description: `Every part of ${series.name}, a series on ${SITE.name}'s blog.`,
    path,
  });
}

export default function BlogSeries({ loaderData }: Route.ComponentProps) {
  const { series, ...listing } = loaderData;
  return (
    <TaxonomyListing
      {...listing}
      base={seriesPath(series.name)}
      name={series.name}
      heading={series.name}
      dek={`A series in ${series.total} part${series.total === 1 ? "" : "s"}, in order.`}
    />
  );
}