# 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 | `![alt](assets/pic.svg)` | [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 (`![…](https://…)`) — 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 ![How a docset is built](assets/khb-pipeline.svg) Download the [quick-reference card](assets/quick-reference.txt). ``` ```md ![How a docset is built](assets/khb-pipeline.svg) 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 ![How a docset is built](assets/khb-pipeline.svg) ``` ```md ![How a docset is built](assets/khb-pipeline.svg) ``` ~~~ > [!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 ![Architecture](assets/khb-pipeline.svg "How a docset is built") ``` ```md ![Architecture](assets/khb-pipeline.svg "How a docset is built") ``` ~~~ ## 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 ![Tap the card](assets/tap-screen.png#w=300) ![Result, half-width](assets/result.png#w=50%) ![Tall screenshot](assets/settings.png#h=50vh) ![Thumbnail](assets/full-flow.png#w=300&h=200) ``` ```md ![Tap the card](assets/tap-screen.png#w=300) ![Result, half-width](assets/result.png#w=50%) ![Tall screenshot](assets/settings.png#h=50vh) ![Thumbnail](assets/full-flow.png#w=300&h=200) ``` ~~~ 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 ![The authoring guide open in the viewer](assets/viewer-overview.png) ![Syntax-highlighted code, with copy buttons and tabbed groups](assets/viewer-code.png) ![Colour-coded callouts and blockquotes](assets/viewer-callouts.png) ~~~ Each `![alt](src)` 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 ![1. Waiting for the card](assets/step-wait.png) ![2. Scanning](assets/step-scan.png) ![3. Write confirmed](assets/step-done.png) ~~~ ### 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 ![1. Waiting for the card](assets/step-wait.png) Tap the card — its **UID** appears. ![2. Scanning](assets/step-scan.png) Exactly **one** tag may answer. ~~~ Live, with a description under each tile: ~~~gallery ![The table of contents](assets/viewer-overview.png) All three volumes merge into one tree; the **On this page** box tracks headings. ![Syntax highlighting](assets/viewer-code.png) Baked in at **compile time** — the viewer ships no highlighter. ![Callouts](assets/viewer-callouts.png) `> [!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 ![Idle](assets/a.png) ![Searching](assets/b.png) ![Paired](assets/c.png) ~~~ ``` 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 ![Overview](assets/viewer-overview.png) ![Code blocks](assets/viewer-code.png) ![Callouts](assets/viewer-callouts.png) ![Diagrams](assets/viewer-diagram.png) ~~~ ### 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 `![preview](assets/ext/label/…/out.svg)` — 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![preview](assets/ext/label/intro/0/out.svg)\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"" f"") assets.append({"file": file}) rows.append(f"| {name} | ![{name}]({req['asset_prefix']}{file}) | `{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`. ---