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

CategoryOperators / Syntax
Arithmetic+ - * / % **
Comparison=== !== > < >= <= == !=
Logical&& || !
Nullish coalescing??
Optional chainingobj?.key arr?.[i]
Ternarycondition ? then : else
Member accessobj.key arr[i] arr.length
Function callsfn(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

DirectiveSupportedDescription
v-ifYesRenders element only when expression is truthy
v-else-ifYesMust follow v-if or v-else-if
v-elseYesMust follow v-if or v-else-if
v-forYesRepeats element for each item
v-showYesToggles display:none
v-bindYesDynamically binds attribute or prop
v-htmlYesSets inner HTML (unescaped)
v-textYesSets text content (HTML-escaped)
v-preYesSkips interpolation and directives for element and descendants
v-switch / v-caseYesSwitch/case conditional; use with v-case and v-default on child elements
v-slotYesNamed and scoped slots
v-modelStrippedClient-side only; removed from output
@eventStrippedClient-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>&copy; 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>