htmlc
A server-side Go template engine that uses Vue.js Single File Component (.vue) syntax for authoring but renders entirely in Go with no JavaScript runtime.
This is a static rendering engine. There is no reactivity, virtual DOM, or client-side hydration. Templates are evaluated once per request and produce plain HTML.
Installation
CLI
go install github.com/dhamidi/htmlc/cmd/htmlc@latest
Go package
go get github.com/dhamidi/htmlc
Quick start
Create a component file:
<!-- templates/Greeting.vue -->
<template>
<p>Hello, {{ name }}!</p>
</template>
Render it:
$ htmlc render -dir ./templates Greeting -props '{"name":"world"}'
<p>Hello, world!</p>
Render as a full HTML page:
$ htmlc page -dir ./templates Greeting -props '{"name":"world"}'
<!DOCTYPE html>
<p>Hello, world!</p>
Text interpolation
{{ expr }} evaluates the expression against the current render scope and HTML-escapes the result.
<p>Hello, {{ name }}!</p>
<p>{{ a }} + {{ b }} = {{ a + b }}</p>
Expression language
| Category | Operators / Syntax |
|---|---|
| Arithmetic | + - * / % ** |
| Comparison | === !== > < >= <= == != |
| Logical | && || ! |
| Nullish coalescing | ?? |
| Optional chaining | obj?.key arr?.[i] |
| Ternary | condition ? then : else |
| Member access | obj.key arr[i] arr.length |
| Function calls | fn(args) via engine.RegisterFunc |
| Array literals | [a, b, c] |
| Object literals | {{ key: value } |
Use .length to measure collections — it works on strings, slices, arrays, and maps:
<span>{{ items.length }}</span>
Directives overview
| Directive | Supported | Description |
|---|---|---|
v-if | Yes | Renders element only when expression is truthy |
v-else-if | Yes | Must follow v-if or v-else-if |
v-else | Yes | Must follow v-if or v-else-if |
v-for | Yes | Repeats element for each item |
v-show | Yes | Toggles display:none |
v-bind | Yes | Dynamically binds attribute or prop |
v-html | Yes | Sets inner HTML (unescaped) |
v-text | Yes | Sets text content (HTML-escaped) |
v-pre | Yes | Skips interpolation and directives for element and descendants |
v-switch / v-case | Yes | Switch/case conditional; use with v-case and v-default on child elements |
v-slot | Yes | Named and scoped slots |
v-model | Stripped | Client-side only; removed from output |
@event | Stripped | Client-side only; removed from output |
See the full directives reference for detailed examples.
Component system
Components are .vue Single File Components with up to three sections:
<template>— required; the HTML template with directives<script>— optional; preserved verbatim in output but never executed<style>— optional; global or scoped CSS
<!-- templates/Card.vue -->
<template>
<div class="card">
<h2>{{ title }}</h2>
<slot>Default content</slot>
</div>
</template>
<style scoped>
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 1rem;
}
</style>
Props
Props are passed as a JSON map at render time. In htmlc build, props come from sibling .json files and _data.json files in parent directories.
$ htmlc render -dir ./templates Card -props '{"title":"Hello"}'
// In Go
html, err := engine.RenderFragmentString("Card", map[string]any{
"title": "Hello",
})
Slots
Default slot:
<!-- In Card.vue -->
<slot>Fallback content</slot>
<!-- Usage -->
<Card title="Hello">
<p>This goes into 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>...</nav></template>
<p>Main content</p>
<template #footer><p>© 2024</p></template>
</Layout>
Scoped styles
Add scoped to <style> to keep styles contained to the component. The engine rewrites CSS selectors and adds a scope attribute to matching elements automatically.
<style scoped>
.card { background: white; }
p { color: gray; }
</style>
Becomes (approximately):
<style>
.card[data-v-3a2b1c] { background: white; }
p[data-v-3a2b1c] { color: gray; }
</style>