EuroPython 2026 Bingo — I completed {line.label}!
+ +Bingo
+ {EDITION_COUNT} editions · {UNIQUE_CITIES} cities · One community +
+Completed {line.label}
+diff --git a/Makefile b/Makefile
index 095f9c005..5bf4762e3 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ VPS_HOST ?= static.europython.eu
VPS_PROD_PATH ?= /home/static_content_user/content/europython_websites/ep2026
VPS_PREVIEW_PATH ?= /home/static_content_user/content/previews
REMOTE_CMD=ssh $(VPS_USER)@$(VPS_HOST)
-PREVIEW_SITE_URL ?= "https://$(SAFE_BRANCH).ep-preview.click"
+PREVIEW_SITE_URL ?= https://$(SAFE_BRANCH).ep-preview.click
# Variables for build/deploy
# ==========================
@@ -20,7 +20,7 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
# Replace "/" and other non-alphanumeric characters with "-"
SAFE_BRANCH := $(shell echo "$(BRANCH)" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
FORCE_DEPLOY ?= false
-SITE_URL ?= "https://$(SAFE_BRANCH).ep-preview.click"
+SITE_URL ?= https://$(SAFE_BRANCH).ep-preview.click
.PHONY: build deploy dev clean install
@@ -48,8 +48,8 @@ build:
preview: RELEASES_DIR = $(VPS_PREVIEW_PATH)/$(SAFE_BRANCH)/releases
preview: TARGET = $(RELEASES_DIR)/$(TIMESTAMP)
-preview: build
preview:
+ SITE_URL=$(SITE_URL) $(MAKE) build
@echo "Preview site URL: $(PREVIEW_SITE_URL)"
echo $(TARGET)
@echo "\n\n**** Deploying preview of a branch '$(BRANCH)' (safe: $(SAFE_BRANCH)) to $(TARGET)...\n\n"
diff --git a/package.json b/package.json
index baef14c16..89b361f3c 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"scripts": {
"dev": "astro dev",
"start": "astro dev",
- "build": "astro check && astro build && pnpm pagefind",
+ "build": "astro check && astro build && pnpm pagefind && node scripts/generate-bingo-pngs.mjs",
"preview": "astro preview",
"astro": "astro",
"format": "prettier --write --plugin=prettier-plugin-astro ."
@@ -19,6 +19,7 @@
"@astrojs/ts-plugin": "^1.10.9",
"@fortawesome/fontawesome-free": "^6.7.2",
"@nanostores/persistent": "^1.3.4",
+ "@resvg/resvg-js": "^2.6.2",
"@tailwindcss/typography": "^0.5.20",
"@tailwindcss/vite": "^4.3.1",
"astro": "^6.4.8",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ecb5fdea7..efdbb69fc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
'@nanostores/persistent':
specifier: ^1.3.4
version: 1.3.4(nanostores@1.3.0)
+ '@resvg/resvg-js':
+ specifier: ^2.6.2
+ version: 2.6.2
'@tailwindcss/typography':
specifier: ^0.5.20
version: 0.5.20(tailwindcss@4.3.1)
@@ -784,6 +787,82 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+ '@resvg/resvg-js-android-arm-eabi@2.6.2':
+ resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [android]
+
+ '@resvg/resvg-js-android-arm64@2.6.2':
+ resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@resvg/resvg-js-darwin-arm64@2.6.2':
+ resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@resvg/resvg-js-darwin-x64@2.6.2':
+ resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
+ resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@resvg/resvg-js-linux-arm64-gnu@2.6.2':
+ resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@resvg/resvg-js-linux-arm64-musl@2.6.2':
+ resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@resvg/resvg-js-linux-x64-gnu@2.6.2':
+ resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@resvg/resvg-js-linux-x64-musl@2.6.2':
+ resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@resvg/resvg-js-win32-arm64-msvc@2.6.2':
+ resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@resvg/resvg-js-win32-ia32-msvc@2.6.2':
+ resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@resvg/resvg-js-win32-x64-msvc@2.6.2':
+ resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@resvg/resvg-js@2.6.2':
+ resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==}
+ engines: {node: '>= 10'}
+
'@rollup/pluginutils@5.4.0':
resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==}
engines: {node: '>=14.0.0'}
@@ -3408,6 +3487,57 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
+ '@resvg/resvg-js-android-arm-eabi@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-android-arm64@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-darwin-arm64@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-darwin-x64@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-linux-arm64-gnu@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-linux-arm64-musl@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-linux-x64-gnu@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-linux-x64-musl@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-win32-arm64-msvc@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-win32-ia32-msvc@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js-win32-x64-msvc@2.6.2':
+ optional: true
+
+ '@resvg/resvg-js@2.6.2':
+ optionalDependencies:
+ '@resvg/resvg-js-android-arm-eabi': 2.6.2
+ '@resvg/resvg-js-android-arm64': 2.6.2
+ '@resvg/resvg-js-darwin-arm64': 2.6.2
+ '@resvg/resvg-js-darwin-x64': 2.6.2
+ '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2
+ '@resvg/resvg-js-linux-arm64-gnu': 2.6.2
+ '@resvg/resvg-js-linux-arm64-musl': 2.6.2
+ '@resvg/resvg-js-linux-x64-gnu': 2.6.2
+ '@resvg/resvg-js-linux-x64-musl': 2.6.2
+ '@resvg/resvg-js-win32-arm64-msvc': 2.6.2
+ '@resvg/resvg-js-win32-ia32-msvc': 2.6.2
+ '@resvg/resvg-js-win32-x64-msvc': 2.6.2
+
'@rollup/pluginutils@5.4.0(rollup@4.62.0)':
dependencies:
'@types/estree': 1.0.9
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 000000000..2d717ae3a
Binary files /dev/null and b/public/apple-touch-icon.png differ
diff --git a/public/bingo-cards/329K.png b/public/bingo-cards/329K.png
new file mode 100644
index 000000000..9f7437f05
Binary files /dev/null and b/public/bingo-cards/329K.png differ
diff --git a/public/bingo-cards/47YA.png b/public/bingo-cards/47YA.png
new file mode 100644
index 000000000..fb667958e
Binary files /dev/null and b/public/bingo-cards/47YA.png differ
diff --git a/public/bingo-cards/5G5U.png b/public/bingo-cards/5G5U.png
new file mode 100644
index 000000000..dcc0208ce
Binary files /dev/null and b/public/bingo-cards/5G5U.png differ
diff --git a/public/bingo-cards/6HKX.png b/public/bingo-cards/6HKX.png
new file mode 100644
index 000000000..435eaa46b
Binary files /dev/null and b/public/bingo-cards/6HKX.png differ
diff --git a/public/bingo-cards/7XX6.png b/public/bingo-cards/7XX6.png
new file mode 100644
index 000000000..a1b1a04cd
Binary files /dev/null and b/public/bingo-cards/7XX6.png differ
diff --git a/public/bingo-cards/99SR.png b/public/bingo-cards/99SR.png
new file mode 100644
index 000000000..ca7a8a467
Binary files /dev/null and b/public/bingo-cards/99SR.png differ
diff --git a/public/bingo-cards/HFVK.png b/public/bingo-cards/HFVK.png
new file mode 100644
index 000000000..2d9df3e2f
Binary files /dev/null and b/public/bingo-cards/HFVK.png differ
diff --git a/public/bingo-cards/JPYK.png b/public/bingo-cards/JPYK.png
new file mode 100644
index 000000000..f61cf6e7c
Binary files /dev/null and b/public/bingo-cards/JPYK.png differ
diff --git a/public/bingo-cards/JYUW.png b/public/bingo-cards/JYUW.png
new file mode 100644
index 000000000..8fac9e8ab
Binary files /dev/null and b/public/bingo-cards/JYUW.png differ
diff --git a/public/bingo-cards/PFKN.png b/public/bingo-cards/PFKN.png
new file mode 100644
index 000000000..487370354
Binary files /dev/null and b/public/bingo-cards/PFKN.png differ
diff --git a/public/bingo-cards/VMHN.png b/public/bingo-cards/VMHN.png
new file mode 100644
index 000000000..73e88f63c
Binary files /dev/null and b/public/bingo-cards/VMHN.png differ
diff --git a/public/bingo-cards/W3V8.png b/public/bingo-cards/W3V8.png
new file mode 100644
index 000000000..5b4b9159f
Binary files /dev/null and b/public/bingo-cards/W3V8.png differ
diff --git a/public/social-card-bingo.svg b/public/social-card-bingo.svg
new file mode 100644
index 000000000..ecd83b847
--- /dev/null
+++ b/public/social-card-bingo.svg
@@ -0,0 +1,6 @@
+
diff --git a/scripts/generate-bingo-pngs.mjs b/scripts/generate-bingo-pngs.mjs
new file mode 100644
index 000000000..2a99f8baa
--- /dev/null
+++ b/scripts/generate-bingo-pngs.mjs
@@ -0,0 +1,133 @@
+/**
+ * Generate OG image PNGs (1200×630) for all 12 winning bingo combinations.
+ * Runs after `astro build`. Saves to `public/bingo-cards/{code}.png`
+ */
+
+import { writeFileSync, mkdirSync, existsSync } from "fs";
+import { join, dirname } from "path";
+import sharp from "sharp";
+
+const OUT_DIR = join(import.meta.dirname, "..", "public", "bingo-cards");
+
+const BINGO_LINES = [
+ { code: "329K", label: "Row 1", cells: [0, 1, 2, 3, 4] },
+ { code: "7XX6", label: "Row 2", cells: [5, 6, 7, 8, 9] },
+ { code: "JYUW", label: "Row 3", cells: [10, 11, 12, 13, 14] },
+ { code: "5G5U", label: "Row 4", cells: [15, 16, 17, 18, 19] },
+ { code: "99SR", label: "Row 5", cells: [20, 21, 22, 23, 24] },
+ { code: "PFKN", label: "Col 1", cells: [0, 5, 10, 15, 20] },
+ { code: "HFVK", label: "Col 2", cells: [1, 6, 11, 16, 21] },
+ { code: "JPYK", label: "Col 3", cells: [2, 7, 12, 17, 22] },
+ { code: "W3V8", label: "Col 4", cells: [3, 8, 13, 18, 23] },
+ { code: "6HKX", label: "Col 5", cells: [4, 9, 14, 19, 24] },
+ { code: "VMHN", label: "Diagonal ↘", cells: [0, 6, 12, 18, 24] },
+ { code: "47YA", label: "Diagonal ↗", cells: [4, 8, 12, 16, 20] },
+];
+
+const EDITIONS = [
+ { year: 2002, city: "Charleroi" },
+ { year: 2003, city: "Charleroi" },
+ { year: 2004, city: "Gothenburg" },
+ { year: 2005, city: "Gothenburg" },
+ { year: 2006, city: "CERN, Geneva" },
+ { year: 2007, city: "Vilnius" },
+ { year: 2008, city: "Vilnius" },
+ { year: 2009, city: "Birmingham" },
+ { year: 2010, city: "Birmingham" },
+ { year: 2011, city: "Florence" },
+ { year: 2012, city: "Florence" },
+ { year: 2013, city: "Florence" },
+ { year: 2014, city: "Berlin" },
+ { year: 2015, city: "Bilbao" },
+ { year: 2016, city: "Bilbao" },
+ { year: 2017, city: "Rimini" },
+ { year: 2018, city: "Edinburgh" },
+ { year: 2019, city: "Basel" },
+ { year: 2020, city: "Online" },
+ { year: 2021, city: "Online" },
+ { year: 2022, city: "Dublin" },
+ { year: 2023, city: "Prague" },
+ { year: 2024, city: "Prague" },
+ { year: 2025, city: "Prague" },
+ { year: 2026, city: "Kraków" },
+];
+
+const W = 1200;
+const H = 630;
+
+// Card section — right side
+const CARD_LEFT = 660;
+const CARD_TOP = 40;
+const CELL = 88;
+const GAP = 5;
+const PAD = 16;
+const COLS = 5;
+
+const cardGridW = COLS * CELL + (COLS - 1) * GAP;
+const cardGridH = COLS * CELL + (COLS - 1) * GAP;
+const cardW = cardGridW + PAD * 2;
+const cardH = cardGridH + PAD * 2;
+const cardX = CARD_LEFT;
+const cardY = CARD_TOP;
+
+function generateSvg(line) {
+ const winSet = new Set(line.cells);
+ let cells = "";
+
+ // Card background
+ cells += `
+ {bingo + ? "🎉 BINGO! You completed a line!" + : checked.filter(Boolean).length === 0 + ? "Flip cards for editions you've attended" + : `${checked.filter(Boolean).length} of 25 flipped`} +
+ +🎉 BINGO!
+You completed a line!
+ ++ {EDITION_COUNT} editions · {UNIQUE_CITIES} cities · One community +
+Completed {line.label}
+