# Structured Content Library — Improvement & Growth Plan

> Package: capell-app/structured-content-library · Kind: package · Tier: free · Product group: Capell Foundation · Bundle: foundation · Status: Complete

## 1. Snapshot

Structured Content Library provides one package-owned Eloquent model (`StructuredContentItem`, table `structured_content_items` — `src/Models/StructuredContentItem.php`) for nine fixed reusable business-content concepts driven by `StructuredContentType` (case study, testimonial, team member, service, FAQ, resource, partner, location, logo — `src/Enums/StructuredContentType.php`). Content is stored as a flat row (`title`, `slug`, `summary`, `content`) plus a single typed `payload` JSON cast to `StructuredContentPayloadData` (19 optional scalar fields — `src/Data/StructuredContentPayloadData.php`). Surfaces are `admin` (one Filament resource, `src/Filament/Resources/StructuredContentItems/StructuredContentItemResource.php`) and `shared`; there is **no** frontend surface (`providers.frontend: []`). Key Actions: `CreateStructuredContentItemAction`, `UpdateStructuredContentItemAction`, `ListStructuredContentItemsAction`, `BuildPublicStructuredContentItemsAction`, `BuildStructuredContentSectionsAction`, `ImportStructuredContentItemsAction`, and the HTML guard `EnsurePortableContentHtmlAction`. Deps: `capell-app/admin`, `capell-app/core`, `filament/support`, `lorisleiva/laravel-actions`, `spatie/laravel-data`, `spatie/laravel-package-tools` (`composer.json`). Marketplace summary now names the concrete reusable content types and theme-safe rendering promise. Screenshot media is open again: `capell.json` lists only the extension card because the prior product captures were illustrative mockups, not Capell runner output.

## 2. Improvements (existing functionality)

- **Screenshot contract reopened and runner-app install blocked.** `docs/screenshots.json` still declares the admin list, create form, and theme-rendering captures, but the previous mock PNG/SVG assets were removed from `docs/screenshots/` and `capell.json`. The Capell runner dry-run validates the entries, but the prepared screenshot app route list has no Structured Content admin route and the package remains absent from the runner app Composer requirements, so real captures must start by installing `capell-app/structured-content-library` in the runner app or adding another supported install path. After that, seed the Structured Content admin resource and a real theme fixture consuming published records before promotion. — positioning — `docs/screenshots.json`, `capell.json` — **S**

- **Done/Shipped: Add a unique index on `(type, site_id, slug)`.** The package migration now repairs duplicate scoped non-null slugs before creating `structured_content_type_site_slug_unique`, and import coverage proves generated slugs are used for dedup when incoming rows omit `slug`. — data integrity — `database/migrations/2026_06_04_000001_add_unique_scope_slug_index_to_structured_content_items_table.php`, `tests/Integration/Actions/ImportStructuredContentItemsActionTest.php`, `tests/Integration/Models/StructuredContentItemTest.php` — **S**

- **Done/Shipped: Implemented the health check.** `StructuredContentLibraryHealthCheck` now probes the storage table, `CapellCore` model registration, protected-table registration, and admin resource contribution with translated labels/messages/remediations. Focused health coverage pins the probe set and failure messages. — manifest/behaviour alignment, diagnostics value — `src/Health/StructuredContentLibraryHealthCheck.php`, `resources/lang/en/health.php`, `tests/Integration/Health/StructuredContentLibraryHealthCheckTest.php` — **S**

- **Done/Shipped: Auto-derive `published_at` when publishing without a date.** Create defaults `published_at` for new published records, and update now defaults to `now()` only when transitioning to `Published` with no supplied date while preserving existing timestamps on published and non-publish updates. Evidence: `UpdateStructuredContentItemActionTest` covers the publish transition, preserving an existing timestamp, and no timestamp change for non-publish transitions. — correctness/UX — `src/Actions/CreateStructuredContentItemAction.php`, `src/Actions/UpdateStructuredContentItemAction.php` — **S**

