Skip to main content

Documentation

No results found.
Features Members

Custom Features (Website-Specific Modules)

Custom features are self-contained modules under app/CustomFeatures/ — the client-owned extension layer of WebProCMS. They are the CMS's equivalent of WordPress plugins: real Laravel/Livewire code (own database tables, dashboard pages, publ...

Custom features are self-contained modules under app/CustomFeatures/ — the client-owned extension layer of WebProCMS. They are the CMS's equivalent of WordPress plugins: real Laravel/Livewire code (own database tables, dashboard pages, public routes) that installs, updates, and version-controls independently of the CMS itself.

The ownership contract in one line: commit = global, install-only = local. Anything committed to the CMS repo ships to every install (that's what app/Features/ is for); anything under app/CustomFeatures/ belongs to one website (or to whoever distributes the module) and is never touched by a CMS update. The CMS .gitignore excludes the whole tree, and release packages are built from git ls-files, so an update can neither ship nor overwrite a custom module.

Who can do what

Capability Requires
Run installed modules, receive their updates nothing — never gated, a lapsed subscription won't break a site
Install modules (ZIP upload / update feed) any active membership (Pro plan or above)
Build modules (make:custom-feature, dev:init-repo) the Enterprise plan (config('cms.developer_tier'), env CMS_DEVELOPER_TIER)

The license server and author-mode installs (CMS_AUTHOR_MODE=true) are exempt from both gates. Setting CMS_DEVELOPER_TIER="" disables the developer gate entirely.

Module anatomy

app/CustomFeatures/AcmeInventory/
├── manifest.json                      ← identity: key, name, version, update_url
├── AcmeInventoryServiceProvider.php   ← auto-registered when the manifest is valid
├── routes/
│   ├── cms.php    ← dashboard routes (web + auth + verified + role + feature:acme-inventory)
│   └── web.php    ← public routes (web + CacheResponse + feature:acme-inventory)
├── resources/views/
│   ├── dashboard/⚡index.blade.php    ← referenced as acme-inventory::dashboard.index
│   └── public/⚡index.blade.php
├── Database/Migrations/               ← run on install/update and by php artisan migrate
└── vendor/                            ← optional committed composer deps (see below)
  • Discovery — CustomFeatures scans app/CustomFeatures/*/manifest.json; CustomFeaturesServiceProvider registers App\CustomFeatures\{Studly}\{Studly}ServiceProvider for every valid manifest. No registration file to edit.
  • Enable/disable — toggled per-install from Settings → Features → Website-Specific; the feature:{key} middleware 404s the module's routes while it's off. Disabling never drops tables.
  • Vendored dependencies — a module may commit its own vendor/; the scaffolded provider bootstraps vendor/autoload.php when present. No composer runs on client installs.
  • Dashboard visibility — module dashboard pages have no automatic sidebar entry; link them from your pages or register a dashboard-home card via DashboardWidgetRegistry.

Building a module (Enterprise)

php artisan make:custom-feature AcmeInventory --feature-description="Inventory sync for Acme"

scaffolds the full anatomy above with working starter pages at /dashboard/acme-inventory and /acme-inventory.

Version control — your repo, not ours

php artisan dev:init-repo

initializes a git repository at the project root that tracks only the client-owned trees — app/CustomFeatures/, resources/themes-custom/, resources/design-library-custom/ — via .git/info/exclude plus per-tree .gitignore re-includes (all of which live outside the release package, so a CMS update can't disturb them). Push it to your own GitHub. Because the CMS and your code never overlap on disk, a CMS package update applies around your repo without ever dirtying git status — no merge conflicts, by construction.

Do not run it on a git-engine install or the CMS author checkout (it refuses unless --forced).

Distributing modules to other installs

  1. Package: cd app/CustomFeatures/AcmeInventory && zip -r ../acme-inventory.zip . (manifest.json at the archive root; one directory below also works).

  2. Install: the receiving install (any active membership) uploads the zip under Settings → Features → Website-Specific → Install Feature. FeatureInstaller validates the manifest, guards against path traversal, extracts, and runs the module's migrations.

  3. Auto-update: set update_url (https) in the manifest. Installs poll daily (features:check-updates) and apply per their per-feature auto-update mode (Manual / Automatic minor / Automatic major). Two feed shapes are accepted (FeatureUpdateFeed):

    {"version": "1.2.0", "notes": "…", "download_url": "https://…/module.zip", "sha256": "…", "min_cms_version": "1.0.0"}
    

    or a GitHub release — point update_url at https://api.github.com/repos/you/repo/releases/latest: tag_name/body map to version/notes, and the first .zip asset supplies the download URL and its sha256 digest. Tag a release on GitHub and every install running your module picks it up on its next daily check.

    Private feeds can require a Bearer token — each install stores it in the per-feature updateToken Setting. (GitHub's browser_download_url only works for public releases; distribute private modules from your own feed.)

Extending the page builder (custom items + primitives)

Beyond whole rows (sections), the Enterprise developer surface lets you add two finer-grained page-builder pieces. Both live in the gitignored resources/design-library-custom/ tree (already whitelisted by dev:init-repo), so they survive CMS updates and version-control in your own repo.

Custom items — new entries in the in-row "Add Item" picker

A reusable snippet (a stat block, a pricing badge, a branded callout) that editors drop into any row.

php artisan make:dl-item pricing-badge --name="Pricing Badge" --icon=tag --category=marketing

writes items/pricing-badge.blade.php (the snippet — compose any x-dl.* components, including your own primitives; __SLUG__/__PREFIX__ are substituted on insert) and items/pricing-badge.json (picker metadata: name, icon, category, context, prefix, optional min_role / feature gate). It appears in the editor's Add Item picker immediately — discovered by CustomDesignLibrary::items(), merged into RowItemLibrary. A new category becomes its own picker heading (label it in an optional items/_categories.json).

Custom primitives — brand-new x-dl.* building blocks

A first-class component with its own editable fields in the sidebar, usable inside any row or item.

php artisan make:dl-primitive stat-tile

writes components/dl/stat-tile.blade.php (the component) and schemas/stat-tile.php — a versioned envelope:

return [
    'schema_abi' => 1,
    'fields' => function (array $attrs): array { /* field list */ },
];

