# Kiln > Kiln is a documentation-site generator written in Swift — type-safe config, localisation, theming, and client-side search. # Kiln **Kiln** is a documentation-site generator written in Swift. You describe your site in a type-safe Swift configuration and Kiln turns a directory of markdown into a fast, modern static website — no YAML, no Python toolchain. It was built to replace MkDocs-based documentation sites (such as [Vapor's](https://docs.vapor.codes)), so it covers the features those sites rely on: multi-language docs with fallback, a themeable UI, client-side search, admonitions, and more. !!! tip "New here?" Head to [Installation](getting-started/installation.md) and the [Quick Start](getting-started/quick-start.md) to get a site running in a couple of minutes — or scaffold one instantly with [`kiln new`](getting-started/cli.md). ## Why Kiln? - **Swift all the way down** — your whole site is a single, type-safe `KilnSite` value with a navigation result-builder DSL. Configuration mistakes are compile errors, not runtime surprises. - **Localised** — first-class multi-language support with automatic fallback to your default language, per-language navigation and UI strings, hreflang alternates, and a language switcher. - **Versioned** — ship multiple documentation versions with a built-in version switcher (try it in the top bar). The latest version lives at the root; older and pre-release versions get their own URLs and a banner. - **Not just docs** — the same engine builds a plain [static site](guides/static-sites.md) (a marketing or landing site) or a [blog](guides/blog.md) with posts, tags, author pages, and an RSS feed. - **Themeable** — ships with a fresh, responsive default theme (light/dark, sidebar nav, on-page table of contents). Override any part with your own [Leaf](https://github.com/vapor/leaf-kit) templates. - **Searchable** — a client-side search index is generated per language. No external service, no build-time JavaScript toolchain. - **SEO-ready** — canonical URLs, OpenGraph/Twitter cards, `sitemap.xml`, `robots.txt`, and a build-time [link checker](guides/link-checking.md). - **AI-friendly** — generates [`llms.txt`](guides/llms.md) and a raw-markdown copy of every page for tools and agents. ## A quick taste ```swift import Kiln let site = KilnSite(name: "My Docs", url: "https://example.com") { Page("Welcome", "index.md") Section("Guides") { Page("Configuration", "guides/configuration.md") } } try await Kiln.build(site, contentDirectory: "Content", outputDirectory: "public") ``` | Feature | Status | | ----------------------------------------------- | ----------- | | Type-safe Swift configuration | ✅ Supported | | Markdown + admonitions + front matter | ✅ Supported | | Localisation with fallback | ✅ Supported | | Client-side search (Unicode/CJK aware) | ✅ Supported | | Theming via Leaf templates | ✅ Supported | | SEO, social cards, sitemap, link checking | ✅ Supported | | `kiln` CLI (`new` / `build` / `serve`) | ✅ Supported | | Versioned documentation | ✅ Supported | | Static (marketing) sites | ✅ Supported | | Blog: posts, tags, authors, RSS feed | ✅ Supported | ## This site is built with Kiln Everything you're reading is generated by Kiln from the markdown in [`Examples/ExampleSite`](https://github.com/brokenhandsio/kiln/tree/main/Examples/ExampleSite). It doubles as the project's worked example — the best reference for how a real site is wired together. # Installation There are two ways to use Kiln, and most projects use both: 1. The **`kiln` command-line tool** — to scaffold, build, and preview sites. 2. The **`Kiln` Swift library** — the dependency your docs project builds against. ## Install the CLI With [Homebrew](https://brew.sh) (macOS, Apple Silicon): ```sh brew install brokenhandsio/tap/kiln ``` Or build it from source (any platform with a Swift toolchain, including Intel Macs and Linux): ```sh git clone https://github.com/brokenhandsio/kiln.git cd kiln swift build -c release cp .build/release/kiln /usr/local/bin/ # or anywhere on your PATH ``` Check it works: ```sh kiln --version ``` !!! tip The fastest way to start a new project is [`kiln new`](cli.md#kiln-new), which scaffolds the SwiftPM package, content directory, and a starter configuration for you. ## Add the library A Kiln site is a small SwiftPM executable that depends on the `Kiln` library. Add it to your `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/brokenhandsio/kiln.git", from: "0.1.0"), ], targets: [ .executableTarget( name: "Docs", dependencies: [ .product(name: "Kiln", package: "kiln"), ] ), ] ``` ## Requirements | Requirement | Version | | ----------- | -------------------------------- | | Swift | 6.2+ | | macOS | 13+ (Ventura) | | Linux | ✅ (any distribution Swift supports) | !!! note Kiln uses [swift-markdown](https://github.com/apple/swift-markdown) for parsing and [Leaf](https://github.com/vapor/leaf-kit) for templating. The bundled default theme ships as a package resource, so there's nothing extra to install. Next: build your first site in the [Quick Start](quick-start.md). # Quick Start This guide gets you from nothing to a live-previewing docs site. ## 1. Scaffold a project The quickest path is the CLI: ```sh kiln new my-docs cd my-docs ``` `kiln new` prompts for a site name, URL, and languages, then writes a ready-to-build SwiftPM project: ``` my-docs/ ├── Package.swift ├── Content/ │ └── index.md └── Sources/ └── MyDocs/ └── main.swift ``` ??? info "Prefer to do it by hand?" Create a SwiftPM executable, add the `Kiln` dependency (see [Installation](installation.md)), and write the two files below yourself. The CLI just saves you the boilerplate. ## 2. Configure the site `Sources/MyDocs/main.swift` is the single source of truth for your site: ```swift import Kiln let site = KilnSite(name: "My Docs", url: "https://example.com") { Page("Welcome", "index.md") Section("Guides") { Page("Configuration", "guides/configuration.md") } } try await Kiln.build(site, contentDirectory: "Content", outputDirectory: "site") ``` See the [Configuration](../guides/configuration.md) reference for every option. ## 3. Write some markdown Content is plain markdown under `Content/`. Create `Content/index.md`: ```markdown # Welcome Hello, world! ``` The path you give a `Page(...)` is the file's path relative to the content directory — see [Navigation](../guides/navigation.md). ## 4. Preview it ```sh kiln serve ``` This builds the site and serves it at , rebuilding whenever you edit a file. Reload your browser to see changes. !!! success "That's it!" When you're ready to publish, run `kiln build` and deploy the `site/` directory to any static host — see [Deployment](../guides/deployment.md). ## Admonition gallery Kiln supports MkDocs-style admonitions out of the box: !!! note This is a note. !!! tip This is a tip. !!! warning This is a warning. !!! danger This is a danger callout. ??? note "Collapsible" Use `???` for a collapsed admonition, or `???+` to start it expanded. # The `kiln` CLI Kiln ships a command-line tool that wraps the common workflows. Because a Kiln site is defined in Swift, `kiln` drives your project's own executable (via `swift run`) rather than reading a config file. Two conventions follow from that: - your build writes to `./site`, and - your content lives in `./Content`. Stick to those and every command works with no flags. See [Installation](installation.md) to get the binary. ## `kiln new` Scaffold a new documentation project. Interactive by default — it prompts for the site name, URL, default language, and any additional languages (from Kiln's built-in locale list): ```sh kiln new my-docs ``` Every prompt has a matching flag, so it's fully scriptable too: ```sh kiln new my-docs \ --name "My Docs" \ --url https://docs.example.com \ --default-language en \ --language de --language fr \ --non-interactive ``` `--language` is repeatable and also accepts comma-separated codes (`--language de,fr`). Unknown locale codes are rejected with the list of valid ones. ## `kiln serve` Build the site and serve it locally, rebuilding automatically when you edit a file: ```sh kiln serve # build, serve ./site at http://127.0.0.1:8080, watch for changes kiln serve --port 3000 # change the port kiln serve --directory public # serve a different output directory kiln serve --no-watch # build + serve once, no rebuild-on-change kiln serve --no-build # serve the existing output without building first ``` The watcher polls for changes (skipping `.build`, `.git`, `.swiftpm`, `node_modules`, and the output directory) and re-runs the build. Reload your browser to see updates. !!! note "No injected live-reload" `serve` rebuilds on change but doesn't auto-refresh the browser — reload to see your changes. This keeps the served output byte-for-byte identical to a production build. ## `kiln build` Build the site by running your project's executable: ```sh kiln build # writes the static site (to ./site by convention) kiln build --release # build in release configuration ``` ## Building assets first (`kiln.json`) If your site has an asset pipeline — say webpack or Sass compiled with `npm` — you'll want it to run *before* the site is generated, so the latest CSS/JS is picked up. Drop an optional `kiln.json` at the project root: ```json { "preBuild": { "command": "npm run build", "watch": ["src/scss", "src/js"] } } ``` Both `kiln build` and `kiln serve` then run `command` before the Swift build, streaming its output (so webpack warnings/errors are visible). During `kiln serve`, the directories in `watch` are watched alongside your project, so editing a stylesheet re-runs the asset build and regenerates the site. The `watch` paths are resolved relative to the project root and may point outside it (e.g. `"../design/src"` for a shared design repo). This is the one piece of CLI configuration Kiln reads from a file — it's a *tooling* concern, separate from your Swift-defined site config. With no `kiln.json`, behaviour is unchanged. You can override or skip the step per-run: ```sh kiln build --pre-build "make assets" # override the configured command kiln serve --no-pre-build # skip the asset step this run ``` ## Reference | Command | What it does | | ------------------ | -------------------------------------------------------- | | `kiln new [path]` | Scaffold a new project (interactive or via flags). | | `kiln build` | Run your project's executable to generate the site. | | `kiln serve` | Build, serve locally, and rebuild on change. | | `kiln --version` | Print the CLI version. | | `kiln --help` | Show help; `kiln --help` for a command. | !!! tip "Optional, not required" Everything the CLI does, you can also do with `swift run` plus any static file server. `kiln` is there for convenience and a familiar `mkdocs`-style workflow. # Configuration Everything about your site is described by a single `KilnSite` value. This page uses a custom front-matter `title` (see the browser tab) to demonstrate the `meta` feature. ## Fields | Field | Type | Notes | | ----------------- | --------------------- | ----- | | `name` | `String` | Site title. | | `url` | `String` | Canonical site URL (used for sitemap, OpenGraph, hreflang). | | `author` | `String?` | Used for meta tags. | | `description` | `String?` | Default meta/OpenGraph description. | | `image` | `String?` | Default social preview image (content-relative path). | | `twitterSite` | `String?` | Handle for the `twitter:site` tag (e.g. `"@brokenhandsio"`). | | `organization` | `Organization?` | Publisher entity → emits Organization + WebSite JSON-LD. `name`, `url`, `logo` (absolute URL, raster ≥112×112), `sameAs`. | | `repository` | `Repository?` | `name`, `url`, optional `editURI` for "edit this page" links. | | `copyright` | `String?` | Footer notice. | | `theme` | `Theme` | `.default(…)` or `.custom(directory:…)` — see [Theming](theming.md). | | `social` | `[SocialLink]` | Icon + link; shown in the header/footer. | | `carbonAds` | `CarbonAds?` | [Carbon Ads](https://www.carbonads.net) `serve`/`placement`. | | `extraCSS` | `[String]` | Extra stylesheets (content-relative). | | `extraJavaScript` | `[String]` | Extra scripts. | | `languages` | `[Language]` | See [Content & Localisation](content-and-localisation.md). | | `markdown` | `MarkdownExtensions` | Feature toggles + table-of-contents options. | | `navigation` | `@NavBuilder` | The nav tree — see [Navigation](navigation.md). | ## A fuller example ```swift let site = KilnSite( name: "My Docs", url: "https://docs.example.com", description: "Documentation for My Project.", repository: .init( name: "GitHub", url: "https://github.com/me/project", editURI: "https://github.com/me/project/edit/main/Content/" ), theme: .default(palette: .autoLightDark(primary: .black, accent: .blue)), social: [.init(icon: .github, link: "https://github.com/me/project")], languages: [ .init(.english, isDefault: true), .init(.german, navTranslations: ["Guides": "Anleitungen"]), ], navigation: { Page("Welcome", "index.md") Section("Guides") { Page("Configuration", "guides/configuration.md") } } ) try await Kiln.build(site, contentDirectory: "Content", outputDirectory: "site") ``` ## Languages Declare each language you support with a `LanguageCode` — a built-in case like `.english` or `.german`, or `.custom(code:name:)`. Exactly one language must be the default; missing translations fall back to it automatically. ```swift languages: [ .init(.english, isDefault: true), .init(.german, navTranslations: ["Guides": "Anleitungen"]), .init(.custom(code: "gd", name: "Gàidhlig")), ] ``` See [Content & Localisation](content-and-localisation.md) for the full story on translations, fallback, and UI strings. ## Theme ```swift theme: .default( palette: .autoLightDark(primary: .black, accent: .blue), logo: "assets/logo.svg", favicon: "assets/logo.svg" ) ``` `Color` accepts presets (`.black`, `.blue`, `.indigo`, …) or any CSS string via `Color("#2f6feb")`. To replace the theme entirely, use `.custom(directory:)` and see [Theming](theming.md). !!! note "Logical paths" Navigation references the *default-language* file path (`guides/configuration.md`). Kiln finds the right translation per language for you — you never hard-code locale-suffixed paths. # Static Sites Kiln isn't only for versioned documentation. The same engine builds a plain **static site** — a marketing site, a landing page, a portfolio — from a flat set of markdown pages. You opt in simply by *not* declaring versions: give the `KilnSite` a navigation closure of `Page`s and Kiln renders each one at a clean URL. ```swift import Kiln let site = KilnSite( name: "Acme", url: "https://acme.example", description: "We make excellent widgets.", theme: .default(palette: .autoLightDark(primary: .black, accent: .blue)) ) { Page("Home", "index.md") Page("Pricing", "pricing.md") Page("About", "about.md") } try await Kiln.build(site, contentDirectory: "Content", outputDirectory: "site") ``` Each `Page(_:_:)` maps a navigation label to a markdown file under the content directory. `index.md` becomes the site root (`/`); `pricing.md` becomes `/pricing/`. There's no version prefix and no version switcher — the URLs stay flat. ## Static site vs. versioned docs vs. blog A `KilnSite` takes one of three shapes, decided entirely by which argument you pass. Everything else — content, theming, SEO — is the same. | You're building… | Configure | Example URLs | | --- | --- | --- | | A marketing/landing site | `navigation: { Page(...) }` | `/`, `/pricing/`, `/about/` | | Docs with multiple released versions | `versions: [DocVersion(...)]` | `/`, `/2.0/`, `/0.9/` | | A blog of dated posts | `blog: Blog()` | see [Blog](blog.md) | A static site is the **default shape**: a `KilnSite` with neither `versions:` nor `blog:` set. [vapor.codes](https://www.vapor.codes) itself is built this way. ## Everything else still applies A static site shares the rest of Kiln unchanged — [theming](theming.md), [search](search.md), [SEO & social cards](seo.md), [link checking](link-checking.md), and [AI-friendly output](llms.md) all work identically. Per-page front matter (title, `description`, `image`) is honoured just as in docs; see [Content & Localisation](content-and-localisation.md). ## Localisation A static site is multilingual the same way the rest of Kiln is: pass `languages:` and translate a page with a locale suffix (`about.de.md`). Untranslated pages fall back to the default language. ```swift let site = KilnSite( name: "Acme", url: "https://acme.example", languages: [ .init(.english, isDefault: true), .init(.german), ] ) { Page("Home", "index.md") Page("About", "about.md") } ``` !!! tip "External links in the nav" A navigation closure can mix `Page`s with `Link`s to point at anything off the site — `Link("GitHub", "https://github.com/acme")`. # Blog Kiln can build a **blog**: a directory of dated markdown posts, rendered as individual post pages plus the listing pages a blog needs — a paginated index, per-tag pages, author pages, and an RSS feed. It's the engine behind [blog.vapor.codes](https://blog.vapor.codes). A blog is **additive** to a `KilnSite`: posts are discovered by scanning a directory, not declared in [navigation](navigation.md). Enable it by passing a `Blog`: ```swift import Kiln let site = KilnSite( name: "Vapor", url: "https://blog.vapor.codes", description: "News and articles from the Vapor team.", blog: Blog() ) try await Kiln.build(site, contentDirectory: "Content", outputDirectory: "site") ``` !!! note "Single-language, unversioned" A site with a blog must be single-language and have no `versions:` — this keeps post URLs free of version and locale prefixes. The blog index *is* the site root, so a blog site has no `index.md`. ## Writing a post Each `*.md` file in the posts directory (`posts/` by default, under your content directory) becomes a post at `/posts//`. The leading `# Heading` becomes the title and is stripped from the body. ```markdown --- date: 2024-09-03 09:00 description: A short summary used for cards, SEO, and the RSS feed. tags: swift, release author: tim image: /images/posts/my-post.png --- # My first post Everything below the title is the post body… ``` | Front matter | Purpose | | --- | --- | | `date` | Publish date, parsed with the blog's `dateFormat`. Sorts posts newest-first. | | `description` | Summary for cards, `` description, and the RSS feed. | | `tags` | Comma-separated; each becomes a tag page. | | `author` / `authorImageURL` | A single author (a registry [username](#authors) or a literal name). | | `authors` / `authorImageURLs` | Semicolon-separated, for co-authored posts. | | `image` | Per-post social/OpenGraph image (falls back to the site default). | ## What Kiln generates From the posts directory Kiln produces: | URL | Page | | --- | --- | | `/posts//` | Each post. | | `/`, `/2/`, … | Paginated index, newest-first. | | `/tags/`, `/tags//` | Tag directory and per-tag listings (paginated). | | `/authors/`, `/authors//` | Author directory and per-author listings (paginated). | | `/feed.rss` | RSS 2.0 feed. | Each post also carries `BlogPosting` + `BreadcrumbList` JSON-LD and is added to the sitemap and [search index](search.md). ## Authors Posts reference an author by `username` in front matter. Register authors on the `Blog` to give them a name, photo, bio, and social links — Kiln then builds an author page (`/authors//`) listing their posts, with `ProfilePage` / `Person` JSON-LD: ```swift blog: Blog( authors: [ Author( username: "tim", name: "Tim", imageURL: "/author-images/tim.jpg", description: "Core team member.", github: "0xTim", mastodon: "https://hachyderm.io/@0xtim", website: "https://timc.dev" ), ] ) ``` A post with `author: tim` then links its byline to that author page (matching is case-insensitive). The typed social fields — `github`, `twitter`, `mastodon`, `bluesky`, `linkedin`, `instagram`, `website`, plus a `links:` array for anything else — each render with the right icon. An `author:` value with no registry match falls back to a plain display name. ## Configuration `Blog()` works with sensible defaults; every option is overridable: | Option | Default | | | --- | --- | --- | | `postsDirectory` | `"posts"` | Where post markdown lives, under the content dir. | | `postsPerPage` | `8` | Posts per page on the index and tag/author listings. | | `feedTitle` / `feedDescription` | site name / description | RSS channel metadata. | | `indexTitle` | site name | Heading on the index. | | `tagsTitle` / `authorsTitle` | `"Tags"` / `"Authors"` | Headings on the directories. | | `dateFormat` | `"yyyy-MM-dd HH:mm"` | Parses each post's `date:` (fixed `en_US_POSIX`/UTC). | | `displayDateFormat` | `"d MMMM yyyy"` | How dates render on pages and cards. | | `readingWordsPerMinute` | `200` | Divisor for the reading-time estimate. | | `authors` | `[]` | The [author registry](#authors). | ## Theming The bundled theme ships templates for every blog page (`blog-post`, `blog-index`, `blog-tags`, `blog-author`, and partials). Override any of them with a [custom theme](theming.md) — drop a same-named `.leaf` file in your theme directory and Kiln uses yours instead. # Content & Localisation Content is plain markdown under your content directory (`Content/` by convention). The directory structure is up to you; navigation references files by their path. ## Translations Translations use a **locale suffix** on the filename: ``` Content/ ├── index.md # default language (e.g. English) ├── index.de.md # German translation of the home page └── guides/ ├── configuration.md └── configuration.de.md ``` `index.md` and `index.de.md` share the same **logical path** (`index.md`), which is what navigation references. You never put the locale in a `Page(...)` path — Kiln resolves the right file per language. ## Fallback When a page has no translation for a language, Kiln falls back to the default language's content and shows a small "translation unavailable" banner — the equivalent of mkdocs-static-i18n's `fallback_to_default: true`. !!! note "Try it" Most pages on this very site aren't translated into German. Switch the language with the picker in the header and you'll see the fallback banner in action, with the navigation and UI fully localised around the English body. The default language is built at the site root; other languages live under `//`: ``` site/ ├── index.html # default language ├── de/ │ └── index.html # German └── … ``` ## Interface strings The theme's own UI strings (search box, navigation labels, error page, …) are localised per language via `LocalisationConfiguration`. Any string left unset falls back to Kiln's built-in English default: ```swift .init(.german, localisation: .init( searchPlaceholder: "Suchen", searchNoResults: "Keine Ergebnisse gefunden", tableOfContentsTitle: "Auf dieser Seite", previousPage: "Zurück", nextPage: "Weiter", editPage: "Diese Seite bearbeiten", notFoundTitle: "Seite nicht gefunden" // … and more; see LocalisationConfiguration )) ``` The same configuration also carries optional labels for marketing-style navigation and footers — `home`, `store`, `blog`, `community`, `resources`, the theme/language/version picker labels, and more — for custom themes that render them with `#(strings.home)`. Kiln's bundled theme doesn't use these, so they're purely opt-in; each falls back to its English default when unset. ## Navigation labels & site name Section and page titles are translated per language via each `Language`'s `navTranslations` map (keyed on the default-language title), and a language can override the whole `siteName`: ```swift .init( .german, siteName: "Meine Doku", navTranslations: ["Guides": "Anleitungen", "Welcome": "Willkommen"] ) ``` ## Theme-defined strings `LocalisationConfiguration` covers Kiln's built-in chrome, but a custom theme often needs strings of its own — a tagline, a "Join our chat" link, a call to action. Define those per language in `customStrings` and look them up in a template with the `#localise("key")` tag: ```swift .init(.english, isDefault: true, customStrings: [ "tagline": "Type-safe documentation sites, built in Swift.", ]) .init(.german, customStrings: [ "tagline": "Typsichere Dokumentations-Websites, gebaut in Swift.", ]) ``` ```leaf

