<link rel="stylesheet" href="/fonts.css" />
On this page Serve via net/http Embed into a binary Validate at startup Hot reload Custom directive Missing prop handling Static site with layout Syntax highlighting

How-to Guides

Practical recipes for common tasks. Each guide assumes you have a working htmlc engine — see the overview for initial setup and the Go API reference for full API details.

Serve a component via net/http

You want to render htmlc components in response to HTTP requests using the standard library.

Use ServeComponent for partial HTML responses (HTMX, turbo frames) and ServePageComponent for full HTML pages. Both return an http.HandlerFunc you register on any *http.ServeMux.

package main

import (
    "log"
    "net/http"

    "github.com/dhamidi/htmlc"
)

func main() {
    engine, err := htmlc.New(htmlc.Options{
        ComponentDir: "./components",
    })
    if err != nil {
        log.Fatal(err)
    }

    mux := http.NewServeMux()

    // Fragment handler — no <html> wrapper, good for HTMX responses.
    // The data function is called once per request.
    mux.HandleFunc("GET /search", engine.ServeComponent(
        "SearchResults",
        func(r *http.Request) map[string]any {
            return map[string]any{"query": r.URL.Query().Get("q")}
        },
    ))

    // Full-page handler — injects <style> into <head> automatically.
    // Return both the data map and the HTTP status code.
    mux.HandleFunc("GET /post/{id}", engine.ServePageComponent(
        "PostPage",
        func(r *http.Request) (map[string]any, int) {
            post, err := db.GetPost(r.PathValue("id"))
            if err != nil {
                return map[string]any{"error": err.Error()}, http.StatusNotFound
            }
            return map[string]any{"post": post}, http.StatusOK
        },
    ))

    log.Fatal(http.ListenAndServe(":8080", mux))
}

Pass per-request data (current user, CSRF token, feature flags) to every handler at once with WithDataMiddleware instead of repeating the logic in each data function.

Embed components into a Go binary

You want to ship a self-contained binary that has no dependency on files being present at the deployment path.

Use //go:embed to bundle the components/ directory into the binary, then pass the resulting embed.FS as Options.FS. When FS is set, all directory walks and file reads use it instead of the OS filesystem.

package main

import (
    "embed"
    "log"
    "net/http"

    "github.com/dhamidi/htmlc"
)

//go:embed components
var componentsFS embed.FS