- **Done/Shipped: Sanitize `summary` through the portable-HTML guard.** Create and update actions now pass `summary` through `EnsurePortableContentHtmlAction` with field-specific validation errors, matching the `content` portability contract before values reach public DTOs. Evidence: `CreateStructuredContentItemActionTest` and `UpdateStructuredContentItemActionTest` persist safe portable summary HTML and reject script tags plus inline event handlers without storing unsafe values. — public-output safety + contract consistency — `src/Actions/CreateStructuredContentItemAction.php`, `src/Actions/UpdateStructuredContentItemAction.php`, `src/Actions/EnsurePortableContentHtmlAction.php` — **S**

- **Done/Shipped: Make the `payload` form fields type-aware.** The Filament form now scopes payload inputs with `Get $get('type')` + `->visible()` and a tested `payloadFieldsForType()` map, so each reusable content type shows only relevant payload fields. Evidence: `StructuredContentItemResourceTest` covers FAQ, Testimonial, Location, and the full payload-field union. — admin UX, reduces data-entry error — `src/Filament/Resources/StructuredContentItems/StructuredContentItemResource.php`, `tests/Unit/Filament/StructuredContentItemResourceTest.php` — **M**

- **Done/Shipped: Add an `archived()`/trashed table filter and surface soft-deletes.** The table now includes `TrashedFilter`, edit/delete/restore record actions, delete/restore/force-delete toolbar actions, and the edit page exposes restore/delete/force-delete header actions. — admin UX, data safety — `src/Filament/Resources/StructuredContentItems/StructuredContentItemResource.php`, `src/Filament/Resources/StructuredContentItems/Pages/EditStructuredContentItem.php` — **S**

- **Done/Shipped: Add slug-uniqueness handling in write actions.** `ResolveUniqueStructuredContentSlugAction` now reserves soft-deleted slugs and suffixes collisions per type/site scope. Evidence: create/update action coverage proves generated slug suffixing, same-slug allowance across site scopes, owner update preservation, and soft-deleted slug reservation. — data integrity, theme URL stability — `src/Actions/ResolveUniqueStructuredContentSlugAction.php`, `src/Actions/CreateStructuredContentItemAction.php`, `src/Actions/UpdateStructuredContentItemAction.php`, `tests/Integration/Actions/CreateStructuredContentItemActionTest.php`, `tests/Integration/Actions/UpdateStructuredContentItemActionTest.php` — **M**

- **Done/Shipped: Expose `sort_order` editing in the table.** The resource table is now `->reorderable('sort_order')` while the existing metadata form continues to expose direct `sort_order` and `published_at` edits. — admin UX — `src/Filament/Resources/StructuredContentItems/StructuredContentItemResource.php` — **S**

## 3. Missing Features (gaps)

Manifest `capabilities[]`: `structured-content-library`, `structured-content-public-adapter`, `structured-content-section-adapter`, `structured-content-theme-adapter`, `structured-content-import`.

- **Done/Shipped: Section/theme adapter capabilities are no longer advertised as delivered.** The unwired `structured-content-section-adapter` and `structured-content-theme-adapter` capability strings were removed from `capell.json`, while `content-section-adapter` and `theme-adapter` are now listed under `contributionTraceability.deferredContributions`. `StructuredContentLibraryProviderTest` preserves the shipped in-process `BuildStructuredContentSectionsAction` contract and asserts the adapter claims remain deferred until real integrations exist. — `capell.json` (`capabilities`, `contributionTraceability`), `tests/Unit/Providers/StructuredContentLibraryProviderTest.php`

- **Custom / extensible content types (differentiator).** `StructuredContentType` is a hard-coded 9-case enum. A structured-content library's headline value vs. WordPress is _user-defined_ content types. There is no registry (`CapellCore::registerStructuredContentType(...)`-style) for downstream packages/themes to add a type. This is the single biggest growth lever. — `src/Enums/StructuredContentType.php`

- **Repeatable / nested field groups (table-stakes for structured content).** `payload` is a single flat DTO of scalars. There is no support for repeatable groups (e.g. a service with N feature bullets, a team member with N social links, an FAQ _set_). Today this forces one row per item with no grouping primitive. — `src/Data/StructuredContentPayloadData.php`

