colophon docs
← colophon docs

Configuration reference

The annotated colophon.yaml reference: every option, default and shape in one file.

Every colophon.yaml option in one annotated file. It is not a starter config — colophon init writes a lean one — but the single place where every knob, default and shape is written down. Copy the bits you need.

Any string value may interpolate the environment with {env:VAR} or {env:VAR:-fallback}; deploy secrets are always read from the environment and never stored in the file.

# ─────────────────────────────────────────────────────────────────────────────
# colophon.yaml — ANNOTATED REFERENCE
#
# Not a starter config (`colophon init` writes a lean one). This shows every
# option in one place, with a few list entries set side-by-side so the shapes
# are clear. Copy the bits you need.
#
# Markers used in comments below:
#   [required]   must be set; no default
#   [optional]   may be omitted; the shown value is the default
#   [inherits]   only meaningful inside a profile — when omitted, the value is
#                taken from that modality's default block (see "generation")
#
# Any string may interpolate the environment: {env:VAR} or {env:VAR:-fallback}.
# Deploy secrets are read from the environment at build time and are NEVER stored
# in this file.
# ─────────────────────────────────────────────────────────────────────────────


# ═══ sites ═══════════════════════════════════════════════════════════════════
# One or more sites built from the shared content. Most projects have exactly one.
sites:
  - id: main                                   # [required] stable id, used in URLs/serve paths
    title: "My Blog"                           # [required]
    base_url: "{env:SITE_URL:-http://localhost:8080}"  # [required] canonical origin
    theme: press                               # [optional] default: the bundled "default" theme
    lang: en                                   # [optional] site default BCP-47 language (default "en")
    languages: [en, es]                        # [optional] enable multi-language posts. A file
                                               #   <slug>.es.md is the Spanish translation of <slug>.md,
                                               #   published at /es/<slug>/, linked by hreflang + a
                                               #   theme language selector. The default lang stays at /.
    personas: [default]                        # [optional] writing voices available to the agent
    federation:
      feeds: [rss, atom, json]                 # [optional] default: none; emit at least one
    # search accepts a bare mode string OR a block. Bare form: `search: lexical`.
    search:
      mode: lexical                            # [optional] lexical | semantic | off (default off)
      fuzzy: true                              # [optional] default false; trigram+Levenshtein typo tolerance
    # Derived slide decks. Site defaults; a post overrides either key in its `slides:`
    # frontmatter (shallow/by-key — a key replaces this value, omitted keys inherit).
    slides:
      enabled: false                           # [optional] default off; a post opts in with `slides: true`
      split: [h2]                              # [optional] slide boundaries (a list). default: every heading.
                                               #   targets: h1..h6, hr, splitslide, image, table, code,
                                               #   math, diagram, audio, video, text:<match>
    # Reader analytics for THIS site. One block per provider; inert until configured.
    analytics:
      statsfactory:
        server_url: "{env:STATSFACTORY_SERVER_URL:-}"
        app_key: "{env:STATSFACTORY_APP_KEY:-}"
      # google_analytics:                      # GA4 — sets cookies, brings its own consent duties
      #   measurement_id: "{env:GA_MEASUREMENT_ID:-}"
    # Routing rewrites matching output paths to a publisher's object store instead of
    # shipping them with the page host (which has a file/size budget). Inert until that
    # publisher resolves a public URL, so local builds keep assets co-located.
    routing:
      - match: "**assets/**"                   # co-located post assets + the root /assets tree
        publisher: r2
      - match: "_search/**"                    # keep the search index off the page-host budget
        publisher: r2


# ═══ sources ═════════════════════════════════════════════════════════════════
# Where content comes from. Driver selects the implementation; the rest is
# driver-specific. List several and their content is merged.
sources:
  - id: content                                # [required]
    driver: md-dir                             # [required] a plain Markdown directory
    path: ./content
  - id: vault                                  # a second source, merged with the first
    driver: obsidian
    vault: "{env:OBSIDIAN_VAULT:-}"            # vault root; empty → contributes nothing
    path: "{env:BLOG_PATH:-}"                  # vault-relative folder(s); empty → whole vault
    tag: "{env:BLOG_TAGS:-}"                   # publish by tag(s); empty → use publish_required
    publish_required: false


