Tags — Improvement & Growth Plan
Package: capell-app/tags · Kind: package · Tier: free · Product group: Capell Foundation · Bundle: foundation · Status: Draft
1. Snapshot
Section titled “1. Snapshot”Tags is a foundation package that provides a shared, site-scoped, translatable taxonomy layer for Capell content. It contributes one Filament admin resource (TagResource with list/create/edit pages and a PagesRelationManager), a reusable abstract SpatieTagsInput subclass (src/Filament/Components/Forms/TagsInput.php), a HasTags model concern (src/Models/Concerns/HasTags.php), Tag/Taggable models, one install Action + command (capell:tags-install), and a single migration that augments Spatie’s tags/taggables tables (database/migrations/2026_05_10_190872_01_alter_tags_table.php). It is built on spatie/laravel-tags + filament/spatie-laravel-tags-plugin; it owns no frontend surface (surfaces: ["admin","console"]). Its sole real-world consumer in this monorepo is Blog — Blog subclasses TagsInput, renders tag landing pages via Tag::getUrl(), builds the tags sitemap, and drives all public taxonomy UX. capell.json now describes a shared multilingual, multi-site taxonomy and promotes only the styled create/edit tag form captures until index, relation-manager, and host TagsInput screenshots are recaptured.
Completed Improvement Slices
Section titled “Completed Improvement Slices”- 2026-06-03: Rewrote composer/marketplace copy, reconciled manifest screenshots with shipped assets, and replaced the version-only health check with real diagnostics.
- 2026-06-04: Bound admin tag type selection to
TagTypeEnum, updated factories to generate only enum-backed types, declared taxonomy capabilities, and added atagscache tag to the manifest.
2. Improvements (existing functionality)
Section titled “2. Improvements (existing functionality)”Prioritized.
-
Tag
typeis now enum-backed. —TagForm::typeSelect()uses an enum-backedSelect,TagTypeEnumimplements labels, and the factory now emits only enum case values. Keep the smoke tests as the guard against free-text drift returning. —src/Filament/Resources/Tags/Schemas/TagForm.php,src/Enums/TagTypeEnum.php,database/factories/TagFactory.php— S -
Done/Shipped: make
statusgate public visibility. —Tagalready exposesenabled()throughHasStatus, and Blog’s publicTagLoadernow applies it to page tag chips, tag lists, and tag-page lookup so disabled tags no longer render through Blog public surfaces. —src/Models/Tag.php,packages/blog/src/Support/Loader/TagLoader.php— M -
Done/Shipped: resolve the
workspace_idownership boundary. — Tags owns the taxonomy storage columns ontagsandtaggables, exposesworkspace_idas fillable/cast model state, and documents that Publishing Studio owns workspace lifecycle behavior while Tags owns taxonomy assignment storage. Dropping the columns would be migration-hostile for existing installs, so the package now makes the boundary explicit. —database/migrations/2026_05_10_190872_01_alter_tags_table.php,src/Models/Tag.php,src/Models/Taggable.php,README.md,docs/overview.md— M -
Promote tag landing-page URL logic out of an undeclared contract. —
Tag::getUrl(Page $tagPage, Language $language)lives in Tags but is only ever called from Blog (TagsSitemap,BlogTagLinkData). It assumes apageUrlrelation and*wildcard substitution that is a Blog convention. Either move it to Blog, or formalise it as a documented public API with its own test in this package (currently untested here). —src/Models/Tag.php:211— S -
Done/Shipped: justify the
publishing-studiohard requirement. — Tags keeps Publishing Studio as a required dependency because taxonomy assignments need workspace-aware storage. The docs now state the split: Tags ownsworkspace_idstorage on taxonomy records and Publishing Studio owns workspace lifecycle behavior. —composer.json,capell.json,README.md,docs/overview.md— M -
Composer description and keywords shipped. — The composer description and keywords now describe shared multilingual tagging, site scoping, polymorphic taggable relationships, reusable Filament input, and taxonomy positioning. —
composer.json— S -
Reopened: manifest screenshots need populated recapture. — The manifest now lists only the styled create/edit tag form screenshots. Empty index/relation-manager captures and unrelated host dashboard captures were demoted from buyer-facing media. —
capell.json,docs/screenshots.json— S -
Health check shipped. —
TagsHealthChecknow probes table existence, the configured package tag model, install status, and admin resource registration. —src/Health/TagsHealthCheck.php— S -
Index the
taggables.taggable_type+typequery path. — Suggestion queries andfindFromStringForSitefilter ontype+site_id+name->locale;TagLoaderfilterstype+ counts taggables. The migration indexesfeatured,status,workspace_id,site_idbut nottype. Add atags(type, site_id)composite index to keep theadminQueryBudget: 40realistic at scale. —database/migrations/2026_05_10_190872_01_alter_tags_table.php— S -
Set a real
adminQueryBudget/cache posture for the index page. —TagsTableeager-loadssite,withTranslatedLocales, and ataggables_countper row; with the locale-flags column and badge this is several queries. Validate against the declared budget and addcacheTags/invalidation if tag lists are reused on the frontend by consumers. —src/Filament/Resources/Tags/Tables/TagsTable.php,capell.jsonperformance— M
3. Missing Features (gaps)
Section titled “3. Missing Features (gaps)”capell.json now declares taxonomy, multilingual, site-scoped, polymorphic taggable, and reusable-input capabilities. Remaining gaps below tie back to taxonomy table-stakes vs differentiators.
- Done/Shipped: polymorphic tagging across content types.
TagModelRegistrar::registerTaggable()now gives consumer packages a first-class helper for registering a model’stagsmorph relation plus optional inverse relation onTag, Blog uses it for Article/Page/Section, and the manifest declarestags-taggable-content. - Tag types/groups as managed vocabulary (table-stakes). Admin-created tags now use
TagTypeEnum, but there is still no package-extensible type registry or notion of tag groups (e.g. “Genre”, “Mood”) beyond the flattypestring. ATagGroupconcept or registered type vocabulary is the obvious foundation feature. - Done/Shipped: merge / rename / dedupe.
MergeTagsActionmoves taggable assignments from compatible duplicate source tags into a target tag, deletes duplicate pivots, and removes the source tags. The Tags admin index exposes this through a translated bulk merge action with a target-tag selector, giving editors a safe cleanup path for taxonomy hygiene. —src/Actions/MergeTagsAction.php,src/Filament/Resources/Tags/Tables/TagsTable.php,resources/lang/en/generic.php - Tag landing pages as an owned surface (differentiator).
getUrl()exists but tag pages are entirely a Blog feature. A foundation “tag archive” page type/route that any taggable content can opt into would make Tags a platform primitive rather than a Blog appendage. - Done/Shipped: tag cloud / related-by-tag helpers.
BuildTagCloudActionreturns weightedTagCloudItemDatafrom enabled tags and taggable counts, whileFindRelatedTaggablesActionreturns hydrated related records ranked by shared tags. These are Tags-owned render helpers that consumers can call before public Blade rendering, keeping taxonomy queries out of views. —src/Actions/BuildTagCloudAction.php,src/Actions/FindRelatedTaggablesAction.php,src/Data/TagCloudItemData.php,src/Data/RelatedTaggableData.php,capell.json - Multi-locale tag UX is partial. Storage is translatable (
name/slugJSON, per-locale fallbacks ingetAttributeValue), and the admin has a locale switcher — good. But there is no bulk “translate all tags” workflow and no guard that a tag has a slug in every site language. - Slug management (table-stakes, mostly present). Slugs auto-generate on create/replicate via
SlugGenerator; gap is no uniqueness validation per(type, site_id, locale)and no redirect handling when a slug changes (orphans existing tag-page URLs). - Featured tags surface (declared-but-inert).
featuredboolean exists with an admin toggle but no consumer reads it. Either expose a “featured tags” query helper or treat it as host-defined and document it.
4. Issues / Risks
Section titled “4. Issues / Risks”- Typed-taxonomy drift fixed for admin-created tags.
TagFormnow constrainstypethroughTagTypeEnum, and factories emit enum case values. Remaining work: decide whether host packages can register additional types through a future type registry. —src/Filament/Resources/Tags/Schemas/TagForm.php,database/factories/TagFactory.php,src/Enums/TagTypeEnum.php - Closed:
statustoggle has public effect. Blog’s public tag loader appliesenabled()to page tag chips, tag archive lists, chunked/static-site tag queries, and tag-page lookup. —packages/blog/src/Support/Loader/TagLoader.php - Closed:
workspace_idownership is explicit. Tags ownsworkspace_idstorage ontagsandtaggables, exposes it on both models, and documents Publishing Studio as the lifecycle owner for workspace behavior. —database/migrations/2026_05_10_190872_01_alter_tags_table.php,src/Models/Tag.php,src/Models/Taggable.php,README.md,docs/overview.md - Polymorphic integrity / deletion behaviour unverified. README and overview both flag: “Deletion behaviour for taggables should be verified before removing shared tags.”
Taggablehastimestamps = falseand no cascade declared in this package (relies on Spatie defaults). Deleting aTagshared across sites/types could strandtaggablesrows or remove tags still in use elsewhere — no test covers cross-consumer deletion. —src/Models/Taggable.php,src/Models/Tag.php - Public output safety is consumer-dependent, untested here. Tags has no public Blade, but
Tag::getUrl()and translatedname/slugflow into Blog’s public rendering. No test in this package proves anonymous output excludes admin-only data (it’s deferred entirely to Blog). For a foundation package whose output reaches the frontend through others, an output-safety contract test is warranted. —src/Models/Tag.php - Closed: cache safety now declares taxonomy invalidation sources.
capell.jsonexposestagscache tags plusTagandTaggablesave/delete sources, so consumers can discover the taxonomy invalidation boundary instead of inferring it from Blog internals. —capell.jsonperformance.cacheSafety TagsHealthCheckshipped. The health check now covers the storage tables,tags.tag_model, package install status, and admin resource registration. Remaining opportunity: add a package-specific doctor command if Tags needs direct console diagnostics. —src/Health/TagsHealthCheck.php,src/Providers/TagsServiceProvider.php- Missing
typeindex vs declared admin query budget (performance).type-filtered queries (suggestions, loader, find-for-site) are unindexed. —database/migrations/2026_05_10_190872_01_alter_tags_table.php,capell.jsonperformance.adminQueryBudget - Test gaps. 11 test files cover model CRUD, site-scoping, translation fallback, global-tag reuse, Filament list/create/edit/relation-manager, and a boundary arch test. Not covered in-package:
TagPolicy(zero tests; site-scope authorisation logic is untested here),Tag::getUrl()(tested only in Blog), the free-texttypedivergence,TagsInputsuggestion site-scoping (accessibleSuggestionSiteIds),workspace_id, and shared-tag deletion integrity. —tests/ - Package-extensible type labels are not solved yet. Admin-created tag types now use translated
TagTypeEnumlabels, but host packages cannot register their own typed taxonomy labels without changing this package enum. —src/Enums/TagTypeEnum.php,src/Filament/Resources/Tags/Schemas/TagForm.php
5. Marketplace & Positioning
Section titled “5. Marketplace & Positioning”Tags is correctly positioned as free / foundation / bundled — it is plumbing that other paid content packages (Blog, Events, Search, Newsletter) build on. That is the right call; do not make it premium. Its commercial value is as a dependency magnet: everything taxonomy-shaped should route through it.
Current marketplace.summary (verbatim): “Tags adds tag management, taggable relationships, a reusable tags input, and model traits for Capell content.” — Accurate but feature-listy and inward (“model traits”, “taggable relationships” are developer jargon). It sells mechanics, not outcomes.
Original composer description: “Tags for Capell.” — This placeholder-grade copy has been replaced with the improved description below.
Improved summary (outcome-led): “One shared, multilingual, multi-site taxonomy for every Capell content type — tag articles, pages, and events from a single managed tag list with reusable tag inputs and per-site scoping.”
Improved composer description: “Shared multilingual tagging and taxonomy for Capell: site-scoped tags, polymorphic taggable relationships, and a reusable Filament tags input for Blog, Events, and other content packages.”
Platform-pitch contribution & cross-sell. Tags is the connective tissue of the content story: it is what lets Blog tag pages, Events categorisation, and Search faceting share one vocabulary instead of N siloed tag tables (this is exactly the README’s “instead of package-specific tag fields” pitch). Lead the bundle narrative with it: Blog/Events provide content, Tags unifies how it’s classified, Search/SEO turn that taxonomy into discoverability. Concrete cross-sell hooks to build and advertise: tag landing pages (→ SEO Suite sitemaps, already partially wired via TagsSitemap), related-by-tag and tag-cloud components (→ Blog/structured-content engagement), tag facets (→ Search). Each is a reason to install an adjacent paid package.
Screenshot/media gaps. Manifest now promotes only the create/edit tag form pair. Recapture a populated index, real relation manager, and Blog-mounted TagsInput form before marking media complete.
8–12 keywords/tags: capell, tags, taxonomy, tagging, categories, multilingual-tags, multi-site, polymorphic, content-classification, filament, laravel, spatie-tags.
6. Prioritized Roadmap
Section titled “6. Prioritized Roadmap”| Item | Bucket | Effort | Impact | Section ref |
|---|---|---|---|---|
Fix composer description + expand keywords | Done | S | Med | §2.6, §5 |
| Recapture populated index, relation-manager, and host TagsInput screenshots before promoting the remaining product media. Deferred until styled runner recapture; no implementation blocker. | Later | S | Med | §2.7, §5 |
Bind tag type to enum / remove dead TagTypeEnum cases | Done | S | High | §2.1, §4 |
Add real TagsHealthCheck probes (tables, tag_model, resource) | Done | S | High | §2.8, §4 |
Add tags(type, site_id) composite index | Done | S | Med | §2.9, §4 |
Add TagPolicy + getUrl() + deletion-integrity tests | Done | M | High | §4 |
Done 2026-06-06: resolve workspace_id ownership (wired in and documented) | Done | M | High | §2.3, §4 |
Done 2026-06-06: make status gate public visibility (model scope + Blog) | Done | M | High | §2.2, §4 |
Declare capabilities[] in manifest | Done | S | High | §3, §4 |
| Done 2026-06-06: declare cache invalidation sources in manifest | Done | S | High | §3, §4 |
Done 2026-06-06: provide registerTaggable() helper for consumers | Done | M | High | §3 |
Done 2026-06-06: improve marketplace summary to outcome-led copy | Done | S | Med | §5 |
| Done/Shipped: Tag merge / rename / dedupe admin action | Done | L | High | §3 |
| Done/Shipped: Tag-cloud + related-by-tag render helpers | Done | M | Med | §3, §5 |
| First-class tag landing-page surface / page type | Later | L | Med | §3, §5 |
| Slug uniqueness validation + change redirects | Later | M | Med | §3 |