flowedit.
FLOWEDIT DOCUMENTATION

Creating custom FlowEdit blocks

From the plugin documentation · View source ↗

For creating blocks without template code, use the visual block builder. This guide covers the existing developer-managed Twig path; both kinds appear in the same block library.

This guide is for theme and plugin developers who want to add structured content blocks that authors insert via FlowEdit’s / slash menu.

Twig blocks are config + fields + Twig. Authors fill a form in Admin2; values are stored in page Markdown as a [flow-block] shortcode; the front end renders a Twig template (overridable by the active theme).

Plugin / admin config (id, fields, template)
        ↓
GET /api/v1/flow-edit/settings  →  FlowEdit slash menu + edit modal
        ↓
page.md  →  [flow-block type="…" …]…[/flow-block]
        ↓
Shortcode Core → FlowBlockShortcode → Twig template

Quick start (new block type)

1. Register the block in config

Add an entry to custom_blocks in user/config/plugins/flow-edit.yaml. The shipped examples are in flow-edit.yaml; avoid putting site customizations in plugin defaults, which an update can replace. Preserve the rest of the collection when editing it. The old nested custom-block settings form has been replaced by a link to FlowEdit → Blocks. New types created in that library are visual; register new Twig definitions in configuration.

Example — a simple “promo” banner:

custom_blocks:
  - id: promo
    enabled: true
    label: Promo banner
    keywords: promo, banner, ad
    template: flow-blocks/promo.html.twig
    fields:
      - name: headline
        type: text
        label: Headline
        default: Special offer
      - name: href
        type: url
        label: Link URL
        default: /
      - name: body
        type: textarea
        label: Body
        default: ''
        content: true

2. Add a Twig template

Ship a default in the FlowEdit plugin (or another plugin that adds Twig paths), or only in your theme:

Plugin default (optional):

user/plugins/flow-edit/templates/flow-blocks/promo.html.twig

Theme override / theme-only (recommended for branded markup):

user/themes/your-theme/templates/flow-blocks/promo.html.twig

