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.

  1. Template-local helpers declared with #set inside the template body.
  2. Site variables defined per tenant — one record per site, shared by every template on that site.
  3. 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 #set loses the precedence fight.

Syntax rules that trip people up

  • Variable names are case-insensitive.
  • Use ASCII only: letters, numbers, underscore. No spaces, no hyphens.
  • #set only 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

SignalAction
Phrase appears in 2+ templatesExtract to a site variable.
Fact changes per siteMust be a site variable.
List should shuffle or differ per siteSite variable with a permutation inside.
Used exactly once in one templateUsually 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:

  1. Runtime variables
  2. Site variables
  3. System variables
  4. 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: PascalCase for regular strings, PascalCaseWithSuffix for 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 variableUse inline spintax
Phrase repeats across templatesOne-off synonym inside one sentence
Fact changes per siteGeneric verb or noun synonym
List should differ by tenantSmall 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'tWhyDo instead
Hardcode a tenant fact in a shared templateAll sites output the same copy, defeating multi-site reuse.Move the fact to a site variable.
Use #set to override a runtime variableRuntime always wins, your override silently does nothing.Rename the helper so it doesn't shadow the runtime name.
Assume %X% ... %X% repeats the same wordEach occurrence rerolls. You may get two different words.Rewrite the sentence or use two different variables.
Assume missing variables throwThey 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 fragmentProduces 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 #set name 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.


Continue the series