- **Relations / references between items (differentiator).** No way to reference one item from another (e.g. testimonial → team member, case study → service). No `references` column, morph, or relation beyond `site`. — `src/Models/StructuredContentItem.php`

- **Media / image handling (table-stakes).** Payload carries only `image_alt` / `logo_alt` strings — no image URL, asset id, or media-library integration. "Logo" and "team member" types are effectively text-only. — `src/Data/StructuredContentPayloadData.php`

- **Per-field validation beyond HTML portability (table-stakes).** Only `title` (required) and `content`/`summary` (portability) are validated in actions. `url`/`email`/`phone`/`country_code` payload fields have Filament-level `->url()`/`->email()` hints but **no** action-level validation, so imports and programmatic writes accept anything. — `src/Actions/CreateStructuredContentItemAction.php`, `src/Data/StructuredContentPayloadData.php`

- **Read API exposure (gap given the "portable" pitch).** `composer.json` requires `spatie/laravel-data` and the public DTO exists, but there is no JSON/REST endpoint or API resource (`grep` finds no `JsonResource`/`ApiResource`). "Portable" currently means "in-process Action only". A signed/public read endpoint would make the package genuinely portable to JS themes and headless consumers. — package-wide

- **Versioning / revision history (differentiator).** No revisions, no draft-vs-published divergence, no audit trail. Updates overwrite in place. Sibling packages (e.g. KnowledgeBase article versions) model this. — `src/Models/StructuredContentItem.php`

- **Export counterpart to import.** `ImportStructuredContentItemsAction` exists with no `ExportStructuredContentItemsAction`, undercutting the "portable/migration tool" positioning and round-trip demo-kit story. — `src/Actions/`

- **Localised content (i18n gap).** Manifest declares `cacheSafety.variesBy: ["site","locale"]`, but the model has no per-locale content columns or translation strategy — only a `site_id`. Cached output varying by locale would serve identical text across locales. — `src/Models/StructuredContentItem.php`, `capell.json`

## 4. Issues / Risks

- **Done/Shipped: public payload values are sanitized at the package boundary.** `BuildPublicStructuredContentPayloadAction` now decodes HTML entities before stripping dangerous blocks/tags, keeps public payload output as plain text, and drops unsafe payload URLs such as `javascript:`. Evidence: `BuildPublicStructuredContentItemsActionTest` proves raw and entity-encoded HTML/JS is removed before public serialization or deliberately raw Blade rendering. — `src/Actions/BuildPublicStructuredContentPayloadAction.php`, `tests/Integration/Actions/BuildPublicStructuredContentItemsActionTest.php`

- **Done/Shipped: Critical health check is real.** Diagnostics now fail on missing storage table, missing Core model registration, missing protected-table registration, or missing admin resource contribution. — `src/Health/StructuredContentLibraryHealthCheck.php`, `tests/Integration/Health/StructuredContentLibraryHealthCheckTest.php`

- **Done/Shipped: unique constraint and null-slug import dedup are covered.** Existing duplicate scoped non-null slugs are repaired before the unique index is added, and imports without an explicit `slug` deduplicate against the generated slug. Note: direct future inserts with `site_id = NULL` may still depend on database-specific nullable unique-index semantics. — `database/migrations/2026_06_04_000001_add_unique_scope_slug_index_to_structured_content_items_table.php`, `tests/Integration/Actions/ImportStructuredContentItemsActionTest.php`

- **Done/Shipped: Cache safety is wired for public builders.** `StructuredContentCache` now caches batched public structured-content DTOs by type set, site, locale, and version; model save/delete/restore events advance the version and flush tagged stores; and the provider registers `StructuredContentItem` as a frontend cache invalidation dependency when `CacheInvalidationRegistry` is installed. Evidence: `BuildStructuredContentSectionsActionTest` proves cached second renders do not hit `structured_content_items` and that new records invalidate cached output; `StructuredContentLibraryProviderTest` covers frontend registry wiring. — `src/Support/StructuredContentCache.php`, `src/Actions/BuildPublicStructuredContentItemsForTypesAction.php`, `src/Actions/BuildPublicStructuredContentItemsAction.php`, `src/Providers/StructuredContentLibraryServiceProvider.php`, `tests/Integration/Actions/BuildStructuredContentSectionsActionTest.php`, `tests/Unit/Providers/StructuredContentLibraryProviderTest.php`

