<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>colophon docs</title>
  <id>https://docs.colophon.blog</id>
  <updated>2001-12-30T00:00:00Z</updated>
  <link href="https://docs.colophon.blog" rel="alternate"></link>
  <link href="https://docs.colophon.blog/atom.xml" rel="self" type="application/atom+xml"></link>
  <entry>
    <title>colophon documentation</title>
    <id>https://docs.colophon.blog/start/</id>
    <link href="https://docs.colophon.blog/start/" rel="alternate"></link>
    <updated>2001-12-30T00:00:00Z</updated>
    <published>2001-12-30T00:00:00Z</published>
    <summary type="text">End-user guides for authoring, theming and publishing a colophon site.</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/README.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;End-user guides for authoring, theming and publishing a colophon site.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/start/content/&#34;&gt;Authoring content&lt;/a&gt;&lt;/strong&gt; — frontmatter, supported Markdown, the rich blocks&#xA;(maths, diagrams, callouts, code) and how they render, wikilinks, embeds and images.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/start/themes/&#34;&gt;Themes&lt;/a&gt;&lt;/strong&gt; — selecting a theme, per-environment theme overrides, writing or&#xA;overriding a theme, the template variables, and progressive enhancement.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/guides/seo/&#34;&gt;SEO &amp;amp; social&lt;/a&gt;&lt;/strong&gt; — the &lt;code&gt;seo:&lt;/code&gt; frontmatter block and the canonical / Open Graph /&#xA;Twitter / JSON-LD metadata colophon emits.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/guides/image-generation/&#34;&gt;Image &amp;amp; audio generation&lt;/a&gt;&lt;/strong&gt; — &lt;code&gt;gen:&lt;/code&gt; image prompts, AI or recorded&#xA;post audio (podcast feeds), providers, the &lt;code&gt;--generate-ai&lt;/code&gt; step, the kill switch, and pruning.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/guides/personas/&#34;&gt;Authors &amp;amp; personas&lt;/a&gt;&lt;/strong&gt; — the &lt;strong&gt;author&lt;/strong&gt; (the shown byline + h-card) vs the&#xA;&lt;strong&gt;persona&lt;/strong&gt; (a hidden, shareable writing voice), and the &lt;code&gt;persona context&lt;/code&gt; command that emits&#xA;write-as context (style guide + relevant exemplars) for an AI author.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/start/publishing/&#34;&gt;Publishing&lt;/a&gt;&lt;/strong&gt; — environments vs publishers, credentials, and routing&#xA;assets to an object store.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/guides/syndication/&#34;&gt;Syndication (POSSE)&lt;/a&gt;&lt;/strong&gt; — cross-post to Mastodon/Bluesky/anywhere with&#xA;&lt;code&gt;colophon syndicate&lt;/code&gt;: the ledger, gating, and the &lt;code&gt;command&lt;/code&gt;/&lt;code&gt;mastodon&lt;/code&gt;/&lt;code&gt;bluesky&lt;/code&gt;/&lt;code&gt;bridgy&lt;/code&gt; drivers.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/guides/skills/&#34;&gt;Agent skills&lt;/a&gt;&lt;/strong&gt; &lt;em&gt;(design)&lt;/em&gt; — the planned authoring skills (seo, draft, tag,&#xA;social…) and the prompt packs that drive them.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/guides/howto/&#34;&gt;How-to guides&lt;/a&gt;&lt;/strong&gt; — short zero-to-published recipes: federate via Bridgy Fed, show&#xA;webmentions, syndicate to Mastodon/Bluesky. Each notes whether it&#39;s shipped or planned.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/reference/changelog/&#34;&gt;Changelog&lt;/a&gt;&lt;/strong&gt; — user-facing changes per release, each pointing at the guide that&#xA;documents it in full.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Design notes and the roadmap live in &lt;a href=&#34;PLAN.md&#34;&gt;PLAN.md&lt;/a&gt; and &lt;a href=&#34;/internals/&#34;&gt;design/&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/README.md&#34;&gt;&lt;code&gt;docs/README.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Authoring content</title>
    <id>https://docs.colophon.blog/start/content/</id>
    <link href="https://docs.colophon.blog/start/content/" rel="alternate"></link>
    <updated>2001-12-29T00:00:00Z</updated>
    <published>2001-12-29T00:00:00Z</published>
    <summary type="text">A colophon post is a Markdown file with a YAML frontmatter block. Files come from one or more sources (a content/ folder, an Obsidian vault, …); the source&#39;s folder structure…</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/content.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;A colophon post is a Markdown file with a YAML &lt;em&gt;frontmatter&lt;/em&gt; block. Files come from one or&#xA;more &lt;strong&gt;sources&lt;/strong&gt; (a &lt;code&gt;content/&lt;/code&gt; folder, an Obsidian vault, …); the source&#39;s folder structure&#xA;becomes the site&#39;s URL structure.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-markdown&#34;&gt;---&#xA;title: My first post&#xA;date: 2026-06-15&#xA;description: A one-line summary for feeds and link previews.&#xA;tags: [notes, colophon]&#xA;draft: false&#xA;---&#xA;&#xA;Write the body in **Markdown**.&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;frontmatter-fields&#34;&gt;Frontmatter fields&lt;/h2&gt;&#xA;&lt;p&gt;All fields are optional unless noted.&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Field&lt;/th&gt;&#xA;&lt;th&gt;Meaning&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Post title. If omitted, falls back to a leading &lt;code&gt;# heading&lt;/code&gt; or the file name.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Publish date (&lt;code&gt;YYYY-MM-DD&lt;/code&gt;). If omitted (Obsidian), the file&#39;s modified time is used.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;type&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Page type (&lt;code&gt;post&lt;/code&gt;, &lt;code&gt;page&lt;/code&gt;, or a custom value). Overrides the date-based default — see &lt;a href=&#34;#page-types&#34;&gt;Page types&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;slug&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Overrides the final URL segment (otherwise derived from the file path).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;aliases&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Old/alternate URL paths that redirect here (e.g. after a rename) — see &lt;a href=&#34;#redirects-aliases&#34;&gt;Redirects&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Summary for feeds, &lt;code&gt;&amp;lt;meta name=&amp;quot;description&amp;quot;&amp;gt;&lt;/code&gt; and &lt;code&gt;og:description&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;categories&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Lists for organisation/feeds.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;author&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The byline — an &lt;code&gt;authors/&amp;lt;id&amp;gt;.yaml&lt;/code&gt; id. Defaults to the first author, else &amp;quot;Anonymous&amp;quot;. See &lt;a href=&#34;/guides/personas/&#34;&gt;Authors &amp;amp; personas&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;persona&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The hidden writing &lt;em&gt;voice&lt;/em&gt; (a &lt;code&gt;personas/&amp;lt;id&amp;gt;.yaml&lt;/code&gt; id) used by the agent; never shown.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;hero&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Banner image shown at the top of the post. A path, an Obsidian &lt;code&gt;&amp;quot;[[image.png]]&amp;quot;&lt;/code&gt;, or a &lt;code&gt;&amp;quot;gen:&amp;lt;prompt&amp;gt;&amp;quot;&lt;/code&gt; to generate one — see &lt;a href=&#34;/guides/image-generation/&#34;&gt;Image generation&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Preview/social-card image (&lt;code&gt;og:image&lt;/code&gt; + index thumbnail). Accepts a path or a &lt;code&gt;&amp;quot;gen:&amp;lt;prompt&amp;gt;&amp;quot;&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;hero_alt&lt;/code&gt;, &lt;code&gt;image_alt&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Alt text for those images. Empty = decorative (&lt;code&gt;alt=&amp;quot;&amp;quot;&lt;/code&gt;); set it when the image carries meaning.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;hero_fit&lt;/code&gt;, &lt;code&gt;image_fit&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;How the image fills its box — CSS &lt;code&gt;object-fit&lt;/code&gt;: &lt;code&gt;cover&lt;/code&gt; (crop, default), &lt;code&gt;contain&lt;/code&gt; (letterbox), &lt;code&gt;fill&lt;/code&gt;, &lt;code&gt;scale-down&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;hero_position&lt;/code&gt;, &lt;code&gt;image_position&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Which part shows when cropping — CSS &lt;code&gt;object-position&lt;/code&gt;, e.g. &lt;code&gt;top&lt;/code&gt; or &lt;code&gt;50% 20%&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;audio&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Spoken (TTS) reading of the post. Omit to follow the site default (on when a speech provider is configured); set &lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt; to force it. Needs &lt;code&gt;generation.speech&lt;/code&gt;. See &lt;a href=&#34;/guides/image-generation/&#34;&gt;Image &amp;amp; audio generation&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;audio_file&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Attach a pre-recorded audio file (a path or &lt;code&gt;[[embed]]&lt;/code&gt;) instead of generating one — no AI. Wins over &lt;code&gt;audio&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;audio_voice&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Override the reading voice id (generated audio only); else the author&#39;s/persona&#39;s &lt;code&gt;voice&lt;/code&gt;, else the site default.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;attachments&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Downloadable files shipped with the post (scripts, archives, datasets, PDFs…). A list of paths or &lt;code&gt;{path, label, feed}&lt;/code&gt; mappings — see &lt;a href=&#34;#attachments-downloads&#34;&gt;Attachments (downloads)&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;syndication&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;URLs where this post also lives (e.g. a Mastodon/Bluesky copy you cross-posted). A list of absolute URLs, rendered as mf2 &lt;code&gt;u-syndication&lt;/code&gt; &amp;quot;Also posted on…&amp;quot; links. The &lt;code&gt;colophon syndicate&lt;/code&gt; ledger also feeds these automatically.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;syndicate&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;POSSE control for &lt;code&gt;colophon syndicate&lt;/code&gt;: &lt;code&gt;false&lt;/code&gt; opts this post out; a list (&lt;code&gt;[mastodon]&lt;/code&gt;) picks a subset of the environment&#39;s targets; absent = all of them.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;syndicate_text&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Optional custom blurb for the syndicated copy (else the driver derives one from the title/summary).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;lang&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Per-post language (BCP-47, e.g. &lt;code&gt;fr&lt;/code&gt;), overriding the site &lt;code&gt;lang&lt;/code&gt;. Emitted as &lt;code&gt;&amp;lt;html lang&amp;gt;&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;glossary&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;false&lt;/code&gt; turns off automatic &lt;a href=&#34;#glossary&#34;&gt;glossary&lt;/a&gt; decoration for this post; an explicit &lt;code&gt;&amp;lt;abbr&amp;gt;&lt;/code&gt; still works.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;true&lt;/code&gt; keeps the post out of production builds (shown in preview/serve).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;publish&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Obsidian whitelist flag, honoured when a source sets &lt;code&gt;publish_required: true&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;publish_after&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Embargo: not published until this time (ISO 8601, e.g. &lt;code&gt;2026-07-01T09:00:00Z&lt;/code&gt;).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;predecessor&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The slug (or bare filename) of the post that &lt;em&gt;immediately precedes&lt;/em&gt; this one in a series — see &lt;a href=&#34;#post-series&#34;&gt;Post series&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;series&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Optional series &lt;strong&gt;title&lt;/strong&gt;. Latest-wins: the newest post in the chain that sets it names the series.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Slugs are normalised: each path segment is lower-cased and non-alphanumerics collapse to&#xA;single hyphens, so &lt;code&gt;Archive/My Post.md&lt;/code&gt; → &lt;code&gt;archive/my-post&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;page-types&#34;&gt;Page types&lt;/h2&gt;&#xA;&lt;p&gt;Every entry has a &lt;strong&gt;type&lt;/strong&gt; that decides how it&#39;s placed and which theme template renders it:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;A &lt;strong&gt;&lt;code&gt;post&lt;/code&gt;&lt;/strong&gt; is chronological — listed on the index, included in feeds, and shown on its tag&#xA;pages.&lt;/li&gt;&#xA;&lt;li&gt;A &lt;strong&gt;&lt;code&gt;page&lt;/code&gt;&lt;/strong&gt; is standing chrome — surfaced in the theme&#39;s nav menu instead, and kept out of&#xA;the list and feeds (e.g. About, Now).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;By default the type is inferred: an entry &lt;strong&gt;with a date&lt;/strong&gt; is a &lt;code&gt;post&lt;/code&gt;, one &lt;strong&gt;without&lt;/strong&gt; is a&#xA;&lt;code&gt;page&lt;/code&gt;. Set &lt;code&gt;type:&lt;/code&gt; in frontmatter to override that, or to use a custom type:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;---&#xA;title: Side Projects&#xA;type: project        # a custom type; styled by a theme&amp;#39;s project.html if it has one&#xA;---&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;type: page&lt;/code&gt; makes a &lt;em&gt;dated&lt;/em&gt; entry standing (nav, not feeds); &lt;code&gt;type: post&lt;/code&gt; makes a &lt;em&gt;dateless&lt;/em&gt;&#xA;entry a listed post.&lt;/li&gt;&#xA;&lt;li&gt;A custom type (e.g. &lt;code&gt;project&lt;/code&gt;) is listed like a post, but a theme can give it its own look —&#xA;see &lt;a href=&#34;/start/themes/#page-types&#34;&gt;Themes → Page types&lt;/a&gt;. This &lt;code&gt;type&lt;/code&gt; is unrelated to &lt;code&gt;seo.type&lt;/code&gt;&#xA;(the schema.org type).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;post-series&#34;&gt;Post series&lt;/h2&gt;&#xA;&lt;p&gt;A post can declare that it follows an earlier post with a single &lt;strong&gt;backward&lt;/strong&gt; link, and&#xA;colophon reconstructs the whole ordered series from those links — adding &amp;quot;Part N of M&amp;quot; /&#xA;previous / next navigation to &lt;strong&gt;every&lt;/strong&gt; member.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;---&#xA;title: &amp;#34;Building a Widget, Part Two&amp;#34;&#xA;date: 2026-06-11&#xA;predecessor: building-a-widget-part-one   # the post just before this one&#xA;---&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;predecessor:&lt;/code&gt;&lt;/strong&gt; pins the slug (or bare filename, resolved like a &lt;code&gt;[[wikilink]]&lt;/code&gt;) of the&#xA;immediately preceding post. It&#39;s a single linear chain — a post is in at most one series.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;series:&lt;/code&gt;&lt;/strong&gt; is the optional title. It&#39;s &lt;strong&gt;latest-wins&lt;/strong&gt;: the name is taken from the &lt;em&gt;newest&lt;/em&gt;&#xA;post in the chain that sets it; if no member sets it, the series is untitled.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Because colophon rebuilds the whole site every time, a backward pointer is enough — &lt;strong&gt;you never&#xA;edit old posts&lt;/strong&gt;. Publishing Part Two (which points back at Part One) is what gives Part One its&#xA;forward link to Part Two; the engine walks the chain and regenerates both. The series renders&#xA;oldest→newest, and the current post is highlighted in the list.&lt;/p&gt;&#xA;&lt;p&gt;Themes get per-post variables (&lt;code&gt;series_name&lt;/code&gt;, &lt;code&gt;series_total&lt;/code&gt;, &lt;code&gt;series_index&lt;/code&gt;,&#xA;&lt;code&gt;series_parts&lt;/code&gt;, &lt;code&gt;series_prev&lt;/code&gt;, &lt;code&gt;series_next&lt;/code&gt;) — set only for posts in a series of two or more —&#xA;and a &lt;code&gt;series&lt;/code&gt; flag on each post-list item. The bundled &lt;strong&gt;press&lt;/strong&gt; theme shows the series in the&#xA;left rail and marks series entries on the index; other themes can adopt the variables.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;colophon doctor&lt;/code&gt; warns (without failing) when a &lt;code&gt;predecessor:&lt;/code&gt; doesn&#39;t resolve to a known post,&#xA;when the links form a cycle, or when two posts name the same predecessor (a branch).&lt;/p&gt;&#xA;&lt;h2 id=&#34;markdown-support&#34;&gt;Markdown support&lt;/h2&gt;&#xA;&lt;p&gt;colophon parses &lt;a href=&#34;https://github.github.com/gfm/&#34;&gt;GitHub Flavored Markdown&lt;/a&gt; — tables,&#xA;strikethrough, task lists, autolinks — plus automatic heading IDs (so &lt;code&gt;## My Heading&lt;/code&gt; is&#xA;linkable as &lt;code&gt;#my-heading&lt;/code&gt;).&lt;/p&gt;&#xA;&lt;h3 id=&#34;the-raw-block-contract-progressive-enhancement&#34;&gt;The raw-block contract (progressive enhancement)&lt;/h3&gt;&#xA;&lt;p&gt;Rich blocks are rendered as &lt;strong&gt;semantic HTML that carries its raw source as text&lt;/strong&gt;, tagged by&#xA;type. colophon itself loads &lt;strong&gt;no JavaScript&lt;/strong&gt; — it only guarantees the markup. A theme then&#xA;chooses how to present each block: a no-JS/minimal theme shows readable raw text; the default&#xA;theme upgrades it with &lt;a href=&#34;https://highlightjs.org/&#34;&gt;highlight.js&lt;/a&gt;,&#xA;&lt;a href=&#34;https://katex.org/&#34;&gt;KaTeX&lt;/a&gt; and &lt;a href=&#34;https://mermaid.js.org/&#34;&gt;Mermaid&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;You write&lt;/th&gt;&#xA;&lt;th&gt;colophon emits&lt;/th&gt;&#xA;&lt;th&gt;Enhanced by&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;```go … ```&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;code class=&amp;quot;language-go&amp;quot;&amp;gt;…&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;a syntax highlighter&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;```mermaid … ```&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;pre class=&amp;quot;mermaid&amp;quot;&amp;gt;…&amp;lt;/pre&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Mermaid&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;$E=mc^2$&lt;/code&gt; (inline)&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;span class=&amp;quot;math math-inline&amp;quot;&amp;gt;E=mc^2&amp;lt;/span&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;KaTeX&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;$$ … $$&lt;/code&gt; (display)&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;div class=&amp;quot;math math-display&amp;quot;&amp;gt;…&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;KaTeX&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;gt; [!note] Title&lt;/code&gt; …&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;div class=&amp;quot;callout callout-note&amp;quot;&amp;gt;…&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;CSS only (no JS)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;gt; [!quote] Attribution&lt;/code&gt; …&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;figure class=&amp;quot;pullquote&amp;quot;&amp;gt;&amp;lt;blockquote&amp;gt;…&amp;lt;/blockquote&amp;gt;&amp;lt;figcaption&amp;gt;…&amp;lt;/figcaption&amp;gt;&amp;lt;/figure&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;CSS only (no JS)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Notes:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Maths&lt;/strong&gt; is matched on a single line. A currency heuristic leaves prose like &lt;code&gt;$5 and $10&lt;/code&gt;&#xA;alone. The LaTeX source is preserved verbatim, so it is readable even without KaTeX.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Callouts&lt;/strong&gt; use Obsidian syntax — a blockquote whose first line is &lt;code&gt;[!type] Optional Title&lt;/code&gt;. The body is normal Markdown. Types map to colours via CSS classes&#xA;(&lt;code&gt;note&lt;/code&gt;/&lt;code&gt;info&lt;/code&gt;, &lt;code&gt;tip&lt;/code&gt;/&lt;code&gt;success&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;danger&lt;/code&gt;, &lt;code&gt;example&lt;/code&gt;, …).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Pull-quotes&lt;/strong&gt; are the &lt;code&gt;[!quote]&lt;/code&gt; callout type — they render as a semantic &lt;code&gt;&amp;lt;figure&amp;gt;&lt;/code&gt; with&#xA;the text after &lt;code&gt;[!quote]&lt;/code&gt; as the attribution &lt;code&gt;&amp;lt;figcaption&amp;gt;&lt;/code&gt; (omit it for an unattributed&#xA;quote). The &lt;strong&gt;press&lt;/strong&gt; theme styles this as a large display epigraph; other themes can target&#xA;&lt;code&gt;.pullquote&lt;/code&gt;. Plain blockquotes (&lt;code&gt;&amp;gt;&lt;/code&gt; without &lt;code&gt;[!quote]&lt;/code&gt;) are unchanged.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Mermaid&lt;/strong&gt; uses the diagram source as the element&#39;s text, so it degrades to a readable&#xA;description without the library.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Tip — preview every feature in your theme.&lt;/strong&gt; &lt;code&gt;colophon serve --showcase&lt;/code&gt; injects a built-in&#xA;&lt;code&gt;/showcase/&lt;/code&gt; page (embedded in the binary, never written to your content) that renders every one&#xA;of these blocks — callouts, pull-quotes, tables, maths, diagrams, media, attachments, glossary —&#xA;in your active theme, with the source shown alongside. Handy when writing or styling a theme.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 id=&#34;links-wikilinks-and-images&#34;&gt;Links, wikilinks and images&lt;/h3&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Standard Markdown links and images work as usual.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Wikilinks&lt;/strong&gt; resolve across every source at build time — a vault note can link to a post&#xA;in &lt;code&gt;content/&lt;/code&gt; and vice versa: &lt;code&gt;[[note]]&lt;/code&gt;, &lt;code&gt;[[note|alias]]&lt;/code&gt;, &lt;code&gt;[[note#heading]]&lt;/code&gt;. An&#xA;unresolved link degrades to plain text rather than breaking.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Tags&lt;/strong&gt; (&lt;code&gt;tags:&lt;/code&gt; frontmatter) render on each post and on the index, linked to a generated&#xA;page per tag at &lt;code&gt;/tags/&amp;lt;tag&amp;gt;/&lt;/code&gt; that lists every post sharing it — so tags become sideways&#xA;navigation across entries.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Embeds&lt;/strong&gt; (&lt;code&gt;![[image.png]]&lt;/code&gt;, &lt;code&gt;![[image.png|alt]]&lt;/code&gt;) resolve attachments vault-wide and are&#xA;copied next to the page.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;![](relative.png)&lt;/code&gt; images are copied beside the page so the relative &lt;code&gt;src&lt;/code&gt; resolves;&#xA;external (&lt;code&gt;https://…&lt;/code&gt;) images are left untouched.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Generated images&lt;/strong&gt; — &lt;code&gt;![alt](&amp;lt;gen:a prompt here&amp;gt;)&lt;/code&gt; produces the image with an AI provider&#xA;and caches it. Wrap the prompt in &lt;code&gt;&amp;lt;…&amp;gt;&lt;/code&gt; when it contains spaces. See&#xA;&lt;a href=&#34;/guides/image-generation/&#34;&gt;Image generation&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Video &amp;amp; audio embeds&lt;/strong&gt; — an image embed whose target is a media file renders as a player,&#xA;not a broken image. &lt;code&gt;![A short demo](demo.mp4)&lt;/code&gt; becomes a &lt;code&gt;&amp;lt;video controls&amp;gt;&lt;/code&gt;; &lt;code&gt;![](clip.mp3)&lt;/code&gt;&#xA;becomes an &lt;code&gt;&amp;lt;audio controls&amp;gt;&lt;/code&gt;. The file is copied/routed exactly like an image (so object&#xA;storage works the same), and the embed&#39;s alt text becomes the player&#39;s &lt;code&gt;aria-label&lt;/code&gt;. See&#xA;&lt;a href=&#34;#embedding-video-and-audio&#34;&gt;Embedding video and audio&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;images-and-object-storage&#34;&gt;Images and object storage&lt;/h3&gt;&#xA;&lt;p&gt;By default images are co-located with the page and served relatively. A site can &lt;strong&gt;route&lt;/strong&gt;&#xA;images (or any path glob) to an object store (e.g. Cloudflare R2) instead — see the publisher&#xA;configuration. When routing is active the build rewrites those image URLs to the store&#39;s&#xA;public base, so the page references &lt;code&gt;https://assets.example.com/…&lt;/code&gt; while the bytes are&#xA;uploaded to the store rather than your HTML host.&lt;/p&gt;&#xA;&lt;h3 id=&#34;embedding-video-and-audio&#34;&gt;Embedding video and audio&lt;/h3&gt;&#xA;&lt;p&gt;There is &lt;strong&gt;no new syntax&lt;/strong&gt; — use the markdown image embed you already know, pointing at a media&#xA;file. colophon recognises the extension and renders a player instead of an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-markdown&#34;&gt;![A short demo](demo.mp4)     &amp;lt;!-- → &amp;lt;video controls&amp;gt;, with the alt as its aria-label --&amp;gt;&#xA;![[demo.mp4]]                  &amp;lt;!-- Obsidian embeds work too --&amp;gt;&#xA;![](interview.mp3)            &amp;lt;!-- → &amp;lt;audio controls&amp;gt; --&amp;gt;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Video&lt;/strong&gt;: &lt;code&gt;.mp4&lt;/code&gt;, &lt;code&gt;.webm&lt;/code&gt;, &lt;code&gt;.mov&lt;/code&gt;, &lt;code&gt;.m4v&lt;/code&gt;, &lt;code&gt;.ogv&lt;/code&gt;. &lt;strong&gt;Audio&lt;/strong&gt;: &lt;code&gt;.mp3&lt;/code&gt;, &lt;code&gt;.m4a&lt;/code&gt;, &lt;code&gt;.aac&lt;/code&gt;,&#xA;&lt;code&gt;.oga&lt;/code&gt;, &lt;code&gt;.ogg&lt;/code&gt;, &lt;code&gt;.wav&lt;/code&gt;, &lt;code&gt;.flac&lt;/code&gt;, &lt;code&gt;.opus&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;The file is discovered, copied beside the page, and &lt;strong&gt;routed to object storage&lt;/strong&gt; exactly like&#xA;an image — self-hosting &amp;quot;just works&amp;quot;, including via R2.&lt;/li&gt;&#xA;&lt;li&gt;A direct &lt;strong&gt;external&lt;/strong&gt; file URL plays too (e.g. &lt;code&gt;![](https://cdn.example.com/clip.mp4)&lt;/code&gt;); it is&#xA;left untouched and not copied.&lt;/li&gt;&#xA;&lt;li&gt;This is independent of &lt;code&gt;audio_file:&lt;/code&gt;/&lt;code&gt;audio:&lt;/code&gt;, which attach a single &lt;em&gt;podcast-style reading&lt;/em&gt; of&#xA;the whole post (with the themed player and feed enclosure). Inline embeds are just media in the&#xA;body.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Big files belong in object storage or a CDN, not your Git host. Route a &lt;code&gt;**/*.mp4&lt;/code&gt; glob to R2&#xA;(see the publisher config) and the embed URL is rewritten automatically.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 id=&#34;attachments-downloads&#34;&gt;Attachments (downloads)&lt;/h3&gt;&#xA;&lt;p&gt;List downloadable files in frontmatter and colophon copies/routes them like images and renders a&#xA;&lt;strong&gt;Downloads&lt;/strong&gt; block on the post. Each entry is either a bare path or a &lt;code&gt;{path, label, feed}&lt;/code&gt;&#xA;mapping:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;---&#xA;title: Release Notes&#xA;attachments:&#xA;  - changelog.txt                                   # label defaults to the file name&#xA;  - { path: build.sh, label: &amp;#34;Build script&amp;#34;, description: &amp;#34;Sets up the toolchain&amp;#34; }&#xA;  - { path: dataset.zip, label: &amp;#34;Dataset&amp;#34;, description: &amp;#34;Raw measurements&amp;#34;, feed: true }&#xA;---&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Paths resolve &lt;strong&gt;relative to the post&lt;/strong&gt; (same rules as an image embed); &lt;code&gt;[[embed]]&lt;/code&gt; works too.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;label&lt;/code&gt; sets the link text (defaults to the file name); &lt;code&gt;description&lt;/code&gt; adds a one-line note&#xA;beneath it. The file&#39;s &lt;strong&gt;size&lt;/strong&gt; and a short &lt;strong&gt;filetype&lt;/strong&gt; badge (ZIP, PDF, MP4…) are shown&#xA;automatically.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;feed: true&lt;/code&gt; also lists the file as a feed enclosure/attachment (see below). Without it, the&#xA;file is downloadable on the page but stays out of the feeds.&lt;/li&gt;&#xA;&lt;li&gt;Posts with attachments get a small paperclip marker in the listing (alongside the audio&#xA;speaker), in the press and contrib themes.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;Attachments in feeds.&lt;/strong&gt; A post&#39;s audio reading and any &lt;code&gt;feed: true&lt;/code&gt; attachment are emitted to&#xA;the syndication feeds so podcast/feed clients can fetch them:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;JSON Feed&lt;/strong&gt; — every item appears in &lt;code&gt;attachments&lt;/code&gt; (multiple allowed).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Atom&lt;/strong&gt; — each is a &lt;code&gt;&amp;lt;link rel=&amp;quot;enclosure&amp;quot;&amp;gt;&lt;/code&gt; (multiple allowed).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;RSS&lt;/strong&gt; — carries a single &lt;code&gt;&amp;lt;enclosure&amp;gt;&lt;/code&gt; per the spec: the audio reading wins, else the first&#xA;&lt;code&gt;feed: true&lt;/code&gt; attachment.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;slide-decks&#34;&gt;Slide decks&lt;/h3&gt;&#xA;&lt;p&gt;A post can be projected into a &lt;strong&gt;themed slide deck&lt;/strong&gt; — published at &lt;code&gt;…/&amp;lt;slug&amp;gt;/slides/&lt;/code&gt;, linked from&#xA;the post&#39;s Downloads box, and flagged with a slides marker in the listing (alongside the audio and&#xA;attachment markers). It&#39;s &lt;strong&gt;derived&lt;/strong&gt; from the post: headings become slides (or bullets), prose&#xA;becomes speaker notes, and other blocks render on the slide. With JavaScript it&#39;s a keyboard/swipe&#xA;presentation (&lt;kbd&gt;P&lt;/kbd&gt; = presenter notes, &lt;kbd&gt;F&lt;/kbd&gt; = fullscreen); with JS off the same file&#xA;reads as a long-form document.&lt;/p&gt;&#xA;&lt;p&gt;Set the site default in &lt;code&gt;colophon.yaml&lt;/code&gt; (&lt;code&gt;slides.enabled&lt;/code&gt;), then opt a post in or out:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;---&#xA;title: A Short Talk&#xA;slides: true                 # or the block form below&#xA;# slides:&#xA;#   enabled: true&#xA;#   split: [h2]              # slide boundaries (a list). default: every heading.&#xA;---&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;split&lt;/code&gt;&lt;/strong&gt; lists the boundaries: &lt;code&gt;h1&lt;/code&gt;–&lt;code&gt;h6&lt;/code&gt;, &lt;code&gt;hr&lt;/code&gt;, &lt;code&gt;splitslide&lt;/code&gt;, the block kinds &lt;code&gt;image&lt;/code&gt;/&lt;code&gt;table&lt;/code&gt;/&#xA;&lt;code&gt;code&lt;/code&gt;/&lt;code&gt;math&lt;/code&gt;/&lt;code&gt;diagram&lt;/code&gt;/&lt;code&gt;audio&lt;/code&gt;/&lt;code&gt;video&lt;/code&gt;, and &lt;code&gt;text:&amp;lt;match&amp;gt;&lt;/code&gt; (split before a block whose text begins&#xA;with the match). The default splits on every heading; narrow it (e.g. &lt;code&gt;[h2]&lt;/code&gt;) to fold deeper&#xA;headings into bullets.&lt;/li&gt;&#xA;&lt;li&gt;The post&#39;s &lt;code&gt;slides:&lt;/code&gt; &lt;strong&gt;overwrites&lt;/strong&gt; the site default by key (it does not deep-merge): a key you set&#xA;replaces that value, keys you omit inherit.&lt;/li&gt;&#xA;&lt;li&gt;A site-wide &lt;code&gt;slides.enabled: true&lt;/code&gt; applies to &lt;strong&gt;listed content&lt;/strong&gt; (posts and custom types); standing&#xA;&lt;strong&gt;pages&lt;/strong&gt; (About, etc.) don&#39;t get a deck from the default — they opt in with their own &lt;code&gt;slides: true&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;An &lt;strong&gt;environment&lt;/strong&gt; can override the site default (&lt;code&gt;slides: { enabled, split }&lt;/code&gt; under the environment),&#xA;e.g. decks &lt;strong&gt;on in preview, off in production&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Three inline markers mirror the &lt;code&gt;&amp;lt;tts&amp;gt;&lt;/code&gt; family: &lt;code&gt;&amp;lt;splitslide&amp;gt;&lt;/code&gt; forces a break, &lt;code&gt;&amp;lt;slide&amp;gt;…&amp;lt;/slide&amp;gt;&lt;/code&gt;&#xA;makes one verbatim slide, and &lt;code&gt;&amp;lt;noslide&amp;gt;…&amp;lt;/noslide&amp;gt;&lt;/code&gt; stays in the post but is kept out of the deck.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;multiple-languages-translations&#34;&gt;Multiple languages (translations)&lt;/h3&gt;&#xA;&lt;p&gt;Publish the same post in several languages. Enable the languages on the site, then add a translation&#xA;with a &lt;code&gt;.&amp;lt;lang&amp;gt;.md&lt;/code&gt; filename:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# colophon.yaml&#xA;sites:&#xA;  - lang: en                 # the default language (stays at the normal URL)&#xA;    languages: [en, es, fr]  # the languages you publish in&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;content/posts/my-post.md       → English   →  /posts/my-post/&#xA;content/posts/my-post.es.md    → Spanish   →  /es/posts/my-post/&#xA;content/posts/my-post.fr.md    → French    →  /fr/posts/my-post/&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Translations are linked by their &lt;strong&gt;base slug&lt;/strong&gt; (&lt;code&gt;my-post&lt;/code&gt;); each can set its own &lt;code&gt;title&lt;/code&gt;,&#xA;&lt;code&gt;description&lt;/code&gt;, hero, even &lt;code&gt;slug&lt;/code&gt;. The default language stays at the normal path; others are&#xA;published under a &lt;strong&gt;&lt;code&gt;/&amp;lt;lang&amp;gt;/&lt;/code&gt;&lt;/strong&gt; prefix.&lt;/li&gt;&#xA;&lt;li&gt;Every translation emits &lt;strong&gt;&lt;code&gt;hreflang&lt;/code&gt; alternates&lt;/strong&gt; (plus &lt;code&gt;x-default&lt;/code&gt;) so search engines serve the&#xA;right language, and the &lt;strong&gt;press&lt;/strong&gt; theme shows a &lt;strong&gt;language selector&lt;/strong&gt; in the post header.&lt;/li&gt;&#xA;&lt;li&gt;A small, dismissible banner offers a reader their preferred language (from the browser) when the&#xA;post is available in it — it never force-redirects.&lt;/li&gt;&#xA;&lt;li&gt;Each translation is a normal post, so it gets its own spoken reading, feeds, glossary and deck.&lt;/li&gt;&#xA;&lt;li&gt;A &lt;code&gt;.&amp;lt;lang&amp;gt;&lt;/code&gt; is only treated as a language when &lt;code&gt;&amp;lt;lang&amp;gt;&lt;/code&gt; is in &lt;code&gt;languages&lt;/code&gt; — a file like&#xA;&lt;code&gt;my.notes.md&lt;/code&gt; is unaffected.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;how-file-references-resolve&#34;&gt;How file references resolve&lt;/h3&gt;&#xA;&lt;p&gt;Two kinds of reference resolve differently:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Per-post references&lt;/strong&gt; — markdown embeds/images, and a post&#39;s &lt;code&gt;hero&lt;/code&gt;/&lt;code&gt;image&lt;/code&gt; — resolve&#xA;against &lt;em&gt;that post&#39;s own source&lt;/em&gt; (its driver&#39;s rules: a vault searches its scan roots and the&#xA;vault, an &lt;code&gt;md-dir&lt;/code&gt; resolves dir-relative). They stay driver-relative so a missing embed is a&#xA;real error, not silently masked by another source.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Project-level references&lt;/strong&gt; — an author &lt;code&gt;avatar&lt;/code&gt; — resolve across &lt;em&gt;every&lt;/em&gt; content source and&#xA;then fall back to the &lt;strong&gt;project root&lt;/strong&gt;. The same &lt;code&gt;avatar: assets/me.png&lt;/code&gt; therefore works whether&#xA;the file lives in a content dir, a vault, or the project&#39;s own &lt;code&gt;assets/&lt;/code&gt; — portable across&#xA;drivers.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;code&gt;colophon doctor&lt;/code&gt; dry-resolves every &lt;em&gt;defined&lt;/em&gt; reference through the same machinery and warns when&#xA;one can&#39;t be sourced (a likely broken link). An &lt;em&gt;undefined&lt;/em&gt; reference is fine — it just means none&#xA;was wanted. &lt;code&gt;data:&lt;/code&gt;/&lt;code&gt;http(s)://&lt;/code&gt; references always pass through untouched.&lt;/p&gt;&#xA;&lt;h2 id=&#34;redirects-aliases&#34;&gt;Redirects (aliases)&lt;/h2&gt;&#xA;&lt;p&gt;When you rename a post (or want short links), list the old paths in &lt;code&gt;aliases:&lt;/code&gt; so the old URLs&#xA;keep working:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;---&#xA;title: A Renamed Post&#xA;slug: renamed&#xA;aliases:&#xA;  - old-name            # /old-name/        → /posts/renamed/&#xA;  - 2020/legacy-post    # nested paths fine&#xA;---&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Each alias is normalised like a slug (lower-cased, non-alphanumerics → hyphens, &lt;code&gt;/&lt;/code&gt; kept). For&#xA;each, the build emits:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;a &lt;strong&gt;meta-refresh stub&lt;/strong&gt; at &lt;code&gt;&amp;lt;alias&amp;gt;/index.html&lt;/code&gt; → the post (works on &lt;em&gt;any&lt;/em&gt; static host),&lt;/li&gt;&#xA;&lt;li&gt;a line in a root &lt;strong&gt;&lt;code&gt;_redirects&lt;/code&gt;&lt;/strong&gt; file, and&lt;/li&gt;&#xA;&lt;li&gt;a root &lt;strong&gt;&lt;code&gt;.nojekyll&lt;/code&gt;&lt;/strong&gt; (so GitHub Pages serves the stubs and the &lt;code&gt;_search/&lt;/code&gt; index).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;How that becomes a redirect depends on the host: &lt;strong&gt;Cloudflare Pages, Netlify and GitLab Pages&lt;/strong&gt;&#xA;read &lt;code&gt;_redirects&lt;/code&gt; and serve a real &lt;strong&gt;301&lt;/strong&gt;; &lt;strong&gt;S3 static-website&lt;/strong&gt; hosting gets a 301 too (colophon&#xA;sets the object redirect header on publish); plain object stores (R2, bare S3/MinIO) and GitHub&#xA;Pages fall back to the client-side meta-refresh stub. Either way the old URL resolves.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Collisions&lt;/strong&gt; are resolved deterministically with a warning: an alias that matches a real page is&#xA;ignored (the page wins), and if two posts claim the same alias the newest wins.&lt;/p&gt;&#xA;&lt;h2 id=&#34;glossary&#34;&gt;Glossary&lt;/h2&gt;&#xA;&lt;p&gt;Drop a &lt;code&gt;glossary.yaml&lt;/code&gt; (term → definition) at the project root and colophon publishes it as&#xA;&lt;code&gt;glossary.json&lt;/code&gt;; a JS-enabled theme then &lt;strong&gt;automatically&lt;/strong&gt; decorates the first occurrence of&#xA;each term in your prose with an accessible pop-over (a &amp;quot;dictionary stanza&amp;quot; with the term and&#xA;its definition). It is never rendered as a page, and it degrades gracefully — the text-only&#xA;theme just shows the words plain.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# glossary.yaml&#xA;API: &amp;#34;Application Programming Interface — the contract one program exposes for another to call.&amp;#34;&#xA;SSG: &amp;#34;Static Site Generator — renders content into static HTML served as-is.&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;You write naturally — no markup needed. When you &lt;em&gt;do&lt;/em&gt; want control over a specific word, three&#xA;controls are available (the syntactic sugar):&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;You want…&lt;/th&gt;&#xA;&lt;th&gt;Write…&lt;/th&gt;&#xA;&lt;th&gt;Effect&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Turn the whole post off&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;glossary: false&lt;/code&gt; in frontmatter&lt;/td&gt;&#xA;&lt;td&gt;No automatic matching. Explicit &lt;code&gt;&amp;lt;abbr&amp;gt;&lt;/code&gt; forces still work.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;Force&lt;/strong&gt; a specific word&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;abbr&amp;gt;API&amp;lt;/abbr&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Always decorated, even mid-post or in an opted-out post — the same &lt;code&gt;&amp;lt;abbr&amp;gt;&lt;/code&gt; auto-match produces. An &lt;code&gt;&amp;lt;abbr title=&amp;quot;…&amp;quot;&amp;gt;&lt;/code&gt; you write yourself is left alone.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;Suppress&lt;/strong&gt; one word&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;noabbr&amp;gt;Go&amp;lt;/noabbr&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;That occurrence is left plain (use it when a term is also a common word). The mirror of &lt;code&gt;&amp;lt;abbr&amp;gt;&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Decoration always skips code, links, headings, your own &lt;code&gt;&amp;lt;abbr title=&amp;quot;…&amp;quot;&amp;gt;&lt;/code&gt; and anything inside&#xA;&lt;code&gt;&amp;lt;noabbr&amp;gt;&lt;/code&gt;, and only the &lt;strong&gt;first&lt;/strong&gt; occurrence of a term is auto-decorated, so a post is never&#xA;peppered with repeats.&lt;/p&gt;&#xA;&lt;h2 id=&#34;sources&#34;&gt;Sources&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;md-dir&lt;/code&gt;&lt;/strong&gt; — a directory of Markdown files (default: &lt;code&gt;content/&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;obsidian&lt;/code&gt;&lt;/strong&gt; — an Obsidian vault, read in place. By convention it publishes only notes&#xA;with &lt;code&gt;publish: true&lt;/code&gt; (unless the source sets &lt;code&gt;publish_required: false&lt;/code&gt;), derives a missing&#xA;title from a leading &lt;code&gt;# heading&lt;/code&gt; or the file name, and a missing date from the file&#39;s&#xA;modified time.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Multiple sources are merged into one site; deletions and renames flow through the build&#39;s&#xA;reconciliation, so the output always matches the inputs.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/content.md&#34;&gt;&lt;code&gt;docs/content.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Publishing</title>
    <id>https://docs.colophon.blog/start/publishing/</id>
    <link href="https://docs.colophon.blog/start/publishing/" rel="alternate"></link>
    <updated>2001-12-28T00:00:00Z</updated>
    <published>2001-12-28T00:00:00Z</published>
    <summary type="text">colophon separates what/where (environments) from how (publishers).</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/publishing.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;colophon separates &lt;strong&gt;what/where&lt;/strong&gt; (environments) from &lt;strong&gt;how&lt;/strong&gt; (publishers).&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;A &lt;strong&gt;publisher&lt;/strong&gt; is a deploy mechanism: copy to a folder, upload to Cloudflare Pages, push&#xA;to an object store. Publishers are pure mechanism and carry no policy.&lt;/li&gt;&#xA;&lt;li&gt;An &lt;strong&gt;environment&lt;/strong&gt; is a named build+deploy profile: which publishers to deploy to, whether&#xA;to include drafts, and optional overrides (title, base_url, &lt;strong&gt;theme&lt;/strong&gt;).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;publishers:&#xA;  - id: local&#xA;    driver: local&#xA;    path: ./dist&#xA;&#xA;environments:&#xA;  - name: production&#xA;    publish: [local]&#xA;    allow_publish: false   # safety latch: requires --allow-publish to deploy&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon publish --env production --allow-publish&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;publishers&#34;&gt;Publishers&lt;/h2&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Driver&lt;/th&gt;&#xA;&lt;th&gt;Purpose&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Copy the built tree to a directory (offline preview / diffing).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;cloudflare-pages&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Deploy the site to Cloudflare Pages (direct upload).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;cloudflare-r2&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Upload files to Cloudflare R2 (S3 + R2 control-plane: public-URL discovery, &lt;code&gt;--create&lt;/code&gt; expose).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;s3&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Upload files to any S3-compatible store (MinIO, B2, Wasabi, Amazon S3) — pure data plane, no SDK.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;tigris&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The &lt;code&gt;s3&lt;/code&gt; driver with Tigris (Fly.io) defaults — needs only a bucket.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;git&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Force-push the built tree to a branch of any git remote (GitHub/GitLab/Codeberg Pages, mirrors, self-hosted).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;github-pages&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The &lt;code&gt;git&lt;/code&gt; driver with GitHub-friendly defaults (branch &lt;code&gt;gh-pages&lt;/code&gt;).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;command&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Run any CLI against the built tree (surge, Netlify, Vercel, rsync, …) — the escape hatch.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;h3 id=&#34;configuration-and-interpolation&#34;&gt;Configuration and interpolation&lt;/h3&gt;&#xA;&lt;p&gt;colophon has &lt;strong&gt;two distinct interpolation layers&lt;/strong&gt; — they look similar (&lt;code&gt;{…}&lt;/code&gt;) but resolve at&#xA;different times, and every driver supports the first:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;1. Config interpolation — &lt;code&gt;{env:VAR}&lt;/code&gt; (all drivers, all config).&lt;/strong&gt; Any string value in the&#xA;config may reference the environment, resolved &lt;em&gt;before the YAML is parsed&lt;/em&gt;, so it works in any&#xA;setting of any publisher (or anywhere else in the config):&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Form&lt;/th&gt;&#xA;&lt;th&gt;Resolves to&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;{env:VAR}&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;the value of &lt;code&gt;VAR&lt;/code&gt;, or empty if unset&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;{env:VAR:-default}&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;the value of &lt;code&gt;VAR&lt;/code&gt;, or &lt;code&gt;default&lt;/code&gt; if unset&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Values come from the process environment and from &lt;code&gt;.env&lt;/code&gt; / &lt;code&gt;.env.defaults&lt;/code&gt; (loaded first; a real&#xA;env var wins over a &lt;code&gt;.env&lt;/code&gt; entry). &lt;code&gt;colophon env&lt;/code&gt; lists every &lt;code&gt;{env:VAR}&lt;/code&gt; a project references,&#xA;set or not. This is how non-secret settings stay flexible while &lt;strong&gt;secrets stay in the&#xA;environment&lt;/strong&gt; — you never write a token into config:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;publishers:&#xA;  - id: r2&#xA;    driver: cloudflare-r2&#xA;    bucket: &amp;#34;{env:R2_BUCKET:-my-assets}&amp;#34;          # default when unset&#xA;    account_id: &amp;#34;{env:CLOUDFLARE_ACCOUNT_ID}&amp;#34;     # required; empty if unset&#xA;    public_url: &amp;#34;{env:R2_PUBLIC_URL:-}&amp;#34;           # optional; empty default&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;2. Command interpolation — &lt;code&gt;{dir}&lt;/code&gt;, &lt;code&gt;{public_url}&lt;/code&gt;, … (the &lt;code&gt;command&lt;/code&gt; driver only).&lt;/strong&gt; The&#xA;&lt;code&gt;command&lt;/code&gt; publisher additionally interpolates its &lt;code&gt;command&lt;/code&gt; argv &lt;em&gt;at publish time&lt;/em&gt; with runtime&#xA;values (the materialised directory, the manifest path, …) and the publisher&#39;s own settings — see&#xA;&lt;a href=&#34;#run-any-cli-the-command-publisher&#34;&gt;Run any CLI&lt;/a&gt;. The two layers compose: &lt;code&gt;{env:VAR}&lt;/code&gt; is&#xA;substituted when the config loads, then &lt;code&gt;{placeholder}&lt;/code&gt; when the command runs, so a single&#xA;&lt;code&gt;command&lt;/code&gt; entry can use both.&lt;/p&gt;&#xA;&lt;p&gt;Per-driver settings and their interpolation are documented in each driver&#39;s README — linked from&#xA;the &lt;a href=&#34;#publishers&#34;&gt;Publishers&lt;/a&gt; table targets below and listed in&#xA;&lt;a href=&#34;#secrets-and-permissions&#34;&gt;Secrets and permissions&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h3 id=&#34;secrets-and-permissions&#34;&gt;Secrets and permissions&lt;/h3&gt;&#xA;&lt;p&gt;Deploy credentials are &lt;strong&gt;never&lt;/strong&gt; read from config — they come from the environment, so they&#xA;never pass through the agent or the YAML:&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Publisher&lt;/th&gt;&#xA;&lt;th&gt;Secret env vars&lt;/th&gt;&#xA;&lt;th&gt;Token permission&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;cloudflare-pages&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Account → Cloudflare Pages → &lt;strong&gt;Edit&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;cloudflare-r2&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;R2_ACCESS_KEY_ID&lt;/code&gt; / &lt;code&gt;R2_SECRET_ACCESS_KEY&lt;/code&gt; (or &lt;code&gt;AWS_*&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;td&gt;R2 → &lt;strong&gt;Object Read &amp;amp; Write&lt;/strong&gt; (+ bucket-create for &lt;code&gt;--create&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;s3&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; / &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Object read &amp;amp; write (+ bucket-create for &lt;code&gt;--create&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;tigris&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;TIGRIS_ACCESS_KEY_ID&lt;/code&gt; / &lt;code&gt;TIGRIS_SECRET_ACCESS_KEY&lt;/code&gt; (or &lt;code&gt;AWS_*&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;td&gt;Tigris access key (&lt;code&gt;tid_&lt;/code&gt;/&lt;code&gt;tsec_&lt;/code&gt;) — Editor&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;git&lt;/code&gt; / &lt;code&gt;github-pages&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;GITHUB_TOKEN&lt;/code&gt; / &lt;code&gt;GH_TOKEN&lt;/code&gt; / &lt;code&gt;GIT_TOKEN&lt;/code&gt; (HTTPS remotes only)&lt;/td&gt;&#xA;&lt;td&gt;Repo contents → &lt;strong&gt;write&lt;/strong&gt; (e.g. a GitHub fine-grained PAT or &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; in Actions). SSH remotes use the agent — no token.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Non-secret settings (account id, bucket, project) may use &lt;code&gt;{env:VAR}&lt;/code&gt;&#xA;&lt;a href=&#34;#configuration-and-interpolation&#34;&gt;config interpolation&lt;/a&gt;. Each driver&#39;s README documents its&#xA;settings and interpolation:&#xA;&lt;a href=&#34;../internal/publish/local/README.md&#34;&gt;local&lt;/a&gt;,&#xA;&lt;a href=&#34;../internal/publish/cloudflare/README.md&#34;&gt;cloudflare-pages&lt;/a&gt;,&#xA;&lt;a href=&#34;../internal/publish/r2/README.md&#34;&gt;cloudflare-r2&lt;/a&gt;,&#xA;&lt;a href=&#34;../internal/publish/s3/README.md&#34;&gt;s3 / tigris&lt;/a&gt;,&#xA;&lt;a href=&#34;../internal/publish/git/README.md&#34;&gt;git / github-pages&lt;/a&gt;,&#xA;&lt;a href=&#34;../internal/publish/command/README.md&#34;&gt;command&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h3 id=&#34;provisioning-with---create&#34;&gt;Provisioning with &lt;code&gt;--create&lt;/code&gt;&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;colophon publish --env &amp;lt;name&amp;gt; --create&lt;/code&gt; provisions destinations before deploying:&#xA;&lt;code&gt;cloudflare-pages&lt;/code&gt; creates the Pages project; &lt;code&gt;cloudflare-r2&lt;/code&gt; / &lt;code&gt;s3&lt;/code&gt; / &lt;code&gt;tigris&lt;/code&gt; create the&#xA;bucket. All are idempotent — an existing destination is left untouched.&lt;/p&gt;&#xA;&lt;p&gt;For the object stores, &lt;code&gt;--create&lt;/code&gt; also sets a &lt;strong&gt;CORS policy&lt;/strong&gt; allowing cross-origin &lt;code&gt;GET&lt;/code&gt;/&lt;code&gt;HEAD&lt;/code&gt;&#xA;from any origin (via the S3 &lt;code&gt;PutBucketCors&lt;/code&gt; API — the only way to configure CORS on R2, which has&#xA;no dashboard for it). This matters when assets are fetched with &lt;code&gt;fetch()&lt;/code&gt; or imported as an ES&#xA;module rather than via an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag — notably a &lt;strong&gt;routed search index&lt;/strong&gt; (below): a&#xA;cross-origin &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; needs no CORS, but &lt;code&gt;fetch()&lt;/code&gt;/&lt;code&gt;import()&lt;/code&gt; do. The step is best-effort: if a&#xA;store doesn&#39;t support &lt;code&gt;PutBucketCors&lt;/code&gt;, the publish warns and continues, and you set CORS manually.&lt;/p&gt;&#xA;&lt;h3 id=&#34;generic-s3--minio--backblaze--wasabi&#34;&gt;Generic S3 / MinIO / Backblaze / Wasabi&lt;/h3&gt;&#xA;&lt;p&gt;The &lt;code&gt;s3&lt;/code&gt; driver is plain S3 (SigV4) with no control-plane code — point it at any S3-compatible&#xA;store with an &lt;code&gt;endpoint&lt;/code&gt; and &lt;code&gt;region&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;publishers:&#xA;  - id: s3&#xA;    driver: s3&#xA;    bucket: my-assets&#xA;    endpoint: &amp;#34;https://s3.us-east-1.amazonaws.com&amp;#34;   # or http://localhost:9000 (MinIO)&#xA;    region: us-east-1&#xA;    public_url: &amp;#34;https://my-assets.s3.us-east-1.amazonaws.com&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Credentials come from &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; / &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;. &lt;code&gt;publish --create&lt;/code&gt; creates&#xA;the bucket (idempotent). &lt;code&gt;public_url&lt;/code&gt; is how colophon learns the public base URL — there&#39;s no&#xA;control-plane lookup, so set it (a route with no resolvable URL stays inactive).&lt;/p&gt;&#xA;&lt;h3 id=&#34;tigris-flyio&#34;&gt;Tigris (Fly.io)&lt;/h3&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://www.tigrisdata.com/&#34;&gt;Tigris&lt;/a&gt; is Fly.io&#39;s global object store, and it&#39;s &lt;strong&gt;plain S3&lt;/strong&gt; —&#xA;colophon talks to it with the same client as any S3 store, so &lt;strong&gt;no &lt;code&gt;flyctl&lt;/code&gt; / Fly SDK /&#xA;control-plane token is involved.&lt;/strong&gt; The &lt;code&gt;tigris&lt;/code&gt; driver is the &lt;code&gt;s3&lt;/code&gt; driver with the endpoint&#xA;(&lt;code&gt;https://t3.storage.dev&lt;/code&gt;) and region (&lt;code&gt;auto&lt;/code&gt;) defaulted, so it needs only a bucket:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;publishers:&#xA;  - id: assets&#xA;    driver: tigris&#xA;    bucket: my-blog-assets&#xA;    public_url: &amp;#34;https://my-blog-assets.t3.storage.dev&amp;#34;   # the bucket&amp;#39;s public/CDN domain&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Credentials come from &lt;code&gt;TIGRIS_ACCESS_KEY_ID&lt;/code&gt; / &lt;code&gt;TIGRIS_SECRET_ACCESS_KEY&lt;/code&gt; (the &lt;code&gt;tid_&lt;/code&gt;/&lt;code&gt;tsec_&lt;/code&gt;&#xA;keys, falling back to &lt;code&gt;AWS_*&lt;/code&gt;). &lt;code&gt;publish --create&lt;/code&gt; creates the bucket. Two things are one-time&#xA;&lt;strong&gt;dashboard&lt;/strong&gt; settings (Tigris has no data-plane API for them, which is what keeps publishing&#xA;SDK-free): &lt;strong&gt;make the bucket public&lt;/strong&gt; to serve a site from it, and optionally &lt;strong&gt;attach a custom&#xA;domain&lt;/strong&gt; (a CNAME to &lt;code&gt;&amp;lt;bucket&amp;gt;.t3.storage.dev&lt;/code&gt;). Newer accounts serve public content from&#xA;&lt;code&gt;t3.tigrisfiles.io&lt;/code&gt; — set &lt;code&gt;public_url&lt;/code&gt; to whatever the bucket actually serves at.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Provisioning credentials is separate: &lt;code&gt;flyctl storage create&lt;/code&gt; issues a bucket + keys, but&#xA;that&#39;s a one-time setup step, not part of &lt;code&gt;colophon publish&lt;/code&gt;. colophon only consumes the keys&#xA;from the environment.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;git-based-hosting-github--gitlab--codeberg-pages&#34;&gt;Git-based hosting (GitHub / GitLab / Codeberg Pages)&lt;/h2&gt;&#xA;&lt;p&gt;The &lt;code&gt;git&lt;/code&gt; driver publishes by &lt;strong&gt;force-pushing the built tree as a single orphan commit&lt;/strong&gt; to a&#xA;nominated branch of a git remote. Whatever serves that branch — GitHub Pages, GitLab Pages,&#xA;Codeberg Pages, a mirror, a self-hosted bare repo — then serves the site. It uses&#xA;&lt;a href=&#34;https://github.com/go-git/go-git&#34;&gt;go-git&lt;/a&gt; (pure Go), so &lt;strong&gt;no &lt;code&gt;git&lt;/code&gt; binary is required&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Because each publish is a fresh orphan commit, the branch always mirrors exactly the current&#xA;build — there&#39;s no history to drift and no stale files to prune. It never touches your working&#xA;tree: the build is staged in a temp repo and pushed from there.&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Setting&lt;/th&gt;&#xA;&lt;th&gt;Default&lt;/th&gt;&#xA;&lt;th&gt;Purpose&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;repo&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;em&gt;(required)&lt;/em&gt;&lt;/td&gt;&#xA;&lt;td&gt;Remote URL (&lt;code&gt;https://…&lt;/code&gt;, &lt;code&gt;git@host:owner/repo&lt;/code&gt;, &lt;code&gt;ssh://…&lt;/code&gt;) or local path.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;branch&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;main&lt;/code&gt; (&lt;code&gt;gh-pages&lt;/code&gt; for &lt;code&gt;github-pages&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;td&gt;The branch to force-push.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;public_url&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;em&gt;(provider-derived)&lt;/em&gt;&lt;/td&gt;&#xA;&lt;td&gt;The site&#39;s canonical URL. Auto-derived for known hosts (below); set it for a custom domain.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;commit_author&lt;/code&gt; / &lt;code&gt;commit_email&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;colophon&lt;/code&gt; / &lt;code&gt;colophon@users.noreply.github.com&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Author of the publish commit.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;commit_message&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;colophon: publish &amp;lt;timestamp&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Commit message.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;&lt;code&gt;public_url&lt;/code&gt; is auto-derived from the remote for known hosts, so you usually don&#39;t set it:&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Host&lt;/th&gt;&#xA;&lt;th&gt;Repo&lt;/th&gt;&#xA;&lt;th&gt;Derived URL&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;github.com&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;me/blog&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;https://me.github.io/blog/&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;github.com&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;me/me.github.io&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;https://me.github.io/&lt;/code&gt; (user/org site)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;gitlab.com&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;me/site&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;https://me.gitlab.io/site/&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;codeberg.org&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;me/pages&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;https://me.codeberg.page/&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Anything else (a self-hosted host, a custom domain via a &lt;code&gt;CNAME&lt;/code&gt;) resolves no URL — set&#xA;&lt;code&gt;public_url&lt;/code&gt; explicitly.&lt;/p&gt;&#xA;&lt;h3 id=&#34;github-pages&#34;&gt;GitHub Pages&lt;/h3&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;publishers:&#xA;  - id: pages&#xA;    driver: github-pages              # branch defaults to gh-pages&#xA;    repo: &amp;#34;git@github.com:me/blog.git&amp;#34;   # SSH: pushes via your ssh-agent&#xA;&#xA;environments:&#xA;  - name: production&#xA;    publish: [pages]&#xA;    allow_publish: true&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;In GitHub Actions, use an HTTPS remote and the workflow token instead of SSH:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;  - id: pages&#xA;    driver: github-pages&#xA;    repo: &amp;#34;https://github.com/me/blog.git&amp;#34;   # GITHUB_TOKEN → push over HTTPS&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then point the repo&#39;s &lt;strong&gt;Settings → Pages&lt;/strong&gt; at the &lt;code&gt;gh-pages&lt;/code&gt; branch.&lt;/p&gt;&#xA;&lt;h3 id=&#34;gitlab-pages&#34;&gt;GitLab Pages&lt;/h3&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;  - id: pages&#xA;    driver: git&#xA;    repo: &amp;#34;git@gitlab.com:me/site.git&amp;#34;&#xA;    branch: pages                      # match your .gitlab-ci.yml `pages` job source&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;GitLab serves Pages from a CI job, so the branch is whatever your &lt;code&gt;pages:&lt;/code&gt; job builds from.&lt;/p&gt;&#xA;&lt;h3 id=&#34;codeberg-pages&#34;&gt;Codeberg Pages&lt;/h3&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;  - id: pages&#xA;    driver: git&#xA;    repo: &amp;#34;git@codeberg.org:me/pages.git&amp;#34;&#xA;    branch: pages                      # Codeberg serves the `pages` branch&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h3 id=&#34;any-git-remote&#34;&gt;Any git remote&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;git&lt;/code&gt; is not GitHub-specific — push to a mirror, a self-hosted Forgejo/Gitea, or a local bare&#xA;repo (handy for tests). Set &lt;code&gt;public_url&lt;/code&gt; since the host is unknown:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;  - id: mirror&#xA;    driver: git&#xA;    repo: &amp;#34;git@git.example.com:web/site.git&amp;#34;&#xA;    branch: deploy&#xA;    public_url: &amp;#34;https://www.example.com&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; follows the remote scheme: an &lt;code&gt;https://&lt;/code&gt; remote uses a token from&#xA;&lt;code&gt;GITHUB_TOKEN&lt;/code&gt; / &lt;code&gt;GH_TOKEN&lt;/code&gt; / &lt;code&gt;GIT_TOKEN&lt;/code&gt;; an &lt;code&gt;git@…&lt;/code&gt; / &lt;code&gt;ssh://&lt;/code&gt; remote uses your SSH agent; a&#xA;local path needs neither.&lt;/p&gt;&#xA;&lt;h2 id=&#34;run-any-cli-the-command-publisher&#34;&gt;Run any CLI: the &lt;code&gt;command&lt;/code&gt; publisher&lt;/h2&gt;&#xA;&lt;p&gt;When no built-in driver fits, the &lt;code&gt;command&lt;/code&gt; driver runs an arbitrary CLI against the built tree&#xA;— so any deploy tool that takes a directory (surge, Netlify, Vercel, Wrangler, exe.dev, Azure&#xA;SWA, &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;scp&lt;/code&gt;, &lt;code&gt;aws s3 sync&lt;/code&gt;, a bespoke script) is a publisher with no driver of its own.&lt;/p&gt;&#xA;&lt;p&gt;colophon materialises the (routed) tree to a temp directory and runs your command there, with&#xA;the directory as its working dir, the parent environment inherited (so the tool&#39;s own token var&#xA;flows through), &lt;code&gt;COLOPHON_*&lt;/code&gt; context injected, and &lt;code&gt;CI=true&lt;/code&gt;. A non-zero exit fails the publish.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;publishers:&#xA;  - id: surge&#xA;    driver: command&#xA;    command: [&amp;#34;surge&amp;#34;, &amp;#34;{dir}&amp;#34;, &amp;#34;myblog.surge.sh&amp;#34;]   # argv list — never a shell&#xA;    public_url: &amp;#34;https://myblog.surge.sh&amp;#34;            # SURGE_TOKEN comes from the env&#xA;&#xA;  - id: rsync&#xA;    driver: command&#xA;    host: &amp;#34;deploy@example.com:/var/www/blog&amp;#34;          # any custom setting → {host}&#xA;    command: [&amp;#34;rsync&amp;#34;, &amp;#34;-az&amp;#34;, &amp;#34;--delete&amp;#34;, &amp;#34;{dir}/&amp;#34;, &amp;#34;{host}&amp;#34;]&#xA;    public_url: &amp;#34;https://www.example.com&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The command is an &lt;strong&gt;argv list executed directly — never through a shell&lt;/strong&gt;, so there&#39;s no shell&#xA;injection surface. For a one-liner with pipes or &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, make the shell explicit:&#xA;&lt;code&gt;[&amp;quot;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;aws s3 sync {dir} s3://bucket --cache-control max-age=3600&amp;quot;]&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Interpolation.&lt;/strong&gt; Every argument is interpolated with &lt;code&gt;{placeholder}&lt;/code&gt; tokens drawn from your own&#xA;publisher settings plus colophon runtime values (which win on a clash): &lt;code&gt;{dir}&lt;/code&gt; / &lt;code&gt;{output_dir}&lt;/code&gt;&#xA;(the materialised tree, also the CWD), &lt;code&gt;{manifest}&lt;/code&gt; (a JSON file classifying each path as&#xA;page/asset/feed/… with content-type and size, written &lt;em&gt;beside&lt;/em&gt; the tree so it isn&#39;t published&#xA;unless you reference it), &lt;code&gt;{public_url}&lt;/code&gt;, &lt;code&gt;{id}&lt;/code&gt;, &lt;code&gt;{file_count}&lt;/code&gt;, and any setting you declare&#xA;(&lt;code&gt;{host}&lt;/code&gt;, &lt;code&gt;{domain}&lt;/code&gt;, &lt;code&gt;{project}&lt;/code&gt;, …). Unknown placeholders error, so a typo fails loudly.&#xA;Per-environment &lt;code&gt;overrides&lt;/code&gt; vary any setting, so one &lt;code&gt;command&lt;/code&gt; publisher can target staging vs&#xA;production. The same values are exposed as &lt;code&gt;COLOPHON_OUTPUT_DIR&lt;/code&gt; / &lt;code&gt;COLOPHON_MANIFEST&lt;/code&gt; /&#xA;&lt;code&gt;COLOPHON_PUBLIC_URL&lt;/code&gt; / … env vars.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Secrets stay in the environment.&lt;/strong&gt; colophon never injects a token into the command line — the&#xA;child inherits the environment and the target tool reads its own &lt;code&gt;$SURGE_TOKEN&lt;/code&gt; / &lt;code&gt;$VERCEL_TOKEN&lt;/code&gt;&#xA;there, matching colophon&#39;s env-only rule and the deploy-CLI best practice of keeping credentials&#xA;out of argv (where they&#39;d leak into process listings and shell history). The command runs with&#xA;your privileges from your own config — same trust as a Makefile — and is gated behind&#xA;&lt;code&gt;--allow-publish&lt;/code&gt; like every deploy.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;per-environment-overrides&#34;&gt;Per-environment overrides&lt;/h2&gt;&#xA;&lt;p&gt;An environment can override any publisher &lt;em&gt;setting&lt;/em&gt; via &lt;code&gt;overrides&lt;/code&gt;, keyed by publisher id —&#xA;so one publisher definition serves several environments. For example, a single &lt;code&gt;local&lt;/code&gt;&#xA;publisher can write a &lt;strong&gt;distinct output directory per environment&lt;/strong&gt; (handy for previewing a&#xA;different theme side by side) without defining a publisher per env:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;publishers:&#xA;  - id: local&#xA;    driver: local&#xA;    path: ./dist            # default output dir&#xA;&#xA;environments:&#xA;  - name: dist&#xA;    publish: [local]        # → ./dist&#xA;  - name: text&#xA;    publish: [local]&#xA;    theme: minimal&#xA;    overrides:&#xA;      local:&#xA;        path: ./dist-text   # same publisher, a different dir for this env&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Overrides also carry per-environment publisher tweaks like a Cloudflare Pages &lt;code&gt;branch&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;redirects-aliases&#34;&gt;Redirects (aliases)&lt;/h2&gt;&#xA;&lt;p&gt;A post&#39;s &lt;code&gt;aliases:&lt;/code&gt; (&lt;a href=&#34;/start/content/#redirects-aliases&#34;&gt;Authoring → Redirects&lt;/a&gt;) produce, at build&#xA;time, a meta-refresh stub per old URL, a root &lt;code&gt;_redirects&lt;/code&gt; file, and a root &lt;code&gt;.nojekyll&lt;/code&gt;. How that&#xA;becomes a redirect depends on the host:&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Host&lt;/th&gt;&#xA;&lt;th&gt;Result&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Cloudflare Pages, Netlify, GitLab Pages&lt;/td&gt;&#xA;&lt;td&gt;real &lt;strong&gt;301&lt;/strong&gt; from &lt;code&gt;_redirects&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;S3 static-website hosting&lt;/td&gt;&#xA;&lt;td&gt;real &lt;strong&gt;301&lt;/strong&gt; — the &lt;code&gt;s3&lt;/code&gt; publisher sets the object redirect header on publish&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Cloudflare R2, bare S3/MinIO, local&lt;/td&gt;&#xA;&lt;td&gt;client-side &lt;strong&gt;meta-refresh&lt;/strong&gt; stub (no in-bucket redirects)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;GitHub Pages&lt;/td&gt;&#xA;&lt;td&gt;client-side &lt;strong&gt;meta-refresh&lt;/strong&gt; stub; the &lt;code&gt;.nojekyll&lt;/code&gt; is what keeps &lt;code&gt;_search/&lt;/code&gt; and the stubs from being stripped&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;So redirects work everywhere; hosts that support server-side rules get a true 301, the rest fall&#xA;back to the (always-emitted) stub. &lt;code&gt;colophon doctor&lt;/code&gt; warns about alias conflicts before you ship.&lt;/p&gt;&#xA;&lt;h2 id=&#34;websub-real-time-feeds&#34;&gt;WebSub (real-time feeds)&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://www.w3.org/TR/websub/&#34;&gt;WebSub&lt;/a&gt; lets subscribers get your new posts pushed instantly&#xA;instead of polling. List one or more public hubs and colophon does both halves: it advertises them&#xA;in every feed (&lt;code&gt;&amp;lt;link rel=&amp;quot;hub&amp;quot;&amp;gt;&lt;/code&gt; in RSS/Atom, a &lt;code&gt;hubs&lt;/code&gt; entry in JSON Feed, plus &lt;code&gt;rel=&amp;quot;self&amp;quot;&lt;/code&gt;), and&#xA;&lt;strong&gt;pings them after each successful publish&lt;/strong&gt; so the hub re-fetches and fans out to subscribers.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    federation:&#xA;      websub:&#xA;        hubs:&#xA;          - https://pubsubhubbub.appspot.com/   # Google&amp;#39;s public hub (or run your own)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The ping is best-effort: it runs only on a real deploy (a public &lt;code&gt;base_url&lt;/code&gt;, not gated), and a hub&#xA;that&#39;s slow or down only logs a &lt;code&gt;WEBSUB … ping failed&lt;/code&gt; line — it never fails the publish. No hubs&#xA;configured → nothing is advertised or pinged.&lt;/p&gt;&#xA;&lt;h2 id=&#34;on-site-search&#34;&gt;On-site search&lt;/h2&gt;&#xA;&lt;p&gt;colophon builds a &lt;strong&gt;static search index&lt;/strong&gt; at build time — a sharded, content-addressed BM25 index&#xA;a tiny browser reader queries client-side (no server, no service). Enable it per site with the&#xA;&lt;code&gt;search&lt;/code&gt; stanza:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    search:&#xA;      mode: lexical     # off (default) | lexical&#xA;      fuzzy: true       # opt-in typo tolerance (trigram + Levenshtein); roughly doubles the index&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The string shorthand &lt;code&gt;search: lexical&lt;/code&gt; still works (equivalent to &lt;code&gt;mode: lexical&lt;/code&gt;, no fuzzy).&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;mode&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;lexical&lt;/code&gt; turns search on; omitted/&lt;code&gt;off&lt;/code&gt; leaves it out entirely (no index, no box).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;fuzzy&lt;/code&gt;&lt;/strong&gt; — when on, a query token that finds no exact/&lt;strong&gt;prefix&lt;/strong&gt; match falls back to&#xA;typo-tolerant matching (so &amp;quot;wikilnk&amp;quot; finds &amp;quot;wikilinks&amp;quot;). It&#39;s opt-in because the trigram index&#xA;it needs roughly doubles the index size — a cost a low-bandwidth search shouldn&#39;t pay unasked.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Results are &lt;strong&gt;prefix-matched&lt;/strong&gt; by default (&amp;quot;wiki&amp;quot; → &amp;quot;wikilinks&amp;quot;), with query-aware highlighting&#xA;and an occurrence count. The index + reader ship only when search is on, under &lt;code&gt;_search/&lt;/code&gt;; a theme&#xA;renders the box (the &lt;code&gt;press&lt;/code&gt;, &lt;code&gt;press-gazette&lt;/code&gt; and &lt;code&gt;press-broadsheet&lt;/code&gt; themes include one). &lt;code&gt;colophon search &amp;quot;&amp;lt;query&amp;gt;&amp;quot;&lt;/code&gt; queries&#xA;the same engine from the CLI (always fuzzy), in text or &lt;code&gt;--json&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;For large sites, the index can be &lt;strong&gt;routed to an object store&lt;/strong&gt; to keep it off a Pages-style file&#xA;budget — see &lt;a href=&#34;#routing-the-search-index&#34;&gt;Routing the search index&lt;/a&gt; below (it also covers the CORS&#xA;that a cross-origin index needs, set automatically by &lt;code&gt;--create&lt;/code&gt;).&lt;/p&gt;&#xA;&lt;h2 id=&#34;routing-assets-to-an-object-store&#34;&gt;Routing assets to an object store&lt;/h2&gt;&#xA;&lt;p&gt;Shipping large or numerous images with a Pages/Workers deployment can exhaust its file&#xA;budget. &lt;strong&gt;Routing&lt;/strong&gt; sends matching paths to a different publisher — typically images to an&#xA;object store — and rewrites their URLs to that store&#39;s public base.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    routing:&#xA;      - match: &amp;#34;**/assets/**&amp;#34;          # glob; ** crosses slashes, * does not&#xA;        publisher: r2                  # rewrite target inherited from the r2 publisher&#xA;&#xA;publishers:&#xA;  - id: r2&#xA;    driver: cloudflare-r2&#xA;    bucket: &amp;#34;{env:R2_BUCKET:-my-assets}&amp;#34;&#xA;    account_id: &amp;#34;{env:CLOUDFLARE_ACCOUNT_ID}&amp;#34;&#xA;    public_url: &amp;#34;{env:R2_PUBLIC_URL:-}&amp;#34;  # optional; auto-discovered for R2 (see below)&#xA;&#xA;environments:&#xA;  - name: production&#xA;    publish: [cf, r2]    # HTML to Pages, routed images to R2&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;How it works:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Build&lt;/strong&gt; rewrites every routed image reference to the route&#39;s URL + path, so the HTML&#xA;points at the object store.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Publish&lt;/strong&gt; partitions the tree: the route&#39;s publisher (&lt;code&gt;r2&lt;/code&gt;) receives only the matched&#xA;files; every other publisher (&lt;code&gt;cf&lt;/code&gt;) receives the unrouted remainder.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The route&#39;s URL is resolved as: the route&#39;s own &lt;code&gt;base_url&lt;/code&gt;, else the target publisher&#39;s&#xA;&lt;code&gt;public_url&lt;/code&gt;, else — on &lt;code&gt;publish&lt;/code&gt;, for Cloudflare R2 with a &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; — the&#xA;bucket&#39;s &lt;strong&gt;auto-discovered&lt;/strong&gt; URL (a connected custom domain, preferring the shortest, else&#xA;the &lt;code&gt;r2.dev&lt;/code&gt; managed URL). So &lt;code&gt;publish --create&lt;/code&gt; provisions the bucket, enables &lt;code&gt;r2.dev&lt;/code&gt;,&#xA;and images serve from it with no URL in config; connect a custom domain later and the next&#xA;publish prefers it automatically.&lt;/p&gt;&#xA;&lt;p&gt;A rule is &lt;strong&gt;inactive until a URL resolves &lt;em&gt;and&lt;/em&gt; its publisher is deploying&lt;/strong&gt; in the&#xA;environment. With nothing resolvable, routing is a no-op: images stay co-located and the&#xA;whole tree goes to the default publisher — so local builds and previews work with no object&#xA;store configured.&lt;/p&gt;&#xA;&lt;h3 id=&#34;routing-the-search-index&#34;&gt;Routing the search index&lt;/h3&gt;&#xA;&lt;p&gt;The static search index (&lt;code&gt;_search/**&lt;/code&gt;) can be routed to the object store too, to keep it off a&#xA;Pages-style file budget:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;routing:&#xA;  - match: &amp;#34;_search/**&amp;#34;&#xA;    publisher: r2&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;When routed, colophon points the browser reader at the store&#39;s URL automatically (the&#xA;&lt;code&gt;search_base&lt;/code&gt; it emits follows the route). Because the reader loads the index with &lt;code&gt;fetch()&lt;/code&gt; and&#xA;imports &lt;code&gt;search.js&lt;/code&gt; as a module — neither CORS-exempt — the bucket must allow cross-origin &lt;code&gt;GET&lt;/code&gt;;&#xA;&lt;code&gt;publish --create&lt;/code&gt; sets that policy for you (see &lt;a href=&#34;#provisioning-with---create&#34;&gt;Provisioning&lt;/a&gt;).&#xA;Unrouted, the index stays on the same origin and no CORS is involved.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/publishing.md&#34;&gt;&lt;code&gt;docs/publishing.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Themes</title>
    <id>https://docs.colophon.blog/start/themes/</id>
    <link href="https://docs.colophon.blog/start/themes/" rel="alternate"></link>
    <updated>2001-12-27T00:00:00Z</updated>
    <published>2001-12-27T00:00:00Z</published>
    <summary type="text">A theme turns colophon&#39;s page data into HTML. Themes are pongo2 templates (Jinja2/Django syntax) plus static assets. Three themes ship built in:</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/themes.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;A theme turns colophon&#39;s page data into HTML. Themes are&#xA;&lt;a href=&#34;https://github.com/flosch/pongo2&#34;&gt;pongo2&lt;/a&gt; templates (Jinja2/Django syntax) plus static&#xA;assets. Three themes ship built in:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;default&lt;/code&gt;&lt;/strong&gt; — full-featured: hero banners, index thumbnails, and vendored&#xA;highlight.js / KaTeX / Mermaid for code, maths and diagrams, plus self-hosted web fonts.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;press&lt;/code&gt;&lt;/strong&gt; — colophon.blog&#39;s brand theme. Literary-modern (Fraunces over Inter), light &amp;amp;&#xA;dark, drifting glow, ink-blob title reveal, feed popouts. It &lt;em&gt;inherits&lt;/em&gt; &lt;code&gt;default&lt;/code&gt; (see&#xA;&lt;a href=&#34;#base-themes-inheriting-another-theme&#34;&gt;base themes&lt;/a&gt;), so it reuses the same vendored&#xA;libraries and fonts without shipping its own copy. The home-page lede under the title comes&#xA;from the site&#39;s optional &lt;code&gt;tagline:&lt;/code&gt; (presentational, distinct from the SEO &lt;code&gt;description:&lt;/code&gt;);&#xA;unset renders no lede.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;minimal&lt;/code&gt;&lt;/strong&gt; — plain, readable text. No JavaScript and no web fonts; rich blocks show as&#xA;their raw source (the &lt;a href=&#34;/start/content/#the-raw-block-contract-progressive-enhancement&#34;&gt;raw-block contract&lt;/a&gt;).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;More themes (&lt;code&gt;flux&lt;/code&gt;, &lt;code&gt;signal&lt;/code&gt;, &lt;code&gt;obsidian&lt;/code&gt;) live in &lt;a href=&#34;#community-themes-contribthemes&#34;&gt;&lt;code&gt;contrib/themes/&lt;/code&gt;&lt;/a&gt;&#xA;and are installed by copying them into your project.&lt;/p&gt;&#xA;&lt;h2 id=&#34;selecting-a-theme&#34;&gt;Selecting a theme&lt;/h2&gt;&#xA;&lt;p&gt;Set it on the site, and optionally override it per environment — handy for previewing a theme&#xA;before promoting it to production:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    theme: default        # site default&#xA;&#xA;environments:&#xA;  - name: production&#xA;    # inherits theme: default&#xA;  - name: text&#xA;    theme: minimal        # this environment builds with the minimal theme&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Precedence: &lt;strong&gt;environment &lt;code&gt;theme&lt;/code&gt; &amp;gt; site &lt;code&gt;theme&lt;/code&gt; &amp;gt; &lt;code&gt;default&lt;/code&gt;&lt;/strong&gt;. Build or serve an environment&#xA;to see its theme:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon build --env text     # builds public/ with the minimal theme&#xA;colophon serve                # serves every environment, each with its own theme&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;inspecting-and-ejecting-themes&#34;&gt;Inspecting and ejecting themes&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon themes list            # default, minimal, press&#xA;colophon themes eject minimal   # copies the built-in into themes/minimal/ to edit&#xA;colophon themes eject default   # full default theme, incl. its vendored libraries&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;eject&lt;/code&gt; writes a built-in theme to &lt;code&gt;themes/&amp;lt;name&amp;gt;/&lt;/code&gt; in your project; the on-disk copy then&#xA;overrides the built-in (use &lt;code&gt;--force&lt;/code&gt; to overwrite an existing directory). It&#39;s the easiest&#xA;way to start customising — eject, then edit only the files you care about. Ejecting an&#xA;overlay theme (e.g. &lt;code&gt;press&lt;/code&gt;) writes only &lt;em&gt;its own&lt;/em&gt; files; the base theme&#39;s inherited assets&#xA;stay in the binary and still resolve at build, so the eject stays small.&lt;/p&gt;&#xA;&lt;h2 id=&#34;supplying-your-own-theme&#34;&gt;Supplying your own theme&lt;/h2&gt;&#xA;&lt;p&gt;Put files under &lt;code&gt;themes/&amp;lt;name&amp;gt;/&lt;/code&gt; in your project root and set &lt;code&gt;theme: &amp;lt;name&amp;gt;&lt;/code&gt; (or eject one&#xA;to start from). Files there &lt;strong&gt;override the built-in default per file&lt;/strong&gt;, so you only write&#xA;what you want to change:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;themes/&#xA;  mytheme/&#xA;    page.html      # overrides the post template&#xA;    style.css      # overrides the stylesheet&#xA;    logo.svg       # a new static asset, copied to the output root&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;An unknown theme name with no &lt;code&gt;themes/&amp;lt;name&amp;gt;/&lt;/code&gt; directory falls back to the &lt;code&gt;default&lt;/code&gt; theme.&lt;/p&gt;&#xA;&lt;h3 id=&#34;base-themes-inheriting-another-theme&#34;&gt;Base themes (inheriting another theme)&lt;/h3&gt;&#xA;&lt;p&gt;A theme can inherit another theme&#39;s templates and static assets by declaring a base. For an&#xA;on-disk theme this is automatic: any &lt;code&gt;themes/&amp;lt;name&amp;gt;/&lt;/code&gt; directory &lt;strong&gt;inherits &lt;code&gt;default&lt;/code&gt;&lt;/strong&gt;, so it&#xA;only needs the files it changes (this is why dropping in a single &lt;code&gt;style.css&lt;/code&gt; works). A&#xA;built-in theme inherits explicitly via a one-line &lt;code&gt;base&lt;/code&gt; file naming the base theme — the&#xA;built-in &lt;code&gt;press&lt;/code&gt; theme contains &lt;code&gt;base&lt;/code&gt; → &lt;code&gt;default&lt;/code&gt;, so it reuses the default&#39;s vendored&#xA;libraries and fonts and supplies only its own &lt;code&gt;page.html&lt;/code&gt;, &lt;code&gt;index.html&lt;/code&gt; and &lt;code&gt;style.css&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Resolution order, highest precedence first: your project&#39;s &lt;code&gt;themes/&amp;lt;name&amp;gt;/&lt;/code&gt; → the theme&#39;s&#xA;own files → its base theme&#39;s files. The &lt;code&gt;base&lt;/code&gt; marker is never copied to the output.&lt;/p&gt;&#xA;&lt;h3 id=&#34;community-themes-contribthemes&#34;&gt;Community themes (&lt;code&gt;contrib/themes/&lt;/code&gt;)&lt;/h3&gt;&#xA;&lt;p&gt;The colophon repo ships extra themes under &lt;code&gt;contrib/themes/&lt;/code&gt; that are &lt;strong&gt;not&lt;/strong&gt; baked into the&#xA;binary. To use one, copy it into your project and select it:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;cp -r contrib/themes/flux myblog/themes/flux&#xA;# then, in colophon.yaml:  theme: flux&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Because on-disk themes inherit &lt;code&gt;default&lt;/code&gt;, a contrib theme only carries its own templates and&#xA;&lt;code&gt;style.css&lt;/code&gt;; the vendored libraries and fonts come from the built-in &lt;code&gt;default&lt;/code&gt; at build time.&lt;/p&gt;&#xA;&lt;h3 id=&#34;theme-files&#34;&gt;Theme files&lt;/h3&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;File&lt;/th&gt;&#xA;&lt;th&gt;Role&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;page.html&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Renders a single entry (post or page). &lt;strong&gt;Required&lt;/strong&gt; — the default for every page type.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;index.html&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Renders the site index (post list).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;type&amp;gt;.html&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;em&gt;Optional.&lt;/em&gt; Renders entries of page type &lt;code&gt;&amp;lt;type&amp;gt;&lt;/code&gt; (e.g. &lt;code&gt;project.html&lt;/code&gt;); falls back to &lt;code&gt;page.html&lt;/code&gt;. See &lt;a href=&#34;#page-types&#34;&gt;Page types&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;favicon.svg&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Default site icon (override per-site with &lt;code&gt;favicon:&lt;/code&gt; pointing at a project file).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;theme.yaml&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;em&gt;Optional.&lt;/em&gt; Theme metadata — a &lt;code&gt;description&lt;/code&gt;, and &lt;code&gt;image.genai.system_prompt&lt;/code&gt; (the house style for &lt;a href=&#34;/guides/image-generation/#house-style-theme-system-prompt&#34;&gt;generated images&lt;/a&gt;). Not copied to the output.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;em&gt;anything else&lt;/em&gt;&lt;/td&gt;&#xA;&lt;td&gt;Any non-&lt;code&gt;.html&lt;/code&gt; file is copied verbatim to the output root (CSS, JS, fonts, images).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Static assets keep their relative path: &lt;code&gt;themes/mytheme/vendor/app.js&lt;/code&gt; is written to&#xA;&lt;code&gt;/vendor/app.js&lt;/code&gt; and referenced as &lt;code&gt;{{ base_path }}vendor/app.js&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h3 id=&#34;page-types&#34;&gt;Page types&lt;/h3&gt;&#xA;&lt;p&gt;Every entry has a &lt;strong&gt;type&lt;/strong&gt;. By default it&#39;s derived from whether the entry has a date — a&#xA;dated entry is a &lt;code&gt;post&lt;/code&gt; (chronological: listed on the index, in feeds, on tag pages), a&#xA;dateless one is a &lt;code&gt;page&lt;/code&gt; (standing chrome: surfaced in the nav menu, not in the list/feeds).&#xA;An author can override this with a &lt;code&gt;type:&lt;/code&gt; in frontmatter (see&#xA;&lt;a href=&#34;/start/content/#page-types&#34;&gt;Authoring → page types&lt;/a&gt;), including custom types like &lt;code&gt;project&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;As a theme author you don&#39;t have to do anything: &lt;strong&gt;every type renders with &lt;code&gt;page.html&lt;/code&gt;&lt;/strong&gt; unless&#xA;you opt in. When you want a type to look different, you have two ways — pick whichever suits.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;1. A dedicated template&lt;/strong&gt; — add &lt;code&gt;themes/&amp;lt;theme&amp;gt;/&amp;lt;type&amp;gt;.html&lt;/code&gt;. An entry of that type renders&#xA;with it; any type without its own file falls back to &lt;code&gt;page.html&lt;/code&gt;. The file is an ordinary&#xA;single-entry template and receives the &lt;strong&gt;same variables as &lt;code&gt;page.html&lt;/code&gt;&lt;/strong&gt; (see the table below).&#xA;For example, to give &lt;code&gt;type: project&lt;/code&gt; entries a bespoke layout:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;{# themes/mytheme/project.html — renders entries with `type: project` #}&#xA;&amp;lt;!doctype html&amp;gt;&#xA;&amp;lt;html lang=&amp;#34;{{ lang }}&amp;#34;&amp;gt;&#xA;&amp;lt;head&amp;gt;&#xA;  &amp;lt;meta charset=&amp;#34;utf-8&amp;#34;&amp;gt;&amp;lt;title&amp;gt;{{ meta_title }}&amp;lt;/title&amp;gt;&#xA;  &amp;lt;link rel=&amp;#34;stylesheet&amp;#34; href=&amp;#34;{{ base_path }}style.css&amp;#34;&amp;gt;{{ seo_head|safe }}&#xA;&amp;lt;/head&amp;gt;&#xA;&amp;lt;body&amp;gt;&#xA;  &amp;lt;article class=&amp;#34;project&amp;#34;&amp;gt;&#xA;    &amp;lt;h1&amp;gt;{{ title }}&amp;lt;/h1&amp;gt;&#xA;    {% if image %}&amp;lt;img class=&amp;#34;project-shot&amp;#34; src=&amp;#34;{{ image }}&amp;#34; alt=&amp;#34;{{ title }}&amp;#34;&amp;gt;{% endif %}&#xA;    {{ content|safe }}&#xA;    {% if tags %}&amp;lt;footer&amp;gt;{% for t in tags %}&amp;lt;a href=&amp;#34;{{ t.url }}&amp;#34;&amp;gt;{{ t.name }}&amp;lt;/a&amp;gt; {% endfor %}&amp;lt;/footer&amp;gt;{% endif %}&#xA;  &amp;lt;/article&amp;gt;&#xA;&amp;lt;/body&amp;gt;&#xA;&amp;lt;/html&amp;gt;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;2. Branch inside &lt;code&gt;page.html&lt;/code&gt;&lt;/strong&gt; — the &lt;code&gt;page_type&lt;/code&gt; variable holds the resolved type, so one&#xA;template can switch on it without a separate file:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;{% if page_type == &amp;#34;project&amp;#34; %}&#xA;  &amp;lt;span class=&amp;#34;badge&amp;#34;&amp;gt;Project&amp;lt;/span&amp;gt;&#xA;{% elif page_type == &amp;#34;page&amp;#34; %}&#xA;  {# a standing page — maybe hide the date/reading-time line #}&#xA;{% else %}&#xA;  &amp;lt;time&amp;gt;{{ date }}&amp;lt;/time&amp;gt; · {{ read_time }} min&#xA;{% endif %}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Placement.&lt;/strong&gt; A custom type is &lt;em&gt;listed&lt;/em&gt; (post-like) by default; the built-in &lt;code&gt;page&lt;/code&gt; is the only&#xA;&lt;em&gt;standing&lt;/em&gt; (nav) type. So &lt;code&gt;type: page&lt;/code&gt; makes a dated entry standing (it appears in &lt;code&gt;nav_pages&lt;/code&gt;,&#xA;not in the index list or feeds), and &lt;code&gt;type: post&lt;/code&gt; makes a dateless one listed. You don&#39;t render&#xA;the nav/list yourself per type — the build routes entries into &lt;code&gt;nav_pages&lt;/code&gt; (standing) vs &lt;code&gt;pages&lt;/code&gt;&#xA;(listed) for you; your per-type template only styles the single entry.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-templating-language&#34;&gt;The templating language&lt;/h2&gt;&#xA;&lt;p&gt;Templates are &lt;a href=&#34;https://github.com/flosch/pongo2&#34;&gt;pongo2&lt;/a&gt; — Jinja2/Django syntax:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;{{ value }}&lt;/code&gt; prints a value (HTML-escaped by default).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;{{ value|safe }}&lt;/code&gt; prints pre-rendered HTML &lt;strong&gt;without&lt;/strong&gt; escaping — required for &lt;code&gt;content&lt;/code&gt;,&#xA;&lt;code&gt;feed_head&lt;/code&gt; and &lt;code&gt;seo_head&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;{% if x %}…{% elif y %}…{% else %}…{% endif %}&lt;/code&gt; and &lt;code&gt;{% for item in list %}…{% endfor %}&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Filters chain with &lt;code&gt;|&lt;/code&gt;, e.g. &lt;code&gt;{{ title|default:site_title }}&lt;/code&gt;, &lt;code&gt;{{ tags|length }}&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;{# comment #}&lt;/code&gt; (keep it on one line — pongo2 rejects a newline inside &lt;code&gt;{# … #}&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Always prefix internal links with &lt;code&gt;{{ base_path }}&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;{{ base_path }}style.css&lt;/code&gt;,&#xA;&lt;code&gt;{{ base_path }}{{ p.url }}&lt;/code&gt;). &lt;code&gt;base_path&lt;/code&gt; makes the theme work whether the site is served&#xA;from &lt;code&gt;/&lt;/code&gt; or a sub-path.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;template-variables&#34;&gt;Template variables&lt;/h2&gt;&#xA;&lt;h3 id=&#34;pagehtml-a-single-post&#34;&gt;&lt;code&gt;page.html&lt;/code&gt; (a single post)&lt;/h3&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Variable&lt;/th&gt;&#xA;&lt;th&gt;Description&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;site_title&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The site title.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Post metadata (&lt;code&gt;date&lt;/code&gt; is a date; may be empty).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;meta_title&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Pre-resolved &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; text (SEO title → title → site title).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;content&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The rendered post HTML. Output with &lt;code&gt;{{ content|safe }}&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;base_path&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;URL prefix for internal links (always starts and ends with &lt;code&gt;/&lt;/code&gt;).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;base_url&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Absolute site root, for canonical/social URLs.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;feed_head&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;link rel=&amp;quot;alternate&amp;quot;&amp;gt;&lt;/code&gt; feed-discovery tags. Output with &lt;code&gt;{{ feed_head|safe }}&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;seo_head&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Full SEO &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;: canonical, robots, Open Graph, Twitter, JSON-LD. &lt;code&gt;{{ seo_head|safe }}&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;analytics_head&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Analytics provider markup (statsfactory beacon and/or GA loader). Output once before &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; with &lt;code&gt;{{ analytics_head|safe }}&lt;/code&gt;. Empty when the site configures no analytics. See &lt;a href=&#34;#analytics&#34;&gt;Analytics&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;glossary_head&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Glossary styles + decorator &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;. Output once before &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; with &lt;code&gt;{{ glossary_head|safe }}&lt;/code&gt;. Empty unless the page uses a glossary term. See &lt;a href=&#34;#glossary&#34;&gt;Glossary&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;lang&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The page&#39;s BCP-47 language tag — put it on &lt;code&gt;&amp;lt;html lang=&amp;quot;{{ lang }}&amp;quot;&amp;gt;&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;favicon&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Favicon filename, or empty.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;hero&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Hero banner URL (page-relative, or absolute when routed), or empty.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;hero_alt&lt;/code&gt;, &lt;code&gt;hero_style&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Hero alt text, and a ready &lt;code&gt;object-fit&lt;/code&gt;/&lt;code&gt;object-position&lt;/code&gt; style string (may be empty). Use as &lt;code&gt;alt=&amp;quot;{{ hero_alt }}&amp;quot;&lt;/code&gt;{% if hero_style %} &lt;code&gt;style=&amp;quot;{{ hero_style }}&amp;quot;&lt;/code&gt;{% endif %}.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;image&lt;/code&gt;, &lt;code&gt;image_abs&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Preview image href; absolute preview URL for &lt;code&gt;og:image&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;image_alt&lt;/code&gt;, &lt;code&gt;image_style&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Card-image alt text and &lt;code&gt;object-fit&lt;/code&gt; style (the index list items carry &lt;code&gt;image_alt&lt;/code&gt;/&lt;code&gt;image_style&lt;/code&gt; too).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;List of &lt;code&gt;{name, url}&lt;/code&gt; — linked tag chips. Prefix nothing; &lt;code&gt;url&lt;/code&gt; is ready to use.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Primary category string (first category, else first tag, else empty).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;read_time&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Estimated reading time in whole minutes (integer).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;toc&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;List of &lt;code&gt;{level, id, text}&lt;/code&gt; headings, for a table of contents.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;page_type&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The resolved page type (&lt;code&gt;post&lt;/code&gt;, &lt;code&gt;page&lt;/code&gt;, or a custom value) — for branching within a shared template.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;, &lt;code&gt;embargoed&lt;/code&gt;, &lt;code&gt;embargo_until&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Preview-only flags for not-yet-public posts.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;has_code&lt;/code&gt;, &lt;code&gt;has_math&lt;/code&gt;, &lt;code&gt;has_mermaid&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;True when the post uses that block type — load the matching library only when set.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;author_name&lt;/code&gt;, &lt;code&gt;author_initials&lt;/code&gt;, &lt;code&gt;author_bio&lt;/code&gt;, &lt;code&gt;author_url&lt;/code&gt;, &lt;code&gt;author_avatar&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Author h-card fields for the byline (empty when unset). &lt;code&gt;author_avatar&lt;/code&gt; is a ready-to-use &lt;code&gt;src&lt;/code&gt;: a file-path avatar is published to &lt;code&gt;/assets/&amp;lt;name&amp;gt;&lt;/code&gt; and emitted root-anchored (or as the object-store URL when routed); &lt;code&gt;data:&lt;/code&gt;/&lt;code&gt;http(s)://&lt;/code&gt; and &lt;code&gt;gravatar&lt;/code&gt; avatars resolve to a URL that passes through.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;has_audio&lt;/code&gt;, &lt;code&gt;audio&lt;/code&gt;, &lt;code&gt;audio_type&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;True when the post has an audio reading (recorded or generated TTS); &lt;code&gt;audio&lt;/code&gt; is its URL and &lt;code&gt;audio_type&lt;/code&gt; its MIME. See &lt;a href=&#34;#audio-video--downloads&#34;&gt;Audio, video &amp;amp; downloads&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;audio_listen&lt;/code&gt;, &lt;code&gt;audio_play&lt;/code&gt;, &lt;code&gt;audio_pause&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Localised player UI strings (figcaption + play/pause aria-labels), in the page&#39;s language. Present only when &lt;code&gt;has_audio&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;has_attachments&lt;/code&gt;, &lt;code&gt;attachments&lt;/code&gt;, &lt;code&gt;attachments_html&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Downloads. &lt;code&gt;attachments_html&lt;/code&gt; is a ready-to-drop-in, no-JS block (&lt;code&gt;{{ attachments_html|safe }}&lt;/code&gt;); &lt;code&gt;attachments&lt;/code&gt; is the structured list — &lt;code&gt;{url, label, description, name, type, type_label, size, bytes}&lt;/code&gt; — if you&#39;d rather build your own. &lt;code&gt;has_attachments&lt;/code&gt; is the flag. See &lt;a href=&#34;#audio-video--downloads&#34;&gt;Audio, video &amp;amp; downloads&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;mentions_enabled&lt;/code&gt;, &lt;code&gt;has_mentions&lt;/code&gt;, &lt;code&gt;mentions&lt;/code&gt;, &lt;code&gt;mentions_html&lt;/code&gt;, &lt;code&gt;mentions_attrs&lt;/code&gt;, &lt;code&gt;mentions_src&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Webmentions (replies/likes/reposts). What&#39;s populated depends on the site&#39;s &lt;code&gt;display.mode&lt;/code&gt; — see &lt;a href=&#34;#webmentions-responses&#34;&gt;Webmentions&lt;/a&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;has_syndication&lt;/code&gt;, &lt;code&gt;syndication&lt;/code&gt;, &lt;code&gt;syndication_html&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&amp;quot;Also posted on…&amp;quot; links from the post&#39;s &lt;code&gt;syndication:&lt;/code&gt; frontmatter (absolute URLs). &lt;code&gt;syndication_html&lt;/code&gt; is a no-JS drop-in of mf2 &lt;code&gt;u-syndication&lt;/code&gt; links (&lt;code&gt;{{ syndication_html|safe }}&lt;/code&gt;, empty when none); &lt;code&gt;syndication&lt;/code&gt; is the raw URL list to build your own.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;h3 id=&#34;indexhtml-the-post-list-and-per-tag-pages&#34;&gt;&lt;code&gt;index.html&lt;/code&gt; (the post list, and per-tag pages)&lt;/h3&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Variable&lt;/th&gt;&#xA;&lt;th&gt;Description&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;lang&lt;/code&gt;, &lt;code&gt;site_title&lt;/code&gt;, &lt;code&gt;base_path&lt;/code&gt;, &lt;code&gt;base_url&lt;/code&gt;, &lt;code&gt;feed_head&lt;/code&gt;, &lt;code&gt;favicon&lt;/code&gt;, &lt;code&gt;analytics_head&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;As above. (Listing pages carry no prose, so no &lt;code&gt;glossary_head&lt;/code&gt;.)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;heading&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Page heading — the site title on the home page, or &lt;code&gt;Tagged “&amp;lt;name&amp;gt;”&lt;/code&gt; on a tag page.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;tagline&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The site&#39;s optional &lt;code&gt;tagline:&lt;/code&gt;, for a hero lede under the title. Empty when unset — guard with &lt;code&gt;{% if tagline %}&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;seo_head&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;The listing&#39;s SEO &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; block (canonical, Open Graph/Twitter, JSON-LD). Emit with &lt;code&gt;{{ seo_head|safe }}&lt;/code&gt;.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;feeds&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;List of &lt;code&gt;{label, href}&lt;/code&gt; for subscribe links.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;pages&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;List of posts: &lt;code&gt;{title, url, date, draft, embargoed, embargo_until, image, image_alt, image_style, audio, has_audio, has_attachments, tags, series}&lt;/code&gt;. Prefix &lt;code&gt;url&lt;/code&gt; with &lt;code&gt;base_path&lt;/code&gt;. Use &lt;code&gt;has_audio&lt;/code&gt;/&lt;code&gt;has_attachments&lt;/code&gt; to flag entries with media (see below).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;h2 id=&#34;enhancing-rich-blocks&#34;&gt;Enhancing rich blocks&lt;/h2&gt;&#xA;&lt;p&gt;colophon emits the raw-block markup; &lt;strong&gt;how to enhance it is entirely the theme&#39;s choice&lt;/strong&gt;.&#xA;The &lt;code&gt;default&lt;/code&gt; theme loads vendored libraries from &lt;code&gt;themes/default/vendor/&lt;/code&gt;, gated on the&#xA;&lt;code&gt;has_*&lt;/code&gt; flags so a page only pulls in what it uses:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;{% if has_math %}&#xA;&amp;lt;link rel=&amp;#34;stylesheet&amp;#34; href=&amp;#34;{{ base_path }}vendor/katex/katex.min.css&amp;#34;&amp;gt;&#xA;&amp;lt;script defer src=&amp;#34;{{ base_path }}vendor/katex/katex.min.js&amp;#34;&amp;gt;&amp;lt;/script&amp;gt;&#xA;&amp;lt;script&amp;gt;/* render every .math element with katex */&amp;lt;/script&amp;gt;&#xA;{% endif %}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Your theme is free to do something else with the same markup: load the libraries from a CDN,&#xA;swap in a different highlighter, or — like the &lt;code&gt;minimal&lt;/code&gt; theme — do nothing and let the raw&#xA;text stand. The markup contract (&lt;code&gt;&amp;lt;pre class=&amp;quot;mermaid&amp;quot;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;span class=&amp;quot;math …&amp;quot;&amp;gt;&lt;/code&gt;,&#xA;&lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;code class=&amp;quot;language-…&amp;quot;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;div class=&amp;quot;callout …&amp;quot;&amp;gt;&lt;/code&gt;) does not change.&lt;/p&gt;&#xA;&lt;h2 id=&#34;audio-video--downloads&#34;&gt;Audio, video &amp;amp; downloads&lt;/h2&gt;&#xA;&lt;p&gt;colophon resolves media; the theme decides how it looks. There are four touch-points.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;1. Inline video/audio embeds.&lt;/strong&gt; A body embed pointing at a media file (&lt;code&gt;![](demo.mp4)&lt;/code&gt;,&#xA;&lt;code&gt;![](clip.mp3)&lt;/code&gt;) is rendered for you as &lt;code&gt;&amp;lt;video class=&amp;quot;post-video&amp;quot; controls …&amp;gt;&lt;/code&gt; or&#xA;&lt;code&gt;&amp;lt;audio class=&amp;quot;post-inline-audio&amp;quot; controls …&amp;gt;&lt;/code&gt;. You only need to &lt;strong&gt;style&lt;/strong&gt; them — make them&#xA;responsive in your prose:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-css&#34;&gt;.prose .post-video { display: block; max-width: 100%; height: auto; }&#xA;.prose .post-inline-audio { display: block; width: 100%; }&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;2. The audio reading + player.&lt;/strong&gt; When &lt;code&gt;has_audio&lt;/code&gt; is set, a post has a podcast-style reading&#xA;(recorded &lt;code&gt;audio_file:&lt;/code&gt; or generated TTS). The build emits a shared, dependency-free&#xA;&lt;code&gt;player.js&lt;/code&gt; to the site root whenever any page has audio. Opt in with the markup contract — a&#xA;container marked &lt;code&gt;data-audioplayer&lt;/code&gt; with the source and localised labels, plus the script:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;{% if has_audio %}&#xA;&amp;lt;figure class=&amp;#34;post-audio&amp;#34; data-audioplayer data-src=&amp;#34;{{ audio }}&amp;#34;&#xA;        data-label-play=&amp;#34;{{ audio_play }}&amp;#34; data-label-pause=&amp;#34;{{ audio_pause }}&amp;#34;&amp;gt;&#xA;  &amp;lt;figcaption&amp;gt;{{ audio_listen }}&amp;lt;/figcaption&amp;gt;&#xA;  &amp;lt;audio controls preload=&amp;#34;none&amp;#34; src=&amp;#34;{{ audio }}&amp;#34;&amp;gt;&amp;lt;/audio&amp;gt;   &amp;lt;!-- no-JS fallback --&amp;gt;&#xA;&amp;lt;/figure&amp;gt;&#xA;{% endif %}&#xA;...&#xA;{% if has_audio %}&amp;lt;script defer src=&amp;#34;{{ base_path }}player.js&amp;#34;&amp;gt;&amp;lt;/script&amp;gt;{% endif %}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;player.js&lt;/code&gt; progressively enhances the &lt;code&gt;&amp;lt;figure&amp;gt;&lt;/code&gt; into a play/pause control with a scrubbable&#xA;waveform (a &lt;code&gt;&amp;lt;src&amp;gt;.json&lt;/code&gt; peaks sidecar when present, else peaks decoded from the audio in-browser&#xA;on first play and cached, else live Web Audio, else idle); with JS off the native &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; still&#xA;plays. Style the enhanced parts via &lt;code&gt;.post-audio.ap-ready&lt;/code&gt;,&#xA;&lt;code&gt;.ap-toggle&lt;/code&gt;, &lt;code&gt;.ap-wave&lt;/code&gt;, &lt;code&gt;.ap-time&lt;/code&gt; — see any bundled theme&#39;s CSS.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The &lt;em&gt;content&lt;/em&gt; of a generated reading is shaped by authoring hints — type-aware cues for&#xA;code/diagrams/tables, and &lt;code&gt;&amp;lt;notts&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;tts&amp;gt;&lt;/code&gt; to hide or force text. Those are an author concern,&#xA;documented in &lt;a href=&#34;/start/content/&#34;&gt;Authoring content&lt;/a&gt;; a theme doesn&#39;t handle them.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;3. The downloads block.&lt;/strong&gt; The engine renders the whole Downloads list for you — a no-JS,&#xA;semantic fragment with stable classes. Drop it in wherever you like (the bundled themes put it&#xA;below the author box) and style it with CSS:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;{{ attachments_html|safe }}   {# empty when the post has none, so no guard needed #}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It emits &lt;code&gt;.post-downloads &amp;gt; .downloads-title + .downloads-list &amp;gt; .dl-item &amp;gt; a.dl&lt;/code&gt;, each row with&#xA;&lt;code&gt;.dl-ico&lt;/code&gt; (paperclip), &lt;code&gt;.dl-main&lt;/code&gt; (&lt;code&gt;.dl-label&lt;/code&gt; + optional &lt;code&gt;.dl-desc&lt;/code&gt;) and &lt;code&gt;.dl-meta&lt;/code&gt;&#xA;(&lt;code&gt;.dl-type&lt;/code&gt; badge + &lt;code&gt;.dl-size&lt;/code&gt;). Style those classes to taste.&lt;/p&gt;&#xA;&lt;p&gt;Prefer your own markup? Ignore the fragment and loop the structured &lt;code&gt;attachments&lt;/code&gt; list instead —&#xA;each entry has &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;label&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;type_label&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt;, &lt;code&gt;bytes&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;{% if attachments %}&amp;lt;ul class=&amp;#34;my-downloads&amp;#34;&amp;gt;&#xA;  {% for f in attachments %}&#xA;  &amp;lt;li&amp;gt;&amp;lt;a href=&amp;#34;{{ f.url }}&amp;#34; download&amp;gt;{{ f.label }}&amp;lt;/a&amp;gt;&#xA;      {% if f.description %}&amp;lt;p&amp;gt;{{ f.description }}&amp;lt;/p&amp;gt;{% endif %}&#xA;      &amp;lt;span&amp;gt;{{ f.type_label }} · {{ f.size }}&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&#xA;  {% endfor %}&#xA;&amp;lt;/ul&amp;gt;{% endif %}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;4. Listing markers.&lt;/strong&gt; On &lt;code&gt;index.html&lt;/code&gt;, each &lt;code&gt;pages&lt;/code&gt; entry carries &lt;code&gt;has_audio&lt;/code&gt; and&#xA;&lt;code&gt;has_attachments&lt;/code&gt;. The press and contrib themes show a small inert speaker / paperclip mark in&#xA;the row&#39;s top-right corner so readers can spot posts with media at a glance — copy that pattern,&#xA;or surface it however suits your design:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;{% if p.has_audio or p.has_attachments %}&amp;lt;span class=&amp;#34;post-flags&amp;#34;&amp;gt;&#xA;  {% if p.has_audio %}&amp;lt;span class=&amp;#34;pf&amp;#34; aria-label=&amp;#34;Has an audio reading&amp;#34;&amp;gt;…speaker svg…&amp;lt;/span&amp;gt;{% endif %}&#xA;  {% if p.has_attachments %}&amp;lt;span class=&amp;#34;pf&amp;#34; aria-label=&amp;#34;Has downloadable files&amp;#34;&amp;gt;…paperclip svg…&amp;lt;/span&amp;gt;{% endif %}&#xA;&amp;lt;/span&amp;gt;{% endif %}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;microformats2-indieweb&#34;&gt;Microformats2 (IndieWeb)&lt;/h2&gt;&#xA;&lt;p&gt;The bundled themes annotate posts with &lt;a href=&#34;https://microformats.org/wiki/microformats2&#34;&gt;microformats2&lt;/a&gt;&#xA;— invisible class names that let other software (IndieWeb readers, Webmention senders, Bridgy)&#xA;understand your content. It&#39;s pure markup: no JS, no config (the old &lt;code&gt;microformats&lt;/code&gt; toggle was&#xA;removed — it&#39;s always on). If you write your own theme, mirror the pattern so it stays parseable:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Post page: the post container is &lt;code&gt;h-entry&lt;/code&gt;, with &lt;code&gt;p-name&lt;/code&gt; (title), &lt;code&gt;dt-published&lt;/code&gt; (the &lt;code&gt;&amp;lt;time&amp;gt;&lt;/code&gt;),&#xA;&lt;code&gt;e-content&lt;/code&gt; (the rendered body), a &lt;code&gt;u-url&lt;/code&gt; permalink (&lt;code&gt;{{ permalink }}&lt;/code&gt; is the absolute URL), and a&#xA;nested &lt;code&gt;p-author h-card&lt;/code&gt; (&lt;code&gt;p-name&lt;/code&gt; + &lt;code&gt;u-url&lt;/code&gt; on the author, &lt;code&gt;u-photo&lt;/code&gt; on the avatar).&lt;/li&gt;&#xA;&lt;li&gt;Listing/index: the list is &lt;code&gt;h-feed&lt;/code&gt;, each entry &lt;code&gt;h-entry&lt;/code&gt;, the title link &lt;code&gt;u-url p-name&lt;/code&gt;, the date&#xA;&lt;code&gt;dt-published&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Identity: author profile links carry &lt;code&gt;rel=&amp;quot;me&amp;quot;&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;When a theme splits the title/byline away from the body (e.g. a hero block), carry the stray&#xA;properties into the &lt;code&gt;h-entry&lt;/code&gt; root as hidden elements (&lt;code&gt;&amp;lt;data class=&amp;quot;p-name&amp;quot; value=&amp;quot;…&amp;quot;&amp;gt;&lt;/code&gt;, a hidden&#xA;&lt;code&gt;&amp;lt;time class=&amp;quot;dt-published&amp;quot;&amp;gt;&lt;/code&gt;) — see the &lt;code&gt;signal&lt;/code&gt;/&lt;code&gt;obsidian&lt;/code&gt; contrib themes.&lt;/p&gt;&#xA;&lt;h2 id=&#34;webmentions-responses&#34;&gt;Webmentions (responses)&lt;/h2&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Shipped. All bundled + contrib themes carry the responses block; configure&#xA;&lt;code&gt;federation.indieweb.webmention.display.mode&lt;/code&gt; (and run &lt;code&gt;colophon webmention fetch&lt;/code&gt;) to populate it.&#xA;See &lt;a href=&#34;/guides/webmentions/&#34;&gt;Show webmentions&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Webmentions are replies/likes/reposts from other sites, shown under a post. &lt;strong&gt;The engine never&#xA;decides how they render — it only exposes the data&lt;/strong&gt;, and the site picks a &lt;code&gt;display.mode&lt;/code&gt;&#xA;(&lt;code&gt;federation.indieweb.webmention.display.mode&lt;/code&gt;). What the engine populates depends on that mode:&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Variable&lt;/th&gt;&#xA;&lt;th&gt;&lt;code&gt;live&lt;/code&gt;&lt;/th&gt;&#xA;&lt;th&gt;&lt;code&gt;asset&lt;/code&gt;&lt;/th&gt;&#xA;&lt;th&gt;&lt;code&gt;disabled&lt;/code&gt;&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;mentions_enabled&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;true&lt;/code&gt; (unless the post sets &lt;code&gt;webmentions: false&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;true&lt;/code&gt; (unless opted out)&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;&lt;code&gt;false&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;has_mentions&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;false&lt;/code&gt; — &lt;strong&gt;not known at build&lt;/strong&gt; (JS fills the count)&lt;/td&gt;&#xA;&lt;td&gt;accurate, from the synced list&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;mentions&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;empty — no build-time data&lt;/td&gt;&#xA;&lt;td&gt;structured list (bake your own)&lt;/td&gt;&#xA;&lt;td&gt;empty&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;mentions_html&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;empty&lt;/td&gt;&#xA;&lt;td&gt;engine-rendered drop-in block&lt;/td&gt;&#xA;&lt;td&gt;empty&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;mentions_src&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;the receiver&#39;s client-fetch endpoint&lt;/td&gt;&#xA;&lt;td&gt;your published &lt;code&gt;_mentions/&amp;lt;post&amp;gt;.json&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;unset&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;live&lt;/code&gt;&lt;/strong&gt; — the browser fetches the receiver directly; most realtime, no rebuild, but no&#xA;build-time data (so no count, no baking) and nothing for no-JS readers.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;asset&lt;/code&gt;&lt;/strong&gt; — the browser fetches &lt;em&gt;your&lt;/em&gt; curated &lt;code&gt;_mentions/&lt;/code&gt; asset (refreshed out-of-band by&#xA;&lt;code&gt;webmention publish&lt;/code&gt;); because that list also exists at build, the engine fills &lt;code&gt;mentions&lt;/code&gt; /&#xA;&lt;code&gt;mentions_html&lt;/code&gt; / &lt;code&gt;has_mentions&lt;/code&gt;, so a theme &lt;strong&gt;may bake&lt;/strong&gt; instead of relying on JS.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;disabled&lt;/code&gt;&lt;/strong&gt; — nothing is exposed or shipped, regardless of the per-post setting.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Always guard the block on &lt;code&gt;mentions_enabled&lt;/code&gt; and keep it a &lt;strong&gt;sibling of the content element&lt;/strong&gt;, never&#xA;inside it (so it&#39;s never read aloud by the TTS reading — same rule as the author card/downloads).&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Progressive-enhancement block (what &lt;code&gt;press&lt;/code&gt; uses).&lt;/strong&gt; Bake the asset-mode HTML server-side, then let&#xA;the engine-emitted &lt;code&gt;mentions.js&lt;/code&gt; refresh it (or, in &lt;code&gt;live&lt;/code&gt; mode, populate it):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-django&#34;&gt;{% if mentions_enabled %}&#xA;&amp;lt;section class=&amp;#34;responses&amp;#34; {{ mentions_attrs|safe }} aria-label=&amp;#34;Responses&amp;#34;&amp;gt;{{ mentions_html|safe }}&amp;lt;/section&amp;gt;&#xA;{% endif %}&#xA;…&#xA;{% if mentions_enabled %}&amp;lt;script defer src=&amp;#34;{{ base_path }}mentions.js&amp;#34;&amp;gt;&amp;lt;/script&amp;gt;{% endif %}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;mentions_attrs&lt;/code&gt; is the engine-owned &lt;code&gt;data-mentions*&lt;/code&gt; wiring (the source URL, plus &lt;code&gt;data-mentions-live&lt;/code&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;target + blocklist in &lt;code&gt;live&lt;/code&gt; mode) — drop it in and the same markup works across modes and reader&#xA;drivers. With JS off (and &lt;code&gt;asset&lt;/code&gt; mode) the baked &lt;code&gt;mentions_html&lt;/code&gt; still shows; with JS on, &lt;code&gt;mentions.js&lt;/code&gt;&#xA;fetches and renders/refreshes.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;Build your own from the structured list&lt;/strong&gt; (&lt;code&gt;asset&lt;/code&gt; mode), e.g. to restyle or split by type:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-django&#34;&gt;{% if has_mentions %}&amp;lt;section class=&amp;#34;responses&amp;#34;&amp;gt;&#xA;  {% for m in mentions %}&#xA;  &amp;lt;article class=&amp;#34;response {{ m.type }} h-cite&amp;#34;&amp;gt;&#xA;    {% if m.author.photo %}&amp;lt;img class=&amp;#34;u-photo&amp;#34; src=&amp;#34;{{ m.author.photo }}&amp;#34; alt=&amp;#34;&amp;#34;&amp;gt;{% endif %}&#xA;    &amp;lt;a class=&amp;#34;p-author h-card u-url&amp;#34; href=&amp;#34;{{ m.author.url }}&amp;#34;&amp;gt;{{ m.author.name }}&amp;lt;/a&amp;gt;&#xA;    {% if m.content %}&amp;lt;div class=&amp;#34;p-content&amp;#34;&amp;gt;{{ m.content }}&amp;lt;/div&amp;gt;{% endif %}&#xA;    &amp;lt;a class=&amp;#34;u-url&amp;#34; href=&amp;#34;{{ m.url }}&amp;#34;&amp;gt;&amp;lt;time class=&amp;#34;dt-published&amp;#34;&amp;gt;{{ m.published }}&amp;lt;/time&amp;gt;&amp;lt;/a&amp;gt;&#xA;  &amp;lt;/article&amp;gt;&#xA;  {% endfor %}&#xA;&amp;lt;/section&amp;gt;{% endif %}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Each &lt;code&gt;mentions&lt;/code&gt; item is &lt;code&gt;{type (like|repost|reply|mention), author{name,url,photo}, url, content, published}&lt;/code&gt;. A &lt;strong&gt;no-JS / text theme&lt;/strong&gt; (e.g. &lt;code&gt;minimal&lt;/code&gt;) just uses &lt;code&gt;asset&lt;/code&gt; mode + &lt;code&gt;{{ mentions_html|safe }}&lt;/code&gt;&#xA;and skips the script entirely.&lt;/p&gt;&#xA;&lt;h3 id=&#34;silo-icons&#34;&gt;Silo icons&lt;/h3&gt;&#xA;&lt;p&gt;Responses, the &amp;quot;Also posted on…&amp;quot; (&lt;code&gt;u-syndication&lt;/code&gt;) links, and the author h-card links show a small&#xA;&lt;strong&gt;brand icon&lt;/strong&gt; when colophon recognises the source silo (Bluesky, Mastodon, GitHub, X, Reddit,&#xA;Hacker News, Threads, Flickr, LinkedIn, Tumblr, GitLab — else a generic website globe). These are&#xA;glyphs in a tiny webfont, &lt;strong&gt;&lt;code&gt;silos.woff2&lt;/code&gt;&lt;/strong&gt;, that the engine emits at the site root (when responses&#xA;are active) and renders into &lt;code&gt;&amp;lt;span class=&amp;quot;silo&amp;quot;&amp;gt;&lt;/code&gt;. A theme just declares the face and styles the&#xA;span:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-css&#34;&gt;@font-face { font-family: &amp;#34;Colophon Silos&amp;#34;; src: url(&amp;#34;silos.woff2&amp;#34;) format(&amp;#34;woff2&amp;#34;); font-display: swap; }&#xA;.silo { font-family: &amp;#34;Colophon Silos&amp;#34;; line-height: 1; }&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The font is curated from Font Awesome (Brands + Solid) by&#xA;&lt;a href=&#34;../contrib/scripts/silo-font/&#34;&gt;&lt;code&gt;contrib/scripts/silo-font&lt;/code&gt;&lt;/a&gt; — re-run it to add a network or change&#xA;the set. Detection (&lt;code&gt;host → silo&lt;/code&gt;) lives in &lt;code&gt;internal/build/mentions.go&lt;/code&gt; + &lt;code&gt;assets/mentions.js&lt;/code&gt;, kept&#xA;in sync with the font&#39;s &lt;code&gt;silos.json&lt;/code&gt; codepoints.&lt;/p&gt;&#xA;&lt;h2 id=&#34;analytics&#34;&gt;Analytics&lt;/h2&gt;&#xA;&lt;p&gt;colophon owns the analytics clients; a theme&#39;s only job is to &lt;strong&gt;include&lt;/strong&gt; them. When a site&#xA;configures a provider (statsfactory and/or Google Analytics — see &lt;a href=&#34;/guides/analytics/&#34;&gt;analytics&lt;/a&gt;),&#xA;the build writes that provider&#39;s loader to the site root — &lt;code&gt;analytics-sf.js&lt;/code&gt; for the cookieless&#xA;statsfactory beacon, &lt;code&gt;analytics-ga.js&lt;/code&gt; for the Google Analytics loader — and exposes the&#xA;matching &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; markup (with each page&#39;s dimensions) as the &lt;code&gt;analytics_head&lt;/code&gt; variable.&lt;/p&gt;&#xA;&lt;p&gt;A theme leverages both providers with one line, just before &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;{% if analytics_head %}{{ analytics_head|safe }}{% endif %}&#xA;&amp;lt;/body&amp;gt;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The theme never names a provider: &lt;code&gt;analytics_head&lt;/code&gt; already contains whichever loaders are&#xA;enabled (statsfactory, GA, both, or — when the site configures none — nothing, leaving the line&#xA;inert). Every built-in and contrib theme includes it; a JS-enabled custom theme should too.&lt;/p&gt;&#xA;&lt;h2 id=&#34;glossary&#34;&gt;Glossary&lt;/h2&gt;&#xA;&lt;p&gt;If the site ships a &lt;code&gt;glossary.yaml&lt;/code&gt; (see &lt;a href=&#34;/start/content/#glossary&#34;&gt;authoring&lt;/a&gt;) and a page uses a&#xA;term, the build publishes the data + a small decorator and exposes a &lt;code&gt;glossary_head&lt;/code&gt; variable.&#xA;A theme opts in with one line before &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; (right where the analytics line goes):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;{% if glossary_head %}{{ glossary_head|safe }}{% endif %}&#xA;&amp;lt;/body&amp;gt;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That&#39;s it — no per-theme CSS or JS needed:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;The &lt;strong&gt;decorator and styles are engine-provided&lt;/strong&gt; (&lt;code&gt;glossary.js&lt;/code&gt; + &lt;code&gt;glossary.css&lt;/code&gt;, written to&#xA;the site root). The decorator wraps terms in &lt;code&gt;&amp;lt;abbr class=&amp;quot;gloss&amp;quot; data-gloss=&amp;quot;…&amp;quot;&amp;gt;&lt;/code&gt; and shows&#xA;an accessible pop-over on hover/focus (&lt;code&gt;role=&amp;quot;tooltip&amp;quot;&lt;/code&gt; + &lt;code&gt;aria-describedby&lt;/code&gt;, Escape to&#xA;dismiss).&lt;/li&gt;&#xA;&lt;li&gt;The default styling &lt;strong&gt;adapts to your theme&lt;/strong&gt; through the token contract: it uses&#xA;&lt;code&gt;--accent&lt;/code&gt; for the underline and &lt;code&gt;--elevated&lt;/code&gt;/&lt;code&gt;--text&lt;/code&gt;/&lt;code&gt;--muted&lt;/code&gt;/&lt;code&gt;--border&lt;/code&gt; + &lt;code&gt;--serif&lt;/code&gt;/&lt;code&gt;--sans&lt;/code&gt;&#xA;for the dictionary-stanza card (with neutral fallbacks for token-less themes).&lt;/li&gt;&#xA;&lt;li&gt;To customise the look, &lt;strong&gt;override &lt;code&gt;.gloss&lt;/code&gt; and &lt;code&gt;.gloss-tip&lt;/code&gt;&lt;/strong&gt; (and &lt;code&gt;.gloss-tip .gloss-term&lt;/code&gt;&#xA;/ &lt;code&gt;.gloss-def&lt;/code&gt;) in your own stylesheet — e.g. &lt;code&gt;press&lt;/code&gt; gives &lt;code&gt;.gloss&lt;/code&gt; a light wavy underline.&lt;/li&gt;&#xA;&lt;li&gt;A &lt;strong&gt;text-only / no-JS theme&lt;/strong&gt; simply omits the line: terms stay plain readable text, so the&#xA;page still works correctly without the decorator (this is what &lt;code&gt;minimal&lt;/code&gt; does).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;build-a-theme--step-by-step&#34;&gt;Build a theme — step by step&lt;/h2&gt;&#xA;&lt;p&gt;The fastest route is to start from a built-in and change only what you want. A new theme&#xA;inherits &lt;code&gt;default&lt;/code&gt;, so you can ship as little as one CSS file.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;1. Create the theme directory&lt;/strong&gt; in your project and point a build at it:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;mkdir -p themes/mytheme&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# colophon.yaml&#xA;sites:&#xA;  - id: main&#xA;    theme: mytheme          # or set it on one environment to preview first&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;2. Add a stylesheet.&lt;/strong&gt; With nothing else present, &lt;code&gt;mytheme&lt;/code&gt; uses the &lt;code&gt;default&lt;/code&gt; templates&#xA;and your CSS:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-css&#34;&gt;/* themes/mytheme/style.css */&#xA;body { font-family: Georgia, serif; max-width: 42rem; margin: 2rem auto; }&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon serve            # open the printed URL; edits live-reload&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That alone is a working theme. Everything below is optional, added when you want more control.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;3. Take over the post template.&lt;/strong&gt; Copy a built-in as a starting point, then edit it:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon themes eject default   # writes themes/default/ — copy what you need into mytheme/&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A minimal &lt;code&gt;themes/mytheme/page.html&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;&amp;lt;!doctype html&amp;gt;&#xA;&amp;lt;html lang=&amp;#34;{{ lang }}&amp;#34;&amp;gt;&#xA;&amp;lt;head&amp;gt;&#xA;  &amp;lt;meta charset=&amp;#34;utf-8&amp;#34;&amp;gt;&#xA;  &amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1&amp;#34;&amp;gt;&#xA;  &amp;lt;title&amp;gt;{{ meta_title }}&amp;lt;/title&amp;gt;&#xA;  &amp;lt;link rel=&amp;#34;stylesheet&amp;#34; href=&amp;#34;{{ base_path }}style.css&amp;#34;&amp;gt;&#xA;  {{ feed_head|safe }}&#xA;  {{ seo_head|safe }}&#xA;&amp;lt;/head&amp;gt;&#xA;&amp;lt;body&amp;gt;&#xA;  &amp;lt;header&amp;gt;&amp;lt;a href=&amp;#34;{{ base_path }}&amp;#34;&amp;gt;{{ site_title }}&amp;lt;/a&amp;gt;&amp;lt;/header&amp;gt;&#xA;  &amp;lt;article&amp;gt;&#xA;    &amp;lt;h1&amp;gt;{{ title }}&amp;lt;/h1&amp;gt;&#xA;    {% if date %}&amp;lt;time&amp;gt;{{ date }}&amp;lt;/time&amp;gt;{% endif %}&#xA;    {% if read_time %}&amp;lt;span&amp;gt;· {{ read_time }} min read&amp;lt;/span&amp;gt;{% endif %}&#xA;    {{ content|safe }}&#xA;    {% if tags %}&amp;lt;footer&amp;gt;{% for t in tags %}&amp;lt;a href=&amp;#34;{{ t.url }}&amp;#34;&amp;gt;{{ t.name }}&amp;lt;/a&amp;gt; {% endfor %}&amp;lt;/footer&amp;gt;{% endif %}&#xA;  &amp;lt;/article&amp;gt;&#xA;&amp;lt;/body&amp;gt;&#xA;&amp;lt;/html&amp;gt;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;4. Add the index&lt;/strong&gt; (&lt;code&gt;themes/mytheme/index.html&lt;/code&gt;) — the post list, the nav menu (standing&#xA;pages like About), and per-tag pages:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;&amp;lt;!doctype html&amp;gt;&#xA;&amp;lt;html lang=&amp;#34;{{ lang }}&amp;#34;&amp;gt;&#xA;&amp;lt;head&amp;gt;&amp;lt;meta charset=&amp;#34;utf-8&amp;#34;&amp;gt;&amp;lt;title&amp;gt;{{ heading }}&amp;lt;/title&amp;gt;&#xA;  &amp;lt;link rel=&amp;#34;stylesheet&amp;#34; href=&amp;#34;{{ base_path }}style.css&amp;#34;&amp;gt;{{ feed_head|safe }}&amp;lt;/head&amp;gt;&#xA;&amp;lt;body&amp;gt;&#xA;  {% if nav_pages %}&amp;lt;nav&amp;gt;{% for n in nav_pages %}&amp;lt;a href=&amp;#34;{{ n.url }}&amp;#34;&amp;gt;{{ n.title }}&amp;lt;/a&amp;gt; {% endfor %}&amp;lt;/nav&amp;gt;{% endif %}&#xA;  &amp;lt;h1&amp;gt;{{ heading }}&amp;lt;/h1&amp;gt;&#xA;  &amp;lt;ul&amp;gt;&#xA;  {% for p in pages %}&#xA;    &amp;lt;li&amp;gt;&amp;lt;a href=&amp;#34;{{ base_path }}{{ p.url }}&amp;#34;&amp;gt;{{ p.title }}&amp;lt;/a&amp;gt;&#xA;        {% if p.date %}&amp;lt;small&amp;gt;{{ p.date }}&amp;lt;/small&amp;gt;{% endif %}&amp;lt;/li&amp;gt;&#xA;  {% endfor %}&#xA;  &amp;lt;/ul&amp;gt;&#xA;&amp;lt;/body&amp;gt;&#xA;&amp;lt;/html&amp;gt;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;nav_pages&lt;/code&gt; is the list of standing pages (&lt;code&gt;{title, url}&lt;/code&gt;); &lt;code&gt;pages&lt;/code&gt; is the chronological posts.&#xA;The build sorts entries into these two buckets by &lt;a href=&#34;#page-types&#34;&gt;page type&lt;/a&gt; — you just render&#xA;them. (Add the same &lt;code&gt;nav_pages&lt;/code&gt; block to &lt;code&gt;page.html&lt;/code&gt; so the menu appears on entries too.)&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;5. (Optional) Tailor specific page types.&lt;/strong&gt; Add a &lt;code&gt;&amp;lt;type&amp;gt;.html&lt;/code&gt; template, or branch on the&#xA;&lt;code&gt;page_type&lt;/code&gt; variable inside &lt;code&gt;page.html&lt;/code&gt;, to give a type its own look. See&#xA;&lt;a href=&#34;#page-types&#34;&gt;Page types&lt;/a&gt;. Skip this and every entry just uses &lt;code&gt;page.html&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;6. Decide how rich blocks render.&lt;/strong&gt; Do nothing (raw text shows, like &lt;code&gt;minimal&lt;/code&gt;), or enhance&#xA;them with the &lt;code&gt;has_*&lt;/code&gt; gates as shown in &lt;a href=&#34;#enhancing-rich-blocks&#34;&gt;Enhancing rich blocks&lt;/a&gt;. The&#xA;vendored libraries are inherited from &lt;code&gt;default&lt;/code&gt;, so &lt;code&gt;{{ base_path }}vendor/katex/…&lt;/code&gt; resolves&#xA;with no extra files in your theme.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;7. Add your own assets.&lt;/strong&gt; Any non-&lt;code&gt;.html&lt;/code&gt; file under &lt;code&gt;themes/mytheme/&lt;/code&gt; is copied to the&#xA;output root, keeping its path: &lt;code&gt;themes/mytheme/logo.svg&lt;/code&gt; → &lt;code&gt;/logo.svg&lt;/code&gt;, referenced as&#xA;&lt;code&gt;{{ base_path }}logo.svg&lt;/code&gt;. Self-host fonts the same way and &lt;code&gt;@import&lt;/code&gt; them from your CSS.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;8. Build and ship:&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon build --env production    # writes public/ with your theme&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Checklist: every internal &lt;code&gt;href&lt;/code&gt;/&lt;code&gt;src&lt;/code&gt; starts with &lt;code&gt;{{ base_path }}&lt;/code&gt;; &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;feed_head&lt;/code&gt;&#xA;and &lt;code&gt;seo_head&lt;/code&gt; use &lt;code&gt;|safe&lt;/code&gt;; &lt;code&gt;{# comments #}&lt;/code&gt; stay on one line. To contribute a theme back, drop&#xA;it in &lt;a href=&#34;#community-themes-contribthemes&#34;&gt;&lt;code&gt;contrib/themes/&lt;/code&gt;&lt;/a&gt; following the existing ones.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/themes.md&#34;&gt;&lt;code&gt;docs/themes.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>SEO &amp; social metadata</title>
    <id>https://docs.colophon.blog/guides/seo/</id>
    <link href="https://docs.colophon.blog/guides/seo/" rel="alternate"></link>
    <updated>2001-12-26T00:00:00Z</updated>
    <published>2001-12-26T00:00:00Z</published>
    <summary type="text">colophon gives every post correct search and social metadata by default — canonical URL, description, Open Graph + Twitter cards, schema.org JSON-LD, and robots — derived from…</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/seo.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;colophon gives every post correct search and social metadata &lt;strong&gt;by default&lt;/strong&gt; — canonical URL,&#xA;description, Open Graph + Twitter cards, schema.org JSON-LD, and robots — derived from the&#xA;post&#39;s existing fields (title, description, tags, date, image, persona). An optional &lt;code&gt;seo:&lt;/code&gt;&#xA;frontmatter block overrides any of it, and is the precise target an&#xA;&lt;a href=&#34;/guides/skills/&#34;&gt;authoring skill&lt;/a&gt; can fill in.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-seo-block&#34;&gt;The &lt;code&gt;seo:&lt;/code&gt; block&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;seo:&#xA;  title:        # &amp;lt;title&amp;gt; / og:title; ≤60 chars (else: the post title)&#xA;  description:  # meta description; 140–160 chars (else: description / excerpt)&#xA;  keywords: []  # focus terms (else: the post&amp;#39;s tags)&#xA;  canonical:    # absolute URL override (else: base_url + slug)&#xA;  noindex: false# robots noindex (drafts &amp;amp; embargoed posts are noindex regardless)&#xA;  image:        # absolute social-image URL override (else: the `image` field)&#xA;  type:         # schema.org @type (default: BlogPosting)&#xA;  social:       # copy tuned for sharing, when it should differ from the search copy&#xA;    title:&#xA;    description:&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Every field maps to exactly one piece of output, so what you set is what gets rendered.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-gets-emitted-page-head&#34;&gt;What gets emitted (page &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;)&lt;/h2&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Output&lt;/th&gt;&#xA;&lt;th&gt;Source&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;link rel=&amp;quot;canonical&amp;quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;seo.canonical&lt;/code&gt; → &lt;code&gt;base_url&lt;/code&gt; + slug&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;meta name=&amp;quot;description&amp;quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;seo.description&lt;/code&gt; → &lt;code&gt;description&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;meta name=&amp;quot;robots&amp;quot;&amp;gt;&lt;/code&gt; noindex&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;seo.noindex&lt;/code&gt;, or any draft/embargoed post&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;meta name=&amp;quot;keywords&amp;quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;seo.keywords&lt;/code&gt; → tags&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Open Graph (&lt;code&gt;og:type/site_name/url/title/description/image/locale&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;seo&lt;/code&gt; → post fields; &lt;code&gt;og:title&lt;/code&gt;/&lt;code&gt;description&lt;/code&gt; prefer &lt;code&gt;seo.social.*&lt;/code&gt;; &lt;code&gt;og:image&lt;/code&gt; is &lt;code&gt;seo.image&lt;/code&gt; → the &lt;code&gt;image&lt;/code&gt; field → the &lt;code&gt;hero&lt;/code&gt; cover art; &lt;code&gt;og:locale&lt;/code&gt; from the page/site &lt;code&gt;lang&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;article:published_time&lt;/code&gt; / &lt;code&gt;modified_time&lt;/code&gt; / &lt;code&gt;tag&lt;/code&gt; / &lt;code&gt;author&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;the post date, tags, and persona&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Twitter card (&lt;code&gt;summary_large_image&lt;/code&gt; when an image exists, else &lt;code&gt;summary&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;td&gt;the resolved image&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;&amp;lt;script type=&amp;quot;application/ld+json&amp;quot;&amp;gt;&lt;/code&gt; &lt;strong&gt;BlogPosting&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;headline, description, image, dates, keywords, &lt;code&gt;author&lt;/code&gt; (persona → Person/Organization), &lt;code&gt;publisher&lt;/code&gt; (site)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;The JSON-LD author comes from the post&#39;s &lt;strong&gt;persona&lt;/strong&gt; (&lt;code&gt;persona:&lt;/code&gt; frontmatter, else the first&#xA;configured persona): an &lt;code&gt;individual&lt;/code&gt; persona renders as a &lt;code&gt;Person&lt;/code&gt;, a &lt;code&gt;brand&lt;/code&gt; persona as an&#xA;&lt;code&gt;Organization&lt;/code&gt;, with the persona&#39;s first h-card URL as &lt;code&gt;author.url&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;defaults-vs-overrides&#34;&gt;Defaults vs. overrides&lt;/h2&gt;&#xA;&lt;p&gt;You never need an &lt;code&gt;seo:&lt;/code&gt; block — a plain post already produces all of the above from its&#xA;title/description/tags/date/image/persona. Use &lt;code&gt;seo:&lt;/code&gt; to:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;give search a tighter &lt;code&gt;title&lt;/code&gt;/&lt;code&gt;description&lt;/code&gt; than the on-page ones,&lt;/li&gt;&#xA;&lt;li&gt;write punchier &lt;code&gt;social:&lt;/code&gt; copy for shares,&lt;/li&gt;&#xA;&lt;li&gt;set an explicit &lt;code&gt;canonical&lt;/code&gt; (e.g. a syndicated original),&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;noindex&lt;/code&gt; a page, or point &lt;code&gt;image&lt;/code&gt; at an external social card.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Generating a good &lt;code&gt;seo:&lt;/code&gt; block from the article is what the planned &lt;strong&gt;seo skill&lt;/strong&gt;&#xA;(&lt;a href=&#34;/guides/skills/&#34;&gt;skills.md&lt;/a&gt;) does.&lt;/p&gt;&#xA;&lt;h2 id=&#34;listing-pages-home-tags-authors&#34;&gt;Listing pages (home, tags, authors)&lt;/h2&gt;&#xA;&lt;p&gt;The home page and every generated listing — &lt;code&gt;/tags/&amp;lt;tag&amp;gt;/&lt;/code&gt; and &lt;code&gt;/authors/&amp;lt;id&amp;gt;/&lt;/code&gt; — also carry&#xA;their own metadata: a canonical URL, &lt;code&gt;description&lt;/code&gt;, website-flavoured Open Graph + Twitter&#xA;cards, &lt;code&gt;og:locale&lt;/code&gt;, and schema.org JSON-LD (a &lt;code&gt;Blog&lt;/code&gt; for the home page, a &lt;code&gt;CollectionPage&lt;/code&gt; for&#xA;tag/author listings). These draw on two optional site-level fields:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    title: My Blog&#xA;    base_url: https://example.com&#xA;    description: One line that becomes the home page&amp;#39;s meta/OG description.&#xA;    image: /assets/social.png   # default share image (absolute URL, or resolved against base_url)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Both are optional — unset simply omits the corresponding tags. &lt;code&gt;description&lt;/code&gt; feeds the listing&#xA;pages&#39; &lt;code&gt;&amp;lt;meta name=&amp;quot;description&amp;quot;&amp;gt;&lt;/code&gt; and the JSON-LD; &lt;code&gt;image&lt;/code&gt; is their default &lt;code&gt;og:image&lt;/code&gt;/&#xA;&lt;code&gt;twitter:image&lt;/code&gt;. Per-tag and per-author listings reuse the same &lt;code&gt;description&lt;/code&gt;/&lt;code&gt;image&lt;/code&gt; with their&#xA;own heading as the title.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/seo.md&#34;&gt;&lt;code&gt;docs/seo.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Image &amp; audio generation</title>
    <id>https://docs.colophon.blog/guides/image-generation/</id>
    <link href="https://docs.colophon.blog/guides/image-generation/" rel="alternate"></link>
    <updated>2001-12-25T00:00:00Z</updated>
    <published>2001-12-25T00:00:00Z</published>
    <summary type="text">colophon can generate images from a text prompt with an AI provider, anywhere it accepts an image: a post&#39;s hero:/image: frontmatter and Markdown body embeds. A generated image is…</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/image-generation.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;colophon can generate images from a text prompt with an AI provider, anywhere it&#xA;accepts an image: a post&#39;s &lt;code&gt;hero:&lt;/code&gt;/&lt;code&gt;image:&lt;/code&gt; frontmatter and Markdown body embeds. A&#xA;generated image is cached on disk (and committed with your content), so it is produced&#xA;once and then ships through the normal &lt;a href=&#34;/start/content/#images-and-object-storage&#34;&gt;asset pipeline&lt;/a&gt;&#xA;like any other image — including routing to an object store.&lt;/p&gt;&#xA;&lt;p&gt;The feature is &lt;strong&gt;opt-in&lt;/strong&gt; and off until you configure a provider.&lt;/p&gt;&#xA;&lt;h2 id=&#34;writing-a-gen-reference&#34;&gt;Writing a &lt;code&gt;gen:&lt;/code&gt; reference&lt;/h2&gt;&#xA;&lt;p&gt;Anywhere you&#39;d put an image path, write &lt;code&gt;gen:&lt;/code&gt; followed by the prompt.&lt;/p&gt;&#xA;&lt;p&gt;In frontmatter:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;---&#xA;title: On Patience&#xA;hero: &amp;#34;gen:a weathered fisherman waiting by a still lake at dawn, photoreal&amp;#34;&#xA;hero_alt: &amp;#34;A fisherman waiting by a calm lake at first light&amp;#34;&#xA;image: &amp;#34;gen:a minimalist book cover, muted teal palette&amp;#34;&#xA;---&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;In the body — &lt;strong&gt;wrap the prompt in &lt;code&gt;&amp;lt;…&amp;gt;&lt;/code&gt; whenever it contains spaces&lt;/strong&gt; (plain Markdown&#xA;image syntax stops at the first space):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-markdown&#34;&gt;![A fisherman at dawn](&amp;lt;gen:a weathered fisherman by a still lake at dawn, photoreal&amp;gt;)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The alt text works exactly as for any image — write it for meaning, leave it empty for&#xA;purely decorative banners.&lt;/p&gt;&#xA;&lt;h3 id=&#34;tuning-a-single-image&#34;&gt;Tuning a single image&lt;/h3&gt;&#xA;&lt;p&gt;Append query-style parameters to the prompt:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-markdown&#34;&gt;![cover](&amp;lt;gen:a quiet mountain road in fog?aspect=16:9&amp;gt;)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;aspect&lt;/code&gt; (e.g. &lt;code&gt;1:1&lt;/code&gt;, &lt;code&gt;16:9&lt;/code&gt;, &lt;code&gt;9:16&lt;/code&gt;) is the common one; available parameters depend on&#xA;the provider. Per-reference parameters override the &lt;code&gt;defaults:&lt;/code&gt; in your config.&lt;/p&gt;&#xA;&lt;h3 id=&#34;reuse&#34;&gt;Reuse&lt;/h3&gt;&#xA;&lt;p&gt;Two references with the &lt;strong&gt;same prompt, model and parameters&lt;/strong&gt; resolve to the &lt;strong&gt;same&lt;/strong&gt;&#xA;generated file — so a hero and an in-body embed sharing one prompt are generated once.&#xA;Change the prompt, the model, or a parameter and you get a new image.&lt;/p&gt;&#xA;&lt;h2 id=&#34;configuration&#34;&gt;Configuration&lt;/h2&gt;&#xA;&lt;p&gt;Add a &lt;code&gt;generation:&lt;/code&gt; block to &lt;code&gt;colophon.yaml&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;generation:&#xA;  image:&#xA;    provider: minimax            # which service to use (see table below)&#xA;    model: &amp;#34;&amp;#34;                    # optional — defaults per provider&#xA;    defaults:                    # optional — params applied to every prompt&#xA;      aspect: &amp;#34;16:9&amp;#34;&#xA;    output_dir: &amp;#34;&amp;#34;               # optional — default: content/assets/generated&#xA;    concurrency: 5               # optional — max images generated at once&#xA;    system_prompt: &amp;#34;&amp;#34;            # optional — house style; overrides the theme&amp;#39;s (see below)&#xA;    # api_key: &amp;#34;{env:MINIMAX_API_KEY}&amp;#34;   # optional — see &amp;#34;API keys&amp;#34; below&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h3 id=&#34;providers&#34;&gt;Providers&lt;/h3&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;&lt;code&gt;provider&lt;/code&gt;&lt;/th&gt;&#xA;&lt;th&gt;Default model&lt;/th&gt;&#xA;&lt;th&gt;API key (environment variable)&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;google&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;gemini-3.1-flash-image&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;GEMINI_API_KEY&lt;/code&gt; (or &lt;code&gt;GOOGLE_API_KEY&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;minimax&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;image-01&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;MINIMAX_API_KEY&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;openai&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;gpt-image-1&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;OPENAI_API_KEY&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;xai&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;grok-imagine-image-quality&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;XAI_API_KEY&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;together&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;em&gt;(set &lt;code&gt;model:&lt;/code&gt;)&lt;/em&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;TOGETHER_API_KEY&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;deepinfra&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;em&gt;(set &lt;code&gt;model:&lt;/code&gt;)&lt;/em&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;DEEPINFRA_API_KEY&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;custom&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;em&gt;(set &lt;code&gt;model:&lt;/code&gt;)&lt;/em&gt;&lt;/td&gt;&#xA;&lt;td&gt;set &lt;code&gt;api_key:&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;&lt;code&gt;custom&lt;/code&gt; targets any OpenAI-compatible images endpoint — also set &lt;code&gt;base_url:&lt;/code&gt; and&#xA;&lt;code&gt;api_path:&lt;/code&gt; (e.g. &lt;code&gt;/images/generations&lt;/code&gt;).&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;xai&lt;/code&gt; is Grok Imagine: xAI&#39;s OpenAI-compatible images endpoint. Set&#xA;&lt;code&gt;model: grok-imagine-image&lt;/code&gt; for the cheaper tier, and use &lt;code&gt;aspect&lt;/code&gt; as usual (it is&#xA;sent as xAI&#39;s &lt;code&gt;aspect_ratio&lt;/code&gt;):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;generation:&#xA;  image:&#xA;    provider: xai                # Grok Imagine — reads XAI_API_KEY&#xA;    defaults:&#xA;      aspect: &amp;#34;16:9&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h3 id=&#34;api-keys&#34;&gt;API keys&lt;/h3&gt;&#xA;&lt;p&gt;Keys are read from the environment, never required in the file:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;If you set &lt;code&gt;api_key:&lt;/code&gt; (typically &lt;code&gt;&amp;quot;{env:VAR}&amp;quot;&lt;/code&gt;), that value is used.&lt;/li&gt;&#xA;&lt;li&gt;Otherwise colophon reads the provider&#39;s default environment variable from the table.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Either way the secret stays in your shell / &lt;code&gt;.env&lt;/code&gt; / CI secrets, not in &lt;code&gt;colophon.yaml&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;house-style-theme-system-prompt&#34;&gt;House style (theme system prompt)&lt;/h2&gt;&#xA;&lt;p&gt;So you don&#39;t repeat the same look in every prompt, a &lt;strong&gt;house style&lt;/strong&gt; is applied to every&#xA;generated image — describe only the &lt;em&gt;subject&lt;/em&gt; in each post and let the style come from the&#xA;theme. The style is resolved in this order (first wins):&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Per-reference&lt;/strong&gt; &lt;code&gt;?systemprompt=…&lt;/code&gt; (below)&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Site config&lt;/strong&gt; — &lt;code&gt;generation.image.system_prompt&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Theme&lt;/strong&gt; — &lt;code&gt;image.genai.system_prompt&lt;/code&gt; in the theme&#39;s &lt;code&gt;theme.yaml&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Theme&lt;/strong&gt; — the theme&#39;s &lt;code&gt;description&lt;/code&gt; (a gentle fallback)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;A theme sets its style in &lt;code&gt;theme.yaml&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# themes/&amp;lt;name&amp;gt;/theme.yaml&#xA;description: &amp;#34;A clean editorial broadsheet theme.&amp;#34;&#xA;image:&#xA;  genai:&#xA;    system_prompt: &amp;#34;editorial illustration, muted palette, soft depth, no text or lettering&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;So &lt;code&gt;hero: &amp;quot;gen:a lighthouse on a rocky coast&amp;quot;&lt;/code&gt; is generated &lt;em&gt;in the theme&#39;s style&lt;/em&gt; with no&#xA;extra wording in the post.&lt;/p&gt;&#xA;&lt;p&gt;Per image, you can override or switch it off with the reserved &lt;code&gt;systemprompt&lt;/code&gt; parameter:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;?systemprompt=bold woodcut print, two-tone&lt;/code&gt; — use this style instead for this image&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;?systemprompt=none&lt;/code&gt; (or &lt;code&gt;nil&lt;/code&gt;, or empty) — no house style for this image&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-markdown&#34;&gt;![A lighthouse, in woodcut](&amp;lt;gen:a lighthouse on a rocky coast?systemprompt=bold woodcut, two-tone&amp;gt;)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The house style is part of the cache identity, so changing it (in the theme, the config, or a&#xA;post) regenerates the affected images.&lt;/p&gt;&#xA;&lt;h2 id=&#34;audio-readings-podcasts&#34;&gt;Audio readings (podcasts)&lt;/h2&gt;&#xA;&lt;p&gt;A post can carry an audio reading — generated by AI or a file you recorded yourself. Both&#xA;become a player in the theme and a podcast &lt;code&gt;&amp;lt;enclosure&amp;gt;&lt;/code&gt; in the feeds (RSS/Atom/JSON).&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# pre-recorded — no AI, just attach a file (copied like a hero image)&#xA;audio_file: episode.mp3&#xA;# AI text-to-speech: omit to use the site default; set true/false to force per-post&#xA;audio: true&#xA;audio_voice: &amp;#34;English_Graceful_Lady&amp;#34;   # optional; else the author&amp;#39;s/persona&amp;#39;s voice, else the default&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;audio:&lt;/code&gt; is optional and three-state: &lt;strong&gt;omit it&lt;/strong&gt; to follow the site default (read aloud when&#xA;a speech provider is configured), or set &lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt; to force it for this post. &lt;code&gt;audio_file&lt;/code&gt;&#xA;wins if both are set. The reading voice resolves: post &lt;code&gt;audio_voice&lt;/code&gt; → the&#xA;author&#39;s (or persona&#39;s) &lt;code&gt;voice:&lt;/code&gt; → the &lt;code&gt;generation.speech&lt;/code&gt; default.&lt;/p&gt;&#xA;&lt;p&gt;Configure the speech provider alongside the image one:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;generation:&#xA;  speech:&#xA;    provider: minimax        # MiniMax t2a (more providers later)&#xA;    model: speech-2.6-hd&#xA;    voice: &amp;#34;English_Graceful_Lady&amp;#34;&#xA;    format: mp3              # default — the podcast-portable, now-patent-free choice&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Authors/personas can carry a default voice (e.g. a cloned voice id):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# authors/sam.yaml&#xA;id: sam&#xA;name: Sam Avery&#xA;voice: &amp;#34;English_Graceful_Lady&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Themes get &lt;code&gt;has_audio&lt;/code&gt; (bool) and &lt;code&gt;audio&lt;/code&gt; (URL) to render a player or filter audio posts. The&#xA;press theme shows a themed play/pause + &lt;strong&gt;scrubbable waveform&lt;/strong&gt; player under the byline.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Waveform.&lt;/strong&gt; The player derives the waveform from the audio itself, in the browser (Web Audio&#xA;&lt;code&gt;decodeAudioData&lt;/code&gt;), on first play — so generated readings cost a single TTS call, not a second one,&#xA;and the computed peaks are cached in &lt;code&gt;localStorage&lt;/code&gt; for instant redraws. For a &lt;strong&gt;recorded&lt;/strong&gt; file you&#xA;can still ship a precomputed &lt;code&gt;episode.mp3.json&lt;/code&gt; of the form &lt;code&gt;{&amp;quot;peaks&amp;quot;:[0.1, 0.7, …]}&lt;/code&gt; (values 0–1)&#xA;next to it — e.g. produced with &lt;code&gt;ffmpeg&lt;/code&gt;/&lt;code&gt;audiowaveform&lt;/code&gt; — and it renders instantly (paused,&#xA;pre-play); a &lt;code&gt;.wav&lt;/code&gt; is read directly at build. Until peaks exist, the player shows a resting shape&#xA;or a live Web Audio visualiser (same-origin). When audio is routed cross-origin to an object store,&#xA;the in-browser decode and any peaks fetch need CORS (a GET policy) on the bucket.&lt;/p&gt;&#xA;&lt;h3 id=&#34;what-gets-read-aloud&#34;&gt;What gets read aloud&lt;/h3&gt;&#xA;&lt;p&gt;Blocks that don&#39;t translate to speech — code, math, tables, diagrams — aren&#39;t read verbatim&#xA;(reading code or LaTeX aloud is gibberish). By default each is replaced with a short spoken&#xA;cue (&amp;quot;Here, the post shows a Go code example.&amp;quot;), the first cue adds &amp;quot;Visit the post to view&#xA;it.&amp;quot;, and a closing note is appended. Prose, headings, lists, blockquotes and callouts are&#xA;read normally. Tune it per type:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;generation:&#xA;  speech:&#xA;    transcript:&#xA;      blocks:                # cue | drop (silent) | keep (read the text)&#xA;        code: cue&#xA;        math_display: cue&#xA;        math_inline: drop&#xA;        table: cue&#xA;        diagram: cue&#xA;        inline_code: spell   # spell | keep | drop — &amp;#34;spell&amp;#34; voices symbols (&amp;#34;/etc&amp;#34; → &amp;#34;slash etc&amp;#34;)&#xA;      wrap_up: true          # append the closing &amp;#34;visit the post&amp;#34; note&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Acronyms&lt;/strong&gt; in your &lt;a href=&#34;/start/content/#glossary&#34;&gt;glossary&lt;/a&gt; are read as their expansion so they&#39;re&#xA;spoken as words, not letters — &lt;code&gt;SSH&lt;/code&gt; → &amp;quot;Secure Shell&amp;quot;, &lt;code&gt;DDD&lt;/code&gt; → &amp;quot;Domain Driven Design&amp;quot;. Only&#xA;entries that &lt;em&gt;look&lt;/em&gt; like acronym expansions qualify (an all-caps term with a short Title-Case&#xA;definition whose letters spell the acronym), so an ordinary term like &lt;code&gt;Rust&lt;/code&gt; is left alone.&#xA;Turn it off with &lt;code&gt;generation.speech.transcript.expand_acronyms: false&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;For finer control, wrap content in the body:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;&amp;lt;notts&amp;gt;…&amp;lt;/notts&amp;gt;&lt;/code&gt; — shown on the page but &lt;strong&gt;never spoken&lt;/strong&gt; (e.g. an aside, a pronunciation-&#xA;hostile string).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;&amp;lt;tts&amp;gt;…&amp;lt;/tts&amp;gt;&lt;/code&gt; — &lt;strong&gt;always read&lt;/strong&gt; verbatim, overriding the rules above (e.g. a short code&#xA;snippet you &lt;em&gt;do&lt;/em&gt; want spoken).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Both the injected speech (cues, hint, wrap-up, inline-code symbol names) &lt;strong&gt;and the player UI&lt;/strong&gt;&#xA;(the &amp;quot;Listen&amp;quot; caption and Play/Pause labels) are &lt;strong&gt;localised to the post&#39;s language&lt;/strong&gt; (&lt;code&gt;lang:&lt;/code&gt;&#xA;frontmatter, else the site &lt;code&gt;lang&lt;/code&gt;). English, Spanish, French, German, Italian and Mandarin ship&#xA;built in; add or override any language with a project &lt;code&gt;i18n/tts/replacements.json&lt;/code&gt; (same shape&#xA;as the built-in table). A missing language or field falls back to English.&lt;/p&gt;&#xA;&lt;p&gt;(No LLM is involved — cues are fixed text. An abstractive &lt;em&gt;summary&lt;/em&gt; reading would need a&#xA;text-LLM provider, which colophon doesn&#39;t have yet.)&lt;/p&gt;&#xA;&lt;h2 id=&#34;turning-generation-onoff&#34;&gt;Turning generation on/off&lt;/h2&gt;&#xA;&lt;p&gt;Three nested switches, all defaulting &lt;strong&gt;on&lt;/strong&gt;, all behaving the same way: when off, no new&#xA;assets of that kind are generated (no provider/API calls) even with &lt;code&gt;--generate-ai&lt;/code&gt;, while&#xA;everything already generated and committed keeps being served.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;generation:&#xA;  enabled: true          # master — turns ALL generation off in one line&#xA;  image:&#xA;    enabled: true        # just images&#xA;  speech:&#xA;    enabled: true        # just audio (also the per-post audio default — see below)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;generation.speech.enabled&lt;/code&gt; does double duty: it&#39;s the audio guard &lt;strong&gt;and&lt;/strong&gt; the default for a&#xA;post&#39;s &lt;code&gt;audio:&lt;/code&gt; field. With a speech provider configured and speech enabled, &lt;strong&gt;every post&#xA;reads aloud by default&lt;/strong&gt;; a post opts out with &lt;code&gt;audio: false&lt;/code&gt;. Without a provider, audio is&#xA;off regardless (so speech effectively self-disables). Image generation has no per-post&#xA;equivalent (images are explicit &lt;code&gt;gen:&lt;/code&gt; references), so &lt;code&gt;image.enabled&lt;/code&gt; is purely the guard.&lt;/p&gt;&#xA;&lt;h2 id=&#34;cleaning-up-the-cache&#34;&gt;Cleaning up the cache&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;colophon doctor&lt;/code&gt; reports cached generated assets that no content references any more (after&#xA;an edited prompt, a changed style/model/voice, or a deleted post). &lt;code&gt;colophon doctor --prune&lt;/code&gt;&#xA;deletes them (and their sidecars).&lt;/p&gt;&#xA;&lt;h2 id=&#34;image-post-processing&#34;&gt;Image post-processing&lt;/h2&gt;&#xA;&lt;p&gt;Some providers bake black letterbox bars into images. colophon removes them deterministically&#xA;after generation (&lt;code&gt;generation.image.postprocess.trim_letterbox&lt;/code&gt;, on by default): it crops&#xA;solid black borders and re-frames to the requested aspect. It&#39;s a no-op on clean images and&#xA;needs no configuration; set it to &lt;code&gt;false&lt;/code&gt; to disable.&lt;/p&gt;&#xA;&lt;h2 id=&#34;generating-images&#34;&gt;Generating images&lt;/h2&gt;&#xA;&lt;p&gt;Generation is a separate, explicit step — ordinary builds never call the provider:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;colophon build --generate-ai&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This produces only the images that &lt;strong&gt;aren&#39;t already cached&lt;/strong&gt;, writing each into&#xA;&lt;code&gt;output_dir&lt;/code&gt; alongside a &lt;code&gt;.json&lt;/code&gt; sidecar that records the prompt, provider, model,&#xA;parameters and date. Already-cached images are reused with no API call. Generations run&#xA;in parallel (up to &lt;code&gt;concurrency&lt;/code&gt;, default 5 — lower it if your provider rate-limits you).&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;--generate-ai&lt;/code&gt; is also a flag on &lt;code&gt;colophon publish&lt;/code&gt;, so a deploy can generate any&#xA;still-uncached media first: &lt;code&gt;colophon publish --env production --generate-ai&lt;/code&gt;. (The same&#xA;applies to TTS audio readings — without the flag, an uncached reading is skipped with a&#xA;&lt;code&gt;build --generate-ai to create it&lt;/code&gt; hint.)&lt;/p&gt;&#xA;&lt;p&gt;Then &lt;strong&gt;commit the generated files&lt;/strong&gt; (&lt;code&gt;content/assets/generated/&lt;/code&gt; by default). The cache&#xA;is part of your content: it makes builds reproducible and means a plain &lt;code&gt;colophon build&lt;/code&gt;&#xA;or &lt;code&gt;colophon publish&lt;/code&gt; — including in CI — needs no API key and incurs no cost.&lt;/p&gt;&#xA;&lt;p&gt;If a &lt;code&gt;gen:&lt;/code&gt; reference has no cached image and you build &lt;em&gt;without&lt;/em&gt; &lt;code&gt;--generate-ai&lt;/code&gt; (or the&#xA;provider errors, or no provider is configured), colophon &lt;strong&gt;warns and skips&lt;/strong&gt; it — the&#xA;build still succeeds, the image is just absent, exactly like any other missing asset.&#xA;&lt;code&gt;colophon doctor&lt;/code&gt; will not flag &lt;code&gt;gen:&lt;/code&gt; references as broken.&lt;/p&gt;&#xA;&lt;h2 id=&#34;good-to-know&#34;&gt;Good to know&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;File format follows the provider.&lt;/strong&gt; colophon names each file by its actual content&#xA;(e.g. MiniMax returns JPEG, so you get &lt;code&gt;.jpg&lt;/code&gt;), so the served &lt;code&gt;Content-Type&lt;/code&gt; is always&#xA;correct.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Changing the model or a default reuses nothing&lt;/strong&gt; — it&#39;s a new prompt identity, so the&#xA;old image stays in the cache until you delete it. There is no automatic pruning yet.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Provider model names change.&lt;/strong&gt; Pin &lt;code&gt;model:&lt;/code&gt; if you need stability; the defaults track&#xA;each provider&#39;s current recommended image model.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/image-generation.md&#34;&gt;&lt;code&gt;docs/image-generation.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Authors &amp; personas</title>
    <id>https://docs.colophon.blog/guides/personas/</id>
    <link href="https://docs.colophon.blog/guides/personas/" rel="alternate"></link>
    <updated>2001-12-24T00:00:00Z</updated>
    <published>2001-12-24T00:00:00Z</published>
    <summary type="text">colophon separates who is shown from how it&#39;s written:</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/personas.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;colophon separates &lt;strong&gt;who is shown&lt;/strong&gt; from &lt;strong&gt;how it&#39;s written&lt;/strong&gt;:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;An &lt;strong&gt;author&lt;/strong&gt; is the &lt;strong&gt;byline readers see&lt;/strong&gt; — an identity (a person, or a brand name).&lt;/li&gt;&#xA;&lt;li&gt;A &lt;strong&gt;persona&lt;/strong&gt; is a &lt;strong&gt;hidden writing voice&lt;/strong&gt; the agent writes in — never shown, and&#xA;&lt;strong&gt;shareable across authors&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;A post carries up to two fields: &lt;code&gt;author:&lt;/code&gt; (the byline) and &lt;code&gt;persona:&lt;/code&gt; (the voice). The&#xA;voice is purely an authoring aid; nothing about a persona is rendered.&lt;/p&gt;&#xA;&lt;h2 id=&#34;authors&#34;&gt;Authors&lt;/h2&gt;&#xA;&lt;p&gt;Authors live in &lt;code&gt;authors/&amp;lt;id&amp;gt;.yaml&lt;/code&gt; and supply the byline, author page, feed author and&#xA;JSON-LD &lt;code&gt;author&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;id: ada                       # the id (defaults to the file stem, e.g. authors/ada.yaml)&#xA;name: &amp;#34;Ada Lovelace&amp;#34;          # the byline shown to readers&#xA;bio: &amp;#34;Writes about distributed systems.&amp;#34;&#xA;avatar: assets/avatar.png     # see below — a file, a data:/https:// URL, or `gravatar`&#xA;urls: [&amp;#34;https://example.com&amp;#34;]&#xA;email: ada@example.com&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &lt;code&gt;avatar&lt;/code&gt; may be:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;a &lt;strong&gt;file under a content source&lt;/strong&gt; (e.g. &lt;code&gt;assets/avatar.png&lt;/code&gt;, resolved the same way a&#xA;markdown image embed is — the first source that can open it wins). It is published once to&#xA;&lt;code&gt;/assets/&amp;lt;name&amp;gt;&lt;/code&gt; and the byline/topbar &lt;code&gt;src&lt;/code&gt; is root-anchored, so it renders from every page&#xA;depth (and is rewritten to the object-store URL when an &lt;code&gt;assets/**&lt;/code&gt; route is active).&lt;/li&gt;&#xA;&lt;li&gt;a &lt;strong&gt;&lt;code&gt;data:&lt;/code&gt; URI&lt;/strong&gt; (a self-contained inline image),&lt;/li&gt;&#xA;&lt;li&gt;a fully-qualified &lt;strong&gt;&lt;code&gt;http(s)://&lt;/code&gt; URL&lt;/strong&gt; (a hosted image), or&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Gravatar&lt;/strong&gt; — &lt;code&gt;avatar: gravatar&lt;/code&gt; uses this author&#39;s &lt;code&gt;email:&lt;/code&gt;, or &lt;code&gt;avatar: &amp;quot;gravatar:me@example.com&amp;quot;&lt;/code&gt;&#xA;carries the address inline. It resolves to the author&#39;s Gravatar image. Append Gravatar options&#xA;after a &lt;code&gt;?&lt;/code&gt; to override the defaults (&lt;code&gt;s=200&amp;amp;d=mp&lt;/code&gt;), e.g. &lt;code&gt;&amp;quot;gravatar:me@example.com?d=identicon&amp;amp;s=256&amp;quot;&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;code&gt;data:&lt;/code&gt;, &lt;code&gt;http(s)://&lt;/code&gt; and resolved &lt;code&gt;gravatar&lt;/code&gt; avatars pass through untouched; a file path that no&#xA;source can open is warned about (no broken &lt;code&gt;src&lt;/code&gt; is emitted).&lt;/p&gt;&#xA;&lt;p&gt;A post names one with &lt;code&gt;author: ada&lt;/code&gt;. If a post sets no &lt;code&gt;author:&lt;/code&gt;, the &lt;strong&gt;first configured&#xA;author&lt;/strong&gt; is the default; with no authors at all, the byline is &lt;strong&gt;&amp;quot;Anonymous&amp;quot;&lt;/strong&gt; (a post&#xA;without an author still builds — it&#39;s just unattributed).&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon authors               # list bylines (alias: colophon author)&#xA;colophon author show ada       # one author&amp;#39;s full h-card&#xA;colophon authors --json        # machine-readable (for a skill/agent)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;personas-the-writing-voice&#34;&gt;Personas (the writing voice)&lt;/h2&gt;&#xA;&lt;p&gt;Personas live in &lt;code&gt;personas/&amp;lt;id&amp;gt;.yaml&lt;/code&gt; and are &lt;strong&gt;only&lt;/strong&gt; a voice — a style/character the agent&#xA;writes in, plus the references it may draw on:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;id: technical&#xA;name: &amp;#34;Senior engineer&amp;#34;        # a human label (not shown)&#xA;style:&#xA;  guide: &amp;#34;Plain, precise, technical. Short sentences. No hype. Senior-engineer perspective.&amp;#34;&#xA;  references:&#xA;    - &amp;#34;https://example.com/glossary&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The same persona can be used by &lt;strong&gt;different authors&lt;/strong&gt; — Ada and Grace can both publish in the&#xA;&lt;code&gt;technical&lt;/code&gt; voice under their own bylines. A persona&#39;s &lt;em&gt;corpus&lt;/em&gt; is every post written in it,&#xA;regardless of author, so the voice stays consistent and the exemplar pool grows.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon persona list            # id, label, and whether a style guide is set&#xA;colophon persona list --json     # machine-readable (for a skill/agent)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;write-as-context&#34;&gt;Write-as context&lt;/h2&gt;&#xA;&lt;p&gt;colophon does &lt;strong&gt;not&lt;/strong&gt; generate prose. It emits &lt;em&gt;context&lt;/em&gt; and the calling agent does the&#xA;writing. &lt;code&gt;persona context&lt;/code&gt; returns a voice&#39;s style guide and references plus the most relevant&#xA;&lt;strong&gt;exemplars&lt;/strong&gt; drawn from the posts written in that voice:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon persona context technical --topic &amp;#34;raft leader election&amp;#34;&#xA;colophon persona context technical --topic &amp;#34;raft&amp;#34; --tag distributed --top-k 5 --json&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;With &lt;code&gt;--topic&lt;/code&gt;, exemplars are ranked by relevance (a pure-Go BM25 over the persona&#39;s posts —&#xA;no embeddings, no API key). Without a topic, the most recent posts are returned.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;--tag&lt;/code&gt; (repeatable) narrows the corpus to exemplars carrying that tag.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;--top-k&lt;/code&gt; sets how many exemplars to emit (default 3).&lt;/li&gt;&#xA;&lt;li&gt;The persona id is optional when there is a single persona (or one named &lt;code&gt;default&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;finding-where-to-write--what-exists&#34;&gt;Finding where to write &amp;amp; what exists&lt;/h2&gt;&#xA;&lt;p&gt;Two commands round out the authoring toolbox (both take &lt;code&gt;--json&lt;/code&gt;):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon sources               # where each source&amp;#39;s content lives + how a post is marked live&#xA;colophon posts                 # existing entries: slug, title, type, author, persona, tags&#xA;colophon posts --tag go --author ada   # filter, e.g. to find cross-reference targets&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;creating--previewing&#34;&gt;Creating &amp;amp; previewing&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;colophon new post|page&lt;/code&gt; validates the author and persona, derives a &lt;strong&gt;unique pinned slug&lt;/strong&gt;,&#xA;writes a frontmatter skeleton to the right source, and reports the disk path and URL — then a&#xA;person &lt;em&gt;or&lt;/em&gt; an agent fills the body (colophon never generates prose):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon new post &amp;#34;Raft leader election&amp;#34; --author ada --persona technical --tag distributed&#xA;# wrote:  content/posts/raft-leader-election.md&#xA;# slug:   posts/raft-leader-election&#xA;# url:    /posts/raft-leader-election/   (preview: colophon serve)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;--author&lt;/code&gt; / &lt;code&gt;--persona&lt;/code&gt; are validated against &lt;code&gt;authors/*.yaml&lt;/code&gt; / &lt;code&gt;personas/*.yaml&lt;/code&gt; (errors&#xA;list the valid ids); both are optional.&lt;/li&gt;&#xA;&lt;li&gt;the slug derives from the title and is made unique — &lt;code&gt;--unique=hash&lt;/code&gt; (default) appends a&#xA;short id on a collision, &lt;code&gt;--unique=counter&lt;/code&gt; appends &lt;code&gt;-2&lt;/code&gt;; &lt;code&gt;--slug&lt;/code&gt; sets it explicitly.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;--in &amp;lt;source&amp;gt;&lt;/code&gt; chooses the source to write into; &lt;code&gt;--print&lt;/code&gt; emits to stdout instead of writing.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Preview, and jump straight to a page:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon serve --open=latest     # opens the newest post in the browser&#xA;colophon serve --open=sitemap    # also: home | atom | rss | json | robots | &amp;lt;slug&amp;gt;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;serve&lt;/code&gt; also prints the home/latest/sitemap/feed URLs at startup, so an agent can read them&#xA;without a browser.&lt;/p&gt;&#xA;&lt;p&gt;This is the core of the agent write-as flow: an agent picks a &lt;strong&gt;voice&lt;/strong&gt; (persona) for style&#xA;and an &lt;strong&gt;author&lt;/strong&gt; for the byline, fetches the write-as context, drafts in that voice, previews,&#xA;and publishes — all through the CLI, with deploy secrets resolved server-side and never passed&#xA;to the agent.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Retrieval is built in memory on each call (zero state). A persisted/​semantic index is a&#xA;future option; the command shape stays the same.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/personas.md&#34;&gt;&lt;code&gt;docs/personas.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Syndication (POSSE)</title>
    <id>https://docs.colophon.blog/guides/syndication/</id>
    <link href="https://docs.colophon.blog/guides/syndication/" rel="alternate"></link>
    <updated>2001-12-23T00:00:00Z</updated>
    <published>2001-12-23T00:00:00Z</published>
    <summary type="text">your blog and pushes copies to social accounts (&#34;silos&#34;) that link back. colophon does this with colophon syndicate: it walks your published posts, sends each to the configured…</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/syndication.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;&lt;strong&gt;POSSE&lt;/strong&gt; — &lt;em&gt;Publish on your Own Site, Syndicate Elsewhere&lt;/em&gt; — keeps the canonical copy of a post on&#xA;your blog and pushes copies to social accounts (&amp;quot;silos&amp;quot;) that link back. colophon does this with&#xA;&lt;code&gt;colophon syndicate&lt;/code&gt;: it walks your published posts, sends each to the configured &lt;strong&gt;syndicators&lt;/strong&gt;,&#xA;and records the result in a committed &lt;strong&gt;ledger&lt;/strong&gt; so re-runs never double-post. The recorded silo&#xA;URLs render on the post as &lt;code&gt;u-syndication&lt;/code&gt; &amp;quot;Also posted on…&amp;quot; chips, each with the silo&#39;s brand icon&#xA;and network name (e.g. &amp;quot;Bluesky&amp;quot;) — the same &lt;a href=&#34;/start/themes/#silo-icons&#34;&gt;silo icons&lt;/a&gt; used by responses.&lt;/p&gt;&#xA;&lt;p&gt;This page is the full reference; the &lt;a href=&#34;/guides/howto/&#34;&gt;how-to guides&lt;/a&gt; are quick per-silo recipes.&lt;/p&gt;&#xA;&lt;h2 id=&#34;how-it-works&#34;&gt;How it works&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;colophon publish   --env production --allow-publish   # 1. canonical post goes live first&#xA;colophon syndicate --env production --allow-publish    # 2. push copies to the silos, record the ledger&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Publish first.&lt;/strong&gt; Syndication links back to the canonical URL, and some drivers (Bridgy) fetch&#xA;the live page — so the post must be deployed before you syndicate.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;syndicate&lt;/code&gt;&lt;/strong&gt; gathers eligible posts (type &lt;code&gt;post&lt;/code&gt;, not draft, not opted out), works out each&#xA;one&#39;s targets, and for every &lt;code&gt;(post, target)&lt;/code&gt; &lt;em&gt;not already in the ledger&lt;/em&gt;, calls the driver.&lt;/li&gt;&#xA;&lt;li&gt;Each driver returns the &lt;strong&gt;silo URL&lt;/strong&gt;, which is written to &lt;code&gt;.colophon/syndication.json&lt;/code&gt; and, on the&#xA;next build, rendered as a &lt;code&gt;u-syndication&lt;/code&gt; link.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 id=&#34;editing-a-syndicated-copy-when-the-post-changes&#34;&gt;Editing a syndicated copy when the post changes&lt;/h3&gt;&#xA;&lt;p&gt;The ledger stores a &lt;strong&gt;content fingerprint&lt;/strong&gt; (title, summary, custom syndication text, link, tags)&#xA;alongside each silo URL. On a later run, if a post&#39;s content has changed, colophon brings the&#xA;existing silo copy up to date rather than skipping it — but &lt;em&gt;how&lt;/em&gt; depends on what the silo allows:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Mastodon&lt;/strong&gt; edits the status &lt;strong&gt;in place&lt;/strong&gt; (&lt;code&gt;PUT&lt;/code&gt;), preserving its likes/replies/permalink. This&#xA;happens automatically on any content change.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Bluesky can&#39;t edit a card in place&lt;/strong&gt; — its AppView ignores record edits, so an edit is&#xA;invisible. The only way to change a card is an &lt;strong&gt;atomic swap&lt;/strong&gt; (delete + recreate at the &lt;em&gt;same&lt;/em&gt;&#xA;rkey): the card re-indexes and the &lt;strong&gt;permalink is kept&lt;/strong&gt;, but it&#39;s a new record, so the post&#39;s&#xA;&lt;strong&gt;likes/reposts/replies reset&lt;/strong&gt; and the timestamp updates. Because that&#39;s lossy, automatic&#xA;edit-on-change &lt;strong&gt;skips Bluesky&lt;/strong&gt; with a note — it&#39;s only done on an explicit &lt;strong&gt;&lt;code&gt;--resync&lt;/code&gt;&lt;/strong&gt; (below).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Bridgy&lt;/strong&gt; / &lt;strong&gt;command&lt;/strong&gt; can&#39;t edit a published copy; their copies are left as-is with a note.&lt;/li&gt;&#xA;&lt;li&gt;An entry with &lt;strong&gt;no recorded silo URL&lt;/strong&gt; (e.g. an old Bridgy post) is &lt;strong&gt;skipped&lt;/strong&gt;, not failed.&lt;/li&gt;&#xA;&lt;li&gt;A post with an &lt;strong&gt;unchanged&lt;/strong&gt; fingerprint is skipped — edits only fire on a real change.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Upgrade/backfill:&lt;/strong&gt; a ledger written by an older colophon has no fingerprints. The first run&#xA;after upgrading &lt;strong&gt;backfills&lt;/strong&gt; the current fingerprint for each entry &lt;em&gt;without editing anything&lt;/em&gt;&#xA;(there&#39;s nothing to compare against), so only genuine changes &lt;em&gt;after&lt;/em&gt; that trigger an edit. The&#xA;run reports &lt;code&gt;backfilled=N&lt;/code&gt;; commit the updated ledger.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;--resync&lt;/code&gt;:&lt;/strong&gt; a one-shot that brings &lt;strong&gt;every&lt;/strong&gt; already-syndicated copy to the post&#39;s current&#xA;content, ignoring fingerprints. Use it to catch up copies created/changed before adopting this&#xA;feature — the backfill deliberately won&#39;t, since it can&#39;t tell which were stale. On &lt;code&gt;--resync&lt;/code&gt;,&#xA;Mastodon re-edits in place and &lt;strong&gt;Bluesky does the atomic swap&lt;/strong&gt; (so its cards refresh, accepting&#xA;the engagement reset). Reports &lt;code&gt;updated=N&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;It performs &lt;strong&gt;irreversible external actions&lt;/strong&gt; (posting to real accounts), so it&#39;s fenced:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Only an environment&#39;s &lt;code&gt;syndicate:&lt;/code&gt; targets ever fire — a &lt;code&gt;preview&lt;/code&gt;/&lt;code&gt;draft&lt;/code&gt; env that omits the key&#xA;&lt;strong&gt;never&lt;/strong&gt; posts.&lt;/li&gt;&#xA;&lt;li&gt;A gated env (&lt;code&gt;allow_publish: false&lt;/code&gt;, typically production) needs &lt;code&gt;--allow-publish&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;--dry-run&lt;/code&gt; shows exactly what would post and writes nothing.&lt;/li&gt;&#xA;&lt;li&gt;If the ledger file is missing, a real run &lt;strong&gt;refuses to start&lt;/strong&gt; (it would re-post your whole back&#xA;catalogue) unless you pass &lt;code&gt;--allow-publish&lt;/code&gt; to seed it.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;the-ledger--commit-it&#34;&gt;The ledger — commit it&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;.colophon/syndication.json&lt;/code&gt; is authoritative: it&#39;s how colophon knows a post is already syndicated.&#xA;&lt;strong&gt;Commit it to your repo.&lt;/strong&gt; Without it, a fresh CI runner would treat every post as new and re-post&#xA;everything. It maps post → driver → &lt;code&gt;{url, syndicated_at}&lt;/code&gt;; &lt;code&gt;encoding/json&lt;/code&gt; sorts keys so diffs stay&#xA;clean.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;.gitignore&lt;/code&gt; gotcha:&lt;/strong&gt; to commit the ledger out of an otherwise-ignored &lt;code&gt;.colophon/&lt;/code&gt;, ignore the&#xA;directory&#39;s &lt;em&gt;contents&lt;/em&gt;, not the directory — git can&#39;t re-include a file under a wholly-ignored dir:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-gitignore&#34;&gt;/.colophon/*                       # build trees, caches&#xA;!/.colophon/syndication.json       # …but keep the ledger&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;/.colophon/&lt;/code&gt; (trailing slash) followed by a &lt;code&gt;!&lt;/code&gt; negation &lt;strong&gt;silently fails&lt;/strong&gt; — the ledger stays&#xA;ignored. &lt;code&gt;colophon init&lt;/code&gt; scaffolds the correct form.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 id=&#34;persisting-the-ledger-in-ci-commit-it-back&#34;&gt;Persisting the ledger in CI (commit it back)&lt;/h3&gt;&#xA;&lt;p&gt;The catch on GitHub Actions (or any CI): &lt;code&gt;colophon syndicate&lt;/code&gt; &lt;em&gt;writes&lt;/em&gt; the ledger in the runner, but&#xA;that runner is thrown away. Unless the workflow &lt;strong&gt;commits the updated ledger back to the repo&lt;/strong&gt;, the&#xA;next run checks out the old ledger and &lt;strong&gt;re-posts everything&lt;/strong&gt;. So a CI syndication step is two&#xA;parts — syndicate, then commit back:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;permissions:&#xA;  contents: write            # needed to push the ledger back&#xA;concurrency:&#xA;  group: deploy              # serialise: two overlapping runs must not both post before committing&#xA;jobs:&#xA;  deploy:&#xA;    steps:&#xA;      # … build + publish (the post must be live before you syndicate) …&#xA;      - name: Syndicate&#xA;        env:&#xA;          MASTODON_TOKEN: ${{ secrets.MASTODON_TOKEN }}&#xA;          BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }}&#xA;        run: colophon syndicate --env production --allow-publish&#xA;      - name: Commit syndication ledger&#xA;        run: |&#xA;          git diff --quiet .colophon/syndication.json &amp;amp;&amp;amp; exit 0&#xA;          git config user.name  &amp;#34;github-actions[bot]&amp;#34;&#xA;          git config user.email &amp;#34;github-actions[bot]@users.noreply.github.com&amp;#34;&#xA;          git add .colophon/syndication.json&#xA;          git commit -m &amp;#34;chore(syndication): update ledger [skip ci]&amp;#34;&#xA;          git push&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Three things make this safe:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;contents: write&lt;/code&gt;&lt;/strong&gt; — the default workflow is read-only; the commit-back needs write.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;[skip ci]&lt;/code&gt;&lt;/strong&gt; in the commit message — so pushing the ledger doesn&#39;t trigger the workflow again&#xA;(it would self-terminate anyway once the ledger is current, but this avoids the extra run).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;concurrency:&lt;/code&gt;&lt;/strong&gt; — serialises runs, so two in-flight deploys can&#39;t each syndicate before either&#xA;commits, which would double-post.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;code&gt;colophon init&lt;/code&gt; scaffolds these as commented steps in the Deploy workflow. &lt;em&gt;(Alternative, not yet&#xA;built: keeping the ledger in the asset store like the &lt;code&gt;_mentions/&lt;/code&gt; pipeline, avoiding commit-back —&#xA;at the cost of versioning. Commit-back is the supported path today.)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;per-post-controls-frontmatter&#34;&gt;Per-post controls (frontmatter)&lt;/h3&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Frontmatter&lt;/th&gt;&#xA;&lt;th&gt;Effect&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;syndicate: false&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Don&#39;t syndicate this post at all.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;syndicate: [bsky]&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Only these targets (a subset of the env&#39;s &lt;code&gt;syndicate:&lt;/code&gt; list).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;em&gt;(omitted)&lt;/em&gt;&lt;/td&gt;&#xA;&lt;td&gt;All of the environment&#39;s targets.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;syndicate_text: &amp;quot;…&amp;quot;&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;A custom blurb for the silo copy (else the driver uses the title).&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;syndication: [url, …]&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Manually-added &amp;quot;Also posted on…&amp;quot; URLs, shown alongside ledger ones.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;h3 id=&#34;configuration-shape&#34;&gt;Configuration shape&lt;/h3&gt;&#xA;&lt;p&gt;Syndicators are configured like sources/publishers — &lt;code&gt;{id, driver, …settings}&lt;/code&gt; — under the site, and&#xA;each environment lists the ids it may post to:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    federation:&#xA;      syndication:&#xA;        - { id: mastodon, driver: mastodon, instance: https://hachyderm.io, token: &amp;#34;{env:MASTODON_TOKEN}&amp;#34; }&#xA;        - { id: bsky,     driver: bluesky,  handle: me.bsky.social, app_password: &amp;#34;{env:BLUESKY_APP_PASSWORD}&amp;#34; }&#xA;environments:&#xA;  - name: production&#xA;    syndicate: [mastodon, bsky]   # preview/draft omit this → never syndicate&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Secrets (tokens, app passwords) &lt;strong&gt;only&lt;/strong&gt; come from the environment via &lt;code&gt;{env:VAR}&lt;/code&gt; — never written as&#xA;literals.&lt;/p&gt;&#xA;&lt;h3 id=&#34;scheduling&#34;&gt;Scheduling&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;syndicate&lt;/code&gt; is idempotent (the ledger guards it), so it&#39;s safe to run after every publish, or on a&#xA;cron. A typical CI step runs &lt;code&gt;publish&lt;/code&gt; then &lt;code&gt;syndicate&lt;/code&gt; for production.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;drivers&#34;&gt;Drivers&lt;/h2&gt;&#xA;&lt;p&gt;Four drivers, picked by &lt;code&gt;driver:&lt;/code&gt;. All share the harness above (ledger, gating, &lt;code&gt;--dry-run&lt;/code&gt;); they&#xA;differ only in &lt;strong&gt;how the silo post is created&lt;/strong&gt; and &lt;strong&gt;where the auth lives&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;h3 id=&#34;command--run-any-program-you-own-the-integration&#34;&gt;&lt;code&gt;command&lt;/code&gt; — run any program (you own the integration)&lt;/h3&gt;&#xA;&lt;p&gt;&lt;strong&gt;For:&lt;/strong&gt; any target without a built-in driver — a silo&#39;s CLI, a webhook, an internal system, a&#xA;notifier. Maximum flexibility; colophon holds no silo credentials.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;How:&lt;/strong&gt; runs your program once per post. The post is passed as environment variables&#xA;(&lt;code&gt;COLOPHON_POST_URL&lt;/code&gt;, &lt;code&gt;_TITLE&lt;/code&gt;, &lt;code&gt;_SUMMARY&lt;/code&gt;, &lt;code&gt;_TEXT&lt;/code&gt;, &lt;code&gt;_TAGS&lt;/code&gt;, &lt;code&gt;_KEY&lt;/code&gt;, &lt;code&gt;_PUBLISHED&lt;/code&gt;) and as JSON on&#xA;stdin. The &lt;strong&gt;first line of stdout&lt;/strong&gt; is taken as the silo URL (print nothing for fire-and-forget); a&#xA;non-zero exit is a failure. Post content is never interpolated into the command, so it can&#39;t inject&#xA;shell.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;federation:&#xA;  syndication:&#xA;    - { id: silo, driver: command, command: &amp;#34;./bin/post-to-silo&amp;#34; }&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;#!/usr/bin/env bash&#xA;# bin/post-to-silo — receives one post via env/stdin, prints the created URL&#xA;set -euo pipefail&#xA;curl -fsS -X POST https://silo.example/api/posts \&#xA;  -H &amp;#34;Authorization: Bearer $SILO_TOKEN&amp;#34; \&#xA;  --data-urlencode &amp;#34;text=${COLOPHON_POST_TITLE} ${COLOPHON_POST_URL}&amp;#34; | jq -r .url&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h3 id=&#34;mastodon--post-to-a-mastodon-account-you-control&#34;&gt;&lt;code&gt;mastodon&lt;/code&gt; — post to a Mastodon account you control&lt;/h3&gt;&#xA;&lt;p&gt;&lt;strong&gt;For:&lt;/strong&gt; cross-posting to your own Mastodon (any instance). You hold the access token.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;How:&lt;/strong&gt; &lt;code&gt;POST &amp;lt;instance&amp;gt;/api/v1/statuses&lt;/code&gt; with &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;. The status text is&#xA;the blurb (custom text, else the title) plus the canonical link; Mastodon auto-links it and renders a&#xA;preview card. The status&#39;s &lt;code&gt;url&lt;/code&gt; is recorded. Text is trimmed to 500 chars but the link is always&#xA;preserved.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;federation:&#xA;  syndication:&#xA;    - id: mastodon&#xA;      driver: mastodon&#xA;      instance: https://hachyderm.io&#xA;      token: &amp;#34;{env:MASTODON_TOKEN}&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Set up:&lt;/strong&gt; on your instance, &lt;em&gt;Preferences → Development → New application&lt;/em&gt; with the &lt;code&gt;write:statuses&lt;/code&gt;&#xA;scope; copy the access token into &lt;code&gt;MASTODON_TOKEN&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h3 id=&#34;bluesky--post-to-a-bluesky-account-you-control&#34;&gt;&lt;code&gt;bluesky&lt;/code&gt; — post to a Bluesky account you control&lt;/h3&gt;&#xA;&lt;p&gt;&lt;strong&gt;For:&lt;/strong&gt; cross-posting to your own Bluesky. You hold an app password.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;How:&lt;/strong&gt; AT-proto — &lt;code&gt;createSession&lt;/code&gt; (handle + app password) gets a token, then &lt;code&gt;createRecord&lt;/code&gt; writes&#xA;an &lt;code&gt;app.bsky.feed.post&lt;/code&gt; with an &lt;strong&gt;external embed card&lt;/strong&gt; linking back to the canonical post. Returns&#xA;the &lt;code&gt;https://bsky.app/profile/&amp;lt;handle&amp;gt;/post/&amp;lt;id&amp;gt;&lt;/code&gt; permalink. Text (custom or title) is capped at 300&#xA;characters; the link-back is the card.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;federation:&#xA;  syndication:&#xA;    - id: bsky&#xA;      driver: bluesky&#xA;      handle: me.bsky.social&#xA;      app_password: &amp;#34;{env:BLUESKY_APP_PASSWORD}&amp;#34;&#xA;      # service: https://bsky.social   # optional; default&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Set up:&lt;/strong&gt; &lt;em&gt;Settings → Privacy and security → App passwords → Add&lt;/em&gt; (don&#39;t use your main password);&#xA;copy it into &lt;code&gt;BLUESKY_APP_PASSWORD&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h3 id=&#34;bridgy--let-bridgy-post-for-you-no-credentials-in-colophon&#34;&gt;&lt;code&gt;bridgy&lt;/code&gt; — let Bridgy post for you (no credentials in colophon)&lt;/h3&gt;&#xA;&lt;p&gt;&lt;strong&gt;For:&lt;/strong&gt; cross-posting &lt;em&gt;without&lt;/em&gt; colophon holding any silo tokens — &lt;a href=&#34;https://brid.gy&#34;&gt;Bridgy&lt;/a&gt; holds&#xA;your account auth. Good when you&#39;d rather not manage credentials, or you syndicate to several&#xA;networks through one mechanism.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;How:&lt;/strong&gt; colophon sends Bridgy a &lt;em&gt;publish webmention&lt;/em&gt; — &lt;code&gt;source&lt;/code&gt; = your post, &lt;code&gt;target&lt;/code&gt; =&#xA;&lt;code&gt;https://brid.gy/publish/&amp;lt;network&amp;gt;&lt;/code&gt;. Bridgy fetches your post&#39;s microformats2 (which colophon emits)&#xA;and creates the silo post on your behalf, returning its URL. The post must be &lt;strong&gt;live&lt;/strong&gt; (Bridgy&#xA;fetches it), and you must have &lt;strong&gt;connected the account at brid.gy first&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;federation:&#xA;  syndication:&#xA;    - { id: mast-via-bridgy, driver: bridgy, network: mastodon }&#xA;    - { id: bsky-via-bridgy, driver: bridgy, network: bluesky }&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Set up:&lt;/strong&gt; connect your silo account(s) at &lt;a href=&#34;https://brid.gy&#34;&gt;https://brid.gy&lt;/a&gt; and follow its instructions for your&#xA;domain. No tokens go in colophon.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;bridgy&lt;/code&gt; driver vs Bridgy Fed:&lt;/strong&gt; different things. This driver is &lt;em&gt;syndication&lt;/em&gt; (POSSE copies to&#xA;accounts you have). &lt;a href=&#34;/guides/bridgy-fed/&#34;&gt;Bridgy &lt;strong&gt;Fed&lt;/strong&gt;&lt;/a&gt; makes your &lt;em&gt;site itself&lt;/em&gt; followable from&#xA;the fediverse (no silo account) — that&#39;s federation, configured under &lt;code&gt;webmention&lt;/code&gt;, not here.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;choosing-a-driver&#34;&gt;Choosing a driver&lt;/h2&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;You want…&lt;/th&gt;&#xA;&lt;th&gt;Use&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Direct control, own the token, one account&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;mastodon&lt;/code&gt; / &lt;code&gt;bluesky&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;No credentials in colophon; already use Bridgy; several networks&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;bridgy&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;A silo with no built-in driver, a webhook, or custom logic&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;command&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;You can mix them — list several syndicators and put their ids in the env&#39;s &lt;code&gt;syndicate:&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;see-also&#34;&gt;See also&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Quick recipes: &lt;a href=&#34;/guides/syndicate-mastodon/&#34;&gt;Mastodon&lt;/a&gt; · &lt;a href=&#34;/guides/syndicate-bluesky/&#34;&gt;Bluesky&lt;/a&gt; ·&#xA;&lt;a href=&#34;/guides/syndicate-command/&#34;&gt;command&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/guides/webmentions/&#34;&gt;Webmentions&lt;/a&gt; — replies/likes back on your posts (the receive side).&lt;/li&gt;&#xA;&lt;li&gt;Design rationale: &lt;a href=&#34;/internals/federation/&#34;&gt;design/federation.md&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/syndication.md&#34;&gt;&lt;code&gt;docs/syndication.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Analytics &amp; telemetry</title>
    <id>https://docs.colophon.blog/guides/analytics/</id>
    <link href="https://docs.colophon.blog/guides/analytics/" rel="alternate"></link>
    <updated>2001-12-22T00:00:00Z</updated>
    <published>2001-12-22T00:00:00Z</published>
    <summary type="text">colophon has two separate, independent privacy-respecting surfaces. telemetry is the app; analytics is the site. Neither switch affects the other.</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/analytics.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;colophon has two &lt;strong&gt;separate, independent&lt;/strong&gt; privacy-respecting surfaces. &lt;code&gt;telemetry&lt;/code&gt; is the&#xA;&lt;strong&gt;app&lt;/strong&gt;; &lt;code&gt;analytics&lt;/code&gt; is the &lt;strong&gt;site&lt;/strong&gt;. Neither switch affects the other.&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;&lt;/th&gt;&#xA;&lt;th&gt;&lt;strong&gt;Site analytics&lt;/strong&gt;&lt;/th&gt;&#xA;&lt;th&gt;&lt;strong&gt;App telemetry&lt;/strong&gt;&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Answers&lt;/td&gt;&#xA;&lt;td&gt;&amp;quot;how is &lt;em&gt;my blog&lt;/em&gt; doing?&amp;quot;&lt;/td&gt;&#xA;&lt;td&gt;&amp;quot;how is &lt;em&gt;colophon&lt;/em&gt; used?&amp;quot;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Owner&lt;/td&gt;&#xA;&lt;td&gt;the &lt;strong&gt;site owner&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;the colophon &lt;strong&gt;maintainer&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Surface&lt;/td&gt;&#xA;&lt;td&gt;a web beacon in deployed pages&lt;/td&gt;&#xA;&lt;td&gt;the binary reporting its own runs&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Destination&lt;/td&gt;&#xA;&lt;td&gt;the site owner&#39;s statsfactory&lt;/td&gt;&#xA;&lt;td&gt;the maintainer&#39;s (release-baked) statsfactory&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Config&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;sites[].analytics&lt;/code&gt; (per site)&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;telemetry&lt;/code&gt; (top level)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Switch&lt;/td&gt;&#xA;&lt;td&gt;each provider&#39;s own config&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;telemetry.enabled&lt;/code&gt; (this only)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Both are off unless configured.&lt;/p&gt;&#xA;&lt;h2 id=&#34;site-analytics-reader-beacon&#34;&gt;Site analytics (reader beacon)&lt;/h2&gt;&#xA;&lt;p&gt;Per-site, one block per provider — your data, your instance:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    analytics:&#xA;      statsfactory:                       # cookieless, DNT-respecting&#xA;        server_url: &amp;#34;{env:STATSFACTORY_SERVER_URL:-}&amp;#34;&#xA;        app_key: &amp;#34;{env:STATSFACTORY_APP_KEY:-}&amp;#34;&#xA;      google_analytics:                   # GA4 — sets cookies, brings its own consent duties&#xA;        measurement_id: &amp;#34;{env:GA_MEASUREMENT_ID:-}&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Each provider is independent and inert until configured. The &lt;strong&gt;statsfactory&lt;/strong&gt; beacon is a&#xA;~2 KB dependency-free &lt;code&gt;analytics-sf.js&lt;/code&gt; written once to the site root and referenced by every&#xA;page. It:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;sends &lt;code&gt;page_view&lt;/code&gt; on load and &lt;code&gt;page_engagement&lt;/code&gt; (active milliseconds, as the metric value)&#xA;on hide/unload;&lt;/li&gt;&#xA;&lt;li&gt;is &lt;strong&gt;cookieless&lt;/strong&gt; (session id in &lt;code&gt;sessionStorage&lt;/code&gt;, per tab);&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;honours Do-Not-Track / Global Privacy Control&lt;/strong&gt; — sends nothing when either is set.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Its public per-page dimensions are &lt;code&gt;post.slug&lt;/code&gt;, &lt;code&gt;post.type&lt;/code&gt;, &lt;code&gt;post.author&lt;/code&gt;, &lt;code&gt;post.tags&lt;/code&gt;, plus&#xA;&lt;code&gt;page.path&lt;/code&gt; and &lt;code&gt;referrer&lt;/code&gt;. The statsfactory ingest key is a &lt;strong&gt;public &lt;code&gt;sf_live_&lt;/code&gt; key&lt;/strong&gt;, safe to&#xA;embed in pages. The &lt;strong&gt;hidden persona is never sent to the beacon&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Google Analytics&lt;/strong&gt; (GA4) ships its own loader asset, &lt;code&gt;analytics-ga.js&lt;/code&gt;, which injects&#xA;Google&#39;s &lt;code&gt;gtag.js&lt;/code&gt;. Each provider&#39;s asset is written to the site root &lt;strong&gt;only when that provider&#xA;is enabled&lt;/strong&gt; — &lt;code&gt;analytics-sf.js&lt;/code&gt; for statsfactory, &lt;code&gt;analytics-ga.js&lt;/code&gt; for GA, both if both,&#xA;nothing if neither.&lt;/p&gt;&#xA;&lt;p&gt;Every built-in and contrib theme includes the beacon by rendering &lt;code&gt;{{ analytics_head|safe }}&lt;/code&gt;&#xA;before &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt;; a JS-enabled custom theme should too (see &lt;a href=&#34;/start/themes/#analytics&#34;&gt;themes&lt;/a&gt;).&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Google Analytics sets cookies and carries consent obligations the cookieless beacon does&#xA;not — enable it only if that fits your privacy posture.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 id=&#34;injecting-site-credentials&#34;&gt;Injecting site credentials&lt;/h3&gt;&#xA;&lt;p&gt;Values usually come from &lt;code&gt;{env:VAR}&lt;/code&gt; placeholders. colophon loads two dot-env files from the&#xA;project root before interpolation and &lt;strong&gt;never overrides a variable already set in the real&#xA;environment&lt;/strong&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;real environment (e.g. CI secrets)  &amp;gt;  .env (local, gitignored)  &amp;gt;  .env.defaults (committed)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;So: commit your statsfactory endpoint + public key in &lt;code&gt;.env.defaults&lt;/code&gt;, override per-machine in&#xA;a local &lt;code&gt;.env&lt;/code&gt;, and override in CI via repository Variables/Secrets.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt; — &lt;code&gt;colophon init&lt;/code&gt; scaffolds &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;. Set under&#xA;&lt;em&gt;Settings → Secrets and variables → Actions&lt;/em&gt;:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Variables&lt;/strong&gt; (public): &lt;code&gt;STATSFACTORY_SERVER_URL&lt;/code&gt;, &lt;code&gt;STATSFACTORY_APP_KEY&lt;/code&gt; — the ingest key is&#xA;public, so a Variable (not a Secret) is right.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Secrets&lt;/strong&gt; (private): deploy credentials — &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt;, &lt;code&gt;CLOUDFLARE_ACCOUNT_ID&lt;/code&gt;,&#xA;&lt;code&gt;R2_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;R2_SECRET_ACCESS_KEY&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;app-telemetry-colophons-own-usage&#34;&gt;App telemetry (colophon&#39;s own usage)&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;colophon build&lt;/code&gt; and &lt;code&gt;colophon publish&lt;/code&gt; report colophon&#39;s &lt;em&gt;own&lt;/em&gt; operation — never your content&#xA;— to the maintainer, so usage is understood. It is anonymous (a &lt;code&gt;distinct_id&lt;/code&gt; that is a SHA-256&#xA;hash cached at &lt;code&gt;.colophon/telemetry.id&lt;/code&gt;; the raw value is never stored or sent), and&#xA;fire-and-forget — it never blocks or fails a command, and &lt;code&gt;colophon serve&lt;/code&gt; previews emit&#xA;nothing.&lt;/p&gt;&#xA;&lt;p&gt;Credentials default to values &lt;strong&gt;baked into the binary at release&lt;/strong&gt;, so a released colophon&#xA;reports by default (opt-out); a source/dev build has no baked creds and reports nothing. To&#xA;build a release with telemetry:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;go build -ldflags &amp;#34;\&#xA;  -X github.com/jmylchreest/colophon/internal/telemetry.DefaultServerURL=https://stats.example.com \&#xA;  -X github.com/jmylchreest/colophon/internal/telemetry.DefaultAppKey=sf_live_xxxxxxxx&amp;#34; \&#xA;  ./cmd/colophon&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;colophon&#39;s release workflow (&lt;code&gt;.github/workflows/build-release.yml&lt;/code&gt;) bakes these from the&#xA;repository&#39;s &lt;code&gt;COLOPHON_TELEMETRY_*&lt;/code&gt; secrets/variables, alongside the version (from the git tag),&#xA;so tagged binaries are versioned and report by default. A project may override the destination&#xA;(e.g. to self-host the maintainer role) under &lt;code&gt;telemetry.statsfactory&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;event-model&#34;&gt;Event model&lt;/h2&gt;&#xA;&lt;p&gt;statsfactory dimensions are arbitrary and defined at ingest time, so these compose into pivot&#xA;and breakdown views.&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Surface&lt;/th&gt;&#xA;&lt;th&gt;Event&lt;/th&gt;&#xA;&lt;th&gt;Value&lt;/th&gt;&#xA;&lt;th&gt;Key dimensions&lt;/th&gt;&#xA;&lt;th&gt;Answers&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Site&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;page_view&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;—&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;post.slug&lt;/code&gt;, &lt;code&gt;post.type&lt;/code&gt;, &lt;code&gt;post.tags&lt;/code&gt;, &lt;code&gt;post.author&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;most popular posts&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Site&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;page_engagement&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;active ms&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;post.slug&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;engagement time per post&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;App&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;build&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;page count&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;theme&lt;/code&gt;, &lt;code&gt;env&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;builds over time&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;App&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;source_indexed&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;doc count&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;source.type&lt;/code&gt;, &lt;code&gt;source.id&lt;/code&gt;, &lt;code&gt;env&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;document count × source type&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;App&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;publish&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;uploaded&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;publisher.type&lt;/code&gt;, &lt;code&gt;publisher.id&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;env&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;published docs/executions × publisher type&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;h2 id=&#34;opting-out--summary&#34;&gt;Opting out — summary&lt;/h2&gt;&#xA;&lt;p&gt;App telemetry and site analytics are independent — each is disabled on its own:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;App telemetry:&lt;/strong&gt; &lt;code&gt;telemetry.enabled: false&lt;/code&gt;, &lt;code&gt;COLOPHON_TELEMETRY=off&lt;/code&gt;, or&#xA;&lt;code&gt;telemetry.statsfactory.enabled: false&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A site analytics provider:&lt;/strong&gt; omit it, or set its &lt;code&gt;enabled: false&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Readers&lt;/strong&gt; opt out of the beacon automatically via Do-Not-Track / Global Privacy Control.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/analytics.md&#34;&gt;&lt;code&gt;docs/analytics.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Agent skills &amp; prompt packs (design)</title>
    <id>https://docs.colophon.blog/guides/skills/</id>
    <link href="https://docs.colophon.blog/guides/skills/" rel="alternate"></link>
    <updated>2001-12-21T00:00:00Z</updated>
    <published>2001-12-21T00:00:00Z</published>
    <summary type="text">colophon is the context provider; the agent (LLM) does the writing. A skill is a small, deterministic wrapper that:</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/skills.md — do not edit by hand. --&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Status: design.&lt;/strong&gt; This describes the planned authoring skills and the prompts colophon&#xA;would furnish them with. The &lt;em&gt;contracts&lt;/em&gt; they target — the frontmatter schema&#xA;(&lt;a href=&#34;/guides/seo/&#34;&gt;SEO&lt;/a&gt;, tags, persona) and the &lt;code&gt;markdown.Document&lt;/code&gt; round-trip — already exist.&#xA;The skills themselves are not built yet.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;the-model&#34;&gt;The model&lt;/h2&gt;&#xA;&lt;p&gt;colophon is the &lt;strong&gt;context provider&lt;/strong&gt;; the agent (LLM) does the writing. A skill is a small,&#xA;deterministic wrapper that:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;gathers &lt;strong&gt;context&lt;/strong&gt; — the article body, the persona&#39;s style guide + retrieved corpus&#xA;exemplars, and the site&#39;s facts (title, base_url, existing tags),&lt;/li&gt;&#xA;&lt;li&gt;furnishes a &lt;strong&gt;prompt pack&lt;/strong&gt; — a system prompt with a best-practice rubric and the output&#xA;schema,&lt;/li&gt;&#xA;&lt;li&gt;forces &lt;strong&gt;structured output&lt;/strong&gt; — the model returns a validated object (a frontmatter block&#xA;or body fragment), never free text,&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;merges&lt;/strong&gt; it back via &lt;code&gt;markdown.Document&lt;/code&gt; so the body is byte-preserved and only the&#xA;intended fields change.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The frontmatter schema is the contract: because every field has exactly one rendering&#xA;effect, the model can see the consequence of everything it writes, and nothing it can&#39;t set&#xA;affects the output.&lt;/p&gt;&#xA;&lt;h2 id=&#34;shared-infrastructure&#34;&gt;Shared infrastructure&lt;/h2&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Piece&lt;/th&gt;&#xA;&lt;th&gt;Role&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;Persona context&lt;/strong&gt; (&lt;code&gt;colophon persona context&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;td&gt;Emits the persona&#39;s style guide + top-K corpus exemplars (BM25/embedding retrieval). Every writing skill prepends it so output matches the author&#39;s voice.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;Structured output&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;Each skill defines a JSON Schema; the runtime validates and re-prompts on mismatch.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;&lt;code&gt;markdown.Document&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;Parse → patch frontmatter/body → re-marshal, preserving the body and unrelated fields.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;Surface&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;A CLI verb (&lt;code&gt;colophon &amp;lt;skill&amp;gt; &amp;lt;file&amp;gt;&lt;/code&gt;) and the matching MCP tool, sharing one implementation.&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;h2 id=&#34;skill-catalogue&#34;&gt;Skill catalogue&lt;/h2&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Skill&lt;/th&gt;&#xA;&lt;th&gt;Produces&lt;/th&gt;&#xA;&lt;th&gt;Consumes&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;seo&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;the &lt;code&gt;seo:&lt;/code&gt; block&lt;/td&gt;&#xA;&lt;td&gt;body + tags + persona&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;draft&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;body from a brief/outline&lt;/td&gt;&#xA;&lt;td&gt;brief + persona context&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;outline&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;a heading skeleton&lt;/td&gt;&#xA;&lt;td&gt;topic + persona&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;expand&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;fills a section&lt;/td&gt;&#xA;&lt;td&gt;surrounding body + persona&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;retitle&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;title&lt;/code&gt; + &lt;code&gt;slug&lt;/code&gt; candidates&lt;/td&gt;&#xA;&lt;td&gt;body&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;tag&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;tags&lt;/code&gt; suggestions&lt;/td&gt;&#xA;&lt;td&gt;body + the site&#39;s existing tag vocabulary&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;social&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;seo.social&lt;/code&gt; + syndication copy&lt;/td&gt;&#xA;&lt;td&gt;body + target network&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;alt-text&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;![alt]&lt;/code&gt; for images/embeds&lt;/td&gt;&#xA;&lt;td&gt;the image + nearby text&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;summary&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;description&lt;/code&gt; / TL;DR&lt;/td&gt;&#xA;&lt;td&gt;body&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;All are &lt;strong&gt;suggest-by-default&lt;/strong&gt;: they write a block you review, never silently overwrite&#xA;editorial fields. &lt;code&gt;--apply&lt;/code&gt; patches in place.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;prompt-pack-seo&#34;&gt;Prompt pack: &lt;code&gt;seo&lt;/code&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The flagship, since its contract (&lt;a href=&#34;/guides/seo/&#34;&gt;seo.md&lt;/a&gt;) and templating now exist.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Inputs furnished&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;the rendered article text (HTML stripped),&lt;/li&gt;&#xA;&lt;li&gt;the resolved page facts: site title, &lt;code&gt;base_url&lt;/code&gt; + slug (→ canonical), date, existing tags,&lt;/li&gt;&#xA;&lt;li&gt;persona style guide (so the title/description sound like the author, not generic SEO mush).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;Output schema&lt;/strong&gt; — the &lt;code&gt;seo:&lt;/code&gt; object (&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;keywords&lt;/code&gt;, &lt;code&gt;canonical&lt;/code&gt;,&#xA;&lt;code&gt;noindex&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;social{title,description}&lt;/code&gt;). Forced via structured output.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;System prompt (sketch)&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;You write SEO metadata for a blog post, in the author&amp;#39;s voice (style guide below).&#xA;Return ONLY the seo object. Follow these rules:&#xA;&#xA;- title: ≤60 characters. Front-load the primary keyword. Match the post&amp;#39;s actual content&#xA;  and search intent. Voice = the author&amp;#39;s, not clickbait.&#xA;- description: 140–160 characters. A genuine summary that earns the click; no teasing,&#xA;  no &amp;#34;in this post&amp;#34;. Unique to this page.&#xA;- keywords: 4–8 focus terms a reader would actually search; no stuffing.&#xA;- social.title / social.description: only if a punchier share-optimised version helps;&#xA;  otherwise omit and the search copy is reused.&#xA;- canonical / noindex / image / type: set only when you have a specific reason; otherwise&#xA;  omit and colophon&amp;#39;s defaults apply.&#xA;&#xA;Never invent facts not in the article. Prefer the author&amp;#39;s existing tags as keyword seeds.&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Inputs block (sketch)&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;## Author style guide&#xA;{{ persona.style.guide }}&#xA;&#xA;## Site&#xA;title: {{ site.title }}   url: {{ canonical }}   existing tags: {{ all_tags }}&#xA;&#xA;## Article&#xA;{{ body_text }}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The model returns e.g.:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;seo:&#xA;  title: &amp;#34;Rendering math, diagrams and code from one Markdown file&amp;#34;&#xA;  description: &amp;#34;How colophon turns a single note into a page with KaTeX, Mermaid and&#xA;    highlighted code — and degrades to readable text without JavaScript.&amp;#34;&#xA;  keywords: [static site generator, markdown, katex, mermaid, progressive enhancement]&#xA;  social:&#xA;    title: &amp;#34;One Markdown file → math, diagrams, code&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;colophon seo --apply post.md&lt;/code&gt; merges that under &lt;code&gt;seo:&lt;/code&gt;, body untouched; the next build&#xA;renders the canonical/OG/Twitter/JSON-LD from it.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;prompt-pack-draft--outline--expand&#34;&gt;Prompt pack: &lt;code&gt;draft&lt;/code&gt; / &lt;code&gt;outline&lt;/code&gt; / &lt;code&gt;expand&lt;/code&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The writing skills. Each prepends &lt;strong&gt;persona context&lt;/strong&gt; so output is in-voice, and takes a&#xA;brief or the surrounding body.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;outline&lt;/code&gt;&lt;/strong&gt; — input: a topic + angle. Output: a heading tree (&lt;code&gt;##&lt;/code&gt;/&lt;code&gt;###&lt;/code&gt;) with one-line&#xA;intents per section. Rubric: match the persona&#39;s typical structure; no body prose yet.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/strong&gt; — input: an outline (or brief) + persona context. Output: the markdown body.&#xA;Rubric: the author&#39;s voice and formatting conventions (callouts, code fences, length);&#xA;cite only what&#39;s in the references; leave &lt;code&gt;[[wikilink]]&lt;/code&gt; placeholders for cross-links rather&#xA;than inventing URLs.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;expand&lt;/code&gt;&lt;/strong&gt; — input: a section heading + the surrounding body. Output: that section&#39;s prose&#xA;only. Rubric: continuity with the existing voice and tense; no repetition of nearby points.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;prompt-pack-tag-social-alt-text-summary&#34;&gt;Prompt pack: &lt;code&gt;tag&lt;/code&gt;, &lt;code&gt;social&lt;/code&gt;, &lt;code&gt;alt-text&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;tag&lt;/strong&gt; — input: body + the &lt;strong&gt;site&#39;s existing tag vocabulary&lt;/strong&gt;. Output: 3–6 tags, &lt;em&gt;reusing&#xA;existing tags where they fit&lt;/em&gt; (avoid near-duplicate taxonomy), only proposing new ones when&#xA;warranted. This keeps tag pages (&lt;a href=&#34;/start/content/&#34;&gt;content.md&lt;/a&gt;) coherent.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;social&lt;/strong&gt; — input: body + target (Mastodon/Bluesky/X/LinkedIn). Output: &lt;code&gt;seo.social&lt;/code&gt; plus&#xA;a per-network post for &lt;code&gt;syndicate&lt;/code&gt;. Rubric: each network&#39;s norms (length, hashtags, link&#xA;placement) and the author&#39;s voice.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;alt-text&lt;/strong&gt; — input: an image + the paragraph around it. Output: concise, descriptive alt&#xA;text (not &amp;quot;image of&amp;quot;), for accessibility and image SEO.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;summary&lt;/strong&gt; — input: body. Output: a &lt;code&gt;description&lt;/code&gt; (and optionally a longer TL;DR callout).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;why-this-shape&#34;&gt;Why this shape&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;One contract, many skills.&lt;/strong&gt; Every skill writes into the same typed frontmatter the&#xA;templates already render, so adding a skill never needs a templating change.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Voice-preserving.&lt;/strong&gt; Persona context is the common prefix, so SEO copy, drafts and social&#xA;posts all sound like the same author.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Reviewable &amp;amp; reversible.&lt;/strong&gt; Structured output + &lt;code&gt;markdown.Document&lt;/code&gt; round-trip means a&#xA;skill patches exactly the fields it owns and nothing else; suggest-by-default keeps a human&#xA;in the loop for editorial fields.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/skills.md&#34;&gt;&lt;code&gt;docs/skills.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>How-to guides</title>
    <id>https://docs.colophon.blog/guides/howto/</id>
    <link href="https://docs.colophon.blog/guides/howto/" rel="alternate"></link>
    <updated>2001-12-20T00:00:00Z</updated>
    <published>2001-12-20T00:00:00Z</published>
    <summary type="text">Short, zero-to-published recipes. The design behind them is in internals/federation and internals/webmention.</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/howto/README.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;Short, zero-to-published recipes. The design behind them is in&#xA;&lt;a href=&#34;/internals/federation/&#34;&gt;../design/federation.md&lt;/a&gt; and &lt;a href=&#34;/internals/webmention/&#34;&gt;../design/webmention.md&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Guide&lt;/th&gt;&#xA;&lt;th&gt;Status&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;a href=&#34;/guides/bridgy-fed/&#34;&gt;Federate via Bridgy Fed&lt;/a&gt; — be followable from Mastodon/Bluesky&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;works today&lt;/strong&gt; (uses the mf2 + feeds colophon already emits)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;a href=&#34;/guides/webmentions/&#34;&gt;Show webmentions&lt;/a&gt; — replies/likes on your posts&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;shipped&lt;/strong&gt; (&lt;code&gt;colophon webmention fetch/publish&lt;/code&gt; + display modes)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;a href=&#34;/guides/syndicate-command/&#34;&gt;Syndicate with a command&lt;/a&gt; — POSSE to any target&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;shipped&lt;/strong&gt; (the &lt;code&gt;command&lt;/code&gt; driver)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;a href=&#34;/guides/syndicate-mastodon/&#34;&gt;Syndicate to Mastodon&lt;/a&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;shipped&lt;/strong&gt; (native &lt;code&gt;mastodon&lt;/code&gt; driver)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;a href=&#34;/guides/syndicate-bluesky/&#34;&gt;Syndicate to Bluesky&lt;/a&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;shipped&lt;/strong&gt; (native &lt;code&gt;bluesky&lt;/code&gt; driver)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Syndicate via Bridgy — &lt;a href=&#34;/guides/syndication/#bridgy--let-bridgy-post-for-you-no-credentials-in-colophon&#34;&gt;no-credentials POSSE&lt;/a&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;shipped&lt;/strong&gt; (&lt;code&gt;bridgy&lt;/code&gt; driver)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Syndication ships in full: the harness (ledger, env/per-post gating, &lt;code&gt;--dry-run&lt;/code&gt;) plus the&#xA;&lt;code&gt;command&lt;/code&gt;, &lt;code&gt;mastodon&lt;/code&gt;, &lt;code&gt;bluesky&lt;/code&gt;, and &lt;code&gt;bridgy&lt;/code&gt; drivers. The complete reference (every driver, how&#xA;each works, when to use which) is &lt;a href=&#34;/guides/syndication/&#34;&gt;../syndication.md&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/howto/README.md&#34;&gt;&lt;code&gt;docs/howto/README.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>How to federate via Bridgy Fed</title>
    <id>https://docs.colophon.blog/guides/bridgy-fed/</id>
    <link href="https://docs.colophon.blog/guides/bridgy-fed/" rel="alternate"></link>
    <updated>2001-12-19T00:00:00Z</updated>
    <published>2001-12-19T00:00:00Z</published>
    <summary type="text">Bridgy Fed makes your site itself followable from Mastodon and Bluesky: people follow @yourdomain, your posts federate, and replies come back as webmentions — without you…</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/howto/bridgy-fed.md — do not edit by hand. --&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Status: &lt;strong&gt;works today.&lt;/strong&gt; Bridgy Fed needs only microformats2 + a feed (and &lt;code&gt;rel=me&lt;/code&gt;), all of&#xA;which colophon already emits — no colophon code beyond what&#39;s shipped.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://fed.brid.gy&#34;&gt;Bridgy Fed&lt;/a&gt; makes your &lt;em&gt;site itself&lt;/em&gt; followable from Mastodon and Bluesky:&#xA;people follow &lt;code&gt;@yourdomain&lt;/code&gt;, your posts federate, and replies come back as webmentions — without&#xA;you running an ActivityPub server or even having a Mastodon/Bluesky account.&lt;/p&gt;&#xA;&lt;h2 id=&#34;steps&#34;&gt;Steps&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Publish your site&lt;/strong&gt; with colophon as usual. It already emits:&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;h-entry&lt;/code&gt;/&lt;code&gt;h-card&lt;/code&gt;/&lt;code&gt;h-feed&lt;/code&gt; microformats2, an RSS/Atom/JSON feed, and &lt;code&gt;rel=&amp;quot;me&amp;quot;&lt;/code&gt; identity&#xA;links. An author&#39;s &lt;code&gt;rel=&amp;quot;me&amp;quot;&lt;/code&gt; (all of their &lt;code&gt;urls:&lt;/code&gt;) is emitted in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of &lt;strong&gt;their&#xA;posts and their author feed page&lt;/strong&gt; (&lt;code&gt;/authors/&amp;lt;id&amp;gt;/&lt;/code&gt;) — your IndieWeb identity URL is that&#xA;author page, not the bare domain (the home page lists every author, so it carries no single&#xA;identity).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Add a webmention endpoint pointing at Bridgy Fed&lt;/strong&gt; so it can receive interactions for you:&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;federation:&#xA;  indieweb:&#xA;    webmention:&#xA;      receiver: https://fed.brid.gy/webmention   # emitted as &amp;lt;link rel=&amp;#34;webmention&amp;#34;&amp;gt; on every page&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;colophon emits the &lt;code&gt;&amp;lt;link rel=&amp;quot;webmention&amp;quot;&amp;gt;&lt;/code&gt; discovery tag site-wide when &lt;code&gt;receiver&lt;/code&gt; is set — no&#xA;manual theme edit needed.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Enrol&lt;/strong&gt; at &lt;a href=&#34;https://fed.brid.gy&#34;&gt;https://fed.brid.gy&lt;/a&gt; and follow its current instructions for your domain (it&#xA;verifies your site, then your handle becomes &lt;code&gt;@yourdomain@yourdomain&lt;/code&gt;). Bridgy Fed&#39;s onboarding&#xA;changes over time, so use its docs as the source of truth: &lt;a href=&#34;https://fed.brid.gy/docs&#34;&gt;https://fed.brid.gy/docs&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Done.&lt;/strong&gt; Fediverse/Bluesky users can follow you; new posts federate from your feed; replies&#xA;arrive at the webmention endpoint (see &lt;a href=&#34;/guides/webmentions/&#34;&gt;Show webmentions&lt;/a&gt; to display them).&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;notes&#34;&gt;Notes&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;This is &lt;strong&gt;federation&lt;/strong&gt;, not syndication: there&#39;s no separate silo account — your site &lt;em&gt;is&lt;/em&gt; the&#xA;account. For posting copies &lt;em&gt;to&lt;/em&gt; your own Mastodon/Bluesky accounts instead, see the syndication&#xA;guides.&lt;/li&gt;&#xA;&lt;li&gt;Forward-only is fine: you can be followable without displaying replies; add webmention display&#xA;when you want the conversation on your page.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/howto/bridgy-fed.md&#34;&gt;&lt;code&gt;docs/howto/bridgy-fed.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>How to show webmentions (replies, likes, reposts)</title>
    <id>https://docs.colophon.blog/guides/webmentions/</id>
    <link href="https://docs.colophon.blog/guides/webmentions/" rel="alternate"></link>
    <updated>2001-12-18T00:00:00Z</updated>
    <published>2001-12-18T00:00:00Z</published>
    <summary type="text">Webmentions let other sites&#39; replies/likes/reposts appear under your posts — &#34;comments without a database.&#34; A static site can&#39;t receive POSTs, so a hosted receiver…</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/howto/webmentions.md — do not edit by hand. --&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Status: &lt;strong&gt;shipped.&lt;/strong&gt; The full flow works: the &lt;code&gt;&amp;lt;link rel=&amp;quot;webmention&amp;quot;&amp;gt;&lt;/code&gt; tag, &lt;code&gt;rel=me&lt;/code&gt;,&#xA;microformats2, &lt;code&gt;webmention send&lt;/code&gt;, and the receive/display layer — &lt;code&gt;webmention fetch&lt;/code&gt;/&lt;code&gt;publish&lt;/code&gt;, the&#xA;&lt;code&gt;display.mode&lt;/code&gt; (live/asset/disabled), the themed responses block, and a committed glob blocklist&#xA;with the &lt;code&gt;colophon-moderate-mentions&lt;/code&gt; skill. See &lt;a href=&#34;/internals/webmention/&#34;&gt;../design/webmention.md&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Webmentions let other sites&#39; replies/likes/reposts appear under your posts — &amp;quot;comments without a&#xA;database.&amp;quot; A static site can&#39;t receive POSTs, so a hosted receiver (&lt;a href=&#34;https://webmention.io&#34;&gt;webmention.io&lt;/a&gt;)&#xA;collects them and colophon pulls them in at build/refresh time.&lt;/p&gt;&#xA;&lt;h2 id=&#34;steps&#34;&gt;Steps&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Sign in to &lt;a href=&#34;https://webmention.io&#34;&gt;webmention.io&lt;/a&gt;&lt;/strong&gt; via IndieAuth, which needs &lt;code&gt;rel=&amp;quot;me&amp;quot;&lt;/code&gt; on the&#xA;&lt;strong&gt;exact URL you sign in with&lt;/strong&gt;, linking &lt;em&gt;bidirectionally&lt;/em&gt; to a provider it can authenticate&#xA;(GitHub is the easy path). colophon emits an author&#39;s &lt;code&gt;rel=&amp;quot;me&amp;quot;&lt;/code&gt; (all of their &lt;code&gt;urls:&lt;/code&gt;) in the&#xA;&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of &lt;strong&gt;their posts and their author feed page&lt;/strong&gt;, so:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Sign in with your &lt;strong&gt;author feed page&lt;/strong&gt; — &lt;code&gt;https://example.com/authors/&amp;lt;your-id&amp;gt;/&lt;/code&gt; — not the bare&#xA;domain (the home page lists all authors, so it has no &lt;code&gt;rel=&amp;quot;me&amp;quot;&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;Make the link &lt;strong&gt;bidirectional&lt;/strong&gt;: set your GitHub profile&#39;s &lt;em&gt;website&lt;/em&gt; field to that &lt;strong&gt;same&lt;/strong&gt;&#xA;author-page URL (colophon already emits &lt;code&gt;rel=&amp;quot;me&amp;quot;&lt;/code&gt; → your GitHub from your &lt;code&gt;urls:&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;webmention.io then gives you:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;a receiver endpoint: &lt;code&gt;https://webmention.io/yourdomain/webmention&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;an &lt;strong&gt;API token&lt;/strong&gt; (for reading your mentions back).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Configure it&lt;/strong&gt; (token via env, never in config):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;federation:&#xA;  indieweb:&#xA;    webmention:&#xA;      receiver: https://webmention.io/yourdomain/webmention  # emitted as &amp;lt;link rel=&amp;#34;webmention&amp;#34;&amp;gt; (shipped)&#xA;      driver: jf2                                            # reader driver (read API); planned&#xA;      display:&#xA;        mode: asset                                          # live | asset | disabled (planned)&#xA;# export WEBMENTION_IO_TOKEN=...   (CI secret)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Build&lt;/strong&gt; — colophon emits &lt;code&gt;&amp;lt;link rel=&amp;quot;webmention&amp;quot;&amp;gt;&lt;/code&gt; site-wide today; the per-post responses block&#xA;(and &lt;code&gt;fetch&lt;/code&gt;/&lt;code&gt;publish&lt;/code&gt; below) are the planned display layer.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Pull mentions in:&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon webmention fetch        # writes _mentions/&amp;lt;post&amp;gt;.json (the display data)&#xA;colophon webmention publish      # pushes only _mentions/ to your asset host (R2), on its own schedule&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;JS-rendered themes fetch that asset live, so a scheduled &lt;code&gt;webmention publish&lt;/code&gt; keeps responses&#xA;fresh &lt;strong&gt;without rebuilding the site&lt;/strong&gt;. (No-JS/text themes show them as of the last build.)&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Send webmentions&lt;/strong&gt; when &lt;em&gt;you&lt;/em&gt; link to others, so you show up in their comments &lt;em&gt;(shipped)&lt;/em&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon webmention send --env production   # run AFTER publish; the source URLs must be live&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It scans the built output&#39;s outbound links (per page&#39;s canonical URL), discovers each target&#39;s&#xA;endpoint, and POSTs. A sent-cache (&lt;code&gt;.colophon/cache/webmention-sent.json&lt;/code&gt;) makes re-runs send only&#xA;new links and re-ping removed ones. &lt;code&gt;--dry-run&lt;/code&gt; reports without sending.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;(Optional) Social replies via Bridgy&lt;/strong&gt; — connect your silo accounts at &lt;a href=&#34;https://brid.gy&#34;&gt;https://brid.gy&lt;/a&gt;; it&#xA;backfeeds replies/likes from Mastodon/Bluesky to your webmention.io endpoint, so they appear the&#xA;same way. No extra colophon config.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;notes&#34;&gt;Notes&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Self-hosting: webmention.io is open source, or use a JF2-compatible receiver — point &lt;code&gt;source:&lt;/code&gt; at&#xA;its API (&lt;code&gt;driver: jf2&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;Privacy/spam: drop spam with a committed glob blocklist at &lt;code&gt;.colophon/webmention-block.yml&lt;/code&gt;&#xA;(by domain/url/author/content/type), applied at &lt;code&gt;fetch&lt;/code&gt; and shipped to the browser in &lt;code&gt;live&lt;/code&gt; mode.&#xA;The &lt;code&gt;colophon-moderate-mentions&lt;/code&gt; skill helps distill spam into small rules. Treat displayed&#xA;third-party content accordingly.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/howto/webmentions.md&#34;&gt;&lt;code&gt;docs/howto/webmentions.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>How to syndicate with a command (POSSE, any target)</title>
    <id>https://docs.colophon.blog/guides/syndicate-command/</id>
    <link href="https://docs.colophon.blog/guides/syndicate-command/" rel="alternate"></link>
    <updated>2001-12-17T00:00:00Z</updated>
    <published>2001-12-17T00:00:00Z</published>
    <summary type="text">POSSE = Publish on your Own Site, Syndicate Elsewhere. The command driver runs a program of your choice once per new post, so you can cross-post anywhere (a silo&#39;s CLI, a webhook,…</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/howto/syndicate-command.md — do not edit by hand. --&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Status: &lt;strong&gt;shipped.&lt;/strong&gt; The syndication harness — the ledger, gating, &lt;code&gt;--dry-run&lt;/code&gt;, and the&#xA;&lt;code&gt;command&lt;/code&gt; driver — works today. Native &lt;code&gt;mastodon&lt;/code&gt;/&lt;code&gt;bluesky&lt;/code&gt; drivers are planned&#xA;(&lt;a href=&#34;/internals/federation/&#34;&gt;../design/federation.md&lt;/a&gt;); the &lt;code&gt;command&lt;/code&gt; driver lets you wire up any&#xA;target now.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;POSSE = Publish on your Own Site, Syndicate Elsewhere. The &lt;code&gt;command&lt;/code&gt; driver runs a program of your&#xA;choice once per new post, so you can cross-post anywhere (a silo&#39;s CLI, a webhook, a notifier)&#xA;without a built-in driver. colophon records each result in a committed ledger, so re-runs never&#xA;double-post.&lt;/p&gt;&#xA;&lt;h2 id=&#34;steps&#34;&gt;Steps&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Write a command&lt;/strong&gt; that posts one entry. colophon passes the post as environment variables&#xA;(&lt;code&gt;COLOPHON_POST_URL&lt;/code&gt;, &lt;code&gt;_TITLE&lt;/code&gt;, &lt;code&gt;_SUMMARY&lt;/code&gt;, &lt;code&gt;_TEXT&lt;/code&gt;, &lt;code&gt;_TAGS&lt;/code&gt;, &lt;code&gt;_KEY&lt;/code&gt;, &lt;code&gt;_PUBLISHED&lt;/code&gt;) and as JSON&#xA;on stdin. Print the &lt;strong&gt;created URL&lt;/strong&gt; as the first line of stdout (or print nothing for&#xA;fire-and-forget). A non-zero exit is a failure.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;#!/usr/bin/env bash&#xA;# bin/post-to-silo — receives one post via env, prints the silo URL&#xA;set -euo pipefail&#xA;id=$(curl -fsS -X POST https://silo.example/api/posts \&#xA;       -H &amp;#34;Authorization: Bearer $SILO_TOKEN&amp;#34; \&#xA;       --data-urlencode &amp;#34;text=${COLOPHON_POST_TITLE} ${COLOPHON_POST_URL}&amp;#34; | jq -r .url)&#xA;echo &amp;#34;$id&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Configure a syndicator&lt;/strong&gt; (&lt;code&gt;driver: command&lt;/code&gt;) and allow it on the env that should post:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    federation:&#xA;      syndication:&#xA;        - { id: silo, driver: command, command: &amp;#34;./bin/post-to-silo&amp;#34; }&#xA;environments:&#xA;  - name: production&#xA;    syndicate: [silo]      # only this env cross-posts; preview/draft omit it → never post&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Preview, then post&lt;/strong&gt; (run after &lt;code&gt;publish&lt;/code&gt;, so the canonical URL is live):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon syndicate --env production --dry-run        # shows what would post, writes nothing&#xA;colophon syndicate --env production --allow-publish  # posts new entries, records the ledger&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Commit the ledger&lt;/strong&gt; (&lt;code&gt;.colophon/syndication.json&lt;/code&gt;) — it&#39;s authoritative. Without it a fresh&#xA;runner would re-post everything, so a real run refuses to start with no ledger unless you pass&#xA;&lt;code&gt;--allow-publish&lt;/code&gt; to seed it.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The recorded silo URLs render on each post as mf2 &lt;code&gt;u-syndication&lt;/code&gt; &amp;quot;Also posted on…&amp;quot; links.&lt;/p&gt;&#xA;&lt;h2 id=&#34;notes&#34;&gt;Notes&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Safety:&lt;/strong&gt; only an env&#39;s &lt;code&gt;syndicate:&lt;/code&gt; targets fire; a gated env (&lt;code&gt;allow_publish: false&lt;/code&gt;) needs&#xA;&lt;code&gt;--allow-publish&lt;/code&gt;; &lt;code&gt;--dry-run&lt;/code&gt; never posts or writes. Post content is passed via env/stdin, never&#xA;interpolated into the command, so it can&#39;t inject shell.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Per post:&lt;/strong&gt; &lt;code&gt;syndicate: false&lt;/code&gt; to skip one, &lt;code&gt;syndicate: [silo]&lt;/code&gt; to choose targets,&#xA;&lt;code&gt;syndicate_text:&lt;/code&gt; for a custom blurb.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Secrets&lt;/strong&gt; (like &lt;code&gt;SILO_TOKEN&lt;/code&gt;) come from the environment, never the config.&lt;/li&gt;&#xA;&lt;li&gt;Prefer a managed native account? The &lt;code&gt;mastodon&lt;/code&gt;/&lt;code&gt;bluesky&lt;/code&gt; drivers (planned) will hold the auth&#xA;for you; until then &lt;code&gt;command&lt;/code&gt; covers any target.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/howto/syndicate-command.md&#34;&gt;&lt;code&gt;docs/howto/syndicate-command.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>How to syndicate to Mastodon (POSSE)</title>
    <id>https://docs.colophon.blog/guides/syndicate-mastodon/</id>
    <link href="https://docs.colophon.blog/guides/syndicate-mastodon/" rel="alternate"></link>
    <updated>2001-12-16T00:00:00Z</updated>
    <published>2001-12-16T00:00:00Z</published>
    <summary type="text">POSSE = Publish on your Own Site, Syndicate Elsewhere: the post is canonical on your blog, and a copy is cross-posted to Mastodon linking back to it.</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/howto/syndicate-mastodon.md — do not edit by hand. --&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Status: &lt;strong&gt;shipped.&lt;/strong&gt; The &lt;code&gt;mastodon&lt;/code&gt; driver, &lt;code&gt;colophon syndicate&lt;/code&gt;, and the ledger work today.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;POSSE = Publish on your Own Site, Syndicate Elsewhere: the post is canonical on your blog, and a&#xA;copy is cross-posted to Mastodon linking back to it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;steps&#34;&gt;Steps&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Have a Mastodon account&lt;/strong&gt; on any instance (e.g. &lt;code&gt;hachyderm.io&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Create an access token:&lt;/strong&gt; on your instance, &lt;strong&gt;Preferences → Development → New application&lt;/strong&gt;;&#xA;give it the &lt;strong&gt;&lt;code&gt;write:statuses&lt;/code&gt;&lt;/strong&gt; (and &lt;code&gt;write:media&lt;/code&gt; for images) scope; create it; copy the&#xA;&lt;strong&gt;access token&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Export the token&lt;/strong&gt; as a CI secret: &lt;code&gt;export MASTODON_TOKEN=...&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Configure a syndicator&lt;/strong&gt; (&lt;code&gt;driver: mastodon&lt;/code&gt;):&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    federation:&#xA;      syndication:&#xA;        - id: mastodon&#xA;          driver: mastodon&#xA;          instance: https://hachyderm.io&#xA;          token: &amp;#34;{env:MASTODON_TOKEN}&amp;#34;   # never a literal&#xA;environments:&#xA;  - name: production&#xA;    syndicate: [mastodon]     # only this env cross-posts; preview/draft never do&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Publish, then syndicate&lt;/strong&gt; (syndicate runs after the canonical URL is live):&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon publish  --env production --allow-publish&#xA;colophon syndicate --env production --allow-publish&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;The Mastodon post URL is recorded in the syndication ledger and shown as an &amp;quot;Also posted on…&amp;quot;&#xA;(&lt;code&gt;u-syndication&lt;/code&gt;) link on your post. Re-running is idempotent (the ledger prevents double-posting).&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;notes&#34;&gt;Notes&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Commit the syndication ledger&lt;/strong&gt; (&lt;code&gt;.colophon/syndication.json&lt;/code&gt;) — it&#39;s authoritative; a fresh CI&#xA;runner without it would re-post. &lt;code&gt;syndicate&lt;/code&gt; refuses to run blind without it.&lt;/li&gt;&#xA;&lt;li&gt;Per post: &lt;code&gt;syndicate: [mastodon]&lt;/code&gt; to choose targets, &lt;code&gt;syndicate: false&lt;/code&gt; to skip, &lt;code&gt;syndicate_text:&lt;/code&gt;&#xA;for a custom blurb. Long posts are truncated with a link back.&lt;/li&gt;&#xA;&lt;li&gt;Replies/boosts on the Mastodon copy can flow back to your post via Bridgy backfeed — see&#xA;&lt;a href=&#34;/guides/webmentions/&#34;&gt;Show webmentions&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;No token to manage? Use &lt;code&gt;driver: bridgy&lt;/code&gt; with &lt;code&gt;network: mastodon&lt;/code&gt; instead (Bridgy holds the auth).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/howto/syndicate-mastodon.md&#34;&gt;&lt;code&gt;docs/howto/syndicate-mastodon.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>How to syndicate to Bluesky (POSSE)</title>
    <id>https://docs.colophon.blog/guides/syndicate-bluesky/</id>
    <link href="https://docs.colophon.blog/guides/syndicate-bluesky/" rel="alternate"></link>
    <updated>2001-12-15T00:00:00Z</updated>
    <published>2001-12-15T00:00:00Z</published>
    <summary type="text">POSSE = Publish on your Own Site, Syndicate Elsewhere: the post is canonical on your blog, and a copy is cross-posted to Bluesky linking back to it.</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/howto/syndicate-bluesky.md — do not edit by hand. --&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Status: &lt;strong&gt;shipped.&lt;/strong&gt; The &lt;code&gt;bluesky&lt;/code&gt; driver, &lt;code&gt;colophon syndicate&lt;/code&gt;, and the ledger work today.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;POSSE = Publish on your Own Site, Syndicate Elsewhere: the post is canonical on your blog, and a&#xA;copy is cross-posted to Bluesky linking back to it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;steps&#34;&gt;Steps&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Have a Bluesky account&lt;/strong&gt; — note your handle (e.g. &lt;code&gt;me.bsky.social&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Create an app password:&lt;/strong&gt; &lt;strong&gt;Settings → Privacy and security → App passwords → Add&lt;/strong&gt; (don&#39;t use&#xA;your main password). Copy it.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Export it&lt;/strong&gt; as a CI secret: &lt;code&gt;export BLUESKY_APP_PASSWORD=...&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Configure a syndicator&lt;/strong&gt; (&lt;code&gt;driver: bluesky&lt;/code&gt;):&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    federation:&#xA;      syndication:&#xA;        - id: bluesky&#xA;          driver: bluesky&#xA;          handle: me.bsky.social&#xA;          app_password: &amp;#34;{env:BLUESKY_APP_PASSWORD}&amp;#34;   # never a literal&#xA;environments:&#xA;  - name: production&#xA;    syndicate: [bluesky]      # only this env cross-posts; preview/draft never do&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Publish, then syndicate:&lt;/strong&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;colophon publish  --env production --allow-publish&#xA;colophon syndicate --env production --allow-publish&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;colophon authenticates (handle + app password → AT-proto session), creates the post (with a&#xA;link card back to the canonical), records the Bluesky URL in the ledger, and renders it as an&#xA;&amp;quot;Also posted on…&amp;quot; (&lt;code&gt;u-syndication&lt;/code&gt;) link. Idempotent via the ledger.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;notes&#34;&gt;Notes&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Bluesky&#39;s limit is &lt;strong&gt;300 characters&lt;/strong&gt; — long posts are truncated with a link back; set&#xA;&lt;code&gt;syndicate_text:&lt;/code&gt; per post for a custom blurb.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Commit the syndication ledger&lt;/strong&gt; (&lt;code&gt;.colophon/syndication.json&lt;/code&gt;); without it a fresh runner would&#xA;re-post, so &lt;code&gt;syndicate&lt;/code&gt; refuses to run blind.&lt;/li&gt;&#xA;&lt;li&gt;Replies/likes/reposts on the Bluesky copy can flow back to your post via Bridgy backfeed — see&#xA;&lt;a href=&#34;/guides/webmentions/&#34;&gt;Show webmentions&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Prefer not to manage credentials? Use &lt;code&gt;driver: bridgy&lt;/code&gt; with &lt;code&gt;network: bluesky&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/howto/syndicate-bluesky.md&#34;&gt;&lt;code&gt;docs/howto/syndicate-bluesky.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>CLI reference</title>
    <id>https://docs.colophon.blog/cli/</id>
    <link href="https://docs.colophon.blog/cli/" rel="alternate"></link>
    <updated>2001-12-14T00:00:00Z</updated>
    <published>2001-12-14T00:00:00Z</published>
    <summary type="text">Every colophon command, generated from the binary&#39;s --help output.</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;The complete command surface, one page per command. Everything below is emitted by the binary itself (&lt;code&gt;colophon --help&lt;/code&gt;), so it always matches the release it was generated from.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/init/&#34;&gt;colophon init&lt;/a&gt; — Scaffold a new colophon project&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/new/&#34;&gt;colophon new&lt;/a&gt; — Scaffold a new post (dated, chronological) (subcommands: post, page)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/build/&#34;&gt;colophon build&lt;/a&gt; — Build the site into public/ (prints next pending embargo)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/next-build-time/&#34;&gt;colophon next-build-time&lt;/a&gt; — Print the next pending publish_after timestamp (for CI scheduling)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/serve/&#34;&gt;colophon serve&lt;/a&gt; — Serve every environment locally with live reload&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/publish/&#34;&gt;colophon publish&lt;/a&gt; — Build and deploy/mirror to publishers (gated)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/themes/&#34;&gt;colophon themes&lt;/a&gt; — List the built-in themes (subcommands: list, eject)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/authors/&#34;&gt;colophon authors&lt;/a&gt; — List authors (the bylines) (subcommands: list, show)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/persona/&#34;&gt;colophon persona&lt;/a&gt; — List personas (subcommands: list, context)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/sources/&#34;&gt;colophon sources&lt;/a&gt; — Show where content lives and how posts are marked publishable&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/posts/&#34;&gt;colophon posts&lt;/a&gt; — List content entries (for editing and cross-referencing)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/search/&#34;&gt;colophon search&lt;/a&gt; — Search content (lexical or semantic)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/skills/&#34;&gt;colophon skills&lt;/a&gt; — Show which agent harnesses are present and the install status of each skill (subcommands: detect, install, list, uninstall)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/webmention/&#34;&gt;colophon webmention&lt;/a&gt; — Notify the sites your live posts link to (run after publish) (subcommands: send, fetch, publish)&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/syndicate/&#34;&gt;colophon syndicate&lt;/a&gt; — Cross-post (POSSE) to the environment&#39;s configured syndicators&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/doctor/&#34;&gt;colophon doctor&lt;/a&gt; — Validate the project config and report problems&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/cli/env/&#34;&gt;colophon env&lt;/a&gt; — List the environment variables this project uses&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;colophon---help&#34;&gt;colophon --help&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon &amp;lt;command&amp;gt; [flags]&#xA;&#xA;A themed Markdown blog generator with pluggable publishers&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;Commands:&#xA;  init [&amp;lt;dir&amp;gt;] [flags]&#xA;    Scaffold a new colophon project&#xA;&#xA;  new post &amp;lt;title&amp;gt; [flags]&#xA;    Scaffold a new post (dated, chronological)&#xA;&#xA;  new page &amp;lt;title&amp;gt; [flags]&#xA;    Scaffold a new standing page (nav menu, no date)&#xA;&#xA;  build [flags]&#xA;    Build the site into public/ (prints next pending embargo)&#xA;&#xA;  next-build-time [flags]&#xA;    Print the next pending publish_after timestamp (for CI scheduling)&#xA;&#xA;  serve [flags]&#xA;    Serve every environment locally with live reload&#xA;&#xA;  publish --env=ENV,... [flags]&#xA;    Build and deploy/mirror to publishers (gated)&#xA;&#xA;  themes list&#xA;    List the built-in themes&#xA;&#xA;  themes eject &amp;lt;name&amp;gt; [flags]&#xA;    Copy a built-in theme into themes/&amp;lt;name&amp;gt;/ to customise&#xA;&#xA;  authors (author) list [flags]&#xA;    List authors (the bylines)&#xA;&#xA;  authors (author) show &amp;lt;author&amp;gt; [flags]&#xA;    Show one author&amp;#39;s full details&#xA;&#xA;  persona (personas) list [flags]&#xA;    List personas&#xA;&#xA;  persona (personas) context [&amp;lt;persona&amp;gt;] [flags]&#xA;    Emit style guide + top-K exemplars for AI-assisted writing&#xA;&#xA;  sources (source) [flags]&#xA;    Show where content lives and how posts are marked publishable&#xA;&#xA;  posts (post) [flags]&#xA;    List content entries (for editing and cross-referencing)&#xA;&#xA;  search [&amp;lt;query&amp;gt;] [flags]&#xA;    Search content (lexical or semantic)&#xA;&#xA;  skills detect [flags]&#xA;    Show which agent harnesses are present and the install status of each skill&#xA;&#xA;  skills install [flags]&#xA;    Install/update the skills into detected harnesses (or --harness/--dir)&#xA;&#xA;  skills list&#xA;    List the skills embedded in this binary&#xA;&#xA;  skills uninstall [flags]&#xA;    Remove colophon-managed skills from detected harnesses (or --harness/--dir)&#xA;&#xA;  webmention send [flags]&#xA;    Notify the sites your live posts link to (run after publish)&#xA;&#xA;  webmention fetch [flags]&#xA;    Pull received mentions from the configured receiver into the local cache&#xA;&#xA;  webmention publish [flags]&#xA;    Fetch mentions and deploy only _mentions/ (refresh responses without a full&#xA;    re-upload)&#xA;&#xA;  syndicate [flags]&#xA;    Cross-post (POSSE) to the environment&amp;#39;s configured syndicators&#xA;&#xA;  doctor [flags]&#xA;    Validate the project config and report problems&#xA;&#xA;  env [flags]&#xA;    List the environment variables this project uses&#xA;&#xA;Run &amp;#34;colophon &amp;lt;command&amp;gt; --help&amp;#34; for more information on a command.&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon init</title>
    <id>https://docs.colophon.blog/cli/init/</id>
    <link href="https://docs.colophon.blog/cli/init/" rel="alternate"></link>
    <updated>2001-12-13T00:00:00Z</updated>
    <published>2001-12-13T00:00:00Z</published>
    <summary type="text">Scaffold a new colophon project</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Scaffold a new colophon project&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon init [&amp;lt;dir&amp;gt;] [flags]&#xA;&#xA;Scaffold a new colophon project&#xA;&#xA;Arguments:&#xA;  [&amp;lt;dir&amp;gt;]    Target directory&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;      --force      Overwrite an existing colophon.yaml&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon new</title>
    <id>https://docs.colophon.blog/cli/new/</id>
    <link href="https://docs.colophon.blog/cli/new/" rel="alternate"></link>
    <updated>2001-12-12T00:00:00Z</updated>
    <published>2001-12-12T00:00:00Z</published>
    <summary type="text">Scaffold a new post or page (validated author/persona, unique slug)</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Scaffold a new post or page (validated author/persona, unique slug)&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon new &amp;lt;command&amp;gt; [flags]&#xA;&#xA;Scaffold a new post or page (validated author/persona, unique slug)&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;Commands:&#xA;  new post &amp;lt;title&amp;gt; [flags]&#xA;    Scaffold a new post (dated, chronological)&#xA;&#xA;  new page &amp;lt;title&amp;gt; [flags]&#xA;    Scaffold a new standing page (nav menu, no date)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-new-post&#34;&gt;colophon new post&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon new post &amp;lt;title&amp;gt; [flags]&#xA;&#xA;Scaffold a new post (dated, chronological)&#xA;&#xA;Arguments:&#xA;  &amp;lt;title&amp;gt;    Entry title&#xA;&#xA;Flags:&#xA;  -h, --help              Show context-sensitive help.&#xA;      --version           Print version and exit&#xA;&#xA;      --author=STRING     Byline author id (validated; default: first author /&#xA;                          Anonymous)&#xA;      --persona=STRING    Writing-voice persona id (validated; optional)&#xA;      --tag=TAG,...       Tags&#xA;      --slug=STRING       Explicit slug (else derived from the title and made&#xA;                          unique)&#xA;      --unique=&amp;#34;hash&amp;#34;     Slug collision strategy: hash | counter&#xA;      --in=STRING         Source id to write into (default: the first source)&#xA;      --print             Print the file to stdout instead of writing it&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-new-page&#34;&gt;colophon new page&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon new page &amp;lt;title&amp;gt; [flags]&#xA;&#xA;Scaffold a new standing page (nav menu, no date)&#xA;&#xA;Arguments:&#xA;  &amp;lt;title&amp;gt;    Entry title&#xA;&#xA;Flags:&#xA;  -h, --help              Show context-sensitive help.&#xA;      --version           Print version and exit&#xA;&#xA;      --author=STRING     Byline author id (validated; default: first author /&#xA;                          Anonymous)&#xA;      --persona=STRING    Writing-voice persona id (validated; optional)&#xA;      --tag=TAG,...       Tags&#xA;      --slug=STRING       Explicit slug (else derived from the title and made&#xA;                          unique)&#xA;      --unique=&amp;#34;hash&amp;#34;     Slug collision strategy: hash | counter&#xA;      --in=STRING         Source id to write into (default: the first source)&#xA;      --print             Print the file to stdout instead of writing it&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon build</title>
    <id>https://docs.colophon.blog/cli/build/</id>
    <link href="https://docs.colophon.blog/cli/build/" rel="alternate"></link>
    <updated>2001-12-11T00:00:00Z</updated>
    <published>2001-12-11T00:00:00Z</published>
    <summary type="text">Build the site into public/ (prints next pending embargo)</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Build the site into public/ (prints next pending embargo)&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon build [flags]&#xA;&#xA;Build the site into public/ (prints next pending embargo)&#xA;&#xA;Flags:&#xA;  -h, --help           Show context-sensitive help.&#xA;      --version        Print version and exit&#xA;&#xA;      --env=STRING     Build a named environment (applies its overrides)&#xA;  -v, --verbose        Log each step (sources, files, feeds)&#xA;      --generate-ai    Generate uncached AI media (gen: images and TTS audio)&#xA;                       via the configured providers&#xA;      --regenerate     Force a fresh render of generated media even if cached&#xA;                       (re-voice audio / re-roll images); implies work only with&#xA;                       --generate-ai&#xA;      --no-backoff     Don&amp;#39;t retry rate-limited generation; fail fast and warn&#xA;                       instead of backing off&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon next-build-time</title>
    <id>https://docs.colophon.blog/cli/next-build-time/</id>
    <link href="https://docs.colophon.blog/cli/next-build-time/" rel="alternate"></link>
    <updated>2001-12-10T00:00:00Z</updated>
    <published>2001-12-10T00:00:00Z</published>
    <summary type="text">Print the next pending publish_after timestamp (for CI scheduling)</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Print the next pending publish_after timestamp (for CI scheduling)&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon next-build-time [flags]&#xA;&#xA;Print the next pending publish_after timestamp (for CI scheduling)&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;      --json       Output JSON&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon serve</title>
    <id>https://docs.colophon.blog/cli/serve/</id>
    <link href="https://docs.colophon.blog/cli/serve/" rel="alternate"></link>
    <updated>2001-12-09T00:00:00Z</updated>
    <published>2001-12-09T00:00:00Z</published>
    <summary type="text">Serve every environment locally with live reload</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Serve every environment locally with live reload&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon serve [flags]&#xA;&#xA;Serve every environment locally with live reload&#xA;&#xA;Flags:&#xA;  -h, --help            Show context-sensitive help.&#xA;      --version         Print version and exit&#xA;&#xA;      --addr=&amp;#34;:8080&amp;#34;    Address to listen on&#xA;      --open=STRING     Open a target in the browser: latest | home | sitemap |&#xA;                        atom | rss | json | robots | &amp;lt;slug&amp;gt;&#xA;      --showcase        Inject a built-in /showcase/ page (embedded in the&#xA;                        binary, never written to content) demonstrating every&#xA;                        markdown/style feature in the active theme&#xA;  -v, --verbose         Log each rebuild and attach source locations&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon publish</title>
    <id>https://docs.colophon.blog/cli/publish/</id>
    <link href="https://docs.colophon.blog/cli/publish/" rel="alternate"></link>
    <updated>2001-12-08T00:00:00Z</updated>
    <published>2001-12-08T00:00:00Z</published>
    <summary type="text">Build and deploy/mirror to publishers (gated)</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Build and deploy/mirror to publishers (gated)&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon publish --env=ENV,... [flags]&#xA;&#xA;Build and deploy/mirror to publishers (gated)&#xA;&#xA;Flags:&#xA;  -h, --help             Show context-sensitive help.&#xA;      --version          Print version and exit&#xA;&#xA;      --env=ENV,...      Environment to publish; repeat for several&#xA;      --allow-publish    Deploy environments that set allow_publish: false&#xA;      --create           Create the destination (e.g. a Pages project) if it&#xA;                         doesn&amp;#39;t exist&#xA;      --generate-ai      Generate uncached AI media (gen: images and TTS audio)&#xA;                         via the configured providers before deploying&#xA;      --regenerate       Force a fresh render of generated media even if cached&#xA;                         (re-voice audio / re-roll images); needs --generate-ai&#xA;      --no-backoff       Don&amp;#39;t retry rate-limited generation; fail fast and warn&#xA;                         instead of backing off&#xA;  -v, --verbose          Log each step (sources, files, publisher actions)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon themes</title>
    <id>https://docs.colophon.blog/cli/themes/</id>
    <link href="https://docs.colophon.blog/cli/themes/" rel="alternate"></link>
    <updated>2001-12-07T00:00:00Z</updated>
    <published>2001-12-07T00:00:00Z</published>
    <summary type="text">List built-in themes or eject one to customise</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;List built-in themes or eject one to customise&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon themes &amp;lt;command&amp;gt; [flags]&#xA;&#xA;List built-in themes or eject one to customise&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;Commands:&#xA;  themes list&#xA;    List the built-in themes&#xA;&#xA;  themes eject &amp;lt;name&amp;gt; [flags]&#xA;    Copy a built-in theme into themes/&amp;lt;name&amp;gt;/ to customise&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-themes-list&#34;&gt;colophon themes list&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon themes list&#xA;&#xA;List the built-in themes&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-themes-eject&#34;&gt;colophon themes eject&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon themes eject &amp;lt;name&amp;gt; [flags]&#xA;&#xA;Copy a built-in theme into themes/&amp;lt;name&amp;gt;/ to customise&#xA;&#xA;Arguments:&#xA;  &amp;lt;name&amp;gt;    Built-in theme to copy (e.g. default, minimal)&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;      --force      Overwrite an existing themes/&amp;lt;name&amp;gt;/ directory&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon authors</title>
    <id>https://docs.colophon.blog/cli/authors/</id>
    <link href="https://docs.colophon.blog/cli/authors/" rel="alternate"></link>
    <updated>2001-12-06T00:00:00Z</updated>
    <published>2001-12-06T00:00:00Z</published>
    <summary type="text">List authors (the bylines) or show one</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;List authors (the bylines) or show one&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon authors (author) &amp;lt;command&amp;gt; [flags]&#xA;&#xA;List authors (the bylines) or show one&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;Commands:&#xA;  authors (author) list [flags]&#xA;    List authors (the bylines)&#xA;&#xA;  authors (author) show &amp;lt;author&amp;gt; [flags]&#xA;    Show one author&amp;#39;s full details&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-authors-list&#34;&gt;colophon authors list&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon authors (author) list [flags]&#xA;&#xA;List authors (the bylines)&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;      --json       Output JSON&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-authors-show&#34;&gt;colophon authors show&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon authors (author) show &amp;lt;author&amp;gt; [flags]&#xA;&#xA;Show one author&amp;#39;s full details&#xA;&#xA;Arguments:&#xA;  &amp;lt;author&amp;gt;    Author id&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;      --json       Output JSON&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon persona</title>
    <id>https://docs.colophon.blog/cli/persona/</id>
    <link href="https://docs.colophon.blog/cli/persona/" rel="alternate"></link>
    <updated>2001-12-05T00:00:00Z</updated>
    <published>2001-12-05T00:00:00Z</published>
    <summary type="text">List writing voices or emit write-as context</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;List writing voices or emit write-as context&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon persona (personas) &amp;lt;command&amp;gt; [flags]&#xA;&#xA;List writing voices or emit write-as context&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;Commands:&#xA;  persona (personas) list [flags]&#xA;    List personas&#xA;&#xA;  persona (personas) context [&amp;lt;persona&amp;gt;] [flags]&#xA;    Emit style guide + top-K exemplars for AI-assisted writing&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-persona-list&#34;&gt;colophon persona list&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon persona (personas) list [flags]&#xA;&#xA;List personas&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;      --json       Output JSON&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-persona-context&#34;&gt;colophon persona context&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon persona (personas) context [&amp;lt;persona&amp;gt;] [flags]&#xA;&#xA;Emit style guide + top-K exemplars for AI-assisted writing&#xA;&#xA;Arguments:&#xA;  [&amp;lt;persona&amp;gt;]    Persona id (defaults to the only persona, or &amp;#39;default&amp;#39;)&#xA;&#xA;Flags:&#xA;  -h, --help            Show context-sensitive help.&#xA;      --version         Print version and exit&#xA;&#xA;      --topic=STRING    Topic/outline to retrieve exemplars for (ranked by&#xA;                        relevance)&#xA;      --tag=TAG,...     Only draw exemplars tagged with this tag; repeatable&#xA;      --top-k=3         Max number of exemplars to emit&#xA;      --length=INT      Per-exemplar character cap (0 = the default); ignored&#xA;                        with --full&#xA;      --full            Emit each exemplar&amp;#39;s full body (still bounded by&#xA;                        --budget)&#xA;      --budget=10000    Total character budget across all exemplars&#xA;      --json            Output JSON&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon sources</title>
    <id>https://docs.colophon.blog/cli/sources/</id>
    <link href="https://docs.colophon.blog/cli/sources/" rel="alternate"></link>
    <updated>2001-12-04T00:00:00Z</updated>
    <published>2001-12-04T00:00:00Z</published>
    <summary type="text">Show where content lives and how posts are marked publishable</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Show where content lives and how posts are marked publishable&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon sources (source) [flags]&#xA;&#xA;Show where content lives and how posts are marked publishable&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;      --json       Output JSON&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon posts</title>
    <id>https://docs.colophon.blog/cli/posts/</id>
    <link href="https://docs.colophon.blog/cli/posts/" rel="alternate"></link>
    <updated>2001-12-03T00:00:00Z</updated>
    <published>2001-12-03T00:00:00Z</published>
    <summary type="text">List content entries (for editing and cross-referencing)</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;List content entries (for editing and cross-referencing)&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon posts (post) [flags]&#xA;&#xA;List content entries (for editing and cross-referencing)&#xA;&#xA;Flags:&#xA;  -h, --help              Show context-sensitive help.&#xA;      --version           Print version and exit&#xA;&#xA;      --author=STRING     Only entries with this author id&#xA;      --persona=STRING    Only entries with this persona id&#xA;      --tag=TAG,...       Only entries carrying any of these tags&#xA;      --json              Output JSON&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon search</title>
    <id>https://docs.colophon.blog/cli/search/</id>
    <link href="https://docs.colophon.blog/cli/search/" rel="alternate"></link>
    <updated>2001-12-02T00:00:00Z</updated>
    <published>2001-12-02T00:00:00Z</published>
    <summary type="text">Search content (lexical or semantic)</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Search content (lexical or semantic)&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon search [&amp;lt;query&amp;gt;] [flags]&#xA;&#xA;Search content (lexical or semantic)&#xA;&#xA;Arguments:&#xA;  [&amp;lt;query&amp;gt;]    Search query&#xA;&#xA;Flags:&#xA;  -h, --help        Show context-sensitive help.&#xA;      --version     Print version and exit&#xA;&#xA;      --limit=20    Maximum results&#xA;      --json        Output JSON&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon skills</title>
    <id>https://docs.colophon.blog/cli/skills/</id>
    <link href="https://docs.colophon.blog/cli/skills/" rel="alternate"></link>
    <updated>2001-12-01T00:00:00Z</updated>
    <published>2001-12-01T00:00:00Z</published>
    <summary type="text">Install colophon&#39;s authoring skills into a detected agent harness</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Install colophon&#39;s authoring skills into a detected agent harness&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon skills &amp;lt;command&amp;gt; [flags]&#xA;&#xA;Install colophon&amp;#39;s authoring skills into a detected agent harness&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;Commands:&#xA;  skills detect [flags]&#xA;    Show which agent harnesses are present and the install status of each skill&#xA;&#xA;  skills install [flags]&#xA;    Install/update the skills into detected harnesses (or --harness/--dir)&#xA;&#xA;  skills list&#xA;    List the skills embedded in this binary&#xA;&#xA;  skills uninstall [flags]&#xA;    Remove colophon-managed skills from detected harnesses (or --harness/--dir)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-skills-detect&#34;&gt;colophon skills detect&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon skills detect [flags]&#xA;&#xA;Show which agent harnesses are present and the install status of each skill&#xA;&#xA;Flags:&#xA;  -h, --help          Show context-sensitive help.&#xA;      --version       Print version and exit&#xA;&#xA;      --dir=STRING    Inspect a specific skills directory instead of detecting&#xA;                      harnesses&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-skills-install&#34;&gt;colophon skills install&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon skills install [flags]&#xA;&#xA;Install/update the skills into detected harnesses (or --harness/--dir)&#xA;&#xA;Flags:&#xA;  -h, --help                   Show context-sensitive help.&#xA;      --version                Print version and exit&#xA;&#xA;      --harness=HARNESS,...    Install only for these harness ids&#xA;                               (claude,codex,opencode,cursor,copilot,gemini)&#xA;      --dir=STRING             Install into a specific directory instead of&#xA;                               detected harnesses&#xA;      --all                    Install for every supported harness, detected or&#xA;                               not&#xA;      --claude=&amp;#34;ask&amp;#34;           How to install for Claude Code:&#xA;                               ask|marketplace|files|skip&#xA;      --force                  Overwrite locally-modified or unmanaged skills&#xA;      --dry-run                Show what would change without writing&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-skills-list&#34;&gt;colophon skills list&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon skills list&#xA;&#xA;List the skills embedded in this binary&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-skills-uninstall&#34;&gt;colophon skills uninstall&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon skills uninstall [flags]&#xA;&#xA;Remove colophon-managed skills from detected harnesses (or --harness/--dir)&#xA;&#xA;Flags:&#xA;  -h, --help                   Show context-sensitive help.&#xA;      --version                Print version and exit&#xA;&#xA;      --harness=HARNESS,...    Uninstall only for these harness ids&#xA;      --dir=STRING             Uninstall from a specific directory&#xA;      --all                    Consider every supported harness, detected or not&#xA;      --force                  Also remove locally-modified or unmanaged skills&#xA;      --dry-run                Show what would change without deleting&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon webmention</title>
    <id>https://docs.colophon.blog/cli/webmention/</id>
    <link href="https://docs.colophon.blog/cli/webmention/" rel="alternate"></link>
    <updated>2001-11-30T00:00:00Z</updated>
    <published>2001-11-30T00:00:00Z</published>
    <summary type="text">Send webmentions to the sites your live posts link to (run after publish)</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Send webmentions to the sites your live posts link to (run after publish)&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon webmention &amp;lt;command&amp;gt; [flags]&#xA;&#xA;Send webmentions to the sites your live posts link to (run after publish)&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;Commands:&#xA;  webmention send [flags]&#xA;    Notify the sites your live posts link to (run after publish)&#xA;&#xA;  webmention fetch [flags]&#xA;    Pull received mentions from the configured receiver into the local cache&#xA;&#xA;  webmention publish [flags]&#xA;    Fetch mentions and deploy only _mentions/ (refresh responses without a full&#xA;    re-upload)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-webmention-send&#34;&gt;colophon webmention send&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon webmention send [flags]&#xA;&#xA;Notify the sites your live posts link to (run after publish)&#xA;&#xA;Flags:&#xA;  -h, --help                Show context-sensitive help.&#xA;      --version             Print version and exit&#xA;&#xA;      --env=&amp;#34;production&amp;#34;    Environment whose built output to scan&#xA;      --dry-run             Discover endpoints and report, but do not POST&#xA;  -v, --verbose             Log each link and endpoint&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-webmention-fetch&#34;&gt;colophon webmention fetch&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon webmention fetch [flags]&#xA;&#xA;Pull received mentions from the configured receiver into the local cache&#xA;&#xA;Flags:&#xA;  -h, --help             Show context-sensitive help.&#xA;      --version          Print version and exit&#xA;&#xA;      --domain=STRING    Domain to fetch mentions for (default: the site&#xA;                         base_url host)&#xA;  -v, --verbose          Log each post that received mentions&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;colophon-webmention-publish&#34;&gt;colophon webmention publish&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon webmention publish [flags]&#xA;&#xA;Fetch mentions and deploy only _mentions/ (refresh responses without a full&#xA;re-upload)&#xA;&#xA;Flags:&#xA;  -h, --help                Show context-sensitive help.&#xA;      --version             Print version and exit&#xA;&#xA;      --env=&amp;#34;production&amp;#34;    Environment to refresh mentions on&#xA;      --allow-publish       Deploy environments that set allow_publish: false&#xA;      --domain=STRING       Domain to fetch mentions for (default: the site&#xA;                            base_url host)&#xA;  -v, --verbose             Log each step&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon syndicate</title>
    <id>https://docs.colophon.blog/cli/syndicate/</id>
    <link href="https://docs.colophon.blog/cli/syndicate/" rel="alternate"></link>
    <updated>2001-11-29T00:00:00Z</updated>
    <published>2001-11-29T00:00:00Z</published>
    <summary type="text">Cross-post (POSSE) to the environment&#39;s configured syndicators</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Cross-post (POSSE) to the environment&#39;s configured syndicators&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon syndicate [flags]&#xA;&#xA;Cross-post (POSSE) to the environment&amp;#39;s configured syndicators&#xA;&#xA;Flags:&#xA;  -h, --help                Show context-sensitive help.&#xA;      --version             Print version and exit&#xA;&#xA;      --env=&amp;#34;production&amp;#34;    Environment to syndicate&#xA;      --allow-publish       Run for environments gated by allow_publish:&#xA;                            false (required to post)&#xA;      --dry-run             Show what would be syndicated; post nothing,&#xA;                            write no ledger&#xA;      --resync              Re-edit every already-syndicated copy to the post&amp;#39;s&#xA;                            current content, ignoring fingerprints (one-shot;&#xA;                            only drivers that can edit, e.g. mastodon/bluesky)&#xA;  -v, --verbose             Log each candidate&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon doctor</title>
    <id>https://docs.colophon.blog/cli/doctor/</id>
    <link href="https://docs.colophon.blog/cli/doctor/" rel="alternate"></link>
    <updated>2001-11-28T00:00:00Z</updated>
    <published>2001-11-28T00:00:00Z</published>
    <summary type="text">Validate the project config and report problems</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Validate the project config and report problems&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon doctor [flags]&#xA;&#xA;Validate the project config and report problems&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&#xA;      --prune      Delete orphaned generated assets (AI images/audio no content&#xA;                   references)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>colophon env</title>
    <id>https://docs.colophon.blog/cli/env/</id>
    <link href="https://docs.colophon.blog/cli/env/" rel="alternate"></link>
    <updated>2001-11-27T00:00:00Z</updated>
    <published>2001-11-27T00:00:00Z</published>
    <summary type="text">List the environment variables this project uses</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;List the environment variables this project uses&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;Usage: colophon env [flags]&#xA;&#xA;List the environment variables this project uses&#xA;&#xA;Flags:&#xA;  -h, --help       Show context-sensitive help.&#xA;      --version    Print version and exit&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from the binary&#39;s &lt;code&gt;--help&lt;/code&gt; output by &lt;code&gt;tools/gendocs&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Configuration reference</title>
    <id>https://docs.colophon.blog/reference/config/</id>
    <link href="https://docs.colophon.blog/reference/config/" rel="alternate"></link>
    <updated>2001-11-26T00:00:00Z</updated>
    <published>2001-11-26T00:00:00Z</published>
    <summary type="text">The annotated colophon.yaml reference: every option, default and shape in one file.</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/colophon.reference.yaml — do not edit by hand. --&gt;&#xA;&lt;p&gt;Every &lt;code&gt;colophon.yaml&lt;/code&gt; option in one annotated file. It is &lt;strong&gt;not&lt;/strong&gt; a starter config — &lt;code&gt;colophon init&lt;/code&gt; writes a lean one — but the single place where every knob, default and shape is written down. Copy the bits you need.&lt;/p&gt;&#xA;&lt;p&gt;Any string value may interpolate the environment with &lt;code&gt;{env:VAR}&lt;/code&gt; or &lt;code&gt;{env:VAR:-fallback}&lt;/code&gt;; deploy secrets are always read from the environment and never stored in the file.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# ─────────────────────────────────────────────────────────────────────────────&#xA;# colophon.yaml — ANNOTATED REFERENCE&#xA;#&#xA;# Not a starter config (`colophon init` writes a lean one). This shows every&#xA;# option in one place, with a few list entries set side-by-side so the shapes&#xA;# are clear. Copy the bits you need.&#xA;#&#xA;# Markers used in comments below:&#xA;#   [required]   must be set; no default&#xA;#   [optional]   may be omitted; the shown value is the default&#xA;#   [inherits]   only meaningful inside a profile — when omitted, the value is&#xA;#                taken from that modality&amp;#39;s default block (see &amp;#34;generation&amp;#34;)&#xA;#&#xA;# Any string may interpolate the environment: {env:VAR} or {env:VAR:-fallback}.&#xA;# Deploy secrets are read from the environment at build time and are NEVER stored&#xA;# in this file.&#xA;# ─────────────────────────────────────────────────────────────────────────────&#xA;&#xA;&#xA;# ═══ sites ═══════════════════════════════════════════════════════════════════&#xA;# One or more sites built from the shared content. Most projects have exactly one.&#xA;sites:&#xA;  - id: main                                   # [required] stable id, used in URLs/serve paths&#xA;    title: &amp;#34;My Blog&amp;#34;                           # [required]&#xA;    base_url: &amp;#34;{env:SITE_URL:-http://localhost:8080}&amp;#34;  # [required] canonical origin&#xA;    theme: press                               # [optional] default: the bundled &amp;#34;default&amp;#34; theme&#xA;    lang: en                                   # [optional] site default BCP-47 language (default &amp;#34;en&amp;#34;)&#xA;    languages: [en, es]                        # [optional] enable multi-language posts. A file&#xA;                                               #   &amp;lt;slug&amp;gt;.es.md is the Spanish translation of &amp;lt;slug&amp;gt;.md,&#xA;                                               #   published at /es/&amp;lt;slug&amp;gt;/, linked by hreflang + a&#xA;                                               #   theme language selector. The default lang stays at /.&#xA;    personas: [default]                        # [optional] writing voices available to the agent&#xA;    federation:&#xA;      feeds: [rss, atom, json]                 # [optional] default: none; emit at least one&#xA;    # search accepts a bare mode string OR a block. Bare form: `search: lexical`.&#xA;    search:&#xA;      mode: lexical                            # [optional] lexical | semantic | off (default off)&#xA;      fuzzy: true                              # [optional] default false; trigram+Levenshtein typo tolerance&#xA;    # Derived slide decks. Site defaults; a post overrides either key in its `slides:`&#xA;    # frontmatter (shallow/by-key — a key replaces this value, omitted keys inherit).&#xA;    slides:&#xA;      enabled: false                           # [optional] default off; a post opts in with `slides: true`&#xA;      split: [h2]                              # [optional] slide boundaries (a list). default: every heading.&#xA;                                               #   targets: h1..h6, hr, splitslide, image, table, code,&#xA;                                               #   math, diagram, audio, video, text:&amp;lt;match&amp;gt;&#xA;    # Reader analytics for THIS site. One block per provider; inert until configured.&#xA;    analytics:&#xA;      statsfactory:&#xA;        server_url: &amp;#34;{env:STATSFACTORY_SERVER_URL:-}&amp;#34;&#xA;        app_key: &amp;#34;{env:STATSFACTORY_APP_KEY:-}&amp;#34;&#xA;      # google_analytics:                      # GA4 — sets cookies, brings its own consent duties&#xA;      #   measurement_id: &amp;#34;{env:GA_MEASUREMENT_ID:-}&amp;#34;&#xA;    # Routing rewrites matching output paths to a publisher&amp;#39;s object store instead of&#xA;    # shipping them with the page host (which has a file/size budget). Inert until that&#xA;    # publisher resolves a public URL, so local builds keep assets co-located.&#xA;    routing:&#xA;      - match: &amp;#34;**assets/**&amp;#34;                   # co-located post assets + the root /assets tree&#xA;        publisher: r2&#xA;      - match: &amp;#34;_search/**&amp;#34;                    # keep the search index off the page-host budget&#xA;        publisher: r2&#xA;&#xA;&#xA;# ═══ sources ═════════════════════════════════════════════════════════════════&#xA;# Where content comes from. Driver selects the implementation; the rest is&#xA;# driver-specific. List several and their content is merged.&#xA;sources:&#xA;  - id: content                                # [required]&#xA;    driver: md-dir                             # [required] a plain Markdown directory&#xA;    path: ./content&#xA;  - id: vault                                  # a second source, merged with the first&#xA;    driver: obsidian&#xA;    vault: &amp;#34;{env:OBSIDIAN_VAULT:-}&amp;#34;            # vault root; empty → contributes nothing&#xA;    path: &amp;#34;{env:BLOG_PATH:-}&amp;#34;                  # vault-relative folder(s); empty → whole vault&#xA;    tag: &amp;#34;{env:BLOG_TAGS:-}&amp;#34;                   # publish by tag(s); empty → use publish_required&#xA;    publish_required: false&#xA;&#xA;&#xA;# ═══ publishers ══════════════════════════════════════════════════════════════&#xA;# Pure mechanism: HOW to deploy. WHAT/WHERE is decided by environments below.&#xA;# Deploy credentials always come from the environment, never from these fields.&#xA;publishers:&#xA;  - id: local                                  # offline build target, for diffing output&#xA;    driver: local&#xA;    path: ./dist&#xA;  - id: cf                                     # HTML/static host&#xA;    driver: cloudflare-pages&#xA;    project: &amp;#34;{env:CF_PAGES_PROJECT:-my-blog}&amp;#34;&#xA;    account_id: &amp;#34;{env:CLOUDFLARE_ACCOUNT_ID}&amp;#34;&#xA;  - id: r2                                     # S3-compatible object store (routing target above)&#xA;    driver: cloudflare-r2&#xA;    bucket: &amp;#34;{env:R2_BUCKET:-my-blog-assets}&amp;#34;&#xA;    account_id: &amp;#34;{env:CLOUDFLARE_ACCOUNT_ID}&amp;#34;&#xA;    public_url: &amp;#34;{env:R2_PUBLIC_URL:-}&amp;#34;        # empty → auto-discovered on publish&#xA;&#xA;&#xA;# ═══ environments ════════════════════════════════════════════════════════════&#xA;# Named build+deploy profiles. No name is privileged. An environment picks&#xA;# publishers, toggles drafts, and may override a few site fields + select&#xA;# generation profiles (see &amp;#34;generation&amp;#34;). Build/deploy with --env &amp;lt;name&amp;gt;.&#xA;environments:&#xA;  - name: production                           # [required]&#xA;    publish: [cf, r2]                          # [required] publisher ids to deploy to&#xA;    allow_publish: false                       # [optional] default true; false → require --allow-publish&#xA;    base_url: &amp;#34;{env:BASE_URL:-}&amp;#34;               # [optional] override the site base_url here&#xA;    overrides:                                 # [optional] per-publisher Settings overrides, keyed by id&#xA;      cf: { branch: main }&#xA;&#xA;  - name: preview&#xA;    publish: [cf, r2]&#xA;    include_drafts: true                       # [optional] default false&#xA;    title: &amp;#34;My Blog (preview)&amp;#34;                 # [optional] override the site title&#xA;    image_profile: draft                       # [optional] use the cheap image profile in this env&#xA;    speech_profile: minimax                    # [optional] use the minimax voice profile in this env&#xA;    slides:                                    # [optional] override the site slides defaults here&#xA;      enabled: true                            #   e.g. decks on in preview, off in production&#xA;    overrides:&#xA;      cf: { branch: preview }&#xA;&#xA;  - name: dist                                 # local copy; ungated, no creds&#xA;    publish: [local]&#xA;    include_drafts: true&#xA;&#xA;  - name: text                                 # preview an alternate theme before promoting it&#xA;    publish: [local]&#xA;    include_drafts: true&#xA;    theme: minimal                             # [optional] override the site theme&#xA;    overrides:&#xA;      local: { path: ./dist-text }&#xA;&#xA;&#xA;# ═══ generation ══════════════════════════════════════════════════════════════&#xA;# Optional AI media generation. Two modalities: image (satisfies `gen:` refs) and&#xA;# speech (reads posts with `audio: true`). Empty → the feature is off.&#xA;#&#xA;# Each modality has a DEFAULT block plus named PROFILES (a map). A profile inherits&#xA;# every field from the default block and overrides only what it lists. The default&#xA;# block is the implicit profile named &amp;#34;default&amp;#34;.&#xA;#&#xA;# Which profile renders a given asset (narrowest scope wins):&#xA;#&#xA;#   default block&#xA;#     ◀ environment   speech_profile: / image_profile:&#xA;#       ◀ post         audio_profile: / image_profile:   (frontmatter)&#xA;#         ◀ per-ref    &amp;lt;gen:hero?profile=…&amp;gt;              (image refs only)&#xA;#           ◀ post scalar overrides (audio_voice, …)&#xA;#&#xA;# Merge rule: scalars REPLACE, maps DEEP-MERGE (e.g. image `defaults`). Voice/model&#xA;# ids are provider-specific, so a profile that changes provider should also set them.&#xA;generation:&#xA;  enabled: true                                # [optional] default true; master off-switch for all AI gen&#xA;&#xA;  # ── images ──────────────────────────────────────────────────────────────────&#xA;  image:&#xA;    enabled: true                              # [optional] default true; per-modality switch&#xA;    provider: google                           # [required] google|minimax|openai|xai|together|deepinfra|custom&#xA;    model: gemini-3.1-flash-image              # [optional] default: the provider profile&amp;#39;s default model&#xA;    api_key: &amp;#34;{env:GEMINI_API_KEY}&amp;#34;            # [optional] default: the provider&amp;#39;s env var (kept out of config)&#xA;    base_url: &amp;#34;&amp;#34;                               # [optional] override endpoint (required for `custom`)&#xA;    api_path: &amp;#34;&amp;#34;                               # [optional] override request path (OpenAI-compatible hosts)&#xA;    output_dir: content/assets/generated       # [optional] default shown; cache + sidecars (committed)&#xA;    concurrency: 5                             # [optional] default 5; max parallel generations&#xA;    reuse: exact                               # [optional] exact (re-render on provider/model change) | content&#xA;    system_prompt: &amp;#34;&amp;#34;                          # [optional] house style; default: the theme&amp;#39;s&#xA;    defaults:                                  # [optional] tuning params applied to every request&#xA;      aspect: &amp;#34;16:9&amp;#34;&#xA;    postprocess:&#xA;      trim_letterbox: true                     # [optional] default true; strip baked-in letterbox bars&#xA;    profiles:                                  # [optional] named alternates; each inherits the block above&#xA;      draft:                                   # cheap/fast — e.g. for previews&#xA;        provider: together                     # [inherits] all other fields from the default image block&#xA;        model: black-forest-labs/FLUX.1-schnell&#xA;      poster:                                  # premium — e.g. for hero art&#xA;        provider: openai&#xA;        model: gpt-image-1&#xA;        defaults: { quality: high }            # deep-merged over the default `defaults` (aspect kept)&#xA;&#xA;  # ── speech ──────────────────────────────────────────────────────────────────&#xA;  speech:&#xA;    enabled: true                              # [optional] default true; also the per-post `audio:` default&#xA;    provider: elevenlabs                       # [required] elevenlabs | minimax&#xA;    voice: &amp;#34;{env:ELEVENLABS_VOICE:-Ee4WTXzxagFpoj4PUkHV}&amp;#34;  # [optional] default: provider profile&amp;#39;s default voice&#xA;    model: &amp;#34;&amp;#34;                                  # [optional] default: provider profile&amp;#39;s default model&#xA;    api_key: &amp;#34;{env:ELEVENLABS_API_KEY}&amp;#34;        # [optional] default: the provider&amp;#39;s env var&#xA;    base_url: &amp;#34;&amp;#34;                               # [optional] override endpoint&#xA;    api_path: &amp;#34;&amp;#34;                               # [optional] override request path&#xA;    output_dir: content/assets/generated       # [optional] default shown; clip cache + sidecars&#xA;    concurrency: 5                             # [optional] default 5; max parallel clips&#xA;    reuse: exact                               # [optional] exact | content (reuse a prior voice&amp;#39;s reading)&#xA;    pronunciation_dict: en_GB                  # [optional] built-in name (contrib/pronunciation: en_GB, es_ES) or a&#xA;                                               #   YAML path. A naked ref applies to the SITE DEFAULT LANGUAGE only;&#xA;                                               #   per-language: {en: en_GB, es: es_ES} (BCP-47 keys, es-MX → es).&#xA;                                               #   A language with no entry gets no dictionary.&#xA;    transcript:                                # [optional] how post content becomes spoken text&#xA;      wrap_up: true                            # [optional] default true; append a closing &amp;#34;visit the post&amp;#34; note&#xA;      expand_acronyms: true                    # [optional] default true; read SSH as &amp;#34;Secure Shell&amp;#34;&#xA;      blocks:                                  # [optional] per block-type handling: cue|drop|keep|spell&#xA;        code: cue                              #   (defaults: code/table/diagram/math_display→cue,&#xA;        table: cue                             #    math_inline→drop, inline_code→spell, else keep)&#xA;    profiles:                                  # [optional] named alternates; each inherits the block above&#xA;      minimax:                                 # a different provider needs its own voice&#xA;        provider: minimax&#xA;        voice: English_Trustworth_Man          # [inherits] pronunciation_dict/transcript/etc from default&#xA;      narrator-fast:                           # same provider, faster/cheaper model + a different voice&#xA;        model: eleven_turbo_v2_5&#xA;        voice: &amp;#34;{env:ELEVENLABS_NARRATOR_VOICE:-}&amp;#34;&#xA;&#xA;&#xA;# ═══ telemetry ═══════════════════════════════════════════════════════════════&#xA;# The colophon APP&amp;#39;s own anonymous usage reporting (build/source/publisher TYPES —&#xA;# never your content), separate from the site analytics above. Governs only itself.&#xA;telemetry:&#xA;  enabled: true                                # [optional] default true; false (or COLOPHON_TELEMETRY=off) disables&#xA;&#xA;&#xA;# ═══ per-post selection (frontmatter — NOT this file) ════════════════════════&#xA;# Posts pick a profile in their own frontmatter. The selector keys are identical&#xA;# at every scope (config block, environment, frontmatter): speech_profile /&#xA;# image_profile.&#xA;#&#xA;#   ---&#xA;#   audio: true&#xA;#   speech_profile: minimax          # → generation.speech.profiles.minimax&#xA;#   audio_voice: SomeOtherVoiceId    # still wins over the profile&amp;#39;s voice&#xA;#   image_profile: poster            # → generation.image.profiles.poster (hero + inline images)&#xA;#   ---&#xA;#&#xA;#   ![cover](&amp;lt;gen:hero?profile=poster&amp;gt;)   # per-image override, beats image_profile&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/colophon.reference.yaml&#34;&gt;&lt;code&gt;docs/colophon.reference.yaml&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Changelog</title>
    <id>https://docs.colophon.blog/reference/changelog/</id>
    <link href="https://docs.colophon.blog/reference/changelog/" rel="alternate"></link>
    <updated>2001-11-25T00:00:00Z</updated>
    <published>2001-11-25T00:00:00Z</published>
    <summary type="text">User-facing changes by release. Each entry points at the guide where the feature is documented in full (or where it should be, when end-user docs catch up).</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/changelog.md — do not edit by hand. --&gt;&#xA;&lt;p&gt;User-facing changes by release. Each entry points at the guide where the feature is documented&#xA;in full (or where it should be, when end-user docs catch up).&lt;/p&gt;&#xA;&lt;h2 id=&#34;v0033&#34;&gt;v0.0.33&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;xAI (Grok Imagine) image provider.&lt;/strong&gt; &lt;code&gt;generation.image.provider: xai&lt;/code&gt; targets xAI&#39;s&#xA;OpenAI-compatible images endpoint (default model &lt;code&gt;grok-imagine-image-quality&lt;/code&gt;, key from&#xA;&lt;code&gt;XAI_API_KEY&lt;/code&gt;); the standard &lt;code&gt;aspect&lt;/code&gt; param is sent as xAI&#39;s &lt;code&gt;aspect_ratio&lt;/code&gt;. See&#xA;&lt;a href=&#34;/guides/image-generation/#providers&#34;&gt;Image &amp;amp; audio generation → Providers&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Per-language pronunciation dictionaries.&lt;/strong&gt; &lt;code&gt;pronunciation_dict:&lt;/code&gt; now takes either a naked ref&#xA;(&lt;code&gt;en_GB&lt;/code&gt;) — which applies to the &lt;strong&gt;site default language only&lt;/strong&gt;, no longer to every language — or a&#xA;map keyed by BCP-47 tag: &lt;code&gt;pronunciation_dict: {en: en_GB, es: es_ES}&lt;/code&gt; (matched exact-then-base, so&#xA;&lt;code&gt;es-MX&lt;/code&gt; uses &lt;code&gt;es&lt;/code&gt;). A language with no entry gets no dictionary, so an English dict never rewrites a&#xA;Spanish reading. A Spanish dict (&lt;code&gt;es_ES&lt;/code&gt;) is now bundled alongside &lt;code&gt;en_GB&lt;/code&gt;. For ElevenLabs, each dict&#xA;syncs as its own account dictionary (named &lt;code&gt;colophon:&amp;lt;site&amp;gt;/&amp;lt;ref&amp;gt;&lt;/code&gt;); a previously-synced dictionary&#xA;is adopted when its rules are unchanged. Spoken block cues (&amp;quot;Here, the post shows a code example…&amp;quot;)&#xA;now also follow a translation&#39;s language detected from its &lt;code&gt;&amp;lt;slug&amp;gt;.&amp;lt;lang&amp;gt;.md&lt;/code&gt; filename, not just an&#xA;explicit frontmatter &lt;code&gt;lang:&lt;/code&gt;. Default-language readings keep their content identity — no re-render.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Multi-language posts (translations).&lt;/strong&gt; Set &lt;code&gt;languages:&lt;/code&gt; on the site and add a &lt;code&gt;&amp;lt;slug&amp;gt;.&amp;lt;lang&amp;gt;.md&lt;/code&gt;&#xA;file (e.g. &lt;code&gt;my-post.es.md&lt;/code&gt;) to publish a translation at &lt;code&gt;/&amp;lt;lang&amp;gt;/&amp;lt;slug&amp;gt;/&lt;/code&gt;, linked to the original by&#xA;its base slug. Translations emit &lt;code&gt;hreflang&lt;/code&gt; alternates (with &lt;code&gt;x-default&lt;/code&gt;); the &lt;strong&gt;press&lt;/strong&gt; theme shows&#xA;a &lt;strong&gt;language selector&lt;/strong&gt; in the post header and a dismissible &amp;quot;available in your language&amp;quot; banner&#xA;driven by the browser&#39;s preference (no forced redirect). Each translation is a normal post with its&#xA;own reading/feeds/glossary/deck. See &lt;a href=&#34;/start/content/#multiple-languages-translations&#34;&gt;Authoring content → Multiple languages&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Slide decks: styled by the site theme + a fuller reader.&lt;/strong&gt; A published deck now links the active&#xA;theme&#39;s stylesheet and renders content in the theme&#39;s &lt;code&gt;.prose&lt;/code&gt; class, so quotes, callouts, code,&#xA;tables and Mermaid look like the site and theme authors can style &lt;code&gt;.slide*&lt;/code&gt; themselves. The reader&#xA;gained: &lt;strong&gt;touch/swipe&lt;/strong&gt; navigation and on-screen &lt;strong&gt;prev/next&lt;/strong&gt; buttons; on-screen &lt;strong&gt;presenter&lt;/strong&gt; and&#xA;&lt;strong&gt;fullscreen&lt;/strong&gt; toggles (so they work without a keyboard); a &lt;strong&gt;light/dark&lt;/strong&gt; toggle (reusing the&#xA;theme&#39;s &lt;code&gt;data-theme&lt;/code&gt;); a large &lt;strong&gt;mobile presenter card&lt;/strong&gt; (the notes fill the phone as a teleprompter&#xA;while the slide shows on the big screen); and an &lt;strong&gt;autocue&lt;/strong&gt; that auto-scrolls each slide&#39;s notes and&#xA;auto-advances at a reading pace — adjustable live with &lt;code&gt;+&lt;/code&gt;/&lt;code&gt;−&lt;/code&gt; (or the on-screen slower/faster&#xA;buttons), shown in the counter and remembered. Stop with Back, restart from the button. Mermaid&#xA;renders lazily per slide (it can&#39;t measure a hidden one), and a &lt;code&gt;&amp;lt;base href&amp;gt;&lt;/code&gt; fixes co-located&#xA;asset URLs in the deck.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;v0032&#34;&gt;v0.0.32&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Slide decks render the post&#39;s content well by default.&lt;/strong&gt; A &lt;strong&gt;cover slide&lt;/strong&gt; (title, description,&#xA;author avatar/initials) leads; content is &lt;strong&gt;paginated&lt;/strong&gt; to fit (blocks pack onto a slide, overflow&#xA;spills to a continuation slide, an oversized code block truncates with a link back to the post,&#xA;images/video scale to fit); &lt;strong&gt;math, diagrams and syntax highlighting hydrate&lt;/strong&gt; from the published&#xA;&lt;code&gt;/vendor&lt;/code&gt; assets; media (images/audio/video) stays &lt;strong&gt;on the slide&lt;/strong&gt;, not in notes; callouts and&#xA;pull-quotes are styled. Prose paragraphs become the &lt;strong&gt;presenter notes&lt;/strong&gt; (shown in presenter mode);&#xA;everything else is on the slide — never both. The Downloads-box &lt;strong&gt;Slides&lt;/strong&gt; link opens the deck in a&#xA;&lt;strong&gt;new tab&lt;/strong&gt;. New keys: &lt;strong&gt;Enter&lt;/strong&gt; plays/pauses the slide&#39;s media, &lt;strong&gt;Esc&lt;/strong&gt; closes the deck (back to&#xA;the post).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Slide decks (&lt;code&gt;slides:&lt;/code&gt;).&lt;/strong&gt; A post can be projected into a themed slide deck, published at&#xA;&lt;code&gt;…/&amp;lt;slug&amp;gt;/slides/&lt;/code&gt;, linked from the Downloads box, and flagged with a marker in the listing. It&#39;s&#xA;derived from the post (headings → slides/bullets, prose → speaker notes, other blocks on the&#xA;slide); with JS it&#39;s a keyboard/swipe presentation (presenter notes, fullscreen), and with JS off&#xA;the same file reads as a long-form document. Configure with &lt;code&gt;slides.enabled&lt;/code&gt;/&lt;code&gt;slides.split&lt;/code&gt; at the&#xA;site level and override per post (&lt;code&gt;slides: true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt; or the block form; overwrites by key).&#xA;Split targets: &lt;code&gt;h1&lt;/code&gt;–&lt;code&gt;h6&lt;/code&gt;, &lt;code&gt;hr&lt;/code&gt;, &lt;code&gt;splitslide&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;/&lt;code&gt;table&lt;/code&gt;/&lt;code&gt;code&lt;/code&gt;/&lt;code&gt;math&lt;/code&gt;/&lt;code&gt;diagram&lt;/code&gt;/&lt;code&gt;audio&lt;/code&gt;/&#xA;&lt;code&gt;video&lt;/code&gt;, and &lt;code&gt;text:&amp;lt;match&amp;gt;&lt;/code&gt;. Inline markers &lt;code&gt;&amp;lt;splitslide&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;slide&amp;gt;…&amp;lt;/slide&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;noslide&amp;gt;… &amp;lt;/noslide&amp;gt;&lt;/code&gt; mirror the &lt;code&gt;&amp;lt;tts&amp;gt;&lt;/code&gt; family. See &lt;a href=&#34;/start/content/#slide-decks&#34;&gt;Authoring content → Slide decks&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;v0031&#34;&gt;v0.0.31&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Bluesky: refresh a card via an atomic swap, only on &lt;code&gt;--resync&lt;/code&gt;.&lt;/strong&gt; The earlier &amp;quot;edit in place&amp;quot;&#xA;for Bluesky (v0.0.29) was a no-op — Bluesky&#39;s AppView ignores record edits, so the public card&#xA;never changed. colophon now refreshes a Bluesky card by atomically deleting and recreating the&#xA;record at the &lt;strong&gt;same rkey&lt;/strong&gt; (&lt;code&gt;applyWrites&lt;/code&gt;): the card re-indexes and the &lt;strong&gt;permalink is kept&lt;/strong&gt;,&#xA;but it&#39;s a new record so &lt;strong&gt;likes/reposts/replies reset&lt;/strong&gt; and the timestamp updates. Because that&#39;s&#xA;lossy, it runs &lt;strong&gt;only on &lt;code&gt;--resync&lt;/code&gt;&lt;/strong&gt; (an explicit opt-in); automatic edit-on-change now &lt;strong&gt;skips&lt;/strong&gt;&#xA;Bluesky with a note. &lt;strong&gt;Mastodon&lt;/strong&gt; still edits in place automatically (no engagement loss).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Accessibility: a sweep toward WCAG AAA&lt;/strong&gt; (see the &lt;code&gt;wcag-aaa-compliance&lt;/code&gt; decision). Engine: code&#xA;blocks, Mermaid and display math are keyboard-focusable scroll regions (2.1.1); tables are wrapped&#xA;in a focusable &lt;code&gt;.table-scroll&lt;/code&gt; (semantics preserved, no &lt;code&gt;display:block&lt;/code&gt; hack); GFM task-list&#xA;checkboxes get an &lt;code&gt;aria-label&lt;/code&gt;. Press theme: a visible keyboard-focus indicator on every control;&#xA;&lt;code&gt;role=&amp;quot;img&amp;quot;&lt;/code&gt; on the audio/attachment markers; the home page hero moved inside &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;; and a&#xA;contrast pass — &lt;code&gt;--muted&lt;/code&gt;/&lt;code&gt;--faint&lt;/code&gt; raised to ≥7:1 and a new &lt;code&gt;--link&lt;/code&gt; token (≥7:1) for accent&#xA;text (links, inline code, badges, pull-quote attribution), with &lt;code&gt;--accent&lt;/code&gt; kept for decoration.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;v0030&#34;&gt;v0.0.30&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Syndication card descriptions fall back to a body excerpt.&lt;/strong&gt; A post with no &lt;code&gt;description:&lt;/code&gt;&#xA;frontmatter previously syndicated with an empty summary (a bare Bluesky/Mastodon card);&#xA;&lt;code&gt;build.Entries&lt;/code&gt; now mirrors the page — explicit &lt;code&gt;description:&lt;/code&gt;, else a short excerpt of the&#xA;rendered body. Also fixes empty descriptions in feeds for such posts. Re-run&#xA;&lt;code&gt;colophon syndicate --resync&lt;/code&gt; once to push the new descriptions onto existing cards.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;syndicate&lt;/code&gt; skips, doesn&#39;t fail, an entry with no recorded silo URL.&lt;/strong&gt; Ledger entries posted&#xA;via a fire-and-forget driver (Bridgy) have no editable handle; &lt;code&gt;--resync&lt;/code&gt; now reports them as&#xA;&lt;code&gt;skipped&lt;/code&gt; with a note instead of erroring the whole run non-zero.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;v0029&#34;&gt;v0.0.29&lt;/h2&gt;&#xA;&lt;h3 id=&#34;content--themes&#34;&gt;Content &amp;amp; themes&lt;/h3&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Pull-quotes / epigraphs.&lt;/strong&gt; A &lt;code&gt;&amp;gt; [!quote] Attribution&lt;/code&gt; callout renders as a semantic&#xA;&lt;code&gt;&amp;lt;figure class=&amp;quot;pullquote&amp;quot;&amp;gt;&lt;/code&gt; with the attribution as &lt;code&gt;&amp;lt;figcaption&amp;gt;&lt;/code&gt; (omit it for an unattributed&#xA;quote); the &lt;strong&gt;press&lt;/strong&gt; theme styles it as a large display quote. See&#xA;&lt;a href=&#34;/start/content/#markdown-support&#34;&gt;Authoring content → Markdown support&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Glossary reference links.&lt;/strong&gt; A &lt;code&gt;glossary.yaml&lt;/code&gt; term can carry reference links, rendered as&#xA;citation-style superscripts after the decorated term. See&#xA;&lt;a href=&#34;/start/content/#glossary&#34;&gt;Authoring content → Glossary&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Tables are styled in press&lt;/strong&gt; (borders, padding, header underline, row hover, horizontal&#xA;scroll on narrow screens). goldmark&#39;s per-column alignment is preserved.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;colophon serve --showcase&lt;/code&gt;.&lt;/strong&gt; Injects a built-in &lt;code&gt;/showcase/&lt;/code&gt; page — embedded in the binary,&#xA;never written to your content — that renders &lt;em&gt;every&lt;/em&gt; content feature (callouts, pull-quotes,&#xA;tables, maths, diagrams, image/video/audio embeds, attachments, glossary, post hero/description/&#xA;audio reading) in your active theme, with the source shown alongside. The single living&#xA;reference for what a theme can style.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;generation-image--speech&#34;&gt;Generation (image &amp;amp; speech)&lt;/h3&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Named generation profiles.&lt;/strong&gt; Each modality (&lt;code&gt;generation.image&lt;/code&gt; / &lt;code&gt;generation.speech&lt;/code&gt;) takes a&#xA;default block plus named &lt;code&gt;profiles:&lt;/code&gt; that inherit it and override only what they set. Select a&#xA;profile with the same key at every scope — &lt;code&gt;image_profile:&lt;/code&gt; / &lt;code&gt;speech_profile:&lt;/code&gt; — on an&#xA;&lt;em&gt;environment&lt;/em&gt;, in a &lt;em&gt;post&#39;s frontmatter&lt;/em&gt;, or per image via &lt;code&gt;&amp;lt;gen:…?profile=name&amp;gt;&lt;/code&gt;; narrowest&#xA;scope wins. Fully annotated in &lt;a href=&#34;/reference/config/&#34;&gt;&lt;code&gt;colophon.reference.yaml&lt;/code&gt;&lt;/a&gt;; see also&#xA;&lt;a href=&#34;/guides/image-generation/&#34;&gt;Image &amp;amp; audio generation&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Provider-agnostic pronunciation dictionaries&lt;/strong&gt; with a bundled British dict (&lt;code&gt;pronunciation_dict: en_GB&lt;/code&gt;): &lt;code&gt;ipa:&lt;/code&gt; entries render to each provider&#39;s phoneme mechanism (ElevenLabs uploads a&#xA;versioned dictionary; MiniMax sends them inline), &lt;code&gt;say:&lt;/code&gt; entries substitute as plain text.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Speech: headings pause.&lt;/strong&gt; A heading now ends on a sentence boundary in the spoken reading, so&#xA;it no longer runs into the next paragraph.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Speech: the waveform is precomputed&lt;/strong&gt; from the same audio (no second render) and shipped as the&#xA;&lt;code&gt;&amp;lt;audio&amp;gt;.json&lt;/code&gt; sidecar; the in-browser visualiser is the fallback when it&#39;s absent.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;syndication-posse&#34;&gt;Syndication (POSSE)&lt;/h3&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Edit a syndicated copy when the post changes.&lt;/strong&gt; The ledger stores a content fingerprint; a&#xA;later run edits the existing silo copy in place (Mastodon &lt;code&gt;PUT&lt;/code&gt;, Bluesky &lt;code&gt;putRecord&lt;/code&gt;) instead of&#xA;skipping — keeping its permalink/likes/replies. Bridgy/command can&#39;t edit and are left as-is. The&#xA;first run after upgrading &lt;strong&gt;backfills&lt;/strong&gt; fingerprints without editing.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;colophon syndicate --resync&lt;/code&gt;.&lt;/strong&gt; A one-shot that re-edits every already-syndicated copy to its&#xA;current content, ignoring fingerprints — to catch up copies created before the feature. Entries&#xA;with no recorded silo URL (e.g. posted via Bridgy) are skipped, not failed.&lt;/p&gt;&#xA;&lt;p&gt;See &lt;a href=&#34;/guides/syndication/#editing-a-syndicated-copy-when-the-post-changes&#34;&gt;Syndication (POSSE) → Editing a syndicated copy&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/changelog.md&#34;&gt;&lt;code&gt;docs/changelog.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Internals &amp; design notes</title>
    <id>https://docs.colophon.blog/internals/</id>
    <link href="https://docs.colophon.blog/internals/" rel="alternate"></link>
    <updated>2001-11-24T00:00:00Z</updated>
    <published>2001-11-24T00:00:00Z</published>
    <summary type="text">Engineering design documents for colophon&#39;s subsystems — how search, federation, webmentions and the Obsidian source work inside.</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from the colophon CLI — do not edit by hand. --&gt;&#xA;&lt;p&gt;Design documents for the systems behind colophon. These are &lt;strong&gt;engineering notes&lt;/strong&gt;, kept for the curious and for contributors — they explain why the subsystems are shaped the way they are, and can run ahead of or behind the shipped code.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/internals/search/&#34;&gt;Static search&lt;/a&gt; — the fully static lexical engine, index shards and the browser reader.&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/internals/federation/&#34;&gt;Federation&lt;/a&gt; — IndieWeb, POSSE and syndication architecture.&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/internals/webmention/&#34;&gt;Webmention&lt;/a&gt; — sending, receiving, caching and displaying mentions.&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;/internals/obsidian/&#34;&gt;Obsidian&lt;/a&gt; — publishing straight from a vault.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The roadmap itself lives in the repository: &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/PLAN.md&#34;&gt;docs/PLAN.md&lt;/a&gt;.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Design: federation (IndieWeb, POSSE, syndication)</title>
    <id>https://docs.colophon.blog/internals/federation/</id>
    <link href="https://docs.colophon.blog/internals/federation/" rel="alternate"></link>
    <updated>2001-11-23T00:00:00Z</updated>
    <published>2001-11-23T00:00:00Z</published>
    <summary type="text">Goal: let a colophon blog participate fully in the social web — be followable, get replies/likes back, and cross-post to silos — while staying a static site. The organising…</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/design/federation.md — do not edit by hand. --&gt;&#xA;&lt;div class=&#34;callout callout-note&#34; data-callout=&#34;note&#34;&gt;&#xA;&lt;div class=&#34;callout-title&#34;&gt;Internal design note&lt;/div&gt;&#xA;&lt;div class=&#34;callout-body&#34;&gt;&#xA;&lt;p&gt;This is an engineering design document — it describes how colophon works inside, and may run ahead of (or behind) the shipped code. The user-facing guides are the source of truth for behaviour.&lt;/p&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Status: &lt;strong&gt;substrate shipped, the rest designed.&lt;/strong&gt; Built: microformats2 (h-entry/h-card/h-feed),&#xA;&lt;code&gt;rel=me&lt;/code&gt;, RSS/Atom/JSON feeds, and &lt;code&gt;aliases&lt;/code&gt; redirects (URL stability). Webmention is detailed in&#xA;&lt;a href=&#34;/internals/webmention/&#34;&gt;webmention.md&lt;/a&gt;; this is the umbrella — the posture, the reader/syndicator abstractions,&#xA;POSSE, WebSub, and the cross-cutting concerns. No POSSE/WebSub code yet. Relates to PLAN §10.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Goal: let a colophon blog participate fully in the social web — be followable, get replies/likes&#xA;back, and cross-post to silos — while staying a &lt;strong&gt;static site&lt;/strong&gt;. The organising principle:&lt;/p&gt;&#xA;&lt;h2 id=&#34;posture-be-a-source-not-a-server&#34;&gt;Posture: be a source, not a server&lt;/h2&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Be a…&lt;/th&gt;&#xA;&lt;th&gt;Means&lt;/th&gt;&#xA;&lt;th&gt;Static-friendly?&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;emit mf2 + feeds + discovery tags; thin post-publish &amp;quot;notify&amp;quot;/&amp;quot;syndicate&amp;quot; steps; let hosted relays do the rest&lt;/td&gt;&#xA;&lt;td&gt;✅ our lane&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;run an ActivityPub/AT actor, a receiving endpoint, a reader/Microsub&lt;/td&gt;&#xA;&lt;td&gt;❌ needs a live server&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Bridgy Fed needs only &lt;strong&gt;mf2 + webmention, or an RSS/Atom feed&lt;/strong&gt; — both already emitted — to make&#xA;the &lt;em&gt;site itself&lt;/em&gt; followable from Mastodon/Bluesky. The heavy lifting is offloaded to relays; we&#xA;emit standards and run small, decoupled senders.&lt;/p&gt;&#xA;&lt;h2 id=&#34;abstractions-only-where-mechanisms-diverge&#34;&gt;Abstractions only where mechanisms diverge&lt;/h2&gt;&#xA;&lt;p&gt;colophon has two existing naming conventions, split by &lt;em&gt;shape&lt;/em&gt;: a &lt;strong&gt;list of pluggable&#xA;destinations&lt;/strong&gt; uses &lt;code&gt;driver&lt;/code&gt; (publishers and sources are both &lt;code&gt;{id, driver, settings}&lt;/code&gt;), while the&#xA;&lt;strong&gt;single external service that produces/serves content&lt;/strong&gt; for a modality uses &lt;code&gt;provider&lt;/code&gt;&#xA;(generation). Federation adds one of each — and matches the convention by shape:&lt;/p&gt;&#xA;&lt;h3 id=&#34;1-reader-driver--reading-webmentions-back&#34;&gt;1. Reader (&lt;code&gt;driver&lt;/code&gt;) — reading webmentions back&lt;/h3&gt;&#xA;&lt;p&gt;There&#39;s one receiver per site, so this is the &lt;em&gt;single-service&lt;/em&gt; shape → &lt;code&gt;provider&lt;/code&gt;, like generation.&#xA;The Webmention spec standardises &lt;em&gt;receiving&lt;/em&gt;, not &lt;em&gt;reading back&lt;/em&gt;, so each receiver exposes a&#xA;different read API. Model it as a &lt;code&gt;Reader&lt;/code&gt; interface + a &lt;code&gt;driver&lt;/code&gt; (default &lt;code&gt;jf2&lt;/code&gt;,&#xA;plus a &lt;code&gt;custom&lt;/code&gt; JF2 source for self-hosted/compatible), selected by config.&#xA;Bridgy &lt;em&gt;backfeed&lt;/em&gt; arrives in your receiver as ordinary webmentions, so it is read through the same&#xA;Reader driver — not a separate abstraction. Full detail in &lt;a href=&#34;/internals/webmention/&#34;&gt;webmention.md&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h3 id=&#34;2-syndicator-driver--posse-cross-posting&#34;&gt;2. Syndicator (&lt;code&gt;driver&lt;/code&gt;) — POSSE (cross-posting)&lt;/h3&gt;&#xA;&lt;p&gt;POSSE matters for anyone with real social reach, and Bridgy-only POSSE gives little control over&#xA;per-network formatting/threading and depends on a relay — so native syndication is a first-class&#xA;goal. Each target is a &lt;code&gt;Syndicator&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;type Syndicator interface { Syndicate(ctx, post) (siloURL string, err error) }&#xA;//   drivers (mirroring publishers/sources):&#xA;//     mastodon – instance URL + access token (env); statuses + media API&#xA;//     bluesky  – handle + app password (env); AT-proto createRecord + blob upload&#xA;//     bridgy   – POST to brid.gy/publish, parse the created silo URL from the response&#xA;//     command  – run a user command; stdout = silo URL (empty stdout = fire-and-forget webhook)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Syndication is the &lt;em&gt;list-of-destinations&lt;/em&gt; shape, so it uses &lt;strong&gt;&lt;code&gt;driver&lt;/code&gt;&lt;/strong&gt; — each entry is&#xA;&lt;code&gt;{ id, driver, …settings }&lt;/code&gt;, byte-for-byte the &lt;code&gt;PublisherConfig&lt;/code&gt;/&lt;code&gt;SourceConfig&lt;/code&gt; shape (&lt;code&gt;id&lt;/code&gt; +&#xA;&lt;code&gt;driver&lt;/code&gt; + remaining settings). &lt;code&gt;driver&lt;/code&gt; is the concrete mechanism (&lt;code&gt;mastodon&lt;/code&gt;, &lt;code&gt;bluesky&lt;/code&gt;,&#xA;&lt;code&gt;bridgy&lt;/code&gt;, &lt;code&gt;command&lt;/code&gt;); &lt;code&gt;id&lt;/code&gt; is an arbitrary handle that &lt;code&gt;syndicate:&lt;/code&gt; (per-env and per-post)&#xA;references. A site may configure &lt;strong&gt;many&lt;/strong&gt; syndicators (the &lt;code&gt;syndication:&lt;/code&gt; list, like the publishers&#xA;list). Bridgy is simply &lt;code&gt;driver: bridgy&lt;/code&gt; with a &lt;code&gt;network:&lt;/code&gt; field naming the silo to publish to —&#xA;not a special &amp;quot;via&amp;quot;. (It really is &amp;quot;publishers, for silos.&amp;quot;)&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The &lt;code&gt;command&lt;/code&gt; syndicator is also a publish webhook.&lt;/strong&gt; Mirroring the &lt;code&gt;command&lt;/code&gt; &lt;em&gt;publisher&lt;/em&gt;, it&#xA;runs a user-defined command per post with interpolated placeholders (&lt;code&gt;{url}&lt;/code&gt; canonical, &lt;code&gt;{title}&lt;/code&gt;,&#xA;&lt;code&gt;{slug}&lt;/code&gt;, &lt;code&gt;{summary}&lt;/code&gt;, &lt;code&gt;{tags}&lt;/code&gt;, &lt;code&gt;{json}&lt;/code&gt; = path to a metadata file) and env for secrets — so a&#xA;user can wire up anything (a Discord/Slack webhook, a Bluesky CLI, an n8n flow, a custom API). The&#xA;trick that makes it both a syndicator and a generic hook: &lt;strong&gt;stdout is the silo URL.&lt;/strong&gt; If the command&#xA;prints a URL it&#39;s recorded in the ledger and rendered as &lt;code&gt;u-syndication&lt;/code&gt;; if it prints nothing it&#39;s&#xA;a &lt;strong&gt;fire-and-forget publish webhook&lt;/strong&gt;. No separate hook system — the same interface covers both.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Bridgy is transparent for receiving, a syndicator for sending&lt;/strong&gt; — the two are unrelated:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;em&gt;Backfeed (inbound):&lt;/em&gt; Bridgy polls your connected silo accounts on its own schedule and POSTs&#xA;webmentions to your receiver. colophon never calls Bridgy; replies arrive and are read via the&#xA;Reader. &lt;strong&gt;Not modeled&lt;/strong&gt; — it&#39;s invisible infrastructure.&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;POSSE (outbound):&lt;/em&gt; Bridgy does &lt;strong&gt;not&lt;/strong&gt; auto-publish new posts, so automating cross-posting means&#xA;colophon &lt;strong&gt;actively&lt;/strong&gt; POSTs to &lt;code&gt;brid.gy/publish&lt;/code&gt; and records the returned silo URL. That&#39;s an&#xA;explicit syndication action → it&#39;s a &lt;code&gt;Syndicator&lt;/code&gt; driver like the rest. (Implementation note:&#xA;Bridgy verifies the source links to &lt;code&gt;brid.gy/publish/{silo}&lt;/code&gt;, so the driver includes that link&#xA;in the source it sends.)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;So &lt;code&gt;driver: bridgy&lt;/code&gt; buys cross-posting to networks without a native driver (or without holding&#xA;their API tokens yourself), at the cost of per-network formatting control.&lt;/p&gt;&#xA;&lt;p&gt;Syndication runs as a &lt;strong&gt;post-publish step&lt;/strong&gt; (&lt;code&gt;colophon syndicate&lt;/code&gt;, after the canonical URL is live),&#xA;decoupled and best-effort like webmention send — it never blocks the deploy.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-syndication-ledger--and-why-its-different-from-the-webmention-cache&#34;&gt;The syndication ledger — and why it&#39;s different from the webmention cache&lt;/h2&gt;&#xA;&lt;p&gt;A sidecar ledger (e.g. &lt;code&gt;.colophon/syndication.json&lt;/code&gt;) maps &lt;code&gt;post → {network: {url, time}}&lt;/code&gt;. It is&#xA;the idempotency key (don&#39;t repost on rebuild), the &lt;code&gt;u-syndication&lt;/code&gt; data (&amp;quot;Also posted on…&amp;quot;), and the&#xA;backfeed-pairing key.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Crucial contrast with webmentions:&lt;/strong&gt; the webmention export is &lt;em&gt;regenerable&lt;/em&gt; (re-fetch from the&#xA;receiver), so an empty CI runner is fine. The syndication ledger is &lt;strong&gt;authoritative and NOT&#xA;regenerable&lt;/strong&gt; — you cannot reliably re-derive &amp;quot;which Mastodon post is the copy of this entry.&amp;quot; So:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;The ledger must be durable&lt;/strong&gt; — committed to the repo (it&#39;s small and append-mostly) or kept in&#xA;persistent storage. A fresh runner &lt;em&gt;without&lt;/em&gt; it would &lt;strong&gt;re-POSSE everything (double-post)&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Therefore &lt;code&gt;syndicate&lt;/code&gt; must &lt;strong&gt;refuse to run, or run dry, when the ledger is absent/stale&lt;/strong&gt; unless&#xA;explicitly forced — the opposite of the webmention &amp;quot;graceful when empty&amp;quot; rule.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;This is the single biggest operational gotcha in the whole federation surface; the design must make&#xA;double-posting structurally hard (commit the ledger; idempotent against it; &lt;code&gt;--dry-run&lt;/code&gt; default in&#xA;CI without a ledger).&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-does-not-need-an-abstraction&#34;&gt;What does NOT need an abstraction&lt;/h2&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Piece&lt;/th&gt;&#xA;&lt;th&gt;Why&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Webmention &lt;strong&gt;send&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;one spec-standard algorithm (discover endpoint, POST)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;rel=webmention&lt;/code&gt;, &lt;code&gt;rel=hub&lt;/code&gt; tags&lt;/td&gt;&#xA;&lt;td&gt;config strings&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;Bridgy backfeed&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;lands in your receiver → read via the Reader&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;WebSub ping&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;one protocol; the hub is a config URL&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;h2 id=&#34;websub-instant-feed-push&#34;&gt;WebSub (instant feed push)&lt;/h2&gt;&#xA;&lt;p&gt;Emit &lt;code&gt;&amp;lt;link rel=&amp;quot;hub&amp;quot; href=&amp;quot;…&amp;quot;&amp;gt;&lt;/code&gt; in the feeds and &lt;strong&gt;ping the hub on publish&lt;/strong&gt; so readers and&#xA;aggregators update immediately instead of polling. Thin: a discovery tag + one POST in the&#xA;post-publish step. Hubs are hosted (Superfeedr, websubhub.com). No provider abstraction.&lt;/p&gt;&#xA;&lt;h2 id=&#34;cross-cutting-considerations-apply-regardless-of-which-pieces-ship&#34;&gt;Cross-cutting considerations (apply regardless of which pieces ship)&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Where work runs.&lt;/strong&gt; Static = no server; every action is a build/CI step or a hosted relay.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;State &amp;amp; idempotency.&lt;/strong&gt; The syndication ledger (durable, committed) and webmention sent-cache;&#xA;never repeat actions on a rebuild; a rebuild is not a new post.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Loops &amp;amp; dedup.&lt;/strong&gt; Don&#39;t webmention-loop; dedup backfed responses; &lt;code&gt;u-syndication&lt;/code&gt; ties copies to&#xA;the canonical.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Edits/deletes.&lt;/strong&gt; Default: post-once; silo copies are point-in-time and don&#39;t track edits&#xA;(optional propagation later). Canonical is the source of truth; &lt;code&gt;aliases&lt;/code&gt; keep old URLs resolving.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Identity &amp;amp; SEO.&lt;/strong&gt; &lt;code&gt;rel=me&lt;/code&gt;, &lt;code&gt;u-syndication&lt;/code&gt;, canonical URLs; copies cite the canonical to avoid&#xA;duplicate-content problems.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Secrets.&lt;/strong&gt; Per-network tokens, Bridgy OAuth (their side), webmention.io token, WebSub — all&#xA;env-only; more features = more CI-secret surface.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Privacy/moderation.&lt;/strong&gt; Displaying backfed third-party content (avatars, replies) → block/allow&#xA;lists, avatar caching/proxying, opt-in, spam handling.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Failure/decoupling.&lt;/strong&gt; Network steps flake/throttle → best-effort, non-blocking, retryable, and&#xA;decoupled from the content build (the separate publish pipeline).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Per-network formatting.&lt;/strong&gt; Char limits (Mastodon ~500 instance-variable, Bluesky 300), link-back,&#xA;hashtags, media + alt, threading long posts; per-post custom syndication text.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Selective syndication.&lt;/strong&gt; &lt;code&gt;syndicate:&lt;/code&gt; frontmatter chooses targets (opt-in/out) per post.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Display freshness.&lt;/strong&gt; JS-rendered mentions are &lt;em&gt;not&lt;/em&gt; tied to page regeneration: the browser&#xA;fetches the &lt;code&gt;_mentions/&lt;/code&gt; asset live, so a scheduled &lt;code&gt;webmention publish&lt;/code&gt; (refresh that asset, no&#xA;site rebuild) updates them near-live. Only the no-JS &lt;em&gt;bake&lt;/em&gt; path is as-fresh-as-the-last-build.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;environments-read-everywhere-write-only-where-enabled&#34;&gt;Environments: read everywhere, write only where enabled&lt;/h2&gt;&#xA;&lt;p&gt;Federation splits cleanly across environments, and the split is a &lt;strong&gt;safety property&lt;/strong&gt;:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Webmention reading is site-domain-scoped, so it&#39;s shared.&lt;/strong&gt; webmention.io keys mentions by your&#xA;&lt;em&gt;production&lt;/em&gt; target URLs, so the &lt;code&gt;webmention&lt;/code&gt; config lives at the &lt;strong&gt;site&lt;/strong&gt; level and every&#xA;environment inherits it. A &lt;strong&gt;preview build reads the same production mentions&lt;/strong&gt; (previewing how&#xA;real responses look) — no separate receiver, no extra config.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Syndication is environment-gated and off by default&lt;/strong&gt;, like &lt;code&gt;allow_publish&lt;/code&gt;. Which syndicators&#xA;fire is an &lt;em&gt;environment&lt;/em&gt; decision (&lt;code&gt;environments[].syndicate: [ids]&lt;/code&gt;); an env that omits it —&#xA;notably &lt;strong&gt;preview/draft&lt;/strong&gt; — never cross-posts. &lt;code&gt;colophon syndicate&lt;/code&gt; also takes the same kind of&#xA;deploy latch. This makes double-posting (or POSSEing a draft) structurally impossible from preview.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;So: &lt;strong&gt;read in every environment, write only where explicitly enabled.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;config-sketch&#34;&gt;Config sketch&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    federation:&#xA;      feeds: [rss, atom, json]&#xA;      websub:&#xA;        hub: https://pubsubhubbub.superfeedr.com     # rel=hub + ping on publish&#xA;      indieweb:&#xA;        webmention:&#xA;          endpoint: https://webmention.io/blog.example.com/webmention   # advertised rel=webmention&#xA;          source:   https://webmention.io/api/mentions.jf2              # read API (JF2 reader)&#xA;      syndication:                             # a list — many syndicators per site (id + driver + settings)&#xA;        - { id: mastodon, driver: mastodon, instance: https://hachyderm.io }   # token from env MASTODON_TOKEN&#xA;        - { id: bluesky,  driver: bluesky,  handle: me.bsky.social }           # app password from env&#xA;        - { id: discord,  driver: command,  command: &amp;#34;curl -sf -X POST $DISCORD_WEBHOOK -d @{json}&amp;#34; }  # webhook: no stdout → fire-and-forget&#xA;        - { id: twitter,  driver: bridgy,   network: twitter }                 # via brid.gy/publish/twitter&#xA;&#xA;environments:&#xA;  - name: production&#xA;    publish: [cf, r2]&#xA;    syndicate: [mastodon, bluesky, discord, twitter]   # which syndicators fire here&#xA;  - name: preview&#xA;    publish: [cf-preview]&#xA;    # no `syndicate:` → preview never cross-posts, but still reads the production webmentions above&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Per post: &lt;code&gt;syndicate: [mastodon, bluesky]&lt;/code&gt; (else all the env&#39;s configured ids), &lt;code&gt;syndicate: false&lt;/code&gt;&#xA;to opt out, and an optional custom blurb (&lt;code&gt;syndicate_text:&lt;/code&gt;). Resolved silo URLs are written to the&#xA;ledger and rendered as &lt;code&gt;u-syndication&lt;/code&gt; links on the post; manually-added &lt;code&gt;syndication:&lt;/code&gt; frontmatter&#xA;is honoured too. (&lt;code&gt;webmention&lt;/code&gt; sits at the site level, so every environment — preview included —&#xA;reads the same production mentions; &lt;code&gt;syndicate:&lt;/code&gt; is per-environment, so only the envs that list it&#xA;ever post.)&lt;/p&gt;&#xA;&lt;h2 id=&#34;phasing&#34;&gt;Phasing&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Tier 1 — cheap static + thin send:&lt;/strong&gt; &lt;code&gt;rel=webmention&lt;/code&gt; tag, &lt;code&gt;u-syndication&lt;/code&gt;, WebSub &lt;code&gt;rel=hub&lt;/code&gt; +&#xA;ping, webmention send. Unlocks Bridgy + Bridgy Fed + instant feeds with little stateful code.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Tier 2 — receive/display:&lt;/strong&gt; webmention &lt;code&gt;fetch&lt;/code&gt; + &lt;code&gt;_mentions/&lt;/code&gt; assets (&lt;a href=&#34;/internals/webmention/&#34;&gt;webmention.md&lt;/a&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Tier 3 — POSSE:&lt;/strong&gt; the &lt;code&gt;Syndicator&lt;/code&gt; abstraction + sidecar ledger + &lt;code&gt;colophon syndicate&lt;/code&gt;, with&#xA;&lt;code&gt;bridgy&lt;/code&gt;, &lt;code&gt;mastodon&lt;/code&gt;, &lt;code&gt;bluesky&lt;/code&gt;, &lt;code&gt;command&lt;/code&gt; drivers and per-network formatting.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;out-of-scope&#34;&gt;Out of scope&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Running an ActivityPub/AT actor or a reader/Microsub (delegate to Bridgy Fed / hosted readers).&lt;/li&gt;&#xA;&lt;li&gt;A self-hosted webmention receiver (use webmention.io hosted or self-host &lt;em&gt;it&lt;/em&gt;).&lt;/li&gt;&#xA;&lt;li&gt;Propagating edits/deletes to silo copies (point-in-time by default).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/design/federation.md&#34;&gt;&lt;code&gt;docs/design/federation.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Design: publishing from Obsidian</title>
    <id>https://docs.colophon.blog/internals/obsidian/</id>
    <link href="https://docs.colophon.blog/internals/obsidian/" rel="alternate"></link>
    <updated>2001-11-22T00:00:00Z</updated>
    <published>2001-11-22T00:00:00Z</published>
    <summary type="text">Goal: a &#34;publish / preview&#34; flow from an Obsidian vault with the smallest integration footprint, keeping colophon a plain CLI that reads markdown and publishes.</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/design/obsidian.md — do not edit by hand. --&gt;&#xA;&lt;div class=&#34;callout callout-note&#34; data-callout=&#34;note&#34;&gt;&#xA;&lt;div class=&#34;callout-title&#34;&gt;Internal design note&lt;/div&gt;&#xA;&lt;div class=&#34;callout-body&#34;&gt;&#xA;&lt;p&gt;This is an engineering design document — it describes how colophon works inside, and may run ahead of (or behind) the shipped code. The user-facing guides are the source of truth for behaviour.&lt;/p&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Status: &lt;strong&gt;thin design&lt;/strong&gt; · relates to PLAN §7 (Sources)&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Goal: a &amp;quot;publish / preview&amp;quot; flow from an Obsidian vault with the &lt;strong&gt;smallest integration&#xA;footprint&lt;/strong&gt;, keeping colophon a plain CLI that reads markdown and publishes.&lt;/p&gt;&#xA;&lt;h2 id=&#34;key-constraint-ci-cannot-see-the-live-vault&#34;&gt;Key constraint: CI cannot see the live vault&lt;/h2&gt;&#xA;&lt;p&gt;A vault lives on your machine (and Obsidian Sync). A CI/CD runner has no access to it.&#xA;So any CI-based publish requires the publishable content to &lt;strong&gt;reach a git repo the&#xA;runner can clone&lt;/strong&gt;. The important realization:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;colophon&#39;s &lt;code&gt;obsidian&lt;/code&gt; source reads &lt;code&gt;.md&lt;/code&gt; files from a directory — it does &lt;strong&gt;not&lt;/strong&gt;&#xA;require the Obsidian app. A checked-out repo folder is just as readable as a live&#xA;vault.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;So &amp;quot;give CI access to the vault&amp;quot; = &amp;quot;commit the publishable markdown to a repo.&amp;quot; The&#xA;Obsidian → colophon normalization (publish-flag filter, wikilink resolution) then runs&#xA;in CI, at build time, by colophon — on the committed &lt;code&gt;.md&lt;/code&gt; files.&lt;/p&gt;&#xA;&lt;h2 id=&#34;two-deployment-models-same-source-driver&#34;&gt;Two deployment models (same source driver)&lt;/h2&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;&lt;/th&gt;&#xA;&lt;th&gt;Local&lt;/th&gt;&#xA;&lt;th&gt;Git + CI&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Where colophon runs&lt;/td&gt;&#xA;&lt;td&gt;your machine&lt;/td&gt;&#xA;&lt;td&gt;CI runner&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Reads&lt;/td&gt;&#xA;&lt;td&gt;the live vault path&lt;/td&gt;&#xA;&lt;td&gt;the committed repo snapshot&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Secrets (&lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;td&gt;on the device&lt;/td&gt;&#xA;&lt;td&gt;CI secret&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Devices&lt;/td&gt;&#xA;&lt;td&gt;desktop only&lt;/td&gt;&#xA;&lt;td&gt;any (mobile Obsidian Git can push)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;History&lt;/td&gt;&#xA;&lt;td&gt;none implicit&lt;/td&gt;&#xA;&lt;td&gt;every publish is a commit&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Trigger&lt;/td&gt;&#xA;&lt;td&gt;a button / &lt;code&gt;colophon publish&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;git push&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Both use the &lt;strong&gt;same &lt;code&gt;obsidian&lt;/code&gt; source&lt;/strong&gt; (reads a folder of &lt;code&gt;.md&lt;/code&gt;). The only difference&#xA;is where colophon runs and where the files are. &lt;code&gt;serve&lt;/code&gt; (local, hot-reload) is the&#xA;instant-preview path in both models.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-chain-git--ci&#34;&gt;The chain (git + CI)&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Obsidian (write, publish: true)&#xA;   │  commit + push the publishable subset   ← Obsidian Git plugin (existing)&#xA;   ▼&#xA;Git repo  ──on push──▶  CI: colophon publish --env &amp;lt;production|preview&amp;gt;&#xA;                              (secrets in CI; obsidian source reads committed md)&#xA;   ▼&#xA;Cloudflare Pages   (PR/branch → preview env; main → production)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This composes with existing colophon pieces: environments→branches→CF environments,&#xA;&lt;code&gt;publish_after&lt;/code&gt; + &lt;code&gt;next-build-time&lt;/code&gt; (a scheduled CI run publishes embargoed posts when&#xA;due), and per-deploy &lt;code&gt;prune&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-button&#34;&gt;The &amp;quot;button&amp;quot;&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;No custom plugin (recommended start):&lt;/strong&gt; use the existing &lt;strong&gt;Obsidian Git&lt;/strong&gt; plugin&#39;s&#xA;commit+push. colophon needs nothing.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Thin status plugin (polish, later):&lt;/strong&gt; a small plugin that triggers commit+push then&#xA;shows the resulting deploy URL/status. It never touches secrets or runs colophon.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;what-colophon-provides&#34;&gt;What colophon provides&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;obsidian&lt;/code&gt; source (done): folder read + &lt;code&gt;publish: true&lt;/code&gt; whitelist; folder structure →&#xA;slug; deletes/renames flow through build reconciliation.&lt;/li&gt;&#xA;&lt;li&gt;A documented CI workflow (~15 lines) — to add.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;next-build-time&lt;/code&gt; (done) for scheduled embargo publishing.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;open--dependencies&#34;&gt;Open / dependencies&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Vault privacy:&lt;/strong&gt; commit only a &lt;code&gt;Blog/&lt;/code&gt; subfolder or rely on the publish-flag; private&#xA;vaults → private repo.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Wikilinks&lt;/strong&gt; (&lt;code&gt;[[note]]&lt;/code&gt;, &lt;code&gt;[[note|alias]]&lt;/code&gt;): resolved at build via a cross-document&#xA;link map. &lt;em&gt;In progress.&lt;/em&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Embeds&lt;/strong&gt; (&lt;code&gt;![[image.png]]&lt;/code&gt;, &lt;code&gt;![[note]]&lt;/code&gt;): image embeds depend on the &lt;strong&gt;asset&#xA;pipeline&lt;/strong&gt; (PLAN §6a) to copy/host the file; note transclusion is a later step. Until&#xA;then, note links resolve and embeds are left untouched.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/design/obsidian.md&#34;&gt;&lt;code&gt;docs/design/obsidian.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Design: static search</title>
    <id>https://docs.colophon.blog/internals/search/</id>
    <link href="https://docs.colophon.blog/internals/search/" rel="alternate"></link>
    <updated>2001-11-21T00:00:00Z</updated>
    <published>2001-11-21T00:00:00Z</published>
    <summary type="text">Goal: public-site search that is fully static (no server, no external service), low bandwidth (never load the whole index into the browser), and incremental-friendly (a content…</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/design/search.md — do not edit by hand. --&gt;&#xA;&lt;div class=&#34;callout callout-note&#34; data-callout=&#34;note&#34;&gt;&#xA;&lt;div class=&#34;callout-title&#34;&gt;Internal design note&lt;/div&gt;&#xA;&lt;div class=&#34;callout-body&#34;&gt;&#xA;&lt;p&gt;This is an engineering design document — it describes how colophon works inside, and may run ahead of (or behind) the shipped code. The user-facing guides are the source of truth for behaviour.&lt;/p&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Status: &lt;strong&gt;v1 implemented&lt;/strong&gt; · relates to PLAN §8 (search), §9 (publishers). The lexical engine&#xA;(module &lt;code&gt;github.com/jmylchreest/colophon/search&lt;/code&gt;), the browser reader, the build emit, the&#xA;&lt;code&gt;colophon search&lt;/code&gt; CLI, and the press theme box are built; fuzzy and semantic remain designed&#xA;seams. Replaced the &lt;code&gt;SearchCmd&lt;/code&gt; stub (&lt;code&gt;SyncCmd&lt;/code&gt; still stubbed).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Goal: public-site search that is &lt;strong&gt;fully static&lt;/strong&gt; (no server, no external service), &lt;strong&gt;low&#xA;bandwidth&lt;/strong&gt; (never load the whole index into the browser), and &lt;strong&gt;incremental-friendly&lt;/strong&gt; (a&#xA;content edit rewrites a handful of files, not the index). The same engine powers the&#xA;&lt;code&gt;colophon search&lt;/code&gt; CLI and persona exemplar retrieval.&lt;/p&gt;&#xA;&lt;p&gt;The design borrows its &lt;em&gt;architecture&lt;/em&gt; from &lt;a href=&#34;https://github.com/Pagefind/pagefind&#34;&gt;Pagefind&lt;/a&gt;&#xA;(MIT — reviewed, not vendored) — build-from-output, a sharded word index, per-page fragments,&#xA;two-stage fetch — but uses &lt;strong&gt;our own format&lt;/strong&gt; and a &lt;strong&gt;vanilla-JS reader&lt;/strong&gt;, so we own every byte&#xA;and track no private binary spec.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-not-just-use-pagefind&#34;&gt;Why not just use Pagefind&lt;/h2&gt;&#xA;&lt;p&gt;Pagefind is excellent but is a Rust/WASM toolchain — bundling it breaks colophon&#39;s&#xA;single-binary principle, and there is &lt;strong&gt;no Go port of its indexer&lt;/strong&gt; (even Hugo, a Go SSG, shells&#xA;out to the Rust binary). Targeting Pagefind&#39;s on-disk format from Go is worse: it&#39;s an internal,&#xA;versioned, CBOR layout with an index↔WASM version handshake — we&#39;d be the sole maintainer of a&#xA;reverse-engineered emitter chasing every release. So: borrow the ideas, own the format.&lt;/p&gt;&#xA;&lt;h2 id=&#34;surfaces-and-engine-sharing&#34;&gt;Surfaces and engine sharing&lt;/h2&gt;&#xA;&lt;p&gt;One analyzer + one BM25 definition, three surfaces:&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Surface&lt;/th&gt;&#xA;&lt;th&gt;Where&lt;/th&gt;&#xA;&lt;th&gt;Consumes&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;Public site search&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;browser, vanilla JS&lt;/td&gt;&#xA;&lt;td&gt;the static sharded index we emit&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;&lt;code&gt;colophon search&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;Go CLI&lt;/td&gt;&#xA;&lt;td&gt;the same in-memory inverted index, server-side&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;Persona exemplars&lt;/strong&gt; (§8)&lt;/td&gt;&#xA;&lt;td&gt;Go&lt;/td&gt;&#xA;&lt;td&gt;the same analyzer + BM25 (zero-config default)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;The browser &lt;strong&gt;never&lt;/strong&gt; runs Go/Bleve/WASM — it reads our static files with a small JS scorer.&#xA;Bleve&#39;s on-disk (Scorch) format has no JS reader and isn&#39;t shippable; if Bleve is used at all&#xA;it stays &lt;strong&gt;build/CLI-side only&lt;/strong&gt;. The reusable core is &lt;strong&gt;hand-rolled and stdlib-only&lt;/strong&gt; (see&#xA;Packaging) so the inverted index &lt;em&gt;is&lt;/em&gt; the shared structure across all three surfaces.&lt;/p&gt;&#xA;&lt;p&gt;The one hard correctness rule: the &lt;strong&gt;analyzer must be identical&lt;/strong&gt; in the Go builder and the JS&#xA;reader, or a query for &amp;quot;running&amp;quot; won&#39;t match an indexed &amp;quot;run&amp;quot;. The analyzer is therefore&#xA;specified once (below) and implemented twice against that spec.&lt;/p&gt;&#xA;&lt;h2 id=&#34;packaging--module-boundary&#34;&gt;Packaging &amp;amp; module boundary&lt;/h2&gt;&#xA;&lt;p&gt;A &lt;strong&gt;separate Go module in this repo&lt;/strong&gt;, colophon-branded, wired with a root &lt;code&gt;go.work&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;github.com/jmylchreest/colophon            (the SSG — application module)&#xA;github.com/jmylchreest/colophon/search     (the engine — its own go.mod)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Separate module&lt;/strong&gt;, not an in-&lt;code&gt;colophon&lt;/code&gt; package: gives the engine its &lt;strong&gt;own lean &lt;code&gt;go.mod&lt;/code&gt;&lt;/strong&gt;&#xA;(a &lt;code&gt;go get …/search&lt;/code&gt; must not drag in pongo2 / goldmark / koanf / go-git) and &lt;strong&gt;independent&#xA;version tags&lt;/strong&gt; (&lt;code&gt;search/vX.Y.Z&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Not under &lt;code&gt;internal/&lt;/code&gt;&lt;/strong&gt; — that&#39;s compiler-private; reuse needs a public path.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;go.work&lt;/code&gt;&lt;/strong&gt; (committed at the repo root, listing &lt;code&gt;.&lt;/code&gt; and &lt;code&gt;./search&lt;/code&gt;) so colophon builds&#xA;against the local copy with no published tag, while co-development stays one-repo / one-PR.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Hand-rolled, zero-dependency core&lt;/strong&gt; — the engine ships its own inverted index + analyzer,&#xA;stdlib only. This is what makes it attractive to adopt. (colophon may still use Bleve&#xA;&lt;em&gt;separately&lt;/em&gt;, CLI-side, but the published engine does not depend on it.)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The reusable unit is &lt;strong&gt;three artifacts bound by one spec&lt;/strong&gt;:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Go builder/query module&lt;/strong&gt; (&lt;code&gt;…/colophon/search&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;JS reader&lt;/strong&gt; — a single dependency-free ES module (&lt;code&gt;search.js&lt;/code&gt;), also publishable to npm.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;The format + analyzer spec&lt;/strong&gt; — the real public contract. This document is the &lt;em&gt;rationale&lt;/em&gt;;&#xA;the &lt;strong&gt;normative, language-neutral specification&lt;/strong&gt; lives in&#xA;&lt;a href=&#34;../../search/SPEC.md&#34;&gt;&lt;code&gt;search/SPEC.md&lt;/code&gt;&lt;/a&gt; (with &lt;code&gt;search/README.md&lt;/code&gt; as the adopter entry point),&#xA;precise enough to implement a conformant reader or builder in any language against the&#xA;committed test vectors.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 id=&#34;engine-api-source--and-fs-agnostic&#34;&gt;Engine API (source- and FS-agnostic)&lt;/h3&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-go&#34;&gt;package search&#xA;&#xA;// A Doc is anything indexable — the engine knows nothing about colophon pages.&#xA;type Doc struct {&#xA;    ID    string            // stable, caller-provided (colophon uses the page URL/slug)&#xA;    URL   string            // result link&#xA;    Title string&#xA;    Body  string            // already-extracted plain text&#xA;    Meta  map[string]string // shown in the result card; not indexed unless requested&#xA;}&#xA;&#xA;type BuildOptions struct {&#xA;    Analyzer  Analyzer      // default: SimpleAnalyzer (below)&#xA;    ShardFunc ShardFunc     // default: fixed lexical ranges&#xA;    BM25      Params        // k1, b&#xA;}&#xA;&#xA;// Build writes the static index (manifest + shards + fragments) to dst. dst is an&#xA;// abstraction (a dir on disk for most users; colophon routes it through a publisher).&#xA;func Build(docs iter.Seq[Doc], dst Writer, opts BuildOptions) (Manifest, error)&#xA;&#xA;// Open mounts an emitted index for server-side querying (the CLI surface).&#xA;func Open(fsys fs.FS) (*Index, error)&#xA;func (*Index) Search(q string, limit int) ([]Result, error)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;Writer&lt;/code&gt; is a minimal &lt;code&gt;Put(name string, b []byte) error&lt;/code&gt; — the same shape as the publisher&#xA;&lt;code&gt;FileWriter&lt;/code&gt;, so colophon can emit straight through routing, and a standalone user can write to&#xA;a directory.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-analyzer-the-contract&#34;&gt;The analyzer (the contract)&lt;/h2&gt;&#xA;&lt;p&gt;Specified once; implemented identically in Go and JS. &lt;strong&gt;v1 is deliberately trivial&lt;/strong&gt; to make&#xA;parity self-evident and keep the core &lt;strong&gt;stdlib-only&lt;/strong&gt;:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Lowercase (Unicode-aware: Go &lt;code&gt;unicode.ToLower&lt;/code&gt; / JS &lt;code&gt;toLowerCase&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;Split on any run of non-(letter|number) → tokens (Go &lt;code&gt;unicode.IsLetter/IsNumber&lt;/code&gt; via&#xA;&lt;code&gt;strings.FieldsFunc&lt;/code&gt; / JS &lt;code&gt;/[^\p{L}\p{N}]+/u&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;No NFC normalization, no stop-words, no stemming&lt;/strong&gt; in v1.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;That&#39;s it — two implementations of one pure function &lt;code&gt;Analyze(string) []string&lt;/code&gt;. A shared&#xA;golden-vector fixture (&lt;code&gt;testdata/analyzer.json&lt;/code&gt;: input → expected tokens) is run by &lt;strong&gt;both&lt;/strong&gt; the&#xA;Go and JS test suites, so drift is caught mechanically.&lt;/p&gt;&#xA;&lt;p&gt;NFC is deferred deliberately: stdlib Go has no NFC, and adding it would pull in &lt;code&gt;x/text&lt;/code&gt; —&#xA;against the zero-dep goal. The consequence is that &lt;em&gt;decomposed&lt;/em&gt; Unicode (e.g. &lt;code&gt;e&lt;/code&gt;+combining&#xA;accent) tokenizes differently from &lt;em&gt;composed&lt;/em&gt; (&lt;code&gt;é&lt;/code&gt;); content from normal editors is composed, and&#xA;the golden fixture stays ASCII to avoid encoding ambiguity. NFC + a matched Go/JS stemmer arrive&#xA;together behind an analyzer-id bump (&lt;code&gt;simple-1&lt;/code&gt; → &lt;code&gt;…-2&lt;/code&gt;), which a stale reader can detect.&lt;/p&gt;&#xA;&lt;p&gt;Stemming (e.g. a matched Go+JS Snowball/Porter2 pair) and stop words are &lt;strong&gt;deferred&lt;/strong&gt; — added&#xA;only as a matched pair, behind a version bump of the analyzer id recorded in the manifest.&lt;/p&gt;&#xA;&lt;h2 id=&#34;index-format&#34;&gt;Index format&lt;/h2&gt;&#xA;&lt;p&gt;Emitted as plain static files under a configurable base (default &lt;code&gt;/_search/&lt;/code&gt;):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;_search/&#xA;  manifest.json                 # the mutable root — small, short-TTL&#xA;  index/&amp;lt;range&amp;gt;.&amp;lt;hash&amp;gt;.json.gz  # postings shards — immutable, content-addressed&#xA;  fragment/&amp;lt;docid&amp;gt;.&amp;lt;hash&amp;gt;.json  # per-result cards — immutable, content-addressed&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;manifest.json&lt;/code&gt;&lt;/strong&gt; — routing + scoring constants, loaded once (a few KB):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;v&amp;#34;: 1,&#xA;  &amp;#34;analyzer&amp;#34;: &amp;#34;simple-1&amp;#34;,&#xA;  &amp;#34;bm25&amp;#34;: { &amp;#34;k1&amp;#34;: 1.2, &amp;#34;b&amp;#34;: 0.75 },&#xA;  &amp;#34;docs&amp;#34;: 412,&#xA;  &amp;#34;avgdl&amp;#34;: 680.4,&#xA;  &amp;#34;shards&amp;#34;: [&#xA;    { &amp;#34;lo&amp;#34;: &amp;#34;a&amp;#34;,  &amp;#34;hi&amp;#34;: &amp;#34;cz&amp;#34;, &amp;#34;url&amp;#34;: &amp;#34;index/a-cz.7c1e9b.json.gz&amp;#34; },&#xA;    { &amp;#34;lo&amp;#34;: &amp;#34;d&amp;#34;,  &amp;#34;hi&amp;#34;: &amp;#34;gz&amp;#34;, &amp;#34;url&amp;#34;: &amp;#34;index/d-gz.2f4a01.json.gz&amp;#34; }&#xA;  ],&#xA;  &amp;#34;fragments&amp;#34;: { &amp;#34;...&amp;#34;: &amp;#34;docid → fragment/&amp;lt;docid&amp;gt;.&amp;lt;hash&amp;gt;.json&amp;#34; }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;A postings shard&lt;/strong&gt; — &lt;code&gt;term → [[docId, termFreq]]&lt;/code&gt; (positions omitted in v1 → no phrase search):&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{ &amp;#34;tiger&amp;#34;: &amp;#91;[7,1],[88,2]], &amp;#34;tigris&amp;#34;: &amp;#91;[7,3]] }&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;DocIds in postings are &lt;strong&gt;small integers&lt;/strong&gt; interned from the stable string ID via a table in the&#xA;manifest — compact in postings, while the &lt;em&gt;interning is deterministic from sorted stable IDs&lt;/em&gt;&#xA;(see below). Fragments are keyed by the string docId.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;A fragment&lt;/strong&gt; — everything needed to render one result, fetched only for shown hits:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-json&#34;&gt;{ &amp;#34;url&amp;#34;: &amp;#34;/posts/tigris/&amp;#34;, &amp;#34;title&amp;#34;: &amp;#34;Publishing to Tigris&amp;#34;, &amp;#34;excerpt&amp;#34;: &amp;#34;…&amp;#34;, &amp;#34;meta&amp;#34;: {&amp;#34;type&amp;#34;:&amp;#34;post&amp;#34;} }&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;extensibility-shared-substrate--pluggable-index-types&#34;&gt;Extensibility: shared substrate + pluggable index types&lt;/h2&gt;&#xA;&lt;p&gt;The format separates a &lt;strong&gt;substrate&lt;/strong&gt; (shared by every search mode) from &lt;strong&gt;index types&lt;/strong&gt; layered&#xA;over it. This is the seam that lets fuzzy and semantic search be added later as &lt;em&gt;additive&#xA;artifacts&lt;/em&gt;, never a reformat.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Substrate (always present):&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Doc identity&lt;/strong&gt; — the stable string ID ↔ interned int table (in the manifest).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Fragments&lt;/strong&gt; — per-doc result cards (&lt;code&gt;fragment/…&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Manifest&lt;/strong&gt; — the mutable root, listing which index types are present and where their shards live.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;Index types&lt;/strong&gt; (each optional; each sharded + content-addressed + deterministic the same way):&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Type&lt;/th&gt;&#xA;&lt;th&gt;Maps&lt;/th&gt;&#xA;&lt;th&gt;Status&lt;/th&gt;&#xA;&lt;th&gt;Adds&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;lexical&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;term → [docId, tf] (BM25)&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;v1&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;index/&lt;/code&gt; shards&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;fuzzy&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;trigram → [termId]&lt;/td&gt;&#xA;&lt;td&gt;opt-in, additive&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;trigram/&lt;/code&gt; shards&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;semantic&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;docId/chunk → vector (+ ANN)&lt;/td&gt;&#xA;&lt;td&gt;future, additive&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;vector/&lt;/code&gt; shards&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;The manifest gains one optional section per present type; the Go query layer and JS reader&#xA;dispatch on what&#39;s there. Turning a type on is a build flag plus more emitted files — the&#xA;substrate, the postings format, and existing files are untouched. &lt;code&gt;BuildOptions&lt;/code&gt; gains &lt;code&gt;Fuzzy bool&lt;/code&gt; (and later &lt;code&gt;Semantic …&lt;/code&gt;) accordingly.&lt;/p&gt;&#xA;&lt;h3 id=&#34;fuzzy--typo-tolerance-n-gram--levenshtein&#34;&gt;Fuzzy / typo-tolerance (n-gram + Levenshtein)&lt;/h3&gt;&#xA;&lt;p&gt;The optional &lt;code&gt;fuzzy&lt;/code&gt; type is a &lt;strong&gt;character-trigram index&lt;/strong&gt; (&lt;code&gt;trigram → terms&lt;/code&gt;), sharded by&#xA;trigram range like everything else. Query path, when enabled and an exact match yields too few&#xA;hits: decompose the query term into trigrams → fetch those trigram shards → gather candidate&#xA;terms by trigram overlap → keep those within a bounded &lt;strong&gt;Levenshtein&lt;/strong&gt; distance (computed in JS&#xA;over the small candidate set) → fetch the candidates&#39; postings shards → BM25, optionally&#xA;down-weighted by edit distance. Trigrams derive from the &lt;em&gt;same&lt;/em&gt; analyzer output, so there&#39;s no&#xA;new analysis contract. Opt-in because it roughly doubles index size.&lt;/p&gt;&#xA;&lt;p&gt;Two near-free relatives of lexical-range sharding: &lt;strong&gt;prefix/autocomplete&lt;/strong&gt; (shards are sorted, so&#xA;a prefix hits one/few shards via binary search) and &lt;strong&gt;substring&lt;/strong&gt; (falls out of the trigram index).&lt;/p&gt;&#xA;&lt;h2 id=&#34;sharding--fixed-lexical-ranges&#34;&gt;Sharding — fixed lexical ranges&lt;/h2&gt;&#xA;&lt;p&gt;Shards are bucketed by &lt;strong&gt;fixed, stable lexical term ranges&lt;/strong&gt; (&lt;code&gt;a–cz&lt;/code&gt;, &lt;code&gt;d–gz&lt;/code&gt;, …), &lt;strong&gt;not&lt;/strong&gt;&#xA;Pagefind&#39;s fixed-&lt;em&gt;count&lt;/em&gt; chunks. Rationale: fixed-count chunks shift their split points as&#xA;vocabulary grows, cascading rewrites across many shards; fixed ranges mean a new term lands in&#xA;its existing bucket and only that bucket changes. Cost: uneven shard sizes — handled by a&#xA;deterministic rule that &lt;strong&gt;sub-splits only over-large ranges&lt;/strong&gt; (e.g. &lt;code&gt;a&lt;/code&gt; → &lt;code&gt;aa–am&lt;/code&gt;, &lt;code&gt;an–az&lt;/code&gt;),&#xA;which is itself stable given the same vocabulary.&lt;/p&gt;&#xA;&lt;h2 id=&#34;determinism--incrementality&#34;&gt;Determinism &amp;amp; incrementality&lt;/h2&gt;&#xA;&lt;p&gt;The point: an edit should rewrite &lt;strong&gt;as few files as possible&lt;/strong&gt;, so the incremental publisher&#xA;(content-hash diff + orphan prune, already built) uploads almost nothing and the CDN caches the&#xA;rest forever. Five composing rules:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Content-addressed filenames&lt;/strong&gt; — every shard and fragment is named by a hash of its bytes.&#xA;Unchanged content → identical name → publisher sees no change → no upload; and the file can be&#xA;served &lt;code&gt;Cache-Control: immutable, max-age=1y&lt;/code&gt;. The &lt;strong&gt;manifest is the only mutable file&lt;/strong&gt; (it&#xA;maps logical keys → current hashes): a &lt;em&gt;mutable root over an immutable, content-addressed&#xA;tree&lt;/em&gt; (git&#39;s model).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Stable doc IDs, never positional.&lt;/strong&gt; Postings key off a stable per-doc ID (colophon: the&#xA;page URL). Adding/removing a post must not renumber the others. Integer interning for&#xA;compactness is assigned by &lt;strong&gt;sorted stable-ID order recorded in the manifest&lt;/strong&gt; — deterministic&#xA;and stateless (no committed id-map; honors §8 &amp;quot;regenerable, not committed&amp;quot;). &lt;em&gt;(Note: a pure&#xA;insert still renumbers ints after it; if that churn proves costly we revisit with a&#xA;prev-manifest-seeded allocator. v1 keeps it stateless.)&lt;/em&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Stable shard boundaries&lt;/strong&gt; (fixed lexical ranges, above) — vocabulary growth doesn&#39;t reshuffle.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Canonical serialization&lt;/strong&gt; — sorted keys, stable number formatting, fixed field order, and&#xA;&lt;strong&gt;gzip with mtime=0 + fixed level&lt;/strong&gt;. Without this, &amp;quot;unchanged&amp;quot; content re-hashes every build&#xA;(gzip embeds a timestamp by default). This is what makes the content-addressing actually hold.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Postings/presentation split&lt;/strong&gt; — volatile display data (excerpt, title styling, meta) lives&#xA;in &lt;strong&gt;fragments&lt;/strong&gt;; postings are just &lt;code&gt;term → [id, tf]&lt;/code&gt;. A cosmetic edit touches one fragment and&#xA;&lt;strong&gt;zero shards&lt;/strong&gt;; a body edit touches that fragment plus only the shards for the terms that&#xA;actually changed.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;&lt;strong&gt;Inherent churn (accepted):&lt;/strong&gt; the manifest (small, by design); hot-term shards (&lt;code&gt;the&lt;/code&gt;, &lt;code&gt;and&lt;/code&gt;)&#xA;on most edits (a few files, not the index). &lt;strong&gt;Orphans&lt;/strong&gt; (superseded content-addressed files) are&#xA;removed by the publisher&#39;s existing &lt;code&gt;delete_orphaned&lt;/code&gt;; the manifest is marked &lt;code&gt;Protected&lt;/code&gt; so it&#xA;is never deleted mid-swap.&lt;/p&gt;&#xA;&lt;p&gt;Typical &amp;quot;edit one post&amp;quot; outcome: &lt;strong&gt;1 new fragment + 1 changed manifest + a few hot-term shards&lt;/strong&gt;,&#xA;everything else byte-identical and skipped.&lt;/p&gt;&#xA;&lt;h3 id=&#34;multi-deployment-sharing--the-protection-trade-off&#34;&gt;Multi-deployment sharing &amp;amp; the protection trade-off&lt;/h3&gt;&#xA;&lt;p&gt;Several deployments (sites × environments) can publish to &lt;strong&gt;one&lt;/strong&gt; object store. Only the manifest&#xA;is per-deployment — its name is a short hash of &lt;code&gt;(siteID, env)&lt;/code&gt; (&lt;code&gt;manifest-&amp;lt;hash&amp;gt;.json&lt;/code&gt;; the bare&#xA;&lt;code&gt;manifest.json&lt;/code&gt; is the no-site/no-env default) — so the mutable roots don&#39;t collide. The&#xA;content-addressed shards/fragments are &lt;strong&gt;shared&lt;/strong&gt;: identical content across deployments dedupes to&#xA;one object, and each deployment&#39;s reader loads its own manifest (told via &lt;code&gt;data-search-manifest&lt;/code&gt;).&lt;/p&gt;&#xA;&lt;p&gt;To make that safe, the whole &lt;code&gt;_search/&lt;/code&gt; prefix is exempt from orphan-deletion: each publisher&#39;s&#xA;&lt;code&gt;Protected(name)&lt;/code&gt; returns true for it, and the incremental planner deletes a deployed object only&#xA;when it is &lt;em&gt;both&lt;/em&gt; absent from the current build &lt;em&gt;and&lt;/em&gt; not protected. So a deployment never prunes&#xA;another&#39;s shards or manifest — it only writes its own manifest and adds shards.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;TODO (revisit): garbage collection for &lt;code&gt;_search/&lt;/code&gt;.&lt;/strong&gt; The cost of the protection is that&#xA;superseded shards/fragments (and the pre-hash &lt;code&gt;manifest.json&lt;/code&gt;) accumulate, never auto-pruned —&#xA;tiny (~200 B each) and deduped, but unbounded over time. A proper fix is a &lt;strong&gt;mark-and-sweep GC&lt;/strong&gt;:&#xA;read &lt;em&gt;every&lt;/em&gt; live &lt;code&gt;manifest-*.json&lt;/code&gt; in the store, union the shards/fragments they reference, and&#xA;delete the &lt;code&gt;_search/&lt;/code&gt; objects nothing references. This can&#39;t be the per-publish &lt;code&gt;delete_orphaned&lt;/code&gt;&#xA;(which only sees one deployment&#39;s set); it wants to be an explicit pass (e.g. &lt;code&gt;colophon search gc&lt;/code&gt;&#xA;/ &lt;code&gt;publish --gc&lt;/code&gt;). Related: the protected-prefix list is currently &lt;strong&gt;hardcoded and duplicated&lt;/strong&gt;&#xA;across the r2/s3/local &lt;code&gt;Protected&lt;/code&gt; methods — worth centralizing (and possibly making&#xA;configurable) at the same time.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;browser-query-flow&#34;&gt;Browser query flow&lt;/h2&gt;&#xA;&lt;p&gt;The whole reader is ~a screen of dependency-free JS:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Fetch &lt;code&gt;manifest.json&lt;/code&gt; once (cache in memory).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;analyze()&lt;/code&gt; the query (the shared analyzer).&lt;/li&gt;&#xA;&lt;li&gt;For each term, binary-search &lt;code&gt;shards&lt;/code&gt; for its range, fetch &lt;strong&gt;only that shard&lt;/strong&gt; (dedupe + cache).&lt;/li&gt;&#xA;&lt;li&gt;BM25 over the loaded postings:&#xA;&lt;code&gt;idf = ln(1 + (N − df + 0.5)/(df + 0.5))&lt;/code&gt;,&#xA;&lt;code&gt;score += idf · tf·(k1+1) / (tf + k1·(1 − b + b·dl/avgdl))&lt;/code&gt;&#xA;(&lt;code&gt;N&lt;/code&gt;, &lt;code&gt;avgdl&lt;/code&gt;, per-shard &lt;code&gt;df&lt;/code&gt;, per-doc &lt;code&gt;dl&lt;/code&gt; all come from the manifest/shard).&lt;/li&gt;&#xA;&lt;li&gt;Sort, take top-&lt;code&gt;limit&lt;/code&gt;, fetch &lt;strong&gt;only those&lt;/strong&gt; fragments, render.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Memory at any instant = manifest + the shards the query touched + the visible fragments. Never&#xA;the whole index.&lt;/p&gt;&#xA;&lt;h2 id=&#34;progressive-enhancement--theme-integration&#34;&gt;Progressive enhancement &amp;amp; theme integration&lt;/h2&gt;&#xA;&lt;p&gt;Search ships &lt;strong&gt;only when enabled&lt;/strong&gt; (like the glossary ships only when used) and is a&#xA;&lt;strong&gt;progressive enhancement&lt;/strong&gt; (consistent with the raw-block contract): the search box degrades to&#xA;a plain link to an archive/index page without JS, and enhances to live search when &lt;code&gt;search.js&lt;/code&gt;&#xA;loads. The &lt;code&gt;search.js&lt;/code&gt; + CSS are theme/engine-emitted assets; the index files are emitted by the&#xA;build (and routable like any other output — so the index can even live on an object store while&#xA;HTML is on Pages).&lt;/p&gt;&#xA;&lt;h2 id=&#34;semantic--a-future-index-type-over-the-same-substrate&#34;&gt;Semantic — a future index type over the same substrate&lt;/h2&gt;&#xA;&lt;p&gt;v1 is &lt;strong&gt;lexical only&lt;/strong&gt;, but semantic is designed-in as the &lt;code&gt;semantic&lt;/code&gt; index type (above), not a&#xA;parallel system. It emits per-chunk &lt;strong&gt;embedding vectors&lt;/strong&gt; as content-addressed, sharded files&#xA;reusing the &lt;em&gt;same&lt;/em&gt; doc identity and fragments — added alongside lexical, never instead.&lt;/p&gt;&#xA;&lt;p&gt;The only genuinely new cost is &lt;strong&gt;query-time embedding&lt;/strong&gt;: the browser needs a model or a query&#xA;endpoint; the &lt;em&gt;index&lt;/em&gt; slots into the existing file model. Scaling options that fit the sharded&#xA;design: small corpora load all vectors (brute-force cosine); larger ones use IVF-style &lt;strong&gt;centroid&#xA;prefiltering&lt;/strong&gt; (centroids in the manifest → fetch only the nearest clusters&#39; vector shards), or&#xA;the §8 pure-Go &lt;strong&gt;HNSW&lt;/strong&gt; behind the &lt;code&gt;Retriever&lt;/code&gt; interface. The CLI semantic path (§8 &lt;code&gt;vectors.f32&lt;/code&gt;)&#xA;is the same vectors consumed in Go. None of this is in v1 — but the substrate makes it additive.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Embedder-parity contract&lt;/strong&gt; (the embedding analog of the analyzer contract): the build-side&#xA;embedder (Go) and the query-side embedder (browser) must be the &lt;strong&gt;same model&lt;/strong&gt;, or query vectors&#xA;won&#39;t share the doc vectors&#39; space. The recommended future embedder is therefore &lt;strong&gt;static&#xA;embeddings (Model2Vec / &lt;code&gt;potion-*&lt;/code&gt;)&lt;/strong&gt;: a token→vector lookup table that is implementable&#xA;identically in Go and JS from one shared weights file (golden-vector tested, like the analyzer),&#xA;runs in pure code with &lt;strong&gt;no ONNX/WASM runtime on either side&lt;/strong&gt;, and is a few MB rather than ~30MB.&#xA;It trades ~10–20% quality vs a MiniLM transformer — acceptable because semantic is used as a&#xA;&lt;strong&gt;hybrid recall/rerank assist over lexical BM25&lt;/strong&gt;, not a replacement. A full transformers.js&#xA;MiniLM stays the higher-quality opt-in fallback.&lt;/p&gt;&#xA;&lt;h2 id=&#34;key-decisions&#34;&gt;Key decisions&lt;/h2&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Decision&lt;/th&gt;&#xA;&lt;th&gt;Choice&lt;/th&gt;&#xA;&lt;th&gt;Rationale&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Build vs runtime&lt;/td&gt;&#xA;&lt;td&gt;Build-time static index&lt;/td&gt;&#xA;&lt;td&gt;Fully static, no server (§8)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Format&lt;/td&gt;&#xA;&lt;td&gt;Our own JSON(.gz)&lt;/td&gt;&#xA;&lt;td&gt;Own every byte; no Pagefind-format/version coupling&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Browser runtime&lt;/td&gt;&#xA;&lt;td&gt;Vanilla JS scorer&lt;/td&gt;&#xA;&lt;td&gt;No WASM; fine at blog/medium scale; ours to maintain&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Architecture&lt;/td&gt;&#xA;&lt;td&gt;Substrate + pluggable index types&lt;/td&gt;&#xA;&lt;td&gt;Fuzzy/semantic become additive, not a reformat&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Index shape&lt;/td&gt;&#xA;&lt;td&gt;Sharded inverted (BM25)&lt;/td&gt;&#xA;&lt;td&gt;Never load the whole index; low bandwidth&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Fuzzy&lt;/td&gt;&#xA;&lt;td&gt;Opt-in trigram index + Levenshtein filter&lt;/td&gt;&#xA;&lt;td&gt;Typo tolerance without bloating the default index&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Sharding&lt;/td&gt;&#xA;&lt;td&gt;Fixed lexical ranges&lt;/td&gt;&#xA;&lt;td&gt;Stable boundaries → minimal rewrites on edit&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Filenames&lt;/td&gt;&#xA;&lt;td&gt;Content-addressed&lt;/td&gt;&#xA;&lt;td&gt;Incremental publish + immutable CDN caching&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Doc IDs&lt;/td&gt;&#xA;&lt;td&gt;Stable (URL-derived)&lt;/td&gt;&#xA;&lt;td&gt;Edits don&#39;t renumber → postings stay stable&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Serialization&lt;/td&gt;&#xA;&lt;td&gt;Canonical, gzip mtime=0&lt;/td&gt;&#xA;&lt;td&gt;Identical content → identical bytes (determinism)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Analyzer&lt;/td&gt;&#xA;&lt;td&gt;Simple (no stemming) v1&lt;/td&gt;&#xA;&lt;td&gt;Trivial Go/JS parity; golden-vector tested&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Packaging&lt;/td&gt;&#xA;&lt;td&gt;Separate module, same repo, &lt;code&gt;go.work&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Dep isolation + own tags; co-dev stays cheap&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Engine deps&lt;/td&gt;&#xA;&lt;td&gt;Hand-rolled, stdlib-only&lt;/td&gt;&#xA;&lt;td&gt;Adoptable library; Bleve (if any) stays CLI-side&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Semantic&lt;/td&gt;&#xA;&lt;td&gt;CLI-side only&lt;/td&gt;&#xA;&lt;td&gt;Public semantic needs a model at query time (§8)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;h2 id=&#34;acceptance-criteria&#34;&gt;Acceptance criteria&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34; aria-label=&#34;Not completed&#34;&gt; &lt;code&gt;search.Build&lt;/code&gt; emits manifest + shards + fragments to a &lt;code&gt;Writer&lt;/code&gt;; re-running on unchanged&#xA;input produces &lt;strong&gt;byte-identical&lt;/strong&gt; shard/fragment files (determinism).&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34; aria-label=&#34;Not completed&#34;&gt; Editing one doc changes only its fragment, the manifest, and the shards for its changed&#xA;terms — all other files byte-identical.&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34; aria-label=&#34;Not completed&#34;&gt; A query loads the manifest + only the shards for its terms (verified by fetch count), and&#xA;fetches fragments only for displayed results.&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34; aria-label=&#34;Not completed&#34;&gt; BM25 ranking from the JS reader matches the Go &lt;code&gt;Index.Search&lt;/code&gt; ranking on a shared fixture.&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34; aria-label=&#34;Not completed&#34;&gt; The shared analyzer golden-vector fixture passes in &lt;strong&gt;both&lt;/strong&gt; Go and JS suites.&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34; aria-label=&#34;Not completed&#34;&gt; &lt;code&gt;colophon search --json&lt;/code&gt; returns ranked results from the same engine (replaces the stub).&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34; aria-label=&#34;Not completed&#34;&gt; With JS disabled, the search UI degrades to a working archive link.&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34; aria-label=&#34;Not completed&#34;&gt; &lt;code&gt;go get github.com/jmylchreest/colophon/search&lt;/code&gt; pulls a lean module (no SSG deps).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;files-to-create&#34;&gt;Files to create&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;search/go.mod&lt;/code&gt;, root &lt;code&gt;go.work&lt;/code&gt; — the module + workspace.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;search/analyzer.go&lt;/code&gt; (+ &lt;code&gt;testdata/analyzer.json&lt;/code&gt;) — the analyzer spec + golden vectors.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;search/index.go&lt;/code&gt; — inverted index, BM25, &lt;code&gt;Build&lt;/code&gt;, sharding, canonical serialization.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;search/query.go&lt;/code&gt; — &lt;code&gt;Open&lt;/code&gt; / &lt;code&gt;Index.Search&lt;/code&gt; (CLI surface).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;search/format.go&lt;/code&gt; — manifest/shard/fragment types + content-addressing.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;search/search.js&lt;/code&gt; (+ test) — the browser reader; shares the analyzer + golden vectors.&lt;/li&gt;&#xA;&lt;li&gt;colophon side: &lt;code&gt;internal/build/search.go&lt;/code&gt; (extract page text → &lt;code&gt;Doc&lt;/code&gt;s → &lt;code&gt;Build&lt;/code&gt; via a routed&#xA;Writer), wire &lt;code&gt;SearchCmd&lt;/code&gt; to &lt;code&gt;search.Open(...).Search&lt;/code&gt;, theme assets + a &lt;code&gt;search&lt;/code&gt; partial.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;releasing--using-it-elsewhere&#34;&gt;Releasing / using it elsewhere&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;./search&lt;/code&gt; is its own Go module (&lt;code&gt;github.com/jmylchreest/colophon/search&lt;/code&gt;) kept in this repo;&#xA;colophon builds against it locally via the &lt;code&gt;go.work&lt;/code&gt; workspace, so it needs no published tag&#xA;for development. For external consumers it&#39;s published from the monorepo as a &lt;strong&gt;nested module&lt;/strong&gt;:&#xA;the release workflow tags &lt;code&gt;search/v&amp;lt;colophon-version&amp;gt;&lt;/code&gt; whenever &lt;code&gt;./search&lt;/code&gt; has changed since its&#xA;last tag (mirroring the colophon release version — gaps are expected). Others then&#xA;&lt;code&gt;go get github.com/jmylchreest/colophon/search@search/v&amp;lt;version&amp;gt;&lt;/code&gt;. Those &lt;code&gt;search/v*&lt;/code&gt; tags don&#39;t&#xA;trigger the binary-release workflow (its trigger is &lt;code&gt;v*&lt;/code&gt;). If the engine ever warrants a&#xA;standalone identity (e.g. &lt;code&gt;github.com/jmylchreest/n&lt;/code&gt;), it would move to its own repo, since a&#xA;module&#39;s path must match its repository URL.&lt;/p&gt;&#xA;&lt;h2 id=&#34;out-of-scope-future&#34;&gt;Out of scope (future)&lt;/h2&gt;&#xA;&lt;p&gt;Stemming/stop-words (matched Go+JS pair, analyzer-id bump); positions → phrase/proximity;&#xA;filters/facets + sorts; sub-splitting heuristics tuning; in-browser semantic; a published npm&#xA;package for &lt;code&gt;search.js&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/design/search.md&#34;&gt;&lt;code&gt;docs/design/search.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <title>Design: webmention</title>
    <id>https://docs.colophon.blog/internals/webmention/</id>
    <link href="https://docs.colophon.blog/internals/webmention/" rel="alternate"></link>
    <updated>2001-11-20T00:00:00Z</updated>
    <published>2001-11-20T00:00:00Z</published>
    <summary type="text">Goal: let a colophon site participate in Webmention — the W3C standard for &#34;site A notified site B that it linked to / replied to / liked B&#39;s post&#34; — while staying fully…</summary>
    <content type="html">&lt;!-- Generated by tools/gendocs from docs/design/webmention.md — do not edit by hand. --&gt;&#xA;&lt;div class=&#34;callout callout-note&#34; data-callout=&#34;note&#34;&gt;&#xA;&lt;div class=&#34;callout-title&#34;&gt;Internal design note&lt;/div&gt;&#xA;&lt;div class=&#34;callout-body&#34;&gt;&#xA;&lt;p&gt;This is an engineering design document — it describes how colophon works inside, and may run ahead of (or behind) the shipped code. The user-facing guides are the source of truth for behaviour.&lt;/p&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Status: &lt;strong&gt;built&lt;/strong&gt; · relates to PLAN §10 (Federation &amp;amp; IndieWeb), §6 (build pipeline). The whole&#xA;design ships: the &lt;code&gt;&amp;lt;link rel=&amp;quot;webmention&amp;quot;&amp;gt;&lt;/code&gt; tag; &lt;code&gt;webmention send&lt;/code&gt; (sent-cache); &lt;code&gt;webmention fetch&lt;/code&gt; (jf2 reader → &lt;code&gt;_mentions/&lt;/code&gt; cache); per-site &lt;code&gt;display.mode&lt;/code&gt; (live/asset/disabled) with the&#xA;themed responses block + &lt;code&gt;mentions.js&lt;/code&gt;; &lt;code&gt;webmention publish&lt;/code&gt; (decoupled &lt;code&gt;_mentions/&lt;/code&gt;-only deploy);&#xA;and the committed glob blocklist + &lt;code&gt;colophon-moderate-mentions&lt;/code&gt; skill. Code in&#xA;&lt;code&gt;internal/webmention/&lt;/code&gt;, &lt;code&gt;internal/build/{mentions,webmention}.go&lt;/code&gt;, &lt;code&gt;internal/cli/webmention.go&lt;/code&gt;.&#xA;Deferred: semantic moderation and searchable mentions (see those sections).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Goal: let a colophon site participate in &lt;a href=&#34;https://www.w3.org/TR/webmention/&#34;&gt;Webmention&lt;/a&gt; — the&#xA;W3C standard for &amp;quot;site A notified site B that it linked to / replied to / liked B&#39;s post&amp;quot; —&#xA;while staying &lt;strong&gt;fully static&lt;/strong&gt; (no server, no database) and keeping received data in &lt;strong&gt;clean,&#xA;separate assets&lt;/strong&gt; rather than mixed into the author&#39;s content. Two halves: &lt;strong&gt;sending&lt;/strong&gt; (we tell&#xA;others we linked them) and &lt;strong&gt;receiving + display&lt;/strong&gt; (others&#39; mentions appear under our posts).&lt;/p&gt;&#xA;&lt;p&gt;It deliberately reuses five patterns colophon already has, rather than inventing new ones — see&#xA;&lt;a href=&#34;#fit-with-existing-design&#34;&gt;Fit with existing design&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-a-separate-command-not-part-of-build&#34;&gt;Why a separate command (not part of build)&lt;/h2&gt;&#xA;&lt;p&gt;The two operations run at different points in the lifecycle, so neither belongs inside &lt;code&gt;build&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Operation&lt;/th&gt;&#xA;&lt;th&gt;When&lt;/th&gt;&#xA;&lt;th&gt;Why&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;&lt;code&gt;colophon webmention send&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;after&lt;/strong&gt; &lt;code&gt;publish&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;the source URL must be live so the receiver can fetch it back and verify the link&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;&lt;code&gt;colophon webmention fetch&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;build&lt;/code&gt;, or standalone/scheduled&lt;/td&gt;&#xA;&lt;td&gt;pulls received mentions into the local cache so a bake build can read them&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;&lt;code&gt;colophon webmention publish&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;on its own cadence&lt;/strong&gt; (e.g. cron)&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;fetch&lt;/code&gt; + push &lt;strong&gt;only&lt;/strong&gt; the &lt;code&gt;_mentions/&lt;/code&gt; prefix to the object store, decoupled from the site build — see &lt;a href=&#34;#separate-publish-pipeline&#34;&gt;Separate publish pipeline&lt;/a&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;So &lt;code&gt;webmention&lt;/code&gt; is a command group alongside &lt;code&gt;build&lt;/code&gt;/&lt;code&gt;publish&lt;/code&gt;. A typical CI flow:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;colophon webmention fetch     # refresh received mentions → committed cache&#xA;colophon build                # emit _mentions/ assets (+ bake for no-JS themes)&#xA;colophon publish --env production --allow-publish&#xA;colophon webmention send      # now that the new post is live, notify the sites it links to&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;fetch&lt;/code&gt; is also fine to run on a schedule (cron) to keep mentions fresh without a content change.&lt;/p&gt;&#xA;&lt;h2 id=&#34;receiving-data-as-a-separate-asset&#34;&gt;Receiving: data as a separate asset&lt;/h2&gt;&#xA;&lt;p&gt;We don&#39;t run a server, so we can&#39;t accept inbound POSTs. Mentions are received by a &lt;strong&gt;hosted&#xA;receiver&lt;/strong&gt; — &lt;a href=&#34;https://webmention.io&#34;&gt;webmention.io&lt;/a&gt; (free; the page advertises it via&#xA;&lt;code&gt;&amp;lt;link rel=&amp;quot;webmention&amp;quot;&amp;gt;&lt;/code&gt;). &lt;code&gt;fetch&lt;/code&gt; reads them back through its JSON API (token in &lt;code&gt;{env:…}&lt;/code&gt;,&#xA;never config) and normalises them into &lt;strong&gt;one JSON file per post&lt;/strong&gt;, served as its own asset&#xA;namespace — exactly mirroring the &lt;code&gt;_search/&lt;/code&gt; index:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;_mentions/&amp;lt;post-path&amp;gt;.json     e.g. _mentions/posts/hello-world.json&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Each file is a small, normalised list (not the raw webmention.io payload):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;target&amp;#34;: &amp;#34;https://blog.example.com/posts/hello-world/&amp;#34;,&#xA;  &amp;#34;updated&amp;#34;: &amp;#34;2026-06-22T10:00:00Z&amp;#34;,&#xA;  &amp;#34;mentions&amp;#34;: [&#xA;    { &amp;#34;type&amp;#34;: &amp;#34;like&amp;#34;,   &amp;#34;author&amp;#34;: {&amp;#34;name&amp;#34;: &amp;#34;Ada&amp;#34;, &amp;#34;url&amp;#34;: &amp;#34;https://ada.example&amp;#34;, &amp;#34;photo&amp;#34;: &amp;#34;https://…/ada.jpg&amp;#34;},&#xA;      &amp;#34;url&amp;#34;: &amp;#34;https://ada.example/likes/1&amp;#34;, &amp;#34;published&amp;#34;: &amp;#34;2026-06-21T09:00:00Z&amp;#34; },&#xA;    { &amp;#34;type&amp;#34;: &amp;#34;reply&amp;#34;,  &amp;#34;author&amp;#34;: {&amp;#34;name&amp;#34;: &amp;#34;Bob&amp;#34;, &amp;#34;url&amp;#34;: &amp;#34;https://bob.example&amp;#34;, &amp;#34;photo&amp;#34;: &amp;#34;…&amp;#34;},&#xA;      &amp;#34;url&amp;#34;: &amp;#34;https://bob.example/notes/2&amp;#34;, &amp;#34;published&amp;#34;: &amp;#34;…&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Nice post!&amp;#34; }&#xA;  ]&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;type&lt;/code&gt; is normalised to &lt;code&gt;like | repost | reply | mention&lt;/code&gt; (from the sender&#39;s mf2 / wm-property).&lt;/li&gt;&#xA;&lt;li&gt;author fields come from the sender&#39;s &lt;code&gt;h-card&lt;/code&gt; — which is &lt;em&gt;why&lt;/em&gt; mf2 shipped first.&lt;/li&gt;&#xA;&lt;li&gt;The build emits these to the output tree under &lt;code&gt;_mentions/&lt;/code&gt;, &lt;strong&gt;routed to R2&lt;/strong&gt; like &lt;code&gt;_search/&lt;/code&gt;,&#xA;and they get &lt;strong&gt;CORS for free&lt;/strong&gt; from &lt;code&gt;publish --create&lt;/code&gt; (same &lt;code&gt;GET/HEAD&lt;/code&gt; rule the JS search&#xA;index already relies on for cross-origin fetch).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;fetch&lt;/code&gt; writes a local cache (&lt;code&gt;.colophon/cache/webmentions/&lt;/code&gt;). This cache is &lt;strong&gt;not&lt;/strong&gt; treated&#xA;like the generated-image cache — see below.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;One JSON-per-post (no sharded manifest like search) because a post page already knows its own key.&lt;/p&gt;&#xA;&lt;h3 id=&#34;not-reproducible--and-thats-fine&#34;&gt;Not reproducible — and that&#39;s fine&lt;/h3&gt;&#xA;&lt;p&gt;Unlike the gen-image cache (a deterministic function of the prompt, committed for reproducible&#xA;builds), received mentions are &lt;strong&gt;external, time-varying state&lt;/strong&gt; — other people&#39;s posts. The local&#xA;&lt;code&gt;.colophon/cache/webmentions/&lt;/code&gt; is therefore a &lt;strong&gt;derived export, not preserved state&lt;/strong&gt;: webmention.io&#xA;is the source of truth and &lt;code&gt;fetch&lt;/code&gt; &lt;strong&gt;fully regenerates the export from it every run&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;fetch&lt;/code&gt; queries the whole domain — &lt;code&gt;GET /api/mentions.jf2?domain=&amp;lt;domain&amp;gt;&amp;amp;token={env:…}&lt;/code&gt; returns&#xA;the complete current set (JF2, newest-first), paged via &lt;code&gt;per-page&lt;/code&gt;+&lt;code&gt;page&lt;/code&gt; — then buckets by&#xA;&lt;code&gt;target&lt;/code&gt; and writes one JSON per post, replacing whatever was there. Properties of that:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Stateless / idempotent.&lt;/strong&gt; A fresh CI runner with an empty cache just rebuilds it from the API&#xA;(needs only network + token). &amp;quot;Empty&amp;quot; means &amp;quot;rebuild it,&amp;quot; never &amp;quot;lost data.&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Self-reconciling.&lt;/strong&gt; Because each run takes the full current set, deleted/edited mentions&#xA;correct themselves — no stale entries to prune. (&lt;code&gt;since&lt;/code&gt;/&lt;code&gt;since_id&lt;/code&gt; enable delta fetches later&#xA;if volume ever warrants; full-regenerate is the simple default.)&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Graceful when empty.&lt;/strong&gt; Until &lt;code&gt;fetch&lt;/code&gt; runs, a missing &lt;code&gt;_mentions/&amp;lt;post&amp;gt;.json&lt;/code&gt; renders nothing&#xA;and never fails the build — mentions are always additive chrome.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;The JS path removes the build dependency entirely.&lt;/strong&gt; A JS-rendering theme ships only a&#xA;placeholder; the browser fetches the separately-published &lt;code&gt;_mentions/&lt;/code&gt; from R2 (see&#xA;&lt;a href=&#34;#separate-publish-pipeline&#34;&gt;Separate publish pipeline&lt;/a&gt;), refreshed out-of-band.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;So committing the JSON to the repo is &lt;em&gt;optional&lt;/em&gt; (it only helps a baked build show mentions with no&#xA;network, at the cost of churny commits); the default is &amp;quot;regenerated by &lt;code&gt;fetch&lt;/code&gt; in CI / on a&#xA;schedule,&amp;quot; not &amp;quot;committed.&amp;quot;&lt;/p&gt;&#xA;&lt;h3 id=&#34;deletions--pruning-full-replace-not-merge&#34;&gt;Deletions &amp;amp; pruning (full-replace, not merge)&lt;/h3&gt;&#xA;&lt;p&gt;&amp;quot;Self-reconciling&amp;quot; only holds if regenerate is a &lt;strong&gt;full replace of the &lt;code&gt;_mentions/&lt;/code&gt; namespace&lt;/strong&gt;,&#xA;not a per-post upsert — otherwise a mention webmention.io has deleted lingers in a stale file.&#xA;Concretely:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;A deleted mention isn&#39;t in the next &lt;code&gt;fetch&lt;/code&gt; result, so the rewritten per-post file omits it.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Zero-mentions edge:&lt;/strong&gt; when a post loses &lt;em&gt;all&lt;/em&gt; its mentions there&#39;s nothing to write for it.&#xA;&lt;code&gt;fetch&lt;/code&gt; must therefore &lt;strong&gt;clear the namespace and rewrite wholesale&lt;/strong&gt; so that post ends up with&#xA;&lt;strong&gt;no file&lt;/strong&gt; (not a leftover from a prior run), and &lt;strong&gt;&lt;code&gt;publish&lt;/code&gt; must prune&lt;/strong&gt; objects no longer&#xA;present from the store — which the existing incremental publisher already does (the &lt;code&gt;Pruner&lt;/code&gt;:&#xA;&amp;quot;only changed files upload, orphans are pruned&amp;quot;). The stale object is deleted from R2.&lt;/li&gt;&#xA;&lt;li&gt;The JS path then &lt;strong&gt;404s → renders nothing&lt;/strong&gt;; a baked rebuild finds no file → renders nothing.&lt;/li&gt;&#xA;&lt;li&gt;This is &lt;strong&gt;eventually consistent&lt;/strong&gt;: a deletion persists until the next &lt;code&gt;fetch&lt;/code&gt;+&lt;code&gt;publish&lt;/code&gt; (JS) or&#xA;rebuild (bake). The scheduled &lt;code&gt;webmention publish&lt;/code&gt; keeps that window short without a site build.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Implication for &lt;code&gt;publish&lt;/code&gt;: it must apply orphan pruning &lt;strong&gt;scoped to the &lt;code&gt;_mentions/&lt;/code&gt; prefix&lt;/strong&gt; so a&#xA;mentions refresh never deletes content objects (and vice versa).&lt;/p&gt;&#xA;&lt;h2 id=&#34;display-a-per-site-mode-live--asset--disabled&#34;&gt;Display: a per-site &lt;code&gt;mode&lt;/code&gt; (&lt;code&gt;live&lt;/code&gt; / &lt;code&gt;asset&lt;/code&gt; / &lt;code&gt;disabled&lt;/code&gt;)&lt;/h2&gt;&#xA;&lt;p&gt;How responses reach the page is a &lt;strong&gt;per-site setting&lt;/strong&gt; (&lt;code&gt;federation.indieweb.webmention.display.mode&lt;/code&gt;).&#xA;There are exactly &lt;strong&gt;three&lt;/strong&gt; values — there is &lt;em&gt;no&lt;/em&gt; separate &amp;quot;baked&amp;quot; mode (baking is a theme choice&#xA;&lt;em&gt;within&lt;/em&gt; &lt;code&gt;asset&lt;/code&gt;, see below). The mode decides &lt;strong&gt;where the browser fetches from&lt;/strong&gt; and &lt;strong&gt;whether the&#xA;engine ships anything at all&lt;/strong&gt;; both active modes use the same &lt;code&gt;mentions.js&lt;/code&gt; + the same&#xA;&lt;a href=&#34;#moderation-a-distilled-committed-blocklist&#34;&gt;moderation pipeline&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Mode&lt;/th&gt;&#xA;&lt;th&gt;What ships per page&lt;/th&gt;&#xA;&lt;th&gt;Browser fetches from&lt;/th&gt;&#xA;&lt;th&gt;Build-time data?&lt;/th&gt;&#xA;&lt;th&gt;Freshness&lt;/th&gt;&#xA;&lt;th&gt;Privacy&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;&lt;code&gt;live&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;placeholder + JS&lt;/td&gt;&#xA;&lt;td&gt;the &lt;strong&gt;receiver directly&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;next page load&lt;/td&gt;&#xA;&lt;td&gt;visitor hits the receiver&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;&lt;code&gt;asset&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;placeholder + JS (+ optional bake)&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;our &lt;code&gt;_mentions/&lt;/code&gt; on R2&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt; (synced list)&lt;/td&gt;&#xA;&lt;td&gt;the refresh cron&lt;/td&gt;&#xA;&lt;td&gt;self-hosted&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;strong&gt;&lt;code&gt;disabled&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;strong&gt;nothing&lt;/strong&gt; (zero counts)&lt;/td&gt;&#xA;&lt;td&gt;—&lt;/td&gt;&#xA;&lt;td&gt;—&lt;/td&gt;&#xA;&lt;td&gt;—&lt;/td&gt;&#xA;&lt;td&gt;—&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;&lt;strong&gt;Per-page toggle.&lt;/strong&gt; When the mode is active (&lt;code&gt;live&lt;/code&gt;/&lt;code&gt;asset&lt;/code&gt;), webmentions are &lt;strong&gt;on by default for&#xA;every post&lt;/strong&gt;, opt-out per page via frontmatter (&lt;code&gt;webmentions: false&lt;/code&gt;). In &lt;code&gt;disabled&lt;/code&gt; the page setting&#xA;is ignored — nothing is embedded or shipped anywhere, &lt;code&gt;has_mentions&lt;/code&gt; is false and &lt;code&gt;mentions&lt;/code&gt; is empty.&lt;/p&gt;&#xA;&lt;h3 id=&#34;live--js-straight-to-the-receiver-no-fetchpublishbuild&#34;&gt;&lt;code&gt;live&lt;/code&gt; — JS straight to the receiver (no fetch/publish/build)&lt;/h3&gt;&#xA;&lt;p&gt;The engine ships, on each enabled page, an &lt;strong&gt;engine-provided placeholder&lt;/strong&gt; + the shared &lt;code&gt;mentions.js&lt;/code&gt;;&#xA;the browser calls the receiver&#39;s read API directly (webmention.io exposes a public, CORS-enabled,&#xA;token-free read endpoint). &lt;strong&gt;A new mention shows on the next page load&lt;/strong&gt; — no &lt;code&gt;fetch&lt;/code&gt;, no &lt;code&gt;publish&lt;/code&gt;,&#xA;no rebuild, no asset to host. Lowest-infra, most realtime.&lt;/p&gt;&#xA;&lt;p&gt;Crucially, in &lt;code&gt;live&lt;/code&gt; the engine has &lt;strong&gt;no build-time data&lt;/strong&gt; — so &lt;code&gt;has_mentions&lt;/code&gt;/count are &lt;strong&gt;not known&#xA;at build&lt;/strong&gt;. The placeholder ships whenever the page is enabled (not gated on a count), and JS fills&#xA;in the count (and hides the section if it resolves to zero). So themes &lt;strong&gt;cannot bake&lt;/strong&gt; in &lt;code&gt;live&lt;/code&gt; mode.&lt;/p&gt;&#xA;&lt;p&gt;Because a reader speaks a specific read API, the &lt;strong&gt;JS is parameterised by the reader driver&lt;/strong&gt;: the&#xA;driver declares a small &lt;strong&gt;client descriptor&lt;/strong&gt; (endpoint URL template, query params, and the mapping&#xA;from its response → our normalised shape). One shared &lt;code&gt;mentions.js&lt;/code&gt; consumes the descriptor and&#xA;handles any JF2-shaped endpoint (webmention.io and compatibles — all one &lt;code&gt;jf2&lt;/code&gt; driver); a service&#xA;with an exotic API would be a different driver shipping its own client module. So &lt;em&gt;&amp;quot;the fetch JS is&#xA;provided by the specific driver&amp;quot;&lt;/em&gt; — yes, via the descriptor, without forking the renderer per service.&lt;/p&gt;&#xA;&lt;p&gt;Moderation still applies: the &lt;strong&gt;distilled glob blocklist is shipped to the client&lt;/strong&gt; (it&#39;s&#xA;spam-hiding, not a secret) and filtered in-browser. Semantic rules can&#39;t run client-side (no&#xA;embeddings in the browser), so a site that needs semantic moderation should use &lt;code&gt;asset&lt;/code&gt;. Trade-offs&#xA;to accept: every visitor&#39;s browser hits a third party (their uptime/rate-limits become yours; a&#xA;privacy leak), and no-JS/RSS readers see nothing.&lt;/p&gt;&#xA;&lt;h3 id=&#34;asset--js-against-our-published--mentions-with-optional-build-time-bake&#34;&gt;&lt;code&gt;asset&lt;/code&gt; — JS against our published &lt;code&gt;_mentions/&lt;/code&gt;, with optional build-time bake&lt;/h3&gt;&#xA;&lt;p&gt;The default is the same placeholder + &lt;code&gt;mentions.js&lt;/code&gt;, but pointed at &lt;em&gt;our&lt;/em&gt; server-curated&#xA;&lt;code&gt;_mentions/&amp;lt;key&amp;gt;.json&lt;/code&gt; on R2. Freshness comes from a scheduled &lt;strong&gt;&lt;code&gt;webmention publish&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;fetch&lt;/code&gt; +&#xA;push of &lt;strong&gt;only&lt;/strong&gt; the &lt;code&gt;_mentions/&lt;/code&gt; prefix, &lt;strong&gt;no site build&lt;/strong&gt; (see&#xA;&lt;a href=&#34;#separate-publish-pipeline&#34;&gt;Separate publish pipeline&lt;/a&gt;) — near-realtime to whatever cron cadence you&#xA;pick, with full server-side moderation (glob + semantic), self-hosting, and driver-neutrality.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Because the synced mentions list already exists at build time in this mode, the engine also exposes&#xA;it to the template&lt;/strong&gt; (&lt;code&gt;mentions&lt;/code&gt;/&lt;code&gt;mentions_html&lt;/code&gt;/&lt;code&gt;has_mentions&lt;/code&gt;) so a theme that &lt;em&gt;wants&lt;/em&gt; to&#xA;&lt;strong&gt;hard-code / bake&lt;/strong&gt; them into the HTML can — instead of, or alongside, the JS placeholder. This is&#xA;plausible precisely because we already have the built/synced list; it&#39;s a &lt;strong&gt;theme capability, not a&#xA;mode&lt;/strong&gt;. Baked output is then as-of-last-build (the JS placeholder is what stays cron-fresh), and it&#39;s&#xA;the no-JS escape hatch for text-first themes like &lt;code&gt;minimal&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h3 id=&#34;template-surface&#34;&gt;Template surface&lt;/h3&gt;&#xA;&lt;p&gt;Rendering remains a &lt;strong&gt;template responsibility&lt;/strong&gt; — the engine only &lt;strong&gt;exposes the data&lt;/strong&gt;, exactly as&#xA;&lt;code&gt;attachments&lt;/code&gt;/&lt;code&gt;attachments_html&lt;/code&gt; do:&lt;/p&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Var&lt;/th&gt;&#xA;&lt;th&gt;&lt;code&gt;live&lt;/code&gt;&lt;/th&gt;&#xA;&lt;th&gt;&lt;code&gt;asset&lt;/code&gt;&lt;/th&gt;&#xA;&lt;th&gt;&lt;code&gt;disabled&lt;/code&gt;&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;has_mentions&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;unknown at build → &lt;code&gt;false&lt;/code&gt; (JS fills count)&lt;/td&gt;&#xA;&lt;td&gt;accurate (from synced list)&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;mentions&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;empty (no build-time data)&lt;/td&gt;&#xA;&lt;td&gt;structured &lt;code&gt;[{type, author{name,url,photo}, url, content, published}]&lt;/code&gt; — bake your own&lt;/td&gt;&#xA;&lt;td&gt;empty&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;mentions_html&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;empty&lt;/td&gt;&#xA;&lt;td&gt;engine-rendered drop-in block (empty when none)&lt;/td&gt;&#xA;&lt;td&gt;empty&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;mentions_src&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;driver client-descriptor endpoint&lt;/td&gt;&#xA;&lt;td&gt;our &lt;code&gt;_mentions/&amp;lt;key&amp;gt;.json&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;unset&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;&lt;code&gt;mentions_enabled&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;true&lt;/code&gt; unless page opted out&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;true&lt;/code&gt; unless page opted out&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Bundled themes drop the placeholder + &lt;code&gt;mentions.js&lt;/code&gt; for the JS modes; &lt;code&gt;minimal&lt;/code&gt; (no-JS) uses&#xA;&lt;code&gt;asset&lt;/code&gt; + the baked &lt;code&gt;mentions_html&lt;/code&gt;. The mode is overridable per site.&lt;/p&gt;&#xA;&lt;h2 id=&#34;audio--tts-mentions-are-never-spoken&#34;&gt;Audio / TTS: mentions are never spoken&lt;/h2&gt;&#xA;&lt;p&gt;A real hazard for the bake path — handled by an invariant. The TTS reading is generated from the&#xA;&lt;strong&gt;post&#39;s markdown body&lt;/strong&gt; (&lt;code&gt;registerTTS(slug, html, …)&lt;/code&gt;, where &lt;code&gt;html&lt;/code&gt; is the converted content)&#xA;&lt;em&gt;before&lt;/em&gt; the theme runs. Mentions are &lt;strong&gt;theme chrome&lt;/strong&gt; rendered as a sibling of &lt;code&gt;{{ content }}&lt;/code&gt;&#xA;— like the author card, downloads and tags, none of which are spoken. So:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Invariant:&lt;/strong&gt; mentions render &lt;em&gt;outside&lt;/em&gt; the content / &lt;code&gt;e-content&lt;/code&gt; body the TTS extractor reads.&#xA;Themes must keep the mentions block a sibling of the content element, never inside it.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;This means baked mentions are excluded from audio for free (the TTS source predates theming); the&#xA;guard just stops a theme from accidentally nesting them into the content element.&lt;/p&gt;&#xA;&lt;h2 id=&#34;sending&#34;&gt;Sending&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;colophon webmention send&lt;/code&gt; works off the &lt;strong&gt;built output&lt;/strong&gt; (or the deployed URLs):&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Scan each published post&#39;s HTML for outbound links (&lt;code&gt;http(s)://&lt;/code&gt; to other origins).&lt;/li&gt;&#xA;&lt;li&gt;For each target, discover its endpoint — an HTTP &lt;code&gt;Link: rel=&amp;quot;webmention&amp;quot;&lt;/code&gt; header, else a&#xA;&lt;code&gt;&amp;lt;link rel=&amp;quot;webmention&amp;quot;&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;a rel=&amp;quot;webmention&amp;quot;&amp;gt;&lt;/code&gt; in the body.&lt;/li&gt;&#xA;&lt;li&gt;POST &lt;code&gt;source=&amp;lt;post URL&amp;gt;&amp;amp;target=&amp;lt;their URL&amp;gt;&lt;/code&gt; to the endpoint.&lt;/li&gt;&#xA;&lt;li&gt;Maintain a per-post &lt;strong&gt;sent-cache&lt;/strong&gt; of the link set last sent. On re-run, send only &lt;strong&gt;new&lt;/strong&gt;&#xA;targets — and &lt;strong&gt;re-send to dropped targets&lt;/strong&gt; so that if you edit a post to remove a link, the&#xA;receiver re-checks, sees the link gone, and removes its mention (the spec&#39;s update/delete path).&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The sent-cache is what makes &amp;quot;we changed the context after sending&amp;quot; correct rather than silent.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-mutability-question-and-how-it-plays-with-existing-choices&#34;&gt;The mutability question (and how it plays with existing choices)&lt;/h2&gt;&#xA;&lt;p&gt;Three cases, by what changes:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Post &lt;em&gt;content&lt;/em&gt; changes&lt;/strong&gt; → &lt;strong&gt;fine.&lt;/strong&gt; Received mentions target the &lt;em&gt;URL&lt;/em&gt;, not the prose;&#xA;edits are normal. Senders replied to the address.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Post &lt;em&gt;URL/slug&lt;/em&gt; changes&lt;/strong&gt; → &lt;strong&gt;the real gotcha.&lt;/strong&gt; webmention.io holds mentions under the old&#xA;URL; the new page finds none; inbound links rot. Mitigations: (1) keep slugs stable&#xA;(link-rot discipline); (2) once the backlog &lt;strong&gt;&lt;code&gt;aliases:&lt;/code&gt; / redirects&lt;/strong&gt; feature lands, &lt;code&gt;fetch&lt;/code&gt;&#xA;queries the old keys too and the old URL 302s to the new; (3) &lt;code&gt;doctor&lt;/code&gt; warns when a post that&#xA;has cached mentions changes slug. &lt;strong&gt;This makes &lt;code&gt;aliases:&lt;/code&gt; a soft prerequisite for robust&#xA;webmention&lt;/strong&gt; and is the main cross-feature dependency.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A sender deletes/edits a mention, or we drop a sent link&lt;/strong&gt; → reconciled by re-running:&#xA;&lt;code&gt;fetch&lt;/code&gt; always returns the current set (deletions vanish); &lt;code&gt;send&lt;/code&gt;&#39;s sent-cache re-pings dropped&#xA;targets. JS display is always-current; baked display reconciles on the next fetch+build.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;Spam/moderation:&lt;/strong&gt; inbound mentions can be junk — handled by a declarative, committed blocklist&#xA;plus an optional moderation skill; see &lt;a href=&#34;#moderation-a-distilled-committed-blocklist&#34;&gt;Moderation&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;moderation-a-distilled-committed-blocklist&#34;&gt;Moderation: a distilled, committed blocklist&lt;/h2&gt;&#xA;&lt;p&gt;Inbound mentions are third-party content, so the author needs a way to drop spam/abuse — and it has&#xA;to &lt;strong&gt;survive &lt;code&gt;fetch&lt;/code&gt;&#39;s full regenerate&lt;/strong&gt; (editing the generated &lt;code&gt;_mentions/&lt;/code&gt; JSON is pointless; the&#xA;next fetch overwrites it). So moderation is &lt;strong&gt;declarative and committed&lt;/strong&gt;, applied as a filter step&#xA;over the normalised list, and reused by every display mode.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The blocklist is rules over normalised mention attributes, glob-matched.&lt;/strong&gt; Matchable fields:&#xA;&lt;code&gt;domain&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;author.name&lt;/code&gt;, &lt;code&gt;author.url&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;. A bare string is shorthand for&#xA;&lt;code&gt;domain&lt;/code&gt;/&lt;code&gt;author.url&lt;/code&gt;; the structured form targets a field:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# .colophon/webmention-block.yml  (committed)&#xA;- &amp;#34;*.spam.example&amp;#34;            # shorthand: domain/author.url glob&#xA;- author.url: &amp;#34;https://troll.example/*&amp;#34;&#xA;- content: &amp;#34;*free crypto*&amp;#34;&#xA;- domain: &amp;#34;*.cn.example&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;One filter pipeline, two execution sites.&lt;/strong&gt; Applied &lt;strong&gt;server-side in &lt;code&gt;fetch&lt;/code&gt;&lt;/strong&gt; (full power) for&#xA;&lt;code&gt;asset&lt;/code&gt;/&lt;code&gt;baked&lt;/code&gt;; for &lt;code&gt;live&lt;/code&gt; the &lt;strong&gt;distilled blocklist is shipped to the client&lt;/strong&gt; and the same glob&#xA;rules run in &lt;code&gt;mentions.js&lt;/code&gt; (it&#39;s spam-hiding, not a secret). Glob rules run in both places;&#xA;semantic rules (below) are server-only.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Semantic moderation (future).&lt;/strong&gt; When the semantic subsystem lands, a rule kind scores a&#xA;mention&#39;s &lt;code&gt;content&lt;/code&gt; against a concept (&amp;quot;spam&amp;quot;/&amp;quot;abuse&amp;quot;) via embeddings + a threshold, slotting into&#xA;the same server-side pipeline. Not available client-side, so semantic-moderated sites use&#xA;&lt;code&gt;asset&lt;/code&gt;/&lt;code&gt;baked&lt;/code&gt;. (Ties into [decision &lt;code&gt;search&lt;/code&gt;] — the shared embedding subsystem.)&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A &lt;code&gt;moderate-mentions&lt;/code&gt; skill&lt;/strong&gt; (an agent skill alongside the other authoring skills) scans the&#xA;current mention set, flags likely spam/abuse, and either auto-filters or &lt;strong&gt;presents a decision&#xA;list&lt;/strong&gt; for the author to confirm. Crucially it &lt;strong&gt;distills&lt;/strong&gt; confirmed cases into &lt;em&gt;generalised&lt;/em&gt;&#xA;glob (and later semantic) rules appended to the blocklist — so the list stays &lt;strong&gt;small and&#xA;effective&lt;/strong&gt; rather than an ever-growing pile of individual URLs.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Receiver-side delete&lt;/strong&gt; (e.g. webmention.io&#39;s dashboard) remains the quick one-off: delete there,&#xA;the next &lt;code&gt;fetch&lt;/code&gt; won&#39;t return it. The committed blocklist is the version-controlled, reproducible&#xA;path; per-mention approval queues are deferred.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;separate-publish-pipeline&#34;&gt;Separate publish pipeline&lt;/h2&gt;&#xA;&lt;p&gt;The &lt;code&gt;_mentions/&lt;/code&gt; assets are published &lt;strong&gt;independently of the content build/deploy&lt;/strong&gt;, so you can&#xA;update one without the other. colophon already partitions output by path at publish time (the&#xA;router sends &lt;code&gt;_search/**&lt;/code&gt; to the R2 publisher while content goes to Pages); the same machinery&#xA;publishes &lt;em&gt;only&lt;/em&gt; the &lt;code&gt;_mentions/**&lt;/code&gt; prefix.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;colophon webmention publish --env production    # fetch + write _mentions/ + push ONLY that prefix to R2&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;So the two cadences are decoupled:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Content pipeline&lt;/strong&gt; (&lt;code&gt;build&lt;/code&gt; → &lt;code&gt;publish&lt;/code&gt;): ships HTML + JS placeholders. Never depends on the&#xA;mentions cache; a fresh runner is fine.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Webmention pipeline&lt;/strong&gt; (&lt;code&gt;webmention publish&lt;/code&gt;, e.g. hourly cron): refreshes &lt;code&gt;_mentions/&lt;/code&gt; on R2.&#xA;The JS path picks it up in the browser with no site rebuild.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;This is the strongest reason JS is the default render path — it&#39;s the only one that benefits from&#xA;the decoupling (a baked theme still needs a content rebuild to reflect new mentions, so baking&#xA;suits low-frequency / no-JS sites). Implementation reuses the per-publisher routing&#xA;(&lt;code&gt;router.Owns&lt;/code&gt;/&lt;code&gt;Keep&lt;/code&gt;) plus a publish that only materialises the &lt;code&gt;_mentions/&lt;/code&gt; tree.&lt;/p&gt;&#xA;&lt;h2 id=&#34;search-mentions-as-down-ranked-results-optional-asset-mode&#34;&gt;Search: mentions as down-ranked results (optional, &lt;code&gt;asset&lt;/code&gt; mode)&lt;/h2&gt;&#xA;&lt;p&gt;Replies carry real text (&amp;quot;someone said X about post Y&amp;quot;), so indexing them into the site&#39;s lexical&#xA;search is sensible — and plausible, because we already hold the normalised list. Design:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Only &lt;code&gt;asset&lt;/code&gt; mode.&lt;/strong&gt; A static index needs the data at build time; &lt;code&gt;live&lt;/code&gt; has none. So&#xA;search-indexed mentions are an &lt;code&gt;asset&lt;/code&gt;-mode / &lt;code&gt;fetch&lt;/code&gt;-driven feature (off in &lt;code&gt;live&lt;/code&gt;/&lt;code&gt;disabled&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Only text-bearing kinds.&lt;/strong&gt; Index &lt;code&gt;reply&lt;/code&gt;/&lt;code&gt;mention&lt;/code&gt; (they have &lt;code&gt;content&lt;/code&gt;); skip &lt;code&gt;like&lt;/code&gt;/&lt;code&gt;repost&lt;/code&gt;&#xA;(nothing to match). Each indexed doc carries &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, the &lt;strong&gt;target post&lt;/strong&gt; it lives&#xA;under, and &lt;code&gt;kind: mention&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A separate, down-ranked shard.&lt;/strong&gt; Rather than mixing mention docs into the content index (which&#xA;would churn it on every mentions refresh), emit a &lt;strong&gt;second search shard&lt;/strong&gt; for mentions, merged&#xA;client-side. This keeps it on the &lt;strong&gt;webmention publish cadence&lt;/strong&gt; (consistent with &lt;code&gt;_mentions/&lt;/code&gt;&#xA;decoupling) and lets the UI treat it differently: a &lt;strong&gt;rank penalty&lt;/strong&gt; plus grouping so mention&#xA;hits sort &lt;strong&gt;below&lt;/strong&gt; content hits — or render in a distinct &amp;quot;Mentions&amp;quot; group &lt;strong&gt;appended to the&#xA;bottom&lt;/strong&gt; of the results list. Configurable; default down-ranked-and-grouped.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A hit links on-site:&lt;/strong&gt; to the post&#39;s responses anchor (keeps the reader on the site), citing the&#xA;mention&#39;s source URL. Freshness is as-of-last index refresh.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Blocklist-gated.&lt;/strong&gt; Mentions are filtered through the same &lt;a href=&#34;#moderation-a-distilled-committed-blocklist&#34;&gt;moderation pipeline&lt;/a&gt;&#xA;&lt;em&gt;before&lt;/em&gt; indexing — blocked mentions are neither displayed nor searchable.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;This reuses the internal pure-Go BM25 index (decision &lt;code&gt;search&lt;/code&gt;): add a &lt;code&gt;kind&lt;/code&gt; field + a rank weight,&#xA;emit the extra shard, and teach the search UI to group/penalise &lt;code&gt;kind: mention&lt;/code&gt;. Marked a &lt;strong&gt;later&#xA;enhancement&lt;/strong&gt; (after Tier 2 display), gated by an explicit &lt;code&gt;search_index: true&lt;/code&gt; under &lt;code&gt;webmention&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;fit-with-existing-design&#34;&gt;Fit with existing design&lt;/h2&gt;&#xA;&lt;div class=&#34;table-scroll&#34; tabindex=&#34;0&#34;&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Concern&lt;/th&gt;&#xA;&lt;th&gt;Reused pattern&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Separate JSON asset, R2-routed, cross-origin&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;_search/&lt;/code&gt; index + &lt;code&gt;publish --create&lt;/code&gt; CORS&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Browser fetch + render, no-JS fallback&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;search-ui.js&lt;/code&gt; / &lt;code&gt;player.js&lt;/code&gt; progressive enhancement&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Template data exposure (&lt;code&gt;mentions&lt;/code&gt;/&lt;code&gt;mentions_html&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;attachments&lt;/code&gt;/&lt;code&gt;attachments_html&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Per-path publish partitioning&lt;/td&gt;&#xA;&lt;td&gt;the router&#39;s &lt;code&gt;_search/**&lt;/code&gt; → R2 split (&lt;code&gt;router.Owns&lt;/code&gt;/&lt;code&gt;Keep&lt;/code&gt;)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Read token from environment, not config&lt;/td&gt;&#xA;&lt;td&gt;all secrets via &lt;code&gt;{env:…}&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Config wiring&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;federation.indieweb.webmention.receiver&lt;/code&gt; (already present, unread)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Parsing mention authors&lt;/td&gt;&#xA;&lt;td&gt;the mf2 &lt;code&gt;h-card&lt;/code&gt;/&lt;code&gt;h-entry&lt;/code&gt; just shipped&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;h2 id=&#34;config&#34;&gt;Config&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;sites:&#xA;  - id: main&#xA;    federation:&#xA;      indieweb:&#xA;        webmention:&#xA;          receiver: https://webmention.io/blog.example.com/webmention  # the rel=webmention endpoint&#xA;          # token read from env, e.g. WEBMENTION_IO_TOKEN, never written here&#xA;          driver: jf2                  # reader driver (read API + client descriptor); default jf2&#xA;          display:&#xA;            mode: asset                # live | asset | disabled   (see Display)&#xA;          # blocklist lives in .colophon/webmention-block.yml (committed), not inline&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;mode: live&lt;/code&gt; needs nothing else; &lt;code&gt;asset&lt;/code&gt; uses the &lt;code&gt;webmention fetch&lt;/code&gt;/&lt;code&gt;publish&lt;/code&gt; pipeline (and can be&#xA;baked at build by the theme); &lt;code&gt;disabled&lt;/code&gt; ships nothing. Per page: &lt;code&gt;webmentions: false&lt;/code&gt; opts a post&#xA;out when the mode is active.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;&amp;lt;link rel=&amp;quot;webmention&amp;quot; href=&amp;quot;{{ receiver }}&amp;quot;&amp;gt;&lt;/code&gt; is emitted in every page &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; when a receiver&#xA;is configured (the discovery tag senders look for).&lt;/p&gt;&#xA;&lt;h2 id=&#34;key-decisions&#34;&gt;Key decisions&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Separate &lt;code&gt;webmention&lt;/code&gt; command&lt;/strong&gt; (&lt;code&gt;send&lt;/code&gt; after publish, &lt;code&gt;fetch&lt;/code&gt; before build, &lt;code&gt;publish&lt;/code&gt; on its&#xA;own cadence) — not folded into &lt;code&gt;build&lt;/code&gt;/&lt;code&gt;publish&lt;/code&gt;, because of the live-URL ordering constraint&#xA;and the decoupled-refresh goal.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Received data is a separate &lt;code&gt;_mentions/&lt;/code&gt; asset&lt;/strong&gt; (R2-routed, CORS via &lt;code&gt;--create&lt;/code&gt;), never mixed&#xA;into content — mirrors &lt;code&gt;_search/&lt;/code&gt;. The cache is &lt;strong&gt;not reproducible&lt;/strong&gt; (external state); builds&#xA;are &lt;strong&gt;graceful when it&#39;s empty&lt;/strong&gt;, and freshness comes from &lt;code&gt;fetch&lt;/code&gt;/&lt;code&gt;publish&lt;/code&gt;, not the repo.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;One JSON per post&lt;/strong&gt;, normalised (&lt;code&gt;type&lt;/code&gt;/&lt;code&gt;author&lt;/code&gt;/&lt;code&gt;url&lt;/code&gt;/&lt;code&gt;content&lt;/code&gt;), not raw receiver payload.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Display is a per-site &lt;code&gt;mode&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;live&lt;/code&gt; | &lt;code&gt;asset&lt;/code&gt; | &lt;code&gt;disabled&lt;/code&gt; (there is &lt;strong&gt;no&lt;/strong&gt; separate baked&#xA;mode). &lt;code&gt;live&lt;/code&gt; = browser→receiver direct (most realtime, client-side glob blocklist, &lt;strong&gt;no&#xA;build-time data&lt;/strong&gt; so no count/bake, no no-JS); &lt;code&gt;asset&lt;/code&gt; = browser→our R2 asset refreshed by a cron&#xA;&lt;code&gt;publish&lt;/code&gt; (full moderation, self-hosted) &lt;strong&gt;and&lt;/strong&gt; the synced list is exposed at build so a theme&#xA;&lt;em&gt;may&lt;/em&gt; bake it (baking is a theme capability within &lt;code&gt;asset&lt;/code&gt;, not a mode); &lt;code&gt;disabled&lt;/code&gt; = nothing&#xA;ships, zero counts, page toggle ignored. Per-page opt-out via &lt;code&gt;webmentions: false&lt;/code&gt;. The engine&#xA;exposes &lt;code&gt;mentions&lt;/code&gt;/&lt;code&gt;mentions_html&lt;/code&gt;/&lt;code&gt;has_mentions&lt;/code&gt;/&lt;code&gt;mentions_src&lt;/code&gt;/&lt;code&gt;mentions_enabled&lt;/code&gt;; the theme&#xA;renders. &lt;code&gt;live&lt;/code&gt; mode&#39;s fetch JS is parameterised by the &lt;strong&gt;reader driver&#39;s client descriptor&lt;/strong&gt;.&#xA;4b. &lt;strong&gt;Moderation is a declarative, committed blocklist&lt;/strong&gt; of glob rules over mention attributes&#xA;(&lt;code&gt;.colophon/webmention-block.yml&lt;/code&gt;), re-applied every &lt;code&gt;fetch&lt;/code&gt; (and shipped to the client in &lt;code&gt;live&lt;/code&gt;&#xA;mode). Future &lt;strong&gt;semantic&lt;/strong&gt; rules run server-side; a &lt;strong&gt;&lt;code&gt;moderate-mentions&lt;/code&gt; skill&lt;/strong&gt; distills&#xA;confirmed spam into small, general rules. Survives full-regenerate because it&#39;s declarative.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;TTS invariant&lt;/strong&gt;: mentions live outside the content body the speech extractor reads, so they&#39;re&#xA;never spoken — true for both paths.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Separate publish pipeline&lt;/strong&gt;: &lt;code&gt;webmention publish&lt;/code&gt; pushes only &lt;code&gt;_mentions/&lt;/code&gt; to the store, so&#xA;mentions and content update independently (reuses per-publisher path routing).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Sent-cache&lt;/strong&gt; drives correct re-send on link changes; &lt;strong&gt;&lt;code&gt;aliases:&lt;/code&gt;&lt;/strong&gt; is the soft dependency&#xA;for surviving URL changes.&lt;/li&gt;&#xA;&lt;li&gt;webmention.io as the receiver (no self-hosted endpoint) — per PLAN §10/§14.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;acceptance-criteria&#34;&gt;Acceptance criteria&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;colophon webmention send&lt;/code&gt; discovers endpoints and POSTs for outbound links; re-run is a no-op&#xA;unless links changed; dropped links trigger a delete-style re-send.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;colophon webmention fetch&lt;/code&gt; writes normalised JSON into the local cache; &lt;code&gt;webmention publish&lt;/code&gt;&#xA;pushes only &lt;code&gt;_mentions/**&lt;/code&gt; to the store, independent of a content deploy.&lt;/li&gt;&#xA;&lt;li&gt;A build with an &lt;strong&gt;empty&lt;/strong&gt; cache succeeds and shows no mentions (graceful); a build with the cache&#xA;present exposes &lt;code&gt;mentions&lt;/code&gt;/&lt;code&gt;mentions_html&lt;/code&gt; to templates and emits &lt;code&gt;_mentions/&lt;/code&gt; assets.&lt;/li&gt;&#xA;&lt;li&gt;A mention deleted on webmention.io disappears after the next &lt;code&gt;fetch&lt;/code&gt;+&lt;code&gt;publish&lt;/code&gt;; a post that drops&#xA;to &lt;strong&gt;zero&lt;/strong&gt; mentions has its &lt;code&gt;_mentions/&amp;lt;post&amp;gt;.json&lt;/code&gt; removed and the orphan pruned from the store&#xA;(JS path 404s → renders nothing). Pruning is scoped to the &lt;code&gt;_mentions/&lt;/code&gt; prefix.&lt;/li&gt;&#xA;&lt;li&gt;A JS-enhanced theme shows likes/reposts/replies; with JS off the post is unaffected; &lt;code&gt;minimal&lt;/code&gt;&#xA;bakes them statically. Mentions never appear in a post&#39;s TTS audio.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;display.mode: live&lt;/code&gt; renders mentions with &lt;strong&gt;no&lt;/strong&gt; &lt;code&gt;fetch&lt;/code&gt;/&lt;code&gt;publish&lt;/code&gt;/rebuild (browser → receiver),&#xA;honouring the shipped glob blocklist client-side, with no build-time count; &lt;code&gt;asset&lt;/code&gt; renders from&#xA;the published &lt;code&gt;_mentions/&lt;/code&gt; (and exposes the synced list so a theme may bake it at build);&#xA;&lt;code&gt;disabled&lt;/code&gt; ships nothing (zero counts, &lt;code&gt;webmentions:&lt;/code&gt; page setting ignored). &lt;code&gt;webmentions: false&lt;/code&gt;&#xA;opts a single post out when the mode is active.&lt;/li&gt;&#xA;&lt;li&gt;A blocklisted mention (glob over domain/url/author/content) never appears in any mode; the&#xA;blocklist survives a full &lt;code&gt;fetch&lt;/code&gt; regenerate (it&#39;s committed, not edited into the export).&lt;/li&gt;&#xA;&lt;li&gt;Secrets come only from the environment; a site with no &lt;code&gt;webmention&lt;/code&gt; config emits nothing.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;files-to-create-when-built&#34;&gt;Files to create (when built)&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;internal/webmention/&lt;/code&gt; — endpoint discovery + sender (sent-cache) + reader &lt;strong&gt;driver&lt;/strong&gt; (server&#xA;fetch &lt;strong&gt;+ client descriptor&lt;/strong&gt;) + normaliser + the &lt;strong&gt;blocklist filter pipeline&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;internal/cli/webmention.go&lt;/code&gt; — the &lt;code&gt;webmention {send,fetch,publish}&lt;/code&gt; command group; &lt;code&gt;publish&lt;/code&gt;&#xA;supports the incremental (changed-mention-posts-only) re-render for &lt;code&gt;baked&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;build: expose &lt;code&gt;mentions&lt;/code&gt;/&lt;code&gt;mentions_html&lt;/code&gt;/&lt;code&gt;has_mentions&lt;/code&gt;/&lt;code&gt;mentions_src&lt;/code&gt; to templates, emit&#xA;&lt;code&gt;_mentions/&lt;/code&gt; assets + the &lt;code&gt;&amp;lt;link rel=&amp;quot;webmention&amp;quot;&amp;gt;&lt;/code&gt; head tag; in &lt;code&gt;live&lt;/code&gt; mode emit the driver&#xA;client descriptor + the distilled blocklist for client-side filtering.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;internal/render/themes/*/…&lt;/code&gt; — a &lt;code&gt;data-mentions&lt;/code&gt; placeholder + engine-emitted &lt;code&gt;mentions.js&lt;/code&gt;&#xA;(parameterised by the driver descriptor; applies glob blocklist in &lt;code&gt;live&lt;/code&gt;) and a pongo-baked&#xA;block (text themes), kept outside the content/&lt;code&gt;e-content&lt;/code&gt; element.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;contrib/skills/&lt;/code&gt; (+ wiring) — a &lt;code&gt;moderate-mentions&lt;/code&gt; skill that flags spam/abuse and distills&#xA;confirmed cases into blocklist rules.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;out-of-scope-future&#34;&gt;Out of scope (future)&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Self-hosted webmention receiver (PLAN §14 leaves this open; webmention.io is the v1 choice).&lt;/li&gt;&#xA;&lt;li&gt;Rich moderation UI / per-mention approval.&lt;/li&gt;&#xA;&lt;li&gt;Sending &lt;em&gt;as&lt;/em&gt; specific post types (replies/likes from colophon itself) — colophon publishes&#xA;articles; it links, it doesn&#39;t (yet) author reply-posts.&lt;/li&gt;&#xA;&lt;li&gt;Fediverse/Bridgy Fed backfeed — a later layer that reuses this send/receive + mf2 substrate.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;em&gt;Generated from &lt;a href=&#34;https://github.com/jmylchreest/colophon/blob/main/docs/design/webmention.md&#34;&gt;&lt;code&gt;docs/design/webmention.md&lt;/code&gt;&lt;/a&gt; — edit it there.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
</feed>
