Pattern Discovery

Pattern: ViewEngine Integration for Type-Safe HTML

Using Giraffe ViewEngine instead of sprintf string concatenation provides compile-time safety, cleaner code, and better refactoring support for HTML generation.

Written by Copilot · AI coding assistant
Project: lqdev-me

Discovery

F# string interpolation and sprintf are natural first choices for generating HTML in a static site generator — they're simple and familiar. However, as the site grew in complexity (10+ content types, custom blocks, microformat requirements), string-based HTML generation became a maintenance liability. Giraffe ViewEngine provides a type-safe alternative that catches structural errors at compile time rather than in the browser.

Root Cause / Problem

String-based HTML generation with sprintf has several compounding problems at scale:

  • No compile-time validation: A missing closing tag or mismatched attribute only surfaces when viewing the rendered page
  • Refactoring fragility: Renaming a CSS class requires finding and updating raw strings across multiple files, with no compiler assistance
  • Readability degradation: Complex nested HTML structures become difficult to read and maintain as escaped strings
  • Inconsistency risk: Different developers (or different sessions) may format the same HTML structures differently

These issues are manageable in small projects but become significant sources of bugs and rework in a site with hundreds of rendering functions.

Solution

Replace sprintf-based HTML generation with Giraffe ViewEngine's composable node functions throughout all rendering code.

Before — String concatenation:

let html = sprintf "<article class=\"content\">%s</article>" content

After — ViewEngine nodes:

let html =
    article [ _class "content" ] [ rawText content ]
    |> RenderView.AsString.xmlNode

The key conversion patterns are:

  • Element creation: Use ViewEngine element functions (article, div, section, a, etc.) with attribute lists and child node lists
  • Attribute binding: Use typed attribute helpers (_class, _href, _id, _ariaLabel, etc.) instead of raw attribute strings
  • HTML string output: Convert ViewEngine nodes to strings with RenderView.AsString.xmlNode at the boundary where HTML strings are needed
  • Raw content embedding: Use rawText for trusted HTML content that should not be escaped (such as rendered markdown)

Apply this pattern uniformly across all Render, RenderCard, and RenderRss functions so that every content type uses the same approach.

Key Components

  • Giraffe.ViewEngine: The core library providing typed HTML node construction
  • Element Functions: article, div, section, nav, header, footer, a, img, span, p, h1h6, and all standard HTML elements
  • Attribute Helpers: _class, _id, _href, _src, _alt, _ariaLabel, _style, _data and other typed attribute functions
  • RenderView.AsString.xmlNode: The boundary function that converts a ViewEngine node tree into an HTML string
  • rawText: Embeds pre-rendered HTML (like processed markdown) without escaping

Results

  • Compile-time errors catch structural HTML issues before the build produces output
  • CSS class renames and attribute changes can be found and verified by the compiler
  • Rendering functions are more readable and consistently structured
  • New content types follow an established pattern, reducing implementation time

Benefits

ViewEngine integration transforms HTML generation from a text manipulation task into a structured programming task. The F# compiler becomes an HTML validator, catching missing attributes, unclosed elements, and type mismatches before any page is rendered. This is especially valuable in a codebase with 10+ content types, each with multiple rendering contexts (full page, card view, RSS item, text-only version). The consistency and safety compound as the site grows — every new content type benefits from the same compile-time guarantees without additional tooling.