# Documentation
> Full text of 63 page(s) across 4 book(s), for language-model ingestion.
# Authoring KD Help Books
A **KD Help Book** is a folder of Markdown pages plus a small `docset.toml` manifest,
compiled by the `khb` CLI into a single-file **`.khb` docset** that the KD Help Book
Viewer renders — with a table of contents, a keyword index, full-text search, and
offline reading. Pages are rendered to HTML **once, at build time**; the viewer never
runs a Markdown engine, so what you compile is exactly what readers see.
This book is the author's guide: everything you can put in a source folder, and what
each piece does in the viewer.
## How this guide is organized
- **[Getting started](md/khb-authoring/getting-started.md)** — a minimal book, from an empty folder to a
`.khb` open in the viewer.
- **[Differences from GitHub Markdown](md/khb-authoring/differences.md)** — what KD Help Book adds on top
of GFM, and the few things it deliberately doesn't render.
- **[Compiling a book](md/khb-authoring/compiling.md)** — the `khb compile` command, its options, and the
write–compile–preview loop.
- **Reference** — one page per construct and field:
- **Markdown** — the GFM core: [headings](md/khb-authoring/headings.md),
[text formatting](md/khb-authoring/text-formatting.md), [lists](md/khb-authoring/lists.md), [links](md/khb-authoring/links.md),
[images & assets](md/khb-authoring/images.md), [tables](md/khb-authoring/tables.md), [blockquotes](md/khb-authoring/blockquotes.md),
[code blocks](md/khb-authoring/code-blocks.md), [footnotes](md/khb-authoring/footnotes.md), [emoji](md/khb-authoring/emoji.md).
- **Markdown extensions** — the KD Help Book additions sit with their base
construct: [galleries](md/khb-authoring/images.md) (in Images & assets), [callouts](md/khb-authoring/blockquotes.md)
(in Blockquotes), and the [code extensions](md/khb-authoring/code-blocks.md) (in Code blocks);
plus [math](md/khb-authoring/math.md), [diagrams](md/khb-authoring/diagrams.md), and [directives](md/khb-authoring/directives.md).
- **[Frontmatter](md/khb-authoring/frontmatter.md)** — the per-page metadata fields.
- **[docset.toml](md/khb-authoring/docset-toml.md)** — the book manifest.
- **[toc.yaml](md/khb-authoring/toc-yaml.md)** — the table-of-contents file.
## Quick links
| I want to… | Read |
|------------|------|
| Build my first book | [Getting started](md/khb-authoring/getting-started.md) |
| Link between pages and books | [Links](md/khb-authoring/links.md) |
| Bundle images or downloadable files | [Images & assets](md/khb-authoring/images.md) |
| Put tabs, terminals, or file trees around code | [Code blocks](md/khb-authoring/code-blocks.md) |
| Get a page into the keyword index | [keywords (frontmatter)](md/khb-authoring/frontmatter-keywords.md) |
| Shape the table of contents | [toc.yaml](md/khb-authoring/toc-yaml.md) |
| Publish the compiled book as a site | [Getting published](khb-publishing:getting-published) |
> [!NOTE]
> This book eats its own dog food: every page in `docs/authoring/` carries the
> frontmatter it documents, and the folder compiles straight into a `.khb`
> (`khb compile docs/authoring -o authoring.khb`) — quite possibly the copy you are
> reading now.
---
# Getting started
A KD Help Book starts as a plain folder. This page takes you from an empty directory
to a compiled `.khb` open in the viewer.
## 1. Create the source folder
A book needs only two files — a manifest and a page:
~~~code-tree
```toml [my-docs/docset.toml]
id = "my-docs"
title = "My Documentation"
version = "0.1.0"
language = "en"
```
```md [my-docs/welcome.md]
---
title: Welcome
keywords: [welcome, introduction]
---
# Welcome
The first page of **my book**. Plain GitHub-flavoured Markdown works as-is.
```
~~~
Those four manifest fields are the core; [docset.toml](md/khb-authoring/docset-toml.md) covers the rest.
Every `*.md` file in the folder becomes a page whose id is its file name —
`welcome.md` → `welcome` — and the YAML block on top is the page's optional
[frontmatter](md/khb-authoring/frontmatter.md).
## 2. Compile it
Point `khb compile` at the folder and name the output:
~~~code-preview
```bash
khb compile my-docs -o my.khb
```
```
compiled my-docs (1 pages, language en) -> my.khb
```
~~~
The compiler validates the book as it builds — broken table-of-contents ids, unknown
`related` pages, or malformed math fail the compile rather than shipping broken. See
[Compiling a book](md/khb-authoring/compiling.md) for the options and the full list of checks.
## 3. Open it in the viewer
In the KD Help Book Viewer, choose **File → Open docset…** and pick `my.khb` — or
simply **drag the file onto the window**. The book appears in the Contents tree, its
pages join the Index and Search, and it's remembered for your next visit.
## Next steps
- Add more pages, then shape the tree with a [toc.yaml](md/khb-authoring/toc-yaml.md).
- Fill in [keywords](md/khb-authoring/frontmatter-keywords.md), [categories](md/khb-authoring/frontmatter-categories.md) and
[related](md/khb-authoring/frontmatter-related.md) so the Index, the category filter and the See-also
footers light up.
- Bundle images and downloads under `assets/` — see [Images & assets](md/khb-authoring/images.md).
- Ready to put the book on a website? Continue with
[Getting published](khb-publishing:getting-published) in *Publishing KD Help Books*.
---
# Differences from GitHub Markdown
The baseline is **GitHub-flavoured Markdown**: tables, task lists, strikethrough,
autolinks and footnotes all work exactly as on GitHub. On top of that KD Help Book
adds book-aware constructs — and deliberately refuses a few things GFM tolerates.
## What KD Help Book adds
| Addition | Looks like | Reference |
|----------|------------|-----------|
| In-book & cross-book page links | `[label](page-id)`, `[label](book:page)` | [Links](md/khb-authoring/links.md) |
| Bundled images & downloads | `` | [Images & assets](md/khb-authoring/images.md) |
| Callouts | `> [!NOTE]` | [Blockquotes](md/khb-authoring/blockquotes.md) |
| Math, rendered to MathML at build time | `$E = mc^2$`, `$$…$$` | [Math](md/khb-authoring/math.md) |
| Code: `[filename]` bar, copy button, `collapse` / `open` flags | `` ```rust [main.rs] collapse `` | [Code blocks](md/khb-authoring/code-blocks.md) |
| Code groups (tabs), command+output panels, file trees | `~~~code-group`, `~~~code-preview`, `~~~code-tree` | [Code blocks](md/khb-authoring/code-blocks.md) |
| Emoji shortcodes | `:tada:` | [Emoji](md/khb-authoring/emoji.md) |
| Highlight, underline, insert, super-/subscript | `==x==`, `__x__`, `++x++`, `^x^`, `~x~` | [Text formatting](md/khb-authoring/text-formatting.md) |
| Page metadata: keyword index, category facet, See-also footer | YAML frontmatter | [Frontmatter](md/khb-authoring/frontmatter.md) |
| Heading anchors + the "On this page" box | automatic | [Headings](md/khb-authoring/headings.md) |
## What is *not* supported
| Not rendered | Use instead |
|--------------|-------------|
| **Raw inline HTML** — escaped to literal text (`x` shows as-is) | Markdown + the extensions above — see [Text formatting](md/khb-authoring/text-formatting.md) |
| Attribute syntax (`{.class}` on a span or block) | nothing — there are no inline components |
| MDC / `:::` directive blocks | fence flags and `~~~` blocks — see [Code blocks](md/khb-authoring/code-blocks.md) |
| Remote images (``) — never fetched | bundle the file under `assets/` — see [Images & assets](md/khb-authoring/images.md) |
| `__x__` as bold, single-tilde `~x~` as strikethrough | those marks mean **underline** / **subscript** here — bold is `**x**`, strikethrough `~~x~~` — see [Text formatting](md/khb-authoring/text-formatting.md) |
> [!NOTE]
> These are deliberate design decisions, not gaps — the reasoning is described in
> [Security model](khb-internals:security-model) in *KD Help Book Internals*.
---
# Compiling a book
`khb compile` turns a source folder into a docset:
```bash
khb compile my-docs -o my.khb
```
## Options
| Flag | Meaning |
|------|---------|
| `-o ` | where to write the compiled docset |
| `--format khbb` | emit the minimal `.khbb` binary form instead of the default `.khb` (smaller to transfer; rebuilt into a `.khb` before use) |
| `--assets sidecar` | write attachments to a sibling `.khba` pack instead of embedding them in the `.khb` — see [Images & assets](md/khb-authoring/images.md) |
## What the compiler validates
A broken book fails the compile instead of shipping broken:
- every `page:` id in [toc.yaml](md/khb-authoring/toc-yaml.md) must name an existing page, and a folder
node must have a `title:`;
- every in-book id in a page's [`related`](md/khb-authoring/frontmatter-related.md) list must exist
(cross-book `docsetId:pageId` entries are stored as-is — the other book compiles
separately);
- every [math](md/khb-authoring/math.md) formula must parse — the error names the page and the offending
LaTeX;
- the [code](md/khb-authoring/code-blocks.md) containers must be well-formed: a
`~~~code-group` or `~~~code-tree` with no inner blocks, or a `~~~code-preview`
missing its command or output block, is a build error;
- the YAML [frontmatter](md/khb-authoring/frontmatter.md) block must parse (and be terminated).
## The write–compile–preview loop
Compiling is fast enough to keep in the inner loop: edit a page, re-run
`khb compile`, and re-open the `.khb` in the viewer (drag it onto the window —
re-opening a book replaces the loaded copy). For a quick sanity check without the
viewer, `khb inspect` prints a docset's metadata and table-of-contents summary:
```bash
khb inspect my.khb
khb inspect my.khb --json # for CI and other automation
```
## From a docset to a website
`compile` builds one book. Assembling a publishable site — a built viewer plus one or
more docsets and their manifest — is the job of [`khb pack`](khb-publishing:pack), and
updating a published site in place is [`khb patch`](khb-publishing:patch); both are
documented in *Publishing KD Help Books*.
---
# Headings & paragraphs
Use `#` for headings — one to six `#` for levels 1–6. A blank line separates
paragraphs; a single newline inside a paragraph is treated as a space.
~~~code-preview example
```md
# Page title (H1)
## A section
### A subsection
Regular paragraph text. This second line joins the
same paragraph because there is no blank line between them.
A new paragraph starts after a blank line.
```
```md
# Page title (H1)
## A section
### A subsection
Regular paragraph text. This second line joins the
same paragraph because there is no blank line between them.
A new paragraph starts after a blank line.
```
~~~
The **first H1** is used as the page title when the frontmatter has no explicit
`title` — see [title (frontmatter)](md/khb-authoring/frontmatter-title.md). Keep exactly one H1 per
page (the title) and start body sections at H2.
## Anchors
Every heading is given an `id` — the slug of its text — and a hover-revealed `#`
permalink, so a section can be deep-linked with a [`#slug` anchor](md/khb-authoring/links.md). Cross-page
navigation still comes from the docset's `toc.yaml` / folder structure.
~~~code-preview example
```md
## Where to start
…later, from anywhere on this page…
Jump back to [Where to start](#where-to-start).
```
```md
## Where to start
…later, from anywhere on this page…
Jump back to [Where to start](#where-to-start).
```
~~~
Headings also drive the **"On this page"** navigation box: a page with two or more
top-level sections gets one automatically, built from its headings. Force it on or
off with `toc: true` / `toc: false` in the frontmatter — see
[toc (frontmatter)](md/khb-authoring/frontmatter-toc.md).
---
# Text formatting
Inline styling uses the usual Markdown markers — the GitHub ones work exactly as on
GitHub, and a few extra marks come on top.
> [!WARNING]
> Raw inline HTML is **escaped, not rendered** — `x` shows up on the page as
> literal text. Use Markdown, not HTML.
Attribute syntax (`{.class}` on a span) is not supported either, and there are no
inline components — see [Differences from GitHub Markdown](md/khb-authoring/differences.md).
## Bold
Double asterisks make strong emphasis — key terms on first use, UI labels, the one
word a skimming reader must not miss.
~~~code-preview example
```md
Press **Compile** to build the book.
```
```md
Press **Compile** to build the book.
```
~~~
## Italic
Single asterisks make light emphasis — a stressed word, a book title, a term used
in a borrowed sense.
~~~code-preview example
```md
The id is *stable*: links keep working after a rename.
```
```md
The id is *stable*: links keep working after a rename.
```
~~~
## Bold italic
Triple asterisks combine both — rare, for the strongest inline stress.
~~~code-preview example
```md
Back up the file ***before*** converting it.
```
```md
Back up the file ***before*** converting it.
```
~~~
## Strikethrough
Double tildes cross text out — something that no longer applies but should stay
visible, like a superseded value or a corrected claim.
~~~code-preview example
```md
The limit is ~~10~~ 25 attachments.
```
```md
The limit is ~~10~~ 25 attachments.
```
~~~
## Insert
Double pluses mark text as an addition — the counterpart of strikethrough, for
diff-style edits where the old and the new stand side by side.
~~~code-preview example
```md
The limit is ~~10~~ ++25++ attachments.
```
```md
The limit is ~~10~~ ++25++ attachments.
```
~~~
## Inline code
Backticks typeset identifiers verbatim in monospace — file names, ids, field values,
anything a reader might type.
~~~code-preview example
```md
Set `language = "en"` in `docset.toml`.
```
```md
Set `language = "en"` in `docset.toml`.
```
~~~
## Inline code attributes
An inline `` `code` `` span can carry a `{…}` attribute **immediately after** the closing
backtick:
| Write | Effect |
|-------|--------|
| `` `let x = 1;`{:rust} `` | syntax-highlight the snippet, in that language |
| `` `Beta`{.badge} `` | a neutral badge pill |
| `` `New`{.badge-green} `` | a coloured badge (`blue` / `green` / `amber` / `red`) |
`{:lang}` highlights the code with the same engine as fenced blocks (at build time, so
no runtime highlighter). `{.badge…}` turns the code into a small pill — handy for
version tags and status labels. The brace must touch the closing backtick; a `{…}` after
a space is just text.
## Highlight
Double equals signs mark text like a highlighter pen — for drawing the eye to the
key fragment of a sentence, a value in a longer line, the part that changed.
~~~code-preview example
```md
Set the ==id== field before anything else.
```
```md
Set the ==id== field before anything else.
```
~~~
## Underline
Double underscores underline text — useful for terms that carry a defined meaning,
or wherever house style calls for underlining instead of italics.
~~~code-preview example
```md
A __docset__ is one compiled book.
```
```md
A __docset__ is one compiled book.
```
~~~
> [!IMPORTANT]
> In plain Markdown `__x__` means bold — here it means underline. Write bold with
> asterisks only: `**x**`.
## Superscript
Carets raise text — exponents, ordinals, footnote-style markers in prose.
~~~code-preview example
```md
E = mc^2^, the 4^th^ edition
```
```md
E = mc^2^, the 4^th^ edition
```
~~~
## Subscript
Single tildes lower text — chemical formulas, variable indices.
~~~code-preview example
```md
H~2~O, x~1~ … x~n~
```
```md
H~2~O, x~1~ … x~n~
```
~~~
> [!IMPORTANT]
> In GitHub Markdown a single tilde can mean strikethrough — here it means
> subscript; strikethrough is `~~x~~` only.
## Spoiler
Double pipes black out text until the reader clicks it — for hiding answers, solutions,
or plot points.
~~~code-preview example
```md
The answer is ||42||.
```
```md
The answer is ||42||.
```
~~~
## Literal characters
Every marker on this page is just a character until it pairs up — and when you mean
the *character*, a backslash before it turns the syntax off: `\~`, `\*`, `\_`,
`\+`, `\=`, `\\` for the backslash itself. In practice the tilde needs this most
(a single `~` is subscript syntax), asterisks and underscores occasionally, the
rest rarely.
~~~code-preview example
```md
Takes \~5 minutes; required fields are marked with \*.
```
```md
Takes \~5 minutes; required fields are marked with \*.
```
~~~
Inside `inline code` and code blocks nothing is ever escaped — write `~/books`,
`a * b` or `__init__` there as-is.
## Line breaks
A blank line starts a new paragraph. For a **hard line break** inside one — an
address, a verse — end the line with a backslash (or two trailing spaces):
~~~code-preview example
```md
First line\
forced onto a new line.
```
```md
First line\
forced onto a new line.
```
~~~
## Horizontal rules
Three or more `-`, `*`, or `_` alone on a line draw a divider — a scene change
between passages that don't deserve separate headings:
~~~code-preview example
```md
---
```
```md
---
```
~~~
---
# Lists & task lists
All the GitHub list flavours work here: bulleted, numbered (with a chosen starting
number), nested to any depth, and check-box task lists.
## Unordered lists
A `-` at the start of a line makes a bullet item (`*` and `+` work too — pick one
and stay consistent).
~~~code-preview example
```md
- Compile the book
- Open it in the viewer
- Publish
```
```md
- Compile the book
- Open it in the viewer
- Publish
```
~~~
## Ordered lists
A number and a dot make a numbered item. Only the **first** number matters — items
renumber automatically from it, so a lazy `1. / 1. / 1.` still counts up and
inserting a step never means renumbering the rest by hand.
~~~code-preview example
```md
1. Write a page
1. Compile
1. Preview
```
```md
1. Write a page
1. Compile
1. Preview
```
~~~
To start elsewhere, give the first item that number — handy when a procedure
continues after an interruption, like a code block or a paragraph:
~~~code-preview example
```md
4. Package the viewer
5. Deploy
```
```md
4. Package the viewer
5. Deploy
```
~~~
## Nested lists
Indent child items under their parent (align with the parent's text). Bullets and
numbers can mix freely across levels.
~~~code-preview example
```md
1. Prepare the source
- docset.toml
- at least one page
2. Compile
```
```md
1. Prepare the source
- docset.toml
- at least one page
2. Compile
```
~~~
An item can also hold whole paragraphs or code blocks — indent that continuation
content to line up under the item's text.
## Task lists
Task lists work exactly as on GitHub: `- [ ]` for an open item and
`- [x]` for a done one.
~~~code-preview example
```md
- [x] Compile the docset
- [ ] Publish it
```
```md
- [x] Compile the docset
- [ ] Publish it
```
~~~
## Description lists
A **term** on its own line, then a line starting with `: ` for its **definition**, makes
a description list (`
`):
~~~code-preview example
```md
Docset
: A compiled `.khb` — one book.
Collection
: Several docsets that merge into one tree.
```
```md
Docset
: A compiled `.khb` — one book.
Collection
: Several docsets that merge into one tree.
```
~~~
---
# Links
Standard Markdown `[label](target)` links — the target alone decides what happens.
Pages are linked by **id** (no file paths, no `.md` or `.htm` suffixes), so links
survive reorganizing the source folder and keep working when several books are
loaded together.
```md
In-page anchor: [Setup](#setup)
In-book page: [Writing pages](writing-pages)
Cross-book: [SDK reference](sample-sdk:overview)
External: [Nuxt UI](https://ui.nuxt.com)
Autolink: https://example.com
```
## In-page anchors — `#slug`
A `#slug` target scrolls to the **heading on the current page** whose id is `slug`.
Every heading gets an id automatically (the slug of its text), and hovering a heading
reveals a `#` permalink. See [headings](md/khb-authoring/headings.md).
```md
Jump to [Setup](#setup), then read the [Notes](#notes).
```
## In-book pages — a bare `page-id`
A bare target (no `#`, no scheme) is the **id of another page in the same book**. The
viewer navigates to it in the current tab. A page's id defaults to its file name —
see [id (frontmatter)](md/khb-authoring/frontmatter-id.md).
```md
See [Writing pages](writing-pages) for the frontmatter fields.
```
## Cross-book links — `docsetId:pageId`
When several docsets are loaded together, link across them by prefixing the target
page's id with its book's [docset id](md/khb-authoring/docset-id.md):
```md
See the [SDK overview](sample-sdk:overview).
```
The viewer **hides** a cross-book link whose book isn't loaded, so a partial
collection never shows dead ends.
## External links
`http(s)://` and `mailto:` targets open outside the book, per the viewer's link
policy (a new tab with modifier keys held). `javascript:` and other unsafe schemes
are neutralised.
## Autolinks
Bare URLs become links automatically, as on GitHub:
`https://example.com`.
## Compile-time validation
The compiler checks in-book ids where it can: every id in [toc.yaml](md/khb-authoring/toc-yaml.md) and
every in-book entry in a page's `related` list must name an existing page, or the
build fails. Cross-book ids are stored as-is — the other book compiles separately —
and resolve (or hide) at view time.
## The See also footer
For curated onward reading, list page ids in the page's `related` frontmatter instead
of weaving links into prose — the viewer renders them as a **See also** footer, using
the same two id forms. See [related (frontmatter)](md/khb-authoring/frontmatter-related.md).
---
# Images & assets
Everything under the source folder's `assets/` directory (any depth) is stored in
the book and referenced by its **docset-relative `assets/…` path** — an image
renders inline, a link to any other file becomes a download. Use forward slashes
and keep files under `assets/`; other relative paths are left as plain links,
which won't resolve in the rendered page.
~~~code-preview example
```md

Download the [quick-reference card](assets/quick-reference.txt).
```
```md

Download the [quick-reference card](assets/quick-reference.txt).
```
~~~
## Images
Standard image syntax. An image renders inline, and the viewer offers a
**lightbox** (click to enlarge). Write meaningful `alt` text — it's what screen
readers announce and what search sees.
~~~code-preview example
```md

```
```md

```
~~~
> [!WARNING]
> Remote/absolute image URLs (`https://…`) are **not** fetched — content is
> origin-isolated and offline-first. Bundle images under `assets/` instead.
## Captions
Give an image a **title** — the quoted text after the URL — and it renders as a
`` with the title shown as a `` beneath it.
~~~code-preview example
```md

```
```md

```
~~~
## Sizing
By default an image displays at its natural size, capped at the column width — a
full-resolution phone screenshot fills the whole page. Cap the *displayed* size
with hints on the path: `#w=` takes pixels or a percentage of the column, `#h=`
takes pixels or a share of the reading pane (`vh`), and `&` combines them.
~~~code-preview example
```md




```
```md




```
~~~
A hint never upscales a smaller image, still shrinks with narrow viewports, and
keeps the aspect ratio (with both caps, the image fits inside the box). It only
affects display: the stored file is untouched and the lightbox opens the
full-size original.
## Galleries
A `~~~gallery` fence lays a set of images out as **uniform captioned tiles** — the natural
home for a step-by-step screenshot strip. Each image is one tile, its alt text is the
caption, and identical images all render at the same width (no cell-by-cell squeezing the
way a table of images gives you).
Here is a live gallery — three views of this very guide open in the viewer:
~~~gallery



~~~
Each `` on its own line starts a tile; the alt is shown as the caption
beneath the image. In source, a gallery is a `~~~gallery` fence:
~~~gallery



~~~
### Descriptions
Any text on the lines **after** an image — up to the next image or the closing fence —
becomes that tile's description, shown smaller and muted under the caption. Only inline
Markdown (bold, code, links) is used, and it wraps within the tile:
~~~gallery

Tap the card — its **UID** appears.

Exactly **one** tag may answer.
~~~
Live, with a description under each tile:
~~~gallery

All three volumes merge into one tree; the **On this page** box tracks headings.

Baked in at **compile time** — the viewer ships no highlighter.

`> [!NOTE]` blockquotes become labelled boxes, and so do `:::note` directives.
~~~
### Layout flags
Bare words in the fence info string tune the layout:
| Flag | Effect |
|------|--------|
| `w=` | the shared tile width (e.g. `w=180`); defaults to a sensible width when omitted |
| `wrap` | **default** — tiles flow into more rows when the pane is narrow |
| `scroll` | keep a single row that scrolls sideways, preserving the step-strip order |
```md
~~~gallery w=180 scroll



~~~
```
Use `wrap` for a loose set of images that can reflow, and `scroll` for an ordered
sequence you want kept in one line. In print, a `scroll` gallery wraps so nothing is
clipped.
Live — a `scroll` strip of wider tiles (drag, or shift-scroll, sideways):
~~~gallery w=320 scroll




~~~
### Gallery notes
- **Clicking a tile** opens the full-size image in the lightbox; from a gallery the
lightbox adds **‹ prev / next ›** controls (and the ← / → keys) to step through the
strip, with a position counter.
- **Captions are searchable** — the alt text feeds the page's plain-text index.
- A gallery with **no images** is a build error (the same rule the `~~~code-*` fences
follow).
## Downloads
A **link to a non-image asset** (`[label](assets/…)`) becomes a **download**. Every
file under `assets/` is stored whether or not a page references it, so a folder of
downloadable extras needs no inline mentions.
~~~code-preview example
```md
Download the [quick-reference card](assets/quick-reference.txt).
```
```md
Download the [quick-reference card](assets/quick-reference.txt).
```
~~~
## Embedded or sidecar
By default attachments are **embedded** in the `.khb`. Compile with
`--assets sidecar` to write them to a sibling **`.khba` pack** instead, keeping the
`.khb` itself lean — one docset can be backed by several packs, and a pack can be
fetched separately from (even later than) its book. See
[Compiling a book](md/khb-authoring/compiling.md).
> [!TIP]
> Ship a big book lean: compile with `--assets sidecar`, publish the `.khb` and the
> `.khba` side by side, and readers who never open the appendix imagery never fetch
> it.
## How resolution works
At compile time an `assets/…` target is rewritten to the internal `asset:` scheme,
and the viewer resolves it — routed by the docset's `asset_index` straight to its
owning store, the embedded assets table or a specific `.khba` pack. If an asset's
pack isn't loaded, **Manage docsets** shows a "⚠ N missing assets" badge with an
*Add pack…* action.
---
# Tables
Pipe tables work exactly as on GitHub. The header row is
separated from the body by a row of dashes; colons in that separator set column
alignment.
~~~code-preview example
```md
| Prop | Default | Type |
|-------|:-------:|-------:|
| name | | string |
| size | md | string |
```
```md
| Prop | Default | Type |
|-------|:-------:|-------:|
| name | | string |
| size | md | string |
```
~~~
- `:---` left-aligns, `:--:` centres, `---:` right-aligns.
- Cells are inline Markdown, so `**bold**`, `` `code` `` and links work inside them.
- A wide table scrolls horizontally inside the content frame rather than breaking
the layout.
There is no cell-spanning or nested-block syntax — tables are for tabular data;
reach for lists or headings for richer structure.
---
# Blockquotes
Prefix lines with `>`. Separate paragraphs inside a quote with a `>` on its own line.
~~~code-preview example
```md
> A single-line quote.
> First paragraph of a quote.
>
> Second paragraph, still quoted.
```
```md
> A single-line quote.
> First paragraph of a quote.
>
> Second paragraph, still quoted.
```
~~~
Blockquotes can contain other Markdown — lists, code, even nested quotes (prefix with
`> >`).
## Fenced (multi-paragraph) blockquotes
For a long quote with several paragraphs, prefixing every line with `>` is tedious.
Fence the whole quote with `>>>` instead — everything between the markers is quoted:
~~~code-preview example
```md
>>>
First paragraph of the quote.
Second paragraph — no `>` on any line.
>>>
```
```md
>>>
First paragraph of the quote.
Second paragraph — no `>` on any line.
>>>
```
~~~
## Callouts
A plain blockquote stays neutral. For a coloured, labelled box (note / tip / warning
/ …), use a **callout** — a blockquote whose first line is a GitHub-style `[!TYPE]`
alert marker. (A **[directive](md/khb-authoring/directives.md)** like `:::tip` is an interchangeable
alternative.)
~~~code-preview example
```md
> [!NOTE]
> Useful information the reader should know.
> [!WARNING]
> Something that needs attention.
```
```md
> [!NOTE]
> Useful information the reader should know.
> [!WARNING]
> Something that needs attention.
```
~~~
The `[!TYPE]` marker must be **uppercase and alone on the first line**; anything else
renders as a plain blockquote. A callout can hold multiple paragraphs, lists, and code
— just keep them inside the `>` quote.
The five types, each with its own colour — the source, then how it renders:
### Note
Background or context the reader should absorb even when skimming — the fact still
matters if they skip it, so it earns a box rather than a plain sentence.
~~~code-preview example
```md
> [!NOTE]
> The compiled book works fully offline — no network access is needed to read it.
```
```md
> [!NOTE]
> The compiled book works fully offline — no network access is needed to read it.
```
~~~
### Tip
Optional advice: a shortcut, a better habit, a nicer way to do the same thing.
Nothing breaks if the reader ignores it.
~~~code-preview example
```md
> [!TIP]
> Name files after their page ids — links then read like the table of contents.
```
```md
> [!TIP]
> Name files after their page ids — links then read like the table of contents.
```
~~~
### Important
Information the reader *needs* for the task at hand to succeed — skipping it means
something won't work, even though nothing dangerous happens.
~~~code-preview example
```md
> [!IMPORTANT]
> Every page needs a unique id — two files can't share one.
```
```md
> [!IMPORTANT]
> Every page needs a unique id — two files can't share one.
```
~~~
### Warning
Something that demands attention *before* the reader acts — a common trap, a
surprising behavior, a step that's easy to get wrong.
~~~code-preview example
```md
> [!WARNING]
> Raw HTML is escaped, not rendered — `bold` shows up as literal text.
```
```md
> [!WARNING]
> Raw HTML is escaped, not rendered — `bold` shows up as literal text.
```
~~~
### Caution
Consequences: actions that are destructive, irreversible, or costly to undo. The
strongest signal — save it for cases where acting wrongly does real damage.
~~~code-preview example
```md
> [!CAUTION]
> `khb pack` starts from a clean slate — docsets already in the output directory
> are removed before the new ones are copied in.
```
```md
> [!CAUTION]
> `khb pack` starts from a clean slate — docsets already in the output directory
> are removed before the new ones are copied in.
```
~~~
---
# Directives
A **directive** is a fenced container that wraps other Markdown in a labelled box.
Open with three or more colons and a name, then close with a matching row of colons:
~~~code-preview example
```md
:::tip
You can nest **Markdown**, lists, and `code` inside a directive.
:::
```
```md
:::tip
You can nest **Markdown**, lists, and `code` inside a directive.
:::
```
~~~
The compiler turns `:::name … :::` into `
`, and the viewer styles a
curated set of names. The name is HTML-escaped when it becomes the class, so directive
content can't inject markup.
## Callout directives
Five names render as coloured, self-labelling callouts — a portable alternative to the
`> [!NOTE]` [callout](md/khb-authoring/blockquotes.md) syntax:
~~~code-preview example
```md
:::note
Background a reader can skip.
:::
:::tip
A shortcut worth knowing.
:::
:::info
A neutral aside.
:::
:::warning
Something that can bite you.
:::
:::caution
A destructive or irreversible action.
:::
```
```md
:::note
Background a reader can skip.
:::
:::tip
A shortcut worth knowing.
:::
:::info
A neutral aside.
:::
:::warning
Something that can bite you.
:::
:::caution
A destructive or irreversible action.
:::
```
~~~
The available kinds are `note`, `tip`, `info`, `warning`, and `caution` (`danger` is an
alias for `caution`). Each supplies its own heading — the type *is* the label.
## Cards
`:::card` is a plain framed box with no accent bar and no auto-heading — reach for it
when you want to set a block apart without implying note/warning semantics:
~~~code-preview example
```md
:::card
A self-contained block: a summary, a definition, a call-out box of your own making.
:::
```
```md
:::card
A self-contained block: a summary, a definition, a call-out box of your own making.
:::
```
~~~
## Tabs
`:::tabs` wraps a set of `:::tab` panels — one shown at a time, switched by clicking its
label. The words after `tab` are the label; a panel holds any Markdown (prose, code,
even another directive). Give the outer `tabs` fence more colons than the inner `tab`s:
~~~code-preview example
```md
::::tabs
:::tab macOS
Install with Homebrew:
`brew install foo`{:bash}
:::
:::tab Linux
`apt install foo`{:bash}
:::
::::
```
```md
::::tabs
:::tab macOS
Install with Homebrew:
`brew install foo`{:bash}
:::
:::tab Linux
`apt install foo`{:bash}
:::
::::
```
~~~
Tabs are the one directive that needs the viewer's frame bridge (a tiny click handler);
everything else on this page is pure CSS. A `tab` with no label falls back to `Tab 1`,
`Tab 2`, …
## Steps
`:::steps` turns an **ordered list** into a numbered walkthrough — big numerals down a
connector line, with room for rich content under each step:
~~~code-preview example
```md
:::steps
1. **Install** the CLI.
`cargo install khb`{:bash}
2. **Compile** your sources into a `.khb`.
3. **Open** it in the viewer.
:::
```
```md
:::steps
1. **Install** the CLI.
`cargo install khb`{:bash}
2. **Compile** your sources into a `.khb`.
3. **Open** it in the viewer.
:::
```
~~~
## Nesting
To put a directive inside another, give the **outer** fence more colons than the inner
one:
~~~code-preview example
```md
::::card
A card with a callout inside it:
:::tip
Nested with three colons; the card uses four.
:::
::::
```
```md
::::card
A card with a callout inside it:
:::tip
Nested with three colons; the card uses four.
:::
::::
```
~~~
## Notes for KD Help Book
- The name becomes the box's `class` verbatim — a name outside the styled set above
(`:::sidebar`, say) renders as an unstyled `
`. There's no attribute or custom-title
syntax: `:::note Heading` doesn't set a heading, it just adds stray classes.
- Callout directives and `> [!NOTE]` callouts are interchangeable; use whichever reads
better in the source. Both compile to the same kind of box.
- `:::tabs` here is for **prose** panels; to tab between *code* samples with real syntax
highlighting use [`~~~code-group`](md/khb-authoring/code-blocks.md) instead. Collapsible code isn't a
directive either — it's the `collapse` fence flag.
---
# Code blocks
Fence a block with triple backticks. Declare a **language** after the opening fence
and the block is **syntax-highlighted** — at compile time, with nothing to configure;
the colours follow the viewer's theme automatically.
````md
```rust
fn main() {
println!("Hello from a docset!");
}
```
````
Renders (highlighted):
```rust
fn main() {
println!("Hello from a docset!");
}
```
A fence **without** a language renders as plain monospace text. Inline code uses
single backticks: `` `let x = 1` ``.
## Beyond plain fences
KD Help Book extends plain fences two ways: **flags on the fence info string**
(filename, collapse) and **`~~~` containers** that combine several blocks into one
widget (tabs, command+output, file trees). No raw HTML, no directive syntax — just
fences. These are the only container blocks; there is no generic `:::` directive
syntax — see [Differences from GitHub Markdown](md/khb-authoring/differences.md).
### Filename + copy
Add `[filename]` after the language to label the block with a header bar, and every
code block gets a **Copy** button (revealed on hover; always shown on touch).
````md
```ts [nuxt.config.ts]
export default defineConfig({})
```
````
### Highlight lines
Add a **`{2,4-6}`** range after the language (and optional `[filename]`) to tint specific
lines — single numbers and `start-end` ranges, comma-separated, 1-based.
````md
```rust {2,4-5}
fn main() {
let base = 10; // highlighted
let mut total = 0;
for i in 1..=base { // highlighted
total += i; // highlighted
}
}
```
````
```rust {2,4-5}
fn main() {
let base = 10; // highlighted
let mut total = 0;
for i in 1..=base { // highlighted
total += i; // highlighted
}
}
```
### Collapsible blocks
Add the **`collapse`** flag after the language (and optional `[filename]`) to clamp a
long block to a short **preview** — the first few lines stay visible and fade out under
an *Expand code* / *Collapse code* button. Collapsed by default; add **`open`** to start
expanded.
````md
```rust [main.rs] collapse
fn main() {
let mut total = 0;
for i in 1..=10 {
total += i;
println!("running total after {i}: {total}");
}
println!("sum 1..=10 = {total}");
// …plus a long tail of code you'd rather tuck away until it's wanted.
}
```
````
```rust [main.rs] collapse
fn main() {
let mut total = 0;
for i in 1..=10 {
total += i;
println!("running total after {i}: {total}");
}
println!("sum 1..=10 = {total}");
// …plus a long tail of code you'd rather tuck away until it's wanted.
}
```
### Groups (tabs)
Wrap several code blocks in a **`~~~code-group … ~~~`** fence (tildes on the outside,
so the inner blocks keep their backticks) to render them as **tabs** — one highlighted
panel per block, its `[label]` (or the language) as the tab. Handy for
npm/pnpm/yarn or the same idea in several languages.
`````md
~~~code-group
```bash [npm]
npm install khb
```
```bash [pnpm]
pnpm add khb
```
```bash [yarn]
yarn add khb
```
~~~
`````
~~~code-group
```bash [npm]
npm install khb
```
```bash [pnpm]
pnpm add khb
```
```bash [yarn]
yarn add khb
```
~~~
A group with no inner code blocks is a **build error** (a likely authoring mistake).
These malformed-container errors are part of [compile-time validation](md/khb-authoring/compiling.md) —
an empty group or a preview missing its output block never ships.
### Command + output (`code-preview`)
A **`~~~code-preview … ~~~`** fence pairs a **command** (first inner block, syntax
highlighted) with its **output** (second block), rendered as a terminal panel. Missing
either block is a build error.
`````md
~~~code-preview
```bash
khb compile docs/authoring -o authoring.khb
```
```
compiled khb-authoring (36 pages, language en) -> authoring.khb
```
~~~
`````
~~~code-preview
```bash
khb compile docs/authoring -o authoring.khb
```
```
compiled khb-authoring (36 pages, language en) -> authoring.khb
```
~~~
That terminal panel is the **default skin**. Add a skin token to change how the second
block renders — see `example` below.
### Source + rendered result (`example` skin)
`~~~code-preview example` pairs a construct's **source** (first block, shown as code) with
its **rendered result** (second block, rendered as Markdown) — for showing syntax next to
what it produces, in one connected frame. Both blocks are still required, and given
separately, so the result needn't be the literal render of the source.
`````md
~~~code-preview example
```md
> [!TIP]
> Name files after their page ids.
```
```md
> [!TIP]
> Name files after their page ids.
```
~~~
`````
~~~code-preview example
```md
> [!TIP]
> Name files after their page ids.
```
```md
> [!TIP]
> Name files after their page ids.
```
~~~
Add **`split`** (`~~~code-preview example split`) to place source and result side by side;
it falls back to stacked on a narrow pane.
~~~code-preview example split
```md
**Bold**, ==highlight==, and `inline code`.
```
```md
**Bold**, ==highlight==, and `inline code`.
```
~~~
> [!TIP]
> Writing the snippet twice gets tedious. This guide writes it **once** with the
> [`ext:example`](md/khb-authoring/extensions.md) extension — the tool emits this widget for you (compile the
> book with `--allow-extensions`).
### File tree (`code-tree`)
A **`~~~code-tree … ~~~`** fence turns each block's `[path]` label into a **file tree**
(folders nest by `/`) beside the selected file's code — click a file to switch. A tree
with no files is a build error.
`````md
~~~code-tree
```toml [docset.toml]
id = "my-book"
title = "My Book"
```
```md [pages/index.md]
# Home
Welcome.
```
```md [pages/guide/setup.md]
# Setup
Steps…
```
~~~
`````
~~~code-tree
```toml [docset.toml]
id = "my-book"
title = "My Book"
```
```md [pages/index.md]
# Home
Welcome.
```
```md [pages/guide/setup.md]
# Setup
Steps…
```
~~~
---
# Diagrams
A fenced ` ```dot ` (or ` ```graphviz `) block is laid out to an **SVG at build time** and
embedded straight into the page — the same approach as [math](md/khb-authoring/math.md) (`$…$` → MathML). The
viewer runs no diagram engine, and the SVG is static and sandbox-safe (no scripts).
````md
```dot
digraph {
rankdir=LR;
Source [shape=note];
Source -> Compile -> "khb" -> Viewer;
Compile -> FTS5 [label="index"];
}
```
````
Renders as:
```dot
digraph {
rankdir=LR;
Source [shape=note];
Source -> Compile -> "khb" -> Viewer;
Compile -> FTS5 [label="index"];
}
```
The syntax is [Graphviz **DOT**](https://graphviz.org/doc/info/lang.html): `digraph { … }`
for directed graphs (arrows `->`), `graph { … }` for undirected (`--`). Node and edge
attributes like `shape`, `label`, and `rankdir` work; the graph is laid out by a
pure-Rust engine bundled into the compiler, so no Graphviz install is needed.
## Notes for KD Help Book
- **A DOT syntax error fails the build** — a broken diagram is caught at compile time, not
shipped as a blank space (same policy as math).
- The engine covers the common flowchart / graph cases. Very large or exotic graphs may
lay out less cleanly than desktop Graphviz.
- **Diagrams follow the viewer's colour theme.** A default (uncoloured) DOT diagram is
emitted so its lines and labels take the page's text colour and its node fills take a
themed surface — so it flips to light-on-dark automatically in the viewer's dark mode,
no separate dark image needed. If you set an explicit **light** `fillcolor`, that fill
is kept while the label text still follows the theme (the engine ignores `fontcolor`),
so pick node colours that read against both a light and a dark background.
- **Mermaid** (` ```mermaid `) isn't supported: it's a JavaScript library that needs a
headless browser to render, which would break the compiler's single-toolchain, offline
build. DOT gives the same static-SVG result without that dependency. If you do need to
shell out to an external renderer, that's what opt-in [extensions](md/khb-authoring/extensions.md)
(`--allow-extensions`) are for — the one deliberate exception to the offline default.
- The SVG scales down to fit narrow screens and scrolls if it's wider than the page.
---
# Footnotes
Footnotes work exactly as on GitHub. Place a reference
`[^id]` in the text and define it anywhere in the page.
~~~code-preview example
```md
KD Help Book stores rendered HTML, never the source Markdown[^format].
[^format]: The optional `md` column is an enrichment for AI export, not the render.
```
```md
KD Help Book stores rendered HTML, never the source Markdown[^format].
[^format]: The optional `md` column is an enrichment for AI export, not the render.
```
~~~
Renders with a numbered marker[^demo] and a collected list of notes at the foot of the
page — definitions are gathered into that footnotes section regardless of where you
wrote them. Ids are page-local; the same `[^1]` on two pages doesn't collide.
[^demo]: This is the footnote's text; the viewer links the marker to it and back.
## Inline footnotes
For a short aside you don't want to define separately, write it inline with `^[…]`^[like
this one] — it joins the same numbered list at the foot of the page.
---
# Emoji
Write emoji with `:shortcode:` names — the compiler replaces them with the Unicode
character at build time.
~~~code-preview example
```md
Shipped it :tada: — tests are green :white_check_mark:.
```
```md
Shipped it :tada: — tests are green :white_check_mark:.
```
~~~
> [!NOTE]
> An unknown `:name:` is left in the text as-is — a typo in a shortcode shows up
> literally on the page.
## All shortcodes
Every shortcode, in Unicode (CLDR) order — where an emoji has several names, any of
them works.
### Smileys & emotion
| Emoji | Shortcodes |
|-------|------------|
| :grinning: | `:grinning:` |
| :smiley: | `:smiley:` |
| :smile: | `:smile:` |
| :grin: | `:grin:` |
| :laughing: | `:laughing:` `:satisfied:` |
| :sweat_smile: | `:sweat_smile:` |
| :rofl: | `:rofl:` |
| :joy: | `:joy:` |
| :slightly_smiling_face: | `:slightly_smiling_face:` |
| :upside_down_face: | `:upside_down_face:` |
| :melting_face: | `:melting_face:` |
| :wink: | `:wink:` |
| :blush: | `:blush:` |
| :innocent: | `:innocent:` |
| :smiling_face_with_three_hearts: | `:smiling_face_with_three_hearts:` |
| :heart_eyes: | `:heart_eyes:` |
| :star_struck: | `:star_struck:` |
| :kissing_heart: | `:kissing_heart:` |
| :kissing: | `:kissing:` |
| :relaxed: | `:relaxed:` |
| :kissing_closed_eyes: | `:kissing_closed_eyes:` |
| :kissing_smiling_eyes: | `:kissing_smiling_eyes:` |
| :smiling_face_with_tear: | `:smiling_face_with_tear:` |
| :yum: | `:yum:` |
| :stuck_out_tongue: | `:stuck_out_tongue:` |
| :stuck_out_tongue_winking_eye: | `:stuck_out_tongue_winking_eye:` |
| :zany_face: | `:zany_face:` |
| :stuck_out_tongue_closed_eyes: | `:stuck_out_tongue_closed_eyes:` |
| :money_mouth_face: | `:money_mouth_face:` |
| :hugs: | `:hugs:` |
| :hand_over_mouth: | `:hand_over_mouth:` |
| :face_with_open_eyes_and_hand_over_mouth: | `:face_with_open_eyes_and_hand_over_mouth:` |
| :face_with_peeking_eye: | `:face_with_peeking_eye:` |
| :shushing_face: | `:shushing_face:` |
| :thinking: | `:thinking:` |
| :saluting_face: | `:saluting_face:` |
| :zipper_mouth_face: | `:zipper_mouth_face:` |
| :raised_eyebrow: | `:raised_eyebrow:` |
| :neutral_face: | `:neutral_face:` |
| :expressionless: | `:expressionless:` |
| :no_mouth: | `:no_mouth:` |
| :dotted_line_face: | `:dotted_line_face:` |
| :face_in_clouds: | `:face_in_clouds:` |
| :smirk: | `:smirk:` |
| :unamused: | `:unamused:` |
| :roll_eyes: | `:roll_eyes:` |
| :grimacing: | `:grimacing:` |
| :face_exhaling: | `:face_exhaling:` |
| :lying_face: | `:lying_face:` |
| :shaking_face: | `:shaking_face:` |
| :relieved: | `:relieved:` |
| :pensive: | `:pensive:` |
| :sleepy: | `:sleepy:` |
| :drooling_face: | `:drooling_face:` |
| :sleeping: | `:sleeping:` |
| :mask: | `:mask:` |
| :face_with_thermometer: | `:face_with_thermometer:` |
| :face_with_head_bandage: | `:face_with_head_bandage:` |
| :nauseated_face: | `:nauseated_face:` |
| :vomiting_face: | `:vomiting_face:` |
| :sneezing_face: | `:sneezing_face:` |
| :hot_face: | `:hot_face:` |
| :cold_face: | `:cold_face:` |
| :woozy_face: | `:woozy_face:` |
| :dizzy_face: | `:dizzy_face:` |
| :face_with_spiral_eyes: | `:face_with_spiral_eyes:` |
| :exploding_head: | `:exploding_head:` |
| :cowboy_hat_face: | `:cowboy_hat_face:` |
| :partying_face: | `:partying_face:` |
| :disguised_face: | `:disguised_face:` |
| :sunglasses: | `:sunglasses:` |
| :nerd_face: | `:nerd_face:` |
| :monocle_face: | `:monocle_face:` |
| :confused: | `:confused:` |
| :face_with_diagonal_mouth: | `:face_with_diagonal_mouth:` |
| :worried: | `:worried:` |
| :slightly_frowning_face: | `:slightly_frowning_face:` |
| :frowning_face: | `:frowning_face:` |
| :open_mouth: | `:open_mouth:` |
| :hushed: | `:hushed:` |
| :astonished: | `:astonished:` |
| :flushed: | `:flushed:` |
| :pleading_face: | `:pleading_face:` |
| :face_holding_back_tears: | `:face_holding_back_tears:` |
| :frowning: | `:frowning:` |
| :anguished: | `:anguished:` |
| :fearful: | `:fearful:` |
| :cold_sweat: | `:cold_sweat:` |
| :disappointed_relieved: | `:disappointed_relieved:` |
| :cry: | `:cry:` |
| :sob: | `:sob:` |
| :scream: | `:scream:` |
| :confounded: | `:confounded:` |
| :persevere: | `:persevere:` |
| :disappointed: | `:disappointed:` |
| :sweat: | `:sweat:` |
| :weary: | `:weary:` |
| :tired_face: | `:tired_face:` |
| :yawning_face: | `:yawning_face:` |
| :triumph: | `:triumph:` |
| :rage: | `:rage:` `:pout:` |
| :angry: | `:angry:` |
| :cursing_face: | `:cursing_face:` |
| :smiling_imp: | `:smiling_imp:` |
| :imp: | `:imp:` |
| :skull: | `:skull:` |
| :skull_and_crossbones: | `:skull_and_crossbones:` |
| :hankey: | `:hankey:` `:poop:` `:shit:` |
| :clown_face: | `:clown_face:` |
| :japanese_ogre: | `:japanese_ogre:` |
| :japanese_goblin: | `:japanese_goblin:` |
| :ghost: | `:ghost:` |
| :alien: | `:alien:` |
| :space_invader: | `:space_invader:` |
| :robot: | `:robot:` |
| :smiley_cat: | `:smiley_cat:` |
| :smile_cat: | `:smile_cat:` |
| :joy_cat: | `:joy_cat:` |
| :heart_eyes_cat: | `:heart_eyes_cat:` |
| :smirk_cat: | `:smirk_cat:` |
| :kissing_cat: | `:kissing_cat:` |
| :scream_cat: | `:scream_cat:` |
| :crying_cat_face: | `:crying_cat_face:` |
| :pouting_cat: | `:pouting_cat:` |
| :see_no_evil: | `:see_no_evil:` |
| :hear_no_evil: | `:hear_no_evil:` |
| :speak_no_evil: | `:speak_no_evil:` |
| :love_letter: | `:love_letter:` |
| :cupid: | `:cupid:` |
| :gift_heart: | `:gift_heart:` |
| :sparkling_heart: | `:sparkling_heart:` |
| :heartpulse: | `:heartpulse:` |
| :heartbeat: | `:heartbeat:` |
| :revolving_hearts: | `:revolving_hearts:` |
| :two_hearts: | `:two_hearts:` |
| :heart_decoration: | `:heart_decoration:` |
| :heavy_heart_exclamation: | `:heavy_heart_exclamation:` |
| :broken_heart: | `:broken_heart:` |
| :heart_on_fire: | `:heart_on_fire:` |
| :mending_heart: | `:mending_heart:` |
| :heart: | `:heart:` |
| :pink_heart: | `:pink_heart:` |
| :orange_heart: | `:orange_heart:` |
| :yellow_heart: | `:yellow_heart:` |
| :green_heart: | `:green_heart:` |
| :blue_heart: | `:blue_heart:` |
| :light_blue_heart: | `:light_blue_heart:` |
| :purple_heart: | `:purple_heart:` |
| :brown_heart: | `:brown_heart:` |
| :black_heart: | `:black_heart:` |
| :grey_heart: | `:grey_heart:` |
| :white_heart: | `:white_heart:` |
| :kiss: | `:kiss:` |
| :100: | `:100:` |
| :anger: | `:anger:` |
| :boom: | `:boom:` `:collision:` |
| :dizzy: | `:dizzy:` |
| :sweat_drops: | `:sweat_drops:` |
| :dash: | `:dash:` |
| :hole: | `:hole:` |
| :speech_balloon: | `:speech_balloon:` |
| :eye_speech_bubble: | `:eye_speech_bubble:` |
| :left_speech_bubble: | `:left_speech_bubble:` |
| :right_anger_bubble: | `:right_anger_bubble:` |
| :thought_balloon: | `:thought_balloon:` |
| :zzz: | `:zzz:` |
### People & body
| Emoji | Shortcodes |
|-------|------------|
| :wave: | `:wave:` |
| :raised_back_of_hand: | `:raised_back_of_hand:` |
| :raised_hand_with_fingers_splayed: | `:raised_hand_with_fingers_splayed:` |
| :hand: | `:hand:` `:raised_hand:` |
| :vulcan_salute: | `:vulcan_salute:` |
| :rightwards_hand: | `:rightwards_hand:` |
| :leftwards_hand: | `:leftwards_hand:` |
| :palm_down_hand: | `:palm_down_hand:` |
| :palm_up_hand: | `:palm_up_hand:` |
| :leftwards_pushing_hand: | `:leftwards_pushing_hand:` |
| :rightwards_pushing_hand: | `:rightwards_pushing_hand:` |
| :ok_hand: | `:ok_hand:` |
| :pinched_fingers: | `:pinched_fingers:` |
| :pinching_hand: | `:pinching_hand:` |
| :v: | `:v:` |
| :crossed_fingers: | `:crossed_fingers:` |
| :hand_with_index_finger_and_thumb_crossed: | `:hand_with_index_finger_and_thumb_crossed:` |
| :love_you_gesture: | `:love_you_gesture:` |
| :metal: | `:metal:` |
| :call_me_hand: | `:call_me_hand:` |
| :point_left: | `:point_left:` |
| :point_right: | `:point_right:` |
| :point_up_2: | `:point_up_2:` |
| :middle_finger: | `:middle_finger:` `:fu:` |
| :point_down: | `:point_down:` |
| :point_up: | `:point_up:` |
| :index_pointing_at_the_viewer: | `:index_pointing_at_the_viewer:` |
| :+1: | `:+1:` `:thumbsup:` |
| :-1: | `:-1:` `:thumbsdown:` |
| :fist_raised: | `:fist_raised:` `:fist:` |
| :fist_oncoming: | `:fist_oncoming:` `:facepunch:` `:punch:` |
| :fist_left: | `:fist_left:` |
| :fist_right: | `:fist_right:` |
| :clap: | `:clap:` |
| :raised_hands: | `:raised_hands:` |
| :heart_hands: | `:heart_hands:` |
| :open_hands: | `:open_hands:` |
| :palms_up_together: | `:palms_up_together:` |
| :handshake: | `:handshake:` |
| :pray: | `:pray:` |
| :writing_hand: | `:writing_hand:` |
| :nail_care: | `:nail_care:` |
| :selfie: | `:selfie:` |
| :muscle: | `:muscle:` |
| :mechanical_arm: | `:mechanical_arm:` |
| :mechanical_leg: | `:mechanical_leg:` |
| :leg: | `:leg:` |
| :foot: | `:foot:` |
| :ear: | `:ear:` |
| :ear_with_hearing_aid: | `:ear_with_hearing_aid:` |
| :nose: | `:nose:` |
| :brain: | `:brain:` |
| :anatomical_heart: | `:anatomical_heart:` |
| :lungs: | `:lungs:` |
| :tooth: | `:tooth:` |
| :bone: | `:bone:` |
| :eyes: | `:eyes:` |
| :eye: | `:eye:` |
| :tongue: | `:tongue:` |
| :lips: | `:lips:` |
| :biting_lip: | `:biting_lip:` |
| :baby: | `:baby:` |
| :child: | `:child:` |
| :boy: | `:boy:` |
| :girl: | `:girl:` |
| :adult: | `:adult:` |
| :blond_haired_person: | `:blond_haired_person:` |
| :man: | `:man:` |
| :bearded_person: | `:bearded_person:` |
| :man_beard: | `:man_beard:` |
| :woman_beard: | `:woman_beard:` |
| :red_haired_man: | `:red_haired_man:` |
| :curly_haired_man: | `:curly_haired_man:` |
| :white_haired_man: | `:white_haired_man:` |
| :bald_man: | `:bald_man:` |
| :woman: | `:woman:` |
| :red_haired_woman: | `:red_haired_woman:` |
| :person_red_hair: | `:person_red_hair:` |
| :curly_haired_woman: | `:curly_haired_woman:` |
| :person_curly_hair: | `:person_curly_hair:` |
| :white_haired_woman: | `:white_haired_woman:` |
| :person_white_hair: | `:person_white_hair:` |
| :bald_woman: | `:bald_woman:` |
| :person_bald: | `:person_bald:` |
| :blond_haired_woman: | `:blond_haired_woman:` `:blonde_woman:` |
| :blond_haired_man: | `:blond_haired_man:` |
| :older_adult: | `:older_adult:` |
| :older_man: | `:older_man:` |
| :older_woman: | `:older_woman:` |
| :frowning_person: | `:frowning_person:` |
| :frowning_man: | `:frowning_man:` |
| :frowning_woman: | `:frowning_woman:` |
| :pouting_face: | `:pouting_face:` |
| :pouting_man: | `:pouting_man:` |
| :pouting_woman: | `:pouting_woman:` |
| :no_good: | `:no_good:` |
| :no_good_man: | `:no_good_man:` `:ng_man:` |
| :no_good_woman: | `:no_good_woman:` `:ng_woman:` |
| :ok_person: | `:ok_person:` |
| :ok_man: | `:ok_man:` |
| :ok_woman: | `:ok_woman:` |
| :tipping_hand_person: | `:tipping_hand_person:` `:information_desk_person:` |
| :tipping_hand_man: | `:tipping_hand_man:` `:sassy_man:` |
| :tipping_hand_woman: | `:tipping_hand_woman:` `:sassy_woman:` |
| :raising_hand: | `:raising_hand:` |
| :raising_hand_man: | `:raising_hand_man:` |
| :raising_hand_woman: | `:raising_hand_woman:` |
| :deaf_person: | `:deaf_person:` |
| :deaf_man: | `:deaf_man:` |
| :deaf_woman: | `:deaf_woman:` |
| :bow: | `:bow:` |
| :bowing_man: | `:bowing_man:` |
| :bowing_woman: | `:bowing_woman:` |
| :facepalm: | `:facepalm:` |
| :man_facepalming: | `:man_facepalming:` |
| :woman_facepalming: | `:woman_facepalming:` |
| :shrug: | `:shrug:` |
| :man_shrugging: | `:man_shrugging:` |
| :woman_shrugging: | `:woman_shrugging:` |
| :health_worker: | `:health_worker:` |
| :man_health_worker: | `:man_health_worker:` |
| :woman_health_worker: | `:woman_health_worker:` |
| :student: | `:student:` |
| :man_student: | `:man_student:` |
| :woman_student: | `:woman_student:` |
| :teacher: | `:teacher:` |
| :man_teacher: | `:man_teacher:` |
| :woman_teacher: | `:woman_teacher:` |
| :judge: | `:judge:` |
| :man_judge: | `:man_judge:` |
| :woman_judge: | `:woman_judge:` |
| :farmer: | `:farmer:` |
| :man_farmer: | `:man_farmer:` |
| :woman_farmer: | `:woman_farmer:` |
| :cook: | `:cook:` |
| :man_cook: | `:man_cook:` |
| :woman_cook: | `:woman_cook:` |
| :mechanic: | `:mechanic:` |
| :man_mechanic: | `:man_mechanic:` |
| :woman_mechanic: | `:woman_mechanic:` |
| :factory_worker: | `:factory_worker:` |
| :man_factory_worker: | `:man_factory_worker:` |
| :woman_factory_worker: | `:woman_factory_worker:` |
| :office_worker: | `:office_worker:` |
| :man_office_worker: | `:man_office_worker:` |
| :woman_office_worker: | `:woman_office_worker:` |
| :scientist: | `:scientist:` |
| :man_scientist: | `:man_scientist:` |
| :woman_scientist: | `:woman_scientist:` |
| :technologist: | `:technologist:` |
| :man_technologist: | `:man_technologist:` |
| :woman_technologist: | `:woman_technologist:` |
| :singer: | `:singer:` |
| :man_singer: | `:man_singer:` |
| :woman_singer: | `:woman_singer:` |
| :artist: | `:artist:` |
| :man_artist: | `:man_artist:` |
| :woman_artist: | `:woman_artist:` |
| :pilot: | `:pilot:` |
| :man_pilot: | `:man_pilot:` |
| :woman_pilot: | `:woman_pilot:` |
| :astronaut: | `:astronaut:` |
| :man_astronaut: | `:man_astronaut:` |
| :woman_astronaut: | `:woman_astronaut:` |
| :firefighter: | `:firefighter:` |
| :man_firefighter: | `:man_firefighter:` |
| :woman_firefighter: | `:woman_firefighter:` |
| :police_officer: | `:police_officer:` `:cop:` |
| :policeman: | `:policeman:` |
| :policewoman: | `:policewoman:` |
| :detective: | `:detective:` |
| :male_detective: | `:male_detective:` |
| :female_detective: | `:female_detective:` |
| :guard: | `:guard:` |
| :guardsman: | `:guardsman:` |
| :guardswoman: | `:guardswoman:` |
| :ninja: | `:ninja:` |
| :construction_worker: | `:construction_worker:` |
| :construction_worker_man: | `:construction_worker_man:` |
| :construction_worker_woman: | `:construction_worker_woman:` |
| :person_with_crown: | `:person_with_crown:` |
| :prince: | `:prince:` |
| :princess: | `:princess:` |
| :person_with_turban: | `:person_with_turban:` |
| :man_with_turban: | `:man_with_turban:` |
| :woman_with_turban: | `:woman_with_turban:` |
| :man_with_gua_pi_mao: | `:man_with_gua_pi_mao:` |
| :woman_with_headscarf: | `:woman_with_headscarf:` |
| :person_in_tuxedo: | `:person_in_tuxedo:` |
| :man_in_tuxedo: | `:man_in_tuxedo:` |
| :woman_in_tuxedo: | `:woman_in_tuxedo:` |
| :person_with_veil: | `:person_with_veil:` |
| :man_with_veil: | `:man_with_veil:` |
| :woman_with_veil: | `:woman_with_veil:` `:bride_with_veil:` |
| :pregnant_woman: | `:pregnant_woman:` |
| :pregnant_man: | `:pregnant_man:` |
| :pregnant_person: | `:pregnant_person:` |
| :breast_feeding: | `:breast_feeding:` |
| :woman_feeding_baby: | `:woman_feeding_baby:` |
| :man_feeding_baby: | `:man_feeding_baby:` |
| :person_feeding_baby: | `:person_feeding_baby:` |
| :angel: | `:angel:` |
| :santa: | `:santa:` |
| :mrs_claus: | `:mrs_claus:` |
| :mx_claus: | `:mx_claus:` |
| :superhero: | `:superhero:` |
| :superhero_man: | `:superhero_man:` |
| :superhero_woman: | `:superhero_woman:` |
| :supervillain: | `:supervillain:` |
| :supervillain_man: | `:supervillain_man:` |
| :supervillain_woman: | `:supervillain_woman:` |
| :mage: | `:mage:` |
| :mage_man: | `:mage_man:` |
| :mage_woman: | `:mage_woman:` |
| :fairy: | `:fairy:` |
| :fairy_man: | `:fairy_man:` |
| :fairy_woman: | `:fairy_woman:` |
| :vampire: | `:vampire:` |
| :vampire_man: | `:vampire_man:` |
| :vampire_woman: | `:vampire_woman:` |
| :merperson: | `:merperson:` |
| :merman: | `:merman:` |
| :mermaid: | `:mermaid:` |
| :elf: | `:elf:` |
| :elf_man: | `:elf_man:` |
| :elf_woman: | `:elf_woman:` |
| :genie: | `:genie:` |
| :genie_man: | `:genie_man:` |
| :genie_woman: | `:genie_woman:` |
| :zombie: | `:zombie:` |
| :zombie_man: | `:zombie_man:` |
| :zombie_woman: | `:zombie_woman:` |
| :troll: | `:troll:` |
| :massage: | `:massage:` |
| :massage_man: | `:massage_man:` |
| :massage_woman: | `:massage_woman:` |
| :haircut: | `:haircut:` |
| :haircut_man: | `:haircut_man:` |
| :haircut_woman: | `:haircut_woman:` |
| :walking: | `:walking:` |
| :walking_man: | `:walking_man:` |
| :walking_woman: | `:walking_woman:` |
| :standing_person: | `:standing_person:` |
| :standing_man: | `:standing_man:` |
| :standing_woman: | `:standing_woman:` |
| :kneeling_person: | `:kneeling_person:` |
| :kneeling_man: | `:kneeling_man:` |
| :kneeling_woman: | `:kneeling_woman:` |
| :person_with_probing_cane: | `:person_with_probing_cane:` |
| :man_with_probing_cane: | `:man_with_probing_cane:` |
| :woman_with_probing_cane: | `:woman_with_probing_cane:` |
| :person_in_motorized_wheelchair: | `:person_in_motorized_wheelchair:` |
| :man_in_motorized_wheelchair: | `:man_in_motorized_wheelchair:` |
| :woman_in_motorized_wheelchair: | `:woman_in_motorized_wheelchair:` |
| :person_in_manual_wheelchair: | `:person_in_manual_wheelchair:` |
| :man_in_manual_wheelchair: | `:man_in_manual_wheelchair:` |
| :woman_in_manual_wheelchair: | `:woman_in_manual_wheelchair:` |
| :runner: | `:runner:` `:running:` |
| :running_man: | `:running_man:` |
| :running_woman: | `:running_woman:` |
| :woman_dancing: | `:woman_dancing:` `:dancer:` |
| :man_dancing: | `:man_dancing:` |
| :business_suit_levitating: | `:business_suit_levitating:` |
| :dancers: | `:dancers:` |
| :dancing_men: | `:dancing_men:` |
| :dancing_women: | `:dancing_women:` |
| :sauna_person: | `:sauna_person:` |
| :sauna_man: | `:sauna_man:` |
| :sauna_woman: | `:sauna_woman:` |
| :climbing: | `:climbing:` |
| :climbing_man: | `:climbing_man:` |
| :climbing_woman: | `:climbing_woman:` |
| :person_fencing: | `:person_fencing:` |
| :horse_racing: | `:horse_racing:` |
| :skier: | `:skier:` |
| :snowboarder: | `:snowboarder:` |
| :golfing: | `:golfing:` |
| :golfing_man: | `:golfing_man:` |
| :golfing_woman: | `:golfing_woman:` |
| :surfer: | `:surfer:` |
| :surfing_man: | `:surfing_man:` |
| :surfing_woman: | `:surfing_woman:` |
| :rowboat: | `:rowboat:` |
| :rowing_man: | `:rowing_man:` |
| :rowing_woman: | `:rowing_woman:` |
| :swimmer: | `:swimmer:` |
| :swimming_man: | `:swimming_man:` |
| :swimming_woman: | `:swimming_woman:` |
| :bouncing_ball_person: | `:bouncing_ball_person:` |
| :bouncing_ball_man: | `:bouncing_ball_man:` `:basketball_man:` |
| :bouncing_ball_woman: | `:bouncing_ball_woman:` `:basketball_woman:` |
| :weight_lifting: | `:weight_lifting:` |
| :weight_lifting_man: | `:weight_lifting_man:` |
| :weight_lifting_woman: | `:weight_lifting_woman:` |
| :bicyclist: | `:bicyclist:` |
| :biking_man: | `:biking_man:` |
| :biking_woman: | `:biking_woman:` |
| :mountain_bicyclist: | `:mountain_bicyclist:` |
| :mountain_biking_man: | `:mountain_biking_man:` |
| :mountain_biking_woman: | `:mountain_biking_woman:` |
| :cartwheeling: | `:cartwheeling:` |
| :man_cartwheeling: | `:man_cartwheeling:` |
| :woman_cartwheeling: | `:woman_cartwheeling:` |
| :wrestling: | `:wrestling:` |
| :men_wrestling: | `:men_wrestling:` |
| :women_wrestling: | `:women_wrestling:` |
| :water_polo: | `:water_polo:` |
| :man_playing_water_polo: | `:man_playing_water_polo:` |
| :woman_playing_water_polo: | `:woman_playing_water_polo:` |
| :handball_person: | `:handball_person:` |
| :man_playing_handball: | `:man_playing_handball:` |
| :woman_playing_handball: | `:woman_playing_handball:` |
| :juggling_person: | `:juggling_person:` |
| :man_juggling: | `:man_juggling:` |
| :woman_juggling: | `:woman_juggling:` |
| :lotus_position: | `:lotus_position:` |
| :lotus_position_man: | `:lotus_position_man:` |
| :lotus_position_woman: | `:lotus_position_woman:` |
| :bath: | `:bath:` |
| :sleeping_bed: | `:sleeping_bed:` |
| :people_holding_hands: | `:people_holding_hands:` |
| :two_women_holding_hands: | `:two_women_holding_hands:` |
| :couple: | `:couple:` |
| :two_men_holding_hands: | `:two_men_holding_hands:` |
| :couplekiss: | `:couplekiss:` |
| :couplekiss_man_woman: | `:couplekiss_man_woman:` |
| :couplekiss_man_man: | `:couplekiss_man_man:` |
| :couplekiss_woman_woman: | `:couplekiss_woman_woman:` |
| :couple_with_heart: | `:couple_with_heart:` |
| :couple_with_heart_woman_man: | `:couple_with_heart_woman_man:` |
| :couple_with_heart_man_man: | `:couple_with_heart_man_man:` |
| :couple_with_heart_woman_woman: | `:couple_with_heart_woman_woman:` |
| :family_man_woman_boy: | `:family_man_woman_boy:` |
| :family_man_woman_girl: | `:family_man_woman_girl:` |
| :family_man_woman_girl_boy: | `:family_man_woman_girl_boy:` |
| :family_man_woman_boy_boy: | `:family_man_woman_boy_boy:` |
| :family_man_woman_girl_girl: | `:family_man_woman_girl_girl:` |
| :family_man_man_boy: | `:family_man_man_boy:` |
| :family_man_man_girl: | `:family_man_man_girl:` |
| :family_man_man_girl_boy: | `:family_man_man_girl_boy:` |
| :family_man_man_boy_boy: | `:family_man_man_boy_boy:` |
| :family_man_man_girl_girl: | `:family_man_man_girl_girl:` |
| :family_woman_woman_boy: | `:family_woman_woman_boy:` |
| :family_woman_woman_girl: | `:family_woman_woman_girl:` |
| :family_woman_woman_girl_boy: | `:family_woman_woman_girl_boy:` |
| :family_woman_woman_boy_boy: | `:family_woman_woman_boy_boy:` |
| :family_woman_woman_girl_girl: | `:family_woman_woman_girl_girl:` |
| :family_man_boy: | `:family_man_boy:` |
| :family_man_boy_boy: | `:family_man_boy_boy:` |
| :family_man_girl: | `:family_man_girl:` |
| :family_man_girl_boy: | `:family_man_girl_boy:` |
| :family_man_girl_girl: | `:family_man_girl_girl:` |
| :family_woman_boy: | `:family_woman_boy:` |
| :family_woman_boy_boy: | `:family_woman_boy_boy:` |
| :family_woman_girl: | `:family_woman_girl:` |
| :family_woman_girl_boy: | `:family_woman_girl_boy:` |
| :family_woman_girl_girl: | `:family_woman_girl_girl:` |
| :speaking_head: | `:speaking_head:` |
| :bust_in_silhouette: | `:bust_in_silhouette:` |
| :busts_in_silhouette: | `:busts_in_silhouette:` |
| :people_hugging: | `:people_hugging:` |
| :family: | `:family:` |
| :footprints: | `:footprints:` |
### Animals & nature
| Emoji | Shortcodes |
|-------|------------|
| :monkey_face: | `:monkey_face:` |
| :monkey: | `:monkey:` |
| :gorilla: | `:gorilla:` |
| :orangutan: | `:orangutan:` |
| :dog: | `:dog:` |
| :dog2: | `:dog2:` |
| :guide_dog: | `:guide_dog:` |
| :service_dog: | `:service_dog:` |
| :poodle: | `:poodle:` |
| :wolf: | `:wolf:` |
| :fox_face: | `:fox_face:` |
| :raccoon: | `:raccoon:` |
| :cat: | `:cat:` |
| :cat2: | `:cat2:` |
| :black_cat: | `:black_cat:` |
| :lion: | `:lion:` |
| :tiger: | `:tiger:` |
| :tiger2: | `:tiger2:` |
| :leopard: | `:leopard:` |
| :horse: | `:horse:` |
| :moose: | `:moose:` |
| :donkey: | `:donkey:` |
| :racehorse: | `:racehorse:` |
| :unicorn: | `:unicorn:` |
| :zebra: | `:zebra:` |
| :deer: | `:deer:` |
| :bison: | `:bison:` |
| :cow: | `:cow:` |
| :ox: | `:ox:` |
| :water_buffalo: | `:water_buffalo:` |
| :cow2: | `:cow2:` |
| :pig: | `:pig:` |
| :pig2: | `:pig2:` |
| :boar: | `:boar:` |
| :pig_nose: | `:pig_nose:` |
| :ram: | `:ram:` |
| :sheep: | `:sheep:` |
| :goat: | `:goat:` |
| :dromedary_camel: | `:dromedary_camel:` |
| :camel: | `:camel:` |
| :llama: | `:llama:` |
| :giraffe: | `:giraffe:` |
| :elephant: | `:elephant:` |
| :mammoth: | `:mammoth:` |
| :rhinoceros: | `:rhinoceros:` |
| :hippopotamus: | `:hippopotamus:` |
| :mouse: | `:mouse:` |
| :mouse2: | `:mouse2:` |
| :rat: | `:rat:` |
| :hamster: | `:hamster:` |
| :rabbit: | `:rabbit:` |
| :rabbit2: | `:rabbit2:` |
| :chipmunk: | `:chipmunk:` |
| :beaver: | `:beaver:` |
| :hedgehog: | `:hedgehog:` |
| :bat: | `:bat:` |
| :bear: | `:bear:` |
| :polar_bear: | `:polar_bear:` |
| :koala: | `:koala:` |
| :panda_face: | `:panda_face:` |
| :sloth: | `:sloth:` |
| :otter: | `:otter:` |
| :skunk: | `:skunk:` |
| :kangaroo: | `:kangaroo:` |
| :badger: | `:badger:` |
| :feet: | `:feet:` `:paw_prints:` |
| :turkey: | `:turkey:` |
| :chicken: | `:chicken:` |
| :rooster: | `:rooster:` |
| :hatching_chick: | `:hatching_chick:` |
| :baby_chick: | `:baby_chick:` |
| :hatched_chick: | `:hatched_chick:` |
| :bird: | `:bird:` |
| :penguin: | `:penguin:` |
| :dove: | `:dove:` |
| :eagle: | `:eagle:` |
| :duck: | `:duck:` |
| :swan: | `:swan:` |
| :owl: | `:owl:` |
| :dodo: | `:dodo:` |
| :feather: | `:feather:` |
| :flamingo: | `:flamingo:` |
| :peacock: | `:peacock:` |
| :parrot: | `:parrot:` |
| :wing: | `:wing:` |
| :black_bird: | `:black_bird:` |
| :goose: | `:goose:` |
| :frog: | `:frog:` |
| :crocodile: | `:crocodile:` |
| :turtle: | `:turtle:` |
| :lizard: | `:lizard:` |
| :snake: | `:snake:` |
| :dragon_face: | `:dragon_face:` |
| :dragon: | `:dragon:` |
| :sauropod: | `:sauropod:` |
| :t-rex: | `:t-rex:` |
| :whale: | `:whale:` |
| :whale2: | `:whale2:` |
| :dolphin: | `:dolphin:` `:flipper:` |
| :seal: | `:seal:` |
| :fish: | `:fish:` |
| :tropical_fish: | `:tropical_fish:` |
| :blowfish: | `:blowfish:` |
| :shark: | `:shark:` |
| :octopus: | `:octopus:` |
| :shell: | `:shell:` |
| :coral: | `:coral:` |
| :jellyfish: | `:jellyfish:` |
| :crab: | `:crab:` |
| :lobster: | `:lobster:` |
| :shrimp: | `:shrimp:` |
| :squid: | `:squid:` |
| :oyster: | `:oyster:` |
| :snail: | `:snail:` |
| :butterfly: | `:butterfly:` |
| :bug: | `:bug:` |
| :ant: | `:ant:` |
| :bee: | `:bee:` `:honeybee:` |
| :beetle: | `:beetle:` |
| :lady_beetle: | `:lady_beetle:` |
| :cricket: | `:cricket:` |
| :cockroach: | `:cockroach:` |
| :spider: | `:spider:` |
| :spider_web: | `:spider_web:` |
| :scorpion: | `:scorpion:` |
| :mosquito: | `:mosquito:` |
| :fly: | `:fly:` |
| :worm: | `:worm:` |
| :microbe: | `:microbe:` |
| :bouquet: | `:bouquet:` |
| :cherry_blossom: | `:cherry_blossom:` |
| :white_flower: | `:white_flower:` |
| :lotus: | `:lotus:` |
| :rosette: | `:rosette:` |
| :rose: | `:rose:` |
| :wilted_flower: | `:wilted_flower:` |
| :hibiscus: | `:hibiscus:` |
| :sunflower: | `:sunflower:` |
| :blossom: | `:blossom:` |
| :tulip: | `:tulip:` |
| :hyacinth: | `:hyacinth:` |
| :seedling: | `:seedling:` |
| :potted_plant: | `:potted_plant:` |
| :evergreen_tree: | `:evergreen_tree:` |
| :deciduous_tree: | `:deciduous_tree:` |
| :palm_tree: | `:palm_tree:` |
| :cactus: | `:cactus:` |
| :ear_of_rice: | `:ear_of_rice:` |
| :herb: | `:herb:` |
| :shamrock: | `:shamrock:` |
| :four_leaf_clover: | `:four_leaf_clover:` |
| :maple_leaf: | `:maple_leaf:` |
| :fallen_leaf: | `:fallen_leaf:` |
| :leaves: | `:leaves:` |
| :empty_nest: | `:empty_nest:` |
| :nest_with_eggs: | `:nest_with_eggs:` |
| :mushroom: | `:mushroom:` |
### Food & drink
| Emoji | Shortcodes |
|-------|------------|
| :grapes: | `:grapes:` |
| :melon: | `:melon:` |
| :watermelon: | `:watermelon:` |
| :tangerine: | `:tangerine:` `:orange:` `:mandarin:` |
| :lemon: | `:lemon:` |
| :banana: | `:banana:` |
| :pineapple: | `:pineapple:` |
| :mango: | `:mango:` |
| :apple: | `:apple:` |
| :green_apple: | `:green_apple:` |
| :pear: | `:pear:` |
| :peach: | `:peach:` |
| :cherries: | `:cherries:` |
| :strawberry: | `:strawberry:` |
| :blueberries: | `:blueberries:` |
| :kiwi_fruit: | `:kiwi_fruit:` |
| :tomato: | `:tomato:` |
| :olive: | `:olive:` |
| :coconut: | `:coconut:` |
| :avocado: | `:avocado:` |
| :eggplant: | `:eggplant:` |
| :potato: | `:potato:` |
| :carrot: | `:carrot:` |
| :corn: | `:corn:` |
| :hot_pepper: | `:hot_pepper:` |
| :bell_pepper: | `:bell_pepper:` |
| :cucumber: | `:cucumber:` |
| :leafy_green: | `:leafy_green:` |
| :broccoli: | `:broccoli:` |
| :garlic: | `:garlic:` |
| :onion: | `:onion:` |
| :peanuts: | `:peanuts:` |
| :beans: | `:beans:` |
| :chestnut: | `:chestnut:` |
| :ginger_root: | `:ginger_root:` |
| :pea_pod: | `:pea_pod:` |
| :bread: | `:bread:` |
| :croissant: | `:croissant:` |
| :baguette_bread: | `:baguette_bread:` |
| :flatbread: | `:flatbread:` |
| :pretzel: | `:pretzel:` |
| :bagel: | `:bagel:` |
| :pancakes: | `:pancakes:` |
| :waffle: | `:waffle:` |
| :cheese: | `:cheese:` |
| :meat_on_bone: | `:meat_on_bone:` |
| :poultry_leg: | `:poultry_leg:` |
| :cut_of_meat: | `:cut_of_meat:` |
| :bacon: | `:bacon:` |
| :hamburger: | `:hamburger:` |
| :fries: | `:fries:` |
| :pizza: | `:pizza:` |
| :hotdog: | `:hotdog:` |
| :sandwich: | `:sandwich:` |
| :taco: | `:taco:` |
| :burrito: | `:burrito:` |
| :tamale: | `:tamale:` |
| :stuffed_flatbread: | `:stuffed_flatbread:` |
| :falafel: | `:falafel:` |
| :egg: | `:egg:` |
| :fried_egg: | `:fried_egg:` |
| :shallow_pan_of_food: | `:shallow_pan_of_food:` |
| :stew: | `:stew:` |
| :fondue: | `:fondue:` |
| :bowl_with_spoon: | `:bowl_with_spoon:` |
| :green_salad: | `:green_salad:` |
| :popcorn: | `:popcorn:` |
| :butter: | `:butter:` |
| :salt: | `:salt:` |
| :canned_food: | `:canned_food:` |
| :bento: | `:bento:` |
| :rice_cracker: | `:rice_cracker:` |
| :rice_ball: | `:rice_ball:` |
| :rice: | `:rice:` |
| :curry: | `:curry:` |
| :ramen: | `:ramen:` |
| :spaghetti: | `:spaghetti:` |
| :sweet_potato: | `:sweet_potato:` |
| :oden: | `:oden:` |
| :sushi: | `:sushi:` |
| :fried_shrimp: | `:fried_shrimp:` |
| :fish_cake: | `:fish_cake:` |
| :moon_cake: | `:moon_cake:` |
| :dango: | `:dango:` |
| :dumpling: | `:dumpling:` |
| :fortune_cookie: | `:fortune_cookie:` |
| :takeout_box: | `:takeout_box:` |
| :icecream: | `:icecream:` |
| :shaved_ice: | `:shaved_ice:` |
| :ice_cream: | `:ice_cream:` |
| :doughnut: | `:doughnut:` |
| :cookie: | `:cookie:` |
| :birthday: | `:birthday:` |
| :cake: | `:cake:` |
| :cupcake: | `:cupcake:` |
| :pie: | `:pie:` |
| :chocolate_bar: | `:chocolate_bar:` |
| :candy: | `:candy:` |
| :lollipop: | `:lollipop:` |
| :custard: | `:custard:` |
| :honey_pot: | `:honey_pot:` |
| :baby_bottle: | `:baby_bottle:` |
| :milk_glass: | `:milk_glass:` |
| :coffee: | `:coffee:` |
| :teapot: | `:teapot:` |
| :tea: | `:tea:` |
| :sake: | `:sake:` |
| :champagne: | `:champagne:` |
| :wine_glass: | `:wine_glass:` |
| :cocktail: | `:cocktail:` |
| :tropical_drink: | `:tropical_drink:` |
| :beer: | `:beer:` |
| :beers: | `:beers:` |
| :clinking_glasses: | `:clinking_glasses:` |
| :tumbler_glass: | `:tumbler_glass:` |
| :pouring_liquid: | `:pouring_liquid:` |
| :cup_with_straw: | `:cup_with_straw:` |
| :bubble_tea: | `:bubble_tea:` |
| :beverage_box: | `:beverage_box:` |
| :mate: | `:mate:` |
| :ice_cube: | `:ice_cube:` |
| :chopsticks: | `:chopsticks:` |
| :plate_with_cutlery: | `:plate_with_cutlery:` |
| :fork_and_knife: | `:fork_and_knife:` |
| :spoon: | `:spoon:` |
| :hocho: | `:hocho:` `:knife:` |
| :jar: | `:jar:` |
| :amphora: | `:amphora:` |
### Travel & places
| Emoji | Shortcodes |
|-------|------------|
| :earth_africa: | `:earth_africa:` |
| :earth_americas: | `:earth_americas:` |
| :earth_asia: | `:earth_asia:` |
| :globe_with_meridians: | `:globe_with_meridians:` |
| :world_map: | `:world_map:` |
| :japan: | `:japan:` |
| :compass: | `:compass:` |
| :mountain_snow: | `:mountain_snow:` |
| :mountain: | `:mountain:` |
| :volcano: | `:volcano:` |
| :mount_fuji: | `:mount_fuji:` |
| :camping: | `:camping:` |
| :beach_umbrella: | `:beach_umbrella:` |
| :desert: | `:desert:` |
| :desert_island: | `:desert_island:` |
| :national_park: | `:national_park:` |
| :stadium: | `:stadium:` |
| :classical_building: | `:classical_building:` |
| :building_construction: | `:building_construction:` |
| :bricks: | `:bricks:` |
| :rock: | `:rock:` |
| :wood: | `:wood:` |
| :hut: | `:hut:` |
| :houses: | `:houses:` |
| :derelict_house: | `:derelict_house:` |
| :house: | `:house:` |
| :house_with_garden: | `:house_with_garden:` |
| :office: | `:office:` |
| :post_office: | `:post_office:` |
| :european_post_office: | `:european_post_office:` |
| :hospital: | `:hospital:` |
| :bank: | `:bank:` |
| :hotel: | `:hotel:` |
| :love_hotel: | `:love_hotel:` |
| :convenience_store: | `:convenience_store:` |
| :school: | `:school:` |
| :department_store: | `:department_store:` |
| :factory: | `:factory:` |
| :japanese_castle: | `:japanese_castle:` |
| :european_castle: | `:european_castle:` |
| :wedding: | `:wedding:` |
| :tokyo_tower: | `:tokyo_tower:` |
| :statue_of_liberty: | `:statue_of_liberty:` |
| :church: | `:church:` |
| :mosque: | `:mosque:` |
| :hindu_temple: | `:hindu_temple:` |
| :synagogue: | `:synagogue:` |
| :shinto_shrine: | `:shinto_shrine:` |
| :kaaba: | `:kaaba:` |
| :fountain: | `:fountain:` |
| :tent: | `:tent:` |
| :foggy: | `:foggy:` |
| :night_with_stars: | `:night_with_stars:` |
| :cityscape: | `:cityscape:` |
| :sunrise_over_mountains: | `:sunrise_over_mountains:` |
| :sunrise: | `:sunrise:` |
| :city_sunset: | `:city_sunset:` |
| :city_sunrise: | `:city_sunrise:` |
| :bridge_at_night: | `:bridge_at_night:` |
| :hotsprings: | `:hotsprings:` |
| :carousel_horse: | `:carousel_horse:` |
| :playground_slide: | `:playground_slide:` |
| :ferris_wheel: | `:ferris_wheel:` |
| :roller_coaster: | `:roller_coaster:` |
| :barber: | `:barber:` |
| :circus_tent: | `:circus_tent:` |
| :steam_locomotive: | `:steam_locomotive:` |
| :railway_car: | `:railway_car:` |
| :bullettrain_side: | `:bullettrain_side:` |
| :bullettrain_front: | `:bullettrain_front:` |
| :train2: | `:train2:` |
| :metro: | `:metro:` |
| :light_rail: | `:light_rail:` |
| :station: | `:station:` |
| :tram: | `:tram:` |
| :monorail: | `:monorail:` |
| :mountain_railway: | `:mountain_railway:` |
| :train: | `:train:` |
| :bus: | `:bus:` |
| :oncoming_bus: | `:oncoming_bus:` |
| :trolleybus: | `:trolleybus:` |
| :minibus: | `:minibus:` |
| :ambulance: | `:ambulance:` |
| :fire_engine: | `:fire_engine:` |
| :police_car: | `:police_car:` |
| :oncoming_police_car: | `:oncoming_police_car:` |
| :taxi: | `:taxi:` |
| :oncoming_taxi: | `:oncoming_taxi:` |
| :car: | `:car:` `:red_car:` |
| :oncoming_automobile: | `:oncoming_automobile:` |
| :blue_car: | `:blue_car:` |
| :pickup_truck: | `:pickup_truck:` |
| :truck: | `:truck:` |
| :articulated_lorry: | `:articulated_lorry:` |
| :tractor: | `:tractor:` |
| :racing_car: | `:racing_car:` |
| :motorcycle: | `:motorcycle:` |
| :motor_scooter: | `:motor_scooter:` |
| :manual_wheelchair: | `:manual_wheelchair:` |
| :motorized_wheelchair: | `:motorized_wheelchair:` |
| :auto_rickshaw: | `:auto_rickshaw:` |
| :bike: | `:bike:` |
| :kick_scooter: | `:kick_scooter:` |
| :skateboard: | `:skateboard:` |
| :roller_skate: | `:roller_skate:` |
| :busstop: | `:busstop:` |
| :motorway: | `:motorway:` |
| :railway_track: | `:railway_track:` |
| :oil_drum: | `:oil_drum:` |
| :fuelpump: | `:fuelpump:` |
| :wheel: | `:wheel:` |
| :rotating_light: | `:rotating_light:` |
| :traffic_light: | `:traffic_light:` |
| :vertical_traffic_light: | `:vertical_traffic_light:` |
| :stop_sign: | `:stop_sign:` |
| :construction: | `:construction:` |
| :anchor: | `:anchor:` |
| :ring_buoy: | `:ring_buoy:` |
| :boat: | `:boat:` `:sailboat:` |
| :canoe: | `:canoe:` |
| :speedboat: | `:speedboat:` |
| :passenger_ship: | `:passenger_ship:` |
| :ferry: | `:ferry:` |
| :motor_boat: | `:motor_boat:` |
| :ship: | `:ship:` |
| :airplane: | `:airplane:` |
| :small_airplane: | `:small_airplane:` |
| :flight_departure: | `:flight_departure:` |
| :flight_arrival: | `:flight_arrival:` |
| :parachute: | `:parachute:` |
| :seat: | `:seat:` |
| :helicopter: | `:helicopter:` |
| :suspension_railway: | `:suspension_railway:` |
| :mountain_cableway: | `:mountain_cableway:` |
| :aerial_tramway: | `:aerial_tramway:` |
| :artificial_satellite: | `:artificial_satellite:` |
| :rocket: | `:rocket:` |
| :flying_saucer: | `:flying_saucer:` |
| :bellhop_bell: | `:bellhop_bell:` |
| :luggage: | `:luggage:` |
| :hourglass: | `:hourglass:` |
| :hourglass_flowing_sand: | `:hourglass_flowing_sand:` |
| :watch: | `:watch:` |
| :alarm_clock: | `:alarm_clock:` |
| :stopwatch: | `:stopwatch:` |
| :timer_clock: | `:timer_clock:` |
| :mantelpiece_clock: | `:mantelpiece_clock:` |
| :clock12: | `:clock12:` |
| :clock1230: | `:clock1230:` |
| :clock1: | `:clock1:` |
| :clock130: | `:clock130:` |
| :clock2: | `:clock2:` |
| :clock230: | `:clock230:` |
| :clock3: | `:clock3:` |
| :clock330: | `:clock330:` |
| :clock4: | `:clock4:` |
| :clock430: | `:clock430:` |
| :clock5: | `:clock5:` |
| :clock530: | `:clock530:` |
| :clock6: | `:clock6:` |
| :clock630: | `:clock630:` |
| :clock7: | `:clock7:` |
| :clock730: | `:clock730:` |
| :clock8: | `:clock8:` |
| :clock830: | `:clock830:` |
| :clock9: | `:clock9:` |
| :clock930: | `:clock930:` |
| :clock10: | `:clock10:` |
| :clock1030: | `:clock1030:` |
| :clock11: | `:clock11:` |
| :clock1130: | `:clock1130:` |
| :new_moon: | `:new_moon:` |
| :waxing_crescent_moon: | `:waxing_crescent_moon:` |
| :first_quarter_moon: | `:first_quarter_moon:` |
| :moon: | `:moon:` `:waxing_gibbous_moon:` |
| :full_moon: | `:full_moon:` |
| :waning_gibbous_moon: | `:waning_gibbous_moon:` |
| :last_quarter_moon: | `:last_quarter_moon:` |
| :waning_crescent_moon: | `:waning_crescent_moon:` |
| :crescent_moon: | `:crescent_moon:` |
| :new_moon_with_face: | `:new_moon_with_face:` |
| :first_quarter_moon_with_face: | `:first_quarter_moon_with_face:` |
| :last_quarter_moon_with_face: | `:last_quarter_moon_with_face:` |
| :thermometer: | `:thermometer:` |
| :sunny: | `:sunny:` |
| :full_moon_with_face: | `:full_moon_with_face:` |
| :sun_with_face: | `:sun_with_face:` |
| :ringed_planet: | `:ringed_planet:` |
| :star: | `:star:` |
| :star2: | `:star2:` |
| :stars: | `:stars:` |
| :milky_way: | `:milky_way:` |
| :cloud: | `:cloud:` |
| :partly_sunny: | `:partly_sunny:` |
| :cloud_with_lightning_and_rain: | `:cloud_with_lightning_and_rain:` |
| :sun_behind_small_cloud: | `:sun_behind_small_cloud:` |
| :sun_behind_large_cloud: | `:sun_behind_large_cloud:` |
| :sun_behind_rain_cloud: | `:sun_behind_rain_cloud:` |
| :cloud_with_rain: | `:cloud_with_rain:` |
| :cloud_with_snow: | `:cloud_with_snow:` |
| :cloud_with_lightning: | `:cloud_with_lightning:` |
| :tornado: | `:tornado:` |
| :fog: | `:fog:` |
| :wind_face: | `:wind_face:` |
| :cyclone: | `:cyclone:` |
| :rainbow: | `:rainbow:` |
| :closed_umbrella: | `:closed_umbrella:` |
| :open_umbrella: | `:open_umbrella:` |
| :umbrella: | `:umbrella:` |
| :parasol_on_ground: | `:parasol_on_ground:` |
| :zap: | `:zap:` |
| :snowflake: | `:snowflake:` |
| :snowman_with_snow: | `:snowman_with_snow:` |
| :snowman: | `:snowman:` |
| :comet: | `:comet:` |
| :fire: | `:fire:` |
| :droplet: | `:droplet:` |
| :ocean: | `:ocean:` |
### Activities
| Emoji | Shortcodes |
|-------|------------|
| :jack_o_lantern: | `:jack_o_lantern:` |
| :christmas_tree: | `:christmas_tree:` |
| :fireworks: | `:fireworks:` |
| :sparkler: | `:sparkler:` |
| :firecracker: | `:firecracker:` |
| :sparkles: | `:sparkles:` |
| :balloon: | `:balloon:` |
| :tada: | `:tada:` |
| :confetti_ball: | `:confetti_ball:` |
| :tanabata_tree: | `:tanabata_tree:` |
| :bamboo: | `:bamboo:` |
| :dolls: | `:dolls:` |
| :flags: | `:flags:` |
| :wind_chime: | `:wind_chime:` |
| :rice_scene: | `:rice_scene:` |
| :red_envelope: | `:red_envelope:` |
| :ribbon: | `:ribbon:` |
| :gift: | `:gift:` |
| :reminder_ribbon: | `:reminder_ribbon:` |
| :tickets: | `:tickets:` |
| :ticket: | `:ticket:` |
| :medal_military: | `:medal_military:` |
| :trophy: | `:trophy:` |
| :medal_sports: | `:medal_sports:` |
| :1st_place_medal: | `:1st_place_medal:` |
| :2nd_place_medal: | `:2nd_place_medal:` |
| :3rd_place_medal: | `:3rd_place_medal:` |
| :soccer: | `:soccer:` |
| :baseball: | `:baseball:` |
| :softball: | `:softball:` |
| :basketball: | `:basketball:` |
| :volleyball: | `:volleyball:` |
| :football: | `:football:` |
| :rugby_football: | `:rugby_football:` |
| :tennis: | `:tennis:` |
| :flying_disc: | `:flying_disc:` |
| :bowling: | `:bowling:` |
| :cricket_game: | `:cricket_game:` |
| :field_hockey: | `:field_hockey:` |
| :ice_hockey: | `:ice_hockey:` |
| :lacrosse: | `:lacrosse:` |
| :ping_pong: | `:ping_pong:` |
| :badminton: | `:badminton:` |
| :boxing_glove: | `:boxing_glove:` |
| :martial_arts_uniform: | `:martial_arts_uniform:` |
| :goal_net: | `:goal_net:` |
| :golf: | `:golf:` |
| :ice_skate: | `:ice_skate:` |
| :fishing_pole_and_fish: | `:fishing_pole_and_fish:` |
| :diving_mask: | `:diving_mask:` |
| :running_shirt_with_sash: | `:running_shirt_with_sash:` |
| :ski: | `:ski:` |
| :sled: | `:sled:` |
| :curling_stone: | `:curling_stone:` |
| :dart: | `:dart:` |
| :yo_yo: | `:yo_yo:` |
| :kite: | `:kite:` |
| :gun: | `:gun:` |
| :8ball: | `:8ball:` |
| :crystal_ball: | `:crystal_ball:` |
| :magic_wand: | `:magic_wand:` |
| :video_game: | `:video_game:` |
| :joystick: | `:joystick:` |
| :slot_machine: | `:slot_machine:` |
| :game_die: | `:game_die:` |
| :jigsaw: | `:jigsaw:` |
| :teddy_bear: | `:teddy_bear:` |
| :pinata: | `:pinata:` |
| :mirror_ball: | `:mirror_ball:` |
| :nesting_dolls: | `:nesting_dolls:` |
| :spades: | `:spades:` |
| :hearts: | `:hearts:` |
| :diamonds: | `:diamonds:` |
| :clubs: | `:clubs:` |
| :chess_pawn: | `:chess_pawn:` |
| :black_joker: | `:black_joker:` |
| :mahjong: | `:mahjong:` |
| :flower_playing_cards: | `:flower_playing_cards:` |
| :performing_arts: | `:performing_arts:` |
| :framed_picture: | `:framed_picture:` |
| :art: | `:art:` |
| :thread: | `:thread:` |
| :sewing_needle: | `:sewing_needle:` |
| :yarn: | `:yarn:` |
| :knot: | `:knot:` |
### Objects
| Emoji | Shortcodes |
|-------|------------|
| :eyeglasses: | `:eyeglasses:` |
| :dark_sunglasses: | `:dark_sunglasses:` |
| :goggles: | `:goggles:` |
| :lab_coat: | `:lab_coat:` |
| :safety_vest: | `:safety_vest:` |
| :necktie: | `:necktie:` |
| :shirt: | `:shirt:` `:tshirt:` |
| :jeans: | `:jeans:` |
| :scarf: | `:scarf:` |
| :gloves: | `:gloves:` |
| :coat: | `:coat:` |
| :socks: | `:socks:` |
| :dress: | `:dress:` |
| :kimono: | `:kimono:` |
| :sari: | `:sari:` |
| :one_piece_swimsuit: | `:one_piece_swimsuit:` |
| :swim_brief: | `:swim_brief:` |
| :shorts: | `:shorts:` |
| :bikini: | `:bikini:` |
| :womans_clothes: | `:womans_clothes:` |
| :folding_hand_fan: | `:folding_hand_fan:` |
| :purse: | `:purse:` |
| :handbag: | `:handbag:` |
| :pouch: | `:pouch:` |
| :shopping: | `:shopping:` |
| :school_satchel: | `:school_satchel:` |
| :thong_sandal: | `:thong_sandal:` |
| :mans_shoe: | `:mans_shoe:` `:shoe:` |
| :athletic_shoe: | `:athletic_shoe:` |
| :hiking_boot: | `:hiking_boot:` |
| :flat_shoe: | `:flat_shoe:` |
| :high_heel: | `:high_heel:` |
| :sandal: | `:sandal:` |
| :ballet_shoes: | `:ballet_shoes:` |
| :boot: | `:boot:` |
| :hair_pick: | `:hair_pick:` |
| :crown: | `:crown:` |
| :womans_hat: | `:womans_hat:` |
| :tophat: | `:tophat:` |
| :mortar_board: | `:mortar_board:` |
| :billed_cap: | `:billed_cap:` |
| :military_helmet: | `:military_helmet:` |
| :rescue_worker_helmet: | `:rescue_worker_helmet:` |
| :prayer_beads: | `:prayer_beads:` |
| :lipstick: | `:lipstick:` |
| :ring: | `:ring:` |
| :gem: | `:gem:` |
| :mute: | `:mute:` |
| :speaker: | `:speaker:` |
| :sound: | `:sound:` |
| :loud_sound: | `:loud_sound:` |
| :loudspeaker: | `:loudspeaker:` |
| :mega: | `:mega:` |
| :postal_horn: | `:postal_horn:` |
| :bell: | `:bell:` |
| :no_bell: | `:no_bell:` |
| :musical_score: | `:musical_score:` |
| :musical_note: | `:musical_note:` |
| :notes: | `:notes:` |
| :studio_microphone: | `:studio_microphone:` |
| :level_slider: | `:level_slider:` |
| :control_knobs: | `:control_knobs:` |
| :microphone: | `:microphone:` |
| :headphones: | `:headphones:` |
| :radio: | `:radio:` |
| :saxophone: | `:saxophone:` |
| :trumpet: | `:trumpet:` |
| :accordion: | `:accordion:` |
| :guitar: | `:guitar:` |
| :musical_keyboard: | `:musical_keyboard:` |
| :violin: | `:violin:` |
| :banjo: | `:banjo:` |
| :drum: | `:drum:` |
| :long_drum: | `:long_drum:` |
| :maracas: | `:maracas:` |
| :flute: | `:flute:` |
| :iphone: | `:iphone:` |
| :calling: | `:calling:` |
| :phone: | `:phone:` `:telephone:` |
| :telephone_receiver: | `:telephone_receiver:` |
| :pager: | `:pager:` |
| :fax: | `:fax:` |
| :battery: | `:battery:` |
| :low_battery: | `:low_battery:` |
| :electric_plug: | `:electric_plug:` |
| :computer: | `:computer:` |
| :desktop_computer: | `:desktop_computer:` |
| :printer: | `:printer:` |
| :keyboard: | `:keyboard:` |
| :computer_mouse: | `:computer_mouse:` |
| :trackball: | `:trackball:` |
| :minidisc: | `:minidisc:` |
| :floppy_disk: | `:floppy_disk:` |
| :cd: | `:cd:` |
| :dvd: | `:dvd:` |
| :abacus: | `:abacus:` |
| :movie_camera: | `:movie_camera:` |
| :film_strip: | `:film_strip:` |
| :film_projector: | `:film_projector:` |
| :clapper: | `:clapper:` |
| :tv: | `:tv:` |
| :camera: | `:camera:` |
| :camera_flash: | `:camera_flash:` |
| :video_camera: | `:video_camera:` |
| :vhs: | `:vhs:` |
| :mag: | `:mag:` |
| :mag_right: | `:mag_right:` |
| :candle: | `:candle:` |
| :bulb: | `:bulb:` |
| :flashlight: | `:flashlight:` |
| :izakaya_lantern: | `:izakaya_lantern:` `:lantern:` |
| :diya_lamp: | `:diya_lamp:` |
| :notebook_with_decorative_cover: | `:notebook_with_decorative_cover:` |
| :closed_book: | `:closed_book:` |
| :book: | `:book:` `:open_book:` |
| :green_book: | `:green_book:` |
| :blue_book: | `:blue_book:` |
| :orange_book: | `:orange_book:` |
| :books: | `:books:` |
| :notebook: | `:notebook:` |
| :ledger: | `:ledger:` |
| :page_with_curl: | `:page_with_curl:` |
| :scroll: | `:scroll:` |
| :page_facing_up: | `:page_facing_up:` |
| :newspaper: | `:newspaper:` |
| :newspaper_roll: | `:newspaper_roll:` |
| :bookmark_tabs: | `:bookmark_tabs:` |
| :bookmark: | `:bookmark:` |
| :label: | `:label:` |
| :coin: | `:coin:` |
| :moneybag: | `:moneybag:` |
| :yen: | `:yen:` |
| :dollar: | `:dollar:` |
| :euro: | `:euro:` |
| :pound: | `:pound:` |
| :money_with_wings: | `:money_with_wings:` |
| :credit_card: | `:credit_card:` |
| :receipt: | `:receipt:` |
| :chart: | `:chart:` |
| :envelope: | `:envelope:` |
| :email: | `:email:` `:e-mail:` |
| :incoming_envelope: | `:incoming_envelope:` |
| :envelope_with_arrow: | `:envelope_with_arrow:` |
| :outbox_tray: | `:outbox_tray:` |
| :inbox_tray: | `:inbox_tray:` |
| :package: | `:package:` |
| :mailbox: | `:mailbox:` |
| :mailbox_closed: | `:mailbox_closed:` |
| :mailbox_with_mail: | `:mailbox_with_mail:` |
| :mailbox_with_no_mail: | `:mailbox_with_no_mail:` |
| :postbox: | `:postbox:` |
| :ballot_box: | `:ballot_box:` |
| :pencil2: | `:pencil2:` |
| :black_nib: | `:black_nib:` |
| :fountain_pen: | `:fountain_pen:` |
| :pen: | `:pen:` |
| :paintbrush: | `:paintbrush:` |
| :crayon: | `:crayon:` |
| :memo: | `:memo:` `:pencil:` |
| :briefcase: | `:briefcase:` |
| :file_folder: | `:file_folder:` |
| :open_file_folder: | `:open_file_folder:` |
| :card_index_dividers: | `:card_index_dividers:` |
| :date: | `:date:` |
| :calendar: | `:calendar:` |
| :spiral_notepad: | `:spiral_notepad:` |
| :spiral_calendar: | `:spiral_calendar:` |
| :card_index: | `:card_index:` |
| :chart_with_upwards_trend: | `:chart_with_upwards_trend:` |
| :chart_with_downwards_trend: | `:chart_with_downwards_trend:` |
| :bar_chart: | `:bar_chart:` |
| :clipboard: | `:clipboard:` |
| :pushpin: | `:pushpin:` |
| :round_pushpin: | `:round_pushpin:` |
| :paperclip: | `:paperclip:` |
| :paperclips: | `:paperclips:` |
| :straight_ruler: | `:straight_ruler:` |
| :triangular_ruler: | `:triangular_ruler:` |
| :scissors: | `:scissors:` |
| :card_file_box: | `:card_file_box:` |
| :file_cabinet: | `:file_cabinet:` |
| :wastebasket: | `:wastebasket:` |
| :lock: | `:lock:` |
| :unlock: | `:unlock:` |
| :lock_with_ink_pen: | `:lock_with_ink_pen:` |
| :closed_lock_with_key: | `:closed_lock_with_key:` |
| :key: | `:key:` |
| :old_key: | `:old_key:` |
| :hammer: | `:hammer:` |
| :axe: | `:axe:` |
| :pick: | `:pick:` |
| :hammer_and_pick: | `:hammer_and_pick:` |
| :hammer_and_wrench: | `:hammer_and_wrench:` |
| :dagger: | `:dagger:` |
| :crossed_swords: | `:crossed_swords:` |
| :bomb: | `:bomb:` |
| :boomerang: | `:boomerang:` |
| :bow_and_arrow: | `:bow_and_arrow:` |
| :shield: | `:shield:` |
| :carpentry_saw: | `:carpentry_saw:` |
| :wrench: | `:wrench:` |
| :screwdriver: | `:screwdriver:` |
| :nut_and_bolt: | `:nut_and_bolt:` |
| :gear: | `:gear:` |
| :clamp: | `:clamp:` |
| :balance_scale: | `:balance_scale:` |
| :probing_cane: | `:probing_cane:` |
| :link: | `:link:` |
| :chains: | `:chains:` |
| :hook: | `:hook:` |
| :toolbox: | `:toolbox:` |
| :magnet: | `:magnet:` |
| :ladder: | `:ladder:` |
| :alembic: | `:alembic:` |
| :test_tube: | `:test_tube:` |
| :petri_dish: | `:petri_dish:` |
| :dna: | `:dna:` |
| :microscope: | `:microscope:` |
| :telescope: | `:telescope:` |
| :satellite: | `:satellite:` |
| :syringe: | `:syringe:` |
| :drop_of_blood: | `:drop_of_blood:` |
| :pill: | `:pill:` |
| :adhesive_bandage: | `:adhesive_bandage:` |
| :crutch: | `:crutch:` |
| :stethoscope: | `:stethoscope:` |
| :x_ray: | `:x_ray:` |
| :door: | `:door:` |
| :elevator: | `:elevator:` |
| :mirror: | `:mirror:` |
| :window: | `:window:` |
| :bed: | `:bed:` |
| :couch_and_lamp: | `:couch_and_lamp:` |
| :chair: | `:chair:` |
| :toilet: | `:toilet:` |
| :plunger: | `:plunger:` |
| :shower: | `:shower:` |
| :bathtub: | `:bathtub:` |
| :mouse_trap: | `:mouse_trap:` |
| :razor: | `:razor:` |
| :lotion_bottle: | `:lotion_bottle:` |
| :safety_pin: | `:safety_pin:` |
| :broom: | `:broom:` |
| :basket: | `:basket:` |
| :roll_of_paper: | `:roll_of_paper:` |
| :bucket: | `:bucket:` |
| :soap: | `:soap:` |
| :bubbles: | `:bubbles:` |
| :toothbrush: | `:toothbrush:` |
| :sponge: | `:sponge:` |
| :fire_extinguisher: | `:fire_extinguisher:` |
| :shopping_cart: | `:shopping_cart:` |
| :smoking: | `:smoking:` |
| :coffin: | `:coffin:` |
| :headstone: | `:headstone:` |
| :funeral_urn: | `:funeral_urn:` |
| :nazar_amulet: | `:nazar_amulet:` |
| :hamsa: | `:hamsa:` |
| :moyai: | `:moyai:` |
| :placard: | `:placard:` |
| :identification_card: | `:identification_card:` |
### Symbols
| Emoji | Shortcodes |
|-------|------------|
| :atm: | `:atm:` |
| :put_litter_in_its_place: | `:put_litter_in_its_place:` |
| :potable_water: | `:potable_water:` |
| :wheelchair: | `:wheelchair:` |
| :mens: | `:mens:` |
| :womens: | `:womens:` |
| :restroom: | `:restroom:` |
| :baby_symbol: | `:baby_symbol:` |
| :wc: | `:wc:` |
| :passport_control: | `:passport_control:` |
| :customs: | `:customs:` |
| :baggage_claim: | `:baggage_claim:` |
| :left_luggage: | `:left_luggage:` |
| :warning: | `:warning:` |
| :children_crossing: | `:children_crossing:` |
| :no_entry: | `:no_entry:` |
| :no_entry_sign: | `:no_entry_sign:` |
| :no_bicycles: | `:no_bicycles:` |
| :no_smoking: | `:no_smoking:` |
| :do_not_litter: | `:do_not_litter:` |
| :non-potable_water: | `:non-potable_water:` |
| :no_pedestrians: | `:no_pedestrians:` |
| :no_mobile_phones: | `:no_mobile_phones:` |
| :underage: | `:underage:` |
| :radioactive: | `:radioactive:` |
| :biohazard: | `:biohazard:` |
| :arrow_up: | `:arrow_up:` |
| :arrow_upper_right: | `:arrow_upper_right:` |
| :arrow_right: | `:arrow_right:` |
| :arrow_lower_right: | `:arrow_lower_right:` |
| :arrow_down: | `:arrow_down:` |
| :arrow_lower_left: | `:arrow_lower_left:` |
| :arrow_left: | `:arrow_left:` |
| :arrow_upper_left: | `:arrow_upper_left:` |
| :arrow_up_down: | `:arrow_up_down:` |
| :left_right_arrow: | `:left_right_arrow:` |
| :leftwards_arrow_with_hook: | `:leftwards_arrow_with_hook:` |
| :arrow_right_hook: | `:arrow_right_hook:` |
| :arrow_heading_up: | `:arrow_heading_up:` |
| :arrow_heading_down: | `:arrow_heading_down:` |
| :arrows_clockwise: | `:arrows_clockwise:` |
| :arrows_counterclockwise: | `:arrows_counterclockwise:` |
| :back: | `:back:` |
| :end: | `:end:` |
| :on: | `:on:` |
| :soon: | `:soon:` |
| :top: | `:top:` |
| :place_of_worship: | `:place_of_worship:` |
| :atom_symbol: | `:atom_symbol:` |
| :om: | `:om:` |
| :star_of_david: | `:star_of_david:` |
| :wheel_of_dharma: | `:wheel_of_dharma:` |
| :yin_yang: | `:yin_yang:` |
| :latin_cross: | `:latin_cross:` |
| :orthodox_cross: | `:orthodox_cross:` |
| :star_and_crescent: | `:star_and_crescent:` |
| :peace_symbol: | `:peace_symbol:` |
| :menorah: | `:menorah:` |
| :six_pointed_star: | `:six_pointed_star:` |
| :khanda: | `:khanda:` |
| :aries: | `:aries:` |
| :taurus: | `:taurus:` |
| :gemini: | `:gemini:` |
| :cancer: | `:cancer:` |
| :leo: | `:leo:` |
| :virgo: | `:virgo:` |
| :libra: | `:libra:` |
| :scorpius: | `:scorpius:` |
| :sagittarius: | `:sagittarius:` |
| :capricorn: | `:capricorn:` |
| :aquarius: | `:aquarius:` |
| :pisces: | `:pisces:` |
| :ophiuchus: | `:ophiuchus:` |
| :twisted_rightwards_arrows: | `:twisted_rightwards_arrows:` |
| :repeat: | `:repeat:` |
| :repeat_one: | `:repeat_one:` |
| :arrow_forward: | `:arrow_forward:` |
| :fast_forward: | `:fast_forward:` |
| :next_track_button: | `:next_track_button:` |
| :play_or_pause_button: | `:play_or_pause_button:` |
| :arrow_backward: | `:arrow_backward:` |
| :rewind: | `:rewind:` |
| :previous_track_button: | `:previous_track_button:` |
| :arrow_up_small: | `:arrow_up_small:` |
| :arrow_double_up: | `:arrow_double_up:` |
| :arrow_down_small: | `:arrow_down_small:` |
| :arrow_double_down: | `:arrow_double_down:` |
| :pause_button: | `:pause_button:` |
| :stop_button: | `:stop_button:` |
| :record_button: | `:record_button:` |
| :eject_button: | `:eject_button:` |
| :cinema: | `:cinema:` |
| :low_brightness: | `:low_brightness:` |
| :high_brightness: | `:high_brightness:` |
| :signal_strength: | `:signal_strength:` |
| :wireless: | `:wireless:` |
| :vibration_mode: | `:vibration_mode:` |
| :mobile_phone_off: | `:mobile_phone_off:` |
| :female_sign: | `:female_sign:` |
| :male_sign: | `:male_sign:` |
| :transgender_symbol: | `:transgender_symbol:` |
| :heavy_multiplication_x: | `:heavy_multiplication_x:` |
| :heavy_plus_sign: | `:heavy_plus_sign:` |
| :heavy_minus_sign: | `:heavy_minus_sign:` |
| :heavy_division_sign: | `:heavy_division_sign:` |
| :heavy_equals_sign: | `:heavy_equals_sign:` |
| :infinity: | `:infinity:` |
| :bangbang: | `:bangbang:` |
| :interrobang: | `:interrobang:` |
| :question: | `:question:` |
| :grey_question: | `:grey_question:` |
| :grey_exclamation: | `:grey_exclamation:` |
| :exclamation: | `:exclamation:` `:heavy_exclamation_mark:` |
| :wavy_dash: | `:wavy_dash:` |
| :currency_exchange: | `:currency_exchange:` |
| :heavy_dollar_sign: | `:heavy_dollar_sign:` |
| :medical_symbol: | `:medical_symbol:` |
| :recycle: | `:recycle:` |
| :fleur_de_lis: | `:fleur_de_lis:` |
| :trident: | `:trident:` |
| :name_badge: | `:name_badge:` |
| :beginner: | `:beginner:` |
| :o: | `:o:` |
| :white_check_mark: | `:white_check_mark:` |
| :ballot_box_with_check: | `:ballot_box_with_check:` |
| :heavy_check_mark: | `:heavy_check_mark:` |
| :x: | `:x:` |
| :negative_squared_cross_mark: | `:negative_squared_cross_mark:` |
| :curly_loop: | `:curly_loop:` |
| :loop: | `:loop:` |
| :part_alternation_mark: | `:part_alternation_mark:` |
| :eight_spoked_asterisk: | `:eight_spoked_asterisk:` |
| :eight_pointed_black_star: | `:eight_pointed_black_star:` |
| :sparkle: | `:sparkle:` |
| :copyright: | `:copyright:` |
| :registered: | `:registered:` |
| :tm: | `:tm:` |
| :hash: | `:hash:` |
| :asterisk: | `:asterisk:` |
| :zero: | `:zero:` |
| :one: | `:one:` |
| :two: | `:two:` |
| :three: | `:three:` |
| :four: | `:four:` |
| :five: | `:five:` |
| :six: | `:six:` |
| :seven: | `:seven:` |
| :eight: | `:eight:` |
| :nine: | `:nine:` |
| :keycap_ten: | `:keycap_ten:` |
| :capital_abcd: | `:capital_abcd:` |
| :abcd: | `:abcd:` |
| :1234: | `:1234:` |
| :symbols: | `:symbols:` |
| :abc: | `:abc:` |
| :a: | `:a:` |
| :ab: | `:ab:` |
| :b: | `:b:` |
| :cl: | `:cl:` |
| :cool: | `:cool:` |
| :free: | `:free:` |
| :information_source: | `:information_source:` |
| :id: | `:id:` |
| :m: | `:m:` |
| :new: | `:new:` |
| :ng: | `:ng:` |
| :o2: | `:o2:` |
| :ok: | `:ok:` |
| :parking: | `:parking:` |
| :sos: | `:sos:` |
| :up: | `:up:` |
| :vs: | `:vs:` |
| :koko: | `:koko:` |
| :sa: | `:sa:` |
| :u6708: | `:u6708:` |
| :u6709: | `:u6709:` |
| :u6307: | `:u6307:` |
| :ideograph_advantage: | `:ideograph_advantage:` |
| :u5272: | `:u5272:` |
| :u7121: | `:u7121:` |
| :u7981: | `:u7981:` |
| :accept: | `:accept:` |
| :u7533: | `:u7533:` |
| :u5408: | `:u5408:` |
| :u7a7a: | `:u7a7a:` |
| :congratulations: | `:congratulations:` |
| :secret: | `:secret:` |
| :u55b6: | `:u55b6:` |
| :u6e80: | `:u6e80:` |
| :red_circle: | `:red_circle:` |
| :orange_circle: | `:orange_circle:` |
| :yellow_circle: | `:yellow_circle:` |
| :green_circle: | `:green_circle:` |
| :large_blue_circle: | `:large_blue_circle:` |
| :purple_circle: | `:purple_circle:` |
| :brown_circle: | `:brown_circle:` |
| :black_circle: | `:black_circle:` |
| :white_circle: | `:white_circle:` |
| :red_square: | `:red_square:` |
| :orange_square: | `:orange_square:` |
| :yellow_square: | `:yellow_square:` |
| :green_square: | `:green_square:` |
| :blue_square: | `:blue_square:` |
| :purple_square: | `:purple_square:` |
| :brown_square: | `:brown_square:` |
| :black_large_square: | `:black_large_square:` |
| :white_large_square: | `:white_large_square:` |
| :black_medium_square: | `:black_medium_square:` |
| :white_medium_square: | `:white_medium_square:` |
| :black_medium_small_square: | `:black_medium_small_square:` |
| :white_medium_small_square: | `:white_medium_small_square:` |
| :black_small_square: | `:black_small_square:` |
| :white_small_square: | `:white_small_square:` |
| :large_orange_diamond: | `:large_orange_diamond:` |
| :large_blue_diamond: | `:large_blue_diamond:` |
| :small_orange_diamond: | `:small_orange_diamond:` |
| :small_blue_diamond: | `:small_blue_diamond:` |
| :small_red_triangle: | `:small_red_triangle:` |
| :small_red_triangle_down: | `:small_red_triangle_down:` |
| :diamond_shape_with_a_dot_inside: | `:diamond_shape_with_a_dot_inside:` |
| :radio_button: | `:radio_button:` |
| :white_square_button: | `:white_square_button:` |
| :black_square_button: | `:black_square_button:` |
### Flags
| Emoji | Shortcodes |
|-------|------------|
| :checkered_flag: | `:checkered_flag:` |
| :triangular_flag_on_post: | `:triangular_flag_on_post:` |
| :crossed_flags: | `:crossed_flags:` |
| :black_flag: | `:black_flag:` |
| :white_flag: | `:white_flag:` |
| :rainbow_flag: | `:rainbow_flag:` |
| :transgender_flag: | `:transgender_flag:` |
| :pirate_flag: | `:pirate_flag:` |
| :ascension_island: | `:ascension_island:` |
| :andorra: | `:andorra:` |
| :united_arab_emirates: | `:united_arab_emirates:` |
| :afghanistan: | `:afghanistan:` |
| :antigua_barbuda: | `:antigua_barbuda:` |
| :anguilla: | `:anguilla:` |
| :albania: | `:albania:` |
| :armenia: | `:armenia:` |
| :angola: | `:angola:` |
| :antarctica: | `:antarctica:` |
| :argentina: | `:argentina:` |
| :american_samoa: | `:american_samoa:` |
| :austria: | `:austria:` |
| :australia: | `:australia:` |
| :aruba: | `:aruba:` |
| :aland_islands: | `:aland_islands:` |
| :azerbaijan: | `:azerbaijan:` |
| :bosnia_herzegovina: | `:bosnia_herzegovina:` |
| :barbados: | `:barbados:` |
| :bangladesh: | `:bangladesh:` |
| :belgium: | `:belgium:` |
| :burkina_faso: | `:burkina_faso:` |
| :bulgaria: | `:bulgaria:` |
| :bahrain: | `:bahrain:` |
| :burundi: | `:burundi:` |
| :benin: | `:benin:` |
| :st_barthelemy: | `:st_barthelemy:` |
| :bermuda: | `:bermuda:` |
| :brunei: | `:brunei:` |
| :bolivia: | `:bolivia:` |
| :caribbean_netherlands: | `:caribbean_netherlands:` |
| :brazil: | `:brazil:` |
| :bahamas: | `:bahamas:` |
| :bhutan: | `:bhutan:` |
| :bouvet_island: | `:bouvet_island:` |
| :botswana: | `:botswana:` |
| :belarus: | `:belarus:` |
| :belize: | `:belize:` |
| :canada: | `:canada:` |
| :cocos_islands: | `:cocos_islands:` |
| :congo_kinshasa: | `:congo_kinshasa:` |
| :central_african_republic: | `:central_african_republic:` |
| :congo_brazzaville: | `:congo_brazzaville:` |
| :switzerland: | `:switzerland:` |
| :cote_divoire: | `:cote_divoire:` |
| :cook_islands: | `:cook_islands:` |
| :chile: | `:chile:` |
| :cameroon: | `:cameroon:` |
| :cn: | `:cn:` |
| :colombia: | `:colombia:` |
| :clipperton_island: | `:clipperton_island:` |
| :costa_rica: | `:costa_rica:` |
| :cuba: | `:cuba:` |
| :cape_verde: | `:cape_verde:` |
| :curacao: | `:curacao:` |
| :christmas_island: | `:christmas_island:` |
| :cyprus: | `:cyprus:` |
| :czech_republic: | `:czech_republic:` |
| :de: | `:de:` |
| :diego_garcia: | `:diego_garcia:` |
| :djibouti: | `:djibouti:` |
| :denmark: | `:denmark:` |
| :dominica: | `:dominica:` |
| :dominican_republic: | `:dominican_republic:` |
| :algeria: | `:algeria:` |
| :ceuta_melilla: | `:ceuta_melilla:` |
| :ecuador: | `:ecuador:` |
| :estonia: | `:estonia:` |
| :egypt: | `:egypt:` |
| :western_sahara: | `:western_sahara:` |
| :eritrea: | `:eritrea:` |
| :es: | `:es:` |
| :ethiopia: | `:ethiopia:` |
| :eu: | `:eu:` `:european_union:` |
| :finland: | `:finland:` |
| :fiji: | `:fiji:` |
| :falkland_islands: | `:falkland_islands:` |
| :micronesia: | `:micronesia:` |
| :faroe_islands: | `:faroe_islands:` |
| :fr: | `:fr:` |
| :gabon: | `:gabon:` |
| :gb: | `:gb:` `:uk:` |
| :grenada: | `:grenada:` |
| :georgia: | `:georgia:` |
| :french_guiana: | `:french_guiana:` |
| :guernsey: | `:guernsey:` |
| :ghana: | `:ghana:` |
| :gibraltar: | `:gibraltar:` |
| :greenland: | `:greenland:` |
| :gambia: | `:gambia:` |
| :guinea: | `:guinea:` |
| :guadeloupe: | `:guadeloupe:` |
| :equatorial_guinea: | `:equatorial_guinea:` |
| :greece: | `:greece:` |
| :south_georgia_south_sandwich_islands: | `:south_georgia_south_sandwich_islands:` |
| :guatemala: | `:guatemala:` |
| :guam: | `:guam:` |
| :guinea_bissau: | `:guinea_bissau:` |
| :guyana: | `:guyana:` |
| :hong_kong: | `:hong_kong:` |
| :heard_mcdonald_islands: | `:heard_mcdonald_islands:` |
| :honduras: | `:honduras:` |
| :croatia: | `:croatia:` |
| :haiti: | `:haiti:` |
| :hungary: | `:hungary:` |
| :canary_islands: | `:canary_islands:` |
| :indonesia: | `:indonesia:` |
| :ireland: | `:ireland:` |
| :israel: | `:israel:` |
| :isle_of_man: | `:isle_of_man:` |
| :india: | `:india:` |
| :british_indian_ocean_territory: | `:british_indian_ocean_territory:` |
| :iraq: | `:iraq:` |
| :iran: | `:iran:` |
| :iceland: | `:iceland:` |
| :it: | `:it:` |
| :jersey: | `:jersey:` |
| :jamaica: | `:jamaica:` |
| :jordan: | `:jordan:` |
| :jp: | `:jp:` |
| :kenya: | `:kenya:` |
| :kyrgyzstan: | `:kyrgyzstan:` |
| :cambodia: | `:cambodia:` |
| :kiribati: | `:kiribati:` |
| :comoros: | `:comoros:` |
| :st_kitts_nevis: | `:st_kitts_nevis:` |
| :north_korea: | `:north_korea:` |
| :kr: | `:kr:` |
| :kuwait: | `:kuwait:` |
| :cayman_islands: | `:cayman_islands:` |
| :kazakhstan: | `:kazakhstan:` |
| :laos: | `:laos:` |
| :lebanon: | `:lebanon:` |
| :st_lucia: | `:st_lucia:` |
| :liechtenstein: | `:liechtenstein:` |
| :sri_lanka: | `:sri_lanka:` |
| :liberia: | `:liberia:` |
| :lesotho: | `:lesotho:` |
| :lithuania: | `:lithuania:` |
| :luxembourg: | `:luxembourg:` |
| :latvia: | `:latvia:` |
| :libya: | `:libya:` |
| :morocco: | `:morocco:` |
| :monaco: | `:monaco:` |
| :moldova: | `:moldova:` |
| :montenegro: | `:montenegro:` |
| :st_martin: | `:st_martin:` |
| :madagascar: | `:madagascar:` |
| :marshall_islands: | `:marshall_islands:` |
| :macedonia: | `:macedonia:` |
| :mali: | `:mali:` |
| :myanmar: | `:myanmar:` |
| :mongolia: | `:mongolia:` |
| :macau: | `:macau:` |
| :northern_mariana_islands: | `:northern_mariana_islands:` |
| :martinique: | `:martinique:` |
| :mauritania: | `:mauritania:` |
| :montserrat: | `:montserrat:` |
| :malta: | `:malta:` |
| :mauritius: | `:mauritius:` |
| :maldives: | `:maldives:` |
| :malawi: | `:malawi:` |
| :mexico: | `:mexico:` |
| :malaysia: | `:malaysia:` |
| :mozambique: | `:mozambique:` |
| :namibia: | `:namibia:` |
| :new_caledonia: | `:new_caledonia:` |
| :niger: | `:niger:` |
| :norfolk_island: | `:norfolk_island:` |
| :nigeria: | `:nigeria:` |
| :nicaragua: | `:nicaragua:` |
| :netherlands: | `:netherlands:` |
| :norway: | `:norway:` |
| :nepal: | `:nepal:` |
| :nauru: | `:nauru:` |
| :niue: | `:niue:` |
| :new_zealand: | `:new_zealand:` |
| :oman: | `:oman:` |
| :panama: | `:panama:` |
| :peru: | `:peru:` |
| :french_polynesia: | `:french_polynesia:` |
| :papua_new_guinea: | `:papua_new_guinea:` |
| :philippines: | `:philippines:` |
| :pakistan: | `:pakistan:` |
| :poland: | `:poland:` |
| :st_pierre_miquelon: | `:st_pierre_miquelon:` |
| :pitcairn_islands: | `:pitcairn_islands:` |
| :puerto_rico: | `:puerto_rico:` |
| :palestinian_territories: | `:palestinian_territories:` |
| :portugal: | `:portugal:` |
| :palau: | `:palau:` |
| :paraguay: | `:paraguay:` |
| :qatar: | `:qatar:` |
| :reunion: | `:reunion:` |
| :romania: | `:romania:` |
| :serbia: | `:serbia:` |
| :ru: | `:ru:` |
| :rwanda: | `:rwanda:` |
| :saudi_arabia: | `:saudi_arabia:` |
| :solomon_islands: | `:solomon_islands:` |
| :seychelles: | `:seychelles:` |
| :sudan: | `:sudan:` |
| :sweden: | `:sweden:` |
| :singapore: | `:singapore:` |
| :st_helena: | `:st_helena:` |
| :slovenia: | `:slovenia:` |
| :svalbard_jan_mayen: | `:svalbard_jan_mayen:` |
| :slovakia: | `:slovakia:` |
| :sierra_leone: | `:sierra_leone:` |
| :san_marino: | `:san_marino:` |
| :senegal: | `:senegal:` |
| :somalia: | `:somalia:` |
| :suriname: | `:suriname:` |
| :south_sudan: | `:south_sudan:` |
| :sao_tome_principe: | `:sao_tome_principe:` |
| :el_salvador: | `:el_salvador:` |
| :sint_maarten: | `:sint_maarten:` |
| :syria: | `:syria:` |
| :swaziland: | `:swaziland:` |
| :tristan_da_cunha: | `:tristan_da_cunha:` |
| :turks_caicos_islands: | `:turks_caicos_islands:` |
| :chad: | `:chad:` |
| :french_southern_territories: | `:french_southern_territories:` |
| :togo: | `:togo:` |
| :thailand: | `:thailand:` |
| :tajikistan: | `:tajikistan:` |
| :tokelau: | `:tokelau:` |
| :timor_leste: | `:timor_leste:` |
| :turkmenistan: | `:turkmenistan:` |
| :tunisia: | `:tunisia:` |
| :tonga: | `:tonga:` |
| :tr: | `:tr:` |
| :trinidad_tobago: | `:trinidad_tobago:` |
| :tuvalu: | `:tuvalu:` |
| :taiwan: | `:taiwan:` |
| :tanzania: | `:tanzania:` |
| :ukraine: | `:ukraine:` |
| :uganda: | `:uganda:` |
| :us_outlying_islands: | `:us_outlying_islands:` |
| :united_nations: | `:united_nations:` |
| :us: | `:us:` |
| :uruguay: | `:uruguay:` |
| :uzbekistan: | `:uzbekistan:` |
| :vatican_city: | `:vatican_city:` |
| :st_vincent_grenadines: | `:st_vincent_grenadines:` |
| :venezuela: | `:venezuela:` |
| :british_virgin_islands: | `:british_virgin_islands:` |
| :us_virgin_islands: | `:us_virgin_islands:` |
| :vietnam: | `:vietnam:` |
| :vanuatu: | `:vanuatu:` |
| :wallis_futuna: | `:wallis_futuna:` |
| :samoa: | `:samoa:` |
| :kosovo: | `:kosovo:` |
| :yemen: | `:yemen:` |
| :mayotte: | `:mayotte:` |
| :south_africa: | `:south_africa:` |
| :zambia: | `:zambia:` |
| :zimbabwe: | `:zimbabwe:` |
| :england: | `:england:` |
| :scotland: | `:scotland:` |
| :wales: | `:wales:` |
---
# Math
Write LaTeX math between dollar signs. It is converted to **MathML at build time**, so
the browser renders it natively — no KaTeX or MathJax at runtime.
Inline math uses single `$…$`:
~~~code-preview example
```md
The relation $E = mc^2$ links mass and energy.
```
```md
The relation $E = mc^2$ links mass and energy.
```
~~~
Display math (its own centred block) uses `$$…$$`:
~~~code-preview example
```md
$$\int_0^1 x^2 \, dx = \frac{1}{3}$$
```
```md
$$\int_0^1 x^2 \, dx = \frac{1}{3}$$
```
~~~
## Code-fence syntax
If your text has literal `$` (currency), the **code syntax** avoids ambiguity — inline
`` $`…`$ `` and a ` ```math ` block. Both convert to the same MathML:
````md
Inline: $`a^2 + b^2 = c^2`$
```math
\sum_{k=1}^n k = \frac{n(n+1)}{2}
```
````
Inline: $`a^2 + b^2 = c^2`$
```math
\sum_{k=1}^n k = \frac{n(n+1)}{2}
```
MathML is stored in the page, so math works offline and needs no scripts in the
sandboxed content frame.
> [!NOTE]
> A formula the converter can't parse **fails the build** — the error names the page and
> the offending LaTeX, so a broken equation is caught at compile time rather than silently
> degrading to raw text.
---
# Frontmatter
Each page may begin with a YAML **frontmatter** block, fenced by `---`, that sets the
page's metadata. Every field is optional — a page of pure Markdown is a valid page.
```md
---
id: writing-pages
title: Writing pages
keywords: [Markdown, frontmatter, authoring]
categories: [authoring]
related: [table-of-contents, other-book:overview]
toc: true
---
# Writing pages
Body content…
```
The block is stripped before rendering — it never appears in the page body. A block
that doesn't parse (or isn't terminated by a closing `---`) fails the
[compile](md/khb-authoring/compiling.md).
## Fields
| Field | Sets | Details |
|-------|------|---------|
| `id` | the page's stable id (defaults to the file name) | [id](md/khb-authoring/frontmatter-id.md) |
| `title` | the display title in the TOC, tabs and search | [title](md/khb-authoring/frontmatter-title.md) |
| `keywords` | the page's entries in the keyword index | [keywords](md/khb-authoring/frontmatter-keywords.md) |
| `categories` | facet tags for the category filter | [categories](md/khb-authoring/frontmatter-categories.md) |
| `related` | the **See also** footer | [related](md/khb-authoring/frontmatter-related.md) |
| `toc` | forces the "On this page" box on or off | [toc](md/khb-authoring/frontmatter-toc.md) |
---
# id (frontmatter)
The page's stable identifier — the name every link uses: [links](md/khb-authoring/links.md) in
other pages, [`related`](md/khb-authoring/frontmatter-related.md) lists, and [toc.yaml](md/khb-authoring/toc-yaml.md).
## Syntax
```yaml
id: writing-pages
```
## Default
The file name without `.md`, slugged: ASCII letters and digits are kept (lowercased),
every other character becomes `-` — `Writing Pages.md` → `writing-pages`. Set `id`
explicitly when the file name isn't the id you want to commit to, e.g. under numeric
prefix ordering (`01-intro.md` would otherwise become `01-intro` — see
[TOC ordering](md/khb-authoring/toc-ordering.md)).
## Example
This book's own `README.md` starts with:
```md [README.md]
---
id: index
title: Authoring KD Help Books
---
```
so the intro page's id is `index`, not `readme`.
## In the viewer
Ids are namespaced per book — `docsetId:pageId` — so books never collide; the address
bar reads `khb://my-docs/writing-pages.htm`. An id is your public contract: changing
one on a published page breaks inbound cross-book links and readers' bookmarks.
---
# title (frontmatter)
The page's display title.
## Syntax
```yaml
title: Writing pages
```
## Default
Falls back to the page's first `# H1`, then to the page [id](md/khb-authoring/frontmatter-id.md). In
practice you rarely need the field: keep one H1 equal to the title you want and omit
it — set it only when the two must differ.
## Example
```md
---
title: Writing pages
---
# Writing pages
…
```
## In the viewer
The title labels the page everywhere: the table-of-contents entry (unless a
[toc.yaml node overrides it](md/khb-authoring/toc-nodes.md)), the tab caption, search results, and the
link text of **See also** entries pointing at this page.
---
# keywords (frontmatter)
The terms under which the page appears in the **keyword index** — the classic
F1-style Index panel.
## Syntax
```yaml
keywords: [installation, setup, requirements]
```
## Default
None — the page simply has no Index entries. (It stays fully searchable; keywords are
curated lookup terms, not the search corpus.)
## Example
```yaml
keywords: [compile, build, CLI, khb compile]
```
## In the viewer
Each term becomes an entry in the **Index** panel's type-ahead list, jumping straight
to the page. The index unions across all loaded books, and one term may be claimed by
several pages — across books, too. Keywords are also indexed for full-text search
alongside the title and body, so they lift the page's ranking for those queries (see
[Full-text search](khb-internals:full-text-search)).
> [!TIP]
> Write keywords as a reader would look them up: 5–8 concrete terms, including the
> synonyms your prose avoids.
---
# categories (frontmatter)
Facet tags for the page. Categories are **independent of the TOC hierarchy** — a
many-to-many tagging layer over it.
## Syntax
```yaml
categories: [basics, api]
```
## Default
None — the page is untagged and appears only under the unfiltered view.
## Display titles — `categories.yaml`
A category used in frontmatter but not declared anywhere is **auto-registered** with
its id as its title. To give categories proper display titles, declare them in an
optional `categories.yaml` next to `docset.toml`:
```yaml [categories.yaml]
- id: basics
title: The Basics
- id: api
title: API Reference
```
## In the viewer
The **Filter by category** selector prunes the table of contents to the pages tagged
with the chosen category — keeping the folder structure, not flattening it. The facet
unions across loaded books and composes with the
[product filter](md/khb-authoring/docset-products.md).
---
# related (frontmatter)
Curated onward reading: the page ids rendered as the page's **See also** footer.
## Syntax
```yaml
related: [writing-pages, table-of-contents, other-book:overview]
```
Each entry is an in-book page [id](md/khb-authoring/frontmatter-id.md), or a cross-book
`docsetId:pageId` — the same two forms as [links](md/khb-authoring/links.md).
## Default
None — the page has no See also footer.
## Validation
In-book ids are checked at [compile time](md/khb-authoring/compiling.md); a typo fails the build.
Cross-book ids are stored as-is (the other book compiles separately).
## In the viewer
The footer lists the entries **in the order written**, each labelled with the target
page's title. A cross-book entry whose book isn't loaded is **hidden**, so a book read
on its own shows no dead links.
> [!TIP]
> Keep it to 2–4 genuinely next-step pages. `related` is a recommendation shelf, not
> a sitemap — the TOC already does that job.
---
# toc (frontmatter)
Forces the page's **"On this page"** box — the section navigation built from the
page's headings — on or off.
## Syntax
```yaml
toc: false
```
## Default
Omitted → **automatic**: the box is shown only when the page has two or more
top-level sections (`##` headings), and skipped for short single-section pages.
## Example
A long reference page with one giant section can still opt in:
```yaml
toc: true
```
## In the viewer
The box renders at the top of the page and deep-links each entry to its
[heading anchor](md/khb-authoring/headings.md). `toc: true` shows it regardless of section count;
`toc: false` suppresses it even on a heading-rich page (say, a glossary where the box
would just duplicate the content).
---
# docset.toml
The book manifest — the one required file besides the pages. It sits at the root of
the source folder and identifies the docset:
```toml [docset.toml]
id = "my-docs"
title = "My Documentation"
version = "0.1.0"
language = "en" # selects the search tokenizer
collection = "my-product" # optional: merge/family key (default = id)
collection_title = "My Product" # optional: family display title (default = title)
# optional: products this book belongs to (a many-to-many filter facet)
[[products]]
id = "my-product"
title = "My Product"
[[products]]
id = "suite"
title = "The Suite"
```
One folder, one book, one language: the same product in other languages or versions
is a *separate* source folder whose manifest shares the `collection` (and, for
languages, the `version`).
## Fields
| Field | Required | Sets | Details |
|-------|----------|------|---------|
| `id` | yes | the docset id that namespaces every page | [id](md/khb-authoring/docset-id.md) |
| `title` | yes | the book's display title | [title](md/khb-authoring/docset-title.md) |
| `version` | no (default `0.1.0`) | the edition, and the version switcher | [version](md/khb-authoring/docset-version.md) |
| `language` | no (default `en`) | the content language and search tokenizer | [language](md/khb-authoring/docset-language.md) |
| `collection`, `collection_title` | no | the merge/family key | [collection](md/khb-authoring/docset-collection.md) |
| `[[products]]` | no | the product filter facet | [products](md/khb-authoring/docset-products.md) |
| `[extensions.]` | no | external block transformers | [extensions](md/khb-authoring/extensions.md) |
The manifest is stored in the compiled `.khb`'s metadata — `khb inspect my.khb`
prints it back (see [Compiling a book](md/khb-authoring/compiling.md)).
---
# id (docset.toml)
The docset's identifier. **Required.**
## Syntax
```toml
id = "my-docs"
```
## Rules
There is no fallback — pick one and keep it. The id must be unique among the books a
reader loads together, and it's your public contract: other books link into yours as
`your-id:page`, so changing it breaks them.
## Example
```toml
id = "khb-authoring"
```
## In the viewer
The id namespaces every page — `docsetId:pageId` — which is what lets many books
merge into one collection without colliding. It's the prefix in
[cross-book links](md/khb-authoring/links.md) and cross-book [related](md/khb-authoring/frontmatter-related.md) entries,
the id recorded in a published site's `docsets.json`, and the host in the address bar
(`khb://my-docs/welcome.htm`). Versioned editions of one book use **distinct ids**
that share a [collection](md/khb-authoring/docset-collection.md).
---
# title (docset.toml)
The book's display title. **Required.**
## Syntax
```toml
title = "My Documentation"
```
## Fallback role
It doubles as the default for `collection_title` when the
[collection](md/khb-authoring/docset-collection.md) declares none — so a single-book product needs no
extra naming.
## Example
```toml
title = "Authoring KD Help Books"
```
## In the viewer
The title names the book in **Manage docsets** and **Help → About** (alongside its
language and version), and it's the title recorded in a published site's
`docsets.json`. What labels the top-level folder in the table of contents is the
*family's* `collection_title` — which, for a standalone book, is this title.
---
# version (docset.toml)
The edition of the book — the docs' version, usually tracking the product it
documents.
## Syntax
```toml
version = "1.2.0"
```
## Default
`0.1.0` when omitted.
## Example
Two source folders, one product at two versions — distinct
[ids](md/khb-authoring/docset-id.md), shared [collection](md/khb-authoring/docset-collection.md), different `version`:
~~~code-group
```toml [v2/docset.toml]
id = "sdk-v2"
title = "SDK Guide"
version = "2.0.0"
collection = "sdk"
```
```toml [v1/docset.toml]
id = "sdk-v1"
title = "SDK Guide"
version = "1.0.0"
collection = "sdk"
```
~~~
## In the viewer
The version is surfaced read-only in **Help → About**, in **Manage docsets**, and as
a tooltip on the product folder in the table of contents. When one collection is
loaded in **several versions**, only the **latest** shows by default
(numeric-dotted comparison, so `1.10 > 1.2`); a **Version** selector appears to pin
an older one, and the choice persists across reloads. Publishing archived versions
alongside the tip is covered in [Versioning](khb-publishing:versioning).
---
# language (docset.toml)
The content language of the book. Content is authored as **one docset per language**;
translations are separate source folders sharing a [collection](md/khb-authoring/docset-collection.md).
## Syntax
```toml
language = "en"
```
A regional tag works too — only the primary subtag matters (`en-US` → `en`).
## Default
`en` when omitted.
## Effect at compile time
The language selects the **full-text search tokenizer** baked into the docset:
| Language | Tokenizer | Meaning |
|----------|-----------|---------|
| `en` | `porter unicode61 remove_diacritics 2` | Porter stemming — *fox* matches *foxes* |
| anything else | `unicode61 remove_diacritics 2` | diacritic folding, no stemming |
See [Full-text search](khb-internals:full-text-search) for the machinery.
## In the viewer
Books group by language per collection, and the viewer shows **one language per
collection at a time**: the reader's per-collection override first, then the UI
language, then a fallback (English → browser language → first available). A
collection available in several languages gets a **Display language** selector under
*Manage docsets*.
---
# collection (docset.toml)
The **merge/family key**: books sharing a `collection` belong to one product and
merge into one table of contents.
## Syntax
```toml
collection = "my-product" # the family key
collection_title = "My Product" # the family's display title
```
## Default
`collection` defaults to the docset [id](md/khb-authoring/docset-id.md), `collection_title` to the
docset [title](md/khb-authoring/docset-title.md) — a standalone book is its own one-book family with no
extra configuration.
## Example
A product split across three books that should read as one:
```toml
# in guide/, api/ and tutorials/ docset.toml alike:
collection = "myapp"
collection_title = "MyApp Documentation"
```
## In the viewer
- Books of **one family merge seamlessly** — one table of contents, no wrapper.
- When **several families** are loaded, each becomes a collapsible **top-level
folder** labelled with its `collection_title`, keeping products visually apart.
- The collection is also how **editions pair up**: language variants and
[versions](md/khb-authoring/docset-version.md) of one book share a collection, driving the display
language and version switchers.
---
# products (docset.toml)
The products this book belongs to — a **filter facet**, independent of the
[collection](md/khb-authoring/docset-collection.md) merge key, and **many-to-many**: one book can belong
to several products, and one product can span several families.
## Syntax
```toml
[[products]]
id = "my-product"
title = "My Product"
[[products]]
id = "suite"
title = "The Suite"
```
## Default
Omitted → the book is filed under a single product named after its `collection`, so
the product filter keeps working for books that never declare any.
## Example
A shared "Getting started" book that should surface under both products of a suite:
```toml
[[products]]
id = "editor"
title = "The Editor"
[[products]]
id = "server"
title = "The Server"
```
## In the viewer
The Index and Search **union across all products by default**. The **Filter by
product** selector (Contents and Index) and the **Product** scope on the Search page
narrow to the books tagged with one product — pruning the tree while keeping the
family folder structure. Because products are tags, one selection can reveal books
from several families; the [category facet](md/khb-authoring/frontmatter-categories.md) composes with it.
---
# Extensions
An **extension** hands the body of a fenced block to an **external program** that turns it
into other Markdown — and, optionally, generated image files. The compiler splices the
returned Markdown back into the page (rendering it like any other Markdown) and stores any
images the program produced as [assets](md/khb-authoring/images.md).
It's the escape hatch for content the bundled compiler can't produce on its own: compile a
domain-specific snippet into an example plus a rendered visualization, turn a data file into
a table, shell out to a real diagram engine, and so on.
> [!NOTE]
> Extensions run **external processes**, so they're **off by default**. The compiler only
> runs them when you pass `--allow-extensions`, and only the ones a book declares in its
> `docset.toml`. A book that uses extensions still compiles without the flag — its `ext:`
> blocks are just left as plain code blocks. See [Running](#running) below.
## A motivating example
Say a `khb-label` tool compiles a label definition into a preview. In a page you write an
` ```ext:label ` block:
````md
```ext:label
name: Fragile
color: red
```
````
With extensions enabled, `khb-label` receives that body and returns Markdown — e.g. the
source shown as a code sample followed by `` — and the
generated `out.svg` is embedded in the book. Readers see the example and its visualization;
without the flag, they just see the label source as a code block.
## Declaring an extension
Add an `[extensions.]` table to [`docset.toml`](md/khb-authoring/docset-toml.md). The `` is what the
` ```ext: ` fence refers to:
```toml [docset.toml]
[extensions.label]
command = "khb-label" # bare name → looked up on PATH
args = ["--theme", "dark"] # optional fixed arguments, passed every run
```
- **`command`** — the executable to run. A **bare name** (`khb-label`) is resolved on your
`PATH`. A **path** (`./tools/label`, `bin/label`) is resolved relative to the source folder,
so a book can ship its own tool.
- **`args`** — optional fixed arguments passed on every invocation (also handed to the tool
in the request, see [the protocol](#the-protocol)).
A `` must be non-empty and contain no `:` or whitespace.
## The `ext:` block
Trigger an extension with a fenced code block whose language is `ext:` + the declared name.
Anything after the name on the info line is passed to the tool as its `meta` string:
````md
```ext:label --variant compact
name: Fragile
```
````
Here `label` selects the extension and `--variant compact` arrives as `meta`. The `ext:`
prefix keeps these blocks from ever colliding with a real language or a built-in block like
` ```dot `.
## Running
Extension processes only run when you compile with the opt-in flag:
```bash
khb compile my-docs -o my-docs.khb --allow-extensions
```
Why opt-in: a `docset.toml` may come from an untrusted source, and running its declared
commands is arbitrary code execution. The flag keeps the default build **hermetic and
offline** — the same reason the bundled compiler avoids browser-based tools (see the note in
[Diagrams](md/khb-authoring/diagrams.md)). Compiling *without* the flag is always safe: each `ext:` block is left
as a plain code block and a note is printed, so the book still builds.
When the flag *is* set, an `ext:` block whose `` isn't declared is a build error
(it's almost always a typo).
## The protocol
An extension is any executable that speaks this JSON-over-stdio contract.
**Request** — the compiler writes one JSON object to the tool's **stdin**:
```json
{
"khb_extension_protocol": 1,
"lang": "label",
"meta": "--variant compact",
"args": ["--theme", "dark"],
"body": "name: Fragile\n",
"page_id": "intro",
"assets_dir": "/tmp/khb-ext-1234-0",
"asset_prefix": "assets/ext/label/intro/0/",
"source_dir": "/abs/path/to/my-docs",
"page_path": "pages/guide/intro.md",
"page_dir": "pages/guide"
}
```
- `body` — the verbatim block body (no fences).
- `meta` — the info-line text after the name; `args` — the `docset.toml` arguments.
- `assets_dir` — a scratch directory the tool may write generated files into.
- `asset_prefix` — the path prefix to reference those files by in the returned Markdown.
- `source_dir` — the docset source root (absolute); `page_path` / `page_dir` — the page's
file and directory relative to it. See [Referencing files](#referencing-files).
The process runs **with the page's own directory as its working directory**, and the same
values are exposed as environment variables: `KHB_EXTENSION=1`, `KHB_PAGE_ID`, `KHB_LANG`,
`KHB_SOURCE_DIR`, `KHB_PAGE_PATH`, `KHB_PAGE_DIR`.
**Response** — the tool writes one JSON object to **stdout**:
```json
{
"markdown": "```\nname: Fragile\n```\n\n\n",
"assets": [ { "file": "out.svg" } ]
}
```
- `markdown` — replaces the block. It is rendered to HTML like ordinary page Markdown.
- `assets` — files the tool wrote into `assets_dir`, each by a **bare filename** (no `/`,
`\`, or `..`). Each is stored in the book and can be referenced from `markdown` as
`asset_prefix` + the filename.
A generated file at `assets_dir/out.svg` becomes the asset
`assets/ext////out.svg` — a namespace that can't clash with your own
`assets/` files or with other blocks. Reference it in the returned Markdown exactly as
`asset_prefix` + `out.svg`, and it resolves like any other image.
## A complete example
`swatch` is a real, runnable extension: it turns a block of `Name: #hex` lines into a table
with a generated SVG colour swatch per entry — the "example plus visualization" pattern in
miniature. The full example docset (tool, manifest, page) is in `compiler/examples/ext-swatch/`.
The tool — any executable in any language works; this one is a ~25-line Python script:
```python [swatch.py]
#!/usr/bin/env python3
import json, os, re, sys
req = json.load(sys.stdin)
rows, assets = [], []
for i, line in enumerate(l for l in req["body"].splitlines() if l.strip()):
m = re.match(r"\s*(.+?)\s*[:=]\s*(#[0-9a-fA-F]{3,8})\s*$", line)
if not m:
sys.exit(f"swatch: cannot parse line {line!r}")
name, color = m.group(1), m.group(2)
file = f"swatch-{i}.svg"
with open(os.path.join(req["assets_dir"], file), "w") as f:
f.write(f"")
assets.append({"file": file})
rows.append(f"| {name} |  | `{color}` |")
json.dump({
"markdown": "| Colour | Swatch | Hex |\n|---|---|---|\n" + "\n".join(rows) + "\n",
"assets": assets,
}, sys.stdout)
```
Declare it (a source-relative command, so the docset ships its own tool):
```toml [docset.toml]
[extensions.swatch]
command = "./swatch.py"
```
Use it on a page:
````md
```ext:swatch
Coral: #ff7f50
Teal: #008080
Slate: #334155
```
````
Compile with `khb compile examples/ext-swatch -o swatch.khb --allow-extensions`, and the
block becomes a three-row table — each row a colour name, its rendered swatch, and its hex —
with the SVGs stored in the book under `assets/ext/swatch/…`.
## A second example: the widget this guide runs on
The built-in `~~~code-preview example` widget shows a snippet's **source** next to its
**rendered result** — but as two separate blocks, so authors write the snippet twice. This
very guide avoids that with an `ext:example` extension (`docs/authoring/tools/example.py`):
you write the snippet **once** and the tool emits the widget with both halves filled in.
It's a nice illustration of *when* extensions run — **before** the built-in widgets. The
Markdown it returns is a `~~~code-preview example` block, which the widget chain then expands
exactly as if you'd hand-written it. `meta` carries an optional source language (default
`md`) and/or `split` (source and result side by side).
```python [tools/example.py]
#!/usr/bin/env python3
import json, re, sys
def fence(ch, text, minimum=3):
longest = max((len(m.group()) for m in re.finditer(re.escape(ch) + "+", text)), default=0)
return ch * max(minimum, longest + 1)
req = json.load(sys.stdin)
body = req["body"].rstrip("\n")
opts = req["meta"].split()
skin = "example split" if "split" in opts else "example"
lang = next((o for o in opts if o != "split"), "md")
bt = fence("`", body) # inner code fences, sized to the body
td = fence("~", bt + body) # outer fence, longer than any tilde run inside
json.dump({"markdown":
f"{td}code-preview {skin}\n"
f"{bt}{lang}\n{body}\n{bt}\n" # source, shown as code
f"{bt}md\n{body}\n{bt}\n" # result, rendered as Markdown
f"{td}\n"}, sys.stdout)
```
Declared in this guide's manifest:
```toml [docset.toml]
[extensions.example]
command = "./tools/example.py"
```
So every source-plus-result box in these pages is written as just:
````md
```ext:example
> [!TIP]
> Written once — shown as source *and* rendered.
```
````
Unlike `swatch`, it generates no assets — it only rewrites Markdown. (This is also why
`code-blocks.md`, which *documents* the raw `~~~code-preview` syntax, keeps writing it by
hand: an extension that emits that widget can't also show its literal source.)
## Referencing files
A block often points the tool at another file — say a source file to compile:
````md
```ext:codesample ./examples/label.rs
```
````
Because the process runs **in the page's own directory**, the tool can read that path
relative to its working directory (`std::fs::read("./examples/label.rs")` or equivalent) and
it resolves next to the `.md` — no path juggling needed. For paths relative to the docset
root instead, the request/env also carry `source_dir` (absolute) and `page_dir`/`page_path`
(relative to it). The path itself is just text in the block's info line (`meta`); the
compiler doesn't interpret it — the tool does.
## Errors
A tool that **exits non-zero**, writes **unparseable JSON**, or names an **unsafe asset
filename** fails the build, with the page, the extension name, and the tool's stderr in the
message — the same "a broken block is a build error, not a blank space" policy as
[diagrams](md/khb-authoring/diagrams.md) and [math](md/khb-authoring/math.md).
## Notes and limits
- **Determinism** — a build is only as reproducible and offline as the tools it runs. Keep
extensions deterministic; treat them as part of your toolchain.
- **The result is real Markdown** — expansion happens *before* the rest of the render, so the
Markdown a tool returns flows through the whole pipeline: nested built-in blocks (a
` ```dot ` diagram, a `~~~gallery`, `$…$` math) inside the output render normally. One
`ext:` block is not re-scanned for extensions, so a tool can't recursively invoke itself.
- **Windows** — `command` must be an executable; to run a `.bat`/`.cmd` or a script, point
`command` at the interpreter and pass the script through `args`.
- **AI text** — the [AI export](khb-publishing:pack-llms) and the `md` column store the
**expanded** Markdown (the tool's output), so AI-facing surfaces see the same content as
readers, not the raw `ext:` source.
---
# toc.yaml
An optional file next to `docset.toml` that defines the book's table-of-contents
tree, referencing pages by [id](md/khb-authoring/frontmatter-id.md):
```yaml [toc.yaml]
- page: getting-started
children:
- page: what-is-khb
- title: Reference # folder node — groups its children, can't be opened
children:
- page: reference-a
- page: reference-b
title: The B parts # label override for this node only
```
Two node kinds — **page nodes** (`page:`) and **folder nodes** (`title:` only) — may
nest freely via `children:`; order in the file is order in the tree. Every `page:`
id must name an existing page, and every folder node needs a `title:` — either
mistake fails the [compile](md/khb-authoring/compiling.md). Without a `toc.yaml` the book gets a flat
table of contents in file-name order.
The tree only *arranges* pages: a page absent from `toc.yaml` still compiles, is
searchable and linkable — it just has no Contents entry.
## In this section
| Page | Covers |
|------|--------|
| [TOC nodes](md/khb-authoring/toc-nodes.md) | page nodes, label overrides, folder nodes |
| [TOC ordering](md/khb-authoring/toc-ordering.md) | ordering with and without a `toc.yaml` |
---
# TOC nodes
A [toc.yaml](md/khb-authoring/toc-yaml.md) is a nested list of two node kinds.
## Page nodes
```yaml
- page: getting-started
```
A `page:` node puts the page in the tree; its label defaults to the page's
[title](md/khb-authoring/frontmatter-title.md). Add a `title:` to **override the label** in the tree
only — the page itself keeps its own title everywhere else:
```yaml
- page: reference-b
title: The B parts
```
## Folder nodes
```yaml
- title: Reference
children:
- page: reference-a
- page: reference-b
```
A node with a `title:` and **no `page:`** is a **folder node**: it only groups its
children — in the viewer it expands and collapses but cannot be opened as a page. A
folder node without a `title:` fails the [compile](md/khb-authoring/compiling.md).
## Nesting
Either kind may carry `children:`, to any depth — so a section can be an openable
page *with* subpages (a landing page node with children) or a pure grouping (a folder
node).
```yaml
- page: frontmatter # a landing page with subpages
children:
- page: frontmatter-id
- page: frontmatter-title
- title: Appendices # a pure grouping
children:
- page: glossary
```
---
# TOC ordering
## With a toc.yaml
Order in the file **is** order in the tree — top to bottom, at every level. Reorder
the nodes, recompile, done.
## Without a toc.yaml
If the book has no [toc.yaml](md/khb-authoring/toc-yaml.md), the compiler produces a **flat** table of
contents in **file-name order**. Numeric filename prefixes are the idiomatic way to
control it:
```text
01-intro.md
02-setup.md
03-usage.md
10-reference.md
```
> [!WARNING]
> The prefix becomes part of the page id (`01-intro.md` → id `01-intro`) unless the
> page sets an explicit [`id`](md/khb-authoring/frontmatter-id.md) in its frontmatter. If the book might
> ever grow a `toc.yaml` — or be linked into from other books — set clean ids from
> the start, so renumbering files never breaks links.
## Choosing
The flat fallback suits a handful of pages; the moment a book wants sections,
subpages or label overrides, add a `toc.yaml` — the [nodes](md/khb-authoring/toc-nodes.md) page covers
the syntax.
---
# KD Help Book Internals
This volume is for people who look **under** the viewer: viewer hackers, tooling
authors, and anyone writing a **third-party compiler** that produces `.khb` books
from a different source format. It explains how a book is stored, indexed, streamed,
sandboxed and described — the machinery the other two volumes take for granted.
If you *write* books, start with [Authoring KD Help Books](khb-authoring:index)
instead; if you *ship* them, see [Publishing KD Help Books](khb-publishing:index).
Come back here when you need to know what those tools actually produce.
> [!NOTE]
> These pages are a readable rendition, not the specification itself. The normative
> specs live in the repository: `docs/format.md` (file formats, schema, security)
> and `docs/streaming.md` (the Range-VFS design). Where they and this volume
> disagree, the spec files win.
## What's in this volume
| Page | Covers |
|------|--------|
| [File formats](md/khb-internals/file-formats.md) | `.khb`, `.khbb`, `.khba`, and the `.gz` transfer suffix |
| [SQLite schema](md/khb-internals/sqlite-schema.md) | every table, the `meta` keys, the format version |
| [Full-text search](md/khb-internals/full-text-search.md) | FTS5 external content, bm25, per-language tokenizers |
| [Streaming](md/khb-internals/streaming.md) | the Range-VFS: reading a remote book page-by-page |
| [Security model](md/khb-internals/security-model.md) | rendering untrusted books in a sandboxed frame |
| [Manifest schemas](md/khb-internals/manifest-schemas.md) | `docsets.json`, `config.json` and `.khbm`, field by field |
| [Building a compiler](md/khb-internals/building-a-compiler.md) | the checklist for producing valid `.khb` files yourself |
## The one idea everything follows from
A `.khb` book is an **ordinary SQLite database** with everything precomputed at
build time — rendered HTML, plain text, the TOC, the keyword index, the FTS index.
That single choice explains most of the architecture:
- **Any SQLite can open it** — the native Rust engine (CLI, Tauri), sql.js in the
browser, or your own tooling.
- **Search is instant and offline** — nothing is computed at read time.
- **Streaming falls out for free** — SQLite reads fixed-size pages, and a page read
maps one-to-one onto an HTTP `Range` request (see [Streaming](md/khb-internals/streaming.md)).
- **The format is source-agnostic** — the viewer renders stored HTML and never runs
a Markdown engine, so any front end can produce a book (see
[Building a compiler](md/khb-internals/building-a-compiler.md)).
---
# File formats
The KD Help Book family is three file kinds plus one orthogonal compression
convention. The normative description is `docs/format.md` in the repository; this
page is the tour.
| Extension | What it is | Read by |
|-----------|------------|---------|
| `.khb` | the SQLite docset — the canonical, queried form | native SQLite / sql.js / wa-sqlite |
| `.khbb` | a minimal binary (no SQLite container, no indexes) | rebuilt into a `.khb` before use |
| `.khba` | a sidecar SQLite file of attachments (images, downloads) | opened beside its `.khb` |
## `.khb` — the docset
A `.khb` is a plain **SQLite database**. Everything the viewer needs is precomputed
at build time — rendered HTML, plain text, the table of contents, the category
facet, the F1 keyword index and the full-text index — so search is instant and
works offline, and anything that reads SQLite can open the file. The database is
`VACUUM`ed after writing, and its fixed 4096-byte page size is what makes
[streaming](md/khb-internals/streaming.md) possible later.
The format is **independent of the source format**: the canonical render a `.khb`
stores is HTML, and the viewer never needs Markdown. A producer *may* also stash a
clean Markdown rendition in the optional `pages.md` column, but that is an
enrichment for AI-facing consumers, not a requirement — see the
[SQLite schema](md/khb-internals/sqlite-schema.md).
## `.khbb` — the minimal binary
`.khbb` is a compact [postcard](https://docs.rs/postcard) encoding of the rendered
docset: pages as HTML + plain text, the TOC, categories, keywords **and embedded
assets** — but **no SQLite container and no FTS index**. It is the smallest way to
ship a docset; the consumer rebuilds a real `.khb` from it (the browser does this
in wasm and caches the result in IndexedDB).
The payload sits inside a **versioned wrapper** so it can be validated before use:
the file carries a `format_version`, and a reader rejects any version other than
the one it was built for. Unlike the SQLite form — where old tables simply keep
working — a `.khbb` is a serialized snapshot of the rendered-docset layout, so
every format bump (see the [SQLite schema](md/khb-internals/sqlite-schema.md)) gates it.
`khb convert` turns a `.khb` into a `.khbb` and back; the direction is inferred
from the file extensions.
## `.khba` — attachment sidecars
A `.khba` holds binary attachments — the same `assets` table a `.khb` embeds, plus
a `meta` table — as a **separate SQLite file** shipped next to a lean `.khb` whose
own `assets` table is empty. Each sidecar carries a stable id in `meta.pack` (its
filename), and one `.khb` may be backed by **several** packs.
Resolution never probes: the `.khb`'s `asset_index` table maps every asset path to
its owning store (`''` = embedded, otherwise a sidecar's `meta.pack` id), so
opening an asset is one lookup followed by one read of the right file. Routing by
id rather than position means packs can be opened in any order — and, when packs
are streamed over HTTP, that one ranged read replaces N probes.
## `.gz` — compression is a suffix, not a format
Any of the files above may ship gzip-compressed as `.gz` (`foo.khb.gz`,
`foo.khba.gz`, …). Compression is **orthogonal**: there is no distinct compressed
format, and the viewer decides by the gzip **magic bytes** (`1f 8b`), never by the
name.
> [!TIP]
> Magic-based sniffing means a host that auto-applies `Content-Encoding: gzip` for
> `.gz` files — and therefore hands the browser *pre-decompressed* bytes — works
> just as well as one that serves the bytes verbatim. Either way the viewer ends up
> with a valid SQLite file.
The exception is a **streamed** docset: `Range` requests must address raw SQLite
pages, so streamed files always ship uncompressed (see [Streaming](md/khb-internals/streaming.md) and
[khb pack's compact mode](khb-publishing:pack-mode)).
---
# SQLite schema
Every `.khb` contains the tables below, identified by `meta.format_version`
(currently **1**). The DDL is quoted from `compiler/core/src/schema.rs`, which —
together with `docs/format.md` — is the source of truth.
## Core tables
```sql [compiler/core/src/schema.rs]
CREATE TABLE meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE pages (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
body_html TEXT NOT NULL,
plain TEXT NOT NULL,
keywords TEXT NOT NULL DEFAULT '',
md TEXT
);
CREATE TABLE toc (
id INTEGER PRIMARY KEY,
page_id TEXT REFERENCES pages(id),
parent_id INTEGER REFERENCES toc(id),
position INTEGER NOT NULL,
title TEXT NOT NULL
);
CREATE INDEX idx_toc_parent ON toc(parent_id, position);
```
Three columns deserve a closer look:
- **`pages.plain`** is the page's plain text, stored for full-text search and
snippets. It is the *only* copy of the searchable text — the FTS index references
it rather than duplicating it (see [Full-text search](md/khb-internals/full-text-search.md)).
- **`pages.keywords`** is a space-joined copy of the page's keyword terms, present
only so the FTS index can tokenize it. The *structured* F1 index lives in the
`keywords` table below.
- **`pages.md`** (v5, nullable) is an **optional** clean-Markdown rendition of the
body. The viewer never reads it — `body_html` is the canonical render — it exists
for AI-facing consumers (the `llms.txt` export, a future MCP `get_page`). It is
deliberately the **last** column: SQLite serialises a row column-by-column and
stops at the last requested column, so hot-path reads (`SELECT id, title,
body_html`) never stream its bytes.
- **`toc.page_id`** is `NULL` for a **pure folder node**: a grouping row that
only holds children and cannot be opened. `NULL` passes the foreign-key check by
design.
## Facets, keywords and "See also"
```sql [compiler/core/src/schema.rs]
CREATE TABLE categories (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
position INTEGER NOT NULL
);
CREATE TABLE page_categories (
page_id TEXT NOT NULL REFERENCES pages(id),
category_id TEXT NOT NULL REFERENCES categories(id),
PRIMARY KEY (page_id, category_id)
);
CREATE TABLE keywords (
term TEXT NOT NULL,
page_id TEXT NOT NULL REFERENCES pages(id),
PRIMARY KEY (term, page_id)
);
CREATE INDEX idx_keywords_term ON keywords(term);
CREATE TABLE related (
page_id TEXT NOT NULL REFERENCES pages(id),
related_id TEXT NOT NULL,
position INTEGER NOT NULL,
PRIMARY KEY (page_id, related_id)
);
CREATE INDEX idx_related_page ON related(page_id, position);
CREATE TABLE products (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
position INTEGER NOT NULL
);
```
- **Categories are a facet** (tags, many-to-many), independent of the TOC
hierarchy.
- **`related`** holds the curated "See also" links, ordered by `position`.
`related_id` is a page id in this book *or* a namespaced `docsetId:localId` for a
cross-book link — which is why it has no foreign key.
- **`products`** is the "Filter by product" facet, separate from
`meta.collection` (the merge/family key). A docset with no explicit products
defaults to one named after its collection.
## Assets and routing
```sql [compiler/core/src/schema.rs]
CREATE TABLE assets (
path TEXT PRIMARY KEY,
mime TEXT NOT NULL,
data BLOB NOT NULL
);
CREATE TABLE asset_index (
path TEXT PRIMARY KEY,
pack TEXT NOT NULL
);
```
`assets` is present (possibly empty) in every `.khb` and is the sole content table
of a sidecar `.khba`. `asset_index` routes each path to its store — `pack` is `''`
for embedded, otherwise the owning sidecar's `meta.pack` id — so resolution is one
lookup, never a probe across packs (see [File formats](md/khb-internals/file-formats.md)).
## `meta` keys
| Key | Meaning |
|-----|---------|
| `format_version` | schema version this file conforms to (currently `1`) |
| `docset_id` | the book's id — the namespace prefix in `docsetId:pageId` links |
| `title` | display title |
| `version` | content version (drives the viewer's version switcher) |
| `language` | content language; also selects the FTS tokenizer |
| `tokenizer` | the FTS5 tokenizer string actually used at build time |
| `generator` | the producing tool, e.g. `khb-core 0.1.0` |
| `collection` | family/merge key — books sharing it merge in the viewer |
> [!NOTE]
> There is also a language-dependent virtual table, `pages_fts`, created with a
> per-docset tokenizer rather than fixed DDL — it gets its own page:
> [Full-text search](md/khb-internals/full-text-search.md).
---
# Full-text search
Search over a `.khb` is a single SQLite **FTS5** query — built at compile time,
ranked with `bm25()`, highlighted with `snippet()`. `docs/format.md` is the
normative spec; this page explains the design.
## External content: the text is stored once
The FTS index is created as an **external-content** table over
`pages(title, plain, keywords)`:
```sql
CREATE VIRTUAL TABLE pages_fts USING fts5(
title, plain, keywords,
content='pages', content_rowid='rowid',
tokenize=''
);
```
With `content='pages'`, the virtual table holds **only the inverted index** and
reads the text itself from the `pages` table when needed. The searchable text
(`plain`) therefore exists exactly once in the file — no second copy bloating the
docset — and `snippet()` draws from that same column.
## The query
```sql
SELECT p.id, p.title,
snippet(pages_fts, 1, '', '', '…', 12) AS snip,
-bm25(pages_fts) AS score
FROM pages_fts JOIN pages p ON p.rowid = pages_fts.rowid
WHERE pages_fts MATCH ?
ORDER BY score DESC;
```
`bm25()` returns *lower-is-better* values, hence the negation to sort a
higher-is-better `score`. `snippet()` picks the best 12-token window from `plain`
(column 1) and wraps the matched terms.
## Per-language tokenizers
The tokenizer is chosen from `meta.language` at build time (the mapping lives in
`tokenizer_for_language` in `compiler/core/src/schema.rs`) and recorded in
`meta.tokenizer`:
| Language | Tokenizer |
|----------|-----------|
| `en` | `porter unicode61 remove_diacritics 2` — English stemming, so *fox* matches *foxes* |
| any other | `unicode61 remove_diacritics 2` — diacritics folded, no stemmer |
Only the primary subtag matters (`en-US` → `en`), and the returned value comes
from a fixed set, so it is safe to interpolate into the DDL. Folding diacritics
without stemming is the safe default for other languages until per-language
stemmers are added — for Polish, `wyjątek` still matches `wyjatek`.
This is also why content ships as **one docset per language**: each book gets an
index tokenized for its own language.
## Where each engine differs
The same file is searched by three engines, and they are not equal:
| Engine | Where | Search |
|--------|-------|--------|
| Rust `core` (rusqlite) | CLI, Tauri | real FTS5: bm25 + stemming |
| sql.js | browser, whole-file books | **no FTS5** — JS scan over `plain` |
| custom wa-sqlite | browser, streamed books | real FTS5: bm25 + stemming |
> [!WARNING]
> The stock `sql.js` wasm build ships **without FTS5**, so the prebuilt
> `pages_fts` index is unusable in the browser's default engine. For whole-file
> books the viewer instead searches the stored `plain` column in JS — a heuristic,
> not bm25. The index in the file is *not* wasted, though: native (CLI/Tauri) uses
> it directly, and so does the browser's **streaming** engine, a custom
> FTS5-enabled `wa-sqlite` build (see [Streaming](md/khb-internals/streaming.md)).
When books on different engines merge into one collection, the viewer normalizes
each book's scores before interleaving results, so bm25 values and the sql.js
heuristic compete fairly.
Keyword terms (from page frontmatter — see
[keywords](khb-authoring:frontmatter-keywords)) take part in full-text matching via
the space-joined `pages.keywords` column, *and* feed the separate structured
`keywords` table that drives the F1 index.
---
# Streaming
A `.khb` can be read **page-by-page over HTTP `Range`** — open a remote book,
browse its TOC, read a page and run a real full-text search while fetching only a
fraction of the file. `docs/streaming.md` in the repository is the normative
design document; this page explains how it works and what a host must provide.
## Why SQLite makes it possible
A `.khb` is SQLite with a fixed **4096-byte page size**, and every read SQLite
performs is "give me page N" — which maps one-to-one onto an HTTP `Range:`
request. A static file server is therefore enough to serve *only the pages a query
touches*: a search hits the FTS/B-tree pages it needs, opening a page reads its
row's overflow pages, and nothing else is downloaded. (A zip archive is not
page-addressable this way; choosing SQLite is what kept this door open.)
Attachments compound the win: the `asset_index` routing table resolves an
`asset:` with one lookup → one ranged read of the *one* `.khba` pack that
holds it, never a probe across every pack.
## The Range-VFS design
Streaming is a **SQLite VFS over byte ranges**, implemented twice — natively and
in the browser — with the same shape:
- **Immutable, read-only.** Writes and locks are no-ops and the device reports
`IMMUTABLE`, so SQLite never wants a journal or WAL. A streamed file must never
change in place (publish a new file instead).
- **Block-coalesced reads.** Individual page reads are coalesced into aligned
**64 KiB cached blocks**, so chatty small reads become a few larger fetches.
- **A minimal reader interface.** All I/O funnels through one trait:
```rust [compiler/core/src/vfs.rs]
pub trait RangeReader: Send + Sync {
fn size(&self) -> u64;
fn read_at(&self, offset: u64, buf: &mut [u8]) -> anyhow::Result<()>;
}
```
### Native (CLI / Tauri)
`compiler/core/src/vfs.rs` registers the VFS directly against `rusqlite::ffi` —
the same bundled SQLite the rest of the engine uses, avoiding the "two SQLite
libraries" clash — and `Docset::open_reader(reader)` makes every existing query
stream. The HTTP reader lives in the CLI (`HttpRangeReader`: `read_at` becomes a
`GET` with a `Range:` header, `size` comes from `Content-Range`), kept out of
`core` so each consumer picks its own HTTP client. `khb inspect ` opens a
remote book this way; a 2 MB docset streams roughly **15 %** of its bytes for open
+ TOC + one page + one search.
### Browser (wa-sqlite + Asyncify)
sql.js cannot `await` inside a read callback, so the browser streaming engine is
built on **wa-sqlite**, whose Asyncify build lets a VFS method `await
fetch(url, {headers: {Range: …}})`. `viewer-ts/src/data/streaming.ts` implements
the async Range VFS (same immutability and block cache); `StreamingDocset` wraps
it as a regular docset that **eager-loads the small structure** (TOC, categories,
keywords, related) at open and **streams the heavy parts** (page bodies, assets,
search) on demand, so a streamed book merges into the same TOC/index/search as
whole-file books.
Two practical notes:
- The prebuilt `wa-sqlite` ships **without FTS5**, so the viewer vendors a
**custom FTS5-enabled build** (SQLite 3.53, `-DSQLITE_ENABLE_FTS5`) under
`viewer-ts/vendor/wa-sqlite/` — streamed books get genuine bm25 search, unlike
the sql.js fallback (see [Full-text search](md/khb-internals/full-text-search.md)).
- The engine is **code-split**: sessions that never open a streamed docset never
download it.
Measured on a 618 KB demo docset: **\~11 %** of the file to open, **\~21 %** to also
read a full page, **\~32 %** to also run a bm25 search.
## What a host must provide
| Requirement | Why |
|-------------|-----|
| HTTP `Range` support (`206 Partial Content`) | every SQLite page read is a ranged `GET` |
| The streamed file served **raw** — no gzip, no `.gz` | `Range` offsets must address raw SQLite pages |
| CORS allowing the viewer's origin (for remote books) | the browser fetches cross-origin |
| A file that never changes in place | the VFS treats it as immutable and caches blocks |
> [!NOTE]
> Streaming is a preference, not a promise. The viewer probes the host with a
> cheap `Range` request and validates with a streamed peek; on any failure it
> falls back silently to fetching the whole file — so a non-Range host (or a proxy
> that strips the header) costs nothing but the fallback. Because the Cache API
> can't hold partial responses, a streamed book is online-only rather than part of
> the offline PWA cache.
How to *mark* a published book for streaming (`khb pack --stream`, the
uncompressed-under-compact rule, when it pays off) is covered in
[pack --stream](khb-publishing:pack-stream) and [Hosting](khb-publishing:hosting).
---
# Security model
A `.khb` can come from anywhere — a user opens, uploads or streams one — so every
stored `body_html` is treated as **untrusted**. The viewer's answer is origin
isolation, not sanitization. `docs/format.md` §Security is the normative
description.
## The boundary: a sandboxed frame without `allow-same-origin`
Every page body renders in a sandboxed `iframe` with
`sandbox="allow-scripts allow-downloads"` — crucially **without**
`allow-same-origin` — so the frame is an isolated, opaque origin. That origin
isolation (not script-blocking) is the security boundary:
- Untrusted JS **may run**, but in a different origin it cannot reach the app: no
parent DOM, no `localStorage`, no access to the IndexedDB where other docsets
live. Content CSS is confined to the frame and can't spoof the app chrome.
- Beyond `allow-downloads` — which only lets an asset link save its file (the
book's own bytes) and grants no app or same-origin reach — the frame gets **no
other sandbox tokens**: no popups, modals, forms or top-navigation, so hostile
content can't even navigate away or open a window.
> [!IMPORTANT]
> The model deliberately does *not* rely on stripping scripts from stored HTML.
> Sanitizers are a moving target; an opaque origin is a browser-enforced wall. A
> malicious book gets a JavaScript playpen with nothing in it.
App-generated UI (the Search page) renders in the normal document, never in the
frame — it is trusted output of the app itself.
## The bridge: one narrow, validated channel
A small **trusted bridge** injected into the frame is the *only* channel across
the boundary. Outbound, it `postMessage`s **link intents** — open a page id, or
open an external URL, carrying the click's modifier keys so the app can honour
"open in new tab" — plus scroll state (and scrolls the first search hit into
view). Inbound, it accepts display-only messages such as font size and colour theme.
The app side treats every inbound message as hostile:
| Check | Effect |
|-------|--------|
| Source | the message must come from the content frame itself |
| Shape | only known message shapes (`open`, `ext`, …) are accepted |
| Safe-by-design actions | an `open` just routes — an unknown id shows "not found"; `ext` only opens vetted URL schemes |
Nothing the frame can say makes the app execute content-controlled code; the worst
a message can do is navigate to a page or be ignored.
## Assets and links
- Attachments are inlined as **`data:` URLs**, so they are self-contained and load
inside the isolated frame without granting it any network identity.
- External fetches from content are effectively blocked: the frame has no origin
to make credentialed requests from, and the app never proxies for it — a
hostile book can't exfiltrate through the app or phone home with the reader's
credentials.
- `javascript:` and other unknown link schemes are stripped when rendering.
## Defence in depth from the compiler
The bundled compiler renders Markdown with **raw HTML escaped**, so first-party
docsets contain no markup that would ever need neutralising. This is a courtesy,
not the boundary — the sandbox assumes third-party compilers (see
[Building a compiler](md/khb-internals/building-a-compiler.md)) may emit arbitrary HTML, and holds
regardless.
---
# Manifest schemas
Three small JSON documents describe books to the viewer: `docsets.json` and
`config.json` (both written into a packed distribution by `khb pack`) and `.khbm`
(an import manifest authored by hand). This page is the field-by-field schema —
what publishers do with them lives in
[Distribution anatomy](khb-publishing:distribution) and
[.khbm manifests](khb-publishing:khbm-manifests).
## `docsets.json` — the packed-dist manifest
Loaded by the viewer on start; lists the bundled docsets. All paths are relative
to the dist root.
```json [docsets.json]
{
"docsets": [
{ "file": "docsets/docs.khb.gz", "id": "my-docs", "title": "My Docs",
"language": "en", "collection": "my-product", "version": "1.2.0",
"attachments": ["docsets/docs.khba.gz"] },
{ "file": "docsets/big-book.khb", "id": "big-book", "title": "Big Book",
"language": "en", "collection": "big-book", "streaming": true }
]
}
```
| Field | Required | Meaning |
|-------|----------|---------|
| `file` | yes | path under the dist root; a trailing `.gz` means gzip-compressed, decompressed after fetch |
| `id` | yes | the docset id (`meta.docset_id`) — the namespace in `docsetId:pageId` |
| `title` | yes | display title |
| `language` | yes | content language; drives per-collection language selection |
| `collection` | no (default `""`) | product/family key (`meta.collection`); books sharing it are one product across languages/versions |
| `version` | no (omitted when empty) | content version (`meta.version`), surfaced in the viewer and its version switcher |
| `attachments` | no (omitted when empty) | sidecar `.khba` pack paths (each optionally `.gz`), opened alongside the docset |
| `streaming` | no (default `false`) | opt-in page-level streaming: open this docset (and its packs) over HTTP `Range`, falling back to a whole fetch when the host can't `Range` |
| `hash` | no | stable content identity used to version the URL and offline-cache entry; `khb pack` hashes the shipped bytes, while the registry uses the R2 ETag |
Besides `docsets`, the manifest may carry one optional top-level field:
| Field | Required | Meaning |
|-------|----------|---------|
| `folders` | no | a nested presentation tree grouping product families into TOC folders (below) |
> [!NOTE]
> `streaming` and `.gz` are mutually exclusive in practice: streamed files must be
> served raw, so the viewer ignores the flag on `.gz` entries (see
> [Streaming](md/khb-internals/streaming.md)).
### `folders` — nested TOC folders (optional)
Groups product **families** (`collection` ids) into arbitrarily nested folders
rendered above the family level in the Contents tree. Written by `khb pack
--folders ` (the file holds the bare array) and preserved verbatim by
`khb patch`.
```json [docsets.json (fragment)]
"folders": [
{ "id": "tools", "title": "Developer Tools", "titles": { "pl": "Narzędzia" },
"children": [
{ "collection": "my-product" },
{ "id": "legacy", "title": "Legacy",
"children": [ { "collection": "old-product" } ] }
] }
]
```
A child is either a **leaf ref** `{ "collection": "" }` (places that family
here) or a **nested folder** of the same shape.
| Field | Required | Meaning |
|-------|----------|---------|
| `id` | yes | stable folder key — the viewer persists expansion state on it (`@shelf:`), so renaming the title is safe, renaming the id resets its open/closed state |
| `title` | yes | default display title |
| `titles` | no | per-UI-language titles; the viewer picks `titles[uiLang]`, else `title` |
| `children` | no | leaf refs and/or nested folders |
Rules (enforced by the CLI; the viewer warns and ignores a broken tree rather
than failing to boot):
- a collection may be placed **once** in the whole tree, and folder `id`s must
be unique — duplicates are a pack error;
- a ref to a collection that isn't among the packed docsets is only a
**warning** (the same folders file may serve a registry hosting more books);
- a family the tree doesn't mention renders at the **root**, after the folders
— so do uploaded and remote books, whose collections a shipped manifest can't
know. A manifest without `folders` behaves exactly as before;
- folders whose families aren't loaded (and refs to absent collections) are
dropped, never rendered empty.
## `config.json` — the distribution profile
Written next to `docsets.json`; drives the viewer's profile.
```json [config.json]
{
"externalSources": true,
"pwa": true,
"home": "my-docs:getting-started",
"prefetch": true
}
```
| Field | Type | Meaning |
|-------|------|---------|
| `externalSources` | boolean | `true` (reader profile): users may open/upload/add docsets. `false` (`bundled --lock`): those affordances are hidden and remote sources are never used |
| `pwa` | boolean | `true` registers a service worker for best-effort offline use |
| `home` | string, optional | the landing view on a cold start: a page id (`docsetId:localId`) or the literal `"search"`. Omitted → the viewer defaults to the Search page |
| `prefetch` | boolean, optional | default the per-device “Keep books offline” toggle to on for streamed books |
| `prefetchLocked` | boolean, optional | hide and hard-disable offline prefetch, overriding any saved per-device choice |
## `.khbm` — the import manifest
A `.khbm` names several remote docsets so a whole product can be added in one step
(*Manage docsets → Import manifest…*). It is **not** `docsets.json`: a
`docsets.json` describes a packed dist with dist-root-relative paths, while a
`.khbm` is authored for import and its URLs resolve **relative to the manifest's
own URL** — so it can ship beside its `.khb`/`.khba` files and reference them with
plain relative paths.
```json [books.khbm]
{
"khbm": 1,
"title": "My Product Docs",
"docsets": [
{ "url": "en.khb", "attachments": ["en.khba"] },
{ "url": "https://cdn.example/pl.khb.gz" }
]
}
```
| Field | Required | Meaning |
|-------|----------|---------|
| `khbm` | yes | format marker/version (`1`); its absence rejects the file |
| `title` | no | display name for the imported set |
| `docsets` | yes | array of entries |
| `docsets[].url` | yes | the `.khb` URL, resolved against the manifest URL |
| `docsets[].attachments` | no | `.khba` pack URLs, each resolved against the manifest URL |
Parsing is lenient about entries and strict about the envelope: a missing `khbm`
marker or a non-array `docsets` is an error, while an entry without a usable `url`
is silently skipped. Note there is deliberately **no** per-entry `streaming` field
— the manifest describes *what* the docsets are, not *how* to fetch them; whether
to stream is a reader/transport choice negotiated per docset. The reference
parser is `viewer-ts/src/data/khbm.ts`.
---
# Building a compiler
A `.khb` is plain SQLite, so any tool that can create a SQLite database can
produce a valid book — from reStructuredText, AsciiDoc, DocBook, a wiki export,
anything. This page is the checklist. The bundled Markdown compiler
([khb compile](khb-authoring:compiling)) is one producer among possible many;
`docs/format.md` is the contract they all target.
## The checklist
1. **Create the schema tables** — every table on the
[SQLite schema](md/khb-internals/sqlite-schema.md) page: `meta`, `pages`, `toc`, `categories`,
`page_categories`, `keywords`, `related`, `products`, `assets` (may be empty)
and `asset_index`. The DDL to copy is in `compiler/core/src/schema.rs`.
2. **Fill `meta`** — at minimum `format_version` (currently `1`), `docset_id`,
`title`, `version`, `language`, `tokenizer` (the string you actually used —
see step 5) and `generator` (name your tool). Set `collection` if the book
should merge with siblings into one product family.
3. **Render HTML and plain text yourself.** The viewer runs **no** Markdown
engine — `pages.body_html` is the canonical, final render, and `pages.plain`
is the extracted plain text used for search and snippets. Whatever your source
format, both must be produced at compile time. Two rendering rules worth
copying from the bundled compiler: emit syntax highlighting as **CSS
classes**, not inline colours — the viewer injects a theme stylesheet into
the content frame, so class-tagged code follows the app theme (including
dark mode) while hard-coded colours would not; and derive `plain`
from an **unhighlighted** render, so per-token markup never leaks into
full-text search.
4. **Optionally fill `pages.md`** with a clean Markdown rendition (nullable). The viewer ignores it; it feeds AI-facing surfaces such as the
`llms.txt` export. Skip it if your source has no sensible Markdown form.
5. **Create the FTS index with the right tokenizer.** Emit the
external-content `pages_fts` table (`content='pages'`) and pick the tokenizer
from the language — `porter unicode61 remove_diacritics 2` for English,
`unicode61 remove_diacritics 2` otherwise — then populate it (with external
content, insert into `pages_fts(rowid, title, plain, keywords)` yourself or
use the `rebuild` command). Details: [Full-text search](md/khb-internals/full-text-search.md).
6. **Reference binary files via the `asset:` scheme.** Rewrite image/link
targets to `asset:`, store the bytes in `assets` (embedded) or in
`.khba` sidecars, and fill `asset_index` for **every** path — `''` for
embedded, the sidecar's `meta.pack` id otherwise (see
[File formats](md/khb-internals/file-formats.md)).
7. **Validate in-book links.** The viewer resolves a bare `page-id` link within
the book and shows "not found" for a dangling one, so check at compile time
that every in-book link, TOC `page_id` and in-book `related` target names an
existing page. Cross-book ids (`docsetId:localId`) are stored as-is — the
target book may not be loaded, and the viewer hides such links.
8. **Build the TOC** in `toc` with `parent_id`/`position`; use `page_id = NULL`
for pure folder nodes. `VACUUM` the finished database.
## What you get for free
Do the above and the whole stack works without any extra effort:
- the book opens in the KD Help Book Viewer (upload, URL, or packed) and
**merges** into collections by `meta.collection`;
- native and streamed search use your FTS index directly;
- **streaming included** — a valid `.khb` is streaming-ready as-is: the Range-VFS
reads any well-formed SQLite file page-by-page, and `khb inspect ` is a
quick way to prove yours streams;
- `khb pack` / `patch` will pick it up like any bundled book.
> [!TIP]
> Cheap conformance test: compile a book, then run `khb inspect my.khb` and open
> the file in the viewer next to a first-party docset. If metadata, TOC, index,
> search and images all behave, you have hit the contract.
> [!WARNING]
> Remember that whatever HTML you emit is rendered as **untrusted** content — the
> viewer sandboxes it regardless of who produced it (see the
> [Security model](md/khb-internals/security-model.md)). Don't rely on scripts reaching the app, and
> keep pages self-contained: external URLs won't be fetched; anything a page
> needs must be an `asset:`.
---
# Publishing KD Help Books
This volume is for **maintainers and publishers**: you have one or more compiled
`.khb` books (see [Authoring KD Help Books](khb-authoring:index) for how they are
written and compiled) and want readers to open them — on a website, offline, or
straight from a URL.
The whole pipeline is **static**. `khb pack` assembles a self-contained directory —
the KD Help Book Viewer plus your books plus two small JSON files — and any static
file host serves it as-is. No backend, no database, no build step on the server.
## What's in this volume
| Page | Covers |
|------|--------|
| [Getting published](md/khb-publishing/getting-published.md) | a working site in five minutes |
| [pack](md/khb-publishing/pack.md) | assembling a distribution — every flag |
| [patch](md/khb-publishing/patch.md) | updating a built distribution in place |
| [Anatomy of a distribution](md/khb-publishing/distribution.md) | `docsets.json`, `config.json`, and how the viewer reads them |
| [Hosting](md/khb-publishing/hosting.md) | static hosts, GitHub Pages, HTTP `Range`, CORS |
| [CI with GitHub Actions](md/khb-publishing/ci.md) | a copy-paste workflow that builds and deploys the site |
| [Versioning](md/khb-publishing/versioning.md) | shipping several versions of one book side by side |
| [.khbm manifests](md/khb-publishing/khbm-manifests.md) | a fetchable list of docsets readers import in one step |
## Where to go next
- New to publishing? Start with [Getting published](md/khb-publishing/getting-published.md).
- Writing the books themselves? That's the
[authoring volume](khb-authoring:index).
- Curious what's inside a `.khb`, how streaming works, or building your own
compiler? See [KD Help Book Internals](khb-internals:index).
---
# Getting published
From a compiled book to a live documentation site in five minutes. You need two
inputs: a **built viewer** and at least one **`.khb` docset**.
## 1. Get the viewer
Every release ships the viewer as a ready-built archive
(`khb-viewer-vX.Y.Z.tar.gz` under the release's assets) — unpack it anywhere. Or
build it from source:
```bash
cd viewer-ts
npm ci
npm run build # -> viewer-ts/dist
```
## 2. Get a book
If you haven't compiled one yet, the [authoring volume](khb-authoring:compiling)
covers `khb compile`:
```bash
khb compile my-docs -o my.khb
```
## 3. Pack
`khb pack` copies the viewer, bundles the books, and writes the manifest and
config the viewer reads on start:
~~~code-preview
```bash
khb pack --viewer viewer-ts/dist --docset my.khb -o publish
```
```
packed 1 docset(s) + viewer -> publish
```
~~~
What lands in `publish/`:
| Entry | What it is |
|-------|------------|
| `index.html`, `assets/…` | the viewer, copied verbatim |
| `docsets/my.khb` | your book (plus any sidecar `.khba` packs found next to it) |
| `docsets.json` | the manifest listing every bundled book |
| `config.json` | the distribution profile (external sources, PWA, home page) |
Both JSON files are described in [Anatomy of a distribution](md/khb-publishing/distribution.md).
## 4. Serve it
The output is plain static files — serve the directory with anything:
```bash
python3 -m http.server -d publish 8080
```
then open `http://localhost:8080`. For real hosting (GitHub Pages included) see
[Hosting](md/khb-publishing/hosting.md).
> [!TIP]
> The defaults produce a `reader` profile: visitors can open their own docsets and
> a service worker caches the app for offline use. Publishing a single product's
> locked-down docs? See [Profiles](md/khb-publishing/pack-profiles.md).
---
# pack — build a distribution
`khb pack` assembles a **publishable static distribution**: it copies a built
viewer, bundles docsets into `docsets/`, and writes `docsets.json` (metadata read
from each docset — nothing to declare by hand) and `config.json`. For each
`foo.khb` it also picks up any sibling attachment packs (`foo.khba`,
`foo..khba`), records them in the docset's `attachments` array, and rewrites
the book's asset-routing index to cover exactly the packs being shipped.
```bash
khb pack --viewer viewer-ts/dist \
--docset docs.khb --docset extras.khb \
--profile reader \
-o publish/
```
`pack` starts from a clean slate: a stale `docsets.json`, `config.json`, or
`docsets/` left in the output (for example by a dev build) is removed and
rewritten — the manifest describes exactly what you packed, nothing more. To
update an existing distribution without re-packing everything, use
[patch](md/khb-publishing/patch.md).
## Flags
| Flag | Meaning |
|------|---------|
| `--viewer ` | the built viewer to copy |
| `--docset ` | a docset to bundle (repeatable, at least one) |
| `-o ` | output directory |
| `--mode khb\|compact` | how files ship: as-is, or gzipped to `.gz` — see [Compression](md/khb-publishing/pack-mode.md) |
| `--profile reader\|bundled` | sets the external-sources / PWA defaults — see [Profiles](md/khb-publishing/pack-profiles.md) |
| `--lock` | lock the build: no docset management at all — see [Profiles](md/khb-publishing/pack-profiles.md) |
| `--pwa` / `--no-pwa` | force the service worker on / off — see [Profiles](md/khb-publishing/pack-profiles.md) |
| `--home ` | the cold-start landing view — see [The landing page](md/khb-publishing/pack-home.md) |
| `--llms` | also emit the AI-facing `llms.txt` export — see [AI export](md/khb-publishing/pack-llms.md) |
| `--stream […]` | mark docset(s) for page-level streaming — see [Streaming](md/khb-publishing/pack-stream.md) |
| `--prefetch` / `--no-prefetch` | default the offline cache on, or hard-disable it — see [Streaming](md/khb-publishing/pack-stream.md) |
| `--folders ` | copy a validated `folders` tree (nested TOC folders grouping product families) into `docsets.json` — schema in [manifest schemas](khb-internals:manifest-schemas). `patch` preserves it untouched |
---
# Profiles (--profile, --lock, --pwa)
`--profile` picks a pair of defaults that `config.json` records; `--lock`,
`--pwa`, and `--no-pwa` override the two switches individually.
## The two profiles
| Profile | `externalSources` | `pwa` | Use |
|---------|-------------------|-------|-----|
| `reader` (default) | `true` | `true` | a general reader: visitors can open, upload, and manage docsets |
| `bundled` | `false` | `false` | one product's docs, locked down — the site serves exactly the books you packed |
So `--profile reader` writes:
```json [config.json]
{
"externalSources": true,
"pwa": true
}
```
and `--profile bundled` writes both as `false`.
## What each switch does
**`externalSources: false`** removes docset management from the UI entirely:
*File → Open docset…*, *Open docset from URL…*, and the whole **Manage docsets**
page are hidden, and the viewer skips loading any uploaded or remote docsets and
attachment packs a visitor's browser may have persisted. Docsets are read-only
either way — this removes the reader's ability to add, remove, or attach them.
**`pwa: true`** registers a service worker for best-effort offline use.
## Overrides
| Flag | Effect |
|------|--------|
| `--lock` | force `externalSources: false`, whatever the profile |
| `--pwa` | force the service worker on |
| `--no-pwa` | force the service worker off |
`--profile bundled` already implies the lock, so `--lock` matters when you want a
`reader`-style build that still forbids adding sources.
## When to keep the PWA off
> [!WARNING]
> A service worker caches the app — including `docsets.json` and the books. After
> a deploy, returning visitors may keep reading a **stale offline copy** until the
> worker updates in the background. If your docs change often (or you deploy on
> every merge), pack with `--no-pwa` so every visit fetches the current site; turn
> the PWA on when offline reading is worth the update lag.
---
# The landing page (--home)
`--home` sets what a visitor sees on a **cold start** — a first visit, or any
visit that doesn't deep-link to a specific page. It only affects cold starts: a
shared deep link, a bookmark, or a restored session still opens exactly what it
points at.
## Syntax
```bash
khb pack … --home my-docs:index # a page, by full id
khb pack … --home search # the Search page, explicitly
```
The value is either:
| Value | Landing |
|-------|---------|
| `docsetId:localId` | that page, opened with the table of contents revealed to it |
| `search` | the Search page |
The page id is the **namespaced** form (`docsetId:localId`) — the docset id from
the book's `docset.toml` plus the page id, joined by `:`. A bare local id is not
enough because a distribution can bundle several books.
Omit the flag and the viewer defaults to the **Search page** (search-first) — the
same as passing `search`, just left unwritten in `config.json`.
## What it writes
```json [config.json]
{
"externalSources": false,
"pwa": false,
"home": "my-docs:index"
}
```
---
# Streaming (--stream)
`--stream` marks docsets for **page-level streaming**: instead of downloading the
whole `.khb` up front, the viewer opens it over HTTP `Range` and reads only the
pages a visitor actually touches. Worth it for big books; a small book is often
cheaper to fetch whole.
## Syntax
```bash
khb pack … --stream # mark every bundled docset
khb pack … --stream big.khb # mark only this one (repeatable)
khb pack … --stream big.khb --stream atlas.khb
```
A `--stream ` must name one of the `--docset` paths (matched by full path
or by file name) — anything else is an error, so a typo can't silently ship an
unstreamed book. [patch](md/khb-publishing/patch.md) accepts `--stream` too, applied to the docsets
being added or replaced.
## What it does
All the flag changes is one field in the docset's manifest entry:
```json [docsets.json]
{ "file": "docsets/big.khb", "id": "big-book", "title": "Big Book",
"language": "en", "streaming": true }
```
The viewer treats it as a **preference, not a promise**: it opens the file (and
its attachment packs) over `Range`, and if the host doesn't honour `Range` — or
the streamed open fails for any reason — it **falls back automatically** to
fetching the whole file. A streamed distribution works everywhere; it's just
faster where the host cooperates. How the viewer reads a database it never
downloads — the Range-VFS, block coalescing, and the wa-sqlite engine — is
covered in [Internals: streaming](khb-internals:streaming).
## The uncompressed rule
> [!WARNING]
> `Range` requests address **raw SQLite pages by byte offset**, so a streamed file
> must be served exactly as written. Under [`--mode compact`](md/khb-publishing/pack-mode.md) streamed
> docsets (and their packs) are therefore shipped **uncompressed** while everything
> else gzips; likewise, a `"streaming": true` on a `.gz` entry is ignored. Don't
> "fix" this by hand-gzipping a streamed file.
Your host must also serve the file raw — no transparent gzip/brotli re-encoding on
`.khb` responses — or byte offsets stop matching. See [Hosting](md/khb-publishing/hosting.md) for
host-side requirements.
## Keep streamed books offline (`--prefetch`)
Streaming is fast to *start* but re-reads pages from the network. `--prefetch`
adds a **"Keep books offline"** toggle to the viewer's **View** menu and sets its
default on:
```bash
khb pack … --stream --prefetch
```
When the toggle is on, a streamed book is used immediately **and** downloaded
whole in the background; the whole copy is cached in the browser (IndexedDB,
keyed by content hash) and the open book is **hot-swapped** to it with no reload.
Later visits open straight from that cache — instant and offline — until a new
build changes the content hash. It caches the **whole book and all its attachment
packs**, so images and downloads come with it — a prefetched book is fully
offline. It's a per-device user choice: `--prefetch` only sets the **default**,
and a reader can flip it either way. Off (the default without the flag) keeps the
pure page-by-page streaming behaviour.
To turn the feature off entirely — hide the toggle and never prefetch, whatever a
reader chose — pack with `--no-prefetch` (for sites that don't want the offline
cache, e.g. metered bandwidth):
```bash
khb pack … --stream --no-prefetch
```
---
# AI export (--llms)
`--llms` writes, alongside the viewer, the [llms.txt](https://llmstxt.org/)
family — so language models and agents can read your documentation as plain
files instead of scraping a single-page app.
## What it emits
| File | Contents |
|------|----------|
| `llms.txt` | a link index: an `H1` title, a one-line summary, then one section per book listing every page as `- [title](md/…): description`, in TOC order |
| `llms-full.txt` | every page's Markdown inline (with provenance comments), for one-shot ingestion |
| `md//.md` | each page as clean Markdown, fetchable on its own |
## Where the Markdown comes from
The export uses each page's **original Markdown source** — the optional `md`
column a compiler may store. A docset that carries none falls back to
the page's plain text, so the export always works; it's just nicer with the real
source. Books compiled by the bundled `khb compile` carry it.
> [!NOTE]
> Nothing here is loaded by the viewer — these are extra static files sitting next
> to it (though the viewer does _link_ to them from a menu item; see
> [In the viewer](#in-the-viewer)). They are also written as plain text even under
> [`--mode compact`](md/khb-publishing/pack-mode.md): they're meant to be fetched and read as-is.
## In the viewer
When the export is present, the viewer gains a **File → Copy links for LLMs** menu
item. It copies a small, paste-ready block for the page you're on: its title, the
page's `md//.md` URL **and** the in-app deep link, and a line pointing
at `llms.txt` for the full index — handy for handing a single page (or the whole
book) to a chat assistant.
The item keys off the same `` discovery hook `pack` injects, so
it appears **only** on an `--llms` build — a dev server or a plain `pack` hides it,
and never offers links to files that don't exist.
## Why
A `.khb` is a SQLite database rendered by a client-side app — perfect for humans,
opaque to a crawler. The `--llms` export is the **static counterpart to a future
MCP server**: the same content as plain files any static host serves without a
backend, today.
---
# Compression (--mode)
`--mode` decides how the bundled files ship: `khb` (the default) copies every
docset as-is; `compact` gzips them for a smaller download.
## The two modes
| Mode | Ships |
|------|-------|
| `khb` | `docsets/foo.khb` — the file, byte for byte |
| `compact` | `docsets/foo.khb.gz` — gzip-compressed (best compression) |
`compact` compresses **every shipped file**: each docset *and* its `.khba`
attachment packs. The `.gz` suffix simply appends to the real name
(`foo.khb` → `foo.khb.gz`), and `docsets.json` records the `.gz` path, so any
file can be compressed independently of the others.
## How the viewer decompresses
The viewer detects gzip by the file's **magic bytes**, not its name, and inflates
with the browser's native `DecompressionStream('gzip')`. SQLite databases are
full of repeated strings and padding, so the ratio is usually substantial.
> [!NOTE]
> **Exceptions to `compact`:** docsets marked for [streaming](md/khb-publishing/pack-stream.md) ship
> uncompressed even in compact mode (`Range` addresses raw bytes), and the
> [`--llms`](md/khb-publishing/pack-llms.md) export stays plain text (it's meant to be read as-is).
## Compact vs. host-level compression
Many hosts (and CDNs) already apply gzip or brotli on the wire. That helps HTML
and JS, but hosts typically don't compress unknown binary types like `.khb` —
`compact` guarantees the small transfer regardless of host configuration, at the
cost of a one-time inflate in the browser. If your host demonstrably compresses
`.khb` responses, plain `khb` mode serves the same bytes with less indirection.
---
# patch — update a distribution
`khb patch` adds or replaces docsets in an **already-built distribution**,
updating `docsets.json` in place — no need to re-run [pack](md/khb-publishing/pack.md) with the full
docset list, and no viewer files are touched.
~~~code-preview
```bash
khb patch publish/ --docset new.khb
```
```
patched 1 docset(s) into publish/
```
~~~
## Add or replace, by id
Each patched book is matched against the manifest by its **docset id** (read from
the file, not the file name):
- an entry with the **same id** is replaced — the new file, metadata, and
attachment packs take its place;
- a **new id** is appended to the manifest.
Like `pack`, `patch` picks up sibling attachment packs (`foo.khba`,
`foo..khba`) next to each `.khb` and records them in the entry's
`attachments`. Everything else in `docsets.json` — and all of `config.json` — is
left untouched.
## Flags
| Flag | Meaning |
|------|---------|
| `--docset ` | a docset to add or replace (repeatable, at least one) |
| `--mode khb\|compact` | ship the patched books gzipped — see [Compression](md/khb-publishing/pack-mode.md) |
| `--stream […]` | mark the patched books for streaming — see [Streaming](md/khb-publishing/pack-stream.md) |
`--mode` and `--stream` apply **only to the docsets being added or replaced**;
existing entries keep whatever they were packed with. That makes `patch` the
natural CI verb for [archived versions](md/khb-publishing/versioning.md): pack the current site once,
then patch in each archived book downloaded from a release.
---
# Anatomy of a distribution
A packed distribution is four things: the viewer, a `docsets/` folder, and two
JSON files the viewer reads on start. This page explains what each field means
and how the viewer consumes it; the formal field-by-field schema lives in
[Internals: manifest schemas](khb-internals:manifest-schemas).
```text
publish/
├── index.html, assets/… # the viewer, copied verbatim
├── docsets/
│ ├── docs.khb.gz # a bundled book (compact mode)
│ ├── docs.khba.gz # …and its attachment pack
│ └── big-book.khb # a streamed book (always uncompressed)
├── docsets.json # the manifest
├── config.json # the profile
└── llms.txt, llms-full.txt, md/… # only with --llms
```
## docsets.json
One entry per bundled book, all metadata read from the docset itself at pack
time:
```json [docsets.json]
{
"docsets": [
{ "file": "docsets/docs.khb.gz", "id": "my-docs", "title": "My Docs",
"language": "en", "collection": "my-product", "version": "1.2.0",
"attachments": ["docsets/docs.khba.gz"] },
{ "file": "docsets/big-book.khb", "id": "big-book", "title": "Big Book",
"language": "en", "collection": "big-book", "streaming": true }
]
}
```
| Field | Meaning |
|-------|---------|
| `file` | path under the dist root. A trailing `.gz` means gzip-compressed; the viewer decompresses after fetch |
| `id` | the docset id — namespaces every page id (`docsetId:localId`) |
| `title` | display title |
| `language` | the book's content language |
| `collection` | the product/family key: books sharing it are one product across languages and versions, so the viewer picks one language variant per collection |
| `version` | the content version, surfaced read-only and driving the [version switcher](md/khb-publishing/versioning.md); omitted when unset |
| `attachments` | sidecar `.khba` packs (zero or more, each optionally `.gz`); the viewer opens them beside the docset |
| `streaming` | opt-in page-level [streaming](md/khb-publishing/pack-stream.md): open over HTTP `Range`, falling back to a whole fetch; omitted when `false` |
## config.json
The distribution profile:
```json [config.json]
{
"externalSources": false,
"pwa": false,
"home": "my-docs:index",
"prefetch": true
}
```
| Field | Meaning |
|-------|---------|
| `externalSources` | `false` hides all docset management (open / URL / manage) and skips persisted uploads and remotes — see [Profiles](md/khb-publishing/pack-profiles.md) |
| `pwa` | register the service worker for best-effort offline use |
| `home` | cold-start landing: a page id or `"search"`; omitted → the Search page — see [The landing page](md/khb-publishing/pack-home.md) |
| `prefetch` | default the per-device “Keep books offline” toggle to on for streamed books |
| `prefetchLocked` | hide and hard-disable offline prefetch (`khb pack --no-prefetch`) |
## How the viewer consumes them
On start the viewer fetches `config.json`, then `docsets.json`, then loads every
listed book (inflating `.gz` files, opening `attachments` alongside, streaming
the `streaming: true` ones). Bundled books merge with whatever the visitor has
uploaded or added by URL — unless `externalSources` is off — into one table of
contents, index, and search.
> [!NOTE]
> You never edit these files by hand in normal use: [pack](md/khb-publishing/pack.md) writes both, and
> [patch](md/khb-publishing/patch.md) updates `docsets.json` surgically. The schemas matter when you're
> generating a distribution some other way.
---
# Hosting
A packed distribution is plain static files — **any static host works**: GitHub
Pages, Netlify, S3 + CloudFront, nginx, or a directory listing on an intranet
box. This page covers the few host behaviours that matter.
## GitHub Pages walkthrough
The viewer is built with base `"./"`, so the same dist works from a user site
root (`user.github.io/`) **or any repository subpath**
(`user.github.io/my-docs/`) — nothing to configure.
1. Pack your distribution: `khb pack --viewer dist --docset my.khb -o publish`.
2. Push the contents of `publish/` to the branch Pages serves (e.g. `gh-pages`),
or upload it as a Pages artifact from a workflow.
3. Enable Pages on that branch in the repository settings.
GitHub Pages **honours HTTP `Range`**, so [streamed](md/khb-publishing/pack-stream.md) books work
there at full effect — this site itself is packed with `--stream` and served
from Pages.
## The registry (Cloudflare Workers + R2)
Beyond static hosting there is a **dynamic** option: the
[KD Help Book Registry](khb-registry:index) — a central docs site many projects
publish to from their own CI (GitHub Actions OIDC; no shared secrets, and a
permission map keeps one project from ever writing another's books). It serves
the viewer, streams `.khb` files from R2 with full `Range` support, and generates
`docsets.json` (including a central
[`folders` tree](khb-internals:manifest-schemas)) on the fly. The registry
volume covers one-click Cloudflare deployment, configuration, publishing,
updates, and troubleshooting.
## HTTP Range (streaming)
A docset marked `"streaming": true` is opened with `Range` requests. The host
must:
- answer `Range: bytes=…` with `206 Partial Content`;
- serve the `.khb` **raw** — no transparent gzip/brotli re-encoding on it (byte
offsets must match the file on disk).
If either fails, nothing breaks: the viewer falls back to fetching the whole
file. See [Streaming](md/khb-publishing/pack-stream.md) for the packing side.
## Compressed transfer (`.khb.gz`)
Files packed with [`--mode compact`](md/khb-publishing/pack-mode.md) ship pre-gzipped as
`.khb.gz` and are inflated in the browser. This needs nothing from the
host — it's an ordinary binary file — and works even on hosts that never
compress unknown MIME types.
## CORS
**Same-origin hosting is the recommendation**: put the books in the same site as
the viewer (which is exactly what `pack` produces) and CORS never enters the
picture.
CORS only matters when a *viewer on origin A* loads a *docset from origin B* —
e.g. *File → Open docset from URL…* or a [.khbm manifest](md/khb-publishing/khbm-manifests.md)
pointing at a CDN. Then origin B must send `Access-Control-Allow-Origin` for the
fetch to succeed, and expose `Range` handling cross-origin if you want streaming.
> [!WARNING]
> **GitHub release assets are not CORS-readable from browsers.** Since GitHub
> moved release downloads to Azure blob storage, the redirected asset responses
> lack CORS headers — a browser-based viewer simply cannot fetch a `.khb`
> straight from a release URL. That's why our CI **copies archived books into the
> site** ([versioning](md/khb-publishing/versioning.md)) instead of linking to release assets: releases
> remain the archive of record for *people and CLIs* (`khb inspect ` works
> fine — no browser, no CORS), while everything a browser loads lives on the
> Pages origin.
---
# CI with GitHub Actions
Publishing is a build you can automate: fetch the `khb` CLI and the prebuilt
viewer, compile the book, `pack`, deploy. There are three ways to wire it up,
trading brevity for control: a **reusable workflow** that does it all in a few
lines, the **composite actions** it's built from when you want your own job, or
the **full workflow** spelled out step by step (handy for other CI systems).
## The quick way: a reusable workflow
KD Help Book ships a reusable workflow, `book-pages.yml`, that fetches the
toolchain, compiles every book in your repository, packs a bundled site, and
deploys it to *your* repository's GitHub Pages. Your workflow just calls it:
```yaml [.github/workflows/docs.yml]
name: Docs
on:
push:
branches: [main]
jobs:
docs:
uses: KDHelpBook/monorepo/.github/workflows/book-pages.yml@v1
permissions:
contents: read
pages: write
id-token: write
with:
sources: "." # dirs with a docset.toml (globs OK, e.g. docs/*)
home: my-book:index # cold-start landing page
```
One-time setup: **Settings → Pages → Source → GitHub Actions**. Then every push
to `main` rebuilds and redeploys — no secrets, no boilerplate.
The inputs mirror the [`pack`](md/khb-publishing/pack.md) flags:
| Input | Default | Meaning |
|-------|---------|---------|
| `version` | workflow version | khb release to build with; an exact workflow tag uses the matching release, while `@v1`/branches use `latest` |
| `sources` | `.` | source dirs, whitespace/newline separated; shell globs expand |
| `home` | — | cold-start [landing page](md/khb-publishing/pack-home.md) id, or `search` |
| `stream` | `true` | mark books for [streaming](md/khb-publishing/pack-stream.md) |
| `llms` | `true` | emit the [AI export](md/khb-publishing/pack-llms.md) |
| `base-url` | the Pages URL | override the deploy URL (needs a trailing slash) |
| `extra-pack-args` | — | any extra `pack` flags, e.g. `--mode compact` |
Pin `@v1` for the latest v1.x, or a full `@vX.Y.Z` to lock the workflow,
composite actions, CLI, and viewer to the same release. Set `version` explicitly
only when you intentionally want a different CLI/viewer release.
### PR previews
A companion reusable workflow, `book-pr-preview.yml`, publishes a compiled
preview to `pr-preview/pr-/` for any pull request carrying a `preview` label,
and tears it down when the label is removed or the PR closes:
```yaml [.github/workflows/pr-preview.yml]
on:
pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize, closed]
jobs:
preview:
uses: KDHelpBook/monorepo/.github/workflows/book-pr-preview.yml@v1
permissions:
contents: write
pull-requests: write
with:
home: my-book:index
```
One-time setup for previews: **Settings → Pages → Source → Deploy from a branch
→ gh-pages**.
## The building blocks: setup-khb + build-book
The reusable workflows are thin wrappers over two composite actions. Reach for
the actions directly when you want your own job — custom triggers, extra steps,
a deploy target that isn't GitHub Pages — without re-deriving the
compile-and-pack dance.
**`setup-khb`** downloads the khb CLI (and, by default, the prebuilt viewer)
from a release and puts `khb` on `PATH`:
| Input | Default | Meaning |
|-------|---------|---------|
| `version` | `latest` | release to fetch — a tag (`vX.Y.Z`) or `latest` |
| `viewer` | `true` | also download the prebuilt viewer |
| `repository` | `KDHelpBook/monorepo` | where to fetch the release from |
Outputs: `khb` (binary path, also added to `PATH`), `viewer-dir`, and `version`
(the concrete tag fetched — a real `vX.Y.Z` even when you asked for `latest`).
**`build-book`** compiles every source directory that has a `docset.toml` (globs
expand, so `docs/*` picks up each volume) and packs a distribution:
| Input | Default | Meaning |
|-------|---------|---------|
| `viewer-dir` | *(required)* | prebuilt viewer — e.g. `setup-khb`'s `viewer-dir` output |
| `khb` | `khb` | binary path; defaults to the one `setup-khb` put on `PATH` |
| `sources` | `.` | source dirs, whitespace/newline separated; shell globs expand |
| `out` | `publish` | output distribution directory |
| `profile` | `bundled` | `reader` or `bundled` (see [Profiles](md/khb-publishing/pack-profiles.md)) |
| `home` | — | cold-start [landing page](md/khb-publishing/pack-home.md) id or `search` |
| `base-url` | — | absolute deploy URL (trailing slash); with `llms`, writes sitemap + robots |
| `stream` | `true` | mark books for [streaming](md/khb-publishing/pack-stream.md) |
| `llms` | `true` | emit the [AI export](md/khb-publishing/pack-llms.md) |
| `allow-extensions` | `false` | run each docset's `[extensions]` transformers |
| `extra-pack-args` | — | appended verbatim to `khb pack` |
Output: `dist`, the packed directory. A custom job wiring the two together, then
deploying however you like:
```yaml [.github/workflows/docs.yml]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: KDHelpBook/monorepo/.github/actions/setup-khb@v1
id: khb
with:
version: v1.2.0 # pin a tag for reproducible builds
- uses: KDHelpBook/monorepo/.github/actions/build-book@v1
with:
viewer-dir: ${{ steps.khb.outputs.viewer-dir }}
sources: docs/*
home: my-book:index
# …then deploy the `publish/` directory to any static host.
```
Both actions run on Linux and macOS runners.
## The full workflow
When you'd rather not depend on the actions at all — another CI system, or the
most explicit possible pipeline — call the CLI directly. This is what
`setup-khb` and `build-book` do under the hood:
```yaml [.github/workflows/publish-book.yml]
name: Publish the book
on:
push:
branches: [main]
# Deploying to Pages needs these two; contents stays read-only.
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# The khb CLI binary and the prebuilt viewer, from the latest
# KD Help Book release.
- name: Fetch khb and the viewer
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download --repo KDHelpBook/monorepo \
--pattern 'khb-v*-x86_64-unknown-linux-gnu.tar.gz' \
--pattern 'khb-viewer-*.tar.gz'
tar xzf khb-v*-x86_64-unknown-linux-gnu.tar.gz --strip-components=1
mkdir viewer && tar xzf khb-viewer-*.tar.gz -C viewer --strip-components=1
# Compile the book (this repository is the source folder) and
# assemble the site.
- name: Compile and pack
run: |
./khb compile . -o book.khb
./khb pack --viewer viewer \
--docset book.khb \
--profile bundled \
--home my-book:index \
-o publish
- uses: actions/configure-pages@v6
- uses: actions/upload-pages-artifact@v5
with:
path: publish
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v5
```
Adjust three things for your repository: the source path in `khb compile` (here the
repo root *is* the book), the [`--home`](md/khb-publishing/pack-home.md) page id, and any extra
[`pack` flags](md/khb-publishing/pack.md) you publish with (`--stream`, `--llms`, more `--docset`s).
One-time setup: the repository's **Settings → Pages → Source** must be set to
*GitHub Actions*.
## Notes
- **Pin the tools for reproducible builds.** `gh release download` without a tag
takes the *latest* KD Help Book release; pass a tag (`gh release download
v1.2.0 …`) to pin, and bump it deliberately.
- **Several books?** Compile each source folder and pass several `--docset` flags
to one `pack` call.
- **Shipping old versions side by side?** Keep each release's compiled `.khb` (a
release asset works well) and [`patch`](md/khb-publishing/patch.md) them into the freshly packed site —
the pattern is described in [Versioning](md/khb-publishing/versioning.md).
- The result is a plain static directory — everything on the [Hosting](md/khb-publishing/hosting.md)
page applies to it, whatever CI system you use; GitHub Actions is just the worked
example.
---
# Versioning
The viewer can carry **several versions of one book** and show exactly one at a
time, with a switcher to reach the others. This page covers what the switcher
needs from a publisher and a convention that keeps a version archive cheap to
run.
## How the switcher works
Version editions of a book are **separate docsets** that share a `collection`
but differ in `version` (both from `docset.toml` — see
[Authoring: version](khb-authoring:docset-version)). When more than one is
loaded:
- the viewer shows only the **latest** by default — a numeric-dotted comparison,
so `1.10 > 1.2`;
- a **Version** selector appears (in the left panel, and per product under
*Manage docsets…*) to pin an older one; the choice persists across reloads;
- the same book never appears twice in the merged table of contents.
## Unique ids per version
Page ids are namespaced `docsetId:localId`, so two versions of one book **must
not share a docset id** — they'd collide. The convention: the tip keeps the bare
id, and each archived edition suffixes it with its version:
| Edition | `id` | `version` | `collection` |
|---------|------|-----------|--------------|
| current | `my-docs` | `latest` | `my-product` |
| archive | `my-docs-v1.1.2` | `1.1.2` | `my-product` |
| archive | `my-docs-v1.0.4` | `1.0.4` | `my-product` |
## The "latest" convention
Publish the tip with the literal version **`latest`**: non-numeric strings sort
*above* numeric versions in the viewer's comparison, so the current build is
always the default pick, with the numbered archives selectable behind it — and no
release ever "overtakes" the tip.
## One archive per minor series
Keep the switcher list short: merge only the **newest patch of each minor
series** into the site (`1.1.0`/`1.1.1`/`1.1.2` → only `1.1.2`). Superseded
patches stay downloadable from your releases; they just aren't merged.
## The worked example: our CI
This documentation is published with exactly this scheme, in three workflows:
1. **Release** — bumps `version` in every volume's `docset.toml`, tags, and
creates the GitHub release.
2. **Build** — on a release tag, compiles each volume with a version-suffixed id
(`khb-publishing` → `khb-publishing-v1.2.0`) and uploads the `.khb`s as
**release assets**: the durable archive.
3. **Publish** — compiles the current volumes as version `latest`, packs the
site, then downloads the newest patch of each minor series from past releases
and merges each with `khb patch publish --docset .khb --stream`.
Release assets are the archive *source*, not what browsers load — they're copied
into the site because browsers can't fetch them directly (see
[Hosting](md/khb-publishing/hosting.md) on CORS).
> [!TIP]
> The id and version suffixing happens **at build time only** (a `sed` over
> `docset.toml` in the workflow) — nothing version-suffixed is ever committed.
---
# .khbm manifests
A **`.khbm`** is a small JSON file naming several remote docsets, so a reader can
add a whole product in one step — *Manage docsets → Import manifest…* in the
viewer — instead of pasting URLs one by one. Import needs a build with external
sources enabled — a [locked distribution](md/khb-publishing/pack-profiles.md) has no *Manage docsets*
page.
## Format
```json [books.khbm]
{
"khbm": 1,
"title": "My Product Docs",
"docsets": [
{ "url": "en.khb", "attachments": ["en.khba"] },
{ "url": "https://cdn.example/pl.khb.gz" }
]
}
```
| Field | Meaning |
|-------|---------|
| `khbm` | format marker, required (currently `1`) |
| `title` | optional display name for the import |
| `docsets[].url` | the `.khb` to add (plain or `.gz`) |
| `docsets[].attachments` | optional `.khba` pack URLs for that docset |
## Resolution rules
Every `url` and `attachments` entry is resolved **relative to the manifest's own
URL**. That's the point: publish `books.khbm` in the same directory as the
`.khb`/`.khba` files and reference them with bare relative paths — move or mirror
the directory and the manifest keeps working. Absolute URLs are allowed too and
pass through untouched. Relative resolution also lets a `.khbm` act as a disk
entry point for the future desktop (Tauri) app, reading its books straight from a
folder.
This is the key difference from `docsets.json`, whose paths are relative to a
**packed dist root** and which carries per-book metadata; a `.khbm` is authored
for *import* and stays deliberately minimal. (Formal schemas for both:
[Internals: manifest schemas](khb-internals:manifest-schemas).)
## What it doesn't say
A `.khbm` describes **what** the docsets are, not **how** to fetch them: there is
no `streaming` field. Whether an imported book streams page-by-page or fetches
whole is the reader's auto-negotiated choice (host `Range` support, file size).
There is also deliberately **no `folders` field** (the nested TOC grouping a
`docsets.json` may carry): a `.khbm` lists URLs, and the collection ids folders
group by are only known after each book is fetched — besides, imported books are
*remote* sources, which render at the TOC root by rule. Folders belong to the
site shipping the manifest, not to an import.
> [!NOTE]
> The docsets a `.khbm` points at are fetched by the reader's **browser**, so a
> manifest hosted on another origin needs CORS on the files it names — see
> [Hosting](md/khb-publishing/hosting.md). Parsing is forgiving: an entry without a usable `url` is
> skipped; a missing `khbm` marker or a non-array `docsets` rejects the file.
---
# KD Help Book Registry
The KD Help Book Registry is a self-hosted documentation site for teams that
publish several help books from separate repositories. One Cloudflare Worker
serves the viewer and a dynamic manifest, while a private R2 bucket stores
immutable `.khb` editions.
Publishing repositories authenticate with short-lived GitHub Actions OIDC
tokens. There is no shared upload password, Cloudflare API token, or registry
secret to distribute. The registry configuration names exactly which
repository, ref, and optional GitHub environment may publish each docset.
## When to use it
Use a registry when:
- several repositories should publish into one documentation portal;
- books should stream directly from R2 with HTTP `Range`;
- publication should be secretless and restricted per docset;
- site order, folders, and viewer configuration should be managed centrally;
- old versions must remain immutable while `latest` moves atomically.
For one repository publishing one static site, [`khb pack`](khb-publishing:pack)
and an ordinary static host are usually simpler.
## How the pieces fit
| Component | Responsibility |
|---|---|
| `@kdhelpbook/cf-registry` | Worker runtime, configuration schema, CLI, and matching viewer |
| `KDHelpBook/cf-registry-template` | Deployable instance repository |
| `khb-registry.yml` | Site layout and publisher authorization |
| Cloudflare Worker | HTTP API, viewer, OIDC verification, and manifest generation |
| R2 bucket | Immutable `.khb`/`.khba` files and mutable `latest.json` pointers |
| `publish-registry.yml` | Reusable GitHub Actions publishing workflow |
Start with [Deploy to Cloudflare](md/khb-registry/deploy.md), then configure the allowed
[publishers](md/khb-registry/configuration.md) and add a [publishing workflow](md/khb-registry/publishing.md).
---
# Deploy to Cloudflare
The supported installation path is the public
[`KDHelpBook/cf-registry-template`](https://github.com/KDHelpBook/cf-registry-template).
It contains a thin Worker entrypoint, an exactly pinned registry package,
Wrangler configuration, CI, and one instance configuration file.
## One-click deployment
1. Open the template and click **Deploy to Cloudflare**.
2. Choose the GitHub organization or account that will own the instance
repository.
3. Connect the repository to your Cloudflare account.
4. Keep `main` as the production branch.
5. Leave non-production branch deployments and preview URLs disabled.
6. Wait for Cloudflare to deploy the Worker and provision its `DOCSETS` R2
bucket.
7. Open the generated `*.workers.dev` address.
A new registry is intentionally empty. It opens the **Manage docsets** page;
**File → Open docset**, **Open from URL**, and **Help → About** remain available
before the first automated publication.
## Configure the instance
Edit `khb-registry.yml` in the generated repository and run:
```sh
npm ci
npm run validate
npm run build
```
Commit the configuration and merge it to `main`. Cloudflare deploys production
from `main`; pull requests only validate, typecheck, and build the instance, so
unreviewed code never receives access to the production R2 bucket.
## Local development
```sh
npm ci
npm run check
npm run dev
```
Wrangler uses a local R2 database under `.wrangler`. Generated runtime JSON and
viewer assets live under `.khb-registry`; both directories are disposable and
must not be edited by hand.
Use `npm run deploy` for a deliberate manual production deployment. Normal
instance updates should be merged to `main` and deployed by the connected
Cloudflare build.
---
# Registry configuration
Every instance is controlled by one `khb-registry.yml`. The template points
YAML language servers at the JSON Schema shipped by the exact installed package,
so editors can complete fields and report mistakes before CI.
```yaml
# yaml-language-server: $schema=./node_modules/@kdhelpbook/cf-registry/schema/khb-registry.schema.json
schema: 1
site:
order: [product-docs]
folders:
- id: products
title: Products
children:
- collection: product
config:
externalSources: true
pwa: false
prefetch: false
prefetchLocked: false
publishers:
- repository: acme/product
ref: refs/heads/main
environment: null
docsets: [product-docs]
force: false
```
## Top-level fields
| Field | Meaning |
|---|---|
| `schema` | Configuration format version. Version 1 requires the value `1`. |
| `site` | Viewer manifest layout and runtime viewer configuration. |
| `publishers` | Repositories allowed to write named docsets. |
## Site layout
`site.order` lists docset IDs in display order. Published docsets not listed
there are appended in stable storage-listing order.
`site.folders` is emitted into the dynamic `docsets.json`. Folder nodes can
contain nested folders, docset IDs, or collection selectors. The complete
manifest shape is documented in
[Manifest schemas](khb-internals:manifest-schemas).
`site.config` becomes the viewer's `/config.json`:
| Field | Default behaviour |
|---|---|
| `externalSources` | Allows readers to open or import books outside the registry when `true`. |
| `pwa` | Registers the viewer service worker when `true`. |
| `home` | Optional page ID, or `search`, used for a cold start. |
| `prefetch` | Defaults offline prefetch on when `true`; leave `false` for normal registry streaming. |
| `prefetchLocked` | Hides and disables offline prefetch when `true`. |
## Publisher permissions
Each entry grants one GitHub repository access to a non-empty set of docset IDs:
- `repository` is the exact, case-sensitive `owner/name` OIDC claim;
- `ref`, when present, is an exact ref such as `refs/heads/main`;
- `environment`, when present, is the exact GitHub environment claim;
- `docsets` is the complete set of IDs this entry may publish;
- `force` defaults to `false` and should normally stay off.
Entries can overlap. The effective permission is the union of all entries whose
repository, ref, and environment restrictions match the OIDC token.
The Worker verifies the token audience against the origin handling the request,
for example `https://docs.example.com` or
`https://my-registry.example.workers.dev`. Custom domains need no separate
audience setting.
Validate every change before merging:
```sh
npm run validate
```
---
# Publish from GitHub Actions
An allowed content repository calls the reusable workflow from a job. It needs
read access to the source and permission to request a GitHub OIDC token, but no
secrets:
```yaml
name: Publish documentation
on:
push:
branches: [main]
paths: ["docs/**"]
workflow_dispatch:
jobs:
publish:
uses: KDHelpBook/monorepo/.github/workflows/publish-registry.yml@v1
with:
registry-url: https://your-registry.workers.dev
source: docs
permissions:
contents: read
id-token: write
```
The `id` in `docs/docset.toml` must occur in this repository's `docsets`
permission in `khb-registry.yml`. If the permission restricts `ref`, the
workflow must run on that exact ref.
## Workflow inputs
| Input | Required | Purpose |
|---|---:|---|
| `registry-url` | yes | Public registry URL; its origin becomes the OIDC audience. |
| `source` | yes | Directory containing `docset.toml`. |
| `ref` | no | Content ref to check out instead of the caller commit. |
| `version` | no | Exact KHB release tag, or `latest`; an exact workflow tag pins automatically. |
| `allow-extensions` | no | Enables trusted authoring extensions during compilation. |
## Publication sequence
The reusable workflow:
1. checks out the content and a pinned KHB toolchain;
2. compiles the source to `.khb`;
3. reads stable metadata using `khb inspect --json`;
4. requests a short-lived OIDC token whose audience is the registry origin;
5. uploads the immutable files;
6. finalizes the version by atomically replacing `latest.json`;
7. verifies that the published file answers a one-byte Range request with
`206 Partial Content`.
The main R2 object's ETag is recorded as `hash` in the pointer and dynamic
manifest. The viewer uses it to keep streamed HTTP ranges and offline cache
entries tied to the correct content.
Publishing an existing version returns `409` unless the matching publisher has
`force: true` and explicitly requests a forced publication. Prefer incrementing
the docset version.
---
# Update a registry
The template pins `@kdhelpbook/cf-registry` to an exact version. This keeps the
Worker runtime, configuration validator, schema, and bundled viewer on one
tested release.
Dependabot checks weekly and opens a pull request when a newer release is
available. A normal update is:
1. read the KHB release notes;
2. review the package and lockfile change;
3. let the instance CI run `npm ci`, validation, typecheck, and build;
4. merge the pull request to `main`;
5. wait for the production Cloudflare deployment;
6. open `/config.json`, `/docsets.json`, and one published book.
There are no Cloudflare previews for instance pull requests. Preview Workers
would need a separate R2 data model and are deliberately outside version 1;
ordinary static Book PR Previews remain independent.
## Roll back the engine
Revert the Dependabot merge or restore the previous exact package version and
lockfile, then merge to `main`. Published R2 objects are not deleted or migrated
by an engine deployment, so rolling back the Worker does not discard books.
Configuration format changes are versioned by the top-level `schema` field.
Run `npm run validate` with the new package before deployment and do not change
`schema` until the release notes require it.
---
# Registry troubleshooting
## The registry is empty
An empty **Manage docsets** page is the expected first-run state. It confirms
that the Worker, generated configuration, and viewer assets are reachable. Add
a publishing workflow or use **Open docset** locally to inspect a book.
Check the public endpoints directly:
```text
GET /config.json
GET /docsets.json
```
An empty `docsets` array is valid. A published book appears only after finalize
has written its `latest.json` pointer.
## Publishing returns 401
The token is missing, expired, has an invalid signature, or its audience does
not equal the request origin. Pass the public registry address as
`registry-url`; do not add a path or configure a separate audience.
Reverse proxies and custom domains must forward the request to the Worker under
the same public origin used by the workflow.
## Publishing returns 403
The OIDC token is valid but no publisher entry matches it. Compare:
- the exact `owner/name` in `repository`;
- the token's exact Git ref with `ref`;
- the GitHub environment name with `environment`;
- the compiled `docset.toml` ID with `docsets`.
Configuration changes deploy only after they reach the instance's production
branch.
## Publishing returns 409
The version or file already exists. Registry editions are immutable by default.
Increment `version` in `docset.toml`. Use forced publication only for an
explicit recovery policy with `force: true`.
## The viewer reports a fetch or streaming failure
Request the book with a one-byte range:
```sh
curl -i -H "Range: bytes=0-0" \
https://your-registry.workers.dev/d/BOOK_ID/latest/BOOK_ID.khb
```
A healthy response is `206 Partial Content` with a matching `Content-Range`.
Also verify that `docsets.json` points at the expected ID, version, filename,
and optional hash.
## Local checks
Run the same checks as the template CI:
```sh
npm ci
npm run validate
npm run typecheck
npm run build
npx wrangler deploy --dry-run
```
Delete `.khb-registry` when generated files appear stale; the next build
recreates it from the installed package and `khb-registry.yml`.
---