90a4b8088b
Interim fix for the free-text title-match footguns (issue #4); durable ULID reference + seed migration tracked separately. - New lib/cms.ts: single normCms() match key + cmsSlugByTitle / resolveCmsSlug, used by all three join sites so a plugin can no longer link from its own page yet vanish from a CMS list over case/whitespace drift. - cms/index.astro + cms/[slug].astro: counts and "plugins from / targeting" lists now use the normalized key (were exact-match). - plugins/[slug].astro: drop the local normalize copy; link target_cms too (was source-only) for parity. - warnOrphanCmsRefs(): logs any source_cms/target_cms that resolves to no CMS, so silent orphans surface in the server log.
66 lines
2.3 KiB
TypeScript
66 lines
2.3 KiB
TypeScript
// Plugin↔CMS relation helpers.
|
|
//
|
|
// A plugin references a CMS by the free-text `source_cms` / `target_cms`
|
|
// string, which is matched against a CMS entry's `title`. There is no
|
|
// reference field / foreign key yet (tracked in issue #4 — the durable fix is
|
|
// an Emdash relation field keyed by the CMS ULID + a seed migration). Until
|
|
// then, EVERY page that joins plugins to CMSes must use the SAME normalized
|
|
// match so a plugin can't link from its own page yet vanish from a CMS list
|
|
// over mere casing/whitespace drift.
|
|
|
|
interface CmsEntry {
|
|
id: string;
|
|
data: { title?: string | null };
|
|
}
|
|
interface PluginRefData {
|
|
source_cms?: string | null;
|
|
target_cms?: string | null;
|
|
}
|
|
|
|
/** Canonical CMS-match key: trim + lowercase. The ONE place case/whitespace is normalized. */
|
|
export const normCms = (s: string): string => s.trim().toLowerCase();
|
|
|
|
/** Map of normalized CMS title → CMS entry slug (`entry.id`, used in URLs). */
|
|
export function cmsSlugByTitle(cmses: CmsEntry[]): Map<string, string> {
|
|
return new Map(
|
|
cmses.filter((c) => c.data.title).map((c) => [normCms(c.data.title as string), c.id]),
|
|
);
|
|
}
|
|
|
|
/** Resolve a plugin's CMS reference to a CMS slug, or undefined if it matches no CMS. */
|
|
export function resolveCmsSlug(
|
|
ref: string | null | undefined,
|
|
index: Map<string, string>,
|
|
): string | undefined {
|
|
return ref ? index.get(normCms(ref)) : undefined;
|
|
}
|
|
|
|
/**
|
|
* Integrity check: log any plugin `source_cms`/`target_cms` that resolves to no
|
|
* CMS entry. These are silent orphans (excluded from CMS counts/lists with no
|
|
* user-visible error), so surfacing them in the server log is the cheap guard
|
|
* until the relation becomes a real reference (issue #4). Call from a page that
|
|
* already has both collections in scope; it does not fetch.
|
|
*/
|
|
export function warnOrphanCmsRefs(
|
|
plugins: Array<{ id: string; data: PluginRefData }>,
|
|
cmses: CmsEntry[],
|
|
): void {
|
|
const known = new Set(
|
|
cmses.filter((c) => c.data.title).map((c) => normCms(c.data.title as string)),
|
|
);
|
|
const orphans: string[] = [];
|
|
for (const p of plugins) {
|
|
for (const field of ["source_cms", "target_cms"] as const) {
|
|
const ref = p.data[field];
|
|
if (ref && !known.has(normCms(ref))) orphans.push(`${p.id}.${field}="${ref}"`);
|
|
}
|
|
}
|
|
if (orphans.length) {
|
|
console.warn(
|
|
`[cms] ${orphans.length} plugin CMS reference(s) match no CMS entry (issue #4):`,
|
|
orphans.join(", "),
|
|
);
|
|
}
|
|
}
|