diff --git a/README.md b/README.md index caf59f5..fceb853 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,23 @@ To add new sections (for example `Food`, `Places`, `Transport`) or new cards, ed The page renders all categories and items automatically. +## Sponsor link tracking + +Sponsor and supporter links automatically include `utm_source=maui-day`, +`utm_medium=website`, `utm_content=sponsor-section`, and an event-specific +`utm_campaign` (for example, `cologne-2026`). Existing non-UTM query parameters +and URL fragments are preserved; these four UTM parameters are replaced if +already present. + +Each named conference config in `src/data/configs/` defines an `eventId` using +the archive ID format (`{city}-{year}`, with a month when needed for +disambiguation). Update it when reusing a config for a new edition. The generic +`default-config.mjs` has no event identity: fill in its `eventId` to enable +tracking. If the ID is missing, empty, or whitespace-only, links use their +original URLs unchanged. +Archive pages use the snapshot's existing `id`, so no archive data changes are +needed. Sponsor contact email links are not tracked. + ## 🚀 Project Structure Inside of this Astro project, you'll see the following folders and files: diff --git a/src/components/sponsors.astro b/src/components/sponsors.astro index dae5f76..c1d60cc 100644 --- a/src/components/sponsors.astro +++ b/src/components/sponsors.astro @@ -1,6 +1,7 @@ --- import loadConfig from "../configLoader"; import type { Sponsor } from "../types"; +import { sponsorUrl } from "../utils/sponsorUrl"; const config = Astro.props.config ?? await loadConfig(); const sponsors: Sponsor[] = config.sponsors ?? []; @@ -25,7 +26,7 @@ const supporters: Sponsor[] = config.supporters ?? [];
{sponsors.map((sponsor) => ( {supporters.map((supporter) => ( Sponsors
{event.sponsors.map((sponsor) => ( - + {sponsor.name}
{supporters.map((supporter) => ( - + {supporter.name} { + assert.equal( + sponsorUrl("https://grialkit.com/", "cologne-2026"), + "https://grialkit.com/?utm_source=maui-day&utm_medium=website&utm_campaign=cologne-2026&utm_content=sponsor-section", + ); +}); + +test("preserves destination parameters and fragments while replacing tracking", () => { + const url = new URL(sponsorUrl( + "https://example.com/maui?product=ui&utm_source=old&utm_campaign=old&utm_campaign=duplicate&utm_medium=email&utm_content=logo#pricing", + "london-2026", + )); + assert.equal(url.pathname, "/maui"); + assert.equal(url.hash, "#pricing"); + assert.deepEqual([...url.searchParams], [ + ["product", "ui"], + ["utm_source", "maui-day"], + ["utm_campaign", "london-2026"], + ["utm_medium", "website"], + ["utm_content", "sponsor-section"], + ]); + assert.equal(sponsorUrl(url.href, "london-2026"), url.href); +}); + +test("returns the original URL unchanged when the event identity is absent", () => { + const url = "https://example.com/maui?product=ui&utm_source=existing#pricing"; + for (const eventId of [undefined, null, "", " ", "\t\n"]) { + assert.equal(sponsorUrl(url, eventId), url); + } + assert.equal(sponsorUrl("https://example.com"), "https://example.com"); +}); + +test("rejects invalid URLs when adding tracking", () => { + assert.throws(() => sponsorUrl("not a URL", "cologne-2026"), TypeError); +}); + +test("all named conference sponsor and supporter links have event-specific tracking", async () => { + const directory = new URL("../src/data/configs/", import.meta.url); + for (const filename of await readdir(directory)) { + if (!filename.endsWith("-config.mjs") || filename === "default-config.mjs") continue; + const { default: config } = await import(new URL(filename, directory)); + assert.equal(config.eventId, `${filename.replace("-config.mjs", "")}-2026`); + for (const sponsor of [...config.sponsors, ...(config.supporters ?? [])]) { + const url = new URL(sponsorUrl(sponsor.url, config.eventId)); + assert.equal(url.searchParams.get("utm_campaign"), config.eventId); + } + } +}); + +test("all archive sponsor and supporter links use the snapshot identity", async () => { + const directory = new URL("../src/data/archive/", import.meta.url); + for (const filename of await readdir(directory)) { + if (!filename.endsWith(".json")) continue; + const event = JSON.parse(await readFile(new URL(filename, directory), "utf8")); + for (const sponsor of [...event.sponsors, ...(event.supporters ?? [])]) { + const url = new URL(sponsorUrl(sponsor.url, event.id)); + assert.equal(url.searchParams.get("utm_campaign"), event.id); + } + } +});