This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
adblockplus.org — The public-facing website for the Adblock Plus browser extension. A hybrid static + dynamic architecture:
- Static pages: Generated by the eyeo Python CMS (Jinja2 templates), deployed to Firebase Hosting
- Dynamic service: Node.js/Express on Google Cloud Run (
services/cloudrun/index.js) handling robots.txt, legacy redirects, locale routing - Key config:
services/firebase/firebase.json(redirects, rewrites, headers),services/firebase/.firebaserc(staging/production aliases)
pages/ # HTML/Markdown content pages with key=value header metadata
templates/ # Jinja2 templates (minimal, default, fixed-toc, raw)
includes/ # HTML/template partials (navbar, footer, analytics, cookie, scripts)
locales/ # Translation files — 27 languages, Chrome i18n JSON format
static/ # Static assets + per-country settings
ALL_{cc}/ # Per-country settings.js (generated by scripts/generate-settings.mjs)
js/ # Shared page logic (premium checkout, cookie prompt, analytics, utilities, vendor libs, etc.)
css/ # Stylesheets
services/
firebase/ # Firebase Hosting config + public/ output dir
cloudrun/ # Express service (index.js + handlers/)
scripts/ # Build and utility scripts (Node.js + Python)
tests/ # Playwright E2E test suite
*.spec.js # Test specs
test-pages/ # Page object models
test-helpers/ # Mocking helpers (extension, payment, email)
snapshots/ # Visual regression PNG baselines (Linux Chromium only)
filters/ # Python CMS filter plugins
globals/ # Python CMS global variables (subscriptions, browsers)
macros/ # Jinja2 macros (toc, install_button, social_icons)
settings.ini # CMS configuration: site URL, locales list, RTL settings
Prerequisites:
- Install and start Docker Desktop
- Python 3.9 via pyenv:
pyenv install 3.9 && pyenv local 3.9 - Clone the CMS:
git clone https://gitlab.com/eyeo/websites/cms.git - Set PYTHONPATH in shell profile:
export PYTHONPATH="$HOME/path/to/cms:$PYTHONPATH" - Install npm dependencies:
npm install
# Start CMS dev server (http://localhost:8000 — use 127.0.0.1 on macOS if issues)
npm run fast
# Generate per-country settings.js files (after editing the settings template)
node scripts/generate-settings.mjs
# E2E tests (Playwright)
npx playwright install # Install browsers once
npx playwright install chrome msedge # Additional install for Chrome/Edge projects
npx playwright test --project chromium # Run on Chromium only (fastest)
npx playwright test # Run on all browsers
# Run against a staging URL
STAGING=1 STAGING_URL=<url> npx playwright test --project chromium
# Filter by tag
npx playwright test --grep @all_browsers
npx playwright test --project chromium --grep-invert "@third_party_link|@all_browsers"
# Update visual regression baselines locally
npx playwright test --grep @visual_regression --update-snapshots- Indentation: 2 spaces (HTML, JS, CSS, JSON, Markdown, shell)
- Line endings: LF;
- Trailing whitespace: trimmed;
- Final newline: required
Pages use plain key=value lines at the top of the file (no YAML --- delimiters):
title=My Page Title
template=fixed-toc
Available templates: minimal, default, fixed-toc, raw. Navbar, header, toc, and footer can each be suppressed via no{component} page attributes.
Partials in includes/ are pulled in via <? include partial_name ?>.
- Files:
locales/{locale}/{section}.json(Chrome i18n JSON format) - Two syntax forms for referencing strings in templates:
- Function call:
{{ get_string("key-name", "section") }}— reads fromlocales/{locale}/section.json - Inline:
{{ key-name[optional-attr] Fallback English text }}— used widely in pages (e.g.{{ what-is-abp[heading] What is Adblock Plus }})
- Function call:
- Arabic (
ar) and Hebrew (he) are RTL — configured insettings.iniunder[rtl] - CMS auto-detects available locales per page from the presence of JSON files
- Crowdin manages translations; helper scripts in
scripts/:scripts/export-locale-from-html.mjs— export strings for translationscripts/import-locale-strings.mjs— import from Crowdin exportscripts/copy-locale-strings.mjs— copy strings between locales
Events sent to /access?{params} via adblock.log() in includes/scripts/analytics-functions.html (current logVersion: 2.1.1):
| Event | Source | Purpose |
|---|---|---|
load |
includes/scripts/load-tracking.html |
Page load, browser/OS, screen size, timing |
click |
includes/scripts/click-tracking.html |
Clicks on .track-click / [data-click] elements |
script-error |
includes/scripts/error-reporting.html |
JS errors with stack traces |
experiment.loaded |
includes/scripts/frontend-experiments.html |
A/B variant assignments |
GDPR countries get a no-op setup.js — do not add direct GA calls. Users can opt out via eyeo-ga-opt-out cookie. Pages suppress the cookie banner with prevent_cookie_prompt.
Pages load /settings.js (via includes/late-head-scripts.html) which sets country, currency, privacy restrictions, and VAT state. Firebase i18n rewrites serve a different file per visitor IP from static/ALL_{cc}/settings.js. The fallback (unknown country) is at static/ALL_ALL/settings.js. Run node scripts/generate-settings.mjs to regenerate after editing the template.
Do not hardcode country/currency/VAT logic in pages — use the settings infrastructure.
Framework: Playwright — projects: chromium, firefox, webkit, Google Chrome, Microsoft Edge
Test tags:
@all_browsers— behavior differs across browsers (runs on all browsers in CI)@third_party_link— depends on external sites (excluded from standard CI runs)@visual_regression— screenshot comparison against Linux Chromium baselines
Page objects: Use/extend models in tests/test-pages/ rather than writing raw selectors in spec files.
Extension mocking: Call await ExtensionHelper.mockExtensionData(page, '4.31.0', false) (from tests/test-helpers/extension-helper.js) before opening a page. Second arg is version string, third is whether Premium is enabled. Prefer mocking over full extension installation for speed.
Visual regression: Baselines are Linux Chromium only (tests/snapshots/). To update after a CI failure: download the visual_regression_tests:archive artifact, rename the file to just the browser name, replace the existing file in the linux/ snapshot folder. Do not update baselines locally on macOS — they will mismatch in CI.
Debugging CI failures: Download the test-results/ artifact and open trace.zip at https://trace.playwright.dev.
variant(number) — experiment variant to applycountry(string) — 2-letter country code overridedefaultCurrency(string) — 3-letter currency code overriderestrictPremium(boolean) — disable premium purchasingrestrictPrivacy(boolean) — disable some third-party servicessid(string) — 32-char session ID for correlating access logss(string) — traffic source namedev/design(flag) — development/design mode
testmode(flag) — Paddle sandbox instead of livehas-subscription— fake subscription state:yes,no,error,timeout,finding,found
bc(number) — show alternate CTA with block count
premium-checkout__*— handoff values:handoff,flow,page,product,premiumId,currency,frequency,amount,country,locale,timestampreenroll(flag) — show re-enroll CTA variant
GitLab CI/CD (.gitlab-ci.yml). Pipeline stages:
- prebuild_firebase — generate preview URLs
- build_firebase — CMS generates static pages
- deploy_cloud_run — deploy Express service (named
b-<branch-name>on non-master) - deploy_firebase — deploy to Firebase Hosting (preview channel on non-master, expires 30 days)
- test — Playwright E2E tests against the deployed URL
Production: Merging to master auto-deploys immediately — no manual gate. To rollback: git revert <hash> + open MR. For static-only emergency rollback: revert a release in the Firebase console (does not revert Cloud Run).
services/firebase/public/is generated output — never edit files there directly.htaccessfiles are legacy Apache artifacts, not used in productionnpm run slowis a legacy Apache setup — ignore it.devcontainer/exists but is not actively supported; use the local setupstatic/js/testing/pre-approved.jsandincludes/pre-approved-analytics.tmplare unused legacy files from the old three-tier consent system