AstroCraft — home
AstroCraft
Buy a licence
All documentation

The document editor

Nine block types, seven markdown input rules, three menus and one keyboard — and the byte-fidelity rule that keeps your diff down to the edit you made.

Last updated

The document canvas is the bottom half of the entry editor. It is a block editor: you type, it becomes structure, and what it writes back is markdown a person can read.

The rule that shapes everything

Untouched bytes are re-emitted, never regenerated. Every block keeps the source span it was parsed from, and serializing writes those original bytes back for anything you did not edit.

This matters because remark’s round trip is provably not byte-identical — it rewrites - one to * one, --- to ***, and reflows GFM table delimiter rows. Without span preservation, opening an entry and saving it unchanged would produce a diff, and every real edit would arrive buried in cosmetic churn. The developer who reads your commit sees your edit and nothing else.

The corollary: a block the parser does not recognise keeps its own bytes verbatim. Nothing is dropped and nothing throws, so a component, a directive or a shortcode your markdown pipeline understands survives a round trip through the editor untouched.

The nine block types

Text, Heading 2, Heading 3, Bulleted list, Numbered list, Quote, Code block, Divider, Image.

Headings are levels 2 to 4 only. The <h1> is your title frontmatter field, not a block — which is the right model for a content collection and also why # and ##### fall through to the unknown-block path rather than being coerced.

Markdown as you type

Seven prefixes transform the block you are in the moment you type them:

Type Get
## Heading 2
### Heading 3
- or * Bulleted list
1. Numbered list
> Quote
``` Code block
--- Divider

The divider rule only fires when there is no text to lose, since a divider keeps none.

Each rule emits the same command the menu would, so nothing about typing a prefix is a special path. Undo lands exactly on the transform rather than unwinding the whole typing run it arrived in — though getting back to the literal ## characters takes a second press, because history is snapshots rather than inverse operations. Nothing is eaten; the characters come back.

The three menus

The slash menu — type / and filter the nine block types. It stays open while you type because the caret has to stay in the block.

The bubble — select text and it appears over the words it is about to format: bold, italic, code, link, and a turn-into menu. Link reveals an input inside the bubble rather than calling window.prompt, which would block the page, cannot be styled, and cannot show you the words you are linking.

Turn-into — the block-type dropdown in the toolbar, and the bubble’s fifth tool. Both are rendered from one component, so the nine rows cannot fall out of order with each other.

Keyboard

Key Does
⌘B / Ctrl+B Bold
⌘I / Ctrl+I Italic
⌘E / Ctrl+E Inline code
⌘K / Ctrl+K Link
⌘Z / ⌘⇧Z Undo / redo
⌘⇧↑ / ⌘⇧↓ Move the current block up or down
Tab / ⇧Tab Indent / outdent, inside a list
Enter Split the block
Backspace at position 0 Merge into the previous block

Both Cmd and Ctrl are accepted on every platform, so a Mac with an external PC keyboard works either way.

Every shortcut is one row in a single table shared with the toolbar buttons, so ⌘B and the Bold button are the same entry rather than two paths that happen to agree. An inline tool needs a selection, not just a caret, and is unavailable inside a code block, where a mark cannot mean anything.

Tab indents only inside a list. Outdent turns a nested list item into a sibling item, not into a paragraph — that is the behaviour lists actually want.

Reordering

Drag a block by its gutter handle, or use ⌘⇧↑ / ⌘⇧↓. The handle matters: a permanently draggable block makes every text selection inside it start a drag instead. Drag and keyboard are the same underlying move command, so neither is the only route — which is also why touch works, since HTML5 drag-and-drop does not fire on touch at all.

Undo

Snapshots, on a 500 ms coalescing window per block, capped at 200 states with the oldest dropped. It is the editor’s own stack rather than the browser’s contenteditable history, which is what makes undo behave the same way after a menu pick, a drag and a typed prefix.

Paste

The clipboard’s text/plain is parsed as markdown; text/html is ignored. Every source that offers HTML offers plain text beside it, so nothing is unpasteable — what is lost is formatting that markdown cannot spell anyway.

Pasting into a fenced code block takes the text as text. Running a code sample through the markdown parser would turn its own # and - lines into headings and lists.

Several blocks pasted inside a list item or a quote are flattened rather than tearing the container open.

Images in the body

The Image block opens the image library picker before it inserts anything, so a cancelled pick leaves the block exactly as it was and there is never a broken picture to clean up.

The path written into your markdown is relative to the entry, with the right number of ../ computed from the slug — a nested entry at 2026/post gets ../../../. What the canvas renders is a request-time asset URL; the model keeps the markdown form, so what is saved is what you would have typed.

Using an image asks for a description or an explicit decorative claim. A save with undescribed images warns and counts them; it never blocks.

Saving

Save in the top bar. There is no ⌘S on this screen — that shortcut belongs to the page composer.

Autosave banks your work as a draft two seconds after your last edit. A draft is a save payload that has not been committed: it lives on the server, keyed to your account and the entry, so it follows you to another browser rather than sitting in one machine’s local storage. A landed commit discards it.

Known limits

  • Selection across blocks is block-granular. There is no cross-block text range in this model, so selecting from the middle of one paragraph into the middle of the next selects both whole blocks.
  • A structural command re-renders the whole block list; typing re-renders nothing. On a very long document that shows up as a pause on Enter and on menu picks.
  • The IME guard is untested against a real input method. Nothing reads or rewrites a block between compositionstart and compositionend, which is the correct design, but no one has typed Japanese through it yet.

Troubleshooting

A markdown prefix does nothing. Typing > in a contenteditable writes a non-breaking space rather than a normal one, which is why the input rules normalise it at the seam. If a rule genuinely does not fire, it is a bug worth reporting with the exact keystrokes.

A block will not accept a bold shortcut. You have a caret but no selection, or you are inside a code block. Both are deliberate.

Undo took two presses to reach my text. After a markdown transform, yes — see Markdown as you type above.

A hoisted script inside a section stopped working after an edit. That is the composer’s canvas, not this one. See Composing pages.