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, notpackage.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/:
- run
npm run build, - 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
- Admin2 loads the field bundle and sets the page’s Markdown as the field value.
- The field fetches
GET /api/v1/flow-edit/settings(api.pages.write) to build the slash menu and custom-block forms. - Edits serialize back to Markdown (shortcodes for rich media) on every
change. Admin2 saves the file through the API. FlowEdit’s
onApiBeforePageUpdatehook rejects a stale content baseline and strips its transport-only metadata before the write; the draft remains available for recovery in the editor. - 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.tsnext to sources; there is no per-file test runner configured — run everything withnpm test. -
tsconfigis very strict:noUncheckedIndexedAccess,verbatimModuleSyntax,isolatedModules,noUnusedLocals/Parameters. Useimport typeand index guards accordingly. -
The five shipped custom blocks (
callout,cta,pullquote,accordion,tabs) are asserted byverify-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’swindow.__GRAV_I18N, with hard-coded English fallbacks insrc/field/i18n.ts(t()helper). Add the key toi18n.tsfallbacks and, if it should be translatable in the Admin UI, tolanguages/*.yaml.