Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
nvl-content
nvl/content
claude-code
media
cms
translations
headless
content
laravel
blocks

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.

Install
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.

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.
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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings