On this page
SFC format Registration Composition Props Slots Scoped styles Engine Rendering HTTP handlers ValidateAll Missing props RegisterFunc Hot-reload / FS Error handling Scope rules Custom directivesComponent system
htmlc components are Vue Single File Components — .vue files with template, optional script, and optional style sections.
SFC format
A component file has up to three sections:
<!-- components/Card.vue -->
<template>
<div class="card">
<h2>{{ title }}</h2>
<slot>No content provided.</slot>
</div>
</template>
<!-- Optional: preserved verbatim in output, never executed -->
<script>
export default { props: ['title'] }
</script>
<!-- Optional: global or scoped CSS -->
<style scoped>
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 1rem;
}
</style>
<template>— required; contains the HTML template with directives<script>— optional; preserved verbatim but never executed by the engine<style>— optional; addscopedattribute to scope styles to this component
Component registration
The engine automatically discovers all .vue files in the component directory. Components are referenced by their filename without the extension.
// Go API
engine, err := htmlc.New(htmlc.Options{
ComponentDir: "./components",
})
// Register an additional component explicitly
engine.Register("MyCard", "/path/to/MyCard.vue")
In templates, component names follow PascalCase:
<!-- Card.vue in the component dir -->
<Card :title="post.title">
<p>{{ post.body }}</p>
</Card>
Component composition
Components can nest other components from the same registry. Props are passed as attributes; expressions use : shorthand.
<!-- templates/PostPage.vue -->
<template>
<Layout :title="title">
<Card :title="post.title">
<p>{{ post.body }}</p>
</Card>
<Card v-for="related in relatedPosts" :title="related.title" />
</Layout>
</template>
Props
Props are any data passed to a component. In templates, static props are strings; dynamic props use :.
<!-- Static: value is the literal string "Hello" -->
<Card title="Hello" />
<!-- Dynamic: value is the result of the expression -->
<Card :title="post.title" />
<!-- Spread all props -->
<Card v-bind="post" />
Discover what props a component uses:
$ htmlc props -dir ./templates Card
title
author
body
Slots
Default slot
<!-- In Card.vue -->
<div class="card">
<slot>Fallback when no content is provided</slot>
</div>
<!-- Usage -->
<Card title="Hello">
<p>This renders inside the slot.</p>
</Card>
Named slots
<!-- In Layout.vue -->
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
<!-- Usage -->
<Layout>
<template #header>
<nav><a href="/">Home</a></nav>
</template>
<article>Main content</article>
<template #footer><p>© 2024</p></template>
</Layout>
Scoped slots
<!-- In List.vue -->
<ul>
<li v-for="item in items">
<slot :item="item">{{ item }}</slot>
</li>
</ul>
<!-- Usage: destructure slot props -->
<List :items="posts">
<template #default="{ item }">
<a :href="item.url">{{ item.title }}</a>
</template>
</List>
Scoped styles
Add scoped to <style> to confine styles to the component. The engine rewrites selectors and adds a unique scope attribute to matching elements.
<style scoped>
.card { background: white; border-radius: 8px; }
h2 { color: #333; }
</style>
Output (approximately):
<style>
.card[data-v-a1b2c3] { background: white; border-radius: 8px; }
h2[data-v-a1b2c3] { color: #333; }
</style>
Go API
import "github.com/dhamidi/htmlc"
// Create an engine that loads components from a directory
engine, err := htmlc.New(htmlc.Options{
ComponentDir: "./components",
Debug: false,
})
if err != nil {
log.Fatal(err)
}
Rendering
// Render a fragment (no <!DOCTYPE>)
html, err := engine.RenderFragmentString("Card", map[string]any{
"title": "Hello",
"body": "World",
})
// Render a full page (<!DOCTYPE html>)
err = engine.RenderPage(w, "HomePage", map[string]any{
"title": "My site",
})
HTTP handlers
ServeComponent
Returns an http.HandlerFunc that renders a component as an HTML
fragment and writes it with Content-Type: text/html; charset=utf-8.
The data function is called on every request; pass nil if no data
is needed.
http.Handle("/widget", engine.ServeComponent("Widget", func(r *http.Request) map[string]any {
return map[string]any{"id": r.URL.Query().Get("id")}
}))
ServePageComponent
Like ServeComponent but renders a full HTML page (injecting scoped
styles into </head>) and lets the data function return an HTTP
status code alongside the data map. A status code of 0 is treated as 200.
http.Handle("/post", engine.ServePageComponent("PostPage",
func(r *http.Request) (map[string]any, int) {
post, err := db.GetPost(r.URL.Query().Get("slug"))
if err != nil {
return nil, http.StatusNotFound
}
return map[string]any{"post": post}, http.StatusOK
},
))
Mount
Registers multiple component routes on an http.ServeMux in one
call. Each component is served as a full HTML page. Keys are
http.ServeMux patterns (e.g. "GET /{$}").
engine.Mount(mux, map[string]string{
"GET /{$}": "HomePage",
"GET /about": "AboutPage",
"GET /posts": "PostsPage",
})
WithDataMiddleware
Adds a function that enriches the data map on every HTTP-triggered render. Multiple middleware functions are applied in registration order. Use this to inject values shared across all routes — current user, CSRF token, etc.
Scope note: Middleware values are available only in the
top-level page scope. If a child component needs a middleware-supplied value,
pass it down as an explicit prop or register it with RegisterFunc
instead.
engine.WithDataMiddleware(func(r *http.Request, data map[string]any) map[string]any {
data["currentUser"] = sessionUser(r)
data["csrfToken"] = csrf.Token(r)
return data
})
Startup validation
ValidateAll
Checks every registered component for unresolvable child component references.
Returns a slice of ValidationError (one per problem). Call once
at startup to surface missing-component problems before the first request.
if errs := engine.ValidateAll(); len(errs) > 0 {
for _, e := range errs {
log.Printf("component error: %v", e)
}
os.Exit(1)
}
Missing prop handling
By default a missing prop renders a visible
[missing: propName] placeholder so the page still loads and the
absent prop is immediately obvious. Override this behaviour with
WithMissingPropHandler:
// Abort the render with an error on any missing prop
engine.WithMissingPropHandler(htmlc.ErrorOnMissingProp)
// Silently substitute an empty string
engine.WithMissingPropHandler(func(name string) (any, error) {
return "", nil
})
Template functions
RegisterFunc
Registers a Go function callable from any template expression rendered by this engine. Unlike props, registered functions are available in every component at every nesting depth — no prop threading needed. Engine functions act as lower-priority builtins: the render data scope overrides them.
engine.RegisterFunc("formatDate", func(args ...any) (any, error) {
t, _ := args[0].(time.Time)
return t.Format("2 Jan 2006"), nil
})
engine.RegisterFunc("url", func(args ...any) (any, error) {
name, _ := args[0].(string)
return router.URLFor(name), nil
})
Use them directly in templates:
<span>{{ formatDate(post.CreatedAt) }}</span>
<a :href="url('home')">Home</a>
Advanced options
Hot-reload
Set Reload: true to re-parse changed .vue files
automatically before each render — no server restart required. Disable in
production.
engine, err := htmlc.New(htmlc.Options{
ComponentDir: "templates/",
Reload: true,
})
Embedded filesystem
Set Options.FS to any fs.FS — including
embed.FS — to load component files from an embedded or virtual
filesystem instead of the OS filesystem. ComponentDir is then
interpreted as a path inside the FS.
import "embed"
//go:embed templates
var templateFS embed.FS
engine, err := htmlc.New(htmlc.Options{
FS: templateFS,
ComponentDir: "templates",
})
Note: Hot-reload (Reload: true) only works
when the FS also implements fs.StatFS. The standard
embed.FS does not implement fs.StatFS, so
reload is silently skipped for embedded filesystems.
Context-aware rendering
Use RenderPageContext / RenderFragmentContext to
propagate cancellation and deadlines through the render pipeline.
ServeComponent and ServePageComponent forward
r.Context() automatically.
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
err = engine.RenderPageContext(ctx, w, "Page", data)
err = engine.RenderFragmentContext(ctx, w, "Card", data)
Error handling
Parse and render failures carry structured location information.
Use errors.As to inspect them:
_, err := htmlc.ParseFile("Card.vue", src)
var pe *htmlc.ParseError
if errors.As(err, &pe) {
fmt.Println(pe.Path) // "Card.vue"
if pe.Location != nil {
fmt.Println(pe.Location.Line) // 1-based line number
fmt.Println(pe.Location.Snippet) // 3-line source context
}
}
err = engine.RenderFragment(w, "Card", data)
var re *htmlc.RenderError
if errors.As(err, &re) {
fmt.Println(re.Component)
fmt.Println(re.Expr) // expression that failed
if re.Location != nil {
fmt.Println(re.Location.Line)
fmt.Println(re.Location.Snippet)
}
}
When location is available, err.Error() produces a compiler-style message:
Card.vue:14:5: render Card.vue: expr "post.Title": cannot access property "Title" of null
13 | <div class="card">
> 14 | {{ post.Title }}
15 | </div>
Scope propagation rules
Each component renders in an isolated scope containing only
its own props. Parent scope variables are not inherited. The one exception
is functions registered with RegisterFunc — they are injected
into every component's scope automatically.
| Mechanism | Available in top-level page | Available in child components |
|---|---|---|
RenderPage / RenderFragment data map |
Yes | No — pass as props |
WithDataMiddleware values |
Yes | No — pass as props |
RegisterFunc functions |
Yes | Yes (automatic) |
Explicit :prop="expr" |
— | Yes |
Custom directives
engine.RegisterDirective("v-highlight", func(ctx *htmlc.DirectiveContext) error {
// ctx.Node — the HTML node being rendered
// ctx.Value — the directive value expression result
// ctx.Scope — the current render scope
ctx.Node.Attr = append(ctx.Node.Attr, html.Attribute{
Key: "class", Val: "highlighted",
})
return nil
})