particle-academy/fancy-cms
A Laravel CMS package from Particle Academy offering content management features for Fancy-based projects. Early-stage with minimal adoption and limited documentation, so review the source and test thoroughly before using it in production.
This package is the PHP-side implementation of the fancy-cms 0.8 page-operation spine. The mental model is simple: a PageDoc holds a page tree, a PageOp describes one edit, Reducer::reduce produces the next document, PageJson handles wire/storage encoding, and PageRenderer emits HTML/CSS.
PageOp variants, Reducer, PageJson, PageDoc, PageRenderer).PageOp variant list and Reducer::reduce — these are the contract shared with fancy-cms-ui. Anything you accept from the browser should arrive as one of these variants.PageJson, never json_decode directly. Empty-object vs. empty-list distinctions and numeric map keys matter here.PageJson, apply one op inside a controller action, validate implicitly through PageDoc::apply, persist the encoded result, and render.$doc = PageJson::decode($storedJson); // preserves [] vs {} semantics
$op = PageOp::fromWire($request->validated()); // one of 13 variants
$next = PageDoc::apply($doc, $op); // validates before + after
$doc->store(PageJson::encode($next));
Once that loop works, add PageRenderer and you have an end-to-end server-rendered CMS page.
Persist the doc, not the ops. PageDoc::apply is the storage boundary — it validates the doc before and after an edit and refuses semantic no-ops. Treat the reducer as the only mutator and keep the encoded PageDoc as your source of truth. Optionally log the ops for audit/undo; replaying them is only safe if your stored docs were produced by the same reducer version.
Use Reducer::reduce for previews, dry runs and undo. Because it's pure and reduceAll exists, you can compute a speculative document without touching storage:
$preview = Reducer::reduceAll($doc, $pendingOps); // no persistence
Combine this with the JSON-wire inverses to implement client-side optimistic UI that gets reconciled server-side: the browser applies the op, and on rejection you replay the inverse.
Merge patches are shallow — send whole property objects. Property patches preserve null values and do not merge deeply. If you're updating styles, send the complete style object you want, not a nested fragment. Preserve-null means you can no longer use "missing" to mean "unset"; explicitly null things.
Generate fractional keys server-side with base-62, matching the JS. Insertions need a key between neighbours. The PHP helpers produce the same keys as fancy-cms-ui 0.8, so a key offered by the client will validate. Don't roll your own comparator ordering — use the tree helpers.
Render with the semantic-handle contract. PageRenderer puts the stable data-cms handle on a semantic element (p, h1–h6, a, ul/ol) or a landmark. This is a feature: bind into the supplied data context and let static headings, paragraphs, links/buttons, lists, landmarks and images render without custom HTML. Use theme tokens and responsive stack/grid/auto-fit controls rather than hand-authored CSS where possible; all values pass the same CSS gate as the JS emitter.
Wire integration with fancy-cms-ui: the parity suite is the contract. Keep the JS client and PHP server on matched versions; if you accept ops from a client running an older UI, expect unknown variants or rejected keys.
Register host node types. Known node types and a host registry drive validation. If you add a custom node, register it there — otherwise PageDoc validation will reject it with a structured path error at the point of use.
data-cms attribute now lands on a semantic element, not always a div. Update selectors div[data-cms] → [data-cms]. Set margins explicitly where browser defaults for p/h1/ul differ from what your old div layout assumed — this is the most likely cause of a visually "sudden" regression after upgrade.seq is not an edit counter. It increments only on applied edits. Invalid targets, cycle moves and non-permutation root reorders leave it unchanged. Don't use seq to detect "user touched the page".PageDoc::apply rejects semantic no-ops — writing the same value back will fail or throw rather than bump seq. Short-circuit no-ops in your controller before calling apply.PageJson for storage and transport. It preserves empty object vs. empty list and numeric map keys, and refuses non-JSON objects before a serialization callback can run. Passing an Eloquent model or DTO with a JsonSerializable implementation will be refused — convert to plain arrays first.How can I help you explore Laravel packages today?