Skip to main content

Rich-text editor: keyboard shortcuts & slash commands

The content editor's rich_text blocks use TipTap (ProseMirror) with the StarterKit extensions, plus links and tables, wired up in assets/js/rich_text.js. This is the reference for the formatting shortcuts and the slash-command menu.

Everything the editor can produce is constrained server-side by KilnCMS.HTMLSanitizer.RichText — the toolbar, shortcuts, and slash commands all emit only tags on that allowlist.

Keyboard shortcuts

⌘ is the Command key on macOS; use Ctrl on Windows/Linux. ⇧ is Shift, ⌥ is Option/Alt.

ActionmacOSWindows / Linux
Bold⌘BCtrl B
Italic⌘ICtrl I
Strikethrough⌘⇧SCtrl ⇧ S
Inline code⌘ECtrl E
Heading 1⌘⌥1Ctrl Alt 1
Heading 2⌘⌥2Ctrl Alt 2
Heading 3⌘⌥3Ctrl Alt 3
Paragraph (clear heading)⌘⌥0Ctrl Alt 0
Bullet list⌘⇧8Ctrl ⇧ 8
Numbered list⌘⇧7Ctrl ⇧ 7
Blockquote⌘⇧BCtrl ⇧ B
Code block⌘⌥CCtrl Alt C
Link⌘KCtrl K
Hard line break⇧↵⇧ ↵
Undo⌘ZCtrl Z
Redo⌘⇧ZCtrl ⇧ Z

These are the StarterKit defaults (plus ⌘K, which is ours); the toolbar above each editor exposes the same commands as buttons and highlights the ones active at the cursor. ⌘K opens the editor search palette everywhere except inside a rich-text block, where it makes a link instead.

⌘K — or the toolbar's Link button — opens a small popover at the cursor:

  • with text selected, it links that text;
  • with the caret inside an existing link, it prefills that link's URL, edits the whole link (not the fragment under the caret), and offers Remove;
  • with nothing selected, it inserts the URL as its own link text.

↵ applies, Esc dismisses. Pasting a URL over selected text links the selection, keeping the text as the label — that works for internal paths (/pricing) as well as full URLs. Typing a bare URL does not silently become a link.

Accepted URLs mirror KilnCMS.HTMLSanitizer.safe_href/1: http(s)://, mailto:, same-origin paths starting with /, and in-page #fragments. Anything else — javascript:, data:, ftp:, protocol-relative //host, paths containing .. — is refused in the popover with a message, because the server would blank it on save and the link would silently disappear on the next reload.

That one function is the whole policy: the editor, the block cast, the Portable Text renderer and the legacy-HTML sanitizer all defer to it. A #fragment is carried rather than resolved, though — Kiln renders headings without ids, so a bare #section has nothing to land on in prose it generated.

Links are stored as Portable Text markDefs, which is what Kiln.Advisory.Checks.LinkText and the broken-link checks read.

Slash commands

Type / at the start of an empty paragraph to open the command menu. Keep typing to filter (e.g. /h2, /quote, /code), then:

  • ↑ / ↓ — move the selection
  • ↵ or Tab — apply the highlighted command
  • Esc — dismiss the menu

Selecting a command removes the typed /query and transforms the current block.

CommandTransforms the block intoMatches (keywords)
TextPlain paragraphparagraph, text, body
Heading 1<h1>h1, title, big
Heading 2<h2>h2, subtitle
Heading 3<h3>h3
Bullet list<ul>ul, unordered, bullet
Numbered list<ol>ol, ordered, numbered
Quote<blockquote>blockquote, citation
Code block<pre><code>code, pre, snippet
Divider<hr>hr, horizontal, rule, separator, line

The slash menu only triggers when / is the first character of a paragraph, so typing a slash mid-sentence (e.g. "and/or") never opens it.

Extending

To add a command, edit the TOOLBAR and/or SLASH_COMMANDS arrays in assets/js/rich_text.js. If a new command can emit a tag that isn't already allowed, add that tag to KilnCMS.HTMLSanitizer.RichText and cover it in test/kiln_cms/html_sanitizer_test.exs — otherwise the server will strip it on save.

Pictures

Paste an image from the clipboard into a rich-text block, or drop an image file on it, and it uploads into the media library and lands as an image block right after that block (the rich-text schema has no inline image, so the block editor's own image block is where a picture belongs). A placeholder with the file name and progress stands in the block list while the file travels; a file the browser refuses — over the image size cap, or not an image — stays there with the reason and a Dismiss. Several files pasted at once keep their order. The upload runs through the same pipeline as the media library's own uploads (KilnCMS.Media.Ingest), under your own permissions.