Use this file when adding or changing CMS-backed pages in any Laravel app that uses this package.
php artisan boost:add-skill banulakwin/laravel-page-builder
Save this content to: AGENTS.md
# Agent guide: `banulakwin/laravel-page-builder`
Use this file when adding or changing CMS-backed pages in **any** Laravel app that uses this package.
## Mental model
- **Section order on the site** follows your Inertia page and component tree (and how you call `getPage()` / pass `cms`), not a `sort_order` field in CMS definitions.
- **Files** (`app/Cms/Pages/{key}.php` by default) = schema + **default values** for sync only.
- **Database** (`page_contents`) = runtime source of truth for content (via `PageService::getPage()` / `page()` helper).
- **`php artisan page:sync`** = insert **missing** `(page, section, key)` rows with defaults; **never** overwrites existing values (including after soft-delete restore).
- **`config/pages.php` → `common_sections`** = shared sections synced under reserved page key **`__common`**; merged into **`PageService::getPage()`** only (not **`getSection()`**).
## Adding a new static page (e.g. `promotions`)
1. Pick a **page key** (slug) used in your app and controller: e.g. `promotions`.
2. Create **`app/Cms/Pages/promotions.php`** returning:
```php
<?php
declare(strict_types=1);
return [
'sections' => [
'promotions-header' => [
'fields' => [
'title' => ['type' => 'text', 'default' => 'Promotions'],
'subtitle' => ['type' => 'textarea', 'default' => ''],
],
],
],
];
```
The **section array key** (`promotions-header`) is the section slug at runtime—do **not** duplicate `slug` or `name` as fields unless you need them as editable content. With **`@banulakwin/inertia-page-builder`**, `parseCmsSection(cms, 'promotions-header')` sets `slug` from that key and returns dynamic `fields` for any section shape. Pass optional **field definitions** as a third argument when you need `label` / `meta` / `disk` / `path` on the client (use `PageRegistry::publicFieldMetadataBySection($pageKey)[$sectionSlug]` — no `default` or `rules` in that export).
3. Run **`php artisan page:sync`**.
4. Add **`promotions`** to **`config/pages.php`** → **`catalog`** with **`['enabled' => true]`** (or boolean shorthand) when you use catalog gating.
5. In your controller, pass **`PageService::getPageWhenEnabled('promotions')`** (or **`page_when_enabled('promotions')`**) as the **`cms`** prop, **or** attach route middleware **`page-builder:promotions`** and use **`getPage()`** if you prefer the check in the router only.
## Adding a section to an existing page file
1. Open **`app/Cms/Pages/{pageKey}.php`**.
2. Add a new key under **`sections`** with a **`fields`** array.
3. Run **`php artisan page:sync`** so new keys get default rows.
## Validation errors
`PageRegistry::validate()` throws `InvalidArgumentException` if:
- the file does not return an array;
- there is no top-level **`sections`** array;
- any section omits **`fields`**.
## Optional field metadata
Fields may include **`label`**, **`rules`** (string rules array), **`meta`** (free-form array), **`disk`**, and **`path`** (storage directory on that disk). They are optional; `banulakwin/filament-cms-admin` and HTTP saves via **`FieldResolver`** honor `disk` / `path` for images when set.
## Optional cache
If `page-builder.registry_cache_ttl` > 0, **`page:sync`** clears the registry cache so new files apply immediately.
## Do not
- Load page definition files on normal web requests (use `PageService` / DB only).
- Use `updateOrCreate` in custom sync code if you intend to preserve Filament or user edits; use the same semantics as this package's sync command.
- Commit generated files (`vendor/`, `composer.lock`, `coverage/`).
## Testing & Quality
```bash
composer test # PHPUnit
composer pint # Laravel Pint code style fix
composer pint:check # Pint check only (no fix)
composer phpstan # PHPStan level max on src/
composer quality # All: pint + phpstan + test
```
## CI
GitHub Actions runs tests, Pint, and PHPStan on push/PR (`.github/workflows/tests.yml`).
banulakwin/laravel-page-builderUse this file when adding or changing CMS-backed pages in any Laravel app that uses this package.
getPage() / pass cms), not a sort_order field in CMS definitions.app/Cms/Pages/{key}.php by default) = schema + default values for sync only.page_contents) = runtime source of truth for content (via PageService::getPage() / page() helper).php artisan page:sync = insert missing (page, section, key) rows with defaults; never overwrites existing values (including after soft-delete restore).config/pages.php → common_sections = shared sections synced under reserved page key __common; merged into PageService::getPage() only (not getSection()).promotions)promotions.app/Cms/Pages/promotions.php returning:<?php
declare(strict_types=1);
return [
'sections' => [
'promotions-header' => [
'fields' => [
'title' => ['type' => 'text', 'default' => 'Promotions'],
'subtitle' => ['type' => 'textarea', 'default' => ''],
],
],
],
];
The section array key (promotions-header) is the section slug at runtime—do not duplicate slug or name as fields unless you need them as editable content. With @banulakwin/inertia-page-builder, parseCmsSection(cms, 'promotions-header') sets slug from that key and returns dynamic fields for any section shape. Pass optional field definitions as a third argument when you need label / meta / disk / path on the client (use PageRegistry::publicFieldMetadataBySection($pageKey)[$sectionSlug] — no default or rules in that export).
php artisan page:sync.promotions to config/pages.php → catalog with ['enabled' => true] (or boolean shorthand) when you use catalog gating.PageService::getPageWhenEnabled('promotions') (or page_when_enabled('promotions')) as the cms prop, or attach route middleware page-builder:promotions and use getPage() if you prefer the check in the router only.app/Cms/Pages/{pageKey}.php.sections with a fields array.php artisan page:sync so new keys get default rows.PageRegistry::validate() throws InvalidArgumentException if:
sections array;fields.Fields may include label, rules (string rules array), meta (free-form array), disk, and path (storage directory on that disk). They are optional; banulakwin/filament-cms-admin and HTTP saves via FieldResolver honor disk / path for images when set.
If page-builder.registry_cache_ttl > 0, page:sync clears the registry cache so new files apply immediately.
PageService / DB only).updateOrCreate in custom sync code if you intend to preserve Filament or user edits; use the same semantics as this package's sync command.vendor/, composer.lock, coverage/).composer test # PHPUnit
composer pint # Laravel Pint code style fix
composer pint:check # Pint check only (no fix)
composer phpstan # PHPStan level max on src/
composer quality # All: pint + phpstan + test
GitHub Actions runs tests, Pint, and PHPStan on push/PR (.github/workflows/tests.yml).
How can I help you explore Laravel packages today?