Kinetq Development Group, LLC. - Rendering HTML

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 shared FluidParser used to parse Liquid source into an IFluidTemplate.
  • ILiquidTemplateManager – caches parsed IFluidTemplate instances by key so templates are only parsed once.
  • ITemplateOptionsManager – resolves the TemplateOptions (file provider, registered types, filters, etc.) for a given prefix or route.
  • ILiquidPartialsManager – resolves registered LiquidPartial definitions, used by the partial-based RenderHtml overload.

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 the TemplateOptions.FileProvider.
  • renderModel – The view model exposed to the template as view_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 as view_model.
  • liquidRoute – A LiquidRoute describing 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 as view_model.
  • liquidRoute – The LiquidRoute describing the template to render.
  • streamWriter – The destination TextWriter. Output is written using HtmlEncoder.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:

  1. A cache key is computed ("{prefix}-{templatePath}" for partials, "{RouteTemplate}-{LiquidTemplatePath}" for route-based rendering).
  2. The renderer asks ILiquidTemplateManager.FluidTemplates for a previously parsed IFluidTemplate.
  3. If none exists, the file is loaded from the FileProvider, parsed using IFluidParserManager.FluidParser, and registered back into the template manager via RegisterTemplate.
  4. If parsing produces an error, a LiquidSyntaxException is thrown containing the parser's error message.
  5. A TemplateContext is built from the RenderModel and the resolved TemplateOptions, 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.

Do you have more questions? Contact us

Contact us