@m13v/seo-components 0.34.1 → 0.36.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 +1 -1
- package/registry.json +6 -0
- package/src/components/NewsStrip.tsx +260 -0
- package/src/index.ts +4 -0
- package/src/lib/dm-short-link-redirect.ts +17 -2
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -290,6 +290,12 @@
|
|
|
290
290
|
"quotaTags": ["visual-rich-mandatory"]
|
|
291
291
|
},
|
|
292
292
|
|
|
293
|
+
{
|
|
294
|
+
"name": "NewsStrip",
|
|
295
|
+
"group": "cta",
|
|
296
|
+
"signature": "(href, lead, wedge, pillText?, ctaLabel?, tone?: \"amber\" | \"teal\" | \"rose\" | \"indigo\", site?, section?, datePublished?)",
|
|
297
|
+
"description": "Pinned, pill-anchored announcement strip that sits ABOVE the hero on the homepage and routes existing organic clickthroughs to a freshly minted /t/ analysis. Pattern: a viral post on Twitter/Reddit drives baseline traffic to '/'. Once a dedicated /t/ page ships, every subsequent visitor needs a one-click path from '/' to the deep page. Render at top of homepage, target the new /t/ slug. Tone defaults to amber for breaking news. The seo_top_posts pipeline mounts this automatically when it ships a page from a viral post."
|
|
298
|
+
},
|
|
293
299
|
{
|
|
294
300
|
"name": "InlineCta",
|
|
295
301
|
"group": "cta",
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCapture } from "../lib/analytics-context";
|
|
4
|
+
|
|
5
|
+
export interface NewsStripProps {
|
|
6
|
+
/** Required: link target. Internal /t/ slugs go to the freshly minted page. */
|
|
7
|
+
href: string;
|
|
8
|
+
/** Required: short eyebrow text displayed in the pill (e.g. "MAY 6"). Defaults to "NEW". */
|
|
9
|
+
pillText?: string;
|
|
10
|
+
/** Required: lead phrase rendered in bold inside the body (e.g. "Anthropic doubled limits."). */
|
|
11
|
+
lead: string;
|
|
12
|
+
/** Required: the wedge / hook explaining why the user should click through. */
|
|
13
|
+
wedge: string;
|
|
14
|
+
/** CTA label pinned right. Defaults to "Read the breakdown". */
|
|
15
|
+
ctaLabel?: string;
|
|
16
|
+
/** Color theme. Defaults to amber for breaking news. */
|
|
17
|
+
tone?: "amber" | "teal" | "rose" | "indigo";
|
|
18
|
+
/** Site slug for analytics events. */
|
|
19
|
+
site?: string;
|
|
20
|
+
/** Section label for analytics events. */
|
|
21
|
+
section?: string;
|
|
22
|
+
/** When true, the pill animates with a pulsing dot. Defaults to true. */
|
|
23
|
+
showDot?: boolean;
|
|
24
|
+
/** Optional ISO date the news lands; surfaced as <time> for SEO/aria. */
|
|
25
|
+
datePublished?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TONES: Record<NonNullable<NewsStripProps["tone"]>, {
|
|
29
|
+
bg: string;
|
|
30
|
+
border: string;
|
|
31
|
+
borderHover: string;
|
|
32
|
+
text: string;
|
|
33
|
+
textStrong: string;
|
|
34
|
+
textWedge: string;
|
|
35
|
+
pillBg: string;
|
|
36
|
+
pillText: string;
|
|
37
|
+
arrow: string;
|
|
38
|
+
shadowRgb: string;
|
|
39
|
+
}> = {
|
|
40
|
+
amber: {
|
|
41
|
+
bg: "linear-gradient(135deg, #fff7ed 0%, #fffbeb 100%)",
|
|
42
|
+
border: "#fde68a",
|
|
43
|
+
borderHover: "#f59e0b",
|
|
44
|
+
text: "#92400e",
|
|
45
|
+
textStrong: "#78350f",
|
|
46
|
+
textWedge: "#b45309",
|
|
47
|
+
pillBg: "#d97706",
|
|
48
|
+
pillText: "#ffffff",
|
|
49
|
+
arrow: "#b45309",
|
|
50
|
+
shadowRgb: "180, 83, 9",
|
|
51
|
+
},
|
|
52
|
+
teal: {
|
|
53
|
+
bg: "linear-gradient(135deg, #ecfeff 0%, #f0fdfa 100%)",
|
|
54
|
+
border: "#99f6e4",
|
|
55
|
+
borderHover: "#14b8a6",
|
|
56
|
+
text: "#115e59",
|
|
57
|
+
textStrong: "#134e4a",
|
|
58
|
+
textWedge: "#0f766e",
|
|
59
|
+
pillBg: "#0d9488",
|
|
60
|
+
pillText: "#ffffff",
|
|
61
|
+
arrow: "#0f766e",
|
|
62
|
+
shadowRgb: "13, 148, 136",
|
|
63
|
+
},
|
|
64
|
+
rose: {
|
|
65
|
+
bg: "linear-gradient(135deg, #fff1f2 0%, #fef2f2 100%)",
|
|
66
|
+
border: "#fecdd3",
|
|
67
|
+
borderHover: "#f43f5e",
|
|
68
|
+
text: "#9f1239",
|
|
69
|
+
textStrong: "#881337",
|
|
70
|
+
textWedge: "#be123c",
|
|
71
|
+
pillBg: "#e11d48",
|
|
72
|
+
pillText: "#ffffff",
|
|
73
|
+
arrow: "#be123c",
|
|
74
|
+
shadowRgb: "225, 29, 72",
|
|
75
|
+
},
|
|
76
|
+
indigo: {
|
|
77
|
+
bg: "linear-gradient(135deg, #eef2ff 0%, #eff6ff 100%)",
|
|
78
|
+
border: "#c7d2fe",
|
|
79
|
+
borderHover: "#6366f1",
|
|
80
|
+
text: "#3730a3",
|
|
81
|
+
textStrong: "#312e81",
|
|
82
|
+
textWedge: "#4338ca",
|
|
83
|
+
pillBg: "#4f46e5",
|
|
84
|
+
pillText: "#ffffff",
|
|
85
|
+
arrow: "#4338ca",
|
|
86
|
+
shadowRgb: "79, 70, 229",
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* <NewsStrip /> — a pinned, pill-anchored, dismiss-free news bar that sits
|
|
92
|
+
* above the hero on a marketing homepage and routes existing organic
|
|
93
|
+
* clickthroughs (e.g. tweets pointing at "/") to a freshly minted dedicated
|
|
94
|
+
* page on a current event.
|
|
95
|
+
*
|
|
96
|
+
* Pattern this component encodes (battle-tested on claude-meter.com on
|
|
97
|
+
* 2026-05-07): a viral post on Twitter/Reddit drives baseline traffic to the
|
|
98
|
+
* homepage. Once a dedicated /t/ analysis ships, every subsequent visitor
|
|
99
|
+
* needs a one-click path from "/" to the deep page. NewsStrip is that path.
|
|
100
|
+
*
|
|
101
|
+
* Renders a single horizontal flex link with three slots:
|
|
102
|
+
* [pill] [lead + wedge] [arrow + cta]
|
|
103
|
+
* Tone is configurable per launch (amber for breaking news, teal/indigo/rose
|
|
104
|
+
* for softer announcements). Wraps gracefully on mobile.
|
|
105
|
+
*
|
|
106
|
+
* Analytics: fires `news_strip_click` via the consumer's PostHog instance
|
|
107
|
+
* (no-op if the page doesn't mount <SeoAnalyticsProvider>).
|
|
108
|
+
*/
|
|
109
|
+
export function NewsStrip({
|
|
110
|
+
href,
|
|
111
|
+
pillText = "NEW",
|
|
112
|
+
lead,
|
|
113
|
+
wedge,
|
|
114
|
+
ctaLabel = "Read the breakdown",
|
|
115
|
+
tone = "amber",
|
|
116
|
+
site,
|
|
117
|
+
section = "homepage",
|
|
118
|
+
showDot = true,
|
|
119
|
+
datePublished,
|
|
120
|
+
}: NewsStripProps) {
|
|
121
|
+
const t = TONES[tone];
|
|
122
|
+
const capture = useCapture();
|
|
123
|
+
|
|
124
|
+
const onClick = () => {
|
|
125
|
+
try {
|
|
126
|
+
capture?.("news_strip_click", {
|
|
127
|
+
destination: href,
|
|
128
|
+
site,
|
|
129
|
+
section,
|
|
130
|
+
pill_text: pillText,
|
|
131
|
+
lead,
|
|
132
|
+
tone,
|
|
133
|
+
component: "NewsStrip",
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
/* analytics failures must never block navigation */
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<a
|
|
142
|
+
href={href}
|
|
143
|
+
onClick={onClick}
|
|
144
|
+
aria-label={`${pillText}: ${lead} ${wedge} ${ctaLabel}`}
|
|
145
|
+
style={{
|
|
146
|
+
position: "relative",
|
|
147
|
+
zIndex: 3,
|
|
148
|
+
display: "flex",
|
|
149
|
+
alignItems: "center",
|
|
150
|
+
gap: 14,
|
|
151
|
+
maxWidth: 1240,
|
|
152
|
+
margin: "16px auto 0",
|
|
153
|
+
padding: "12px 22px",
|
|
154
|
+
borderRadius: 999,
|
|
155
|
+
background: t.bg,
|
|
156
|
+
border: `1px solid ${t.border}`,
|
|
157
|
+
boxShadow: `0 1px 0 rgba(${t.shadowRgb}, 0.04), 0 6px 24px -12px rgba(${t.shadowRgb}, 0.18)`,
|
|
158
|
+
textDecoration: "none",
|
|
159
|
+
color: t.text,
|
|
160
|
+
fontSize: 14,
|
|
161
|
+
lineHeight: 1.4,
|
|
162
|
+
transition: "transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease",
|
|
163
|
+
}}
|
|
164
|
+
className="news-strip"
|
|
165
|
+
data-tone={tone}
|
|
166
|
+
onMouseOver={(e) => {
|
|
167
|
+
const el = e.currentTarget as HTMLAnchorElement;
|
|
168
|
+
el.style.transform = "translateY(-1px)";
|
|
169
|
+
el.style.borderColor = t.borderHover;
|
|
170
|
+
el.style.boxShadow = `0 1px 0 rgba(${t.shadowRgb}, 0.08), 0 12px 32px -10px rgba(${t.shadowRgb}, 0.28)`;
|
|
171
|
+
}}
|
|
172
|
+
onMouseOut={(e) => {
|
|
173
|
+
const el = e.currentTarget as HTMLAnchorElement;
|
|
174
|
+
el.style.transform = "";
|
|
175
|
+
el.style.borderColor = t.border;
|
|
176
|
+
el.style.boxShadow = `0 1px 0 rgba(${t.shadowRgb}, 0.04), 0 6px 24px -12px rgba(${t.shadowRgb}, 0.18)`;
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
<span
|
|
180
|
+
aria-hidden
|
|
181
|
+
style={{
|
|
182
|
+
flex: "none",
|
|
183
|
+
display: "inline-flex",
|
|
184
|
+
alignItems: "center",
|
|
185
|
+
gap: 6,
|
|
186
|
+
padding: "4px 10px",
|
|
187
|
+
borderRadius: 999,
|
|
188
|
+
background: t.pillBg,
|
|
189
|
+
color: t.pillText,
|
|
190
|
+
fontFamily:
|
|
191
|
+
"var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
192
|
+
fontSize: 11,
|
|
193
|
+
fontWeight: 700,
|
|
194
|
+
letterSpacing: "0.08em",
|
|
195
|
+
whiteSpace: "nowrap",
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
{showDot && (
|
|
199
|
+
<span
|
|
200
|
+
aria-hidden
|
|
201
|
+
style={{
|
|
202
|
+
width: 6,
|
|
203
|
+
height: 6,
|
|
204
|
+
borderRadius: "50%",
|
|
205
|
+
background: "#ffffff",
|
|
206
|
+
animation: "newsStripPulse 2s infinite",
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
{pillText}
|
|
211
|
+
</span>
|
|
212
|
+
|
|
213
|
+
<span style={{ flex: "1 1 auto", minWidth: 0 }}>
|
|
214
|
+
<strong style={{ color: t.textStrong, fontWeight: 700 }}>{lead}</strong>{" "}
|
|
215
|
+
<span style={{ color: t.textWedge }}>{wedge}</span>
|
|
216
|
+
{datePublished && (
|
|
217
|
+
<time
|
|
218
|
+
dateTime={datePublished}
|
|
219
|
+
style={{ display: "none" }}
|
|
220
|
+
aria-hidden
|
|
221
|
+
>
|
|
222
|
+
{datePublished}
|
|
223
|
+
</time>
|
|
224
|
+
)}
|
|
225
|
+
</span>
|
|
226
|
+
|
|
227
|
+
<span
|
|
228
|
+
aria-hidden
|
|
229
|
+
style={{
|
|
230
|
+
flex: "none",
|
|
231
|
+
fontFamily:
|
|
232
|
+
"var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
233
|
+
fontSize: 12,
|
|
234
|
+
fontWeight: 600,
|
|
235
|
+
color: t.arrow,
|
|
236
|
+
letterSpacing: "0.02em",
|
|
237
|
+
whiteSpace: "nowrap",
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
{ctaLabel} →
|
|
241
|
+
</span>
|
|
242
|
+
|
|
243
|
+
<style>{`
|
|
244
|
+
@keyframes newsStripPulse {
|
|
245
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.6); }
|
|
246
|
+
50% { box-shadow: 0 0 0 4px rgba(255, 255, 255, 0); }
|
|
247
|
+
}
|
|
248
|
+
@media (max-width: 720px) {
|
|
249
|
+
a.news-strip {
|
|
250
|
+
margin: 12px 18px 0 !important;
|
|
251
|
+
padding: 10px 14px !important;
|
|
252
|
+
border-radius: 14px !important;
|
|
253
|
+
flex-wrap: wrap;
|
|
254
|
+
font-size: 13px !important;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
`}</style>
|
|
258
|
+
</a>
|
|
259
|
+
);
|
|
260
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,10 @@ export { AnimatedSection } from "./components/AnimatedSection";
|
|
|
37
37
|
export { AnimatedMetric } from "./components/AnimatedMetric";
|
|
38
38
|
export { MetricsRow } from "./components/MetricsRow";
|
|
39
39
|
|
|
40
|
+
// News bar (announcement strip linking to a freshly minted /t/ analysis)
|
|
41
|
+
export { NewsStrip } from "./components/NewsStrip";
|
|
42
|
+
export type { NewsStripProps } from "./components/NewsStrip";
|
|
43
|
+
|
|
40
44
|
// CTA components
|
|
41
45
|
export { InlineCta } from "./components/InlineCta";
|
|
42
46
|
export { StickyBottomCta } from "./components/StickyBottomCta";
|
|
@@ -27,8 +27,20 @@ export interface DmShortLinkRedirectConfig {
|
|
|
27
27
|
* but does NOT bump post_links.clicks / dm_links.clicks).
|
|
28
28
|
* 2. Skips the PostHog `*_short_link_clicked` event (no fake conversions).
|
|
29
29
|
* 3. Still 302s to target_url so previews render.
|
|
30
|
+
*
|
|
31
|
+
* 2026-05-07: extended after live measurement found ~71% of "human" hits in
|
|
32
|
+
* the last 24h were actually generic HTTP libs and stripped-header scrapers
|
|
33
|
+
* that the original "self-identifying bot" regex missed. New buckets:
|
|
34
|
+
* - Generic HTTP libs (axios, python-requests, python-urllib, node-fetch,
|
|
35
|
+
* bare "node"). No real browser ever sends these.
|
|
36
|
+
* - Specific scraper UAs caught in the wild: dataminr (Twitter intel),
|
|
37
|
+
* Anthill (content scanner).
|
|
38
|
+
* - Spoofed/empty fingerprints: bare "Mozilla/5.0" (no real browser sends
|
|
39
|
+
* just this), Chrome/70.x (2018 release, only seen from headless fleets).
|
|
40
|
+
* - Empty UA is also treated as bot in the caller (see isBot computation
|
|
41
|
+
* below) since real browsers always send a UA.
|
|
30
42
|
*/
|
|
31
|
-
const BOT_UA_RE = /bot|crawler|spider|Twitterbot|LinkedInBot|Slackbot|facebookexternalhit|Discordbot|TelegramBot|WhatsApp|Applebot|Googlebot|Bingbot|YandexBot|DuckDuckBot|redditbot|Pinterest|Embedly|Snapchat
|
|
43
|
+
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\./i;
|
|
32
44
|
|
|
33
45
|
/**
|
|
34
46
|
* Factory for `GET /r/[code]`.
|
|
@@ -86,7 +98,10 @@ export function createDmShortLinkRedirectHandler(config: DmShortLinkRedirectConf
|
|
|
86
98
|
// agent presents passes through this; matched UAs do NOT count as a real
|
|
87
99
|
// click and do NOT fire PostHog events.
|
|
88
100
|
const ua = req.headers.get("user-agent") || "";
|
|
89
|
-
|
|
101
|
+
// Empty UA is treated as bot: real browsers always send one. Stripped-
|
|
102
|
+
// header scrapers were ~6% of "human" hits in the 24h post 2026-05-07
|
|
103
|
+
// measurement.
|
|
104
|
+
const isBot = !ua || BOT_UA_RE.test(ua);
|
|
90
105
|
const referrer = req.headers.get("referer") || "";
|
|
91
106
|
|
|
92
107
|
let target: string | null = null;
|