Rendering HTML
While the LiquidMiddleware is the most common way to render Liquid templates in a Liquid Pages application, it's not the only option. There are situations where you may need to render a Liquid template into an HTML string outside of the request/response pipeline — for example, when generating HTML for emails, server-sent events, AI chat responses, background jobs, or any other scenario where you simply need a string of HTML rather than a full HTTP response.
For these cases, Liquid Pages exposes an injectable interface called IHtmlRenderer.
What is IHtmlRenderer?
IHtmlRenderer is a service registered automatically when you call services.AddLiquidPages(). It is responsible for resolving a Liquid template, parsing it (with caching via the ILiquidTemplateManager), building a TemplateContext from your view model, and producing rendered HTML.
Internally, HtmlRenderer depends on four services:
IFluidParserManager– provides the sharedFluidParserused to parse Liquid source into anIFluidTemplate.ILiquidTemplateManager– caches parsedIFluidTemplateinstances by key so templates are only parsed once.ITemplateOptionsManager– resolves theTemplateOptions(file provider, registered types, filters, etc.) for a given prefix or route.ILiquidPartialsManager– resolves registeredLiquidPartialdefinitions, used by the partial-basedRenderHtmloverload.
Because IHtmlRenderer bypasses the middleware entirely, there is no routing, no request model, and no response model involved — just template + data → HTML.
Available Overloads
IHtmlRenderer exposes three RenderHtml overloads, each targeting a slightly different scenario:
1. RenderHtml(string prefix, string templatePath, RenderModel renderModel)
Renders a registered Liquid partial. This overload is designed for scenarios where you want to render HTML completely outside of the routing system.
prefix– FileProviders/TemplatOptions are registered by route prefixes (/, /blog, /chat ect..). Based on the prefix the TemplateOptions will be fetched by the TemplateOptionsManager.templatePath– The path to the Liquid template file, relative to theTemplateOptions.FileProvider.renderModel– The view model exposed to the template asview_model.
string? html = await _htmlRenderer.RenderHtml(
prefix: "chat",
templatePath: "Partials/Message.liquid",
renderModel: new RenderModel { ViewModel = message });
2. RenderHtml(RenderModel renderModel, LiquidRoute liquidRoute)
Renders a Liquid template specified by a LiquidRoute and returns the rendered HTML as a string. This is the most common overload for rendering inside of a middleware if you aren't looking to stream the response.
renderModel– The view model exposed to the template asview_model.liquidRoute– ALiquidRoutedescribing where the template lives.
If liquidRoute.TemplateOptions is not already set, the renderer resolves it via ITemplateOptionsManager.GetTemplateOptions(liquidRoute.RouteTemplate). The cache key for this overload is "{RouteTemplate}-{LiquidTemplatePath}". If the template file is not found, the method returns null.
3. RenderHtml(RenderModel renderModel, LiquidRoute liquidRoute, TextWriter streamWriter)
Renders a Liquid template directly into a supplied TextWriter instead of allocating a string. This is ideal for streaming scenarios such as server-sent events, large HTML responses, or writing directly to an HTTP response body.
renderModel– The view model exposed to the template asview_model.liquidRoute– TheLiquidRoutedescribing the template to render.streamWriter– The destinationTextWriter. Output is written usingHtmlEncoder.Default.
If the template cannot be found, the method simply returns without writing anything.
await _htmlRenderer.RenderHtml(renderModel, liquidRoute, Response.Writer);
Template Caching and Error Handling
All three overloads share the same parse/cache pipeline:
- A cache key is computed (
"{prefix}-{templatePath}"for partials,"{RouteTemplate}-{LiquidTemplatePath}"for route-based rendering). - The renderer asks
ILiquidTemplateManager.FluidTemplatesfor a previously parsedIFluidTemplate. - If none exists, the file is loaded from the
FileProvider, parsed usingIFluidParserManager.FluidParser, and registered back into the template manager viaRegisterTemplate. - If parsing produces an error, a
LiquidSyntaxExceptionis thrown containing the parser's error message. - A
TemplateContextis built from theRenderModeland the resolvedTemplateOptions, and rendering is performed against it.
This means each unique template is parsed at most once per application lifetime, regardless of which overload triggers the parse.
