@m13v/seo-components 0.32.0 → 0.32.2

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.32.0",
3
+ "version": "0.32.2",
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",
@@ -120,13 +120,18 @@ export function InstallEmailGate({
120
120
 
121
121
  const onOpen = () => {
122
122
  const skip = remember && hasCapturedInstallEmail(storageKey);
123
- trackGetStartedClick({
124
- destination: skip ? "modal:command" : "modal:email",
125
- site,
126
- section,
127
- text: label,
128
- component: "InstallEmailGate",
129
- });
123
+ if (skip) {
124
+ // Gate already passed previously: fire the canonical funnel event for
125
+ // the gated-passed click. No event when the gate is fresh and we're
126
+ // about to ask for the email; that fires on submit instead.
127
+ trackGetStartedClick({
128
+ destination: "modal:command",
129
+ site,
130
+ section,
131
+ text: label,
132
+ component: "InstallEmailGate",
133
+ });
134
+ }
130
135
  setStage(skip ? "command" : "email");
131
136
  setError("");
132
137
  };
@@ -186,12 +191,16 @@ export function InstallEmailGate({
186
191
  try {
187
192
  await navigator.clipboard.writeText(text);
188
193
  setCopied(which);
189
- trackGetStartedClick({
190
- destination: which === "command" ? "clipboard:command" : "clipboard:config",
194
+ // Track the copy as its own event so it does not inflate the
195
+ // canonical `get_started_click` funnel. The gate event fires once
196
+ // when the email is submitted (or once on each subsequent gated-
197
+ // passed click), not for every clipboard copy.
198
+ capture("install_command_copied", {
199
+ component: "InstallEmailGate",
191
200
  site,
192
201
  section,
193
- text: which === "command" ? command : configBlock?.label,
194
- component: "InstallEmailGate",
202
+ which,
203
+ page: typeof window !== "undefined" ? window.location.pathname : undefined,
195
204
  });
196
205
  setTimeout(() => setCopied(null), 1800);
197
206
  } catch {
@@ -91,6 +91,14 @@ export interface BookCallConfig {
91
91
  emailSubject?: string;
92
92
  /** Override the email HTML. Receives the email-click URL and the submitted email. */
93
93
  emailHtml?: (emailClickUrl: string, subscriberEmail: string) => string;
94
+ /**
95
+ * Optional: log the outbound send. Called after the Resend send call returns;
96
+ * receives the subscriber email and the Resend email id (or null on failure).
97
+ * Errors thrown here are logged and swallowed so a flaky DB doesn't break the
98
+ * booking flow. Required for sites that want delivery / open / click webhook
99
+ * events to update the right `<slug>_emails` row by `resend_id`.
100
+ */
101
+ onSent?: (email: string, resendEmailId: string | null) => Promise<void>;
94
102
  }
95
103
 
96
104
  /* ------------------------------------------------------------------ */
@@ -121,6 +129,7 @@ export function createBookCallHandler(config: BookCallConfig) {
121
129
  apiKeyEnv = "RESEND_API_KEY",
122
130
  emailSubject,
123
131
  emailHtml,
132
+ onSent,
124
133
  } = config;
125
134
 
126
135
  return async function POST(req: NextRequest) {
@@ -175,14 +184,43 @@ export function createBookCallHandler(config: BookCallConfig) {
175
184
  ? emailHtml(emailClickUrl, email)
176
185
  : defaultBookCallEmailHtml(brand, siteUrl, emailClickUrl);
177
186
 
178
- fetch("https://api.resend.com/emails", {
179
- method: "POST",
180
- headers: {
181
- Authorization: `Bearer ${resendKey}`,
182
- "Content-Type": "application/json",
183
- },
184
- body: JSON.stringify({ from: fromEmail, to: email, subject, html }),
185
- }).catch((err) => console.error("[book-call] email send threw:", err));
187
+ if (onSent) {
188
+ // Sequential so the `onSent` callback receives the actual Resend id.
189
+ try {
190
+ const sendRes = await fetch("https://api.resend.com/emails", {
191
+ method: "POST",
192
+ headers: {
193
+ Authorization: `Bearer ${resendKey}`,
194
+ "Content-Type": "application/json",
195
+ },
196
+ body: JSON.stringify({ from: fromEmail, to: email, subject, html }),
197
+ });
198
+ let resendEmailId: string | null = null;
199
+ if (sendRes.ok) {
200
+ const data = (await sendRes.json().catch(() => ({}))) as { id?: string };
201
+ resendEmailId = data.id || null;
202
+ } else {
203
+ const detail = await sendRes.text().catch(() => "");
204
+ console.error("[book-call] email send failed:", sendRes.status, detail);
205
+ }
206
+ try {
207
+ await onSent(email, resendEmailId);
208
+ } catch (err) {
209
+ console.error("[book-call] onSent callback error:", err);
210
+ }
211
+ } catch (err) {
212
+ console.error("[book-call] email send threw:", err);
213
+ }
214
+ } else {
215
+ fetch("https://api.resend.com/emails", {
216
+ method: "POST",
217
+ headers: {
218
+ Authorization: `Bearer ${resendKey}`,
219
+ "Content-Type": "application/json",
220
+ },
221
+ body: JSON.stringify({ from: fromEmail, to: email, subject, html }),
222
+ }).catch((err) => console.error("[book-call] email send threw:", err));
223
+ }
186
224
 
187
225
  return new Response(
188
226
  JSON.stringify({ ok: true }),