Add slug to cookie categories for stable consent identifiers

The category slug provides a stable, URL-safe key used as the
data-cookie-consent attribute value and consent data key, replacing
the fragile category name. This prevents breakage when categories
are renamed.

- Add slug column with unique-per-banner constraint and backfill migration
- Add Slug validator (lowercase alphanumeric + hyphens)
- Propagate slug through GraphQL schema, service layer, and snapshot
- Update console UI with slug field in create/edit forms
- Switch cookie-banner widget to use slug as consent data keys

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-23 19:07:11 +04:00
parent bb39daebc5
commit 165b9ad9d3
19 changed files with 218 additions and 22 deletions

View File

@@ -35,6 +35,7 @@ export interface CookieItem {
export interface Category {
name: string;
slug: string;
description: string;
kind: string;
cookies: CookieItem[];
@@ -174,7 +175,7 @@ export class CookieBannerClient {
const consentData: Record<string, boolean> = {};
for (const cat of cfg.categories) {
consentData[cat.name] = true;
consentData[cat.slug] = true;
}
this.recordConsent("ACCEPT_ALL", consentData);
@@ -185,7 +186,7 @@ export class CookieBannerClient {
const consentData: Record<string, boolean> = {};
for (const cat of cfg.categories) {
consentData[cat.name] = cat.kind === "NECESSARY";
consentData[cat.slug] = cat.kind === "NECESSARY";
}
this.recordConsent("REJECT_ALL", consentData);
@@ -196,7 +197,7 @@ export class CookieBannerClient {
const consentData: Record<string, boolean> = {};
for (const cat of cfg.categories) {
consentData[cat.name] = cat.kind === "NECESSARY" || !!categories[cat.name];
consentData[cat.slug] = cat.kind === "NECESSARY" || !!categories[cat.slug];
}
this.recordConsent("CUSTOMIZE", consentData);
@@ -244,8 +245,8 @@ export class CookieBannerClient {
const categoryCookies: Record<string, string[]> = {};
const categoryLabels: Record<string, string> = {};
for (const cat of this.config.categories) {
categoryCookies[cat.name] = cat.cookies.map((c) => c.name);
categoryLabels[cat.name] = cat.name;
categoryCookies[cat.slug] = cat.cookies.map((c) => c.name);
categoryLabels[cat.slug] = cat.name;
}
const texts = this.config.texts;

View File

@@ -59,6 +59,7 @@ export class ProboCategoryList extends ProboElement {
const wrapper = document.createElement("probo-category");
wrapper.setAttribute("name", cat.name);
wrapper.setAttribute("slug", cat.slug);
wrapper.setAttribute("kind", cat.kind);
wrapper.setAttribute("description", cat.description);
wrapper.setAttribute("cookies", JSON.stringify(cat.cookies));

View File

@@ -49,6 +49,7 @@ export class ProboCategoryToggle extends ProboElement {
if (!this.category || !this.root) return;
const name = this.category.categoryName;
const slug = this.category.categorySlug;
this.checkbox.setAttribute("aria-label", name);
const isRequired = this.category.kind === "NECESSARY";
@@ -59,14 +60,14 @@ export class ProboCategoryToggle extends ProboElement {
}
const draft = this.root.consentDraft;
this.checkbox.checked = !!draft[name];
this.checkbox.checked = !!draft[slug];
this.checkbox.addEventListener("change", this.handleChange);
if (this.root) {
this.root.addEventListener("probo-state", (e: Event) => {
const { state } = (e as CustomEvent).detail;
if (state === "panel" && this.checkbox && this.category && this.root) {
this.checkbox.checked = !!this.root.consentDraft[this.category.categoryName];
this.checkbox.checked = !!this.root.consentDraft[this.category.categorySlug];
}
});
}
@@ -74,6 +75,6 @@ export class ProboCategoryToggle extends ProboElement {
private handleChange = (): void => {
if (!this.checkbox || !this.category || !this.root) return;
this.root.updateDraft(this.category.categoryName, this.checkbox.checked);
this.root.updateDraft(this.category.categorySlug, this.checkbox.checked);
};
}

View File

@@ -21,6 +21,10 @@ export class ProboCategory extends ProboElement {
return this.getAttribute("name") ?? "Other";
}
get categorySlug(): string {
return this.getAttribute("slug") ?? this.categoryName.toLowerCase();
}
get kind(): string {
return this.getAttribute("kind") ?? "NORMAL";
}

View File

@@ -109,11 +109,11 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem
for (const cat of config.categories) {
if (cat.kind === "NECESSARY") {
draft[cat.name] = true;
} else if (existing && cat.name in existing) {
draft[cat.name] = existing[cat.name];
draft[cat.slug] = true;
} else if (existing && cat.slug in existing) {
draft[cat.slug] = existing[cat.slug];
} else {
draft[cat.name] = config.consent_mode === "OPT_OUT";
draft[cat.slug] = config.consent_mode === "OPT_OUT";
}
}