flowedit.
FLOWEDIT DOCUMENTATION

Development

From the plugin documentation · View source ↗

How to build, test, and verify changes to the FlowEdit plugin itself. For adding new block types, see Creating custom blocks.

The visual manager lives in src/builder/. npm run build builds both the field and management bundles. The shared color control is a Svelte custom element built with Admin2’s Huey primitives; it registers once even when both bundles load. Both admin-next/fields/flow-edit.js and admin-next/pages/flow-edit.js must ship with source changes. Run php scripts/verify-builder.php for layout validation, preview/public parity, permissions and configuration-filter preservation. Builder tests are included in npm test. The standard npm run dev command watches only the field bundle; use npx vite build --config vite.builder.config.ts --watch for the manager.

Toolchain

  • Frontend: TypeScript + Vite; TipTap 2 for the page editor, Svelte 5/Huey for the shared color custom element. The manager otherwise uses DOM controls. Node.js is required for development only.
  • Backend: PHP (Grav plugin conventions), no Composer install needed — the plugin autoloads classes/ itself via PSR-4 (Grav\Plugin\FlowEdit\).
  • Version/dependency constraints are authoritative in blueprints.yaml, not package.json.

Setup

cd user/plugins/flow-edit
npm install

Commands

npm run dev         # vite build --watch
npm test            # vitest run (roundtrip, media, crop, slash-recents, outline, …)
npm run typecheck   # tsc --noEmit
npm run build       # builds admin-next/{fields,pages}/flow-edit.js

Suggested order before committing field changes:

npm run typecheck && npm test && npm run build

The golden rule: include both built bundles

admin-next/fields/flow-edit.js and admin-next/pages/flow-edit.js are tracked so Grav installs need no Node. After changes under src/field/ or src/builder/:

  1. run npm run build,
  2. include both rebuilt bundles with the source change when committing.

Editing the bundle by hand or leaving it stale is the most common mistake. Vite writes only flow-edit.js / flow-edit.css into admin-next/fields (emptyOutDir: false, iife, minified, no sourcemap, single entry src/field/main.ts, inlineDynamicImports). The second Vite config builds the manager from src/builder/main.ts. Both configs compile color-picker.svelte as a custom element with injected styles. Its guarded registration in color-control.ts prevents duplicate registration when both bundles are loaded. Preserve the browser Svelte resolution used by field tests.

PHP verification scripts

php scripts/verify-list-blocks.php   # standalone — BlockDefinitions vs flow-edit.yaml ids
php scripts/verify-rendering.php     # end-to-end shortcode rendering regression
php scripts/verify-api.php           # crop identity, secret masking, bounded downloads
php scripts/verify-i18n.php          # en/de parity and referenced translation keys
php scripts/verify-content-save-guard.php  # stale Markdown baseline rejection and transport metadata removal
php scripts/verify-builder.php       # layouts, themed previews, config lifecycle, permissions

verify-rendering.php needs a full Grav install: it chdir’s to the Grav root (three levels above the plugin) and boots Grav in CLI mode, so it only runs when this repo lives at user/plugins/flow-edit.

Architecture

src/field/main.ts            Field web component (Admin2 loads admin-next/fields/flow-edit.js)
src/field/{image,youtube,video,gallery,table,list-items,…}.ts
                             TipTap nodes + Markdown serialization per feature
src/field/media.ts           Page/site media pickers, upload, paste URL
src/field/crop-overlay.ts    Draw-to-crop UI → POST /flow-edit/crop
src/field/custom-block.ts    [flow-block] node + modal forms (driven by /flow-edit/settings)
src/field/content-save-guard.ts   Scoped Admin2 save transport + draft conflict recovery
src/field/roundtrip.test.ts  preprocess/postprocess Markdown round-trip guarantees
src/builder/main.ts          Library/designer custom element and Admin2 page events
src/builder/model.ts         Presets, field bindings, layout model, validation
src/builder/api.ts           Config/preview requests with environment and ETag headers
src/field/color-picker.svelte      Shared Huey color picker
src/field/ui/modals/standalone-image-picker.ts  Existing media dialog adapted to return a string
flow-edit.php                Plugin wiring: editor registration, shortcode handlers,
                             Twig paths, API routes
