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.

Toolchain

  • Field bundle: TypeScript + Vite + TipTap 2, Node.js required.
  • 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       # vite build -> admin-next/fields/flow-edit.js

Suggested order before committing field changes:

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

The golden rule: commit the built bundle

admin-next/fields/flow-edit.js is committed to git so Grav installs need no Node. After any change under src/field/:

  1. run npm run build,
  2. commit the rebuilt bundle together with the source change.

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).

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

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
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/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.

API

Route Permission Purpose
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.

  • The five shipped custom blocks (callout, cta, pullquote, accordion, tabs) are asserted by verify-list-blocks.php — do not remove them.

  • 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.