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 ?? [];
{
+ 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);
+ }
+ }
+});