Skip to the content

Capsid menu

app/lib/blog-listing.mjs

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

Functions

The functions of blog-listing.mjs
FunctionLinesComplexityCoveredCRAPMutation
picksFirst23 to 341100%OkCRAP 1No data: no mutants in this code
<anonymous:25>25 to 252100%OkCRAP 2No data: no mutants in this code
rank26 to 292100%OkCRAP 2No data: no mutants in this code
<anonymous:31>31 to 311100%OkCRAP 1No data: no mutants in this code
<anonymous:32>32 to 322100%OkCRAP 2No data: no mutants in this code
<anonymous:33>33 to 331100%OkCRAP 1No data: no mutants in this code
readPage41 to 455100%OkCRAP 5No data: no mutants in this code
pageForPosition51 to 53133%OkCRAP 1.3No data: no mutants in this code
pageCount61 to 63133%OkCRAP 1.3No data: no mutants in this code
splitFeatured74 to 794100%OkCRAP 4No data: no mutants in this code
<anonymous:76>76 to 761100%OkCRAP 1No data: no mutants in this code
<anonymous:78>78 to 781100%OkCRAP 1No data: no mutants in this code
startHere92 to 975100%OkCRAP 5No data: no mutants in this code
listingFacts108 to 12277%CriticalCRAP 46.8No data: no mutants in this code

Imports

It imports no other module of this repo. Imported by app/components/taxonomy-listing.tsx, app/db/posts.ts, app/routes/admin.media._index.tsx, app/routes/blog._index.tsx, app/routes/blog.series.$series.tsx, app/routes/blog.tags.$tag.tsx.

Source

export const POSTS_PER_PAGE = 10;

/** The home page's Writing section is a taste, not a feed: the featured post and two more. */
export const HOME_CARDS = 3;

/**
 * The essays that follow the featured post on the home page, in this order. `featured` marks one post
 * (it leads /writing), so the rest of the home page's best-of is a hand-picked list. A pick that is
 * not published drops out and the newest post fills its place; test/blog-listing.test.mjs holds each
 * pick against a published file in content/posts.
 */
export const HOME_PICKS = ["policy-in-the-api-not-the-mcp", "observable-plot-inside-a-worker"];

/**
 * The home page's picks first, in `picks` order, then everything else in the order given. Stable, so
 * the rest stay newest first. Duplicates by slug are dropped, because the caller merges two queries.
 *
 * @template {{ slug: string }} T
 * @param {T[]} rows none of them featured
 * @param {readonly string[]} [picks]
 * @returns {T[]}
 */
export function picksFirst(rows, picks = HOME_PICKS) {
  const seen = new Set();
  const unique = rows.filter((row) => !seen.has(row.slug) && seen.add(row.slug));
  const rank = (/** @type {string} */ slug) => {
    const i = picks.indexOf(slug);
    return i === -1 ? picks.length : i;
  };
  return unique
    .map((row, i) => ({ row, i }))
    .sort((a, b) => rank(a.row.slug) - rank(b.row.slug) || a.i - b.i)
    .map(({ row }) => row);
}

/**
 * A `?page=` a listing can use: a positive whole number, else 1. `-3`, `1.5` and `abc` all read as 1.
 *
 * @param {string | null} raw
 */
export function readPage(raw) {
  if (raw === null || !/^\d+$/.test(raw)) return 1;
  const page = Number(raw);
  return page > 0 && Number.isSafeInteger(page) ? page : 1;
}

/**
 * @param {number} position 1-based
 * @param {number} [perPage]
 */
export function pageForPosition(position, perPage = POSTS_PER_PAGE) {
  return Math.floor((position - 1) / perPage) + 1;
}

/**
 * An empty corpus still has one page, because /writing renders an empty state rather than a 404.
 *
 * @param {number} total
 * @param {number} [perPage]
 */
export function pageCount(total, perPage = POSTS_PER_PAGE) {
  return Math.max(1, Math.ceil(total / perPage));
}

/**
 * Returns the list with the hero REMOVED, or it renders twice. Matched by slug, not identity,
 * because a serialization boundary upstream would silently break identity.
 *
 * @template {{ slug: string, featured?: boolean }} T
 * @param {T[]} posts the page of posts, in order
 * @param {boolean} eligible whether this request may show a hero at all
 * @returns {{ featured: T | null, posts: T[] }}
 */
export function splitFeatured(posts, eligible) {
  if (!eligible) return { featured: null, posts };
  const featured = posts.find((post) => post.featured) ?? null;
  if (!featured) return { featured: null, posts };
  return { featured, posts: posts.filter((post) => post.slug !== featured.slug) };
}

/**
 * A pure function rather than SQL because the offline machine-readable check renders this section
 * without a database. With no featured post the newest leads. `others` must exclude the featured
 * post or it prints twice.
 *
 * @template {{ slug: string }} T
 * @param {T[]} featuredRows at most one, the featured post
 * @param {T[]} others in the order they follow the lead, none of them featured: `picksFirst` for the home page
 * @param {number} [cards] how many the section shows in total, lead included
 * @returns {{ featured: T | null, recent: T[] }}
 */
export function startHere(featuredRows, others, cards = HOME_CARDS) {
  const lead = featuredRows[0] ?? others[0] ?? null;
  if (lead === null) return { featured: null, recent: [] };
  const recent = featuredRows[0] ? others.slice(0, cards - 1) : others.slice(1, cards);
  return { featured: lead, recent };
}


/**
 * The facts describe the FILTERED list, not the corpus: a tag view stating the whole blog's span
 * would be true of something the page does not show. A null is not a zero; the row omits itself.
 *
 * @param {number} total posts in the whole filtered list, not on this page of it
 * @param {{ firstYear: string | null, lastYear: string | null, minutes: number | null }} span
 * @returns {(string | null)[]} three facts, in the row's order
 */
export function listingFacts(total, span) {
  const years =
    span.firstYear && span.lastYear
      ? span.firstYear === span.lastYear
        ? span.firstYear
        : `${span.firstYear} to ${span.lastYear}`
      : null;
  // Hours only past two: "1 hour" rounded from 38 minutes would be wrong.
  const reading = span.minutes
    ? span.minutes >= 120
      ? `${Math.round(span.minutes / 60)} hours of reading`
      : `${span.minutes} minutes of reading`
    : null;
  return [`${total} post${total === 1 ? "" : "s"}`, years, reading];
}