orchardcore-liquid
Skill for using Liquid templates in Orchard Core. Covers Liquid syntax, Orchard Core-specific filters and tags, shape rendering, content access, and Liquid best practices.
SKILL.mdsrc/CrestApps.AgentSkills/orchardcore/orchardcore-liquid/SKILL.md
GitHubOrchard Core Liquid
Liquid templates use the .liquid extension and receive Orchard Core globals,
shapes, filters, and tags. Liquid is powered by Fluid and all output is HTML
encoded unless the raw filter is used.
{{ Site.SiteName }}
{{ Culture.Name }}
{{ User.Identity.Name }}
{{ Request.Path }}
Use Culture.Name for the current request culture; do not treat
Site.Culture as the current UI culture.
Where Liquid Executes
Liquid runs only in shape templates (theme or admin Templates), shortcode
templates, LiquidPart, LiquidField, the Facebook plugin part, Liquid
settings such as patterns, and explicit APIs (ILiquidTemplateManager, the
liquid filter, Orchard.LiquidToHtmlAsync()). HtmlBodyPart, HtmlField,
MarkdownBodyPart, and MarkdownField treat Liquid syntax as plain source
text; they have no RenderLiquid setting anymore.
LiquidPart and LiquidField
Both store the authored source in a Liquid property exactly as submitted,
validate it on save, and render it when the shape, body aspect, or GraphQL
liquid field is requested. They have no Sanitize HTML setting because
Liquid can produce HTML, JSON, XML, or plain text. LiquidField has no
field-specific settings and only the standard editor (a CodeMirror Liquid
editor). Both are registered by the OrchardCore.Liquid feature when
OrchardCore.Contents is enabled.
await _contentDefinitionManager.AlterPartDefinitionAsync("LandingPage", part => part
.WithField("Intro", field => field
.OfType("LiquidField")
.WithDisplayName("Intro")
.WithPosition("1")));
await _contentDefinitionManager.AlterTypeDefinitionAsync("Snippet", type => type
.WithPart("TitlePart", part => part.WithPosition("0"))
.WithPart("LiquidPart", part => part.WithPosition("1")));
The LiquidField shape (LiquidField, LiquidFieldViewModel) exposes
Liquid (source), Html (rendered output), Field, Part,
PartFieldDefinition, and ContentItem. Inside the source, ContentItem is
the containing content item:
<h2>{{ ContentItem.DisplayText }}</h2>
<p>{{ "now" | local | date: "%Y" }} {{ Site.SiteName }}</p>
Recipe content stores the source under the field or part name:
{
"steps": [
{
"name": "Content",
"data": [
{
"ContentItemId": "[js:uuid()]",
"ContentType": "LandingPage",
"DisplayText": "Home",
"Latest": true,
"Published": true,
"LandingPage": {
"Intro": { "Liquid": "<p>Welcome to {{ Site.SiteName }}</p>" }
}
}
]
}
]
}
ManageLiquidTemplates Permission
OrchardCore.Liquid.Permissions.ManageLiquidTemplates is security critical and
is granted to Administrator and Editor by default (the migration keeps
existing Editor access). It is required, in addition to the normal content or
template permission, to:
- Edit
LiquidPart,LiquidField, orFacebookPluginPartbodies. - Edit, create, or publish any content item that contains one of them, directly
or recursively (for example a widget inside
FlowPart,BagPart, or custom JSON). This applies to the admin editors, the Content API, and remote publishing; an unauthorized mutation is rejected in full. - Use Templates, Admin Templates, and template previews. Template navigation and content-definition template shortcuts are hidden without it.
if (!await _authorizationService.AuthorizeAsync(User, OrchardCore.Liquid.Permissions.ManageLiquidTemplates))
{
return Forbid();
}
Site Properties
{{ Site.SiteSalt }} was removed for security reasons and only returns
[REDACTED]. {{ Site.Properties }} is redacted by
default because site settings can hold secrets. Allow specific property
sections with SettingsLiquidOptions.PermittedSiteProperties:
{
"OrchardCore": {
"Liquid": {
"PermittedSiteProperties": [
"LayerSettings",
"CurrentThemeName"
]
}
}
}
{{ Site.Properties.LayerSettings.Zones | join: ", " }}
Parser Configuration
The parser options live in the OrchardCore:Liquid section (the old
OrchardCore_Liquid section is deprecated but still read). All default to
false:
{
"OrchardCore": {
"Liquid": {
"AllowLiquidTag": true,
"AllowParentheses": false,
"AllowTrailingQuestionMark": false
}
}
}
Enable AllowLiquidTag only when the tenant needs the {% liquid %} tag.
Slugs and Transliteration
slugify lowercases text, strips diacritics, and replaces separators with -;
it keeps non-Latin letters and no longer accepts a transliterate argument.
Chain transliterate (provided by OrchardCore.Localization) when an ASCII
slug is required:
{{ "Crème brûlée" | slugify }} {% comment %} creme-brulee {% endcomment %}
{{ "Ελληνικά" | transliterate | slugify }} {% comment %} ellinika {% endcomment %}
Zones and Sections
zone is a block tag that adds its child content to a named layout zone. Use
render_section to render a zone:
{% zone "Header" %}
<span class="announcement">Welcome</span>
{% endzone %}
{% render_section "Header", required: false %}
{% render_section "Content", required: true %}
Shapes and Content Handles
{% shape "Menu", alias: "alias:main-menu" %}
{% contentitem alias: "alias:featured-article", display_type: "Summary" %}
{% contentitem id: "4j42fxryjcpqkkacmg6cwz3pr5" %}
An alias argument receives an Orchard content handle, including the alias:
prefix. Render an already built shape with shape_render:
{{ Model.Content.HtmlBodyPart | shape_render }}
Render the part or field shape rather than its stored source. The stored HTML
or Markdown is the authored, unsanitized source; the shape output has
shortcodes processed and is sanitized when SanitizeHtml is enabled. If you
must output stored HTML directly, sanitize it explicitly:
{{ Model.ContentItem.Content.Article.Summary.Html | sanitize_html | raw }}
Content and Localized Text
{{ Model.ContentItem.DisplayText }}
{{ Model.ContentItem.Content.AutoroutePart.Path }}
{{ "Welcome to Orchard Core" | t }}
{{ "Hello {0}!" | t: User.Identity.Name }}
Use the t filter for localized UI text; it also works in workflow Liquid
expressions. Use normal Fluid control flow and Orchard filters for content:
{% if User | has_permission: "EditContent", Model.ContentItem %}
<a href="{{ Model.ContentItem | edit_url }}">Edit</a>
{% endif %}
{% assign item = featuredContentItemId | content_item_id %}
{{ item.DisplayText }}
Data Localization Filters
The d filter (from OrchardCore.DataLocalization) translates dynamic data,
such as content type or field display names, using a context:
{{ "Blog" | d: "Content Types" }}
{{ "New {0}" | d: "Content Types", "Article" }}
Liquid Shape Templates
.liquid shape templates are rendered by LiquidShapeTemplateViewEngine,
directly with Fluid, instead of as Razor partials of a LiquidPage. The
LiquidViewExtensionProvider is no longer registered and the Razor shape
engine ignores .liquid files. Liquid files used as controller views still use
LiquidPage. PO localization contexts are unchanged.
Rendering Liquid from Code
Use ILiquidTemplateManager for explicit, trusted templates:
public sealed class SnippetRenderer
{
private readonly ILiquidTemplateManager _liquidTemplateManager;
private readonly HtmlEncoder _htmlEncoder;
public SnippetRenderer(ILiquidTemplateManager liquidTemplateManager, HtmlEncoder htmlEncoder)
{
_liquidTemplateManager = liquidTemplateManager;
_htmlEncoder = htmlEncoder;
}
public async Task<string> RenderAsync(string template, ContentItem contentItem)
{
if (!_liquidTemplateManager.Validate(template, out var errors))
{
throw new InvalidOperationException(string.Join(" ", errors));
}
return await _liquidTemplateManager.RenderStringAsync(
template,
_htmlEncoder,
contentItem,
new Dictionary<string, FluidValue> { ["ContentItem"] = new ObjectValue(contentItem) });
}
}
LiquidViewTemplateExtensions.RenderAsync() renders a parsed template in a
Liquid view scope; InvokeInScopeAsync() runs a callback with Model and
ViewLocalizer set from an explicit ViewContext (pass null outside a
request). Only run the liquid filter or Orchard.LiquidToHtmlAsync() on
trusted source; running them on HtmlField or HtmlBodyPart content
re-enables Liquid for editors who do not have ManageLiquidTemplates.
Custom Filters
using Fluid;
using Fluid.Values;
namespace MyModule;
public sealed class ReadingTimeFilter : ILiquidFilter
{
public ValueTask<FluidValue> ProcessAsync(
FluidValue input,
FilterArguments arguments,
LiquidTemplateContext context)
{
var wordCount = input.ToStringValue()
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Length;
return ValueTask.FromResult<FluidValue>(new StringValue($"{Math.Max(1, wordCount / 200)} min read"));
}
}
services.AddLiquidFilter<ReadingTimeFilter>("reading_time");
Upgrading from 3.x
RenderLiquidonHtmlBodyPart,HtmlField,MarkdownBodyPart, andMarkdownFieldis gone; Liquid in that source now renders as literal text. Tenant startup logs a warning per affected type, part, and field. Move the source toLiquidPartor aLiquidFieldmanually.- Grant
ManageLiquidTemplatesto custom roles that edit Liquid parts or fields, content containing them, or templates. - Stop using
{{ Site.SiteSalt }}(now[REDACTED]) and list anySite.Propertiessections you read inOrchardCore:Liquid:PermittedSiteProperties. slugify: transliterate: trueis no longer supported; usetransliterate | slugify.- Rename the
OrchardCore_Liquidconfiguration section toOrchardCore:Liquid.
See references/liquid-examples.md for layout, content, and zone examples.