#localise("tagline")

``` A key missing from a language falls back to the **default language's** value, so you can translate incrementally; a key that's unknown everywhere renders as the key itself, making missing translations easy to spot. This site's own footer (`Theme/templates/partials/footer.leaf`) uses `#localise("tagline")` — switch the language and watch the tagline change with it. ## hreflang & the language switcher For every page, Kiln emits `` tags for each language and renders a language switcher in the header, so search engines and readers can find the right translation. # Navigation The navigation tree is built with a Swift result builder using three helpers: ```swift navigation: { Page("Welcome", "index.md") // a markdown page Section("Guides") { // a collapsible group Page("Configuration", "guides/configuration.md") Page("Theming", "guides/theming.md") } Link("API Reference", "https://example.com/api") // an external link } ``` ## The three building blocks | Helper | Purpose | | ------ | ------- | | `Page(_ title:, _ path:)` | A markdown page. `path` is relative to the content directory (the *logical* path — no locale suffix). | | `Section(_ title:) { … }` | A named, collapsible group of pages and/or nested sections. | | `Link(_ title:, _ url:)` | An external link rendered in the nav. | Sections can nest, so you can build multi-level navigation: ```swift Section("Guides") { Page("Overview", "guides/index.md") Section("Advanced") { Page("Theming", "guides/theming.md") } } ``` ## Logical paths A `Page` path always points at the **default-language** file, e.g. `guides/configuration.md`. Kiln resolves the correct translation (`guides/configuration.de.md`) per language automatically — see [Content & Localisation](content-and-localisation.md). ## What Kiln derives for you From this single tree Kiln computes, for every page and language: - the **active trail** (which section/page is highlighted), - **previous / next** links following document order, and - **breadcrumbs** and the on-page table of contents. ## Pages outside the navigation Some pages belong on the site but not in the sidebar — a legal notice, a privacy policy, a landing page you link to from a campaign. Declare those as `unlistedPages` instead of `Page` entries: ```swift KilnSite( name: "Kiln", url: "https://kiln.brokenhands.io", unlistedPages: [ UnlistedPage("Legal", "legal.md"), UnlistedPage("Draft", "draft.md", searchable: false, indexed: false), ] ) { Page("Welcome", "index.md") Section("Guides") { … } } ``` On a versioned site, `unlistedPages` goes on the `DocVersion` instead — each version has its own, exactly like `navigation`. An unlisted page is built like any other: the full theme, "pretty" URL, translations (`legal.de.md`), link checking, and an `