# ═══ publishers ══════════════════════════════════════════════════════════════
# Pure mechanism: HOW to deploy. WHAT/WHERE is decided by environments below.
# Deploy credentials always come from the environment, never from these fields.
publishers:
  - id: local                                  # offline build target, for diffing output
    driver: local
    path: ./dist
  - id: cf                                     # HTML/static host
    driver: cloudflare-pages
    project: "{env:CF_PAGES_PROJECT:-my-blog}"
    account_id: "{env:CLOUDFLARE_ACCOUNT_ID}"
  - id: r2                                     # S3-compatible object store (routing target above)
    driver: cloudflare-r2
    bucket: "{env:R2_BUCKET:-my-blog-assets}"
    account_id: "{env:CLOUDFLARE_ACCOUNT_ID}"
    public_url: "{env:R2_PUBLIC_URL:-}"        # empty → auto-discovered on publish


# ═══ environments ════════════════════════════════════════════════════════════
# Named build+deploy profiles. No name is privileged. An environment picks
# publishers, toggles drafts, and may override a few site fields + select
# generation profiles (see "generation"). Build/deploy with --env <name>.
environments:
  - name: production                           # [required]
    publish: [cf, r2]                          # [required] publisher ids to deploy to
    allow_publish: false                       # [optional] default true; false → require --allow-publish
    base_url: "{env:BASE_URL:-}"               # [optional] override the site base_url here
    overrides:                                 # [optional] per-publisher Settings overrides, keyed by id
      cf: { branch: main }

  - name: preview
    publish: [cf, r2]
    include_drafts: true                       # [optional] default false
    title: "My Blog (preview)"                 # [optional] override the site title
    image_profile: draft                       # [optional] use the cheap image profile in this env
    speech_profile: minimax                    # [optional] use the minimax voice profile in this env
    slides:                                    # [optional] override the site slides defaults here
      enabled: true                            #   e.g. decks on in preview, off in production
    overrides:
      cf: { branch: preview }

  - name: dist                                 # local copy; ungated, no creds
    publish: [local]
    include_drafts: true

  - name: text                                 # preview an alternate theme before promoting it
    publish: [local]
    include_drafts: true
    theme: minimal                             # [optional] override the site theme
    overrides:
      local: { path: ./dist-text }