- **Done/Shipped: Section rendering is batched against the query budget.** `BuildStructuredContentSectionsAction` resolves all requested types first, fetches public items through the batched builder once, applies per-section limits locally, and reuses cached output on subsequent renders. Evidence: package coverage asserts a three-section render uses no more than one `structured_content_items` select and cached rerenders use zero. — `src/Actions/BuildStructuredContentSectionsAction.php`, `src/Actions/BuildPublicStructuredContentItemsForTypesAction.php`, `tests/Integration/Actions/BuildStructuredContentSectionsActionTest.php`

- **Test gaps.** 49 package tests pass in the current structured-content-library slice. Covered: data mapping, enum labels, provider/manifest declarations, resource page wiring, payload field type mapping, Filament create/edit save-path Action delegation, CRUD actions, slug uniquing, portable-HTML rejection (create + update), summary portable-HTML validation, list ordering/site filtering, build-public + limit, public payload sanitization, batched/cached build-sections, cache invalidation, provider cache registry wiring, import create/update/skip, model casts/table install. **Not covered:** `archived()`/`draft()` scopes; soft-delete visibility behaviour; `published_at`-in-future exclusion edge; import with null slug. No arch test asserting public-DTO field whitelist. — `tests/`

- **`payload` form fields use `dehydrated` defaults implicitly.** All payload sub-fields are always-present `TextInput`/`Textarea`; an empty payload still serialises 19 null keys into `StructuredContentPayloadData`. Harmless but bloats stored JSON and public output. — `src/Filament/Resources/.../StructuredContentItemResource.php`

- **i18n: only `en` translations.** `resources/lang/en/{admin,status,type,validation}.php` only. All labels are translation-keyed (good), but no other locale ships, and content itself is single-locale (see §3). — `resources/lang/`

## 5. Marketplace & Positioning

This is a **free / foundation / bundled** package (`product.tier: free`, `bundle: foundation`, `commercial.proposedLicense: free`, `requestedCertification: first-party`). Its strategic role is to be the typed-content substrate other paid packages and themes consume — so its value is measured by adoption depth, not standalone revenue.

- **`marketplace.summary` critique.** Shipped: _"A typed content library for testimonials, case studies, team members, FAQs, services and more — reusable, theme-safe records your themes render anywhere."_ Naming the concrete types is the hook; "theme-safe" signals the portability guard that differentiates it from a free-text block.

- **`composer.json` description critique.** Shipped: _"Typed, portable content records (testimonials, case studies, team, FAQs, services…) that Capell themes and packages render safely."_ The manifest description keeps the longer nine-type list while Composer carries the shorter marketing line.

- **free/bundle vs premium.** Correctly free/foundation — it should be the gravity well that makes premium packages (content-sections, themes, automation) more valuable. Keep the model/Actions free; the _premium_ upsell surface is layered features: revisions, API exposure, custom-type registry, media handling (§3). Do not paywall the core read Actions — that would break the foundation role.

- **Screenshot / media status.** `capell.json` currently lists only the extension card. Required captures remain blocked until the screenshot runner app installs the package: (1) the real Filament list table with type/status badges, (2) the create form showing type + payload fields, and (3) a route-backed theme fixture rendering published items to prove the "theme renders the data" story.

- **Platform-pitch contribution.** Structured/typed content is a top differentiator vs. WordPress (which leans on free-text blocks + plugins for custom types). This package is the credibility anchor for that pitch — but only once **custom content types** (§3) and a **wired section/theme adapter** (§3) exist. Today the pitch is partly aspirational: the adapters are manifest strings, and the type set is fixed.

- **Keywords / tags (8–12):** `structured-content`, `typed-content`, `content-modeling`, `reusable-content`, `testimonials`, `case-studies`, `faqs`, `team-members`, `headless-content`, `cms-foundation`, `theme-content`, `portable-content`.

