Skip to content

Commit 3693351

Browse files
authored
fix: bring short tracking links to main
* shorten email tracking links via nuxt server routes * refactor: move tracking redirect helper to shared edge utils * fix: remove unnecessary async in short-link routes
1 parent 5182ab4 commit 3693351

File tree

7 files changed

+113
-25
lines changed

7 files changed

+113
-25
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { buildEmailCampaignEdgeUrl } from '../../utils/email-campaign-proxy';
2+
3+
export default defineEventHandler((event) => {
4+
const token = getRouterParam(event, 'token');
5+
if (!token) {
6+
throw createError({ statusCode: 400, statusMessage: 'Missing token' });
7+
}
8+
9+
const targetUrl = buildEmailCampaignEdgeUrl(
10+
event,
11+
`/track/click/${encodeURIComponent(token)}`,
12+
);
13+
14+
return proxyRequest(event, targetUrl);
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { buildEmailCampaignEdgeUrl } from '../../utils/email-campaign-proxy';
2+
3+
export default defineEventHandler((event) => {
4+
const token = getRouterParam(event, 'token');
5+
if (!token) {
6+
throw createError({ statusCode: 400, statusMessage: 'Missing token' });
7+
}
8+
9+
const targetUrl = buildEmailCampaignEdgeUrl(
10+
event,
11+
`/track/open/${encodeURIComponent(token)}`,
12+
);
13+
14+
return proxyRequest(event, targetUrl);
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { buildEmailCampaignEdgeUrl } from '../../utils/email-campaign-proxy';
2+
3+
export default defineEventHandler((event) => {
4+
const token = getRouterParam(event, 'token');
5+
if (!token) {
6+
throw createError({ statusCode: 400, statusMessage: 'Missing token' });
7+
}
8+
9+
const targetUrl = buildEmailCampaignEdgeUrl(
10+
event,
11+
`/unsubscribe/${encodeURIComponent(token)}`,
12+
);
13+
14+
return proxyRequest(event, targetUrl);
15+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { H3Event } from "h3";
2+
3+
export function buildEmailCampaignEdgeUrl(
4+
event: H3Event,
5+
path: string,
6+
): string {
7+
const config = useRuntimeConfig(event);
8+
const baseUrl =
9+
config.public?.SAAS_SUPABASE_PROJECT_URL || process.env.SAAS_SUPABASE_PROJECT_URL;
10+
11+
if (!baseUrl) {
12+
throw createError({
13+
statusCode: 500,
14+
statusMessage: "Missing SAAS_SUPABASE_PROJECT_URL",
15+
});
16+
}
17+
18+
const normalizedBase = String(baseUrl).replace(/\/$/, "");
19+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
20+
return `${normalizedBase}/functions/v1/email-campaigns${normalizedPath}`;
21+
}

supabase/functions/_shared/http.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import corsHeaders from "./cors.ts";
2+
3+
export const buildRedirectResponse = (location: string): Response => {
4+
const headers = new Headers(corsHeaders);
5+
headers.set("Location", location);
6+
7+
return new Response(null, {
8+
status: 302,
9+
headers,
10+
});
11+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
2+
import corsHeaders from "../_shared/cors.ts";
3+
import { buildRedirectResponse } from "../_shared/http.ts";
4+
5+
Deno.test("buildRedirectResponse returns redirect with cors headers", () => {
6+
const location = "https://example.com/redirect";
7+
const response = buildRedirectResponse(location);
8+
9+
assertEquals(response.status, 302);
10+
assertEquals(response.headers.get("Location"), location);
11+
12+
Object.entries(corsHeaders).forEach(([key, value]) => {
13+
assertEquals(response.headers.get(key), value);
14+
});
15+
});

supabase/functions/email-campaigns/index.ts

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { normalizeEmail } from "../_shared/email.ts";
1010
import { resolveCampaignBaseUrlFromEnv } from "../_shared/url.ts";
1111
import { fillTemplate } from "../_shared/mailing/template.ts";
1212
import { sendEmail, verifyTransport } from "./email.ts";
13+
import { buildRedirectResponse } from "../_shared/http.ts";
1314
import {
1415
getSenderCredentialIssue,
1516
isTokenExpired,
@@ -375,7 +376,18 @@ function toHtmlFromText(template: string): string {
375376
}
376377

377378
function buildUnsubscribeUrl(token: string): string {
378-
return `${PUBLIC_CAMPAIGN_BASE_URL}/functions/v1/email-campaigns/unsubscribe/${token}`;
379+
const base = (FRONTEND_HOST || PUBLIC_CAMPAIGN_BASE_URL).replace(/\/$/, "");
380+
return `${base}/u/${token}`;
381+
}
382+
383+
function buildOpenTrackingUrl(token: string): string {
384+
const base = (FRONTEND_HOST || PUBLIC_CAMPAIGN_BASE_URL).replace(/\/$/, "");
385+
return `${base}/o/${token}`;
386+
}
387+
388+
function buildClickTrackingUrl(token: string): string {
389+
const base = (FRONTEND_HOST || PUBLIC_CAMPAIGN_BASE_URL).replace(/\/$/, "");
390+
return `${base}/c/${token}`;
379391
}
380392

381393
async function triggerCampaignProcessorFromEdge() {
@@ -1041,7 +1053,7 @@ async function injectTrackers(
10411053
recipientId,
10421054
originalUrl,
10431055
);
1044-
const trackedUrl = `${PUBLIC_CAMPAIGN_BASE_URL}/functions/v1/email-campaigns/track/click/${token}`;
1056+
const trackedUrl = buildClickTrackingUrl(token);
10451057

10461058
// Replace both quoted-printable encoded (href=3D"...") and regular (href="...")
10471059
updatedHtml = updatedHtml.replace(
@@ -1055,7 +1067,7 @@ async function injectTrackers(
10551067
}
10561068

10571069
if (trackOpen) {
1058-
const pixelUrl = `${PUBLIC_CAMPAIGN_BASE_URL}/functions/v1/email-campaigns/track/open/${openToken}`;
1070+
const pixelUrl = buildOpenTrackingUrl(openToken);
10591071
updatedHtml += `<img src="${pixelUrl}" alt="" width="1" height="1" style="display:none" />`;
10601072
}
10611073

@@ -2039,13 +2051,9 @@ app.get("/unsubscribe/:token", async (c: Context) => {
20392051
const supabaseAdmin = createSupabaseAdmin();
20402052

20412053
if (token === "preview-unsubscribe") {
2042-
return new Response(null, {
2043-
status: 302,
2044-
headers: {
2045-
...corsHeaders,
2046-
Location: `${FRONTEND_HOST}/unsubscribe/success?preview=true`,
2047-
},
2048-
});
2054+
return buildRedirectResponse(
2055+
`${FRONTEND_HOST}/unsubscribe/success?preview=true`,
2056+
);
20492057
}
20502058

20512059
const { data: recipient, error } = await supabaseAdmin
@@ -2056,13 +2064,7 @@ app.get("/unsubscribe/:token", async (c: Context) => {
20562064
.single();
20572065

20582066
if (error || !recipient) {
2059-
return new Response(null, {
2060-
status: 302,
2061-
headers: {
2062-
...corsHeaders,
2063-
Location: `${FRONTEND_HOST}/unsubscribe/failure`,
2064-
},
2065-
});
2067+
return buildRedirectResponse(`${FRONTEND_HOST}/unsubscribe/failure`);
20662068
}
20672069

20682070
await supabaseAdmin
@@ -2110,13 +2112,7 @@ app.get("/unsubscribe/:token", async (c: Context) => {
21102112
)}`
21112113
: `${FRONTEND_HOST}/unsubscribe/success`;
21122114

2113-
return new Response(null, {
2114-
status: 302,
2115-
headers: {
2116-
...corsHeaders,
2117-
Location: successUrl,
2118-
},
2119-
});
2115+
return buildRedirectResponse(successUrl);
21202116
});
21212117

21222118
app.get("/track/open/:token", async (c: Context) => {
@@ -2174,7 +2170,7 @@ app.get("/track/click/:token", async (c: Context) => {
21742170
url: link.url,
21752171
});
21762172

2177-
return c.redirect(link.url, 302);
2173+
return buildRedirectResponse(link.url);
21782174
});
21792175

21802176
app.post("/email-sending-request", authMiddleware, async (c: Context) => {

0 commit comments

Comments
 (0)