Skip to content

Structured Content Library — Improvement & Growth Plan

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

Structured Content Library provides one package-owned Eloquent model (StructuredContentItem, table structured_content_itemssrc/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.

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

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

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

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

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

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

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

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

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

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

  • 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/

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.

ItemBucketEffortImpactSection 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.DoneSHigh (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.DoneSHigh (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.DoneSHigh (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.DoneMHigh (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.DoneSHigh (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.DoneSMed (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.DoneSMed (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.DoneMMed (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.DoneSMed (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.DoneMMed (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.DoneMMed (perf, manifest match)§4
Custom/extensible content-type registryLaterLHigh (differentiator)§3, §5
Repeatable/nested payload field groupsLaterLHigh (differentiator)§3
Read API (public JSON resource/endpoint)LaterMMed (portability story)§3, §5
Revisions/versioning + Export action; media handling; i18n contentLaterLMed (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.LaterSMed (positioning)§5
Shipped: rewrite marketplace summary/descriptionDoneSMed (positioning)§5