`-derived title. It's simply absent from the navigation tree, from the previous/next reading order, and from `llms.txt` (which mirrors the navigation). !!! note A markdown file that appears in *neither* the navigation nor `unlistedPages` isn't built at all. Declaring pages explicitly is what stops stray drafts in your content directory from being published by accident. Two flags control how discoverable an unlisted page is: | Flag | Default | Effect when `false` | | ---- | ------- | ------------------- | | `searchable` | `true` | Left out of the client-side [search](search.md) index. | | `indexed` | `true` | Left out of `sitemap.xml` and `llms-full.txt`, and rendered with ``. | The defaults suit a footer page you *want* found — a legal or privacy page. Setting both to `false` gives a page reachable only by direct link. This site has one: the **[Legal](../legal.md)** page linked in the footer. It's not in the sidebar, but it's a normal page in every other respect. ## Translating labels Section and page titles are translated per language with each `Language`'s `navTranslations` map, keyed on the default-language title: ```swift .init(.german, navTranslations: [ "Guides": "Anleitungen", "Configuration": "Konfiguration", ]) ``` !!! tip Only titles you include in `navTranslations` are translated; anything left out simply shows in the default language. There's no need to translate every label at once. # Markdown Kiln renders markdown with [swift-markdown](https://github.com/apple/swift-markdown) (CommonMark + GitHub-flavoured extensions). This page is also a live demo — everything shown here is rendered by Kiln. ## Headings & table of contents Every heading gets a GitHub-style slug `id` and a permalink anchor, and Kiln builds an on-page table of contents from them automatically. ## Code blocks Fenced code blocks are highlighted client-side with highlight.js: ```swift struct Greeter { let name: String func greet() -> String { "Hello, \(name)!" } } ``` Inline `code` works too. ## Tables | Syntax | Renders as | | ------------- | -------------- | | `**bold**` | **bold** | | `*italic*` | *italic* | | `` `code` `` | `code` | ## Lists - Unordered item - Another item - Nested item 1. Ordered item 2. Second item - [x] A completed task - [ ] An open task ## Admonitions MkDocs/Python-Markdown-style call-outs: ```markdown !!! tip "Optional title" Body content, rendered as markdown. ??? note "Collapsible" Hidden until expanded (use `???+` to start expanded). ``` Rendered: !!! tip "Optional title" Body content, rendered as markdown. ??? note "Collapsible" Hidden until expanded (use `???+` to start expanded). Supported types include `note`, `tip`, `warning`, `danger`, `info`, and `success`. ## Front matter An optional YAML block at the top of a file (the `meta` extension) overrides per-page metadata: ```markdown --- title: Custom Page Title description: Used for meta and social tags. image: assets/custom-card.png # per-page social preview image template: landing # override the Leaf template for this page --- ``` | Key | Effect | | ------------- | ------ | | `title` | Overrides the `` and heading-derived page title. | | `description` | Sets the meta/OpenGraph description for the page. | | `image` | Per-page social preview image (see [SEO & Social Cards](seo.md)). | | `template` | Renders the page with a different [Leaf template](theming.md). | !!! note This very page sets no front matter, but [Configuration](configuration.md) sets a custom `title` — compare their browser tabs. ## Links & images Standard markdown links and images work, and **intra-doc links between `.md` files are rewritten** to the correct pretty, locale-aware URLs at build time — see [Link Checking](link-checking.md). Relative image paths are resolved against the page's location and copied into the output. <!-- Source: https://kiln.brokenhands.io/guides/theming/ --> # Theming Kiln ships a fresh, responsive default theme as a package resource: light/dark colour schemes, a sidebar nav, an on-page table of contents, and search. You can tweak it with options, or replace any part with your own templates. ## Tweaking the default theme ```swift theme: .default( palette: .autoLightDark(primary: .black, accent: .blue), logo: "assets/logo.svg", favicon: "assets/logo.svg", fonts: .init(text: "Inter", code: "JetBrains Mono"), features: [.backToTop, .searchHighlight] ) ``` | Option | Purpose | | ---------- | ------- | | `palette` | `Palette` with `primary`/`accent` `Color`s and a default mode (`.auto`/`.light`/`.dark`). | | `logo` | Header logo (content-relative path). | | `favicon` | Site favicon. | | `fonts` | `Fonts(text:code:)` for body and code text. | | `features` | Opt-in extras: `.searchSuggest`, `.searchHighlight`, `.navigationTabs`, `.backToTop`. | `Color` has presets (`.black`, `.blue`, `.indigo`, …) or accepts any CSS string via `Color("#2f6feb")`. ## Bringing your own templates To customise the markup, point Kiln at a directory of your own [Leaf](https://github.com/vapor/leaf-kit) templates and assets: ```swift theme: .custom(directory: "Theme") ``` Templates resolve from **your directory first** and fall back to the bundled theme, so you only override what you need. The theme is split into small partials: ``` Theme/ ├── templates/ │ ├── base.leaf # overall page shell (<head>, header, layout, scripts) │ ├── page.leaf # a standard documentation page │ ├── home.leaf # the home page │ ├── 404.leaf # the error page │ └── partials/ │ ├── header.leaf │ ├── footer.leaf │ ├── nav-tree.leaf │ ├── toc.leaf │ ├── search.leaf │ ├── language-switcher.leaf │ └── social-icons.leaf ├── css/ └── js/ ``` ## Sharing templates across sites If several sites share the same look — a common header, footer, and cards — you don't want to copy those templates into every project. Ship them once from a shared Swift package as a bundled resource, then pull them in as a **shared theme layer**. In the shared package, bundle a theme directory (with a `templates/` folder) as a resource and expose its URL: ```swift // Package.swift .target(name: "DesignTheme", resources: [.copy("Theme")]) // DesignTheme.swift public enum DesignTheme { public static var directory: URL { Bundle.module.url(forResource: "Theme", withExtension: nil)! } } ``` Then each site lists it in `sharedLayers`: ```swift theme: .custom(directory: "Theme", sharedLayers: [DesignTheme.directory]) ``` Templates now resolve in order: **your site's `Theme/` → the shared layer(s) → Kiln's bundled default**. So a site overrides anything locally, falls back to the shared design for common partials, and falls back to Kiln's default for the rest. `sharedLayers` also works on `.default(sharedLayers:)` if you don't have a local theme directory. Assets (`css`/`js`) follow the same layering, with later layers overriding earlier ones. ## Template context Templates receive a context with `site`, `page`, `nav`, `language`, `languages`, `strings`, `customStrings`, and `searchIndexURL`. The rendered page body is injected with `#unsafeHTML(page.content)`. `strings` holds Kiln's localised UI strings for the current language — the search box, navigation/footer labels, error-page text, and so on — e.g. `#(strings.previousPage)` or `#(strings.home)`. `customStrings` holds your own theme-defined strings; rather than reaching into it directly, look strings up with the **`#localise("key")`** tag, which resolves against the current language and falls back to the default language: ```leaf <p class="tagline">#localise("tagline")</p> ``` See [Content & Localisation](content-and-localisation.md) for how to define both sets of strings per language. !!! tip "Per-page templates" A page can opt into a different template via the `template` front-matter key (see [Markdown](markdown.md)) — handy for a landing page or a differently laid-out reference. ## Extra CSS / JS For small additions you don't need a full custom theme — just add stylesheets or scripts (content-relative paths): ```swift extraCSS: ["assets/custom.css"], extraJavaScript: ["assets/custom.js"] ``` <!-- Source: https://kiln.brokenhands.io/guides/search/ --> # Search Kiln generates a search index (`search/search_index.json`) **per language** at build time. The bundled theme includes a small, dependency-free client that fetches the index and ranks results in the browser. - **No external service** — nothing to host or pay for, and it works offline. - **No build-time JavaScript toolchain** — the index is produced by Kiln itself. - **Per-language** — each language searches only its own content; the default language's content is indexed for pages that fall back. !!! tip "Try it" Use the search box in the header of this site. It indexes every page, including this one. ## Unicode & CJK Tokenisation is Unicode-aware: - accented Latin scripts are matched whole (so a search without accents still finds accented words), - Han/kana — which aren't space-separated — are segmented into bigrams, so CJK queries work. ## How fallback affects search Because untranslated pages fall back to the default language (see [Content & Localisation](content-and-localisation.md)), a language's search index covers every page in the navigation — translated pages use their own text, and fallback pages use the default-language text. Readers always get complete results. ## Search features Two opt-in theme features refine the experience: ```swift theme: .default(features: [.searchSuggest, .searchHighlight]) ``` | Feature | Effect | | ------------------ | ------ | | `.searchSuggest` | Show suggestions as you type. | | `.searchHighlight` | Highlight the matched term on the destination page. | <!-- Source: https://kiln.brokenhands.io/guides/seo/ --> # SEO & Social Cards Kiln aims to make the generated HTML as discoverable as possible, with no extra work on your part. This page sets a custom `image` in its front matter, so when it's shared the social preview uses that image. ## Per-page meta For every page Kiln emits: - a `<title>` (from the page heading or front-matter `title`), - a `<meta name="description">` (front-matter `description`, falling back to the site description), - a `<link rel="canonical">` pointing at the page's canonical URL, - `hreflang` alternates for each language. ## OpenGraph & Twitter OpenGraph and Twitter Card tags are generated for rich link previews: ```html <meta property="og:type" content="article"> <meta property="og:title" content="SEO & Social Cards"> <meta property="og:url" content="https://kiln.brokenhands.io/guides/seo/"> <meta property="og:image" content="https://kiln.brokenhands.io/assets/social-card.png"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:site" content="@brokenhandsio"> ``` The preview image resolves in this order: 1. the page's front-matter `image`, 2. the site-wide `image`, 3. (none) — the card renders without an image. !!! tip "Use a real raster image" Social scrapers (Slack, X, iMessage, …) handle PNG/JPG far more reliably than SVG, so set `image` to a `1200×630` PNG — like this site's `assets/social-card.png`. ## Sitemap & robots Every build writes: - `sitemap.xml` — all pages across all languages, with `hreflang` alternates, - `robots.txt` — pointing at the sitemap. ``` Sitemap: https://kiln.brokenhands.io/sitemap.xml ``` ## Verifying your domain You don't need a special file for search-console verification — add the meta tag your provider gives you via a [custom template](theming.md) or `extraCSS`/head hook. Most teams use DNS verification instead, which needs nothing in the site. <!-- Source: https://kiln.brokenhands.io/guides/link-checking/ --> # Link Checking Broken links are the most common documentation bug. Kiln validates your internal links against the **built** site, so you catch them before your readers do. ## What gets checked - intra-doc links between `.md` files (including localised `content.de.md` targets), - `#anchor` fragments (against the heading IDs Kiln generates), - relative asset references (images, downloads). External (`http(s)://`) links are left alone — checking those needs the network and is better handled by a separate periodic job. ## Intra-doc link rewriting You write ordinary relative markdown links between pages: ```markdown See the [Configuration](configuration.md) guide, or jump to [fallback](content-and-localisation.md#fallback). ``` At build time Kiln rewrites these to the correct **pretty, locale-aware** URLs (`/guides/configuration/`, `/de/guides/configuration/`, …). The same source link resolves to the right per-language destination automatically. ## Choosing the mode Pass a `linkChecking` mode to the build: ```swift // Warn (default): print issues, but still produce the site. try await Kiln.build(site, contentDirectory: "Content", outputDirectory: "site") // Error: fail the build if any internal link is broken. try await Kiln.build( site, contentDirectory: "Content", outputDirectory: "site", linkChecking: .error ) ``` | Mode | Behaviour | | --------- | --------- | | `.warn` | Print each issue; the build still succeeds. (Default.) | | `.error` | Print issues and throw `BrokenLinksError` — the build fails. | | `.off` | Skip link checking entirely. | !!! tip "Use `.error` in CI" Run your build with `.error` in continuous integration so a broken link fails the pipeline, and keep `.warn` locally for fast iteration. <!-- Source: https://kiln.brokenhands.io/guides/llms/ --> # AI-Friendly Output Documentation is increasingly read by tools and agents, not just people. Kiln generates machine-friendly output alongside the HTML, following the [llmstxt.org](https://llmstxt.org) convention. ## What's generated | File | Contents | | ----------------- | -------- | | `llms.txt` | An index: the site title, summary, and a curated list of page links (grouped by section). | | `llms-full.txt` | The full corpus — every page's markdown concatenated, with source markers. | | `…/index.md` | A raw-markdown copy of every page, sitting next to its `index.html`. | ## Translations When a version has more than one language, each language gets its own `llms.txt` index (the default language's at the root, others under `/<locale>/`, e.g. `/de/llms.txt`). Every index ends with a **Translations** section linking to the other languages' `llms.txt`: ```text ## Translations - [Deutsch](https://kiln.brokenhands.io/de/llms.txt) ``` The [llms.txt convention](https://llmstxt.org) only standardises the single root `/llms.txt`, so this is a best-effort discovery hook: a tool that reads the canonical index can follow the links to the localised ones. The full corpus (`llms-full.txt`) is emitted for the default language only, to avoid duplicating mostly-fallback content for every locale. ## Per-page markdown alternate Every HTML page advertises its markdown twin so tools can discover it: ```html <link rel="alternate" type="text/markdown" href="/guides/llms.md"> ``` So an agent can fetch the clean markdown source of any page by swapping the URL, without scraping HTML. ## Example `llms.txt` looks like this: ```text # Kiln > Kiln is a documentation-site generator written in Swift. ## Getting Started - [Installation](https://kiln.brokenhands.io/getting-started/installation/index.md) - [Quick Start](https://kiln.brokenhands.io/getting-started/quick-start/index.md) ## Guides - [Configuration](https://kiln.brokenhands.io/guides/configuration/index.md) ``` ## Toggling it AI output is on by default. Turn it off with: ```swift KilnSite(name: "My Docs", url: "https://example.com", llmsText: false) { … } ``` !!! note The index uses your navigation structure and the site description, so the better organised your nav and the clearer your `description`, the more useful `llms.txt` is. <!-- Source: https://kiln.brokenhands.io/guides/deployment/ --> # Deployment A Kiln build produces a plain directory of static files. There's no server runtime, so you can host it anywhere that serves static content. ## What a build produces ``` site/ ├── index.html # default language at the root ├── guides/configuration/index.html ├── de/ # other languages under /<locale>/ │ ├── index.html │ └── guides/configuration/index.html ├── search/search_index.json # per-language search index ├── 404.html # per-language error pages ├── index.md # raw-markdown copy of each page ├── _kiln/ # bundled theme assets (css/js) ├── sitemap.xml ├── robots.txt ├── llms.txt # AI/agent index ├── llms-full.txt └── … # your content assets, copied as-is ``` URLs are "pretty" (directory-style): `/guides/configuration/` rather than `/guides/configuration.html`. ## Build for production ```sh kiln build --release ``` …or call the library directly in your executable. Either way, the output lands in `site/` (by convention). ## Hosting | Host | How | | ------------------- | --- | | **GitHub Pages** | Build in CI, push `site/` to the `gh-pages` branch (or use the Pages action with the artifact). | | **Netlify** | Build command runs your executable; publish directory is `site`. | | **Cloudflare Pages**| Same — build command + `site` output directory. | | **Any static host / S3 / nginx** | Upload the contents of `site/`. | ## Continuous deployment A typical CI job: ```sh swift run Docs # or: kiln build --release # deploy the ./site directory with your host's CLI/action ``` !!! tip "Fail on broken links in CI" Build with `linkChecking: .error` (see [Link Checking](link-checking.md)) so a broken internal link fails the deploy instead of shipping. ## Custom domain & canonical URL Set `url` on your `KilnSite` to your final domain **before** deploying — it's used for canonical URLs, `sitemap.xml`, OpenGraph tags, and hreflang alternates. Configure the domain itself with your host (a `CNAME` for GitHub Pages, the dashboard for Netlify/Cloudflare). <!-- Source: https://kiln.brokenhands.io/legal/ --> # Legal This page is **unlisted**: it's built, themed, translated and searchable like any other page, but it doesn't appear in the navigation sidebar. It's reachable from the link in the site footer, or by anyone who has the URL. See [Navigation](guides/navigation.md#pages-outside-the-navigation) for how to declare one. ## Licence Kiln is released under the MIT licence. See the [LICENSE](https://github.com/brokenhandsio/kiln/blob/main/LICENSE) file in the repository for the full text. ## Trademarks Swift and the Swift logo are trademarks of Apple Inc. Kiln is not affiliated with or endorsed by Apple Inc.