Norq

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:

  1. Notification-localnotifications/transactional/account/welcome/_shared/
  2. Category-levelnotifications/transactional/account/_shared/
  3. Type-levelnotifications/transactional/_shared/
  4. Globalnotifications/_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
Email .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 .md files in _shared/
  • {{@ triggers parameter completions inside partial files – lists known parameter names

See the LSP page for editor setup.