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, 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 # 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/:
- run
npm run build, - 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
- 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.
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.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. -
Six custom blocks ship:
callout,cta,pullquote,accordion,tabs, andtoc. The first five are explicitly asserted byverify-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’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. -
Builder messages use
src/builder/messages.jsonand thebt()helper insrc/builder/i18n.ts. Add matching en/de entries underPLUGIN_FLOW_EDIT.verify-i18n.phpincludes 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.