Value interpolation, source resolvers, and pipe transforms.
The resolver system provides OmegaConf-style ${...} value interpolation with
pipe-based chaining. It is the primary mechanism for loading external data,
referencing values, and transforming collections.
${resolver:argument}
${resolver:arg1,arg2}
${path.to.key}
${resolver:arg | transform:arg | transform:arg}
"Text with ${interpolation} inside"
The syntax is modeled after OmegaConf custom resolvers.
When the entire YAML value is a ${...} expression, the result can be
any type — string, dict, list, number, etc.
config: ${load:config.yml}
# → config becomes the dict loaded from config.yml
items: ${glob:posts/*.yml | sort:-year}
# → items becomes a sorted list of dicts
When ${...} appears inside a larger string, each ${...} is resolved
and converted to a string via str().
footer: "© 2026 ${title}"
# → "© 2026 John Doe"
meta_description: "${title} — ${header.description}"
# → "John Doe — PhD Candidate at UofT"
Multiple interpolations in one string are supported. The result is always a string.
Resolvers can be chained with |. The output of each resolver becomes the
input of the next:
${source:arg | transform1:arg | transform2:arg}
│ │ │
▼ ▼ ▼
produce value → transform it → transform again
Source resolvers (first in chain): produce a value from nothing. Transform resolvers (subsequent): receive the previous output and transform it.
# Load all .yml files, exclude drafts, sort by git commit date, take 10
items: ${glob:posts/**/*.yml | filter:draft!=true | sort:-git | limit:10}
Pipes are parsed by splitting on | (space-pipe-space) to avoid conflicts
with YAML values containing |.
load — Load a Fileconfig: ${load:config.yml}
bio: ${load:sections/bio.md}
Loads a file relative to the current YAML file's directory.
.yml / .yaml → parsed as YAML dict/list, recursively resolved.md, .html, .txt, other → loaded as string contentIf no resolver name is given and the argument contains a . with a file
extension or /, load is assumed:
# These are equivalent:
config: ${load:config.yml}
config: ${config.yml} # shorthand — no resolver name, looks like a path
glob — Load Multiple Filesitems: ${glob:publications/*.yml}
items: ${glob:posts/**/*.yml} # recursive glob
Loads all files matching a glob pattern. Returns a list.
_source source-data key with the file path (stripped
before rendering).Supports ** for recursive matching.
env — Environment Variableanalytics: ${env:GOOGLE_ANALYTICS_ID}
port: ${env:PORT,8080} # with default value
debug: ${env:DEBUG,false}
Returns the value of an environment variable. An optional second argument (after comma) provides a default if the variable is unset.
Raises an error if the variable is unset and no default is provided.
ref — Explicit Cross-Referenceheader:
title: John Doe
nav:
brand:
text: ${ref:header.title} # → "John Doe"
Explicit cross-reference by dotted path. Equivalent to bare path syntax
(${header.title}) but unambiguous when the path could be confused with
a filename.
header:
title: John Doe
footer:
text: "© 2026 ${header.title}"
When ${...} contains a dotted path with no resolver prefix and it doesn't
look like a file path, it's treated as a cross-reference into the root document.
Disambiguation rule:
- Contains / or ends with a file extension (.yml, .md, etc.) → load
- Otherwise → cross-reference
${header.title} # cross-reference (no slash, no file extension)
${config.yml} # load (has .yml extension)
${data/items.yml} # load (has slash)
${ref:config.yml} # explicit ref (forces cross-reference even with extension)
${load:header.title} # explicit load (forces file load even without extension)
now — Current Date/Timefooter: "Built on ${now:%B %d, %Y}" # → "Built on March 11, 2026"
year: ${now:%Y} # → "2026"
Format string follows Python's strftime syntax.
baseurl — Prepend Base URLlogo: ${baseurl:assets/images/logo.png}
# → "/my-site/assets/images/logo.png" (if base_url is "/my-site")
asset — Copy and Resolve Assetphoto: ${asset:images/profile.jpg}
Copies the file to the output assets directory and returns the resolved URL (with base URL prepended). Combines file copying with URL generation.
md — Inline Markdownbio_html: ${md:This is **bold** and _italic_}
Renders a short markdown string to HTML inline. Useful when a renderer expects HTML but the source data is markdown.
Transform resolvers receive the output of the previous resolver in the pipe chain and return a transformed value.
sort — Sort a List${glob:*.yml | sort:year} # ascending by field
${glob:*.yml | sort:-year} # descending (prefix with -)
${glob:*.yml | sort:-year,title} # multi-key: year desc, then title asc
${glob:*.yml | sort:-git} # by last git commit date, newest first
See Sort Keys for all available sort keys.
reverse — Reverse a List${glob:*.yml | sort:year | reverse}
limit — Take First N Items${glob:*.yml | sort:-year | limit:5}
offset — Skip First N Items${glob:*.yml | sort:-year | offset:10 | limit:10} # "page 2"
filter — Keep Matching Items${glob:*.yml | filter:status=published}
${glob:*.yml | filter:lang=en,status=published} # AND conditions
${glob:*.yml | filter:image} # key exists
See Filter Syntax for details.
exclude — Remove Matching Items${glob:*.yml | exclude:draft=true}
${glob:*.yml | exclude:draft} # key exists → remove
Inverse of filter.
group — Group by Field${glob:*.yml | group:year}
# → {"2024": [...], "2025": [...], "2026": [...]}
Returns a dict of lists, keyed by the field value. Useful with custom renderers that display grouped content.
flatten — Flatten Nested Lists${glob:**/*.yml | flatten}
unique — Deduplicate by Field${glob:*.yml | unique:title}
Keeps the first occurrence of each unique field value.
map — Extract Fields${glob:*.yml | map:title}
# → ["Title 1", "Title 2", ...]
${glob:*.yml | map:title,src}
# → [{"title": "T1", "src": "..."}, ...]
Single field: returns a list of values. Multiple fields: returns a list of dicts with only those fields.
count — Return Lengthtotal: ${glob:posts/*.yml | count}
# → 42
The sort transform supports these sort keys:
Any key name refers to a field in the loaded data:
${glob:*.yml | sort:year} # sort by the "year" field
${glob:*.yml | sort:title} # sort by the "title" field
${glob:*.yml | sort:meta.order} # sort by nested field (dotted path)
| Key | Meaning |
|---|---|
name |
Source filename (alphabetical) |
modified |
File modification time (OS stat) |
| Key | Meaning |
|---|---|
git |
Last git commit date (committer date) — default |
git:committed |
Last git committer date (same as git) |
git:authored |
Last git author date |
git:created |
First git commit date (file creation in git history) |
# Blog posts sorted by last update
${glob:posts/*.yml | sort:-git}
# Projects sorted by when they were first added
${glob:projects/*.yml | sort:-git:created}
# Posts sorted by author date (ignores rebases)
${glob:posts/*.yml | sort:-git:authored}
Prefix any key with -:
${glob:*.yml | sort:-year} # newest first
${glob:*.yml | sort:-git} # most recently committed first
${glob:*.yml | sort:-name} # reverse alphabetical
Comma-separated keys for tie-breaking:
${glob:*.yml | sort:-year,title} # year desc, then title asc
${glob:*.yml | sort:category,-date} # category asc, then date desc
${glob:*.yml | filter:status=published}
${glob:*.yml | filter:status!=draft}
${glob:*.yml | filter:image} # keep items that have an "image" key
${glob:*.yml | exclude:draft} # remove items that have a "draft" key
${glob:*.yml | filter:status=published,lang=en}
All conditions must match (AND logic). For OR logic, use multiple pipes:
# No built-in OR — use separate globs and flatten
items:
- ${glob:en/*.yml}
- ${glob:fr/*.yml}
${glob:*.yml | filter:meta.featured=true}
Users can register custom resolvers via Python:
# my_resolvers.py
from webifier.core.resolvers import register_resolver
@register_resolver("icon")
def icon_resolver(arg, ctx, input=None):
"""Turn icon names into FontAwesome HTML."""
return f'<i class="fas fa-{arg}"></i>'
@register_resolver("date")
def date_resolver(arg, ctx, input=None):
"""Format a date string."""
from datetime import datetime
return datetime.now().strftime(arg or "%Y-%m-%d")
@register_resolver("shuffle")
def shuffle_resolver(arg, ctx, input=None):
"""Randomly shuffle a list — useful for testimonials."""
import random
items = list(input)
random.shuffle(items)
return items
Expose the resolver from an extension:
webifier:
extensions:
helpers:
uses: my.helpers
Then use in YAML:
footer: "Last updated ${date:%B %Y}"
cta_icon: ${icon:rocket}
testimonials: ${glob:testimonials/*.yml | shuffle | limit:3}
def my_resolver(arg: str, ctx: ResolveContext, input: Any = None) -> Any:
"""
Parameters
----------
arg : str
The argument string after the colon (e.g., "*.yml" in "glob:*.yml").
ctx : ResolveContext
Context object with:
- ctx.base_dir: directory of the current YAML file
- ctx.root: the root document dict (for cross-references)
- ctx.builder: the Builder instance (for asset resolution, etc.)
input : Any
Output from the previous resolver in a pipe chain.
None for source resolvers (first in chain).
Returns
-------
Any
The resolved value. Can be any type for full-value resolution.
Will be converted to str for string interpolation.
"""
${} is resolved after patch and defaults:
1. Parse YAML
2. Resolve patch (load files, merge)
3. Apply defaults (spread to siblings)
4. Resolve ${} (interpolation, references, transforms)
5. Dispatch to renderers
This means ${} can reference values that were brought in via patch.
The resolver detects circular ${ref:...} references and raises a clear error:
ResolverError: Circular reference detected: header.title → nav.brand.text → header.title
All resolver errors include the source file, key path, and resolver expression:
ResolverError: in index.yml at "publications.items":
${glob:publications/*.yml | sort:-year}
Unknown sort key: "year" — no "year" field found in any loaded item.
Available fields: title, authors, tags, date
To use a literal ${ in a value, escape with \:
code_example: "Use \${variable} for interpolation"
# → "Use ${variable} for interpolation" (literal, not resolved)