Plugin↔CMS link is a free-text title match with no referential integrity, and join semantics differ across pages #4
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
A plugin's association to a CMS is modeled as a free-text string (
source_cms/target_cms) matched against thecmsescollection'stitlefield at render time. There is no reference/relation and no validation, so:source_cms/target_cmsproduces an orphan: the plugin appears under no CMS and is excluded from that CMS's counts, with no error surfaced.source_cmsdiffers from a CMStitleonly by case/whitespace still renders a working link from its own page, yet is silently missing from that CMS's "Plugins from this CMS" list and from both counters.Evidence
app/seed/seed.json—plugins:source_cms(type: "string", required) +target_cms(type: "string");cmseskeyed by free-texttitle(type: "string"). No relation/reference field type.app/src/pages/cms/index.astro:18-22,31— counts keyed by exactp.data.source_cms/target_cms, looked up viacountBySource.get(c.data.title)(exact).app/src/pages/cms/[slug].astro:22-24—all.filter((p) => p.data.source_cms === cmsName)(exact).app/src/pages/plugins/[slug].astro:20-23—norm = s => s.trim().toLowerCase(); joinscmsSlugByTitle.get(norm(d.source_cms))(NORMALIZED), and only linkssource_cms, nottarget_cms.CLAUDE.mdEmdash gotchas):entry.data.idis the DB ULID for cross-collection refs; the current join uses display title instead.Options
source_cms/target_cmsan Emdash relation/reference field (confirm Emdash supports relation fields in seed.json), store by CMSentry.data.id(ULID), join on id across all pages. Requires a one-time seed.json migration (title→ULID) + admin-side enforcement.target_cmson the plugin page). (b) Build/dev validation that flags any plugin whosesource_cms/target_cmsdoesn't resolve to an existing CMS.ARCHITECTURE.md.Recommendation
Short term: Option 2a (one normalize helper shared by all three pages + link
target_cmsfrom the plugin page) to remove the exact-vs-normalized inconsistency, paired with 2b validation so orphans are caught not silently dropped. Treat Option 1 (true reference + ULID join + seed migration) as the durable fix, gated on an owner decision and Emdash relation-field support. Phase-0 status is irrelevant here — this is app render logic, not the chart.Surfaced by the deploy-hardening review pass; deferred from auto-fix because it's a data-model/schema design decision.
Interim fix shipped (Option 2a + 2b) — durable fix still open
Commit
90a4b80ondevelop. Removed the exact-vs-normalized inconsistency and added orphan detection; the schema change is deferred.app/src/lib/cms.ts— singlenormCms()match key +cmsSlugByTitle()/resolveCmsSlug(), now the only place the plugin↔CMS join is normalized.cms/index.astro+cms/[slug].astro— counts and "plugins from / targeting" lists use the normalized key (were exact-match), so a plugin can no longer link from its own page yet vanish from a CMS list over case/whitespace drift.plugins/[slug].astro— dropped the local normalize copy; now also linkstarget_cms(was source-only) for parity.warnOrphanCmsRefs()(2b) — logs anysource_cms/target_cmsthat resolves to no CMS, called fromcms/index.astro; silent orphans now surface in the server log.astro checkgreen.⏳ Remaining (Option 1, durable): make
source_cms/target_cmsa real Emdash relation field keyed by the CMS ULID + a one-timeseed.jsonmigration (title→ULID) + admin enforcement. Gated on confirming Emdash relation-field support and your sign-off on the schema/seed change. Keeping this issue open for that.