## 6. Prioritized Roadmap

| Item                                                                                                                                                                                                                                                            | Bucket | Effort | Impact                      | Section ref |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | --------------------------- | ----------- |
| Done/Shipped: Sanitize `summary` via portable-HTML guard. Evidence: `CreateStructuredContentItemActionTest` and `UpdateStructuredContentItemActionTest` persist safe portable summary HTML and reject script tags plus inline event handlers.                   | Done   | S      | High (public safety)        | §2          |
| Done/Shipped: Implement real health check (drop stub). Evidence: storage table, Core model, protected table, and admin resource diagnostics are translated and covered.                                                                                         | Done   | S      | High (false-green critical) | §2, §4      |
| Done/Shipped: Add unique index on `(type, site_id, slug)` + fix null-slug import dedup. Evidence: migration deduplicates legacy scoped slugs before adding the unique index, and import tests prove omitted slugs still deduplicate through the generated slug. | Done   | S      | High (data integrity)       | §2, §4      |
| Done/Shipped: Wire section/theme adapter OR mark capabilities deferred. Evidence: `capell.json` removes unwired adapter capabilities, marks adapter contributions deferred, and `StructuredContentLibraryProviderTest` asserts the truthful manifest contract.  | Done   | M      | High (manifest honesty)     | §3          |
| Done/Shipped: Document/assert payload public-output escaping contract (+ XSS test). Evidence: public payload text decodes entities before stripping dangerous markup, unsafe URLs are dropped, and tests cover raw/encoded HTML/JS.                             | Done   | S      | High (public safety)        | §4          |
| Done/Shipped: Default `published_at` to now() on publish transition. Evidence: `UpdateStructuredContentItemActionTest` covers the publish transition, preserving an existing timestamp, and no timestamp change for non-publish transitions.                    | Done   | S      | Med (correctness)           | §2          |
| Done/Shipped: Add Filament test for admin save path (Action delegation). Evidence: fixture page tests call the create/edit protected save paths and assert persisted Action behavior.                                                                           | Done   | S      | Med (test gap)              | §4          |
| Done/Shipped: Type-aware payload form fields (`->visible()` by type). Evidence: payload map coverage scopes all 19 payload fields across the nine content types.                                                                                                | Done   | M      | Med (admin UX)              | §2          |
| Done/Shipped: Trashed filter + restore/delete actions + reorderable table. Evidence: resource table exposes `TrashedFilter`, delete/restore bulk and record actions, and `->reorderable('sort_order')`; edit page exposes restore/delete/force-delete actions.  | Done   | S      | Med (admin UX/safety)       | §2          |
| Done/Shipped: Slug collision uniquing in write actions. Evidence: create/update coverage proves scoped suffixing, owner preservation, cross-site allowance, and soft-deleted slug reservation.                                                                  | Done   | M      | Med (URL stability)         | §2          |
| Done/Shipped: Implement declared caching + invalidation registry; benchmark budget. Evidence: batched public builder, versioned tagged cache, model-event invalidation, frontend registry wiring, and query-count tests.                                        | Done   | M      | Med (perf, manifest match)  | §4          |
| Custom/extensible content-type registry                                                                                                                                                                                                                         | Later  | L      | High (differentiator)       | §3, §5      |
| Repeatable/nested payload field groups                                                                                                                                                                                                                          | Later  | L      | High (differentiator)       | §3          |
| Read API (public JSON resource/endpoint)                                                                                                                                                                                                                        | Later  | M      | Med (portability story)     | §3, §5      |
| Revisions/versioning + Export action; media handling; i18n content                                                                                                                                                                                              | Later  | L      | Med (premium upsell)        | §3, §5      |
| Generate real Capell runner screenshots for admin list, create form, and theme rendering. Blocked until the runner app installs/seeds this package and exposes the Structured Content admin resource/theme fixture.                                             | Later  | S      | Med (positioning)           | §5          |
| Shipped: rewrite marketplace summary/description                                                                                                                                                                                                                | Done   | S      | Med (positioning)           | §5          |