Syntax
Plume syntax is deliberately small. Templates stay close to HTML, and the extra syntax is reserved for values, control flow, reusable components, resources, and behaviour.
Output
Use {expression} for normal escaped output:
<h1>{post.title}</h1>Expressions can start with values, literals, function calls, or filters:
{"Draft" | downcase}
{"/photos/a b.jpg" | urlEncode}
{asset("images/avatar.png")}Host-provided PlumeSafeHTML renders as HTML. Ordinary strings are escaped. Use | raw only for trusted content.
<article>{post.html}</article>
<article>{customHTML | raw}</article>Expressions
Expressions can read values from the context, local variables, loop variables, component arguments, and host functions:
{site.title}
{post.author.name}
{posts.size}
{asset("images/avatar.png")}Supported literals include strings, numbers, booleans, nil, null, empty, blank, and arrays:
@let widths = [480, 960, 1440]
@let fallbackTitle = "Untitled"Comparisons and boolean operators work in conditionals and bindings:
@if post.title && post.urlPath.startsWith("/notes/") {
<a href="{post.urlPath}">{post.title}</a>
}
<button disabled?="{items.size == 0}">Continue</button>Note that ! negates the whole expression that follows it, so !a == b evaluates as !(a == b). To compare a negated value, bind it first: @let isHidden = !visible and then isHidden == true.
Use ternaries for small inline choices:
<span>{post.title ? post.title : "Untitled"}</span>For conditionals, empty strings, empty arrays, false, nil, and null are falsey. Non-empty strings, non-empty arrays, numbers, dictionaries, and safe HTML are truthy.
Locals
Use @let for local values:
@let currentPath = meta.canonicalUrl.replace(site.url, "")
@let isActive = currentPath == "/photos/"
<a href="/photos/" class:active="{isActive}">Photos</a>Conditionals
Use @if, else if, and else:
@if post.title {
<h1>{post.title}</h1>
} else if site.title {
<h1>{site.title}</h1>
} else {
<h1>Untitled</h1>
}Loops
Use @for to render arrays:
@for post in posts {
<article>
<h2>{post.title}</h2>
</article>
}Loop metadata is available through forloop:
@for item in items {
<span>{forloop.index}</span>
}Available loop values are:
forloop.index, starting at 1.forloop.index0, starting at 0.forloop.rindex, counting down to 1.forloop.rindex0, counting down to 0.forloop.first.forloop.last.forloop.length.
Comments
Use @comment when you want Plume to ignore a block entirely:
@comment {
<p>This does not render.</p>
@PostCard(post)
}Filters
Filters transform values:
{post.title | default("Untitled")}
{post.dateIso | date("d MMMM yyyy")}
{tags | join(", ")}
{content | raw}The most common filters:
default(value)— substitute for missing or empty values. The number0is kept.date(format)— format a date.join(separator),sort(field),where(field, value),map(field)— work with arrays.upcase,downcase,truncate(length),slugify— transform strings.
See Filters for the complete reference, covering every string, array, number, date, and output filter.
Methods
Some values also support method-style calls:
@if post.urlPath.startsWith("/photos/") {
<span>Photo post</span>
}
{post.title.replace(":", " - ")}Useful methods include contains, startsWith, endsWith, replace, replaceFirst, split, lowercased, uppercased, and slugify.
Attributes
Plume includes helpers for common conditional attributes:
<a
href="{post.urlPath}"
class="nav-link"
class:active="{isActive}"
class+="{post.kind}"
aria-current:page="{isActive}"
target?="{target}"
>
{post.title}
</a>class:name="{condition}"appends a class when the condition is true.class+="{value}"appends dynamic class names.attribute?="{value}"omits the attribute when the value is empty or false.attribute:value="{condition}"writesattribute="value"when true.style:name="{value}"binds an inline style property.
Style bindings work with ordinary properties and custom properties:
<span style:--offset="{offset}px" style:opacity="{visible ? 1 : 0}"></span>