schema_abi versions the return contract itself (the PHP twin of the JSON manifests' manifest_version): a schema declaring an ABI newer than the CMS understands is skipped loudly (logged; the primitive still renders, its editor field list is just empty) instead of being mis-read. The bare ABI-1 shorthand — returning the callable(array $attrs): array (or a plain field array) directly — stays supported forever. content($slug, KEY, …) reads in the blade must match the schema keys. Use it anywhere as <x-dl.stat-tile slug="…" prefix="…" />. Its fields render in the page editor's design/content sidebar just like a core primitive.

Core always wins. Custom primitives resolve through an additional view location registered after the shipped one, so a custom slug can never shadow a core x-dl.* component. The scaffolder and the discovery layer both reject a custom item key / primitive slug that collides with a core identifier (mirrors the custom-feature manifest-key rule). Nest custom primitives inside a core <x-dl.section> / <x-dl.wrapper> (the normal row-authoring convention) so their fields attribute cleanly to the enclosing editor card.

Styling just works. A custom primitive's default classes render even on a node-free (or build) install without any extra step: the runtime CSS supplement scans custom primitive blades and generates their utilities via the pure-PHP generator. Use standard Tailwind utilities and the brand tokens (primary/secondary/tone); an exotic value the pure-PHP generator can't produce is dropped silently (same as any editor-added class), so prefer utilities the shipped rows already use for anything unusual.

Why CMS updates and custom code never conflict

Client installs update the CMS via the package engine: a sha256-verified release zip built from the CMS repo's git ls-files, overlaid onto the install after a pre-update snapshot. Since app/CustomFeatures/, resources/themes-custom/, and resources/design-library-custom/ are never in that file list, the overlay flows around them. Your git repo tracks only those trees, so the update is invisible to it — and your deploys are invisible to the CMS. Two layers, one seam, zero merges.