classes/FlowEditApiController.php   GET /flow-edit/settings, POST /flow-edit/crop
classes/BlockDefinitions.php        Config normalization/validation for custom blocks
classes/BlockLayout.php             Visual schema validation and shared fixed renderer
classes/BlockThemePreview.php       Active-theme transient page rendering
classes/BlockBuilderController.php  Authenticated unsaved preview endpoint
classes/ContentSaveGuard.php        Validates content baseline before API writes
classes/BuiltinBlocks.php           Normalization of builtin_blocks toggles
classes/shortcodes/*.php     Server-side [flow-*] renderers (the security boundary)
templates/flow-blocks/*.twig Default structured-block markup (theme-overridable)
css/                         Front-end stylesheets (gallery/video/block, auto-registered)
languages/                   en + de Admin form strings

Data flow

  1. Admin2 loads the field bundle and sets the page’s Markdown as the field value.
  2. The field fetches GET /api/v1/flow-edit/settings (api.pages.write) to build the slash menu and custom-block forms.
  3. Edits serialize back to Markdown (shortcodes for rich media) on every change. Admin2 saves the file through the API. FlowEdit’s onApiBeforePageUpdate hook rejects a stale content baseline and strips its transport-only metadata before the write; the draft remains available for recovery in the editor.
  4. The front end renders the saved shortcodes via Shortcode Core.

Block management uses its own component page, grav-flow-edit--page, and the host’s toolbar page-state / page-action events. The library uses the config API; its onAdminSave event validates definitions before persistence and onApiBlueprintResolved preserves layout/lifecycle metadata through filtering. The field refreshes definitions on focus/visibility and flow-edit:blocks-updated without replacing content or adding an undo step. Keep these two save paths separate: config ETags protect definitions; the content guard protects Markdown.

API

Route Permission Purpose
GET /config/plugins/flow-edit api.config.read Load the block collection and its configuration ETag.
PATCH /config/plugins/flow-edit api.config.write Save only custom_blocks, with If-Match and X-Config-Environment; normal API config hooks validate and invalidate.
POST /flow-edit/blocks/preview api.config.write; optional page needs api.pages.read + page read access Unsaved visual/Twig definition plus samples; returns themed document, block HTML, CSS and theme name. No page writes.
GET /flow-edit/settings api.pages.write Enabled custom blocks, built-in toggles, allow_svg
GET /flow-edit/pages api.pages.write Page search for internal-link suggestions.
GET /flow-edit/routes api.pages.write Route index for Review’s internal-link checks.
GET /flow-edit/stock api.pages.write Search the configured stock photo provider.
POST /flow-edit/stock/download api.media.write + page update right Download a selected stock photo into page media.
GET /flow-edit/gifs api.pages.write Search Klipy GIFs, or fetch trending GIFs with an empty query.
POST /flow-edit/gifs/download api.media.write + page update right Download a selected GIF into page media.
POST /flow-edit/crop api.media.write + page update right Crop page media; body {route, filename, x, y, width, height} (source pixels). Writes {base}-crop-{x}-{y}-{width}x{height}-{sourceHash}.{ext} next to the source; returns the new media record. Min crop size 8×8; jpg/png/webp/gif only. Existing derivatives remain valid.

Conventions and gotchas

  • The storage contract is stable. Any incompatible change requires a migration note and explicit conversion path before release.

  • Tests are colocated *.test.ts next to sources; there is no per-file test runner configured — run everything with npm test.

  • tsconfig is very strict: noUncheckedIndexedAccess, verbatimModuleSyntax, isolatedModules, noUnusedLocals/Parameters. Use import type and index guards accordingly.

  • Six custom blocks ship: callout, cta, pullquote, accordion, tabs, and toc. The first five are explicitly asserted by verify-list-blocks.php.

  • Server-side shortcode handlers are the security boundary: every renderer allow-lists parameter keys, validates ids/URLs, clamps width/align, and escapes output. Keep that posture when adding parameters.

  • Editor UI strings use PLUGIN_FLOW_EDIT.* keys resolved through Admin2’s window.__GRAV_I18N, with hard-coded English fallbacks in src/field/i18n.ts (t() helper). Add the key to i18n.ts fallbacks and, if it should be translatable in the Admin UI, to languages/*.yaml.

  • Builder messages use src/builder/messages.json and the bt() helper in src/builder/i18n.ts. Add matching en/de entries under PLUGIN_FLOW_EDIT. verify-i18n.php includes the builder catalog and Svelte sources.

  • Fixed visual elements escape attributes and sanitize rich text. Keep URL/style validation shared between preview and public output; never accept arbitrary templates or executable markup from layout metadata. Theme templates remain trusted site code, and the browser must isolate their preview output.