Skip to content

Tags — Improvement & Growth Plan

Package: capell-app/tags · Kind: package · Tier: free · Product group: Capell Foundation · Bundle: foundation · Status: Draft

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.

  • 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 a tags cache tag to the manifest.

Prioritized.

  1. Tag type is now enum-backed.TagForm::typeSelect() uses an enum-backed Select, TagTypeEnum implements 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

  2. Done/Shipped: make status gate public visibility.Tag already exposes enabled() through HasStatus, and Blog’s public TagLoader now 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

  3. Done/Shipped: resolve the workspace_id ownership boundary. — Tags owns the taxonomy storage columns on tags and taggables, exposes workspace_id as 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

  4. 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 a pageUrl relation 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

  5. Done/Shipped: justify the publishing-studio hard requirement. — Tags keeps Publishing Studio as a required dependency because taxonomy assignments need workspace-aware storage. The docs now state the split: Tags owns workspace_id storage on taxonomy records and Publishing Studio owns workspace lifecycle behavior. — composer.json, capell.json, README.md, docs/overview.md — M

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

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

  8. Health check shipped.TagsHealthCheck now probes table existence, the configured package tag model, install status, and admin resource registration. — src/Health/TagsHealthCheck.php — S

  9. Index the taggables.taggable_type + type query path. — Suggestion queries and findFromStringForSite filter on type + site_id + name->locale; TagLoader filters type + counts taggables. The migration indexes featured, status, workspace_id, site_id but not type. Add a tags(type, site_id) composite index to keep the adminQueryBudget: 40 realistic at scale. — database/migrations/2026_05_10_190872_01_alter_tags_table.php — S

  10. Set a real adminQueryBudget/cache posture for the index page.TagsTable eager-loads site, withTranslatedLocales, and a taggables_count per row; with the locale-flags column and badge this is several queries. Validate against the declared budget and add cacheTags/invalidation if tag lists are reused on the frontend by consumers. — src/Filament/Resources/Tags/Tables/TagsTable.php, capell.json performance — M

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’s tags morph relation plus optional inverse relation on Tag, Blog uses it for Article/Page/Section, and the manifest declares tags-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 flat type string. A TagGroup concept or registered type vocabulary is the obvious foundation feature.
  • Done/Shipped: merge / rename / dedupe. MergeTagsAction moves 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. BuildTagCloudAction returns weighted TagCloudItemData from enabled tags and taggable counts, while FindRelatedTaggablesAction returns 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/slug JSON, per-locale fallbacks in getAttributeValue), 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). featured boolean 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.
  • Typed-taxonomy drift fixed for admin-created tags. TagForm now constrains type through TagTypeEnum, 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: status toggle has public effect. Blog’s public tag loader applies enabled() 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_id ownership is explicit. Tags owns workspace_id storage on tags and taggables, 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.” Taggable has timestamps = false and no cascade declared in this package (relies on Spatie defaults). Deleting a Tag shared across sites/types could strand taggables rows 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 translated name/slug flow 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.json exposes tags cache tags plus Tag and Taggable save/delete sources, so consumers can discover the taxonomy invalidation boundary instead of inferring it from Blog internals. — capell.json performance.cacheSafety
  • TagsHealthCheck shipped. 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 type index 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.json performance.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-text type divergence, TagsInput suggestion 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 TagTypeEnum labels, 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

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.

ItemBucketEffortImpactSection ref
Fix composer description + expand keywordsDoneSMed§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.LaterSMed§2.7, §5
Bind tag type to enum / remove dead TagTypeEnum casesDoneSHigh§2.1, §4
Add real TagsHealthCheck probes (tables, tag_model, resource)DoneSHigh§2.8, §4
Add tags(type, site_id) composite indexDoneSMed§2.9, §4
Add TagPolicy + getUrl() + deletion-integrity testsDoneMHigh§4
Done 2026-06-06: resolve workspace_id ownership (wired in and documented)DoneMHigh§2.3, §4
Done 2026-06-06: make status gate public visibility (model scope + Blog)DoneMHigh§2.2, §4
Declare capabilities[] in manifestDoneSHigh§3, §4
Done 2026-06-06: declare cache invalidation sources in manifestDoneSHigh§3, §4
Done 2026-06-06: provide registerTaggable() helper for consumersDoneMHigh§3
Done 2026-06-06: improve marketplace summary to outcome-led copyDoneSMed§5
Done/Shipped: Tag merge / rename / dedupe admin actionDoneLHigh§3
Done/Shipped: Tag-cloud + related-by-tag render helpersDoneMMed§3, §5
First-class tag landing-page surface / page typeLaterLMed§3, §5
Slug uniqueness validation + change redirectsLaterMMed§3