filagora / filagora-cms
Pages, blocks, navigations, media and a draft, version, schedule and preview workflow for Filagora.
Requires
- php: ^8.4
- filagora/filagora-core: ^0.2
- filagora/filagora-entitlements: ^0.2
- filament/filament: ^5.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-13 20:43:18 UTC
README
Pages, content blocks, menus, tags and media for Filagora, with a staged-edit workflow (drafts,
named variants, versions, scheduled publishing and previews), as a Laravel 13 / Filament 5 plugin.
Requires filagora/filagora-core and
filagora/filagora-entitlements.
In development: not released and not on Packagist yet. Only the data layer exists so far (tables and models); the services, the draft and preview workflow and the Filament screens are still to come. There is no frontend: the finished package will be models, services, events and staff screens, and you render the pages.
What exists so far
-
Fourteen tables, prefixed
filagora_by default, matching an existing site's tables index for index sophp artisan filagora:adoptcan accept them:cms_pages,cms_blocks,cms_block_versions,cms_page_versions,cms_page_drafts,cms_page_draft_variants,cms_navigations,cms_navigation_versions,cms_navigation_drafts,cms_navigation_draft_variants,cms_tags,cms_page_tag,cms_media_assetsandcms_preview_shares. -
Models for each (the tag pivot has none), swappable under
filagora.models.<key>. A page keeps its custom route as/x, is stamped with a publication time when it becomes published, and is public once that time has arrived (CmsPage::published()). Staged copies of blocks are hidden from every live read. A block'stypeis a free string, so a type the site no longer knows still loads. Saving a menu clears its cached items. -
A schema snapshot for
filagora:adopt. A site's own extra columns are declared with a coreSchemaExtension. -
Readers (
CmsContent): published pages (newest first, optionally by tag), a page by slug or by custom route (a draft page is served only to its preview token), menus, a published reusable block by slug, and media assets by path. Menus are cached undercms:navigation:{name}fornavigation_cache_seconds(default 300) and cleared when a menu is saved. A request that is previewing unpublished changes is served those, through thePreviewResolvercontract (the default previews nothing). -
Menu items for a visitor (
NavigationItems): unknown link kinds become plain URLs, URLs are limited to relative paths and http(s), targets to_self,_blank,_parent,_top, items the visitor lacks the entitlements for are dropped, and so are empty placeholders. A site registers its own link kinds (a legal page, a named page of the application, an action such as logout) withNavigationLinkTypes::register(), giving a resolver from the item'sreferenceto a URL. -
Block types (
BlockTypeRegistry): nineteen generic types ship (raw HTML, hero, image, gallery, card grid, feature list, embed, divider, call-to-action band, stat strip, link grid, logo grid, accordion, icon callout, icon card grid, split info panel, checklist callout, comparison matrix and panel). A site registers its own and aliases for old keys. A stored type nobody knows still loads and is labelled as unknown. Each type says where its media asset ids sit in the block data, which is howMediaAssetUsagetells which live blocks use an asset. -
The staged-edit workflow for pages (
PageDrafts). Edits go into a working draft (the page fields, its tags, and a staged copy of each block) and reach live content only on publish.- Variants: a page has one working draft; others are parked beside it. Start a variant from the current draft or from live, switch, rename and delete them. Names are unique within a page, ignoring case. A draft with a schedule cannot be switched away from.
- Publish applies the fields, tags and blocks (edited, added and removed) in one transaction,
refuses a slug or custom route another page uses, records a numbered version and dispatches
CmsPagePublished.PageDrafts::isPublishing()is true meanwhile, so listeners that react to every saved row can wait for the event. - Versions:
PageVersioningsnapshots a page;restoreVersionToDraftloads one into the draft without touching live content;isStalesays the page was published since the draft began. - Schedule:
schedule,cancelScheduleandpublishDue, run byphp artisan filagora:cms:publish-due(schedule it every minute). A draft that can no longer publish has its schedule cleared and the problem reported; the rest still go out. - Preview:
previewPageandpreviewPageVariantbuild an unsaved in-memory copy as it would look once published. PageDraftDiffdescribes what publishing would change, field by field, tags and blocks.
-
The same for menus (
NavigationDrafts): one working draft of a menu's items, named variants, discard, stale check, publish (saving the menu clears its cache) with a version of the menu's name and items and aCmsNavigationPublishedevent, restore a version to the draft, schedule, anditemsForVariantto read the items of any variant.NavigationDraftDifflists items added, changed and removed, matched by label path ("Support › Donate"). A menu's name is an identifier the site relies on, so it is never staged.filagora:cms:publish-duepublishes due pages and menus. -
The same for reusable blocks (
BlockDrafts): a hidden staged copy, pending-change check, publish with a version and aCmsBlockPublishedevent, restore from a version.
These services do not check who may call them; the Filament screens do.
-
Previews.
PreviewSessionis thePreviewResolverthe readers use, so a browser that is previewing sees draft menus and pages at their normal public addresses while everything else shows live content. Two things start a preview: an editor adding items (addPage,addNavigation; each served from its working draft; needsfilagora.cms.manageorfilagora.cms.preview), or a shared preview link.PreviewShares::create()bundles chosen menus and pages, each pinned to a named variant, with an expiry in days and a label. The link's token is long, random and never serialised.open($token, $session)starts the preview for whoever holds it and counts the open;revoke()ends it, and so does expiry, at the next request. The holder previews in their own browser session; nothing is made public.landingPage()says where to send them.items()describes what is being previewed (for a banner) andcurrentSelection()bundles an editor's own preview for sharing.MarkPreviewResponsesUncacheablemarks every responseno-store, privateandnoindex, nofollowwhile a preview is in effect. It is added to thewebgroup unless you setfilagora-cms.preview.uncacheable_middlewareto false.- The routes, the controllers and the banner are yours: this package has no frontend.
-
Two abilities,
filagora.cms.manageandfilagora.cms.preview, mapped to entitlements infilagora-cms.gates. -
Filament screens (
FilagoraCmsPlugin, in a "Content" navigation group; every screen needsfilagora.cms.manage):- Pages: list with a state badge and filter; create; an editor that works on the draft ("Save draft"), with header actions for variants (switch, new, rename, delete), a shared preview link, preview, publish (with a version summary and a stale-draft warning), schedule, cancel schedule, discard draft and unpublish; a pending-changes panel from the diff; a staged-blocks table (add, edit, delete, with change badges); and a versions list (view the snapshot, restore to draft, delete). Each block type has its own form (see below); a type with no fields, or one nobody registered, falls back to a JSON editor and can still be edited.
- Menus: the same workflow, with a nested link editor (three levels) whose link kinds come
from
NavigationLinkTypes. - Tags, Media (single and bulk upload into
cms-media, details read from the file, usage count, delete with a warning when a block still uses it) and Preview links (status, opens, show link, revoke). - The page URL the editor's Preview sends you to comes from the
PageUrlResolvercontract (default: the custom route or/pages/{slug}); the address of a shared link from the route named infilagora-cms.preview.share_route. Both are yours to provide. - Reusable blocks: list (type filter, pending-changes badge), create, an editor that works on the staged copy (slug changes apply at once), publish, discard, delete, and a versions list.
- Block forms: each of the 19 generic types declares fields with a small DSL
(
Field::text(),url(),textarea(),markdown(),select(),toggle(),number(),strings(),items(),media()), passed as the fifth argument ofBlockType. One definition drives the form, normalises what is saved, upgrades older stored shapes and derives the media paths (so usage tracking follows). A profile package registers its own types the same way.
Still to come
Nothing planned for this package beyond release.
Install
composer require filagora/filagora-cms php artisan migrate php artisan filagora:entitlements:sync
->plugin(\Filagora\Cms\FilagoraCmsPlugin::make())
Schedule filagora:cms:publish-due every minute to publish scheduled pages, menus and blocks.
Implement PageUrlResolver if your pages are not served at /pages/{slug}. Publish the config
with --tag=filagora-cms-config and the editor text with --tag=filagora-cms-lang.
Testing
composer test
Tests run against a real MariaDB database named testing.
Licence
MIT. See LICENSE.