{# templates/flow-blocks/promo.html.twig #}
<aside class="flow-block flow-block--promo">
  {% if params.headline %}
    <h2 class="flow-block__headline">{{ params.headline }}</h2>
  {% endif %}
  {% if content %}
    <div class="flow-block__body">{{ content }}</div>
  {% endif %}
  {% if params.href %}
    <p><a class="flow-block__link" href="{{ params.href }}">Learn more</a></p>
  {% endif %}
</aside>

Grav resolves theme templates before plugin templates, so the theme file wins when both exist. FlowEdit registers user/plugins/flow-edit/templates via onTwigTemplatePaths.

3. Use it in FlowEdit

  1. Ensure the author’s account uses Content Editor: FlowEdit.
  2. Clear Grav cache if config was edited on disk.
  3. In the page editor, type / and choose Promo banner (or search promo).
  4. Fill the form and insert. The page body will contain something like:
[flow-block type="promo" headline="Special offer" href="/deals"]
Limited time — 20% off.
[/flow-block]

Block definition reference

Key Required Description
id yes Stable slug: ^[a-z][a-z0-9_-]{0,63}$. Used as shortcode type and default template basename. Saved IDs are immutable through configuration management.
enabled no (default true) false → hidden from slash menu and front-end Twig output is empty.
archived no (default false) Hides new insertion; existing instances remain editable and render.
label yes Slash menu display name.
keywords no Comma-separated slash search aliases.
icon no Lucide kebab-case name (e.g. lightbulb, max 48 chars) or emoji/glyph shown in front of the block name in the slash menu and on the block card. Unknown Lucide-shaped names fall back to text; legacy emoji still works.
category no Slash-menu grouping label (e.g. Content, Marketing). Blocks are sorted by category, then label; empty → default group.
template no Twig path matching ^flow-blocks/[a-z0-9_-]+\.html\.twig$. Empty or invalid → flow-blocks/{id}.html.twig.
fields yes (non-empty) List of field objects (see below). Entries without valid id/fields are ignored by the settings API.
layout no Versioned visual layout. Omit for Twig types. A matching Twig template takes precedence even when layout metadata exists.

Field reference

Key Required Description
name yes Attribute name: ^[a-zA-Z_][a-zA-Z0-9_]*$. Reserved: type (shortcode type).
type no text (default), textarea, url, select, toggle, list, color, number, date, icon, image.
label yes Label in the insert/edit modal.
default no Initial value when inserting. For list, leave blank to seed two placeholder items.
options for select Comma-separated values, e.g. info,warning,success.
content no If true, this field is the shortcode body (between tags), not an attribute. Only one content field per block; configuration saves reject extras. list fields always occupy the body.
retired no (default false) Hides the authoring input while preserving its definition, existing values, and rendering bindings.

Content vs attributes

  • Non-content fields → shortcode attributes: title="…" tone="…".
  • Content field → inner text between [flow-block …] and [/flow-block]. Prefer textarea for multi-line body copy.
  • list content → nested [flow-item title="…"]…[/flow-item] shortcodes (max 32). Each item has a title attribute and a body.

Select / toggle / url / list

  • select: invalid stored values fall back to the first option or default.
  • toggle: stored as 1 / 0 in attributes; Twig sees the string "1" or "0".
  • url: must be absolute http(s), site-root /…, or #…; otherwise the default (or #) is used.
  • list: editor shows a title + body repeater; storage uses [flow-item].

The URL rule above is the existing Twig field contract. Visual layout URL bindings additionally support safe relative paths and mailto: destinations; protocol-relative URLs, traversal, and executable schemes are rejected.

Color / number / date / icon

  • color: Admin2-style Huey picker with saturation, hue, hex input, swatches, and clear; stored as lowercase #rrggbb. Empty values stay empty. Alpha colors are unsupported.
  • number: numeric input (integers and decimals); non-numeric input is stored as an empty attribute.
  • date: native date picker; stored as ISO yyyy-mm-dd.
  • icon: Lucide kebab-case name (preferred, max 48 chars) or short emoji/glyph (max 4 code points), with a curated Lucide quick-pick row; stored as a plain attribute. Emoji values remain valid for existing content.

All four store plain shortcode attributes, so Twig templates need no extra handling — {{ params.accent }} works like any other field.

Image

image uses Choose image to open FlowEdit’s existing media dialog. The attribute remains a string: a page-media filename, site-root path, or accepted image URL. It does not embed image metadata in the shortcode. Add a separate text field for alt text. The standard visual renderer resolves page filenames against the containing page; custom Twig templates receive the string in params and must resolve page-local media when needed. General-media paths are useful for defaults shared by many pages. Uploads and stock downloads persist files even if the enclosing block dialog is later cancelled.

The Icon selector in block settings controls the type’s library/slash icon. It is distinct from an icon field, which stores an icon value for each instance.

Example list body:

[flow-block type="accordion" multiple="0" open_first="1"]
[flow-item title="What is FlowEdit?"]
A modern slash editor for Grav.
[/flow-item]
[flow-item title="Does it store Markdown?"]
Yes.
[/flow-item]
[/flow-block]

Twig contract

The shortcode renderer calls:

$this->twig->processTemplate($template, [
    'type' => $blockId,   // e.g. "promo"
    'label' => $label,    // slash label from config
    'params' => $params,  // map of non-content field name → string
    'content' => $content, // sanitized Twig\Markup body; empty for list blocks
    'items' => $items,    // list of ['title' => string, 'body' => Twig\Markup]
    'uid' => $uid,        // stable short id for radio/details name attributes
]);

Rules:

  • params and item titles are ordinary strings and remain auto-escaped.
  • content and each item.body are sanitized server-side rich text, then marked safe for Twig. Render them with normal interpolation ({{ content }} and {{ item.body }}); do not add |raw.
  • Keep attribute values quoted in HTML: href="{{ params.href }}".
  • Prefer a stable BEM-like class prefix (defaults use flow-block / flow-block--{type}) so themes can style consistently.
  • For a Twig-only definition, if the template is missing or throws, the shortcode renders nothing and logs the block type, template, and exception.
  • Accordion and tabs load css/flow-blocks.css via Shortcode Core assets when rendered.

Shipped examples:

Theme-only styling

If the plugin already ships markup you like, override CSS in the theme only:

.article-body .flow-block--callout {
  border-left: 4px solid currentColor;
  padding: 0.75rem 1rem;
}

To change structure (not just CSS), copy the Twig file into the theme at the same path and edit it.

For visual definitions, a matching template also overrides the generated layout. The builder’s themed preview uses that override, so changing layout elements will not change the override’s markup. Use Preview page route to test a particular page template/header/media context; test JavaScript behavior on the actual page because the builder’s iframe disables scripts and navigation.

Enable / disable behavior

Kind Where Slash menu Front end
Custom block enabled: false Custom blocks list Hidden [flow-block] for that type renders empty
Custom block archived: true Blocks library Hidden Existing instances keep rendering and can be edited
Built-in command off Built-in slash commands Hidden Unchanged (Markdown still renders)

Authors can still leave disabled custom blocks in saved Markdown; they simply stop displaying until re-enabled.

Settings API (editor)

FlowEdit loads definitions with:

GET /api/v1/flow-edit/settings (requires api.pages.write)

Response data shape (simplified):

{
  "custom_blocks": [
    {
      "id": "promo",
      "label": "Promo banner",
      "keywords": "promo, banner, ad",
      "icon": "rocket",
      "category": "Marketing",
      "template": "flow-blocks/promo.html.twig",
      "fields": [
        {
          "name": "headline",
          "type": "text",
          "label": "Headline",
          "default": "Special offer",
          "options": [],
          "content": false
        }
      ]
    }
  ],
  "builtin_blocks": {
    "paragraph": true,
    "h1": true,
    "hr": true
  }
}

Only enabled custom blocks are returned, including archived types so existing instances can still be edited. The editor excludes archived types from insertion and hides retired fields while retaining their values. The payload includes the archive state and field retirement metadata. It builds the slash items and modal forms without an editor rebuild. Definitions refresh on returning focus/visibility to the editor and on flow-edit:blocks-updated; this does not replace the document or reset undo history.

For definition management, use the existing config API and the dedicated preview endpoint described in the builder integration reference.

Security notes

  • Attribute keys on [flow-block] must match the allow-list for that type (type + configured field names). Unknown keys → no output.
  • Treat Twig templates as trusted code and params / item titles as untrusted author input (rely on Twig escaping).
  • Content bodies allow only common prose elements (paragraphs, headings, lists, emphasis, code, quotes, tables, and safe links). Scripts, embedded media, forms, event attributes, unsafe styles, and unsafe URL schemes are removed. Only text alignment on paragraphs/headings is retained from author styles.
  • Do not apply |raw to author values. FlowEdit itself marks only sanitized body markup as safe.

Checklist

  • id is a stable lowercase slug
  • At least one field is defined
  • At most one field has content: true
  • Twig exists at template (plugin and/or theme)
  • Theme override path matches exactly if branding the markup
  • Block tested via / insert, save, and front-end view
  • npm test and scripts/verify-rendering.php pass
  • Disabled-state checked (slash hidden + front end empty)
Path Role
blueprints.yaml Settings, library link, and persistence blueprint for definitions
flow-edit.yaml Default block definitions
classes/BlockDefinitions.php Config normalization
classes/BlockLayout.php Visual layout validation and rendering
classes/BlockThemePreview.php Transient page rendered with the active theme
classes/shortcodes/FlowBlockShortcode.php Front-end renderer
classes/FlowEditApiController.php Settings API
src/field/custom-block.ts Editor node + Markdown round-trip
src/field/settings.ts Client settings parse

The storage contract freezes field identifiers, shortcode structure, and theme paths. Renaming persisted fields requires a migration note.