@m13v/seo-components 0.38.2 → 0.38.4
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
|
@@ -82,6 +82,21 @@ export interface InstallEmailGateProps {
|
|
|
82
82
|
sentTitle?: string;
|
|
83
83
|
/** Stage 2 (sent) body copy when emailOnly is true. Receives the submitted email. */
|
|
84
84
|
sentDescription?: (email: string) => React.ReactNode;
|
|
85
|
+
/**
|
|
86
|
+
* Redirect-after-submit mode. When true, the submit POST is expected to
|
|
87
|
+
* return JSON `{ url: string }` (e.g. a Stripe Checkout session URL). On
|
|
88
|
+
* success the browser is redirected to that URL instead of advancing the
|
|
89
|
+
* modal to "command" or "sent". Used by paid-checkout flows where the
|
|
90
|
+
* email gate hands off to an external billing page. When set, takes
|
|
91
|
+
* precedence over `emailOnly`.
|
|
92
|
+
*/
|
|
93
|
+
redirectOnSuccess?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Extra fields merged into the submit POST body alongside `{ email }`.
|
|
96
|
+
* Useful for passing `section`, UTMs, or product identifiers to the
|
|
97
|
+
* checkout endpoint.
|
|
98
|
+
*/
|
|
99
|
+
submitExtras?: Record<string, unknown>;
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
type Stage = "closed" | "email" | "command" | "sent";
|
|
@@ -108,6 +123,8 @@ export function InstallEmailGate({
|
|
|
108
123
|
emailOnly = false,
|
|
109
124
|
sentTitle = "Check your inbox",
|
|
110
125
|
sentDescription,
|
|
126
|
+
redirectOnSuccess = false,
|
|
127
|
+
submitExtras,
|
|
111
128
|
}: InstallEmailGateProps) {
|
|
112
129
|
const capture = useCapture();
|
|
113
130
|
const [stage, setStage] = useState<Stage>("closed");
|
|
@@ -166,10 +183,12 @@ export function InstallEmailGate({
|
|
|
166
183
|
setSubmitting(true);
|
|
167
184
|
setError("");
|
|
168
185
|
try {
|
|
186
|
+
const body: Record<string, unknown> = { email: trimmed };
|
|
187
|
+
if (submitExtras) Object.assign(body, submitExtras);
|
|
169
188
|
const res = await fetch(newsletterPath, {
|
|
170
189
|
method: "POST",
|
|
171
190
|
headers: { "Content-Type": "application/json" },
|
|
172
|
-
body: JSON.stringify(
|
|
191
|
+
body: JSON.stringify(body),
|
|
173
192
|
});
|
|
174
193
|
if (res.status >= 400 && res.status < 500) {
|
|
175
194
|
const data = await res.json().catch(() => ({}));
|
|
@@ -178,9 +197,14 @@ export function InstallEmailGate({
|
|
|
178
197
|
return;
|
|
179
198
|
}
|
|
180
199
|
if (!res.ok) {
|
|
181
|
-
console.warn("[InstallEmailGate]
|
|
200
|
+
console.warn("[InstallEmailGate] submit POST failed", res.status);
|
|
182
201
|
}
|
|
183
|
-
|
|
202
|
+
const delivery = redirectOnSuccess
|
|
203
|
+
? "redirect"
|
|
204
|
+
: emailOnly
|
|
205
|
+
? "email_only"
|
|
206
|
+
: "page_reveal";
|
|
207
|
+
if (remember && !redirectOnSuccess) markInstallEmailCaptured(trimmed, storageKey);
|
|
184
208
|
capture("newsletter_subscribed", {
|
|
185
209
|
component: "InstallEmailGate",
|
|
186
210
|
email: trimmed,
|
|
@@ -188,25 +212,51 @@ export function InstallEmailGate({
|
|
|
188
212
|
site,
|
|
189
213
|
section,
|
|
190
214
|
source: "install_gate",
|
|
191
|
-
delivery
|
|
215
|
+
delivery,
|
|
192
216
|
});
|
|
193
217
|
trackGetStartedClick({
|
|
194
|
-
destination:
|
|
218
|
+
destination: redirectOnSuccess
|
|
219
|
+
? "redirect:checkout"
|
|
220
|
+
: emailOnly
|
|
221
|
+
? "email:install"
|
|
222
|
+
: "modal:command",
|
|
195
223
|
site,
|
|
196
224
|
section,
|
|
197
225
|
text: "email-submitted",
|
|
198
226
|
component: "InstallEmailGate",
|
|
199
|
-
extra: { email: trimmed, delivery
|
|
227
|
+
extra: { email: trimmed, delivery },
|
|
200
228
|
});
|
|
229
|
+
|
|
230
|
+
if (redirectOnSuccess) {
|
|
231
|
+
const data = (await res.json().catch(() => ({}))) as { url?: string };
|
|
232
|
+
if (data.url && typeof window !== "undefined") {
|
|
233
|
+
window.location.href = data.url;
|
|
234
|
+
// Keep the modal in the submitting state while the browser navigates
|
|
235
|
+
// away so the user does not see a flash of stale UI.
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
setError("No checkout URL returned. Try again.");
|
|
239
|
+
setSubmitting(false);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
201
243
|
setSubmittedEmail(trimmed);
|
|
202
244
|
setStage(emailOnly ? "sent" : "command");
|
|
203
245
|
} catch (err) {
|
|
204
|
-
console.warn("[InstallEmailGate]
|
|
246
|
+
console.warn("[InstallEmailGate] submit POST network error", err);
|
|
247
|
+
if (redirectOnSuccess) {
|
|
248
|
+
setError("Network error. Try again.");
|
|
249
|
+
setSubmitting(false);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
205
252
|
if (remember) markInstallEmailCaptured(trimmed, storageKey);
|
|
206
253
|
setSubmittedEmail(trimmed);
|
|
207
254
|
setStage(emailOnly ? "sent" : "command");
|
|
208
255
|
} finally {
|
|
209
|
-
|
|
256
|
+
// For redirect mode we returned early above on success; the browser
|
|
257
|
+
// is navigating away so we intentionally do not flip submitting back
|
|
258
|
+
// to false. For other modes, the stage transition is enough.
|
|
259
|
+
if (!redirectOnSuccess) setSubmitting(false);
|
|
210
260
|
}
|
|
211
261
|
};
|
|
212
262
|
|
|
@@ -46,8 +46,17 @@ export interface DmShortLinkRedirectConfig {
|
|
|
46
46
|
* (Google publishes this; any real Android user is on a current device).
|
|
47
47
|
* - AI-Innovation-Radar, a named AI/news scraper.
|
|
48
48
|
* - AFMMainUI/Darwin, Apple's Ads-For-Messages link prefetch (no human tap).
|
|
49
|
+
*
|
|
50
|
+
* 2026-05-10: extended after a 24h click audit found four leak patterns
|
|
51
|
+
* sitting in the human pile:
|
|
52
|
+
* - LinkResolver/1.0 and link-resolver/1.0 (named link-unfurl bots,
|
|
53
|
+
* no "bot/crawler" token in the UA).
|
|
54
|
+
* - Bare "Mozilla/5.0 (compatible)" with no engine info (no real browser
|
|
55
|
+
* ever sends just this; spoofed minimal fingerprint).
|
|
56
|
+
* - curl/<version> (generic HTTP client; "axios/" was already caught,
|
|
57
|
+
* curl was missed).
|
|
49
58
|
*/
|
|
50
|
-
const BOT_UA_RE = /bot|crawler|spider|Twitterbot|LinkedInBot|Slackbot|facebookexternalhit|Discordbot|TelegramBot|WhatsApp|Applebot|Googlebot|Bingbot|YandexBot|DuckDuckBot|redditbot|Pinterest|Embedly|Snapchat|axios\/|python-requests|python-urllib|node-fetch|^node$|dataminr|Anthill|^Mozilla\/5\.0$|Chrome\/70\.|Nexus 5X Build\/MMB29P|AI-Innovation-Radar|^AFMMainUI
|
|
59
|
+
const BOT_UA_RE = /bot|crawler|spider|Twitterbot|LinkedInBot|Slackbot|facebookexternalhit|Discordbot|TelegramBot|WhatsApp|Applebot|Googlebot|Bingbot|YandexBot|DuckDuckBot|redditbot|Pinterest|Embedly|Snapchat|axios\/|python-requests|python-urllib|node-fetch|^node$|dataminr|Anthill|^Mozilla\/5\.0$|Chrome\/70\.|Nexus 5X Build\/MMB29P|AI-Innovation-Radar|^AFMMainUI|LinkResolver|link-resolver|^Mozilla\/5\.0 \(compatible\)$|^curl\//i;
|
|
51
60
|
|
|
52
61
|
/**
|
|
53
62
|
* Factory for `GET /r/[code]`.
|