@m13v/seo-components 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/seo-components",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "scripts": {
5
5
  "build:css": "tailwind -i src/_build.css -o dist/styles.css --minify",
6
6
  "lint:mobile-spans": "node scripts/lint-mobile-spans.mjs",
@@ -19,18 +19,27 @@ export interface DmShortLinkRedirectConfig {
19
19
  /**
20
20
  * Factory for `GET /r/[code]`.
21
21
  *
22
- * Each per-DM short link maps to a Cal.com / Calendly URL with full UTM and
23
- * `metadata[utm_*]` so cal_bookings closes the loop on which DM produced the
24
- * booking. The cached `target_url` is frozen at mint time on the dms row, so
25
- * the resolver is a single DB read with no config.json dependency.
22
+ * Each short link maps to a destination URL. Two rails are supported:
23
+ *
24
+ * DM rail: code is minted from dm_links. Target is a Cal.com / Calendly URL
25
+ * with full UTM and metadata[utm_*] so cal_bookings closes the loop on which
26
+ * DM produced the booking. Fires `dm_short_link_clicked` in PostHog.
27
+ *
28
+ * Post rail: code is minted from post_links (public posts/comments). Target
29
+ * is typically the product homepage or a landing page. UTM params are injected
30
+ * at redirect time (utm_source, utm_medium, utm_campaign, utm_content) so
31
+ * PostHog can stitch the full funnel: post click -> get_started_click ->
32
+ * schedule_click -> checkout_success. Fires `post_short_link_clicked` in
33
+ * PostHog.
26
34
  *
27
35
  * Behavior:
28
36
  * 1. Read `code` from the route param. Reject non-alphanumeric / wrong-length.
29
- * 2. Hit `<resolverBase>/api/short-links/<code>`. The resolver increments
30
- * dms.short_link_clicks and stamps first/last click timestamps.
31
- * 3. Fire a PostHog `dm_short_link_clicked` event with dm_id, project,
32
- * platform, code, site.
33
- * 4. 302 to the resolved target_url. On miss/error, 302 to "/".
37
+ * 2. Hit `<resolverBase>/api/short-links/<code>`. The resolver increments the
38
+ * click counter and stamps first/last click timestamps.
39
+ * 3. For post rail links: inject UTM params into the target URL.
40
+ * 4. Fire the appropriate PostHog event (dm_short_link_clicked or
41
+ * post_short_link_clicked) fire-and-forget, non-blocking.
42
+ * 5. 302 to the resolved target_url. On miss/error, 302 to "/".
34
43
  */
35
44
  export function createDmShortLinkRedirectHandler(config: DmShortLinkRedirectConfig) {
36
45
  const {
@@ -55,6 +64,8 @@ export function createDmShortLinkRedirectHandler(config: DmShortLinkRedirectConf
55
64
 
56
65
  let target: string | null = null;
57
66
  let dmId: number | null = null;
67
+ let postId: number | null = null;
68
+ let replyId: number | null = null;
58
69
  let project: string | null = null;
59
70
  let platform: string | null = null;
60
71
 
@@ -67,12 +78,16 @@ export function createDmShortLinkRedirectHandler(config: DmShortLinkRedirectConf
67
78
  const body = (await resp.json()) as {
68
79
  target_url?: string;
69
80
  dm_id?: number;
81
+ post_id?: number;
82
+ reply_id?: number;
70
83
  project?: string;
71
84
  platform?: string;
72
85
  };
73
86
  if (body.target_url) {
74
87
  target = body.target_url;
75
88
  dmId = body.dm_id ?? null;
89
+ postId = body.post_id ?? null;
90
+ replyId = body.reply_id ?? null;
76
91
  project = body.project ?? null;
77
92
  platform = body.platform ?? null;
78
93
  }
@@ -81,8 +96,29 @@ export function createDmShortLinkRedirectHandler(config: DmShortLinkRedirectConf
81
96
  console.error("[dm-short-link/redirect] resolver fetch failed:", err);
82
97
  }
83
98
 
99
+ // For post rail links (public posts/comments), inject UTM params so
100
+ // PostHog can stitch click -> conversion events. DM rail links already
101
+ // have Cal.com metadata[utm_*] attribution embedded at mint time, so we
102
+ // leave those URLs untouched.
103
+ if (target && (postId != null || replyId != null)) {
104
+ try {
105
+ const targetUrl = new URL(target);
106
+ if (!targetUrl.searchParams.has("utm_source")) {
107
+ if (platform) targetUrl.searchParams.set("utm_source", platform);
108
+ targetUrl.searchParams.set("utm_medium", "social");
109
+ if (project) targetUrl.searchParams.set("utm_campaign", project);
110
+ targetUrl.searchParams.set("utm_content", code);
111
+ target = targetUrl.toString();
112
+ }
113
+ } catch {
114
+ // Keep original target if URL parsing fails (e.g. non-HTTP scheme).
115
+ }
116
+ }
117
+
84
118
  const posthogKey = process.env[posthogKeyEnv];
85
119
  const posthogHost = (process.env[posthogHostEnv] || "https://us.i.posthog.com").replace(/\/+$/, "");
120
+
121
+ // DM rail event
86
122
  if (target && posthogKey && dmId != null) {
87
123
  fetch(`${posthogHost}/i/v0/e/`, {
88
124
  method: "POST",
@@ -99,6 +135,30 @@ export function createDmShortLinkRedirectHandler(config: DmShortLinkRedirectConf
99
135
  );
100
136
  }
101
137
 
138
+ // Post rail event
139
+ if (target && posthogKey && (postId != null || replyId != null)) {
140
+ fetch(`${posthogHost}/i/v0/e/`, {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({
144
+ api_key: posthogKey,
145
+ event: "post_short_link_clicked",
146
+ distinct_id: `post_${postId ?? replyId}`,
147
+ timestamp: new Date().toISOString(),
148
+ properties: {
149
+ post_id: postId,
150
+ reply_id: replyId,
151
+ project,
152
+ platform,
153
+ code,
154
+ site,
155
+ },
156
+ }),
157
+ }).catch((err) =>
158
+ console.error("[dm-short-link/redirect] posthog fetch failed:", err)
159
+ );
160
+ }
161
+
102
162
  return Response.redirect(target || homeUrl, 302);
103
163
  };
104
164
  }
@@ -124,6 +124,7 @@ export interface NewsletterConfig {
124
124
  onSignup?: (email: string, resendEmailId: string | null) => Promise<void>;
125
125
  }
126
126
 
127
+
127
128
  /* ------------------------------------------------------------------ */
128
129
  /* Factory */
129
130
  /* ------------------------------------------------------------------ */