Stable storage contract
From the plugin documentation · View source ↗
Save protection uses a transport-only _flow_edit_revision property in the
API JSON request. FlowEdit removes it in the pre-save hook; it is not Markdown,
frontmatter, or a new on-disk format. Existing pages require no migration.
This contract freezes the supported 0.14 formats as the baseline for 1.0. FlowEdit stores content in the page’s normal Markdown body. There is no editor JSON document or separate block database to migrate.
Page body
- Ordinary paragraphs, headings, lists, quotes, code, links, and tables use
Markdown. Enable
process.markdown: truefor front-end rendering. - A full-width centered image without a caption uses
. Other image layouts use[flow-image]. - Rich content uses
[flow-image],[flow-youtube],[flow-gallery]with[flow-slide],[flow-video],[flow-embed],[flow-columns]with[flow-column], and[flow-block]with optional[flow-item]children. Names, parameters, defaults, and examples are in the shortcode reference. - Aligned paragraphs/headings use
<p style="text-align:center">…</p>or the equivalent heading tag. Inline formatting inside that HTML uses HTML tags (<strong>,<em>,<s>,<code>,<a>), so Grav renders it consistently. - Columns contain 2–4 column bodies, including empty ones. Text, images, galleries, video, supported embeds, and structured blocks render inside them. Hand-authored raw iframes/scripts do not gain permission from being in a column.
- Media paths refer to existing page/site files or validated URLs. Crops create separate files; changing the crop naming algorithm does not invalidate older stored media paths or delete old derivatives.
Switching editor preferences does not convert the page. Keep FlowEdit and
Shortcode Core enabled wherever [flow-*] content still needs to render.
Custom-block schema
Definitions remain in plugins.flow-edit.custom_blocks. Each definition has
id, enabled, label, keywords, icon, category, template, and fields.
The stable field schema consists of name, type, label, default,
options, and content; supported types are text, textarea, url,
select, toggle, list, color, number, date, and icon.
- The block
idbecomes[flow-block type="id"]; non-content field names become attributes. Treat these names as persisted identifiers. - One content field maps to the body. List content uses up to 32
[flow-item title="…"]body[/flow-item]children. - Attribute values are strings; toggles use
"1"/"0". Renderer validation and fallback behavior follow the field reference. - Rich-text body fields are sanitized prose, not arbitrary nested media or layout containers. A whole custom block can be placed inside a column.
- Disabling a custom definition hides it from authors and suppresses its front-end output. Renaming a definition/field requires migrating its stored occurrences; changing a label does not.
Additive visual definitions
Definitions may include archived (default false) and layout, containing
version: 1 and ordered nodes. Fields may include retired (default false);
image is an additional string-valued field type. Archive hides insertion and
retirement hides an input; neither discards values or disables rendering.
Historical enabled behavior remains unchanged.
Image values remain shortcode string attributes. The visual renderer resolves page-media filenames using the containing page; site-root paths remain usable across pages. Image metadata and visual layout trees are not added to page bodies.
A layout node has a kind, optional value/url/alt bindings (each either
{field: name} or {literal: text}), validated style, and container children.
Headings support level (2–6). Definitions contain no executable markup or
generated template files. See the builder guide.
The builder freezes persisted IDs, field names, types and body mappings. Duplicate a type for structural changes. Hand-authored migrations remain developer-managed.
The same configuration API validates collection saves, including normal settings saves: existing types cannot be removed, saved field names/types/body mappings cannot change, and saved types cannot switch between visual and Twig modes. Archive or retire them instead. Configuration saves preserve unknown definition and field metadata and unrelated plugin settings. Defaults and layout changes do not rewrite stored shortcode attributes or Markdown bodies.
Downgrading: existing pages need no conversion. After adopting visual types, back up configuration and provide equivalent Twig templates or replace their instances before downgrading. Older versions do not understand visual layouts, image fields, retirement or archive state. Do not downgrade and resave definitions blindly.
Theme contract
The default template is flow-blocks/{id}.html.twig; configured alternatives
must match flow-blocks/{name}.html.twig. A theme overrides it at:
user/themes/<theme>/templates/flow-blocks/<name>.html.twig
Column layout uses the same mechanism with columns.html.twig. The structured
block template receives type, label, params, content, items, and uid.
params and item titles are untrusted strings; keep Twig escaping enabled.
Only sanitized content and item bodies are marked safe. Templates are trusted
site code. See the Twig contract.
Visual types use their matching Twig template when one exists; otherwise the
shared BlockLayout renderer emits fixed .flow-layout-* markup. The active
theme’s styles still apply. Unsaved previews use the same rendering choice in
a transient themed page. Optional page context and sample values never become
part of the definition or saved page. See preview behavior.
Legacy HTML policy
Legacy <!--flow-block:Label-->…<!--/flow-block--> blocks remain readable
as read-only visual previews with a warning. The preview uses an inert
HTML allow-list; its sanitized display is not written over the original body.
The front end processes the original HTML under Grav’s HTML/security settings.
This policy is an editor behavior, not an additional server security boundary.
New insertions use structured shortcodes. There is no automatic conversion: freeform HTML cannot reliably be mapped to named fields without author review. To replace a legacy block:
- Back up the page and inspect the original in Markdown mode.
- Define a structured block with the needed fields and a visual layout or trusted Twig template, or select an existing shipped block.
- Insert it and copy the intended content into its fields.
- Preview, save, and reopen; remove the legacy block after checking the result.
Advanced authors can explicitly edit the original in Markdown mode. Visual edit/duplicate/snippet controls are unavailable on legacy cards.
Compatibility and migration rules
Changes must continue reading existing supported documents. Serializer output may normalize whitespace/attribute ordering; byte-for-byte preservation of every Markdown spelling is not promised. Unsupported Markdown/HTML constructs are outside this contract and should be checked in preview before saving.
Any incompatible shortcode, field, or template-contract change requires a CHANGELOG migration note before release, a documented conversion/rollback path, and fixtures proving old content is preserved or explicitly converted. Never silently rename stored identifiers or reinterpret their values. Additive features must have defaults compatible with existing documents. Security fixes that restrict formerly accepted unsafe content must also document the impact.
The stability changes following 0.14.0 require no page migration. Existing crop files and legacy markers remain valid; columns gain their intended nested output.