Fast, safe templates for server-rendered websites.
Kiln is a template language with syntax you already know. Templates load their own data, output is escaped by default, and it runs on Node and Cloudflare Workers.
- Zero dependencies
- Strict by default
- MIT licensed
{% set product = document("product", slug) %}
<h1>{{ product.name }}</h1>
<p>{{ product.price|formatNumber(style: "currency", currency: "GBP") }}</p>
{% island "components/stock" with { id: product.id } %}
<p>Checking stock…</p>
{% endisland %}
How it works
Write a template. Get clean HTML.
Components take props and slots, loops know where they are, and data loads where it is used. Every example on this site is rendered by Kiln when the site builds.
{% set posts = documents("post", limit: 2) %}
{% for post in posts %}
{% component "components/card" with {
title: post.title,
href: post.url,
date: post.publishedAt,
} %}
<p>Post {{ loop.index }} of {{ loop.length }}</p>
{% endcomponent %}
{% endfor %}
{% props title, href, date = null %}
<article class="card">
<h2><a href="{{ href }}">{{ title }}</a></h2>
{% if date %}
<time>{{ date|formatDate(dateStyle: "long") }}</time>
{% endif %}
{% slot %}{% endslot %}
</article>
<article class="card">
<h2><a href="/blog/hello-world/">Hello, world</a></h2>
<time>October 1, 2026</time>
<p>Post 1 of 2</p>
</article>
<article class="card">
<h2><a href="/blog/layouts/">Layouts in Kiln</a></h2>
<time>October 8, 2026</time>
<p>Post 2 of 2</p>
</article>
-
Familiar syntax
If you have used Twig, Jinja or Liquid, you can read Kiln already. Names are camelCase, like JavaScript.
-
Async data
Templates call async functions to load what they need. Kiln only waits where a call returns a promise, and runs calls that don't depend on each other at the same time.
-
Runs anywhere
No eval, no Node built-ins and no dependencies in the core. Kiln runs on Node and Cloudflare Workers.
-
Safe by default
Templates only read what you give them, and output is escaped. Limits stop runaway templates.
-
Layouts and components
Share layouts with blocks. Build components with typed props and slots. Keep small snippets in macros.
-
Errors that help
Every error names the template, line and column, shows the code around it and suggests a fix.
Compared
Built in, not bolted on.
Async data, islands, streaming and typed props come with Kiln. Here is how it compares with Twig and the most popular JavaScript template languages.
- Has it
- Partly. Hover for details
- Doesn't have it
| Feature | Kiln | Twig | Nunjucks | LiquidJS | Handlebars |
|---|---|---|---|---|---|
| Components with named slots | Yes | Yes | Partly: One slot | No | Partly: One slot |
| Props with types and defaults | Yes | Partly: Types aren't checked | No | No | No |
| Safe for templates you didn't write | Yes | Partly: Opt-in sandbox | No | Yes | Partly: Blocks prototypes |
| Async data | Yes | No | Partly: Callback API | Yes | No |
| Streaming | Yes | Yes | No | Partly: Node streams only | No |
| Server islands | Yes | No | No | No | No |
| Runs on Cloudflare Workers | Yes | No | Partly: Precompiled only | Yes | Partly: Precompiled only |
| Types generated from templates | Yes | No | No | No | No |
Performance
Fast where it counts.
We render the same 7 real pages with 8 JavaScript template engines, and check every engine's HTML matches before timing. Kiln precompiles templates at build time, so it stays fast on Cloudflare Workers too.
- faster than Nunjucks
- 5.4×
- faster than LiquidJS
- 62×
- faster than Twing
- 49×
Geometric mean across the pages, leaving out CMS data, which mostly measures waiting for data. Apple M4 Pro, Node 24.22.0.
See every result and how we measureEngineRenders per second, higher is better
- Kiln 113.4K
- Pug 55.1K
- Eta 50.2K
- Handlebars 27.8K
- Nunjucks 25.8K
- EJS 20.1K
- Twing 3.8K
- LiquidJS 1.3K
A full article page: a layout with blocks, header and footer partials, trusted body HTML, related posts and twenty escaped comments.
EngineRenders per second, higher is better
- Pug 197.9K
- Kiln 195.5K
- Eta 166.2K
- Handlebars 34.1K
- Nunjucks 25.6K
- EJS 17.5K
- Twing 4.1K
- LiquidJS 3.9K
A page built from ten content sections, where each section's type picks the partial that renders it.
EngineRenders per second, higher is better
- Kiln 46.6K
- Eta 40K
- Pug 26.4K
- Nunjucks 7K
- Handlebars 6.9K
- EJS 4.3K
- Twing 828.8
- LiquidJS 762.2
A shop category page that renders a product card component 48 times, with sale prices, badges and star ratings.
EngineRenders per second, higher is better
- Kiln 18.6K
- Eta 11K
- Pug 7.9K
- Handlebars 3K
- Nunjucks 2.5K
- EJS 1.7K
- Twing 246.8
- LiquidJS 219.6
An admin table with 500 rows and 6 columns. Mostly loop and output speed.
EngineRenders per second, higher is better
- Kiln 126.8K
- Eta 74.3K
- Pug 68.3K
- Handlebars 35.4K
- Nunjucks 29.6K
- EJS 24.4K
- LiquidJS 3.5K
- Twing 3.2K
25 search results that run text through filters: truncate, join, sort, case changes, defaults, URL encoding and number formatting.
EngineRenders per second, higher is better
- Kiln 155.2K
- Eta 117.5K
- Nunjucks 43.9K
- EJS 32.2K
- Twing 2.6K
- LiquidJS 2.3K
- Handlebars Can't wait for async data
- Pug Can't wait for async data
A blog index that loads its own posts, categories and each post's author by calling async functions from the template.
EngineRenders per second, higher is better
- Kiln 384.9
- Eta 195.7
- EJS 98.2
- Nunjucks 96.7
- LiquidJS 87.7
- Twing 87.2
- Handlebars Can't wait for async data
- Pug Can't wait for async data
A blog post from a headless CMS. The page loads its post and the latest posts, and the layout loads the header and footer menus. Each call waits 2 ms, as a database query would.
Errors
Mistakes point straight at the problem.
Strict mode is on by default, so a typo is an error instead of a blank space on the page. Errors show where they happened and suggest what you meant.
Read about strict modeKilnRenderError: Unknown variable "titel" in pages/page.kiln
at line 4, column 9. Did you mean "title"?
3 | {% block content %}
> 4 | <h1>{{ titel }}</h1>
| ^^^^^
5 | {% endblock %}