# ═══ generation ══════════════════════════════════════════════════════════════
# Optional AI media generation. Two modalities: image (satisfies `gen:` refs) and
# speech (reads posts with `audio: true`). Empty → the feature is off.
#
# Each modality has a DEFAULT block plus named PROFILES (a map). A profile inherits
# every field from the default block and overrides only what it lists. The default
# block is the implicit profile named "default".
#
# Which profile renders a given asset (narrowest scope wins):
#
#   default block
#     ◀ environment   speech_profile: / image_profile:
#       ◀ post         audio_profile: / image_profile:   (frontmatter)
#         ◀ per-ref    <gen:hero?profile=…>              (image refs only)
#           ◀ post scalar overrides (audio_voice, …)
#
# Merge rule: scalars REPLACE, maps DEEP-MERGE (e.g. image `defaults`). Voice/model
# ids are provider-specific, so a profile that changes provider should also set them.
generation:
  enabled: true                                # [optional] default true; master off-switch for all AI gen

  # ── images ──────────────────────────────────────────────────────────────────
  image:
    enabled: true                              # [optional] default true; per-modality switch
    provider: google                           # [required] google|minimax|openai|xai|together|deepinfra|custom
    model: gemini-3.1-flash-image              # [optional] default: the provider profile's default model
    api_key: "{env:GEMINI_API_KEY}"            # [optional] default: the provider's env var (kept out of config)
    base_url: ""                               # [optional] override endpoint (required for `custom`)
    api_path: ""                               # [optional] override request path (OpenAI-compatible hosts)
    output_dir: content/assets/generated       # [optional] default shown; cache + sidecars (committed)
    concurrency: 5                             # [optional] default 5; max parallel generations
    reuse: exact                               # [optional] exact (re-render on provider/model change) | content
    system_prompt: ""                          # [optional] house style; default: the theme's
    defaults:                                  # [optional] tuning params applied to every request
      aspect: "16:9"
    postprocess:
      trim_letterbox: true                     # [optional] default true; strip baked-in letterbox bars
    profiles:                                  # [optional] named alternates; each inherits the block above
      draft:                                   # cheap/fast — e.g. for previews
        provider: together                     # [inherits] all other fields from the default image block
        model: black-forest-labs/FLUX.1-schnell
      poster:                                  # premium — e.g. for hero art
        provider: openai
        model: gpt-image-1
        defaults: { quality: high }            # deep-merged over the default `defaults` (aspect kept)

  # ── speech ──────────────────────────────────────────────────────────────────
  speech:
    enabled: true                              # [optional] default true; also the per-post `audio:` default
    provider: elevenlabs                       # [required] elevenlabs | minimax
    voice: "{env:ELEVENLABS_VOICE:-Ee4WTXzxagFpoj4PUkHV}"  # [optional] default: provider profile's default voice
    model: ""                                  # [optional] default: provider profile's default model
    api_key: "{env:ELEVENLABS_API_KEY}"        # [optional] default: the provider's env var
    base_url: ""                               # [optional] override endpoint
    api_path: ""                               # [optional] override request path
    output_dir: content/assets/generated       # [optional] default shown; clip cache + sidecars
    concurrency: 5                             # [optional] default 5; max parallel clips
    reuse: exact                               # [optional] exact | content (reuse a prior voice's reading)
    pronunciation_dict: en_GB                  # [optional] built-in name (contrib/pronunciation: en_GB, es_ES) or a
                                               #   YAML path. A naked ref applies to the SITE DEFAULT LANGUAGE only;
                                               #   per-language: {en: en_GB, es: es_ES} (BCP-47 keys, es-MX → es).
                                               #   A language with no entry gets no dictionary.
    transcript:                                # [optional] how post content becomes spoken text
      wrap_up: true                            # [optional] default true; append a closing "visit the post" note
      expand_acronyms: true                    # [optional] default true; read SSH as "Secure Shell"
      blocks:                                  # [optional] per block-type handling: cue|drop|keep|spell
        code: cue                              #   (defaults: code/table/diagram/math_display→cue,
        table: cue                             #    math_inline→drop, inline_code→spell, else keep)
    profiles:                                  # [optional] named alternates; each inherits the block above
      minimax:                                 # a different provider needs its own voice
        provider: minimax
        voice: English_Trustworth_Man          # [inherits] pronunciation_dict/transcript/etc from default
      narrator-fast:                           # same provider, faster/cheaper model + a different voice
        model: eleven_turbo_v2_5
        voice: "{env:ELEVENLABS_NARRATOR_VOICE:-}"


# ═══ telemetry ═══════════════════════════════════════════════════════════════
# The colophon APP's own anonymous usage reporting (build/source/publisher TYPES —
# never your content), separate from the site analytics above. Governs only itself.
telemetry:
  enabled: true                                # [optional] default true; false (or COLOPHON_TELEMETRY=off) disables


# ═══ per-post selection (frontmatter — NOT this file) ════════════════════════
# Posts pick a profile in their own frontmatter. The selector keys are identical
# at every scope (config block, environment, frontmatter): speech_profile /
# image_profile.
#
#   ---
#   audio: true
#   speech_profile: minimax          # → generation.speech.profiles.minimax
#   audio_voice: SomeOtherVoiceId    # still wins over the profile's voice
#   image_profile: poster            # → generation.image.profiles.poster (hero + inline images)
#   ---
#
#   ![cover](<gen:hero?profile=poster>)   # per-image override, beats image_profile

Generated from docs/colophon.reference.yaml — edit it there.