Markdown to HTML, the way every collection renders it.
One pipeline: strip the frontmatter, mend table separators, parse with
MDEx, rewrite the tree — code-language aliases, mermaid fences,
admonitions, links written to a neighbouring .md — render through the
sanitiser, then fix up image paths on the HTML.
The rewrites work on MDEx.Document nodes rather than on rendered HTML: a
fence inside a list item is still a MDEx.CodeBlock, a :::tip inside a
quote still a MDEx.BlockDirective, and a link's url is a field rather
than an attribute a regex has to find. The one pass left on the HTML is the
image one, because a <figure> written in raw HTML carries an <img> no
node stands for, and every image gets the same loading/decoding
attributes either way.
Raw HTML
Rendered with unsafe: true and always sanitised. The allowlist below is
what makes that safe: a fixed set of tags a guide legitimately contains —
<figure>, <video>, <details>, a <div class> for a gallery, an
inline <svg> for an icon — with the attributes each of them needs and
nothing that runs. Content is the operator's own files, so the sanitiser
is a guard against a pasted snippet, not against an adversary; it stays on
because a rule that is on for everyone is a rule nobody has to remember.
Admonitions
Two spellings, one markup. :::tip[Title] … ::: is what Docusaurus
authors write; > [!TIP] is GitHub's. MDEx parses the first as a
MDEx.BlockDirective whose info is tip[Title] and the second as a
MDEx.Alert. Both become a directive rendered as
<div class="admonition admonition-tip"> with a
<p class="admonition-title"> as its first child, which is the one thing
a stylesheet has to know. A raw <div class="note"> an author wrote is
HTML, not a directive, and stays as written.
Summary
Functions
The first picture in a markdown body, at the URL the rendered page serves it
from (image_src/2, before any :image_url), or nil.
The URL an image written in a collection's markdown is served from.
Inline markup removed, entities left as they are.
Render markdown source. The frontmatter, if any, is dropped.
Render a file, or nil when it cannot be read or parsed.
The h2 and h3 sections of rendered HTML: each heading as toc/1 gives
it, plus :lede, the first sentence of the paragraph that opens the
section, or nil when the section opens with a list, a table or code.
Drop a leading <h1>: the page renders the title itself, so leaving it in
prints it twice. Attribute-tolerant, because headings carry ids now.
The headings of rendered HTML, for a table of contents: h2 and h3
with their ids and plain text.
Types
@type opt() :: {:collection, String.t()} | {:assets, :content | :static} | {:dir, String.t()} | {:base_path, String.t() | nil} | {:slug, String.t() | nil} | {:index, boolean()} | {:id, String.t()} | {:image_url, (String.t() -> String.t()) | nil}
:collection— the registered name, for/content/<collection>/asset paths:assets—:content(the default) rewrites image paths into the collection's asset route;:staticleaves root-absolute paths alone, for a site whose images are served frompriv/static:dir— the file's folder relative to the collection root, for relative image paths:base_path— the route prefix a.mdlink rewrites to;nilleaves such links untouched:slug— the document's slug, which relative links resolve against:index— the document is a folder'sindex.md, whose slug is its folder, so its links resolve against the slug rather than its parent.render_file/2sets it from the file name.:id— a stable prefix for element ids (mermaid diagrams need one):image_url— given each image's URL once it is resolved (never anhttpordata:one), answers the URL to serve instead: a smaller copy the host has built, say.Gamend.Contentpasses the collection's registered:image_urlhere
Functions
The first picture in a markdown body, at the URL the rendered page serves it
from (image_src/2, before any :image_url), or nil.
A markdown image or an <img> written in raw HTML, whichever comes first;
one inside a code block is code, not a picture. For a card that has only
the post itself to take a picture from. Give it the body, after the
frontmatter.
The URL an image written in a collection's markdown is served from.
Points it at /content/<collection>/…, the host content asset route, in
the three ways authors write it:
- Relative:
gamend/auth.png→/content/blog/gamend/auth.png - Absolute:
/gamend/auth.png→/content/blog/gamend/auth.png - Type-prefixed:
/blog/gamend/auth.png→/content/blog/gamend/auth.png
With assets: :static, an absolute path is a URL the host serves and is
left alone; only relative ones are resolved, against the file's folder
(:dir). External URLs (http…, data:) and paths already under
/content/ come back as they are.
Inline markup removed, entities left as they are.
Render markdown source. The frontmatter, if any, is dropped.
Render a file, or nil when it cannot be read or parsed.
@spec sections(String.t() | nil) :: [ %{id: String.t(), text: String.t(), level: 2 | 3, lede: String.t() | nil} ]
The h2 and h3 sections of rendered HTML: each heading as toc/1 gives
it, plus :lede, the first sentence of the paragraph that opens the
section, or nil when the section opens with a list, a table or code.
Drop a leading <h1>: the page renders the title itself, so leaving it in
prints it twice. Attribute-tolerant, because headings carry ids now.
The headings of rendered HTML, for a table of contents: h2 and h3
with their ids and plain text.
Read from the HTML because that is what the cache holds; the renderer puts the id first on the heading tag, so the pattern is stable.