Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions src/components/sponsors.astro
Original file line number Diff line number Diff line change
@@ -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 ?? [];
Expand All @@ -25,7 +26,7 @@ const supporters: Sponsor[] = config.supporters ?? [];
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 justify-center place-items-center">
{sponsors.map((sponsor) => (
<a
href={sponsor.url}
href={sponsorUrl(sponsor.url, config.eventId)}
target="_blank"
rel="noopener noreferrer"
class="flex flex-col items-center p-4 transition-transform opacity-70 transform hover:scale-105 hover:opacity-100 w-full"
Expand Down Expand Up @@ -64,7 +65,7 @@ const supporters: Sponsor[] = config.supporters ?? [];
<div class="flex flex-wrap justify-center gap-6">
{supporters.map((supporter) => (
<a
href={supporter.url}
href={sponsorUrl(supporter.url, config.eventId)}
target="_blank"
rel="noopener noreferrer"
class="flex flex-col items-center p-4 transition-transform opacity-70 transform hover:scale-105 hover:opacity-100 w-full sm:w-[calc(33.333%-1rem)]"
Expand Down
1 change: 1 addition & 0 deletions src/data/configs/cologne-config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default {
eventId: "cologne-2026",
name: "",
sessionizeId: "o0aj9rpg",
sessionizeCfsUrl: "https://sessionize.com/mauiday-cologne-2026",
Expand Down
1 change: 1 addition & 0 deletions src/data/configs/default-config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default {
eventId: "", // Set to the event's archive-style ID to enable sponsor tracking.
name: "",
sessionizeId: "8cknf5zh",
sessionizeCfsUrl: "",
Expand Down
2 changes: 1 addition & 1 deletion src/data/configs/krakow-config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default {
eventId: "krakow-2026",
name: "",
sessionizeId: "0owlbzma",
sessionizeCfsUrl: "https://sessionize.com/maui-day-krakow-2026",
Expand Down Expand Up @@ -92,4 +93,3 @@ export default {
}
],
};

2 changes: 1 addition & 1 deletion src/data/configs/london-config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default {
eventId: "london-2026",
name: "",
sessionizeId: "sm7afub1",
sessionizeCfsUrl: "https://sessionize.com/maui-day-london-2026",
Expand Down Expand Up @@ -72,4 +73,3 @@ export default {
// }
],
};

1 change: 1 addition & 0 deletions src/data/configs/skopje-config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default {
eventId: "skopje-2026",
name: "Skopje",
sessionizeId: "q9b8reb9",
sessionizeCfsUrl: "https://sessionize.com/maui-day-skopje-2026/",
Expand Down
5 changes: 3 additions & 2 deletions src/pages/archive/[event].astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Layout from "../../layouts/Layout.astro";
import { getArchiveEvents } from "../../data/archive";
import type { ArchiveEvent } from "../../data/archive";
import { sponsorUrl } from "../../utils/sponsorUrl";

export async function getStaticPaths() {
const events = getArchiveEvents();
Expand Down Expand Up @@ -217,7 +218,7 @@ function sessionDuration(start: string, end: string) {
<h2 class="text-brand-dark text-3xl uppercase font-header font-bold mb-12 text-center">Sponsors</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6 items-center justify-items-center">
{event.sponsors.map((sponsor) => (
<a href={sponsor.url} target="_blank" rel="noopener noreferrer" class="block p-4 hover:opacity-80 transition">
<a href={sponsorUrl(sponsor.url, event.id)} target="_blank" rel="noopener noreferrer" class="block p-4 hover:opacity-80 transition">
<img
src={`/images/sponsors/${sponsor.logo}.png`}
alt={sponsor.name}
Expand All @@ -242,7 +243,7 @@ function sessionDuration(start: string, end: string) {
</p>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6 items-center justify-items-center">
{supporters.map((supporter) => (
<a href={supporter.url} target="_blank" rel="noopener noreferrer" class="block p-4 hover:opacity-80 transition">
<a href={sponsorUrl(supporter.url, event.id)} target="_blank" rel="noopener noreferrer" class="block p-4 hover:opacity-80 transition">
<img
src={`/images/sponsors/${supporter.logo}.png`}
alt={supporter.name}
Expand Down
12 changes: 12 additions & 0 deletions src/utils/sponsorUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function sponsorUrl(url: string, eventId?: string | null): string {
if (!eventId?.trim()) {
return url;
}

const trackedUrl = new URL(url);
trackedUrl.searchParams.set("utm_source", "maui-day");
trackedUrl.searchParams.set("utm_medium", "website");
trackedUrl.searchParams.set("utm_campaign", eventId);
trackedUrl.searchParams.set("utm_content", "sponsor-section");
return trackedUrl.toString();
}
65 changes: 65 additions & 0 deletions tests/sponsor-url.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import assert from "node:assert/strict";
import { readdir, readFile } from "node:fs/promises";
import test from "node:test";
import { sponsorUrl } from "../src/utils/sponsorUrl.ts";

test("matches the requested sponsor URL", () => {
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);
}
}
});
Loading