Strategies — Markdown-Authored Strategy Documents with Inline Citations
Strategy documents become first-class: markdown in, blog-style pages out, with inline citations and an index of preview cards.
Changelog — 2026-04-25_01
Summary
This changeset adds a new Strategies section to the site where markdown-authored strategy documents render as full blog-style pages with inline citations, callout boxes, code blocks, and a sources list. It also introduces the site's first LFM (Lossless Flavored Markdown) integration for rich content rendering, and fixes a pronoun error in a team member's bio.
What stakeholders see:
New
/strategiesindex page listing strategy articles as preview cardsEach article has its own page (
/strategies/[slug]) with professional formatting, numbered source citations, and styled callout boxes
New content collection: Strategies
Content config
Added
strategiescollection tosrc/content.config.tsLoader:
glob({ pattern: '**/*.md', base: './src/content/strategies' })Schema fields:
title(string, required)description(string, optional)author(string, optional)date(coerced date, optional) — usesz.coerce.date()so string dates in frontmatter parse correctlyimage(string, optional) — for card preview imagestags(string array, optional)
Content directory
src/content/strategies/— already contained one file:why-markdown-needs-to-grow-up.mdNo new content files were added; the existing file is now served by the new collection and pages
New pages
src/pages/strategies/index.astro
Route:
/strategiesLoads all entries from the
strategiescollection viagetCollection('strategies')Renders each entry as a preview card in a responsive CSS grid (
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)))Card displays: title, description, author, date, tags, optional cover image
Uses
BaseThemeLayoutwrapper consistent with other site pagesHero section with gradient background using site CSS custom properties (
--color-primary-950,--color-primary-900)Empty state message when no strategies exist
src/pages/strategies/[slug].astro
Route:
/strategies/:slugUses
getStaticPathsto generate one page per strategy entryParses markdown body through
parseContent()(see below), receiving an enriched MDAST tree and structured citation arrayRenders article with:
Back link to
/strategiesHeader: title, description, author, date, tags
Markdown body via
<AstroMarkdown node={tree} />Sources section via
<Sources citations={citations} />(only rendered when citations exist)
Prose styling via scoped
:global()selectors for headings, paragraphs, lists, links, blockquotes, code, and horizontal rules
LFM integration — Markdown rendering pipeline
Dependencies added
@lossless-group/lfm— already present at^0.1.3inpackage.jsonmdast-util-to-string@^4.0.0— added (used byAstroMarkdown.astrofor heading ID generation)
Markdown components copied from packages/lfm-astro
Per the copy-and-adapt pattern documented in CLAUDE.md, the following components were copied from packages/lfm-astro/components/ into src/components/markdown/:
| Component | Purpose |
AstroMarkdown.astro | Recursive MDAST-to-JSX renderer. Walks the tree and renders 20+ node types including paragraphs, headings (with auto-generated id slugs), lists, links, images, code blocks, tables, blockquotes, directives, footnote references, and HTML pass-through. |
Sources.astro | Renders an ordered citation list at the bottom of an article. Each citation shows index number, linked title, source domain, and published date. Items are anchor-linked to inline [n] markers via id="source-{hex}". |
Callout.astro | Renders containerDirective nodes with name === "callout" as styled boxes. Supports types: info, tip, warning, danger, note, success, quote, example. Each type has distinct border/background colors. |
CodeBlock.astro | Renders fenced code blocks with a language label badge and dark background. |
MarkdownImage.astro | Renders :::image directives with support for float (left/right), captions (bottom/top/side), source attribution, and responsive unfloating via container queries. |
Import paths: AstroMarkdown.astro imports CodeBlock, Callout, and MarkdownImage from ./ (same directory). No path changes were needed since all five components live together in src/components/markdown/.
Key rendering behaviors
footnoteReferencenodes render as superscript[n]usingnode.data.citationIndex(sequential number), with an anchor link to#source-{hex}footnoteDefinitionnodes are suppressed (&& null) — their content is rendered by theSourcescomponent insteadcontainerDirectivenodes withname === "callout"route to theCalloutcomponentcontainerDirectivenodes withname === "image"route to theMarkdownImagecomponentUnrecognized node types fall through to a catch-all that renders their children
src/lib/parse-content.ts — Reusable markdown processing utility
This is the core addition that makes LFM rendering work across the site. It wraps parseMarkdown() from @lossless-group/lfm and polyfills two features that are missing from the published v0.1.3 but present in the unpublished v0.2.x workspace source.
Why this exists
The published @lossless-group/lfm@0.1.3 on GitHub Packages includes remark-gfm, remark-directive, and remark-callouts, but:
remark-citationsdoes not exist in 0.1.3 — it was added in 0.2.x (not yet published). Without it,footnoteReferencenodes retain their raw hex identifiers (a1b2c3) instead of sequential numbers (1).remark-calloutshas a regex bug in 0.1.3 — theCALLOUT_REGEXuses$without the multiline flag, so whenremark-parsemerges the> [!type] Titleline and the body text into a single text node with\n, the regex fails to match. Callouts render as plain blockquotes with raw[!tip]text.
API
import { parseContent } from '../lib/parse-content';
const { tree, citations } = await parseContent(markdownString);Returns:
tree— enriched MDAST root node, ready to pass to<AstroMarkdown node={tree} />citations— ordered array ofCitationobjects for<Sources citations={citations} />
Callout polyfill (fixCallouts)
Runs after
parseMarkdown()returns the treeWalks the tree looking for
blockquotenodes whose first text child starts with[!type]Matches against the first line only (splits on
\nbefore matching), avoiding the multiline regex bugTransforms matching blockquotes into
containerDirectivenodes with:name: 'callout'attributes: { type, title }children: remainder of the paragraph text + subsequent blockquote childrendata.hProperties: { class: 'callout callout-{type}' }
If
remarkCalloutsalready transformed the blockquote (future LFM version), there's nothing left to match, so the polyfill is a no-op
Citation polyfill
Checks
tree.data.citations.ordered— if populated (LFM >= 0.2), returns it directlyOtherwise, replicates the
remark-citationslogic:Collect: Walks tree depth-first collecting
footnoteReferenceidentifiers in document orderIndex: Assigns sequential 1-based indices by order of first appearance
Enrich: Sets
node.data.citationIndexandnode.data.citationHexon eachfootnoteReferencenodeParse definitions: Extracts text from
footnoteDefinitionchildren, parses[Title](URL)andPublished: YYYY-MM-DDpatterns into structuredCitationobjects with fields:identifier,hex,index,title,url,source(hostname),publishedDate,raw,parsedRemove definitions: Filters
footnoteDefinitionnodes out of the tree (they're rendered viaSourcescomponent)Return: Sorted citation array
Pattern notes for other sites
This utility is designed to be portable. To implement LFM rendering in another site:
Copy
src/lib/parse-content.tsandsrc/components/markdown/*.astrofrom this site (or frompackages/lfm-astro/)Install
@lossless-group/lfmandmdast-util-to-stringIn any page:
const { tree, citations } = await parseContent(entry.body)Render:
<AstroMarkdown node={tree} />and<Sources citations={citations} />When
@lossless-group/lfmis published at 0.2.x+ with nativeremark-citations, the polyfills inparseContentwill detect the existing output and skip themselves — no code changes needed
When to remove the polyfills
Once @lossless-group/lfm is published at a version that includes:
remark-citationswithcitationIndexenrichmentremark-calloutswith a multiline-aware regex (or first-line splitting)
...the polyfill code in parseContent becomes dead weight. At that point:
Update
package.jsonto require the new LFM versionSimplify
parseContentto justconst tree = await parseMarkdown(md); return { tree, citations: tree.data.citations.ordered ?? [] };Or remove
parse-content.tsentirely and callparseMarkdowndirectly in pages
Build status
pnpm buildpasses successfullyNew pages generated:
/strategies/index.html/strategies/why-markdown-needs-to-grow-up/index.html
Pre-existing Unsplash 403 warnings during prerender are unrelated (FactCard--w-Image component, missing/expired
UNSPLASH_ACCESS_KEY)
Files changed (tracked)
package.json— addedmdast-util-to-string@^4.0.0dependencysrc/content.config.ts— addedstrategiescollection definitionsrc/content/people/team.json— bio text correctionsrc/content/people/team-extended.json— bio text correction
Files added (untracked)
src/lib/parse-content.ts— reusable LFM parsing + citation/callout polyfillsrc/components/markdown/AstroMarkdown.astro— recursive MDAST renderer (copied frompackages/lfm-astro)src/components/markdown/Callout.astro— callout box component (copied frompackages/lfm-astro)src/components/markdown/CodeBlock.astro— code block component (copied frompackages/lfm-astro)src/components/markdown/MarkdownImage.astro— image directive component (copied frompackages/lfm-astro)src/components/markdown/Sources.astro— citation list component (copied frompackages/lfm-astro)src/pages/strategies/index.astro— strategies listing pagesrc/pages/strategies/[slug].astro— individual strategy article page
Known issues / follow-ups
LFM version: Site is on
@lossless-group/lfm@^0.1.3. TheparseContentpolyfill covers the gap, but publishing LFM 0.2.x would let all sites drop the polyfill.Callout regex bug: Exists in both the published 0.1.3 and the current workspace source (
packages/lfm/src/plugins/remark-callouts.tsline 15). TheCALLOUT_REGEXshould either use them(multiline) flag or split on\nbefore matching. This should be fixed in the LFM package itself.Strategy content: Only one article exists (
why-markdown-needs-to-grow-up.md). The infrastructure is ready for more.No
.npmrcin site root: The site relies on the monorepo root.npmrcfor@lossless-groupregistry resolution. For independent deployment, a site-level.npmrcwill be needed.
