Implement, integrate, test, or review nvl/content in Laravel 13. Use for source-controlled block definitions, typed or custom fields, localized content, Media and reference values, placements, regions, trees, rendering, immutable composition snapshots, Blade starting views, APIs, or package architecture.
php artisan boost:add-skill nvl/content
Save this content to: .claude/skills/nvl-content/SKILL.md
# NVL Content
For major 6, use native Ownership for new isolated integrations and the installed
Ownership README/UPGRADING runbook for configured subclasses and reviewed adoption.
Ordinary unowned hosts keep unscoped declarations; installation does not enable
isolation. Tenant-specific APIs below apply only to the explicitly selected
deprecated Tenancy bridge; they never substitute for native owner admission.
## Tenant ownership
- Keep definitions as immutable platform code vocabulary. Synchronize them only
through an authorized platform operation; never accept tenant identity in a
client DTO.
- In adopted mode, enter `TenantRunner` before Content APIs. Canonically reload
every owner, block, placement, Media asset, and reference result and require
one registered tenant resource/connection graph before authorization or
rendering.
- Capture tenant-bound format-2 snapshots. Convert reviewed format-1 snapshots
explicitly with `Content::adoptSnapshot()`; never add a runtime fallback.
- Adopt Media and owner families before Content, backfill in bounded batches,
verify inherited rows and owner equality, then activate final constraints
under maintenance. Activation is not safely flag-reversible.
Keep block schemas and Blade views source-controlled. Persist only bounded,
validated values, locale rows, placement facts, lifecycle state, and immutable
composition snapshots.
## Define content
- Register definitions in configuration or deterministic `*.content.php` and
`*.content.json` sources.
- Mark deployment-critical source roots as `required_definition_paths`; do not
silently accept a missing authoritative directory.
- Synchronize them through `SyncContentDefinitionsAction` or
`nvl:content:definitions:sync`. Keep an atomic-lock-capable shared cache so
rolling deployment processes cannot race first-time definition writes.
- Treat definition versions as persisted contracts. Register one deterministic
`ContentDefinitionMigration` per sequential version step, dry-run with
`nvl:content:definitions:migrate --dry-run`, then apply bounded atomic
batches. Revalidate the target scope and every dependent placement against
the new contract. Never let an ordinary update silently adopt a newer schema.
- Use built-in aliases or implement `ContentFieldTypeAdapter`; reject duplicate
aliases and arbitrary class names.
- Use semantic `link`, `button`, `image`, `heading`, and `banner` presets for
repeated website structures. Add consumer presets through
`ContentFieldPreset`, not by adding UI classes to schemas.
- Treat preset structure as canonical. A definition may supply its key, label,
required/localized flags, default, and settings, but must not replace the
preset type or recursive fields.
- Use the compiled definition JSON Schema and preset catalog for generic
editors. Keep Filament and other UI-specific components consumer-owned.
- Keep raw `ContentDefinitionSource` values internal. Public definitions,
preset fields, and snapshot blocks must expose the compiled recursive Data
contracts used by Core's Data TypeScript generation.
- Validate preset partition shape in `normalize()`, final locale-resolved
invariants in `validate()`, and mirror semantic editor constraints through
`jsonSchema()`. Non-decorative published images require resolved alt text.
- Implement `ContentFieldDefinitionValidator` when a custom adapter has
type-specific schema settings that must fail during boot.
- Keep definition and field defaults structurally valid at boot. Defer only
live external Media/reference existence and authorization checks until use.
- Mark locale-dependent leaves or subtrees localized. Keep structural Media
IDs, destinations, targets, layout, and emphasis in base values while their
alt text, labels, captions, and copy live in locale rows.
- Register every Content locale in `nvl-translatable.locales`; Content may narrow
that canonical registry but must not create a second locale universe.
- Require complete localized values in `nvl-content.locales.required_on_publish`;
an empty list requires every available locale. Other locales may remain
partial, including nested translations, but every supplied value must still
pass validation. Resolve missing optional copy through normal locale fallback.
- Keep JSON fields bounded and validate them with a Draft 2020-12 schema.
- Never evaluate database content as PHP or Blade.
## Mutate and compose
- Constructor-inject `Nvl\Content\Content` as the canonical boundary for its
documented model-first operations. Its facade is a static proxy to the same
service surface, not a second execution path. Keep undocumented Actions and
services behind that boundary; inject the documented focused DTO-first editor
Actions below when no equivalent `Content` method exists.
- The application surface exposes source synchronization, block browse/read,
the complete block lifecycle, definitions, presets, groups, placements,
live rendering, and snapshots.
- Pass the exact expected revision for updates and lifecycle transitions.
- Register stable owner aliases directly to Eloquent models implementing
`ContentOwner`; use `HasContent` to supply its package capability and
depend on `ContentOwnerRegistrar` from provider integrations. Read placements
through `Content::placements()` or `ListOwnerContentPlacementSummariesAction`
with the explicit actor; `contentPlacements()` is an internal relation.
- Declare every owner composition with the `CONTENT_GROUPS` constant, or one
composition with the `CONTENT_GROUP` constant. Reject undeclared groups and
return declared groups even before they contain placements.
- Treat groups as independent named owner compositions. Keep placement keys,
trees, limits, locks, renders, and snapshots group-scoped; keep regions
inside a group and definition categories in the editor catalog.
- Place blocks only through `Content::place()`.
- Discover active editor schemas through `Content::definitions()`, reusable
semantic contracts through `Content::presets()`, declared groups through
`Content::groups()`, and group placement facts through
`Content::placements()`.
- Prefer `Content::editor()` or
`Nvl\Content\Actions\GetOwnerContentEditorAction::execute(Illuminate\Database\Eloquent\Model&Nvl\Content\Contracts\ContentOwner
$owner, string $group, Nvl\Content\Data\ContentActorData $actor): Nvl\Content\Data\ContentEditorData` when a
consumer needs the complete typed definitions/presets/groups/placements
bootstrap. Use `ContentEditorData::placementLimit` as the editor ceiling and
`ContentPlacementData::block` for definition, lifecycle, values,
translations, metadata, and block revision; do not navigate Eloquent block
relations.
- For bounded indexes, inject
`Nvl\Content\Actions\ListOwnerContentPlacementSummariesAction` and call
`execute(iterable $owners, string $group, Nvl\Content\Data\ContentActorData
$actor): array<string, list<Nvl\Content\Data\ContentPlacementData>>`.
Pass zero to 100 persisted owner entries. Empty input performs no query;
duplicate canonical identities are collapsed; every unique owner is
authorized before SQL; one and 25 owners of one type use the same five-query
projection. Read results by the serialization-safe `<owner-type>:<owner-id>`
key, such as `page:01H...` or `account:42`.
- Treat `Nvl\Content\Enums\ContentAbility::ListPlacements` with
`context.includes_blocks=true` as disclosure of
the placed editable block DTO. Keep this owner-scoped decision explicit in
the consumer authorization adapter.
```php
use Nvl\Content\Actions\GetOwnerContentEditorAction;
use Nvl\Content\Actions\ListOwnerContentPlacementSummariesAction;
use Nvl\Content\Data\ContentActorData;
$actor = ContentActorData::fromAuthenticatable($user);
$editor = app(GetOwnerContentEditorAction::class)
->execute($page, 'content', $actor);
$placementsByOwner = app(ListOwnerContentPlacementSummariesAction::class)
->execute($pages, 'content', $actor);
$pagePlacements = $placementsByOwner['page:'.(string) $page->getKey()] ?? [];
```
- For exact editor lookups, inject
`Nvl\Content\Actions\FindContentBlockByKeyAction::execute(string $key,
Nvl\Content\Data\ContentActorData $actor): Nvl\Content\Data\ContentBlockData`
or
`Nvl\Content\Actions\FindContentPlacementAction::execute(Illuminate\Database\Eloquent\Model&Nvl\Content\Contracts\ContentOwner
$owner, string $group, string $idOrKey,
Nvl\Content\Data\ContentActorData $actor): Nvl\Content\Data\ContentPlacementData`.
Block keys must be byte-exact and unambiguous across active scopes. Placement
IDs/keys stay byte-exact and owner/group scoped, reject collisions, and never
compare a non-UUID key with the UUID primary-key column.
- Replace a placed block through
`Nvl\Content\Actions\ReplaceContentPlacementAction::execute(Illuminate\Database\Eloquent\Model&Nvl\Content\Contracts\ContentOwner
$owner, string $group, string $placement, string $block, int
$expectedRevision, Nvl\Content\Data\ContentActorData $actor):
Nvl\Content\Data\ContentPlacementData`. It locks the complete composition,
revalidates existing overrides against the replacement definition, and
saves normalized overrides and synchronizes their Media associations while
retaining the placement identity and tree position.
- Reorder with a complete
`Nvl\Content\Data\Mutations\ReorderContentPlacementsData` set and inject
`Nvl\Content\Actions\ReorderContentPlacementsAction::execute(Illuminate\Database\Eloquent\Model&Nvl\Content\Contracts\ContentOwner
$owner, string $group,
Nvl\Content\Data\Mutations\ReorderContentPlacementsData $data,
Nvl\Content\Data\ContentActorData $actor): Nvl\Content\Data\ContentEditorData`.
Include one
`Nvl\Content\Data\Mutations\ReorderContentPlacementData` for every placement,
even unchanged rows. Duplicate, partial, stale, cyclic, cross-region, or
over-limit proposals fail atomically; changed rows update and emit events in
ID order, and the Action returns a fresh editor DTO.
- Handle `Nvl\Content\Enums\ContentAbility::Place` contexts
`replaces_placement=true` and `reorders_placements=true` in the consumer
authorization adapter. These workflows are focused injected Actions, not
`Content` facade methods, so the original service constructor stays callable.
- Read `Content::placements()` as a collection of `ContentPlacementData` DTOs.
Pass each DTO's `id` and current `revision` to placement mutations; never
pass the DTO where an Eloquent placement or string identity is required.
- Remove leaf placements through `Content::deletePlacement()`; never delete
placed blocks or silently orphan a child tree.
- Use Patch as the safe update default. Request Replace only when omitted base,
locale, and metadata values are intentionally being removed.
- Keep localized values in block locale rows; placement overrides are
non-localized.
- Validate the final base-plus-override values with the actual owner, group,
and actor context. Block updates, publication, and definition migration must
revalidate every dependent placement and synchronize its Media usages in the
same transaction. Include retained soft-deleted owners internally while
keeping public owner resolution live-only and preserving other model scopes.
- Merge locale fallback schema-aware and leaf by leaf. Match translated
repeater rows to explicit, stable base `_key` values; never infer row
identity from an array position or let translations invent structural rows.
- Validate regions, parents, depth, cycles, stable placement keys, and ordering.
- Keep a complete subtree in one region, and suppress descendants whenever an
ancestor is hidden or unavailable.
## Media and references
- Store Media UUIDs in values; let Media own files, associations, scans,
variations, authorization, and delivery.
- Permit public Media in public blocks. Apply uploader/owner authorization to
private Media.
- Render through safe Media DTOs or authorized temporary URLs, never internal
paths.
- Preserve locale on localized Media associations and keep Content and Media
association writes on the same named database connection.
- Associate override Media with the placement UUID independently of block
usages, through public Media Actions. Creation, edits, block replacement,
publication, definition migration, and removal must preserve shared Media
deletion protection and authorize with the registered composition owner.
- Preserve the provider's stable `nvl-content-placement` morph alias, or an
explicit consumer mapping, when enforcing morph maps.
- Use the `HasContent` deletion boundary for owner cleanup; consumer `delete()`
overrides must delegate to it. Soft deletion retains placements and usages;
hard deletion removes both atomically on the same named owner/Content/Media
connection. A vetoed placement deletion or cleanup failure must roll back
the owner deletion and all usage changes. Keep the provider-injected deletion
runtime bridge clear between application lifecycles.
- Register `ContentReferenceResolver` aliases for consumer models. A resolver
owns existence, policy, and locale-aware display data. Use the supplied
`ContentValidationContext` for actor, owner, locale, visibility, field path,
and public/preview decisions.
## Render and snapshot
- Use `Content::render()` for live model/group compositions.
- Use `Content::capture()` and `Content::renderSnapshot()` when a publishing
consumer needs immutable copy.
- Capture under the owner-group mutation lock and placement/block row locks.
With `publishing: true`, validate the merged placement values using owner
and group context; overrides may satisfy required fields. Freeze the
validator's normalized final payload in snapshot `values` and translations,
with empty `overrides`, so rendering cannot reapply stale values. Keep older
snapshots with separate overrides readable.
- Keep render-resource caches per composition; never move Media/reference
projections into static or long-lived worker state.
- Verify snapshot hashes and owner identity on every render.
- Reject oversized snapshots, missing parents, cross-region nesting, excessive
depth, and cycles. Treat the hash as corruption detection, not authentication
for untrusted caller input.
- Keep snapshot records as `ContentCompositionSnapshotBlockData` with typed
`ContentSchemaData`; array hydration must restore the complete DTO graph.
- Persist snapshots with `ContentCompositionSnapshotCast` so consuming models
expose `ContentCompositionSnapshotData`, not untyped arrays.
- Resolve Media and reference delivery at render time so lifecycle and access
decisions stay current.
- Use the bundled Blade components or publish starting views with
`nvl:content:views:publish`.
## Secure and verify
- Bind `ContentAuthorization`; the default denies non-system actors unless an
explicit callback permits them.
- Implement `ContentBlockQueryScope` on the authorization adapter when block
catalogs require actor/tenant query constraints. Treat published
private-to-public updates as Publish operations as well as edits.
- Keep management and public routes disabled until their middleware and
authorization behavior are tested.
- Enforce payload, metadata, schema, depth, item, string, and snapshot limits.
- Hard-deny `javascript`, `data`, `file`, and `vbscript` URI schemes even when
consumer configuration attempts to allow them.
- Bound definition file count/size and placements per owner; invalid route
middleware must fail closed.
- Run `nvl:content:doctor --strict --format=json`, definition dry runs, Pest,
Pint, PHPStan at maximum strictness, `nvl:data:types:check`, database
matrices, and clean `nvl/laravel-suite` consumer tests on Laravel 13.
- Treat pending block versions or missing migration paths as deployment
failures. Require the doctor to verify semantic columns, indexes, and
foreign keys. Migration plans/events must never contain content values.
## Ownership isolation and release discipline
- Keep unowned applications independent of the enforcement runtime; native ownership never adds an undeclared Auth or Suite dependency. Tenancy-specific APIs are deprecated bridge compatibility.
- Use registered package-owned resources, adoption adapters, Actions, and lifecycle APIs. Never add a generic owner delete-all path or raw cross-package cleanup.
- Treat mapping/configuration hashes, interruption checkpoints, conservation evidence, worker context, owner-partitioned queries, and standalone consumption as release contracts.
## Shared owner identities
- Declare owner class lists in `nvl-core.owners` and reference model classes in `content` capability configuration. Laravel `getMorphClass()` supplies the host-authored stored identity; declarations do not add global host morph mappings.
- Owners still implement ContentOwner, declare composition groups, and use HasContent. Registering identity does not grant a group or mutation ability.
- Keep the package allowlist and authorization independent of Core registration. Never authorize a model merely because Core knows it.
- Preserve resolvers, handlers and authorization. Legacy aliases require agreement with native `getMorphClass()` and remain deprecated in major 6; Doctor reports mismatches and stored identity drift without conversion.
- If the host changes its morph map, explicitly reconcile reviewed package-owned columns and affected host relations before cutover. Core and package capability registration never mutate the host morph map or rewrite stored values.
## Shared consumer diagnostics
Run `php artisan nvl:doctor --strict --format=json` to combine checks from loaded NVL providers. Retain the package Doctor command for its detailed report; both paths reuse the package-owned inspection service.
## Shared infrastructure options
- Resolve definition synchronization and placement cache stores through `locks.store` and Core/Laravel inheritance; operation stores `locks.definitions.store` and `locks.placements.store` may override the shared store. Keep their existing lock durations.
- Use null to inherit shared route middleware; keep explicit lists atomic and apply `authorization.guard` only to bare `auth`. Capability authorization remains required.
### Brownfield storage identities
Resolve all package tables through the table helper and canonical `nvl-content.tables.*`, connections through `nvl-content.connection` with Core/Laravel inheritance. Defaults use `nvl_content_*`; migration filenames include that package slug. Never silently adopt a matching table or generic migration filename. Run shared `nvl:doctor --strict --format=json` and the explicit `nvl:schema:upgrade --package=content --claim-legacy --dry-run --format=json` before upgrading owned legacy storage. Validate the complete plan and choose one migration owner. Preserve host records, constraint names and stored morph values. Deprecated config inputs last one major; canonical options take precedence.
## Cache and lock ownership
Placement locks retain `nvl:content:placement-owner:` outside the tenant identity in every runtime mode. Definition synchronization keeps `nvl:content:definitions:sync`. Existing generic tenant lock entries are never acquired or removed.
Drain old mutation workers and maintenance processes, then wait for their outstanding lock leases to end before starting the new major across all nodes. Running old and new lock prefixes concurrently would create independent serialization domains. Restart workers after cutover; preserve host-selected stores and keys, and do not flush a shared cache to remove old NVL entries.
## Canonical configuration ownership
- Read/write `nvl-content` configuration and publish only canonical `nvl-<package>-<resource>` tags. Keep logical package/tenant resource identifiers unchanged.
- Generic config roots and unprefixed package environment names are foreign by default. For an upgrading NVL host only, select `nvl-core.compatibility.legacy_config` package IDs and `legacy_env` explicitly; both default off. Canonical presence wins, including false/null/empty values. Legacy inputs are read without writing back and remain deprecated in major 6.
- Use canonical `NVL_<PACKAGE>_*` variables only in config evaluation, then rebuild configuration caches and restart workers after cutover. Shared Laravel environment variables retain their names. Consult Core's versioned `support/resources/global-names.json` for all renames.
- Old global aliases and legacy route families require separate explicit `global_aliases`/`legacy_routes` package selections. Preserve collisions and use Doctor diagnostics; never grant generic permissions automatically or claim signed-link compatibility without the same authorization/signature checks.
## Compose host workflows through contracts
- Inject focused `Nvl\Content\Contracts\*Contract` interfaces for the 27 selected public Actions listed in the README; reuse existing equivalent contracts. Preserve the exact native `execute` parameters/defaults and DTO/model/vendor results.
- Use host interface bindings for substitution. Conditional defaults retain prebindings; late replacement reaches newly resolved host services. Keep private package constructor chains concrete and preserve package-owned authorization, queries, validation and mutation semantics.
- Inject `ContentContract` for the complete 24-method application surface. Its scoped default resolves the existing scoped Content concrete through the supplied container, retaining contextual editor injection. The Content facade uses this same contract. Clear `ContentContract::class` with the facade’s `clearResolvedInstance` after late replacement and at scoped/application boundaries.
## Consumer runtime and testing contracts
Start with the package README Quickstart and Testing your app sections. Use `nvl:install <package>` for loaded-package common config publication; it does not enable features, run schema or refresh caches. Preserve native host owner keys/morph maps and selected host Auth and ownership defaults. Read full runtime defaults and publish advanced config only deliberately.
Inject the supported focused interfaces and preserve host bindings. Returned model handles do not permit package-table queries/writes outside documented capability/extension seams. Host tests may substitute contracts in Laravel's container, use shipped model factories (ordinary make may persist parents; withoutParents()->make is detached), and use Laravel effect fakes deliberately. Only Media/Stripe have dedicated provider/library fakes; do not invent a universal package fake. Settings InteractsWithSettings is definition-only. Host PHPStan may include vendor/nvl/core/support/consumer-audit.neon; no unpublished workbench command is a consumer requirement.
Read docs/events.md and the package README error table. Domain events use schemaVersion=1, model-free facts and actual source-connection commit callbacks; only six declared old Event suffix aliases remain deprecated through major 6. Migrate exact listeners/fakes and suffix wildcards, drain old queued payloads, rebuild event cache and restart workers. Delivery is not a durable outbox. The Core exception renderer is opt-in, JSON-only for respondable failures, with exactly message/code/context and host-selected locale. Do not expose diagnostics or reinterpret missing bindings as authorization denial.
Core package logging uses nvl/normal with CSV quiet by default, stable message keys and bounded context; incidents survive quiet. Do not mutate global logger context or log raw row/provider/content/credential payloads. Run only authorized project checks and report new acceptance as pending until actual output exists.
## Compiled cache deployment
- Compiled caching stays opt-in. Set `nvl-content.compiled_cache.enabled` deliberately; required mode additionally needs `required=true` and a nonempty configured `version` (`NVL_CONTENT_DEFINITIONS_VERSION`).
- Generate with `php artisan nvl:content:cache --cache-version=release-2026-10-07`; the supplied token must match configuration and does not modify it. Never use the reserved Artisan `--version` option for this token.
- Build before enabling required mode, run `php artisan nvl:content:doctor --strict` for all presets and cache diagnostics, refresh configuration caches when changed, then restart long-lived workers. `php artisan nvl:content:clear` removes the selected artifact. Required runtime mode fails closed on invalid or absent cache; optional mode may compile source.
### Optional ownership and configured Content models
Ordinary Content installations keep the unscoped base models and require neither Ownership nor Tenancy. An owning application installs `nvl/ownership` and sets `nvl-content.models.block` to its concrete `ContentBlock` subclass. Return `OwnershipDefinition::column(Company::class, 'company_id')` or an exclusive native primary pivot from `ownershipDefinition()`; keep native query/persistence hooks, required casts and UUID identity. Add the actual owner FK or primary pivot in an application migration. There is no inferred actor, default company or generic owner pivot.
Declare host resource keys in `nvl-content.ownership.owner_resources`. Blocks/locales/revisions, placement hosts, block references and parent composition tuples must agree on the primary owner. Use canonical host models and package Actions; raw capability-pivot writes are unsupported. When a host combines `HasContent` and `HasOwnership`, select `HasContent::delete insteadof HasOwnership`; the Content deletion service holds the ownership lease and rolls back cleanup on cancellation.
Run the reviewed `content` adoption participant with explicit mappings for every historical row before enabling traffic. Persisted adoption markers keep prepared/adopted storage closed if its runtime is disabled or removed. Definition code remains an unscoped deployment catalog; its mutations require explicit central execution in an ownership-enabled installation. Dynamic reference resolvers implement `OwnershipSafeContentReferenceResolver` and return the exact persisted native record for the requested identifier.
New snapshots use format 3 with the complete native owner reference and a canonical checksum. Snapshot data is never an admission capability: rendering reloads the canonical owner and applies current authorization. Legacy snapshots require reviewed migration, with no automatic render fallback. `ContentStorage` and `ContentSnapshotIntegrity` are focused package-integration contracts; snapshot hashing alone does not authorize a read, import or migration. Catalog copies verify a current recipient grant, rebind the snapshot identity and require explicit Media mappings; application reference fields are rejected until a dedicated importer provides total target mappings.
For major 6, use native Ownership for new isolated integrations and the installed Ownership README/UPGRADING runbook for configured subclasses and reviewed adoption. Ordinary unowned hosts keep unscoped declarations; installation does not enable isolation. Tenant-specific APIs below apply only to the explicitly selected deprecated Tenancy bridge; they never substitute for native owner admission.
TenantRunner before Content APIs. Canonically reload
every owner, block, placement, Media asset, and reference result and require
one registered tenant resource/connection graph before authorization or
rendering.Content::adoptSnapshot(); never add a runtime fallback.Keep block schemas and Blade views source-controlled. Persist only bounded, validated values, locale rows, placement facts, lifecycle state, and immutable composition snapshots.
*.content.php and
*.content.json sources.required_definition_paths; do not
silently accept a missing authoritative directory.SyncContentDefinitionsAction or
nvl:content:definitions:sync. Keep an atomic-lock-capable shared cache so
rolling deployment processes cannot race first-time definition writes.ContentDefinitionMigration per sequential version step, dry-run with
nvl:content:definitions:migrate --dry-run, then apply bounded atomic
batches. Revalidate the target scope and every dependent placement against
the new contract. Never let an ordinary update silently adopt a newer schema.ContentFieldTypeAdapter; reject duplicate
aliases and arbitrary class names.link, button, image, heading, and banner presets for
repeated website structures. Add consumer presets through
ContentFieldPreset, not by adding UI classes to schemas.ContentDefinitionSource values internal. Public definitions,
preset fields, and snapshot blocks must expose the compiled recursive Data
contracts used by Core's Data TypeScript generation.normalize(), final locale-resolved
invariants in validate(), and mirror semantic editor constraints through
jsonSchema(). Non-decorative published images require resolved alt text.ContentFieldDefinitionValidator when a custom adapter has
type-specific schema settings that must fail during boot.nvl-translatable.locales; Content may narrow
that canonical registry but must not create a second locale universe.nvl-content.locales.required_on_publish;
an empty list requires every available locale. Other locales may remain
partial, including nested translations, but every supplied value must still
pass validation. Resolve missing optional copy through normal locale fallback.Nvl\Content\Content as the canonical boundary for its
documented model-first operations. Its facade is a static proxy to the same
service surface, not a second execution path. Keep undocumented Actions and
services behind that boundary; inject the documented focused DTO-first editor
Actions below when no equivalent Content method exists.ContentOwner; use HasContent to supply its package capability and
depend on ContentOwnerRegistrar from provider integrations. Read placements
through Content::placements() or ListOwnerContentPlacementSummariesAction
with the explicit actor; contentPlacements() is an internal relation.CONTENT_GROUPS constant, or one
composition with the CONTENT_GROUP constant. Reject undeclared groups and
return declared groups even before they contain placements.Content::place().Content::definitions(), reusable
semantic contracts through Content::presets(), declared groups through
Content::groups(), and group placement facts through
Content::placements().Content::editor() or
Nvl\Content\Actions\GetOwnerContentEditorAction::execute(Illuminate\Database\Eloquent\Model&Nvl\Content\Contracts\ContentOwner $owner, string $group, Nvl\Content\Data\ContentActorData $actor): Nvl\Content\Data\ContentEditorData when a
consumer needs the complete typed definitions/presets/groups/placements
bootstrap. Use ContentEditorData::placementLimit as the editor ceiling and
ContentPlacementData::block for definition, lifecycle, values,
translations, metadata, and block revision; do not navigate Eloquent block
relations.Nvl\Content\Actions\ListOwnerContentPlacementSummariesAction and call
execute(iterable $owners, string $group, Nvl\Content\Data\ContentActorData $actor): array<string, list<Nvl\Content\Data\ContentPlacementData>>.
Pass zero to 100 persisted owner entries. Empty input performs no query;
duplicate canonical identities are collapsed; every unique owner is
authorized before SQL; one and 25 owners of one type use the same five-query
projection. Read results by the serialization-safe <owner-type>:<owner-id>
key, such as page:01H... or account:42.Nvl\Content\Enums\ContentAbility::ListPlacements with
context.includes_blocks=true as disclosure of
the placed editable block DTO. Keep this owner-scoped decision explicit in
the consumer authorization adapter.use Nvl\Content\Actions\GetOwnerContentEditorAction;
use Nvl\Content\Actions\ListOwnerContentPlacementSummariesAction;
use Nvl\Content\Data\ContentActorData;
$actor = ContentActorData::fromAuthenticatable($user);
$editor = app(GetOwnerContentEditorAction::class)
->execute($page, 'content', $actor);
$placementsByOwner = app(ListOwnerContentPlacementSummariesAction::class)
->execute($pages, 'content', $actor);
$pagePlacements = $placementsByOwner['page:'.(string) $page->getKey()] ?? [];
For exact editor lookups, inject
Nvl\Content\Actions\FindContentBlockByKeyAction::execute(string $key, Nvl\Content\Data\ContentActorData $actor): Nvl\Content\Data\ContentBlockData
or
Nvl\Content\Actions\FindContentPlacementAction::execute(Illuminate\Database\Eloquent\Model&Nvl\Content\Contracts\ContentOwner $owner, string $group, string $idOrKey, Nvl\Content\Data\ContentActorData $actor): Nvl\Content\Data\ContentPlacementData.
Block keys must be byte-exact and unambiguous across active scopes. Placement
IDs/keys stay byte-exact and owner/group scoped, reject collisions, and never
compare a non-UUID key with the UUID primary-key column.
Replace a placed block through
Nvl\Content\Actions\ReplaceContentPlacementAction::execute(Illuminate\Database\Eloquent\Model&Nvl\Content\Contracts\ContentOwner $owner, string $group, string $placement, string $block, int $expectedRevision, Nvl\Content\Data\ContentActorData $actor): Nvl\Content\Data\ContentPlacementData. It locks the complete composition,
revalidates existing overrides against the replacement definition, and
saves normalized overrides and synchronizes their Media associations while
retaining the placement identity and tree position.
Reorder with a complete
Nvl\Content\Data\Mutations\ReorderContentPlacementsData set and inject
Nvl\Content\Actions\ReorderContentPlacementsAction::execute(Illuminate\Database\Eloquent\Model&Nvl\Content\Contracts\ContentOwner $owner, string $group, Nvl\Content\Data\Mutations\ReorderContentPlacementsData $data, Nvl\Content\Data\ContentActorData $actor): Nvl\Content\Data\ContentEditorData.
Include one
Nvl\Content\Data\Mutations\ReorderContentPlacementData for every placement,
even unchanged rows. Duplicate, partial, stale, cyclic, cross-region, or
over-limit proposals fail atomically; changed rows update and emit events in
ID order, and the Action returns a fresh editor DTO.
Handle Nvl\Content\Enums\ContentAbility::Place contexts
replaces_placement=true and reorders_placements=true in the consumer
authorization adapter. These workflows are focused injected Actions, not
Content facade methods, so the original service constructor stays callable.
Read Content::placements() as a collection of ContentPlacementData DTOs.
Pass each DTO's id and current revision to placement mutations; never
pass the DTO where an Eloquent placement or string identity is required.
Remove leaf placements through Content::deletePlacement(); never delete
placed blocks or silently orphan a child tree.
Use Patch as the safe update default. Request Replace only when omitted base, locale, and metadata values are intentionally being removed.
Keep localized values in block locale rows; placement overrides are non-localized.
Validate the final base-plus-override values with the actual owner, group, and actor context. Block updates, publication, and definition migration must revalidate every dependent placement and synchronize its Media usages in the same transaction. Include retained soft-deleted owners internally while keeping public owner resolution live-only and preserving other model scopes.
Merge locale fallback schema-aware and leaf by leaf. Match translated
repeater rows to explicit, stable base _key values; never infer row
identity from an array position or let translations invent structural rows.
Validate regions, parents, depth, cycles, stable placement keys, and ordering.
Keep a complete subtree in one region, and suppress descendants whenever an ancestor is hidden or unavailable.
nvl-content-placement morph alias, or an
explicit consumer mapping, when enforcing morph maps.HasContent deletion boundary for owner cleanup; consumer delete()
overrides must delegate to it. Soft deletion retains placements and usages;
hard deletion removes both atomically on the same named owner/Content/Media
connection. A vetoed placement deletion or cleanup failure must roll back
the owner deletion and all usage changes. Keep the provider-injected deletion
runtime bridge clear between application lifecycles.ContentReferenceResolver aliases for consumer models. A resolver
owns existence, policy, and locale-aware display data. Use the supplied
ContentValidationContext for actor, owner, locale, visibility, field path,
and public/preview decisions.Content::render() for live model/group compositions.Content::capture() and Content::renderSnapshot() when a publishing
consumer needs immutable copy.publishing: true, validate the merged placement values using owner
and group context; overrides may satisfy required fields. Freeze the
validator's normalized final payload in snapshot values and translations,
with empty overrides, so rendering cannot reapply stale values. Keep older
snapshots with separate overrides readable.ContentCompositionSnapshotBlockData with typed
ContentSchemaData; array hydration must restore the complete DTO graph.ContentCompositionSnapshotCast so consuming models
expose ContentCompositionSnapshotData, not untyped arrays.nvl:content:views:publish.ContentAuthorization; the default denies non-system actors unless an
explicit callback permits them.ContentBlockQueryScope on the authorization adapter when block
catalogs require actor/tenant query constraints. Treat published
private-to-public updates as Publish operations as well as edits.javascript, data, file, and vbscript URI schemes even when
consumer configuration attempts to allow them.nvl:content:doctor --strict --format=json, definition dry runs, Pest,
Pint, PHPStan at maximum strictness, nvl:data:types:check, database
matrices, and clean nvl/laravel-suite consumer tests on Laravel 13.nvl-core.owners and reference model classes in content capability configuration. Laravel getMorphClass() supplies the host-authored stored identity; declarations do not add global host morph mappings.getMorphClass() and remain deprecated in major 6; Doctor reports mismatches and stored identity drift without conversion.Run php artisan nvl:doctor --strict --format=json to combine checks from loaded NVL providers. Retain the package Doctor command for its detailed report; both paths reuse the package-owned inspection service.
locks.store and Core/Laravel inheritance; operation stores locks.definitions.store and locks.placements.store may override the shared store. Keep their existing lock durations.authorization.guard only to bare auth. Capability authorization remains required.Resolve all package tables through the table helper and canonical nvl-content.tables.*, connections through nvl-content.connection with Core/Laravel inheritance. Defaults use nvl_content_*; migration filenames include that package slug. Never silently adopt a matching table or generic migration filename. Run shared nvl:doctor --strict --format=json and the explicit nvl:schema:upgrade --package=content --claim-legacy --dry-run --format=json before upgrading owned legacy storage. Validate the complete plan and choose one migration owner. Preserve host records, constraint names and stored morph values. Deprecated config inputs last one major; canonical options take precedence.
Placement locks retain nvl:content:placement-owner: outside the tenant identity in every runtime mode. Definition synchronization keeps nvl:content:definitions:sync. Existing generic tenant lock entries are never acquired or removed.
Drain old mutation workers and maintenance processes, then wait for their outstanding lock leases to end before starting the new major across all nodes. Running old and new lock prefixes concurrently would create independent serialization domains. Restart workers after cutover; preserve host-selected stores and keys, and do not flush a shared cache to remove old NVL entries.
nvl-content configuration and publish only canonical nvl-<package>-<resource> tags. Keep logical package/tenant resource identifiers unchanged.nvl-core.compatibility.legacy_config package IDs and legacy_env explicitly; both default off. Canonical presence wins, including false/null/empty values. Legacy inputs are read without writing back and remain deprecated in major 6.NVL_<PACKAGE>_* variables only in config evaluation, then rebuild configuration caches and restart workers after cutover. Shared Laravel environment variables retain their names. Consult Core's versioned support/resources/global-names.json for all renames.global_aliases/legacy_routes package selections. Preserve collisions and use Doctor diagnostics; never grant generic permissions automatically or claim signed-link compatibility without the same authorization/signature checks.Nvl\Content\Contracts\*Contract interfaces for the 27 selected public Actions listed in the README; reuse existing equivalent contracts. Preserve the exact native execute parameters/defaults and DTO/model/vendor results.ContentContract for the complete 24-method application surface. Its scoped default resolves the existing scoped Content concrete through the supplied container, retaining contextual editor injection. The Content facade uses this same contract. Clear ContentContract::class with the facade’s clearResolvedInstance after late replacement and at scoped/application boundaries.Start with the package README Quickstart and Testing your app sections. Use nvl:install <package> for loaded-package common config publication; it does not enable features, run schema or refresh caches. Preserve native host owner keys/morph maps and selected host Auth and ownership defaults. Read full runtime defaults and publish advanced config only deliberately.
Inject the supported focused interfaces and preserve host bindings. Returned model handles do not permit package-table queries/writes outside documented capability/extension seams. Host tests may substitute contracts in Laravel's container, use shipped model factories (ordinary make may persist parents; withoutParents()->make is detached), and use Laravel effect fakes deliberately. Only Media/Stripe have dedicated provider/library fakes; do not invent a universal package fake. Settings InteractsWithSettings is definition-only. Host PHPStan may include vendor/nvl/core/support/consumer-audit.neon; no unpublished workbench command is a consumer requirement.
Read docs/events.md and the package README error table. Domain events use schemaVersion=1, model-free facts and actual source-connection commit callbacks; only six declared old Event suffix aliases remain deprecated through major 6. Migrate exact listeners/fakes and suffix wildcards, drain old queued payloads, rebuild event cache and restart workers. Delivery is not a durable outbox. The Core exception renderer is opt-in, JSON-only for respondable failures, with exactly message/code/context and host-selected locale. Do not expose diagnostics or reinterpret missing bindings as authorization denial.
Core package logging uses nvl/normal with CSV quiet by default, stable message keys and bounded context; incidents survive quiet. Do not mutate global logger context or log raw row/provider/content/credential payloads. Run only authorized project checks and report new acceptance as pending until actual output exists.
nvl-content.compiled_cache.enabled deliberately; required mode additionally needs required=true and a nonempty configured version (NVL_CONTENT_DEFINITIONS_VERSION).php artisan nvl:content:cache --cache-version=release-2026-10-07; the supplied token must match configuration and does not modify it. Never use the reserved Artisan --version option for this token.php artisan nvl:content:doctor --strict for all presets and cache diagnostics, refresh configuration caches when changed, then restart long-lived workers. php artisan nvl:content:clear removes the selected artifact. Required runtime mode fails closed on invalid or absent cache; optional mode may compile source.Ordinary Content installations keep the unscoped base models and require neither Ownership nor Tenancy. An owning application installs nvl/ownership and sets nvl-content.models.block to its concrete ContentBlock subclass. Return OwnershipDefinition::column(Company::class, 'company_id') or an exclusive native primary pivot from ownershipDefinition(); keep native query/persistence hooks, required casts and UUID identity. Add the actual owner FK or primary pivot in an application migration. There is no inferred actor, default company or generic owner pivot.
Declare host resource keys in nvl-content.ownership.owner_resources. Blocks/locales/revisions, placement hosts, block references and parent composition tuples must agree on the primary owner. Use canonical host models and package Actions; raw capability-pivot writes are unsupported. When a host combines HasContent and HasOwnership, select HasContent::delete insteadof HasOwnership; the Content deletion service holds the ownership lease and rolls back cleanup on cancellation.
Run the reviewed content adoption participant with explicit mappings for every historical row before enabling traffic. Persisted adoption markers keep prepared/adopted storage closed if its runtime is disabled or removed. Definition code remains an unscoped deployment catalog; its mutations require explicit central execution in an ownership-enabled installation. Dynamic reference resolvers implement OwnershipSafeContentReferenceResolver and return the exact persisted native record for the requested identifier.
New snapshots use format 3 with the complete native owner reference and a canonical checksum. Snapshot data is never an admission capability: rendering reloads the canonical owner and applies current authorization. Legacy snapshots require reviewed migration, with no automatic render fallback. ContentStorage and ContentSnapshotIntegrity are focused package-integration contracts; snapshot hashing alone does not authorize a read, import or migration. Catalog copies verify a current recipient grant, rebind the snapshot identity and require explicit Media mappings; application reference fields are rejected until a dedicated importer provides total target mappings.
How can I help you explore Laravel packages today?