Add cookie_policy_url field to cookie banners

Introduce a required cookie_policy_url alongside the existing
privacy_policy_url (now optional) so banners can link directly to a
dedicated cookie policy — a compliance best practice recommended by
CNIL, ICO, and the EDPB. Existing rows are seeded from their current
privacy_policy_url value.

Both {{cookie_policy_link}} and {{privacy_policy_link}} placeholders
are supported independently in banner description translations.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-24 18:04:05 +04:00
parent 4982cebb9c
commit 598c6b112c
15 changed files with 142 additions and 56 deletions

View File

@@ -104,7 +104,7 @@ var defaultCategoryTranslationsByLanguage = map[string]map[string]struct {
var defaultUIStringsByLanguage = map[string]map[string]string{
"en": {
"banner_title": "Cookie Preferences",
"banner_description": "We use cookies to improve your experience and analyze site traffic. {{privacy_policy_link}}",
"banner_description": "We use cookies to improve your experience and analyze site traffic. {{cookie_policy_link}}",
"button_accept_all": "Accept all",
"button_reject_all": "Reject all",
"button_customize": "Customize",
@@ -116,12 +116,13 @@ var defaultUIStringsByLanguage = map[string]map[string]string{
"aria_hide_details": "Hide cookie details",
"aria_cookie_settings": "Cookie settings",
"privacy_policy_link_text": "Privacy Policy",
"cookie_policy_link_text": "Cookie Policy",
"placeholder_text": "This content requires {{category}} cookies.",
"placeholder_button": "Manage cookie preferences",
},
"fr": {
"banner_title": "Préférences de cookies",
"banner_description": "Nous utilisons des cookies pour améliorer votre expérience et analyser le trafic du site. {{privacy_policy_link}}",
"banner_description": "Nous utilisons des cookies pour améliorer votre expérience et analyser le trafic du site. {{cookie_policy_link}}",
"button_accept_all": "Tout accepter",
"button_reject_all": "Tout refuser",
"button_customize": "Personnaliser",
@@ -133,12 +134,13 @@ var defaultUIStringsByLanguage = map[string]map[string]string{
"aria_hide_details": "Masquer les détails des cookies",
"aria_cookie_settings": "Paramètres des cookies",
"privacy_policy_link_text": "Politique de confidentialité",
"cookie_policy_link_text": "Politique relative aux cookies",
"placeholder_text": "Ce contenu nécessite les cookies {{category}}.",
"placeholder_button": "Gérer les préférences de cookies",
},
"de": {
"banner_title": "Cookie-Einstellungen",
"banner_description": "Wir verwenden Cookies, um Ihre Erfahrung zu verbessern und den Website-Verkehr zu analysieren. {{privacy_policy_link}}",
"banner_description": "Wir verwenden Cookies, um Ihre Erfahrung zu verbessern und den Website-Verkehr zu analysieren. {{cookie_policy_link}}",
"button_accept_all": "Alle akzeptieren",
"button_reject_all": "Alle ablehnen",
"button_customize": "Anpassen",
@@ -150,12 +152,13 @@ var defaultUIStringsByLanguage = map[string]map[string]string{
"aria_hide_details": "Cookie-Details ausblenden",
"aria_cookie_settings": "Cookie-Einstellungen",
"privacy_policy_link_text": "Datenschutzrichtlinie",
"cookie_policy_link_text": "Cookie-Richtlinie",
"placeholder_text": "Dieser Inhalt erfordert {{category}}-Cookies.",
"placeholder_button": "Cookie-Einstellungen verwalten",
},
"es": {
"banner_title": "Preferencias de cookies",
"banner_description": "Utilizamos cookies para mejorar su experiencia y analizar el tráfico del sitio. {{privacy_policy_link}}",
"banner_description": "Utilizamos cookies para mejorar su experiencia y analizar el tráfico del sitio. {{cookie_policy_link}}",
"button_accept_all": "Aceptar todo",
"button_reject_all": "Rechazar todo",
"button_customize": "Personalizar",
@@ -167,6 +170,7 @@ var defaultUIStringsByLanguage = map[string]map[string]string{
"aria_hide_details": "Ocultar detalles de cookies",
"aria_cookie_settings": "Configuración de cookies",
"privacy_policy_link_text": "Política de privacidad",
"cookie_policy_link_text": "Política de cookies",
"placeholder_text": "Este contenido requiere cookies de {{category}}.",
"placeholder_button": "Gestionar preferencias de cookies",
},

View File

@@ -45,7 +45,8 @@ type (
OrganizationID gid.GID
Name string
Origin string
PrivacyPolicyURL string
PrivacyPolicyURL *string
CookiePolicyURL string
ConsentExpiryDays int
ConsentMode coredata.CookieConsentMode
}
@@ -62,6 +63,7 @@ type (
CookieBannerID gid.GID
Name *string
PrivacyPolicyURL *string
CookiePolicyURL *string
ConsentExpiryDays *int
ConsentMode *coredata.CookieConsentMode
DefaultLanguage *string
@@ -133,7 +135,8 @@ type (
Version int `json:"version"`
Language string `json:"language"`
DefaultLanguage string `json:"default_language"`
PrivacyPolicyURL string `json:"privacy_policy_url"`
PrivacyPolicyURL string `json:"privacy_policy_url,omitempty"`
CookiePolicyURL string `json:"cookie_policy_url"`
ConsentExpiryDays int `json:"consent_expiry_days"`
ConsentMode string `json:"consent_mode"`
ShowBranding bool `json:"show_branding"`
@@ -162,7 +165,8 @@ func (r *CreateCookieBannerRequest) Validate() error {
v.Check(r.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(255))
v.Check(r.Origin, "origin", validator.Required(), validator.Origin())
v.Check(r.PrivacyPolicyURL, "privacy_policy_url", validator.Required(), validator.URL())
v.Check(r.PrivacyPolicyURL, "privacy_policy_url", validator.URL())
v.Check(r.CookiePolicyURL, "cookie_policy_url", validator.Required(), validator.URL())
v.Check(r.ConsentExpiryDays, "consent_expiry_days", validator.Required(), validator.Min(1))
v.Check(r.ConsentMode, "consent_mode", validator.Required(), validator.OneOfSlice(coredata.CookieConsentModes()))
@@ -175,6 +179,7 @@ func (r *UpdateCookieBannerRequest) Validate() error {
v.Check(r.CookieBannerID, "cookie_banner_id", validator.Required(), validator.GID(coredata.CookieBannerEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(255))
v.Check(r.PrivacyPolicyURL, "privacy_policy_url", validator.URL())
v.Check(r.CookiePolicyURL, "cookie_policy_url", validator.URL())
v.Check(r.ConsentExpiryDays, "consent_expiry_days", validator.Min(1))
v.Check(r.ConsentMode, "consent_mode", validator.OneOfSlice(coredata.CookieConsentModes()))
v.Check(r.DefaultLanguage, "default_language", validator.OneOfSlice(SupportedLanguages))
@@ -367,6 +372,7 @@ func buildSnapshot(
return coredata.CookieBannerVersionSnapshot{
PrivacyPolicyURL: banner.PrivacyPolicyURL,
CookiePolicyURL: banner.CookiePolicyURL,
ConsentExpiryDays: banner.ConsentExpiryDays,
ConsentMode: string(banner.ConsentMode),
DefaultLanguage: banner.DefaultLanguage,
@@ -541,6 +547,7 @@ func (s *Service) CreateCookieBanner(
Origin: CanonicalizeOrigin(req.Origin),
State: coredata.CookieBannerStateActive,
PrivacyPolicyURL: req.PrivacyPolicyURL,
CookiePolicyURL: req.CookiePolicyURL,
ConsentExpiryDays: req.ConsentExpiryDays,
ConsentMode: req.ConsentMode,
ShowBranding: s.showBranding,
@@ -768,13 +775,20 @@ func (s *Service) UpdateCookieBanner(
return fmt.Errorf("cannot load cookie banner: %w", err)
}
consentChanged := req.PrivacyPolicyURL != nil || req.ConsentExpiryDays != nil || req.ConsentMode != nil || req.DefaultLanguage != nil
consentChanged := req.PrivacyPolicyURL != nil ||
req.CookiePolicyURL != nil ||
req.ConsentExpiryDays != nil ||
req.ConsentMode != nil ||
req.DefaultLanguage != nil
if req.Name != nil {
banner.Name = *req.Name
}
if req.PrivacyPolicyURL != nil {
banner.PrivacyPolicyURL = *req.PrivacyPolicyURL
banner.PrivacyPolicyURL = req.PrivacyPolicyURL
}
if req.CookiePolicyURL != nil {
banner.CookiePolicyURL = *req.CookiePolicyURL
}
if req.ConsentExpiryDays != nil {
banner.ConsentExpiryDays = *req.ConsentExpiryDays
@@ -1817,12 +1831,18 @@ func buildBannerConfig(
}
}
var privacyPolicyURL string
if snapshot.PrivacyPolicyURL != nil {
privacyPolicyURL = *snapshot.PrivacyPolicyURL
}
return &BannerConfig{
BannerID: banner.ID,
Version: version.Version,
Language: resolvedLang,
DefaultLanguage: defaultLang,
PrivacyPolicyURL: snapshot.PrivacyPolicyURL,
PrivacyPolicyURL: privacyPolicyURL,
CookiePolicyURL: snapshot.CookiePolicyURL,
ConsentExpiryDays: snapshot.ConsentExpiryDays,
ConsentMode: snapshot.ConsentMode,
ShowBranding: banner.ShowBranding,

View File

@@ -35,7 +35,8 @@ type (
Name string `db:"name"`
Origin string `db:"origin"`
State CookieBannerState `db:"state"`
PrivacyPolicyURL string `db:"privacy_policy_url"`
PrivacyPolicyURL *string `db:"privacy_policy_url"`
CookiePolicyURL string `db:"cookie_policy_url"`
ConsentExpiryDays int `db:"consent_expiry_days"`
ConsentMode CookieConsentMode `db:"consent_mode"`
ShowBranding bool `db:"show_branding"`
@@ -85,6 +86,7 @@ SELECT
origin,
state,
privacy_policy_url,
cookie_policy_url,
consent_expiry_days,
consent_mode,
show_branding,
@@ -136,6 +138,7 @@ SELECT
origin,
state,
privacy_policy_url,
cookie_policy_url,
consent_expiry_days,
consent_mode,
show_branding,
@@ -188,6 +191,7 @@ SELECT
origin,
state,
privacy_policy_url,
cookie_policy_url,
consent_expiry_days,
consent_mode,
show_branding,
@@ -246,6 +250,7 @@ SELECT
origin,
state,
privacy_policy_url,
cookie_policy_url,
consent_expiry_days,
consent_mode,
show_branding,
@@ -331,6 +336,7 @@ INSERT INTO cookie_banners (
origin,
state,
privacy_policy_url,
cookie_policy_url,
consent_expiry_days,
consent_mode,
show_branding,
@@ -345,6 +351,7 @@ INSERT INTO cookie_banners (
@origin,
@state,
@privacy_policy_url,
@cookie_policy_url,
@consent_expiry_days,
@consent_mode,
@show_branding,
@@ -362,6 +369,7 @@ INSERT INTO cookie_banners (
"origin": b.Origin,
"state": b.State,
"privacy_policy_url": b.PrivacyPolicyURL,
"cookie_policy_url": b.CookiePolicyURL,
"consent_expiry_days": b.ConsentExpiryDays,
"consent_mode": b.ConsentMode,
"show_branding": b.ShowBranding,
@@ -394,6 +402,7 @@ SET
name = @name,
state = @state,
privacy_policy_url = @privacy_policy_url,
cookie_policy_url = @cookie_policy_url,
consent_expiry_days = @consent_expiry_days,
consent_mode = @consent_mode,
show_branding = @show_branding,
@@ -411,6 +420,7 @@ WHERE
"name": b.Name,
"state": b.State,
"privacy_policy_url": b.PrivacyPolicyURL,
"cookie_policy_url": b.CookiePolicyURL,
"consent_expiry_days": b.ConsentExpiryDays,
"consent_mode": b.ConsentMode,
"show_branding": b.ShowBranding,

View File

@@ -30,7 +30,8 @@ import (
type (
CookieBannerVersionSnapshot struct {
PrivacyPolicyURL string `json:"privacy_policy_url"`
PrivacyPolicyURL *string `json:"privacy_policy_url,omitempty"`
CookiePolicyURL string `json:"cookie_policy_url"`
ConsentExpiryDays int `json:"consent_expiry_days"`
ConsentMode string `json:"consent_mode"`
DefaultLanguage string `json:"default_language"`

View File

@@ -0,0 +1,8 @@
-- Add cookie_policy_url (required) and make privacy_policy_url optional.
-- Seed cookie_policy_url from existing privacy_policy_url for data continuity.
ALTER TABLE cookie_banners ADD COLUMN cookie_policy_url TEXT NOT NULL DEFAULT '';
UPDATE cookie_banners SET cookie_policy_url = privacy_policy_url WHERE cookie_policy_url = '';
ALTER TABLE cookie_banners ALTER COLUMN privacy_policy_url DROP NOT NULL;

View File

@@ -235,6 +235,7 @@ func (r *mutationResolver) CreateCookieBanner(ctx context.Context, input types.C
Name: input.Name,
Origin: input.Origin,
PrivacyPolicyURL: input.PrivacyPolicyURL,
CookiePolicyURL: input.CookiePolicyURL,
ConsentExpiryDays: input.ConsentExpiryDays,
ConsentMode: input.ConsentMode,
},
@@ -270,6 +271,7 @@ func (r *mutationResolver) UpdateCookieBanner(ctx context.Context, input types.U
CookieBannerID: input.CookieBannerID,
Name: input.Name,
PrivacyPolicyURL: input.PrivacyPolicyURL,
CookiePolicyURL: input.CookiePolicyURL,
ConsentExpiryDays: input.ConsentExpiryDays,
ConsentMode: input.ConsentMode,
DefaultLanguage: input.DefaultLanguage,

View File

@@ -81,7 +81,8 @@ type CookieBanner implements Node {
name: String!
origin: String!
state: CookieBannerState!
privacyPolicyUrl: String!
privacyPolicyUrl: String
cookiePolicyUrl: String!
consentExpiryDays: Int!
consentMode: CookieConsentMode!
showBranding: Boolean!
@@ -266,7 +267,8 @@ input CreateCookieBannerInput {
organizationId: ID!
name: String!
origin: String!
privacyPolicyUrl: String!
privacyPolicyUrl: String
cookiePolicyUrl: String!
consentExpiryDays: Int!
consentMode: CookieConsentMode!
}
@@ -275,6 +277,7 @@ input UpdateCookieBannerInput {
cookieBannerId: ID!
name: String
privacyPolicyUrl: String
cookiePolicyUrl: String
consentExpiryDays: Int
consentMode: CookieConsentMode
defaultLanguage: String

View File

@@ -70,6 +70,7 @@ func NewCookieBanner(b *coredata.CookieBanner) *CookieBanner {
Origin: b.Origin,
State: b.State,
PrivacyPolicyURL: b.PrivacyPolicyURL,
CookiePolicyURL: b.CookiePolicyURL,
ConsentExpiryDays: b.ConsentExpiryDays,
ConsentMode: b.ConsentMode,
ShowBranding: b.ShowBranding,