diff --git a/.impeccable/design.json b/.impeccable/design.json index 87d0763d..b8f55198 100644 --- a/.impeccable/design.json +++ b/.impeccable/design.json @@ -4,44 +4,293 @@ "title": "Design System: Tech for Palestine", "extensions": { "colorMeta": { - "page": { "role": "neutral", "displayName": "Warm Parchment", "canonical": "oklch(99% 0.005 80)", "tonalRamp": ["oklch(15% 0.005 80)","oklch(30% 0.005 80)","oklch(45% 0.005 80)","oklch(60% 0.005 80)","oklch(75% 0.005 80)","oklch(88% 0.005 80)","oklch(95% 0.005 80)","oklch(99% 0.005 80)"] }, - "cream": { "role": "neutral", "displayName": "Cream", "canonical": "oklch(97% 0.008 65)", "tonalRamp": ["oklch(15% 0.008 65)","oklch(30% 0.008 65)","oklch(45% 0.008 65)","oklch(60% 0.008 65)","oklch(75% 0.008 65)","oklch(88% 0.008 65)","oklch(94% 0.008 65)","oklch(97% 0.008 65)"] }, - "butter": { "role": "neutral", "displayName": "Butter", "canonical": "oklch(95% 0.015 75)", "tonalRamp": ["oklch(15% 0.015 75)","oklch(30% 0.015 75)","oklch(45% 0.015 75)","oklch(60% 0.015 75)","oklch(75% 0.015 75)","oklch(87% 0.015 75)","oklch(92% 0.015 75)","oklch(95% 0.015 75)"] }, - "sand": { "role": "neutral", "displayName": "Sand", "canonical": "oklch(96% 0.010 80)", "tonalRamp": ["oklch(15% 0.010 80)","oklch(30% 0.010 80)","oklch(45% 0.010 80)","oklch(60% 0.010 80)","oklch(75% 0.010 80)","oklch(88% 0.010 80)","oklch(93% 0.010 80)","oklch(96% 0.010 80)"] }, - "ink": { "role": "neutral", "displayName": "Ink", "canonical": "oklch(20% 0.008 350)", "tonalRamp": ["oklch(20% 0.008 350)","oklch(32% 0.008 350)","oklch(44% 0.008 350)","oklch(56% 0.008 350)","oklch(68% 0.008 350)","oklch(80% 0.008 350)","oklch(90% 0.008 350)","oklch(97% 0.008 350)"] }, - "ink-dark": { "role": "neutral", "displayName": "Ink Dark", "canonical": "oklch(17% 0.006 350)", "tonalRamp": ["oklch(17% 0.006 350)","oklch(29% 0.006 350)","oklch(41% 0.006 350)","oklch(53% 0.006 350)","oklch(65% 0.006 350)","oklch(78% 0.006 350)","oklch(89% 0.006 350)","oklch(96% 0.006 350)"] }, - "ink-secondary":{ "role": "neutral", "displayName": "Ink Secondary", "canonical": "oklch(48% 0.012 350)", "tonalRamp": ["oklch(15% 0.012 350)","oklch(28% 0.012 350)","oklch(40% 0.012 350)","oklch(48% 0.012 350)","oklch(62% 0.012 350)","oklch(74% 0.012 350)","oklch(85% 0.012 350)","oklch(95% 0.012 350)"] }, - "ink-muted": { "role": "neutral", "displayName": "Ink Muted", "canonical": "oklch(74% 0.002 0)", "tonalRamp": ["oklch(20% 0.002 0)","oklch(35% 0.002 0)","oklch(50% 0.002 0)","oklch(62% 0.002 0)","oklch(74% 0.002 0)","oklch(84% 0.002 0)","oklch(91% 0.002 0)","oklch(97% 0.002 0)"] }, - "ink-divider": { "role": "neutral", "displayName": "Ink Divider", "canonical": "oklch(85% 0.002 0)", "tonalRamp": ["oklch(20% 0.002 0)","oklch(35% 0.002 0)","oklch(50% 0.002 0)","oklch(65% 0.002 0)","oklch(75% 0.002 0)","oklch(85% 0.002 0)","oklch(92% 0.002 0)","oklch(97% 0.002 0)"] }, - "logo-frame": { "role": "neutral", "displayName": "Logo Frame", "canonical": "oklch(92% 0.018 75)", "tonalRamp": ["oklch(15% 0.018 75)","oklch(30% 0.018 75)","oklch(45% 0.018 75)","oklch(60% 0.018 75)","oklch(75% 0.018 75)","oklch(85% 0.018 75)","oklch(92% 0.018 75)","oklch(97% 0.010 75)"] }, - "brand": { "role": "primary", "displayName": "Pomegranate", "canonical": "oklch(50% 0.12 352)", "tonalRamp": ["oklch(18% 0.12 352)","oklch(27% 0.12 352)","oklch(36% 0.12 352)","oklch(44% 0.12 352)","oklch(50% 0.12 352)","oklch(62% 0.12 352)","oklch(75% 0.10 352)","oklch(90% 0.06 352)"] }, - "brand-hover": { "role": "primary", "displayName": "Pomegranate Light", "canonical": "oklch(57% 0.13 352)", "tonalRamp": ["oklch(20% 0.13 352)","oklch(30% 0.13 352)","oklch(40% 0.13 352)","oklch(50% 0.13 352)","oklch(57% 0.13 352)","oklch(68% 0.11 352)","oklch(80% 0.08 352)","oklch(93% 0.04 352)"] }, - "brand-light": { "role": "primary", "displayName": "Pomegranate on Dark","canonical": "oklch(65% 0.14 12)", "tonalRamp": ["oklch(22% 0.14 12)","oklch(33% 0.14 12)","oklch(44% 0.14 12)","oklch(55% 0.14 12)","oklch(65% 0.14 12)","oklch(76% 0.11 12)","oklch(86% 0.07 12)","oklch(94% 0.03 12)"] } + "page": { + "role": "neutral", + "displayName": "Warm Parchment", + "canonical": "oklch(99% 0.005 80)", + "tonalRamp": [ + "oklch(15% 0.005 80)", + "oklch(30% 0.005 80)", + "oklch(45% 0.005 80)", + "oklch(60% 0.005 80)", + "oklch(75% 0.005 80)", + "oklch(88% 0.005 80)", + "oklch(95% 0.005 80)", + "oklch(99% 0.005 80)" + ] + }, + "cream": { + "role": "neutral", + "displayName": "Cream", + "canonical": "oklch(97% 0.008 65)", + "tonalRamp": [ + "oklch(15% 0.008 65)", + "oklch(30% 0.008 65)", + "oklch(45% 0.008 65)", + "oklch(60% 0.008 65)", + "oklch(75% 0.008 65)", + "oklch(88% 0.008 65)", + "oklch(94% 0.008 65)", + "oklch(97% 0.008 65)" + ] + }, + "butter": { + "role": "neutral", + "displayName": "Butter", + "canonical": "oklch(95% 0.015 75)", + "tonalRamp": [ + "oklch(15% 0.015 75)", + "oklch(30% 0.015 75)", + "oklch(45% 0.015 75)", + "oklch(60% 0.015 75)", + "oklch(75% 0.015 75)", + "oklch(87% 0.015 75)", + "oklch(92% 0.015 75)", + "oklch(95% 0.015 75)" + ] + }, + "sand": { + "role": "neutral", + "displayName": "Sand", + "canonical": "oklch(96% 0.010 80)", + "tonalRamp": [ + "oklch(15% 0.010 80)", + "oklch(30% 0.010 80)", + "oklch(45% 0.010 80)", + "oklch(60% 0.010 80)", + "oklch(75% 0.010 80)", + "oklch(88% 0.010 80)", + "oklch(93% 0.010 80)", + "oklch(96% 0.010 80)" + ] + }, + "ink": { + "role": "neutral", + "displayName": "Ink", + "canonical": "oklch(20% 0.008 350)", + "tonalRamp": [ + "oklch(20% 0.008 350)", + "oklch(32% 0.008 350)", + "oklch(44% 0.008 350)", + "oklch(56% 0.008 350)", + "oklch(68% 0.008 350)", + "oklch(80% 0.008 350)", + "oklch(90% 0.008 350)", + "oklch(97% 0.008 350)" + ] + }, + "ink-dark": { + "role": "neutral", + "displayName": "Ink Dark", + "canonical": "oklch(17% 0.006 350)", + "tonalRamp": [ + "oklch(17% 0.006 350)", + "oklch(29% 0.006 350)", + "oklch(41% 0.006 350)", + "oklch(53% 0.006 350)", + "oklch(65% 0.006 350)", + "oklch(78% 0.006 350)", + "oklch(89% 0.006 350)", + "oklch(96% 0.006 350)" + ] + }, + "ink-secondary": { + "role": "neutral", + "displayName": "Ink Secondary", + "canonical": "oklch(48% 0.012 350)", + "tonalRamp": [ + "oklch(15% 0.012 350)", + "oklch(28% 0.012 350)", + "oklch(40% 0.012 350)", + "oklch(48% 0.012 350)", + "oklch(62% 0.012 350)", + "oklch(74% 0.012 350)", + "oklch(85% 0.012 350)", + "oklch(95% 0.012 350)" + ] + }, + "ink-muted": { + "role": "neutral", + "displayName": "Ink Muted", + "canonical": "oklch(74% 0.002 0)", + "tonalRamp": [ + "oklch(20% 0.002 0)", + "oklch(35% 0.002 0)", + "oklch(50% 0.002 0)", + "oklch(62% 0.002 0)", + "oklch(74% 0.002 0)", + "oklch(84% 0.002 0)", + "oklch(91% 0.002 0)", + "oklch(97% 0.002 0)" + ] + }, + "ink-divider": { + "role": "neutral", + "displayName": "Ink Divider", + "canonical": "oklch(85% 0.002 0)", + "tonalRamp": [ + "oklch(20% 0.002 0)", + "oklch(35% 0.002 0)", + "oklch(50% 0.002 0)", + "oklch(65% 0.002 0)", + "oklch(75% 0.002 0)", + "oklch(85% 0.002 0)", + "oklch(92% 0.002 0)", + "oklch(97% 0.002 0)" + ] + }, + "logo-frame": { + "role": "neutral", + "displayName": "Logo Frame", + "canonical": "oklch(92% 0.018 75)", + "tonalRamp": [ + "oklch(15% 0.018 75)", + "oklch(30% 0.018 75)", + "oklch(45% 0.018 75)", + "oklch(60% 0.018 75)", + "oklch(75% 0.018 75)", + "oklch(85% 0.018 75)", + "oklch(92% 0.018 75)", + "oklch(97% 0.010 75)" + ] + }, + "brand": { + "role": "primary", + "displayName": "Pomegranate", + "canonical": "oklch(50% 0.12 352)", + "tonalRamp": [ + "oklch(18% 0.12 352)", + "oklch(27% 0.12 352)", + "oklch(36% 0.12 352)", + "oklch(44% 0.12 352)", + "oklch(50% 0.12 352)", + "oklch(62% 0.12 352)", + "oklch(75% 0.10 352)", + "oklch(90% 0.06 352)" + ] + }, + "brand-hover": { + "role": "primary", + "displayName": "Pomegranate Light", + "canonical": "oklch(57% 0.13 352)", + "tonalRamp": [ + "oklch(20% 0.13 352)", + "oklch(30% 0.13 352)", + "oklch(40% 0.13 352)", + "oklch(50% 0.13 352)", + "oklch(57% 0.13 352)", + "oklch(68% 0.11 352)", + "oklch(80% 0.08 352)", + "oklch(93% 0.04 352)" + ] + }, + "brand-light": { + "role": "primary", + "displayName": "Pomegranate on Dark", + "canonical": "oklch(65% 0.14 12)", + "tonalRamp": [ + "oklch(22% 0.14 12)", + "oklch(33% 0.14 12)", + "oklch(44% 0.14 12)", + "oklch(55% 0.14 12)", + "oklch(65% 0.14 12)", + "oklch(76% 0.11 12)", + "oklch(86% 0.07 12)", + "oklch(94% 0.03 12)" + ] + } }, "typographyMeta": { - "display": { "displayName": "Display (.ts-display)", "purpose": "Hero headline. Used once per page. Fraunces 42→52→60px." }, - "headline": { "displayName": "Editorial (.ts-editorial)", "purpose": "Section-opening statements. Manifesto, portfolio header. Fraunces 36→42→48px." }, - "title": { "displayName": "Heading (.ts-heading)", "purpose": "Project names, sub-section headings. Fraunces 32→36→38px." }, - "body": { "displayName": "Body Large (.ts-body-large)","purpose": "Primary body copy. Outfit 18→20px, line-height 1.48. Max 65-75ch." }, - "label": { "displayName": "Overline (.ts-overline)", "purpose": "Section eyebrows, uppercase tracked annotations. Outfit 12px, 0.12em, uppercase. Distinct from button label (.ts-label, 16-18px, not uppercase) and the Fraunces eyebrow (.ts-eyebrow, 18-24px serif)." } + "display": { + "displayName": "Display (.ts-display)", + "purpose": "Hero headline. Used once per page. Fraunces 42→52→60px." + }, + "headline": { + "displayName": "Editorial (.ts-editorial)", + "purpose": "Section-opening statements. Manifesto, portfolio header. Fraunces 36→42→48px." + }, + "title": { + "displayName": "Heading (.ts-heading)", + "purpose": "Project names, sub-section headings. Fraunces 32→36→38px." + }, + "body": { + "displayName": "Body Large (.ts-body-large)", + "purpose": "Primary body copy. Outfit 18→20px, line-height 1.48. Max 65-75ch." + }, + "label": { + "displayName": "Overline (.ts-overline)", + "purpose": "Section eyebrows, uppercase tracked annotations. Outfit 12px, 0.12em, uppercase. Distinct from button label (.ts-label, 16-18px, not uppercase) and the Fraunces eyebrow (.ts-eyebrow, 18-24px serif)." + } }, "shadows": [ - { "name": "logo-frame-ambient", "value": "0 6px 20px rgba(0,0,0,0.06)", "purpose": "Rotated butter accent behind logo frames." }, - { "name": "logo-frame-lift", "value": "0 8px 24px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.05)", "purpose": "Logo card foreground — structural lift over accent." } + { + "name": "logo-frame-ambient", + "value": "0 6px 20px rgba(0,0,0,0.06)", + "purpose": "Rotated butter accent behind logo frames." + }, + { + "name": "logo-frame-lift", + "value": "0 8px 24px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.05)", + "purpose": "Logo card foreground — structural lift over accent." + } ], "motion": [ - { "name": "blur-in", "value": "opacity 0 filter blur(12px) translateY(20px) → opacity 1 filter blur(0) translateY(0)", "duration": "1.2s", "easing": "ease-out", "purpose": "Hero headline entrance. Used for primary copy only." }, - { "name": "fade-up", "value": "opacity 0 translateY(12px) → opacity 1 translateY(0)", "duration": "0.6s", "easing": "ease-out", "purpose": "CTA and logo entrance after headline." }, - { "name": "card-in", "value": "opacity 0 scale(0.96) → opacity 1 scale(1)", "duration": "0.8s", "easing": "ease-out", "purpose": "Hero glass card entrance." }, - { "name": "ken-burns", "value": "scale(1) → scale(1.08) → scale(1)", "duration": "14s", "easing": "ease-in-out", "purpose": "Subtle image life. Full-bleed hero and collage photos only. Always motion-reduce:animate-none." }, - { "name": "portfolio-in", "value": "opacity 0 translateY(32px) → opacity 1 translateY(0)", "duration": "0.7s", "easing": "ease-out", "purpose": "Portfolio card scroll-triggered entrance." }, - { "name": "fade-left", "value": "opacity 0 translateX(-16px) → opacity 1 translateX(0)", "duration": "0.6s", "easing": "ease-out", "purpose": "Motif row icon stagger." } + { + "name": "blur-in", + "value": "opacity 0 filter blur(12px) translateY(20px) → opacity 1 filter blur(0) translateY(0)", + "duration": "1.2s", + "easing": "ease-out", + "purpose": "Hero headline entrance. Used for primary copy only." + }, + { + "name": "fade-up", + "value": "opacity 0 translateY(12px) → opacity 1 translateY(0)", + "duration": "0.6s", + "easing": "ease-out", + "purpose": "CTA and logo entrance after headline." + }, + { + "name": "card-in", + "value": "opacity 0 scale(0.96) → opacity 1 scale(1)", + "duration": "0.8s", + "easing": "ease-out", + "purpose": "Hero glass card entrance." + }, + { + "name": "ken-burns", + "value": "scale(1) → scale(1.08) → scale(1)", + "duration": "14s", + "easing": "ease-in-out", + "purpose": "Subtle image life. Full-bleed hero and collage photos only. Always motion-reduce:animate-none." + }, + { + "name": "portfolio-in", + "value": "opacity 0 translateY(32px) → opacity 1 translateY(0)", + "duration": "0.7s", + "easing": "ease-out", + "purpose": "Portfolio card scroll-triggered entrance." + }, + { + "name": "fade-left", + "value": "opacity 0 translateX(-16px) → opacity 1 translateX(0)", + "duration": "0.6s", + "easing": "ease-out", + "purpose": "Motif row icon stagger." + } ], "breakpoints": [ - { "name": "sm", "value": "640px", "note": "Mobile collage and motif adjustments" }, - { "name": "md", "value": "810px", "note": "Hero layout switch: mobile scroll → desktop glass card" }, - { "name": "lg", "value": "1024px", "note": "Portfolio card 2-col, collage all 4 visible" }, - { "name": "xl", "value": "1200px", "note": "Logo repositions to bottom-right in hero card; type scale top step" } + { "name": "sm", "value": "640px", "note": "Mobile collage and motif adjustments" }, + { + "name": "md", + "value": "810px", + "note": "Hero layout switch: mobile scroll → desktop glass card" + }, + { "name": "lg", "value": "1024px", "note": "Portfolio card 2-col, collage all 4 visible" }, + { + "name": "xl", + "value": "1200px", + "note": "Logo repositions to bottom-right in hero card; type scale top step" + } ] }, "components": [ @@ -105,14 +354,46 @@ "Every screen ends with a clear, direct call to action" ], "rules": [ - { "name": "The One Accent Rule", "body": "Pomegranate appears on less than 10% of any screen. It marks action and emphasis only. A second accent would dilute it to decoration.", "section": "colors" }, - { "name": "The Single Dark Surface Rule","body": "Exactly one dark panel exists: the closing CTA (ink-dark #201D1E), with brand-light #E8727F as its on-dark accent. It earns its inversion by being the terminal call to action. Do not introduce dark sections elsewhere.", "section": "colors" }, - { "name": "The Tonal Depth Rule", "body": "Depth is created by stepping through the neutral stack (Parchment → Cream → Sand → Butter), not by drop shadows. Shadows are reserved for interactive elements that need structural lift.", "section": "colors" }, - { "name": "The No Pure Values Rule", "body": "Never use #000000 or #ffffff. Every neutral carries a warm tint. Pure values feel clinical; tinted neutrals feel inhabited.", "section": "colors" }, - { "name": "The Fraunces Hierarchy Rule", "body": "Fraunces owns display, editorial, stat, heading, subheading, quote, and the serif eyebrow (.ts-eyebrow). Outfit owns all interface text: body, label, overline, caption. The serif never appears in a button or overline; the sans never appears in a headline or stat.", "section": "typography" }, - { "name": "The Line Length Rule", "body": "Body copy never exceeds 75 characters per line. On wide viewports, constrain with max-w-[75ch] or equivalent.", "section": "typography" }, - { "name": "The Flat-By-Default Rule", "body": "A surface that doesn't move does not get a shadow. If adding a shadow is the first instinct, ask whether a background step solves the same problem.", "section": "elevation" }, - { "name": "The No Nested Card Rule", "body": "Never place a card inside a card. The logo frame within a portfolio card is a logo display element, not a card.", "section": "components" } + { + "name": "The One Accent Rule", + "body": "Pomegranate appears on less than 10% of any screen. It marks action and emphasis only. A second accent would dilute it to decoration.", + "section": "colors" + }, + { + "name": "The Single Dark Surface Rule", + "body": "Exactly one dark panel exists: the closing CTA (ink-dark #201D1E), with brand-light #E8727F as its on-dark accent. It earns its inversion by being the terminal call to action. Do not introduce dark sections elsewhere.", + "section": "colors" + }, + { + "name": "The Tonal Depth Rule", + "body": "Depth is created by stepping through the neutral stack (Parchment → Cream → Sand → Butter), not by drop shadows. Shadows are reserved for interactive elements that need structural lift.", + "section": "colors" + }, + { + "name": "The No Pure Values Rule", + "body": "Never use #000000 or #ffffff. Every neutral carries a warm tint. Pure values feel clinical; tinted neutrals feel inhabited.", + "section": "colors" + }, + { + "name": "The Fraunces Hierarchy Rule", + "body": "Fraunces owns display, editorial, stat, heading, subheading, quote, and the serif eyebrow (.ts-eyebrow). Outfit owns all interface text: body, label, overline, caption. The serif never appears in a button or overline; the sans never appears in a headline or stat.", + "section": "typography" + }, + { + "name": "The Line Length Rule", + "body": "Body copy never exceeds 75 characters per line. On wide viewports, constrain with max-w-[75ch] or equivalent.", + "section": "typography" + }, + { + "name": "The Flat-By-Default Rule", + "body": "A surface that doesn't move does not get a shadow. If adding a shadow is the first instinct, ask whether a background step solves the same problem.", + "section": "elevation" + }, + { + "name": "The No Nested Card Rule", + "body": "Never place a card inside a card. The logo frame within a portfolio card is a logo display element, not a card.", + "section": "components" + } ], "dos": [ "Use Pomegranate (#AB4956) only on CTAs, brand emphasis, and active states", diff --git a/CLAUDE.md b/CLAUDE.md index e1606dc1..e12f1744 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,7 @@ Astro runs in `output: "server"` mode on the Cloudflare adapter — nearly every ### Request pipeline (middleware) `src/middleware/index.ts` chains two middlewares via `sequence()`, in this order: + 1. `cache-control.ts` — sets `Cache-Control` headers (API routes get `no-store`) 2. `csp.ts` — injects a per-request CSP nonce into inline ` -`** @@ -90,15 +90,14 @@ import "../styles/base.css"; Locate the closing `` tag in `src/pages/index.astro` and insert the tracking script immediately before it: ```astro - - + ``` The guard `v === "control"` ensures the event only fires for users who were properly assigned through the A/B test (not someone who cleared their storage mid-session). @@ -106,6 +105,7 @@ The guard `v === "control"` ensures the event only fires for users who were prop - [ ] **Step 3: Verify locally** Start the dev server: + ```bash pnpm dev ``` @@ -113,6 +113,7 @@ pnpm dev Open `http://localhost:4321` in a browser with devtools open. **Test A — variant assignment:** + 1. Open Application → Local Storage → `http://localhost:4321` 2. Confirm `ab-homepage-variant` is absent 3. Hard-reload the page several times @@ -121,12 +122,14 @@ Open `http://localhost:4321` in a browser with devtools open. 6. If `"control"`: confirm the homepage renders normally **Test B — stickiness:** + 1. Set `ab-homepage-variant` to `"variant"` manually in devtools 2. Navigate to `/` — should immediately redirect to `/home-new` 3. Set `ab-homepage-variant` to `"control"` manually 4. Navigate to `/` — should stay on `/` **Test C — Plausible event (control):** + 1. Set `ab-homepage-variant` to `"control"` 2. Navigate to `/` 3. Open Console and run: `window.plausible.q` — confirm an entry for `"ab-homepage"` with `{ variant: "control" }` is queued @@ -143,9 +146,11 @@ git commit -m "feat(ab-test): add homepage A/B anti-flicker script and variant t ### Task 2: Plausible tracking on the variant page (`home-new.astro`) **Files:** + - Modify: `src/pages/home-new.astro` **Context:** + - `HomeLayout.astro` initialises the Plausible queue in the `` (line 169–181), before ``. Any ` - + ``` - [ ] **Step 2: Verify locally** @@ -170,18 +174,21 @@ Open `src/pages/home-new.astro`. Locate the closing `` tag and insert imm With the dev server running: **Test A — Plausible event fires for assigned variant users:** + 1. Set `ab-homepage-variant` to `"variant"` in devtools 2. Navigate to `/home-new` 3. Open Console, run: `window.plausible.q` 4. Confirm an entry for `"ab-homepage"` with `{ variant: "new" }` is queued **Test B — event does NOT fire for direct visitors:** + 1. Clear `ab-homepage-variant` from localStorage (or delete the key) 2. Navigate directly to `/home-new` 3. Open Console, run: `window.plausible.q` 4. Confirm NO `"ab-homepage"` entry is present **Test C — full flow end-to-end:** + 1. Clear all localStorage 2. Navigate to `/` 3. If redirected to `/home-new`: confirm `ab-homepage-variant === "variant"` and Plausible event queued with `variant: "new"` @@ -216,6 +223,7 @@ Then in your dashboard reports, filter by the `variant` property to compare beha When you have a winner: **If `variant` wins (new design):** + ```bash # 1. Replace index.astro content with home-new.astro content # 2. Delete home-new.astro @@ -226,6 +234,7 @@ echo "/home-new / 301" >> public/_redirects ``` **If `control` wins (old design):** + ```bash # 1. Remove the anti-flicker script and tracking script from index.astro # 2. Delete home-new.astro diff --git a/docs/superpowers/specs/2026-06-30-homepage-ab-test-design.md b/docs/superpowers/specs/2026-06-30-homepage-ab-test-design.md index b6d2ffb9..f071522b 100644 --- a/docs/superpowers/specs/2026-06-30-homepage-ab-test-design.md +++ b/docs/superpowers/specs/2026-06-30-homepage-ab-test-design.md @@ -17,6 +17,7 @@ Run a controlled A/B test comparing the current homepage design (control) agains **Option chosen:** Client-side anti-flicker inline script with localStorage for sticky assignment. **Why:** + - PECR compliance — localStorage is not a cookie; no consent banner required under UK law - Consistent with existing A/B test patterns in the codebase (`donate-new.astro`, `MembershipPage.tsx`) - No infrastructure changes — no middleware, no Cloudflare Workers, no new dependencies @@ -45,24 +46,24 @@ Both / and /home-new ## Assignment Properties -| Property | Value | -|---|---| -| localStorage key | `ab-homepage-variant` | -| Possible values | `control`, `variant` | -| Split | 50/50 (`Math.random() < 0.5`) | -| Persistence | Until localStorage is cleared | +| Property | Value | +| ----------------- | --------------------------------------- | +| localStorage key | `ab-homepage-variant` | +| Possible values | `control`, `variant` | +| Split | 50/50 (`Math.random() < 0.5`) | +| Persistence | Until localStorage is cleared | | Private/incognito | Re-randomised each session (acceptable) | --- ## Edge Cases -| Scenario | Behaviour | -|---|---| -| User lands on `/about` first, then `/` | Assigned on first visit to `/` — correct | -| User lands directly on `/home-new` | Sees new design unassigned; assigned on next `/` visit | +| Scenario | Behaviour | +| ------------------------------------------------ | --------------------------------------------------------------------- | +| User lands on `/about` first, then `/` | Assigned on first visit to `/` — correct | +| User lands directly on `/home-new` | Sees new design unassigned; assigned on next `/` visit | | `control` user navigates manually to `/home-new` | Sees new design — acceptable edge case, negligible statistical impact | -| JS disabled | Always sees control (no redirect fires) | +| JS disabled | Always sees control (no redirect fires) | --- @@ -71,9 +72,9 @@ Both / and /home-new Plausible Analytics is already set up. Variant exposure is recorded via a custom event on both pages: ```js -window.plausible('ab-homepage', { props: { variant: 'control' } }) +window.plausible("ab-homepage", { props: { variant: "control" } }); // or -window.plausible('ab-homepage', { props: { variant: 'new' } }) +window.plausible("ab-homepage", { props: { variant: "new" } }); ``` In the Plausible dashboard: create a custom event goal named `ab-homepage`, then filter by the `variant` property to compare engagement between groups. @@ -82,10 +83,10 @@ In the Plausible dashboard: create a custom event goal named `ab-homepage`, then ## Files Changed -| File | Change | -|---|---| -| `src/pages/index.astro` | Add anti-flicker ` diff --git a/src/components/home/FooterSection.astro b/src/components/home/FooterSection.astro index 840b967c..da1fe49a 100644 --- a/src/components/home/FooterSection.astro +++ b/src/components/home/FooterSection.astro @@ -44,16 +44,32 @@ const socials: { name: string; url: string; icon: string }[] = [ { name: "Twitter", url: "https://twitter.com/tech4palestine", icon: "mdi:twitter" }, { name: "YouTube", url: "https://www.youtube.com/@tech4palestine", icon: "mdi:youtube" }, { name: "GitHub", url: "https://github.com/TechForPalestine/", icon: "mdi:github" }, - { name: "LinkedIn", url: "https://www.linkedin.com/company/techforpalestine/", icon: "mdi:linkedin" }, - { name: "Discord", url: "https://techforpalestine.org/discord-invite", icon: "ic:baseline-discord" }, + { + name: "LinkedIn", + url: "https://www.linkedin.com/company/techforpalestine/", + icon: "mdi:linkedin", + }, + { + name: "Discord", + url: "https://techforpalestine.org/discord-invite", + icon: "ic:baseline-discord", + }, { name: "Mastodon", url: "https://infosec.exchange/@tech4palestine", icon: "mdi:mastodon" }, { name: "TikTok", url: "https://www.tiktok.com/@techforpalestine", icon: "ic:baseline-tiktok" }, - { name: "Bluesky", url: "https://bsky.app/profile/techforpalestine.org", icon: "simple-icons:bluesky" }, - { name: "UpScrolled", url: "https://share.upscrolled.com/en/user/39494e76-22a4-47ee-a67d-cd7049835a53/", icon: "/upscrolled-icon.svg" }, + { + name: "Bluesky", + url: "https://bsky.app/profile/techforpalestine.org", + icon: "simple-icons:bluesky", + }, + { + name: "UpScrolled", + url: "https://share.upscrolled.com/en/user/39494e76-22a4-47ee-a67d-cd7049835a53/", + icon: "/upscrolled-icon.svg", + }, ]; --- -