Homebrew tap for kiln, a template compiler for GFI-like syntax.
brew install teehemkay/kiln/kilnBuild static website prototypes from small, reusable HTML pieces — no build config, no gulpfile.js, no framework. Kiln is a command-line template compiler for GFI-like (gulp-file-include) syntax: you write plain HTML sprinkled with @@ directives — includes, variables, conditionals, loops — and kiln resolves them into finished HTML you can open in a browser or hand to a client.
Kiln ships through two channels:
- Homebrew (macOS / Linux) installs a self-contained binary — no Java, no Node:
brew install teehemkay/kiln/kiln
- npm (macOS / Linux / Windows) installs
@webmaster/kiln, a JVM-uberjar wrapper that needs JDK 17+ on yourPATH:It lives on the European Parliament GitLab npm registry (projectnpm i -g @webmaster/kiln
ep/dg-comm/webmaster/sef/kiln), not public npm, so point the@webmasterscope at it once first — the project page walks through the one-time scope and token setup.
Then confirm it's on your PATH:
kiln --versionWe'll build a tiny two-page site — a home page and a programme page — that share a <head> and a nav bar, with the programme built by repeating one session card over a list of sessions. Then we'll preview it live. Create this layout:
site/
└── src/templates/
├── pages/ ← complete pages (these get built)
│ ├── index.html
│ └── program.html
└── partials/ ← fragments (included, never built on their own)
├── head.html
├── nav.html
└── session-card.html
The rule that makes this work: pages live under pages/, fragments under partials/. Kiln builds the .html files under pages/ — partials are pulled in by @@include / @@loop and never rendered on their own. (src/templates/pages and src/templates/partials are the defaults, so commands need no layout flags.)
src/templates/pages/index.html — owns the <head>, fills it from a partial, and hands each include some data:
<!DOCTYPE html>
<html lang="en">
<head>
@@include('head.html', {title: 'Global Entrepreneurship Week'})
</head>
<body>
@@include('nav.html', {active: 'home'})
<main>
<h1>One week. Thousands of events. Everywhere.</h1>
<p>Welcome to GEW.</p>
</main>
</body>
</html>src/templates/pages/program.html — reuses the same partials, then repeats a card for every session via @@loop:
<!DOCTYPE html>
<html lang="en">
<head>
@@include('head.html', {title: 'Programme'})
</head>
<body>
@@include('nav.html', {active: 'program'})
<main>
<h1>Full programme</h1>
<ul class="sessions">
@@loop('session-card.html', [
{name: 'Founder AMA', from: '18:00', to: '18:45', online: true},
{name: 'Pitch Night', from: '19:30', to: '21:00', online: false},
{name: 'Investor Panel', from: '17:00', to: '18:00', online: true}
])
</ul>
</main>
</body>
</html>Each @@include / @@loop names its target by the path under partials/ — head.html, not partials/head.html.
src/templates/partials/head.html — holds the head's contents (the page already provides the <head> element), and uses the title it was handed:
<meta charset="utf-8">
<title>@@title — GEW</title>
<link rel="stylesheet" href="/assets/site.css">src/templates/partials/nav.html — drops the active value into an attribute so CSS can highlight the current page:
<nav data-active="@@active">
<a href="/index.html">Home</a>
<a href="/program.html">Programme</a>
</nav>src/templates/partials/session-card.html — rendered once per object in the loop. Each object's keys (name, from, to, online) become that iteration's variables, and @@when / @@unless branch on them:
<li class="session">
<h3>@@name</h3>
<p class="time">@@from – @@to</p>
@@when(online)
<span class="tag">Online</span>
@@end-when
@@unless(online)
<span class="tag tag-venue">In person</span>
@@end-unless
</li>From inside the project (cd site):
kiln dev -o buildKiln renders the pages into build/, opens them in your browser, and re-renders on every save with live-reload — edit a partial, watch every page that uses it update. This is the loop you'll spend most of your time in. (dev annotates the output with data-kiln-* provenance attributes — data-kiln-source, data-kiln-chain, data-kiln-projection — so you can trace each element back to its template; add --no-annotate to turn that off.)
program.html's loop compiles to plain HTML — one <li> per session, no @@ left behind (kiln keeps the source whitespace, so the dropped @@unless branches leave blank lines):
<ul class="sessions">
<li class="session">
<h3>Founder AMA</h3>
<p class="time">18:00 – 18:45</p>
<span class="tag">Online</span>
</li>
<li class="session">
<h3>Pitch Night</h3>
<p class="time">19:30 – 21:00</p>
<span class="tag tag-venue">In person</span>
</li>
<li class="session">
<h3>Investor Panel</h3>
<p class="time">17:00 – 18:00</p>
<span class="tag">Online</span>
</li>
</ul>That's the whole idea: write a piece once, reuse it everywhere, feed it data.
Everything above is built from six @@ directives. All of them start with @@, are case-sensitive, and take no space before (.
Splices another file in place. The path is the partial's name under partials/ — independent of which file does the including — in single or double quotes. No ./, no ../, no leading /.
@@include('nav.html')The object becomes the partial's variables. This is how one partial serves many pages:
@@include('head.html', {title: 'Programme', year: 2026})Inside head.html, @@title resolves to Programme and @@year to 2026.
A variable is @@ + a lowercase name (letters, digits, interior hyphens — @@page-title is one name). It's filled from the data the partial was given, or from the current loop item.
<title>@@title</title>
<body class="@@theme">A variable with no value left to fill is reported as a warning when you build — so missing data never silently vanishes (add --strict to turn that warning into a failed build).
Brace form — @@{name} disambiguates where a name runs into literal text. Because hyphens are part of a name, @@slug-thumb reads as one variable; braces mark where the name ends:
<img src="/img/@@{slug}-thumb.jpg">
<!-- slug = "founder-ama" → /img/founder-ama-thumb.jpg -->Wrap content between the directive and its @@end-…. @@when keeps the block when the value is truthy; @@unless keeps it when falsy. (Falsy = false, null, 0, ""; everything else is truthy.) Condition names are letters/digits only — no hyphens here.
@@when(online)
<span class="tag">Online</span>
@@end-when
@@unless(soldout)
<a href="/register">Register</a>
@@end-unlessThey nest:
@@when(featured)
<article class="hero">
@@unless(online)
<p>In-person only</p>
@@end-unless
</article>
@@end-whenRenders a partial once per object in an array (at least one object). Each object's keys become that iteration's variables. Keys are unquoted and strings single-quoted — the JavaScript-object style gulp-file-include uses; double-quoted JSON ({"name": "Founder AMA"}) works too.
<ul>
@@loop('session-card.html', [
{name: 'Founder AMA', from: '18:00', to: '18:45', online: true},
{name: 'Pitch Night', from: '19:30', to: '21:00', online: false}
])
</ul>Look again at the two pages above: they share a whole shell — the same <head> include, the same nav, the same <main> wrapper. Only three things differ between them: the title, the active nav item, and the main content. Factor the shell into one partial and let each page supply just those three.
A layout partial holds the shared chrome and marks each varying region with @@slot(name):
src/templates/partials/layout.html
<!DOCTYPE html>
<html lang="en">
<head>
@@include('head.html', {title: '@@title'})
</head>
<body>
@@include('nav.html', {active: '@@active'})
<main>
@@slot(main)
</main>
</body>
</html>Each page becomes a block include of the layout: it passes the scalar values as data and fills the slot with @@fill(name) … @@end-fill, closing the block with @@end-include.
src/templates/pages/index.html
@@include('layout.html', {title: 'Global Entrepreneurship Week', active: 'home'})
@@fill(main)
<h1>One week. Thousands of events. Everywhere.</h1>
<p>Welcome to GEW.</p>
@@end-fill
@@end-includesrc/templates/pages/program.html
@@include('layout.html', {title: 'Programme', active: 'program'})
@@fill(main)
<h1>Full programme</h1>
<ul class="sessions">
@@loop('session-card.html', [
{name: 'Founder AMA', from: '18:00', to: '18:45', online: true},
{name: 'Pitch Night', from: '19:30', to: '21:00', online: false},
{name: 'Investor Panel', from: '17:00', to: '18:00', online: true}
])
</ul>
@@end-fill
@@end-includeThe chrome now lives in one place. index.html renders to the same page as before:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Global Entrepreneurship Week — GEW</title>
<link rel="stylesheet" href="/assets/site.css">
</head>
<body>
<nav data-active="home">
<a href="/index.html">Home</a>
<a href="/program.html">Programme</a>
</nav>
<main>
<h1>One week. Thousands of events. Everywhere.</h1>
<p>Welcome to GEW.</p>
</main>
</body>
</html>Three rules make this work:
- A slot name is a bare identifier —
@@slot(main), not@@slot('main'). Lowercase letters and digits, no quotes, no hyphens. Each@@fill(main)matches the slot of the same name. - Data handed to the include becomes the layout's variables.
{title: 'Programme'}makes@@titleresolve toProgrammeinsidelayout.html; writing{title: '@@title'}forwards that value on intohead.html, so one value threads down the whole chain of includes. - A block include's body holds only fills. Every contribution sits inside a
@@fill … @@end-fill; text left loose between the fills is an error.
A @@slot placed inside a @@fill re-exposes the hole, forwarding projection further down the chain — so layouts can nest inside layouts.
HTML won't let you put @@ in a tag name or in a bare attribute, so kiln adds two escape hatches.
A tag name from a variable — <kiln-tag kiln-tag="@@var">. Write a kiln-tag element carrying a kiln-tag attribute; kiln renames the element to that attribute's value, drops the attribute, and keeps everything else:
<kiln-tag kiln-tag="@@region" class="callout">
<p>Chosen at render time.</p>
</kiln-tag>With region = section, that becomes:
<section class="callout">
<p>Chosen at render time.</p>
</section>A conditional attribute — kiln-attr-<name>="@@var". kiln emits the attribute <name> from the resolved value, which lets you drive attributes HTML would otherwise force you to hard-code:
<input type="checkbox" kiln-attr-checked="@@selected">
<a href="/now" kiln-attr-aria-current="@@here">Today</a>The value decides what appears:
"true"→ a bare boolean attribute:selected=truegives<input type="checkbox" checked>.- empty or unresolved → the attribute is dropped entirely.
- anything else →
name="value":here=pagegives<a href="/now" aria-current="page">Today</a>.
You met kiln dev above. The rest, run from the project root, as you'll actually use them:
# Build the site once into ./build (the pages, not the partials)
kiln render -o build
# About to share a draft? Confirm every include resolves and no partial
# is missing — writes nothing, just checks.
kiln preflight
# Serve a folder you already built, with live-reload
kiln serve buildrender— compile templates to finished HTML (-o <dir>).dev— render, watch, and serve with live-reload (-o <dir>).serve— serve an already-built directory with live-reload.preflight— validate templates without writing anything: every include resolves, no partial missing.check— report where a template's re-serialized HTML differs from its source (roundtrip fidelity).parse— write each template's parsed syntax tree as EDN (-o <dir>).normalize— migrate a legacy-layout project to the defaultpages/+partials/layout (writes only with-o).version— print kiln's version (alsokiln --version).
The template commands — render, preflight, check, parse, dev — take --root <dir> (project root, default the current directory), plus --pages / --includes to point at a non-default layout. render, preflight, and dev add --strict (turn warnings into a failed build); render and dev add --annotate. serve and dev take --port and --no-browser. Run kiln help <command> (or just kiln) for every flag and examples.
MIT (MIT). The full license text ships in the package's LICENSE file.