Variables and multi-site reuse
Variables are what turn one template into a network. Get the variable design right and 100 sites render from one source. Get it wrong and you are copy-pasting text into every preset.
Three sources, one merged scope
At render time, most engines merge variables from three places into one lookup table. When the template reads %SomeName%, the resolver walks that table and substitutes the value.
- Template-local helpers declared with
#setinside the template body. - Site variables defined per tenant — one record per site, shared by every template on that site.
- Runtime variables passed into the resolver at call time (article context, system context, user context).
Authoring decisions boil down to which layer owns each fact.
Template-local helpers with #set
Use #set for short-lived helpers inside one template:
#set %Lead% = {Welcome|Greetings|Hello}
%Lead% to %brand_name%!
Good uses:
- one-template helpers that would otherwise clutter the body with repetition;
- long repeated phrases used multiple times inside the same template;
- readability, when nesting gets deep enough to hurt the eye.
Bad uses:
- tenant-specific facts — those belong in site variables;
- anything the runtime already provides — local
#setloses the precedence fight.
Syntax rules that trip people up
- Variable names are case-insensitive.
- Use ASCII only: letters, numbers, underscore. No spaces, no hyphens.
#setonly works when it starts a line.- Comments use
/# ... #/and are stripped before processing. - Unknown variables stay literal.
%MissingVar%renders as%MissingVar%, not as an empty string or an error. Treat leftovers as a QA failure.
Site variables — the multi-site multiplier
Site variables are the reason a shared template can serve many sites without reading the same across every domain.
A generic site preset looks like this:
#set %BrandTone% = {practical|no-nonsense|straightforward}
#set %Industry% = SaaS analytics
#set %TopFeatures% = [<minsize=3;maxsize=4;sep=", ";lastsep=" and ">dashboards|alerting|audit logs|SSO|role-based access]
#set %Audience% = {teams|product leads|operations}
Every shared template can now read %BrandTone%, %TopFeatures%, etc., and the output changes per site without anyone touching the template.
When to create a site variable
| Signal | Action |
|---|---|
| Phrase appears in 2+ templates | Extract to a site variable. |
| Fact changes per site | Must be a site variable. |
| List should shuffle or differ per site | Site variable with a permutation inside. |
| Used exactly once in one template | Usually keep it inline. |
Runtime variables
Runtime variables come from the calling context: the article being rendered, the current user, the system clock. They override site variables and template-local helpers with the same name.
Common runtime variables across engines (names depend on your implementation):
%year%— current year%lang%— current language code%site_domain%— current site host%brand_name%,%product_name%— brand/product the article talks about%article_topic%,%category%— article-level metadata
Authors never assign these from a template. Reading them is enough.
Variable precedence
When the same name exists in multiple layers, the highest priority wins. A standard order, from strongest to weakest:
- Runtime variables
- Site variables
- System variables
- Template-local
#set
Practical consequence: #set %brand_name% = Demo inside a template does nothing if the runtime passes %brand_name%. Runtime wins. Pick local helper names that do not shadow the runtime.
Naming conventions
Consistency inside one preset matters more than any specific style. Still, a reasonable default:
- Runtime variables:
lowercase_snake_case, usually. They exist outside your control. - Site variables:
PascalCasefor regular strings,PascalCaseWithSuffixfor grammatical variants. - List variables: pluralize (
%TopFeatures%,%SupportedLanguages%). - Local helpers: short and descriptive —
%Lead%,%Closing%.
Compound variables
Site variables can reference each other. The preset resolver substitutes inter-variable references first while keeping nested spintax raw, so later rerolls still work:
#set %FoundedLine% = launched in %FoundedYear%, based in %HQ%
#set %Pitch% = {fast|lightweight|self-hosted} %ProductCategory%
Use compounds to compose repeated facts once and reuse them across templates.
The reroll gotcha
This is the single most common source of confusion for new authors. If a variable contains raw spintax, every occurrence rerolls independently.
#set %Tone% = {safe|trusted}
%Tone% and %Tone%
Possible output:
Safe and trusted
Do not assume %Tone% resolves once and then echoes. If you need exact repetition, rewrite the sentence so the variable appears only once. If you need two different adjectives, use two variables.
Optional fragments
An empty branch in an enumeration yields an optional fragment:
{|official }website
{fast|secure|} withdrawals
Put the space inside the optional branch when the fragment may disappear, otherwise you get double spaces or jammed words. For an optional list (a permutation that may be empty), wrap the whole permutation:
{|[<minsize=2;maxsize=3;sep=", ";lastsep=" and ">Slack|Jira|Linear]}
The engine cannot pick zero items from a permutation. Wrapping is the only way to make "no list at all" a possible outcome.
Separator collisions
A common rendering bug: a list variable already contains and, and the surrounding text adds another and.
%Integrations% and other tools
If %Integrations% resolves to Slack, Jira, and Linear, the final text reads:
Slack, Jira, and Linear and other tools
Fixes:
- insert a comma:
%Integrations%, and other tools; - restructure:
{Besides|Along with} %Integrations%, other tools...; - drop the trailing conjunction and use a colon or em-dash.
Same issue with a permutation that has lastsep=" and " followed by fixed text starting with and. Preview a few variants before shipping.
Variables vs inline spintax
| Use a variable | Use inline spintax |
|---|---|
| Phrase repeats across templates | One-off synonym inside one sentence |
| Fact changes per site | Generic verb or noun synonym |
| List should differ by tenant | Small fixed one-off list |
| Grammatical form needs multiple spellings (see Russian cases in article 4) | Word used in only one grammatical position |
Rule of thumb: extract repeated grammar-sensitive phrases to variables before adding tiny inline synonym slots. The variable gives you one place to fix mistakes. Inline scatters them.
Common mistakes with variables
| Don't | Why | Do instead |
|---|---|---|
| Hardcode a tenant fact in a shared template | All sites output the same copy, defeating multi-site reuse. | Move the fact to a site variable. |
Use #set to override a runtime variable | Runtime always wins, your override silently does nothing. | Rename the helper so it doesn't shadow the runtime name. |
Assume %X% ... %X% repeats the same word | Each occurrence rerolls. You may get two different words. | Rewrite the sentence or use two different variables. |
| Assume missing variables throw | They render literally as %MissingVar%. | Add a preview pass that flags leftover %...%. |
| Concatenate a list variable with another "and" | Produces "A, B, and C and other things". | Use a comma or restructure. |
| Forget the space in an optional fragment | Produces double spaces or jammed words. | Put the space inside the optional branch. |
Variable-design checklist
- Every tenant-specific fact lives in a site variable, not in the shared template.
- Every article-specific fact lives in a runtime variable, not in
#set. - No helper
#setname shadows a runtime variable. - Variable names are ASCII, no spaces, no hyphens.
- Every repeated variable has been reviewed for the reroll effect.
- Every optional fragment has its whitespace handled inside the branch.
- Every list variable followed by a conjunction has been checked for separator collision.
- Five resolved samples have no leftover
%...%.
Ready for structure? The next guide covers permutations in practice — where the variety actually lives.