func main() {
    engine, err := htmlc.New(htmlc.Options{
        ComponentDir: "components", // path inside the embedded FS
        FS:           componentsFS,
    })
    if err != nil {
        log.Fatal(err)
    }

    mux := http.NewServeMux()
    engine.Mount(mux, map[string]string{
        "GET /{$}":   "HomePage",
        "GET /about": "AboutPage",
    })
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Expected directory layout:

myapp/
├── main.go
└── components/
    ├── Layout.vue
    ├── HomePage.vue
    └── AboutPage.vue

This is recommended for production deployments. Without FS, the engine reads from the OS filesystem and the components/ directory must exist at the working directory of the running process.

Use hot-reload during development

You want component changes to be reflected in the browser without restarting the server.

Set Options.Reload = true. The engine will stat every registered file before each render and re-parse any that have changed.

engine, err := htmlc.New(htmlc.Options{
    ComponentDir: "./components",
    Reload:       true,
})

Tradeoff: Reload adds a stat syscall per component file on every render. Leave it false in production. A common pattern is to gate it behind a flag:

import "flag"

var dev = flag.Bool("dev", false, "enable hot reload")

func main() {
    flag.Parse()

    engine, err := htmlc.New(htmlc.Options{
        ComponentDir: "./components",
        Reload:       *dev,
    })
    // ...
}

Run with go run . -dev locally and without the flag in production. Alternatively, use a build tag to set the constant at compile time so the production binary has zero overhead.

Write a custom directive

You want to add a reusable HTML attribute behaviour that is not covered by the built-in directives.

Implement the htmlc.Directive interface and register it via Options.Directives or Engine.RegisterDirective. The interface has two hooks — Created (before rendering) and Mounted (after rendering). Both receive the working node, the binding, and a context.

Example: a v-uppercase directive that uppercases all direct text children of the element.

package main

import (
    "io"
    "strings"

    "golang.org/x/net/html"
    "github.com/dhamidi/htmlc"
)

type UppercaseDirective struct{}

// Created is called before the element is rendered.
// Mutate node.Attr or child text nodes here.
func (d *UppercaseDirective) Created(
    node *html.Node,
    binding htmlc.DirectiveBinding,
    ctx htmlc.DirectiveContext,
) error {
    for c := node.FirstChild; c != nil; c = c.NextSibling {
        if c.Type == html.TextNode {
            c.Data = strings.ToUpper(c.Data)
        }
    }
    return nil
}

// Mounted is called after the element's closing tag is written to w.
// Bytes written to w appear immediately after the element in the output.
func (d *UppercaseDirective) Mounted(
    w io.Writer,
    node *html.Node,
    binding htmlc.DirectiveBinding,
    ctx htmlc.DirectiveContext,
) error {
    return nil
}

func main() {
    engine, err := htmlc.New(htmlc.Options{
        ComponentDir: "./components",
        Directives: htmlc.DirectiveRegistry{
            "uppercase": &UppercaseDirective{},
        },
    })
    // ...
}

Use in a template:

<p v-uppercase>hello world</p>
<!-- renders: <p>HELLO WORLD</p> -->

See DirectiveBinding and DirectiveContext in the Go API reference for the full set of fields available to directive implementations.

Handle missing props gracefully

You want to control what happens when a template references a variable that was not passed as a prop.

By default, a missing prop renders a visible [missing: <name>] placeholder in the HTML. Use WithMissingPropHandler to choose a different behaviour.

// Abort the render and return an error — recommended for production.
// Any missing prop causes the entire response to fail, making omissions visible
// during development and CI rather than in rendered HTML.
engine.WithMissingPropHandler(htmlc.ErrorOnMissingProp)

// Render a visible placeholder string "MISSING PROP: <name>".
// Useful when gradually migrating templates that have optional props.
engine.WithMissingPropHandler(htmlc.SubstituteMissingProp)

Both are package-level functions with the MissingPropFunc signature — you can write your own to log, metric-count, or substitute a default value:

engine.WithMissingPropHandler(func(name string) (any, error) {
    slog.Warn("missing prop", "name", name)
    return "", nil // silently substitute empty string
})

Validate all components at startup

You want to catch broken component references before the server starts serving traffic.

Call ValidateAll after creating the engine. It checks every registered component for child component references that cannot be resolved and returns one ValidationError per problem. An empty slice means all components are valid.

package main

import (
    "log"
    "net/http"
    "os"

    "github.com/dhamidi/htmlc"
)

func main() {
    engine, err := htmlc.New(htmlc.Options{
        ComponentDir: "./components",
    })
    if err != nil {
        log.Fatal(err)
    }

    // Surface missing-component errors before accepting any traffic.
    if errs := engine.ValidateAll(); len(errs) != 0 {
        for _, e := range errs {
            log.Println(e)
        }
        os.Exit(1)
    }

    mux := http.NewServeMux()
    // ... register routes ...
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Run ValidateAll in CI by building a small cmd/validate/main.go that calls it and exits non-zero on any error. This catches typos in component names at review time rather than at runtime.

Build a static site with layout wrapping

You want to generate static HTML files where every page shares a common layout component.

Using the CLI

Pass -layout to htmlc build. The named component is used as the outer wrapper for every page in the -pages directory.

htmlc build \
  -dir   ./components \
  -pages ./pages \
  -out   ./dist \
  -layout Layout

Each page component receives a slot prop containing the rendered inner page HTML. The layout component must render {{ slot }} (or use v-html="slot") where the page content should appear. See the CLI reference for all flags.

Using the Go API

Call RenderFragment for the inner page, then pass the result as data to RenderPage on the layout:

// Render the inner page as a fragment (no full <html> document).
inner, err := engine.RenderFragmentString("BlogPost", map[string]any{
    "title":   post.Title,
    "content": post.Body,
})
if err != nil {
    return err
}

// Wrap the fragment in the layout, which renders a full HTML document.
// The layout template uses {{ "{{" }} slot }} to embed the inner HTML.
html, err := engine.RenderPageString("Layout", map[string]any{
    "pageTitle": post.Title,
    "slot":      inner,
})
if err != nil {
    return err
}

// Write html to a file or http.ResponseWriter.

This approach gives you full control over which pages receive which layout and what data is passed to each layer.

Add syntax highlighting with an external directive

You want source code blocks in your static site to be syntax-highlighted at build time using htmlc build.

v-syntax-highlight is a ready-made external directive that wraps the Chroma library. Place it in your component directory and htmlc build picks it up automatically.

Prerequisites: htmlc build is working for your project and Go 1.22+ is installed.

Step 1 — Install the directive

go install github.com/dhamidi/htmlc/cmd/v-syntax-highlight@latest

Then copy the binary into your component directory (the -dir you pass to htmlc build):

cp "$(go env GOPATH)/bin/v-syntax-highlight" ./components/

Step 2 — Generate a stylesheet

The directive uses CSS classes emitted by Chroma. Generate a stylesheet for the monokai theme (or any other Chroma style) and save it to your public assets directory:

v-syntax-highlight -print-css -style monokai > public/highlight.css

Link the stylesheet in your layout component:

<link rel="stylesheet" href="/highlight.css">

Step 3 — Mark code blocks in templates

Add v-syntax-highlight="'<language>'" to any <code> or <pre> element. The directive replaces the element's content with highlighted HTML and adds a language-* class:

<pre><code v-syntax-highlight="'go'">package main

import "fmt"

func main() {
    fmt.Println("hello, world")
}
</code></pre>

Step 4 — Build

htmlc build -dir ./components -pages ./pages -out ./dist

The generated HTML will contain highlighted <span> elements styled by the Chroma CSS classes. See the external directives reference for the full protocol and discovery rules.