Something live of the app's own in the document (type "embed"): a
canvas, a grid, a chart. The document keeps what the app needs to find
it, and the app renders it, live, in the editor (Kotoba.Embeds). The
JSON keys are kind, ref and title.
kind- what it is, in the app's words, for example"canvas".ref- which one, for example the canvas's id, as a string.title- its title, for the text and the Markdown of the document, and for a page that renders the document without the app.
Rendering
Kotoba does not know what an embed is: the app says what it renders as,
with the :embed option of Kotoba.Renderer (and of
Kotoba.Components.kotoba_content/1), or for every render (the HTML
cached when content is cast too) with the config:
config :kotoba, embed: &MyApp.Embeds.render/2The function gets the embed and the format (:html, :markdown or
:text) and returns what takes its place: safe HTML for :html (a
~H template, {:safe, iodata}; a string is escaped), Markdown or text
as a string, or nil for the default. For example, a canvas in Markdown
as a Mermaid diagram (see Hakushi.Kit.to_markdown/2):
def render(%{kind: "canvas", ref: id}, :markdown),
do: Diagrams.get!(id) |> then(&Hakushi.Kit.to_markdown(&1.kits, &1.graph))
def render(_embed, _format), do: nilBy default, an embed is its title: a div.kotoba-embed with its kind
and ref in HTML, which a page can fill with the app's content, and the
title in text and Markdown. See the Embeds guide.
In Markdown, and back
In Markdown, an embed is what the app renders (or its title) between two comments that say what it is, which Markdown viewers leave out:
<!-- kotoba:embed {"kind":"grid","ref":"7","title":"Open tasks"} -->
| Task | Due |
| --- | --- |
| Write the guide | 2026-11-02 |
<!-- /kotoba:embed -->Kotoba.Content.from_markdown/2 reads it back as the embed, so a
document exported to Markdown comes back with its embeds, of any kind.