Template Partials
Partials are reusable template fragments. Include them with {{> name}} syntax. They live in the _shared/ directory at the notifications root.
Basic partials
Place shared fragments in notifications/_shared/ as .md files:
notifications/
_shared/
email-header.md
email-footer.md
slack-footer.md
transactional/
order-shipped/
email.md
slack.md
Include a partial by name (without the .md extension):
{{> email-header}}
# Your order has shipped
Body content here.
{{> email-footer}}{{> email-header}} loads _shared/email-header.md and inserts its content inline. The partial can contain any valid template syntax – directives, expressions, images, buttons.
If a partial is not found, the {{> name}} tag is left as-is in the output.
Parameterized partials
Pass named parameters to make partials reusable across different data contexts:
{{> product-card name=item.name price=item.price title="Featured"}}Inside the partial, reference parameters with the {{@param}} prefix:
<!-- _shared/product-card.md -->
**{{@name}}** — {{@price | currency "USD"}}
_{{@title}}_Path vs literal parameters
Parameters come in two forms:
| Form | Syntax | Behavior |
|---|---|---|
| Path | name=item.name |
{{@name}} becomes {{item.name}} – resolved at compile time from the data context |
| Literal | title="Featured" |
{{@title}} becomes the string Featured directly |
Path parameters use bare unquoted values. Literal parameters use double or single quotes.
{{> card
name=product.name
price=product.price
label="New Arrival"
}}After expansion, the partial content contains standard {{expressions}} that the compiler resolves against the data object.
Pipes with parameters
Parameters work with pipe chains. Inside a partial:
Price: {{@price | currency "USD"}}When called with a path parameter (price=item.cost), this expands to:
Price: {{item.cost | currency "USD"}}
When called with a literal parameter (price="49.99"), this expands to:
Price: {{"49.99" | currency "USD"}}
The pipe chain is preserved in both cases – only the @ reference is substituted.
Nesting
Partials can include other partials. A product card might include a price tag partial:
<!-- _shared/product-card.md -->
**{{@name}}**
{{> price-tag price=@price}}<!-- _shared/price-tag.md -->
{{@price | currency "USD"}}Norq detects cycles and prevents infinite recursion. If partial a includes b which includes a, the cycle is broken with an HTML comment:
<!-- partial cycle: a -->Missing parameters
If a partial references a parameter that wasn’t passed by the caller, the {{@unknown}} tag is left as-is. This is not an error – it allows partials to have optional parameters.
<!-- _shared/card.md -->
**{{@title}}**
{{@subtitle}}{{> card title=product.name}}Result: {{@subtitle}} remains in the output unchanged.
Diagnostic line numbers across partial boundaries
Partial expansion substitutes {{> partial}} with the partial’s content, which
can be multi-line. This shifts the line numbers of every subsequent block in
the expanded document. Norq’s parser tracks a line map alongside the
expanded text so that diagnostics from the linter and strict-mode validator
are remapped back to the user’s source file before being surfaced.
Diagnostics that originate inside a partial body are attributed to the
{{> partial}} invocation line in the parent template (not to a position in
the partial file), since the partial isn’t open in the editor.
Example: reusable product card
_shared/product-card.md:
::: callout
**{{@name}}**
{{@price | currency "USD"}}
::: if @description
_{{@description}}_
:::
[View Details]({{@url}}){button.secondary}
:::Caller in email.md:
::: each order.items as item
{{> product-card name=item.name price=item.price url=item.detail_url description=item.summary}}
:::Caller with literal fallback:
{{> product-card name=item.name price=item.price url=item.detail_url description="No description available"}}Hierarchical resolution
Partials resolve from _shared/ directories at four scope levels. The resolver checks each scope in order and uses the first match:
- Notification-local –
notifications/transactional/account/welcome/_shared/ - Category-level –
notifications/transactional/account/_shared/ - Type-level –
notifications/transactional/_shared/ - Global –
notifications/_shared/
notifications/
_shared/ # Global
email-footer.md
transactional/
_shared/ # Type-level
receipt-table.md
account/
_shared/ # Category-level
account-header.md
welcome/
_shared/ # Notification-local
hero-banner.md
email.md
A {{> email-footer}} in welcome/email.md first checks welcome/_shared/, then account/_shared/, then transactional/_shared/, then the global _shared/. First match wins.
Note: The resolver has two internal variants. The standard variant resolves Markdown partials only – native-format partials (.mjml, .blocks.json, .card.json) are left as unexpanded {{> name}} markers. For channels that support native partials (email, Slack, MS Teams), the compiler uses the _with_natives variant, which replaces native partial references with marker comments and injects the content post-parse. This distinction is invisible to template authors but matters if you are extending the compiler.
Shadowing
Closer scopes override broader ones. If email-footer.md exists at both the notification-local and global _shared/ levels, the local version is used. This lets you override a global partial for a specific notification without affecting others.
notifications/
_shared/email-footer.md # used by all other notifications
transactional/
welcome/
_shared/email-footer.md # shadows global for this notification only
email.md
Native format partials
Partials can use channel-native formats instead of Markdown: .mjml for email, .blocks.json for Slack, .card.json for MS Teams. Place them in any _shared/ directory alongside regular .md partials.
Channel-aware resolution
The resolver prefers native formats for their target channel and falls back to .md:
| Channel | Preferred format | Fallback |
|---|---|---|
.mjml |
.md |
|
| Slack | .blocks.json |
.md |
| MS Teams | .card.json |
.md |
| SMS, Push, WhatsApp | .md only |
– |
A partial named pricing-table with both pricing-table.mjml and pricing-table.md in _shared/ resolves to the .mjml version for email and the .md version for all other channels.
Handlebars rendering
Native partials support Handlebars control flow: {{#if}}, {{#each}}, {{#unless}}. Data expressions use double-brace syntax, same as Markdown templates.
<!-- _shared/order-items.mjml -->
<mj-section>
{{#each order.items}}
<mj-column>
<mj-image src="{{this.imageUrl}}" alt="{{this.name}}" />
<mj-text>{{this.name}} -- {{this.price | currency "USD"}}</mj-text>
</mj-column>
{{/each}}
</mj-section>Parameter substitution
Named parameters work the same as in Markdown partials. Use {{@key}} inside the native partial:
<!-- _shared/cta-button.mjml -->
<mj-button href="{{@url}}" background-color="{{@color}}">
{{@label}}
</mj-button>{{> cta-button url=order.trackingUrl color="#18181b" label="Track Order"}}Escaping
MJML partials apply HTML escaping to expression values. Slack .blocks.json and Teams .card.json partials apply JSON escaping – special characters in data values are escaped for safe inclusion in JSON strings.
When to use native partials
Use native format partials when Markdown cannot express the layout you need:
- Email: complex multi-column pricing tables, custom MJML components, layouts that require precise
<mj-column>control - Slack: advanced Block Kit arrangements with interactive elements, overflow menus, or date pickers
- Teams: Adaptive Card layouts with column sets, fact sets, or action groups
If Markdown can express it, prefer .md – it works across all channels and is easier to maintain.
LSP support
The Norq language server provides completions for partials:
{{>triggers partial name completions – lists all.mdfiles in_shared/{{@triggers parameter completions inside partial files – lists known parameter names
See the LSP page for editor setup.