@smalk/nextjs-ads 0.1.0 → 0.1.1
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/README.md +21 -0
- package/dist/app.cjs +12 -1
- package/dist/app.cjs.map +1 -1
- package/dist/app.js +12 -1
- package/dist/app.js.map +1 -1
- package/dist/pages.cjs +6 -1
- package/dist/pages.cjs.map +1 -1
- package/dist/pages.js +6 -1
- package/dist/pages.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,6 +82,27 @@ Then put `<div smalk-ads></div>` anywhere in your page HTML — middleware regex
|
|
|
82
82
|
- Hash compare: when API returns a hash that differs from the last cached hash for that URL, the package calls `revalidatePath()` to bust the Full Route Cache
|
|
83
83
|
- HTTP: callers should not add CDN caching to ad-bearing pages (`Cache-Control: private, no-cache, must-revalidate` recommended at the edge)
|
|
84
84
|
|
|
85
|
+
## Dynamic rendering — important
|
|
86
|
+
|
|
87
|
+
For ads to refresh between deploys, the route hosting `<SmalkAd>` must render dynamically (server-side per request), not be baked into the static build output.
|
|
88
|
+
|
|
89
|
+
The package handles this automatically: `<SmalkAd>` calls `headers()` (built-in Next 13.4+ opt-out) and `unstable_noStore()` (Next 14+ explicit opt-out). This makes the surrounding route dynamic by default.
|
|
90
|
+
|
|
91
|
+
You only need to act manually in two edge cases:
|
|
92
|
+
|
|
93
|
+
1. **`output: 'export'` in `next.config.js`** — full static export, no Node server at runtime. Server Components cannot run on requests; the ad HTML is frozen at build time. Not supported by this plugin. Remove `output: 'export'` (or switch the affected routes off it).
|
|
94
|
+
|
|
95
|
+
2. **Custom `dynamic` export overriding the heuristic** — if your `page.tsx` or a parent `layout.tsx` exports `export const dynamic = 'force-static'` or `'error'`, that overrides the plugin's opt-out. Either remove that line, or explicitly mark the route as dynamic:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// app/blog/layout.tsx (or page.tsx)
|
|
99
|
+
export const dynamic = 'force-dynamic';
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Putting it on `layout.tsx` is the lowest-friction option — it applies to every page under that folder without per-page edits.
|
|
103
|
+
|
|
104
|
+
If you're not sure, leave the plugin to handle it and check `view-source` of your deployed page: if you see `<!-- smalk: no ad -->` updating between deploys (and our dashboard registers a new AdPlacement after first request), you're good.
|
|
105
|
+
|
|
85
106
|
## Trust Boundary
|
|
86
107
|
|
|
87
108
|
Ad HTML is rendered via React's raw-HTML escape hatch inside package-owned components only. The publisher never calls the unsafe React API directly. We do **not** ship a sanitizer (DOMPurify): ads include `<script type="application/ld+json">` JSON-LD that AI crawlers parse for citation freshness, and the default DOMPurify config strips it. Smalk vets ad content server-side; this is the same trust model used by the WordPress (`smalk-ai-ads-pro`) and Drupal (`smalk_d8`) plugins.
|
package/dist/app.cjs
CHANGED
|
@@ -144,13 +144,15 @@ async function sha256Hex(input) {
|
|
|
144
144
|
// src/app.tsx
|
|
145
145
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
146
146
|
async function SmalkAd(props = {}) {
|
|
147
|
+
await noStoreIfAvailable();
|
|
147
148
|
const h = await readHeadersCompat();
|
|
148
149
|
const userAgent = h.get("user-agent") ?? "";
|
|
149
150
|
const referer = h.get("referer") ?? "";
|
|
150
151
|
const clientIp = pickClientIp(h);
|
|
151
152
|
const host = h.get("host") ?? "";
|
|
152
153
|
const pathname = props.pathname ?? "/";
|
|
153
|
-
|
|
154
|
+
if (!host) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SmalkAdComment, { reason: "no host" });
|
|
155
|
+
const pageUrl = `https://${host}${pathname}`;
|
|
154
156
|
const ad = await fetchAd(
|
|
155
157
|
{ pageUrl, userAgent, referer, clientIp },
|
|
156
158
|
{ revalidate: props.revalidate, preview: props.preview }
|
|
@@ -170,6 +172,15 @@ async function SmalkAd(props = {}) {
|
|
|
170
172
|
function SmalkAdComment({ reason }) {
|
|
171
173
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-smalk": "comment", dangerouslySetInnerHTML: { __html: `<!-- smalk: ${reason} -->` } });
|
|
172
174
|
}
|
|
175
|
+
async function noStoreIfAvailable() {
|
|
176
|
+
try {
|
|
177
|
+
const mod = await import("next/cache");
|
|
178
|
+
if (typeof mod.unstable_noStore === "function") {
|
|
179
|
+
mod.unstable_noStore();
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
173
184
|
async function readHeadersCompat() {
|
|
174
185
|
const result = (0, import_headers.headers)();
|
|
175
186
|
if (result && typeof result.then === "function") {
|
package/dist/app.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/app.tsx","../src/index.ts","../src/client.ts"],"sourcesContent":["import 'server-only';\nimport * as React from 'react';\nimport { headers } from 'next/headers';\nimport { fetchAd } from './client';\n\nexport interface SmalkAdProps {\n /** Override revalidation TTL in seconds (default 1200 = 20 min). */\n revalidate?: number;\n /** Page pathname; if omitted, derived from request headers / `/`. */\n pathname?: string;\n /** Optional className applied to the wrapper element. */\n className?: string;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function SmalkAd(props: SmalkAdProps = {}): Promise<React.ReactElement> {\n const h = await readHeadersCompat();\n const userAgent = h.get('user-agent') ?? '';\n const referer = h.get('referer') ?? '';\n const clientIp = pickClientIp(h);\n const host = h.get('host') ?? '';\n const pathname = props.pathname ?? '/';\n const pageUrl = host ? `https://${host}${pathname}` : pathname;\n\n const ad = await fetchAd(\n { pageUrl, userAgent, referer, clientIp },\n { revalidate: props.revalidate, preview: props.preview },\n );\n\n if (!ad) {\n return <SmalkAdComment reason=\"no ad\" />;\n }\n\n return (\n <div\n className={props.className ?? 'smalk-ads'}\n data-smalk-booking={ad.bookingId ?? undefined}\n // Trust boundary: API output is vetted server-side; raw HTML rendered here.\n // No DOMPurify — strips required <script type=\"application/ld+json\"> JSON-LD.\n dangerouslySetInnerHTML={{ __html: ad.html }}\n />\n );\n}\n\nfunction SmalkAdComment({ reason }: { reason: string }): React.ReactElement {\n return <span data-smalk=\"comment\" dangerouslySetInnerHTML={{ __html: `<!-- smalk: ${reason} -->` }} />;\n}\n\nasync function readHeadersCompat(): Promise<{ get: (k: string) => string | null }> {\n // Next 13.4–14.x: headers() is sync. Next 15+: async.\n const result = headers() as unknown;\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n return (await (result as Promise<{ get: (k: string) => string | null }>)) as { get: (k: string) => string | null };\n }\n return result as { get: (k: string) => string | null };\n}\n\nfunction pickClientIp(h: { get: (k: string) => string | null }): string {\n for (const key of ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for']) {\n const v = h.get(key);\n if (v) return v.split(',')[0].trim();\n }\n return '';\n}\n","export interface SmalkAdsConfig {\n projectKey: string;\n apiKey: string;\n apiBaseUrl: string;\n}\n\nexport interface AdInput {\n pageUrl: string;\n userAgent: string;\n referer: string;\n clientIp: string;\n}\n\nexport interface AdResult {\n html: string;\n bookingId: string | null;\n}\n\nexport interface ApiResponse {\n html?: string;\n booking_id?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport const DEFAULT_TIMEOUT_MS = 100;\nexport const DEFAULT_REVALIDATE_S = 1200;\nexport const API_PATH = '/api/v1/transform/ads/content/';\n\nexport function loadConfig(): SmalkAdsConfig {\n const projectKey = process.env.SMALK_PROJECT_KEY;\n const apiKey = process.env.SMALK_API_KEY;\n const apiBaseUrl = process.env.SMALK_API_BASE_URL || 'https://api.smalk.ai';\n const isProd = process.env.NODE_ENV === 'production';\n\n if (!projectKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production');\n console.warn('[smalk] SMALK_PROJECT_KEY not set — ad fetch will be skipped');\n }\n if (!apiKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_API_KEY env var is required in production');\n console.warn('[smalk] SMALK_API_KEY not set — ad fetch will be skipped');\n }\n\n return {\n projectKey: projectKey ?? '',\n apiKey: apiKey ?? '',\n apiBaseUrl,\n };\n}\n","import { loadConfig, AdInput, AdResult, ApiResponse, API_PATH, DEFAULT_TIMEOUT_MS, DEFAULT_REVALIDATE_S } from './index';\n\nconst lastHashByUrl = new Map<string, string>();\n\nexport function _resetHashCacheForTests(): void {\n lastHashByUrl.clear();\n}\n\nexport interface FetchAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function fetchAd(input: AdInput, opts: FetchAdOptions = {}): Promise<AdResult | null> {\n const cfg = loadConfig();\n if (!cfg.projectKey || !cfg.apiKey) return null;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);\n\n try {\n const body: Record<string, unknown> = {\n project_key: cfg.projectKey,\n page_url: input.pageUrl,\n user_agent: input.userAgent,\n referer: input.referer,\n client_ip: input.clientIp,\n };\n if (opts.preview !== undefined && opts.preview !== false) {\n body.preview = opts.preview;\n }\n const res = await fetch(`${cfg.apiBaseUrl}${API_PATH}`, {\n method: 'POST',\n headers: {\n 'Authorization': `Api-Key ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n // `next` is a Next.js fetch extension; type provided by next types when present.\n next: {\n revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,\n tags: ['smalk-ad', `smalk-ad:${input.pageUrl}`],\n },\n } as RequestInit);\n\n if (!res.ok) {\n logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);\n return null;\n }\n\n const data = (await res.json()) as ApiResponse;\n if (!data.html) return null;\n\n const hash = await sha256Hex(data.html);\n const prev = lastHashByUrl.get(input.pageUrl);\n if (prev && prev !== hash) {\n await maybeRevalidatePath(input.pageUrl);\n }\n lastHashByUrl.set(input.pageUrl, hash);\n\n return { html: data.html, bookingId: data.booking_id ?? null };\n } catch (err) {\n if (err instanceof Error && err.name !== 'AbortError') {\n logErrorOnce(`smalk-ads fetch error: ${err.message}`);\n }\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function maybeRevalidatePath(pageUrl: string): Promise<void> {\n try {\n const mod = await import('next/cache');\n if (typeof mod.revalidatePath === 'function') {\n const pathname = new URL(pageUrl).pathname;\n mod.revalidatePath(pathname);\n }\n } catch {\n // next/cache not available (Pages Router runtime, edge, or test env without next).\n }\n}\n\nconst seenErrors = new Set<string>();\nfunction logErrorOnce(msg: string): void {\n if (seenErrors.has(msg)) return;\n seenErrors.add(msg);\n console.error(`[smalk-nextjs-ads] ${msg}`);\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const buf = await globalThis.crypto.subtle.digest('SHA-256', data);\n const bytes = new Uint8Array(buf);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, '0');\n }\n return hex;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAO;AAEP,qBAAwB;;;ACsBjB,IAAM,qBAAqB;AAC3B,IAAM,uBAAuB;AAC7B,IAAM,WAAW;AAEjB,SAAS,aAA6B;AAC3C,QAAM,aAAa,QAAQ,IAAI;AAC/B,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,aAAa,QAAQ,IAAI,sBAAsB;AACrD,QAAM,SAAS,QAAQ,IAAI,aAAa;AAExC,MAAI,CAAC,YAAY;AACf,QAAI,OAAQ,OAAM,IAAI,MAAM,wEAAwE;AACpG,YAAQ,KAAK,mEAA8D;AAAA,EAC7E;AACA,MAAI,CAAC,QAAQ;AACX,QAAI,OAAQ,OAAM,IAAI,MAAM,oEAAoE;AAChG,YAAQ,KAAK,+DAA0D;AAAA,EACzE;AAEA,SAAO;AAAA,IACL,YAAY,cAAc;AAAA,IAC1B,QAAQ,UAAU;AAAA,IAClB;AAAA,EACF;AACF;;;AC9CA,IAAM,gBAAgB,oBAAI,IAAoB;AAY9C,eAAsB,QAAQ,OAAgB,OAAuB,CAAC,GAA6B;AACjG,QAAM,MAAM,WAAW;AACvB,MAAI,CAAC,IAAI,cAAc,CAAC,IAAI,OAAQ,QAAO;AAE3C,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,kBAAkB;AAErE,MAAI;AACF,UAAM,OAAgC;AAAA,MACpC,aAAa,IAAI;AAAA,MACjB,UAAU,MAAM;AAAA,MAChB,YAAY,MAAM;AAAA,MAClB,SAAS,MAAM;AAAA,MACf,WAAW,MAAM;AAAA,IACnB;AACA,QAAI,KAAK,YAAY,UAAa,KAAK,YAAY,OAAO;AACxD,WAAK,UAAU,KAAK;AAAA,IACtB;AACA,UAAM,MAAM,MAAM,MAAM,GAAG,IAAI,UAAU,GAAG,QAAQ,IAAI;AAAA,MACtD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,iBAAiB,WAAW,IAAI,MAAM;AAAA,QACtC,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB,QAAQ,WAAW;AAAA;AAAA,MAEnB,MAAM;AAAA,QACJ,YAAY,KAAK,cAAc;AAAA,QAC/B,MAAM,CAAC,YAAY,YAAY,MAAM,OAAO,EAAE;AAAA,MAChD;AAAA,IACF,CAAgB;AAEhB,QAAI,CAAC,IAAI,IAAI;AACX,mBAAa,gCAAgC,IAAI,MAAM,EAAE;AACzD,aAAO;AAAA,IACT;AAEA,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAI,CAAC,KAAK,KAAM,QAAO;AAEvB,UAAM,OAAO,MAAM,UAAU,KAAK,IAAI;AACtC,UAAM,OAAO,cAAc,IAAI,MAAM,OAAO;AAC5C,QAAI,QAAQ,SAAS,MAAM;AACzB,YAAM,oBAAoB,MAAM,OAAO;AAAA,IACzC;AACA,kBAAc,IAAI,MAAM,SAAS,IAAI;AAErC,WAAO,EAAE,MAAM,KAAK,MAAM,WAAW,KAAK,cAAc,KAAK;AAAA,EAC/D,SAAS,KAAK;AACZ,QAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD,mBAAa,0BAA0B,IAAI,OAAO,EAAE;AAAA,IACtD;AACA,WAAO;AAAA,EACT,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAEA,eAAe,oBAAoB,SAAgC;AACjE,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,YAAY;AACrC,QAAI,OAAO,IAAI,mBAAmB,YAAY;AAC5C,YAAM,WAAW,IAAI,IAAI,OAAO,EAAE;AAClC,UAAI,eAAe,QAAQ;AAAA,IAC7B;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEA,IAAM,aAAa,oBAAI,IAAY;AACnC,SAAS,aAAa,KAAmB;AACvC,MAAI,WAAW,IAAI,GAAG,EAAG;AACzB,aAAW,IAAI,GAAG;AAClB,UAAQ,MAAM,sBAAsB,GAAG,EAAE;AAC3C;AAEA,eAAe,UAAU,OAAgC;AACvD,QAAM,OAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AAC3C,QAAM,MAAM,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,IAAI;AACjE,QAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,WAAO,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAAA,EAC9C;AACA,SAAO;AACT;;;AFtEW;AAfX,eAAsB,QAAQ,QAAsB,CAAC,GAAgC;AACnF,QAAM,IAAI,MAAM,kBAAkB;AAClC,QAAM,YAAY,EAAE,IAAI,YAAY,KAAK;AACzC,QAAM,UAAU,EAAE,IAAI,SAAS,KAAK;AACpC,QAAM,WAAW,aAAa,CAAC;AAC/B,QAAM,OAAO,EAAE,IAAI,MAAM,KAAK;AAC9B,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,UAAU,OAAO,WAAW,IAAI,GAAG,QAAQ,KAAK;AAEtD,QAAM,KAAK,MAAM;AAAA,IACf,EAAE,SAAS,WAAW,SAAS,SAAS;AAAA,IACxC,EAAE,YAAY,MAAM,YAAY,SAAS,MAAM,QAAQ;AAAA,EACzD;AAEA,MAAI,CAAC,IAAI;AACP,WAAO,4CAAC,kBAAe,QAAO,SAAQ;AAAA,EACxC;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,MAAM,aAAa;AAAA,MAC9B,sBAAoB,GAAG,aAAa;AAAA,MAGpC,yBAAyB,EAAE,QAAQ,GAAG,KAAK;AAAA;AAAA,EAC7C;AAEJ;AAEA,SAAS,eAAe,EAAE,OAAO,GAA2C;AAC1E,SAAO,4CAAC,UAAK,cAAW,WAAU,yBAAyB,EAAE,QAAQ,eAAe,MAAM,OAAO,GAAG;AACtG;AAEA,eAAe,oBAAoE;AAEjF,QAAM,aAAS,wBAAQ;AACvB,MAAI,UAAU,OAAQ,OAA4B,SAAS,YAAY;AACrE,WAAQ,MAAO;AAAA,EACjB;AACA,SAAO;AACT;AAEA,SAAS,aAAa,GAAkD;AACtE,aAAW,OAAO,CAAC,oBAAoB,aAAa,iBAAiB,GAAG;AACtE,UAAM,IAAI,EAAE,IAAI,GAAG;AACnB,QAAI,EAAG,QAAO,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAAA,EACrC;AACA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/app.tsx","../src/index.ts","../src/client.ts"],"sourcesContent":["import 'server-only';\nimport * as React from 'react';\nimport { headers } from 'next/headers';\nimport { fetchAd } from './client';\n\nexport interface SmalkAdProps {\n /** Override revalidation TTL in seconds (default 1200 = 20 min). */\n revalidate?: number;\n /** Page pathname; if omitted, derived from request headers / `/`. */\n pathname?: string;\n /** Optional className applied to the wrapper element. */\n className?: string;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function SmalkAd(props: SmalkAdProps = {}): Promise<React.ReactElement> {\n // Opt the surrounding route out of Static Generation so the ad fetch runs at\n // request time, not at build time. Two layers:\n // 1) `headers()` below — opts out since Next 13.4 (built-in heuristic).\n // 2) `unstable_noStore()` — explicit opt-out marker, added in Next 14.\n // Best-effort: dynamic import, swallow if unavailable (Next 13.x or moved).\n await noStoreIfAvailable();\n const h = await readHeadersCompat();\n const userAgent = h.get('user-agent') ?? '';\n const referer = h.get('referer') ?? '';\n const clientIp = pickClientIp(h);\n const host = h.get('host') ?? '';\n const pathname = props.pathname ?? '/';\n if (!host) return <SmalkAdComment reason=\"no host\" />;\n const pageUrl = `https://${host}${pathname}`;\n\n const ad = await fetchAd(\n { pageUrl, userAgent, referer, clientIp },\n { revalidate: props.revalidate, preview: props.preview },\n );\n\n if (!ad) {\n return <SmalkAdComment reason=\"no ad\" />;\n }\n\n return (\n <div\n className={props.className ?? 'smalk-ads'}\n data-smalk-booking={ad.bookingId ?? undefined}\n // Trust boundary: API output is vetted server-side; raw HTML rendered here.\n // No DOMPurify — strips required <script type=\"application/ld+json\"> JSON-LD.\n dangerouslySetInnerHTML={{ __html: ad.html }}\n />\n );\n}\n\nfunction SmalkAdComment({ reason }: { reason: string }): React.ReactElement {\n return <span data-smalk=\"comment\" dangerouslySetInnerHTML={{ __html: `<!-- smalk: ${reason} -->` }} />;\n}\n\nasync function noStoreIfAvailable(): Promise<void> {\n try {\n const mod = (await import('next/cache')) as unknown as { unstable_noStore?: () => void };\n if (typeof mod.unstable_noStore === 'function') {\n mod.unstable_noStore();\n }\n } catch {\n // next/cache not available (Next 13.x) — the `headers()` call below still\n // opts the route out of Static Generation in that version range.\n }\n}\n\nasync function readHeadersCompat(): Promise<{ get: (k: string) => string | null }> {\n // Next 13.4–14.x: headers() is sync. Next 15+: async.\n const result = headers() as unknown;\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n return (await (result as Promise<{ get: (k: string) => string | null }>)) as { get: (k: string) => string | null };\n }\n return result as { get: (k: string) => string | null };\n}\n\nfunction pickClientIp(h: { get: (k: string) => string | null }): string {\n for (const key of ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for']) {\n const v = h.get(key);\n if (v) return v.split(',')[0].trim();\n }\n return '';\n}\n","export interface SmalkAdsConfig {\n projectKey: string;\n apiKey: string;\n apiBaseUrl: string;\n}\n\nexport interface AdInput {\n pageUrl: string;\n userAgent: string;\n referer: string;\n clientIp: string;\n}\n\nexport interface AdResult {\n html: string;\n bookingId: string | null;\n}\n\nexport interface ApiResponse {\n html?: string;\n booking_id?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport const DEFAULT_TIMEOUT_MS = 100;\nexport const DEFAULT_REVALIDATE_S = 1200;\nexport const API_PATH = '/api/v1/transform/ads/content/';\n\nexport function loadConfig(): SmalkAdsConfig {\n const projectKey = process.env.SMALK_PROJECT_KEY;\n const apiKey = process.env.SMALK_API_KEY;\n const apiBaseUrl = process.env.SMALK_API_BASE_URL || 'https://api.smalk.ai';\n const isProd = process.env.NODE_ENV === 'production';\n\n if (!projectKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production');\n console.warn('[smalk] SMALK_PROJECT_KEY not set — ad fetch will be skipped');\n }\n if (!apiKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_API_KEY env var is required in production');\n console.warn('[smalk] SMALK_API_KEY not set — ad fetch will be skipped');\n }\n\n return {\n projectKey: projectKey ?? '',\n apiKey: apiKey ?? '',\n apiBaseUrl,\n };\n}\n","import { loadConfig, AdInput, AdResult, ApiResponse, API_PATH, DEFAULT_TIMEOUT_MS, DEFAULT_REVALIDATE_S } from './index';\n\nconst lastHashByUrl = new Map<string, string>();\n\nexport function _resetHashCacheForTests(): void {\n lastHashByUrl.clear();\n}\n\nexport interface FetchAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function fetchAd(input: AdInput, opts: FetchAdOptions = {}): Promise<AdResult | null> {\n const cfg = loadConfig();\n if (!cfg.projectKey || !cfg.apiKey) return null;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);\n\n try {\n const body: Record<string, unknown> = {\n project_key: cfg.projectKey,\n page_url: input.pageUrl,\n user_agent: input.userAgent,\n referer: input.referer,\n client_ip: input.clientIp,\n };\n if (opts.preview !== undefined && opts.preview !== false) {\n body.preview = opts.preview;\n }\n const res = await fetch(`${cfg.apiBaseUrl}${API_PATH}`, {\n method: 'POST',\n headers: {\n 'Authorization': `Api-Key ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n // `next` is a Next.js fetch extension; type provided by next types when present.\n next: {\n revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,\n tags: ['smalk-ad', `smalk-ad:${input.pageUrl}`],\n },\n } as RequestInit);\n\n if (!res.ok) {\n logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);\n return null;\n }\n\n const data = (await res.json()) as ApiResponse;\n if (!data.html) return null;\n\n const hash = await sha256Hex(data.html);\n const prev = lastHashByUrl.get(input.pageUrl);\n if (prev && prev !== hash) {\n await maybeRevalidatePath(input.pageUrl);\n }\n lastHashByUrl.set(input.pageUrl, hash);\n\n return { html: data.html, bookingId: data.booking_id ?? null };\n } catch (err) {\n if (err instanceof Error && err.name !== 'AbortError') {\n logErrorOnce(`smalk-ads fetch error: ${err.message}`);\n }\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function maybeRevalidatePath(pageUrl: string): Promise<void> {\n try {\n const mod = await import('next/cache');\n if (typeof mod.revalidatePath === 'function') {\n const pathname = new URL(pageUrl).pathname;\n mod.revalidatePath(pathname);\n }\n } catch {\n // next/cache not available (Pages Router runtime, edge, or test env without next).\n }\n}\n\nconst seenErrors = new Set<string>();\nfunction logErrorOnce(msg: string): void {\n if (seenErrors.has(msg)) return;\n seenErrors.add(msg);\n console.error(`[smalk-nextjs-ads] ${msg}`);\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const buf = await globalThis.crypto.subtle.digest('SHA-256', data);\n const bytes = new Uint8Array(buf);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, '0');\n }\n return hex;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAO;AAEP,qBAAwB;;;ACsBjB,IAAM,qBAAqB;AAC3B,IAAM,uBAAuB;AAC7B,IAAM,WAAW;AAEjB,SAAS,aAA6B;AAC3C,QAAM,aAAa,QAAQ,IAAI;AAC/B,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,aAAa,QAAQ,IAAI,sBAAsB;AACrD,QAAM,SAAS,QAAQ,IAAI,aAAa;AAExC,MAAI,CAAC,YAAY;AACf,QAAI,OAAQ,OAAM,IAAI,MAAM,wEAAwE;AACpG,YAAQ,KAAK,mEAA8D;AAAA,EAC7E;AACA,MAAI,CAAC,QAAQ;AACX,QAAI,OAAQ,OAAM,IAAI,MAAM,oEAAoE;AAChG,YAAQ,KAAK,+DAA0D;AAAA,EACzE;AAEA,SAAO;AAAA,IACL,YAAY,cAAc;AAAA,IAC1B,QAAQ,UAAU;AAAA,IAClB;AAAA,EACF;AACF;;;AC9CA,IAAM,gBAAgB,oBAAI,IAAoB;AAY9C,eAAsB,QAAQ,OAAgB,OAAuB,CAAC,GAA6B;AACjG,QAAM,MAAM,WAAW;AACvB,MAAI,CAAC,IAAI,cAAc,CAAC,IAAI,OAAQ,QAAO;AAE3C,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,kBAAkB;AAErE,MAAI;AACF,UAAM,OAAgC;AAAA,MACpC,aAAa,IAAI;AAAA,MACjB,UAAU,MAAM;AAAA,MAChB,YAAY,MAAM;AAAA,MAClB,SAAS,MAAM;AAAA,MACf,WAAW,MAAM;AAAA,IACnB;AACA,QAAI,KAAK,YAAY,UAAa,KAAK,YAAY,OAAO;AACxD,WAAK,UAAU,KAAK;AAAA,IACtB;AACA,UAAM,MAAM,MAAM,MAAM,GAAG,IAAI,UAAU,GAAG,QAAQ,IAAI;AAAA,MACtD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,iBAAiB,WAAW,IAAI,MAAM;AAAA,QACtC,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB,QAAQ,WAAW;AAAA;AAAA,MAEnB,MAAM;AAAA,QACJ,YAAY,KAAK,cAAc;AAAA,QAC/B,MAAM,CAAC,YAAY,YAAY,MAAM,OAAO,EAAE;AAAA,MAChD;AAAA,IACF,CAAgB;AAEhB,QAAI,CAAC,IAAI,IAAI;AACX,mBAAa,gCAAgC,IAAI,MAAM,EAAE;AACzD,aAAO;AAAA,IACT;AAEA,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAI,CAAC,KAAK,KAAM,QAAO;AAEvB,UAAM,OAAO,MAAM,UAAU,KAAK,IAAI;AACtC,UAAM,OAAO,cAAc,IAAI,MAAM,OAAO;AAC5C,QAAI,QAAQ,SAAS,MAAM;AACzB,YAAM,oBAAoB,MAAM,OAAO;AAAA,IACzC;AACA,kBAAc,IAAI,MAAM,SAAS,IAAI;AAErC,WAAO,EAAE,MAAM,KAAK,MAAM,WAAW,KAAK,cAAc,KAAK;AAAA,EAC/D,SAAS,KAAK;AACZ,QAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD,mBAAa,0BAA0B,IAAI,OAAO,EAAE;AAAA,IACtD;AACA,WAAO;AAAA,EACT,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAEA,eAAe,oBAAoB,SAAgC;AACjE,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,YAAY;AACrC,QAAI,OAAO,IAAI,mBAAmB,YAAY;AAC5C,YAAM,WAAW,IAAI,IAAI,OAAO,EAAE;AAClC,UAAI,eAAe,QAAQ;AAAA,IAC7B;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEA,IAAM,aAAa,oBAAI,IAAY;AACnC,SAAS,aAAa,KAAmB;AACvC,MAAI,WAAW,IAAI,GAAG,EAAG;AACzB,aAAW,IAAI,GAAG;AAClB,UAAQ,MAAM,sBAAsB,GAAG,EAAE;AAC3C;AAEA,eAAe,UAAU,OAAgC;AACvD,QAAM,OAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AAC3C,QAAM,MAAM,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,IAAI;AACjE,QAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,WAAO,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAAA,EAC9C;AACA,SAAO;AACT;;;AFxEoB;AAbpB,eAAsB,QAAQ,QAAsB,CAAC,GAAgC;AAMnF,QAAM,mBAAmB;AACzB,QAAM,IAAI,MAAM,kBAAkB;AAClC,QAAM,YAAY,EAAE,IAAI,YAAY,KAAK;AACzC,QAAM,UAAU,EAAE,IAAI,SAAS,KAAK;AACpC,QAAM,WAAW,aAAa,CAAC;AAC/B,QAAM,OAAO,EAAE,IAAI,MAAM,KAAK;AAC9B,QAAM,WAAW,MAAM,YAAY;AACnC,MAAI,CAAC,KAAM,QAAO,4CAAC,kBAAe,QAAO,WAAU;AACnD,QAAM,UAAU,WAAW,IAAI,GAAG,QAAQ;AAE1C,QAAM,KAAK,MAAM;AAAA,IACf,EAAE,SAAS,WAAW,SAAS,SAAS;AAAA,IACxC,EAAE,YAAY,MAAM,YAAY,SAAS,MAAM,QAAQ;AAAA,EACzD;AAEA,MAAI,CAAC,IAAI;AACP,WAAO,4CAAC,kBAAe,QAAO,SAAQ;AAAA,EACxC;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,MAAM,aAAa;AAAA,MAC9B,sBAAoB,GAAG,aAAa;AAAA,MAGpC,yBAAyB,EAAE,QAAQ,GAAG,KAAK;AAAA;AAAA,EAC7C;AAEJ;AAEA,SAAS,eAAe,EAAE,OAAO,GAA2C;AAC1E,SAAO,4CAAC,UAAK,cAAW,WAAU,yBAAyB,EAAE,QAAQ,eAAe,MAAM,OAAO,GAAG;AACtG;AAEA,eAAe,qBAAoC;AACjD,MAAI;AACF,UAAM,MAAO,MAAM,OAAO,YAAY;AACtC,QAAI,OAAO,IAAI,qBAAqB,YAAY;AAC9C,UAAI,iBAAiB;AAAA,IACvB;AAAA,EACF,QAAQ;AAAA,EAGR;AACF;AAEA,eAAe,oBAAoE;AAEjF,QAAM,aAAS,wBAAQ;AACvB,MAAI,UAAU,OAAQ,OAA4B,SAAS,YAAY;AACrE,WAAQ,MAAO;AAAA,EACjB;AACA,SAAO;AACT;AAEA,SAAS,aAAa,GAAkD;AACtE,aAAW,OAAO,CAAC,oBAAoB,aAAa,iBAAiB,GAAG;AACtE,UAAM,IAAI,EAAE,IAAI,GAAG;AACnB,QAAI,EAAG,QAAO,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAAA,EACrC;AACA,SAAO;AACT;","names":[]}
|
package/dist/app.js
CHANGED
|
@@ -8,13 +8,15 @@ import "server-only";
|
|
|
8
8
|
import { headers } from "next/headers";
|
|
9
9
|
import { jsx } from "react/jsx-runtime";
|
|
10
10
|
async function SmalkAd(props = {}) {
|
|
11
|
+
await noStoreIfAvailable();
|
|
11
12
|
const h = await readHeadersCompat();
|
|
12
13
|
const userAgent = h.get("user-agent") ?? "";
|
|
13
14
|
const referer = h.get("referer") ?? "";
|
|
14
15
|
const clientIp = pickClientIp(h);
|
|
15
16
|
const host = h.get("host") ?? "";
|
|
16
17
|
const pathname = props.pathname ?? "/";
|
|
17
|
-
|
|
18
|
+
if (!host) return /* @__PURE__ */ jsx(SmalkAdComment, { reason: "no host" });
|
|
19
|
+
const pageUrl = `https://${host}${pathname}`;
|
|
18
20
|
const ad = await fetchAd(
|
|
19
21
|
{ pageUrl, userAgent, referer, clientIp },
|
|
20
22
|
{ revalidate: props.revalidate, preview: props.preview }
|
|
@@ -34,6 +36,15 @@ async function SmalkAd(props = {}) {
|
|
|
34
36
|
function SmalkAdComment({ reason }) {
|
|
35
37
|
return /* @__PURE__ */ jsx("span", { "data-smalk": "comment", dangerouslySetInnerHTML: { __html: `<!-- smalk: ${reason} -->` } });
|
|
36
38
|
}
|
|
39
|
+
async function noStoreIfAvailable() {
|
|
40
|
+
try {
|
|
41
|
+
const mod = await import("next/cache");
|
|
42
|
+
if (typeof mod.unstable_noStore === "function") {
|
|
43
|
+
mod.unstable_noStore();
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
37
48
|
async function readHeadersCompat() {
|
|
38
49
|
const result = headers();
|
|
39
50
|
if (result && typeof result.then === "function") {
|
package/dist/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/app.tsx"],"sourcesContent":["import 'server-only';\nimport * as React from 'react';\nimport { headers } from 'next/headers';\nimport { fetchAd } from './client';\n\nexport interface SmalkAdProps {\n /** Override revalidation TTL in seconds (default 1200 = 20 min). */\n revalidate?: number;\n /** Page pathname; if omitted, derived from request headers / `/`. */\n pathname?: string;\n /** Optional className applied to the wrapper element. */\n className?: string;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function SmalkAd(props: SmalkAdProps = {}): Promise<React.ReactElement> {\n const h = await readHeadersCompat();\n const userAgent = h.get('user-agent') ?? '';\n const referer = h.get('referer') ?? '';\n const clientIp = pickClientIp(h);\n const host = h.get('host') ?? '';\n const pathname = props.pathname ?? '/';\n const pageUrl =
|
|
1
|
+
{"version":3,"sources":["../src/app.tsx"],"sourcesContent":["import 'server-only';\nimport * as React from 'react';\nimport { headers } from 'next/headers';\nimport { fetchAd } from './client';\n\nexport interface SmalkAdProps {\n /** Override revalidation TTL in seconds (default 1200 = 20 min). */\n revalidate?: number;\n /** Page pathname; if omitted, derived from request headers / `/`. */\n pathname?: string;\n /** Optional className applied to the wrapper element. */\n className?: string;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function SmalkAd(props: SmalkAdProps = {}): Promise<React.ReactElement> {\n // Opt the surrounding route out of Static Generation so the ad fetch runs at\n // request time, not at build time. Two layers:\n // 1) `headers()` below — opts out since Next 13.4 (built-in heuristic).\n // 2) `unstable_noStore()` — explicit opt-out marker, added in Next 14.\n // Best-effort: dynamic import, swallow if unavailable (Next 13.x or moved).\n await noStoreIfAvailable();\n const h = await readHeadersCompat();\n const userAgent = h.get('user-agent') ?? '';\n const referer = h.get('referer') ?? '';\n const clientIp = pickClientIp(h);\n const host = h.get('host') ?? '';\n const pathname = props.pathname ?? '/';\n if (!host) return <SmalkAdComment reason=\"no host\" />;\n const pageUrl = `https://${host}${pathname}`;\n\n const ad = await fetchAd(\n { pageUrl, userAgent, referer, clientIp },\n { revalidate: props.revalidate, preview: props.preview },\n );\n\n if (!ad) {\n return <SmalkAdComment reason=\"no ad\" />;\n }\n\n return (\n <div\n className={props.className ?? 'smalk-ads'}\n data-smalk-booking={ad.bookingId ?? undefined}\n // Trust boundary: API output is vetted server-side; raw HTML rendered here.\n // No DOMPurify — strips required <script type=\"application/ld+json\"> JSON-LD.\n dangerouslySetInnerHTML={{ __html: ad.html }}\n />\n );\n}\n\nfunction SmalkAdComment({ reason }: { reason: string }): React.ReactElement {\n return <span data-smalk=\"comment\" dangerouslySetInnerHTML={{ __html: `<!-- smalk: ${reason} -->` }} />;\n}\n\nasync function noStoreIfAvailable(): Promise<void> {\n try {\n const mod = (await import('next/cache')) as unknown as { unstable_noStore?: () => void };\n if (typeof mod.unstable_noStore === 'function') {\n mod.unstable_noStore();\n }\n } catch {\n // next/cache not available (Next 13.x) — the `headers()` call below still\n // opts the route out of Static Generation in that version range.\n }\n}\n\nasync function readHeadersCompat(): Promise<{ get: (k: string) => string | null }> {\n // Next 13.4–14.x: headers() is sync. Next 15+: async.\n const result = headers() as unknown;\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n return (await (result as Promise<{ get: (k: string) => string | null }>)) as { get: (k: string) => string | null };\n }\n return result as { get: (k: string) => string | null };\n}\n\nfunction pickClientIp(h: { get: (k: string) => string | null }): string {\n for (const key of ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for']) {\n const v = h.get(key);\n if (v) return v.split(',')[0].trim();\n }\n return '';\n}\n"],"mappings":";;;;;;AAAA,OAAO;AAEP,SAAS,eAAe;AA2BJ;AAbpB,eAAsB,QAAQ,QAAsB,CAAC,GAAgC;AAMnF,QAAM,mBAAmB;AACzB,QAAM,IAAI,MAAM,kBAAkB;AAClC,QAAM,YAAY,EAAE,IAAI,YAAY,KAAK;AACzC,QAAM,UAAU,EAAE,IAAI,SAAS,KAAK;AACpC,QAAM,WAAW,aAAa,CAAC;AAC/B,QAAM,OAAO,EAAE,IAAI,MAAM,KAAK;AAC9B,QAAM,WAAW,MAAM,YAAY;AACnC,MAAI,CAAC,KAAM,QAAO,oBAAC,kBAAe,QAAO,WAAU;AACnD,QAAM,UAAU,WAAW,IAAI,GAAG,QAAQ;AAE1C,QAAM,KAAK,MAAM;AAAA,IACf,EAAE,SAAS,WAAW,SAAS,SAAS;AAAA,IACxC,EAAE,YAAY,MAAM,YAAY,SAAS,MAAM,QAAQ;AAAA,EACzD;AAEA,MAAI,CAAC,IAAI;AACP,WAAO,oBAAC,kBAAe,QAAO,SAAQ;AAAA,EACxC;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,MAAM,aAAa;AAAA,MAC9B,sBAAoB,GAAG,aAAa;AAAA,MAGpC,yBAAyB,EAAE,QAAQ,GAAG,KAAK;AAAA;AAAA,EAC7C;AAEJ;AAEA,SAAS,eAAe,EAAE,OAAO,GAA2C;AAC1E,SAAO,oBAAC,UAAK,cAAW,WAAU,yBAAyB,EAAE,QAAQ,eAAe,MAAM,OAAO,GAAG;AACtG;AAEA,eAAe,qBAAoC;AACjD,MAAI;AACF,UAAM,MAAO,MAAM,OAAO,YAAY;AACtC,QAAI,OAAO,IAAI,qBAAqB,YAAY;AAC9C,UAAI,iBAAiB;AAAA,IACvB;AAAA,EACF,QAAQ;AAAA,EAGR;AACF;AAEA,eAAe,oBAAoE;AAEjF,QAAM,SAAS,QAAQ;AACvB,MAAI,UAAU,OAAQ,OAA4B,SAAS,YAAY;AACrE,WAAQ,MAAO;AAAA,EACjB;AACA,SAAO;AACT;AAEA,SAAS,aAAa,GAAkD;AACtE,aAAW,OAAO,CAAC,oBAAoB,aAAa,iBAAiB,GAAG;AACtE,UAAM,IAAI,EAAE,IAAI,GAAG;AACnB,QAAI,EAAG,QAAO,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAAA,EACrC;AACA,SAAO;AACT;","names":[]}
|
package/dist/pages.cjs
CHANGED
|
@@ -154,7 +154,12 @@ async function getSmalkAd(req, opts = {}) {
|
|
|
154
154
|
if (preview === void 0) {
|
|
155
155
|
const m = url.match(/[?&]preview=([^&]+)/);
|
|
156
156
|
if (m) {
|
|
157
|
-
|
|
157
|
+
let v;
|
|
158
|
+
try {
|
|
159
|
+
v = decodeURIComponent(m[1]);
|
|
160
|
+
} catch {
|
|
161
|
+
v = m[1];
|
|
162
|
+
}
|
|
158
163
|
preview = v === "1" || v === "true" ? true : v;
|
|
159
164
|
}
|
|
160
165
|
}
|
package/dist/pages.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/pages.ts","../src/index.ts","../src/client.ts"],"sourcesContent":["import * as React from 'react';\nimport type { IncomingMessage } from 'node:http';\nimport { fetchAd } from './client';\nimport type { AdResult } from './index';\n\nexport interface MinimalReq {\n url?: string;\n headers: Record<string, string | string[] | undefined>;\n}\n\nexport interface GetSmalkAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string. Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function getSmalkAd(\n req: IncomingMessage | MinimalReq,\n opts: GetSmalkAdOptions = {},\n): Promise<AdResult | null> {\n const headers = (req as MinimalReq).headers ?? {};\n const url = (req as MinimalReq).url ?? '/';\n const host = headerValue(headers, 'host') ?? '';\n const pageUrl = host ? `https://${host}${url}` : url;\n const userAgent = headerValue(headers, 'user-agent') ?? '';\n const referer = headerValue(headers, 'referer') ?? '';\n const clientIp = pickClientIp(headers);\n\n // Auto-forward `?preview=` from the page URL (so visiting /blog/x?preview=1 works without\n // a callsite change). Explicit opts.preview takes precedence.\n let preview = opts.preview;\n if (preview === undefined) {\n const m = url.match(/[?&]preview=([^&]+)/);\n if (m) {\n const v = decodeURIComponent(m[1]);\n preview = v === '1' || v === 'true' ? true : v;\n }\n }\n\n return fetchAd(\n { pageUrl, userAgent, referer, clientIp },\n { revalidate: opts.revalidate, preview },\n );\n}\n\nexport interface AdHtmlProps {\n ad: AdResult | null;\n className?: string;\n}\n\nexport function AdHtml({ ad, className }: AdHtmlProps): React.ReactElement {\n if (!ad) {\n return React.createElement('span', {\n 'data-smalk': 'comment',\n dangerouslySetInnerHTML: { __html: '<!-- smalk: no ad -->' },\n });\n }\n return React.createElement('div', {\n className: className ?? 'smalk-ads',\n 'data-smalk-booking': ad.bookingId ?? undefined,\n dangerouslySetInnerHTML: { __html: ad.html },\n });\n}\n\nfunction headerValue(h: Record<string, string | string[] | undefined>, key: string): string | null {\n const v = h[key] ?? h[key.toLowerCase()];\n if (Array.isArray(v)) return v[0] ?? null;\n return v ?? null;\n}\n\nfunction pickClientIp(h: Record<string, string | string[] | undefined>): string {\n for (const key of ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for']) {\n const v = headerValue(h, key);\n if (v) return v.split(',')[0].trim();\n }\n return '';\n}\n","export interface SmalkAdsConfig {\n projectKey: string;\n apiKey: string;\n apiBaseUrl: string;\n}\n\nexport interface AdInput {\n pageUrl: string;\n userAgent: string;\n referer: string;\n clientIp: string;\n}\n\nexport interface AdResult {\n html: string;\n bookingId: string | null;\n}\n\nexport interface ApiResponse {\n html?: string;\n booking_id?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport const DEFAULT_TIMEOUT_MS = 100;\nexport const DEFAULT_REVALIDATE_S = 1200;\nexport const API_PATH = '/api/v1/transform/ads/content/';\n\nexport function loadConfig(): SmalkAdsConfig {\n const projectKey = process.env.SMALK_PROJECT_KEY;\n const apiKey = process.env.SMALK_API_KEY;\n const apiBaseUrl = process.env.SMALK_API_BASE_URL || 'https://api.smalk.ai';\n const isProd = process.env.NODE_ENV === 'production';\n\n if (!projectKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production');\n console.warn('[smalk] SMALK_PROJECT_KEY not set — ad fetch will be skipped');\n }\n if (!apiKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_API_KEY env var is required in production');\n console.warn('[smalk] SMALK_API_KEY not set — ad fetch will be skipped');\n }\n\n return {\n projectKey: projectKey ?? '',\n apiKey: apiKey ?? '',\n apiBaseUrl,\n };\n}\n","import { loadConfig, AdInput, AdResult, ApiResponse, API_PATH, DEFAULT_TIMEOUT_MS, DEFAULT_REVALIDATE_S } from './index';\n\nconst lastHashByUrl = new Map<string, string>();\n\nexport function _resetHashCacheForTests(): void {\n lastHashByUrl.clear();\n}\n\nexport interface FetchAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function fetchAd(input: AdInput, opts: FetchAdOptions = {}): Promise<AdResult | null> {\n const cfg = loadConfig();\n if (!cfg.projectKey || !cfg.apiKey) return null;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);\n\n try {\n const body: Record<string, unknown> = {\n project_key: cfg.projectKey,\n page_url: input.pageUrl,\n user_agent: input.userAgent,\n referer: input.referer,\n client_ip: input.clientIp,\n };\n if (opts.preview !== undefined && opts.preview !== false) {\n body.preview = opts.preview;\n }\n const res = await fetch(`${cfg.apiBaseUrl}${API_PATH}`, {\n method: 'POST',\n headers: {\n 'Authorization': `Api-Key ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n // `next` is a Next.js fetch extension; type provided by next types when present.\n next: {\n revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,\n tags: ['smalk-ad', `smalk-ad:${input.pageUrl}`],\n },\n } as RequestInit);\n\n if (!res.ok) {\n logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);\n return null;\n }\n\n const data = (await res.json()) as ApiResponse;\n if (!data.html) return null;\n\n const hash = await sha256Hex(data.html);\n const prev = lastHashByUrl.get(input.pageUrl);\n if (prev && prev !== hash) {\n await maybeRevalidatePath(input.pageUrl);\n }\n lastHashByUrl.set(input.pageUrl, hash);\n\n return { html: data.html, bookingId: data.booking_id ?? null };\n } catch (err) {\n if (err instanceof Error && err.name !== 'AbortError') {\n logErrorOnce(`smalk-ads fetch error: ${err.message}`);\n }\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function maybeRevalidatePath(pageUrl: string): Promise<void> {\n try {\n const mod = await import('next/cache');\n if (typeof mod.revalidatePath === 'function') {\n const pathname = new URL(pageUrl).pathname;\n mod.revalidatePath(pathname);\n }\n } catch {\n // next/cache not available (Pages Router runtime, edge, or test env without next).\n }\n}\n\nconst seenErrors = new Set<string>();\nfunction logErrorOnce(msg: string): void {\n if (seenErrors.has(msg)) return;\n seenErrors.add(msg);\n console.error(`[smalk-nextjs-ads] ${msg}`);\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const buf = await globalThis.crypto.subtle.digest('SHA-256', data);\n const bytes = new Uint8Array(buf);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, '0');\n }\n return hex;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAAuB;;;ACwBhB,IAAM,qBAAqB;AAC3B,IAAM,uBAAuB;AAC7B,IAAM,WAAW;AAEjB,SAAS,aAA6B;AAC3C,QAAM,aAAa,QAAQ,IAAI;AAC/B,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,aAAa,QAAQ,IAAI,sBAAsB;AACrD,QAAM,SAAS,QAAQ,IAAI,aAAa;AAExC,MAAI,CAAC,YAAY;AACf,QAAI,OAAQ,OAAM,IAAI,MAAM,wEAAwE;AACpG,YAAQ,KAAK,mEAA8D;AAAA,EAC7E;AACA,MAAI,CAAC,QAAQ;AACX,QAAI,OAAQ,OAAM,IAAI,MAAM,oEAAoE;AAChG,YAAQ,KAAK,+DAA0D;AAAA,EACzE;AAEA,SAAO;AAAA,IACL,YAAY,cAAc;AAAA,IAC1B,QAAQ,UAAU;AAAA,IAClB;AAAA,EACF;AACF;;;AC9CA,IAAM,gBAAgB,oBAAI,IAAoB;AAY9C,eAAsB,QAAQ,OAAgB,OAAuB,CAAC,GAA6B;AACjG,QAAM,MAAM,WAAW;AACvB,MAAI,CAAC,IAAI,cAAc,CAAC,IAAI,OAAQ,QAAO;AAE3C,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,kBAAkB;AAErE,MAAI;AACF,UAAM,OAAgC;AAAA,MACpC,aAAa,IAAI;AAAA,MACjB,UAAU,MAAM;AAAA,MAChB,YAAY,MAAM;AAAA,MAClB,SAAS,MAAM;AAAA,MACf,WAAW,MAAM;AAAA,IACnB;AACA,QAAI,KAAK,YAAY,UAAa,KAAK,YAAY,OAAO;AACxD,WAAK,UAAU,KAAK;AAAA,IACtB;AACA,UAAM,MAAM,MAAM,MAAM,GAAG,IAAI,UAAU,GAAG,QAAQ,IAAI;AAAA,MACtD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,iBAAiB,WAAW,IAAI,MAAM;AAAA,QACtC,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB,QAAQ,WAAW;AAAA;AAAA,MAEnB,MAAM;AAAA,QACJ,YAAY,KAAK,cAAc;AAAA,QAC/B,MAAM,CAAC,YAAY,YAAY,MAAM,OAAO,EAAE;AAAA,MAChD;AAAA,IACF,CAAgB;AAEhB,QAAI,CAAC,IAAI,IAAI;AACX,mBAAa,gCAAgC,IAAI,MAAM,EAAE;AACzD,aAAO;AAAA,IACT;AAEA,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAI,CAAC,KAAK,KAAM,QAAO;AAEvB,UAAM,OAAO,MAAM,UAAU,KAAK,IAAI;AACtC,UAAM,OAAO,cAAc,IAAI,MAAM,OAAO;AAC5C,QAAI,QAAQ,SAAS,MAAM;AACzB,YAAM,oBAAoB,MAAM,OAAO;AAAA,IACzC;AACA,kBAAc,IAAI,MAAM,SAAS,IAAI;AAErC,WAAO,EAAE,MAAM,KAAK,MAAM,WAAW,KAAK,cAAc,KAAK;AAAA,EAC/D,SAAS,KAAK;AACZ,QAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD,mBAAa,0BAA0B,IAAI,OAAO,EAAE;AAAA,IACtD;AACA,WAAO;AAAA,EACT,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAEA,eAAe,oBAAoB,SAAgC;AACjE,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,YAAY;AACrC,QAAI,OAAO,IAAI,mBAAmB,YAAY;AAC5C,YAAM,WAAW,IAAI,IAAI,OAAO,EAAE;AAClC,UAAI,eAAe,QAAQ;AAAA,IAC7B;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEA,IAAM,aAAa,oBAAI,IAAY;AACnC,SAAS,aAAa,KAAmB;AACvC,MAAI,WAAW,IAAI,GAAG,EAAG;AACzB,aAAW,IAAI,GAAG;AAClB,UAAQ,MAAM,sBAAsB,GAAG,EAAE;AAC3C;AAEA,eAAe,UAAU,OAAgC;AACvD,QAAM,OAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AAC3C,QAAM,MAAM,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,IAAI;AACjE,QAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,WAAO,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAAA,EAC9C;AACA,SAAO;AACT;;;AFrFA,eAAsB,WACpB,KACA,OAA0B,CAAC,GACD;AAC1B,QAAM,UAAW,IAAmB,WAAW,CAAC;AAChD,QAAM,MAAO,IAAmB,OAAO;AACvC,QAAM,OAAO,YAAY,SAAS,MAAM,KAAK;AAC7C,QAAM,UAAU,OAAO,WAAW,IAAI,GAAG,GAAG,KAAK;AACjD,QAAM,YAAY,YAAY,SAAS,YAAY,KAAK;AACxD,QAAM,UAAU,YAAY,SAAS,SAAS,KAAK;AACnD,QAAM,WAAW,aAAa,OAAO;AAIrC,MAAI,UAAU,KAAK;AACnB,MAAI,YAAY,QAAW;AACzB,UAAM,IAAI,IAAI,MAAM,qBAAqB;AACzC,QAAI,GAAG;AACL,YAAM,IAAI,mBAAmB,EAAE,CAAC,CAAC;AACjC,gBAAU,MAAM,OAAO,MAAM,SAAS,OAAO;AAAA,IAC/C;AAAA,EACF;AAEA,SAAO;AAAA,IACL,EAAE,SAAS,WAAW,SAAS,SAAS;AAAA,IACxC,EAAE,YAAY,KAAK,YAAY,QAAQ;AAAA,EACzC;AACF;AAOO,SAAS,OAAO,EAAE,IAAI,UAAU,GAAoC;AACzE,MAAI,CAAC,IAAI;AACP,WAAa,oBAAc,QAAQ;AAAA,MACjC,cAAc;AAAA,MACd,yBAAyB,EAAE,QAAQ,wBAAwB;AAAA,IAC7D,CAAC;AAAA,EACH;AACA,SAAa,oBAAc,OAAO;AAAA,IAChC,WAAW,aAAa;AAAA,IACxB,sBAAsB,GAAG,aAAa;AAAA,IACtC,yBAAyB,EAAE,QAAQ,GAAG,KAAK;AAAA,EAC7C,CAAC;AACH;AAEA,SAAS,YAAY,GAAkD,KAA4B;AACjG,QAAM,IAAI,EAAE,GAAG,KAAK,EAAE,IAAI,YAAY,CAAC;AACvC,MAAI,MAAM,QAAQ,CAAC,EAAG,QAAO,EAAE,CAAC,KAAK;AACrC,SAAO,KAAK;AACd;AAEA,SAAS,aAAa,GAA0D;AAC9E,aAAW,OAAO,CAAC,oBAAoB,aAAa,iBAAiB,GAAG;AACtE,UAAM,IAAI,YAAY,GAAG,GAAG;AAC5B,QAAI,EAAG,QAAO,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAAA,EACrC;AACA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/pages.ts","../src/index.ts","../src/client.ts"],"sourcesContent":["import * as React from 'react';\nimport type { IncomingMessage } from 'node:http';\nimport { fetchAd } from './client';\nimport type { AdResult } from './index';\n\nexport interface MinimalReq {\n url?: string;\n headers: Record<string, string | string[] | undefined>;\n}\n\nexport interface GetSmalkAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string. Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function getSmalkAd(\n req: IncomingMessage | MinimalReq,\n opts: GetSmalkAdOptions = {},\n): Promise<AdResult | null> {\n const headers = (req as MinimalReq).headers ?? {};\n const url = (req as MinimalReq).url ?? '/';\n const host = headerValue(headers, 'host') ?? '';\n const pageUrl = host ? `https://${host}${url}` : url;\n const userAgent = headerValue(headers, 'user-agent') ?? '';\n const referer = headerValue(headers, 'referer') ?? '';\n const clientIp = pickClientIp(headers);\n\n // Auto-forward `?preview=` from the page URL (so visiting /blog/x?preview=1 works without\n // a callsite change). Explicit opts.preview takes precedence.\n let preview = opts.preview;\n if (preview === undefined) {\n const m = url.match(/[?&]preview=([^&]+)/);\n if (m) {\n let v: string;\n try {\n v = decodeURIComponent(m[1]);\n } catch {\n v = m[1];\n }\n preview = v === '1' || v === 'true' ? true : v;\n }\n }\n\n return fetchAd(\n { pageUrl, userAgent, referer, clientIp },\n { revalidate: opts.revalidate, preview },\n );\n}\n\nexport interface AdHtmlProps {\n ad: AdResult | null;\n className?: string;\n}\n\nexport function AdHtml({ ad, className }: AdHtmlProps): React.ReactElement {\n if (!ad) {\n return React.createElement('span', {\n 'data-smalk': 'comment',\n dangerouslySetInnerHTML: { __html: '<!-- smalk: no ad -->' },\n });\n }\n return React.createElement('div', {\n className: className ?? 'smalk-ads',\n 'data-smalk-booking': ad.bookingId ?? undefined,\n dangerouslySetInnerHTML: { __html: ad.html },\n });\n}\n\nfunction headerValue(h: Record<string, string | string[] | undefined>, key: string): string | null {\n const v = h[key] ?? h[key.toLowerCase()];\n if (Array.isArray(v)) return v[0] ?? null;\n return v ?? null;\n}\n\nfunction pickClientIp(h: Record<string, string | string[] | undefined>): string {\n for (const key of ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for']) {\n const v = headerValue(h, key);\n if (v) return v.split(',')[0].trim();\n }\n return '';\n}\n","export interface SmalkAdsConfig {\n projectKey: string;\n apiKey: string;\n apiBaseUrl: string;\n}\n\nexport interface AdInput {\n pageUrl: string;\n userAgent: string;\n referer: string;\n clientIp: string;\n}\n\nexport interface AdResult {\n html: string;\n bookingId: string | null;\n}\n\nexport interface ApiResponse {\n html?: string;\n booking_id?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport const DEFAULT_TIMEOUT_MS = 100;\nexport const DEFAULT_REVALIDATE_S = 1200;\nexport const API_PATH = '/api/v1/transform/ads/content/';\n\nexport function loadConfig(): SmalkAdsConfig {\n const projectKey = process.env.SMALK_PROJECT_KEY;\n const apiKey = process.env.SMALK_API_KEY;\n const apiBaseUrl = process.env.SMALK_API_BASE_URL || 'https://api.smalk.ai';\n const isProd = process.env.NODE_ENV === 'production';\n\n if (!projectKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production');\n console.warn('[smalk] SMALK_PROJECT_KEY not set — ad fetch will be skipped');\n }\n if (!apiKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_API_KEY env var is required in production');\n console.warn('[smalk] SMALK_API_KEY not set — ad fetch will be skipped');\n }\n\n return {\n projectKey: projectKey ?? '',\n apiKey: apiKey ?? '',\n apiBaseUrl,\n };\n}\n","import { loadConfig, AdInput, AdResult, ApiResponse, API_PATH, DEFAULT_TIMEOUT_MS, DEFAULT_REVALIDATE_S } from './index';\n\nconst lastHashByUrl = new Map<string, string>();\n\nexport function _resetHashCacheForTests(): void {\n lastHashByUrl.clear();\n}\n\nexport interface FetchAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function fetchAd(input: AdInput, opts: FetchAdOptions = {}): Promise<AdResult | null> {\n const cfg = loadConfig();\n if (!cfg.projectKey || !cfg.apiKey) return null;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);\n\n try {\n const body: Record<string, unknown> = {\n project_key: cfg.projectKey,\n page_url: input.pageUrl,\n user_agent: input.userAgent,\n referer: input.referer,\n client_ip: input.clientIp,\n };\n if (opts.preview !== undefined && opts.preview !== false) {\n body.preview = opts.preview;\n }\n const res = await fetch(`${cfg.apiBaseUrl}${API_PATH}`, {\n method: 'POST',\n headers: {\n 'Authorization': `Api-Key ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n // `next` is a Next.js fetch extension; type provided by next types when present.\n next: {\n revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,\n tags: ['smalk-ad', `smalk-ad:${input.pageUrl}`],\n },\n } as RequestInit);\n\n if (!res.ok) {\n logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);\n return null;\n }\n\n const data = (await res.json()) as ApiResponse;\n if (!data.html) return null;\n\n const hash = await sha256Hex(data.html);\n const prev = lastHashByUrl.get(input.pageUrl);\n if (prev && prev !== hash) {\n await maybeRevalidatePath(input.pageUrl);\n }\n lastHashByUrl.set(input.pageUrl, hash);\n\n return { html: data.html, bookingId: data.booking_id ?? null };\n } catch (err) {\n if (err instanceof Error && err.name !== 'AbortError') {\n logErrorOnce(`smalk-ads fetch error: ${err.message}`);\n }\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function maybeRevalidatePath(pageUrl: string): Promise<void> {\n try {\n const mod = await import('next/cache');\n if (typeof mod.revalidatePath === 'function') {\n const pathname = new URL(pageUrl).pathname;\n mod.revalidatePath(pathname);\n }\n } catch {\n // next/cache not available (Pages Router runtime, edge, or test env without next).\n }\n}\n\nconst seenErrors = new Set<string>();\nfunction logErrorOnce(msg: string): void {\n if (seenErrors.has(msg)) return;\n seenErrors.add(msg);\n console.error(`[smalk-nextjs-ads] ${msg}`);\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const buf = await globalThis.crypto.subtle.digest('SHA-256', data);\n const bytes = new Uint8Array(buf);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, '0');\n }\n return hex;\n}\n"],"mappings":"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","names":[]}
|
package/dist/pages.js
CHANGED
|
@@ -17,7 +17,12 @@ async function getSmalkAd(req, opts = {}) {
|
|
|
17
17
|
if (preview === void 0) {
|
|
18
18
|
const m = url.match(/[?&]preview=([^&]+)/);
|
|
19
19
|
if (m) {
|
|
20
|
-
|
|
20
|
+
let v;
|
|
21
|
+
try {
|
|
22
|
+
v = decodeURIComponent(m[1]);
|
|
23
|
+
} catch {
|
|
24
|
+
v = m[1];
|
|
25
|
+
}
|
|
21
26
|
preview = v === "1" || v === "true" ? true : v;
|
|
22
27
|
}
|
|
23
28
|
}
|
package/dist/pages.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/pages.ts"],"sourcesContent":["import * as React from 'react';\nimport type { IncomingMessage } from 'node:http';\nimport { fetchAd } from './client';\nimport type { AdResult } from './index';\n\nexport interface MinimalReq {\n url?: string;\n headers: Record<string, string | string[] | undefined>;\n}\n\nexport interface GetSmalkAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string. Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function getSmalkAd(\n req: IncomingMessage | MinimalReq,\n opts: GetSmalkAdOptions = {},\n): Promise<AdResult | null> {\n const headers = (req as MinimalReq).headers ?? {};\n const url = (req as MinimalReq).url ?? '/';\n const host = headerValue(headers, 'host') ?? '';\n const pageUrl = host ? `https://${host}${url}` : url;\n const userAgent = headerValue(headers, 'user-agent') ?? '';\n const referer = headerValue(headers, 'referer') ?? '';\n const clientIp = pickClientIp(headers);\n\n // Auto-forward `?preview=` from the page URL (so visiting /blog/x?preview=1 works without\n // a callsite change). Explicit opts.preview takes precedence.\n let preview = opts.preview;\n if (preview === undefined) {\n const m = url.match(/[?&]preview=([^&]+)/);\n if (m) {\n
|
|
1
|
+
{"version":3,"sources":["../src/pages.ts"],"sourcesContent":["import * as React from 'react';\nimport type { IncomingMessage } from 'node:http';\nimport { fetchAd } from './client';\nimport type { AdResult } from './index';\n\nexport interface MinimalReq {\n url?: string;\n headers: Record<string, string | string[] | undefined>;\n}\n\nexport interface GetSmalkAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string. Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function getSmalkAd(\n req: IncomingMessage | MinimalReq,\n opts: GetSmalkAdOptions = {},\n): Promise<AdResult | null> {\n const headers = (req as MinimalReq).headers ?? {};\n const url = (req as MinimalReq).url ?? '/';\n const host = headerValue(headers, 'host') ?? '';\n const pageUrl = host ? `https://${host}${url}` : url;\n const userAgent = headerValue(headers, 'user-agent') ?? '';\n const referer = headerValue(headers, 'referer') ?? '';\n const clientIp = pickClientIp(headers);\n\n // Auto-forward `?preview=` from the page URL (so visiting /blog/x?preview=1 works without\n // a callsite change). Explicit opts.preview takes precedence.\n let preview = opts.preview;\n if (preview === undefined) {\n const m = url.match(/[?&]preview=([^&]+)/);\n if (m) {\n let v: string;\n try {\n v = decodeURIComponent(m[1]);\n } catch {\n v = m[1];\n }\n preview = v === '1' || v === 'true' ? true : v;\n }\n }\n\n return fetchAd(\n { pageUrl, userAgent, referer, clientIp },\n { revalidate: opts.revalidate, preview },\n );\n}\n\nexport interface AdHtmlProps {\n ad: AdResult | null;\n className?: string;\n}\n\nexport function AdHtml({ ad, className }: AdHtmlProps): React.ReactElement {\n if (!ad) {\n return React.createElement('span', {\n 'data-smalk': 'comment',\n dangerouslySetInnerHTML: { __html: '<!-- smalk: no ad -->' },\n });\n }\n return React.createElement('div', {\n className: className ?? 'smalk-ads',\n 'data-smalk-booking': ad.bookingId ?? undefined,\n dangerouslySetInnerHTML: { __html: ad.html },\n });\n}\n\nfunction headerValue(h: Record<string, string | string[] | undefined>, key: string): string | null {\n const v = h[key] ?? h[key.toLowerCase()];\n if (Array.isArray(v)) return v[0] ?? null;\n return v ?? null;\n}\n\nfunction pickClientIp(h: Record<string, string | string[] | undefined>): string {\n for (const key of ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for']) {\n const v = headerValue(h, key);\n if (v) return v.split(',')[0].trim();\n }\n return '';\n}\n"],"mappings":";;;;;;AAAA,YAAY,WAAW;AAgBvB,eAAsB,WACpB,KACA,OAA0B,CAAC,GACD;AAC1B,QAAM,UAAW,IAAmB,WAAW,CAAC;AAChD,QAAM,MAAO,IAAmB,OAAO;AACvC,QAAM,OAAO,YAAY,SAAS,MAAM,KAAK;AAC7C,QAAM,UAAU,OAAO,WAAW,IAAI,GAAG,GAAG,KAAK;AACjD,QAAM,YAAY,YAAY,SAAS,YAAY,KAAK;AACxD,QAAM,UAAU,YAAY,SAAS,SAAS,KAAK;AACnD,QAAM,WAAW,aAAa,OAAO;AAIrC,MAAI,UAAU,KAAK;AACnB,MAAI,YAAY,QAAW;AACzB,UAAM,IAAI,IAAI,MAAM,qBAAqB;AACzC,QAAI,GAAG;AACL,UAAI;AACJ,UAAI;AACF,YAAI,mBAAmB,EAAE,CAAC,CAAC;AAAA,MAC7B,QAAQ;AACN,YAAI,EAAE,CAAC;AAAA,MACT;AACA,gBAAU,MAAM,OAAO,MAAM,SAAS,OAAO;AAAA,IAC/C;AAAA,EACF;AAEA,SAAO;AAAA,IACL,EAAE,SAAS,WAAW,SAAS,SAAS;AAAA,IACxC,EAAE,YAAY,KAAK,YAAY,QAAQ;AAAA,EACzC;AACF;AAOO,SAAS,OAAO,EAAE,IAAI,UAAU,GAAoC;AACzE,MAAI,CAAC,IAAI;AACP,WAAa,oBAAc,QAAQ;AAAA,MACjC,cAAc;AAAA,MACd,yBAAyB,EAAE,QAAQ,wBAAwB;AAAA,IAC7D,CAAC;AAAA,EACH;AACA,SAAa,oBAAc,OAAO;AAAA,IAChC,WAAW,aAAa;AAAA,IACxB,sBAAsB,GAAG,aAAa;AAAA,IACtC,yBAAyB,EAAE,QAAQ,GAAG,KAAK;AAAA,EAC7C,CAAC;AACH;AAEA,SAAS,YAAY,GAAkD,KAA4B;AACjG,QAAM,IAAI,EAAE,GAAG,KAAK,EAAE,IAAI,YAAY,CAAC;AACvC,MAAI,MAAM,QAAQ,CAAC,EAAG,QAAO,EAAE,CAAC,KAAK;AACrC,SAAO,KAAK;AACd;AAEA,SAAS,aAAa,GAA0D;AAC9E,aAAW,OAAO,CAAC,oBAAoB,aAAa,iBAAiB,GAAG;AACtE,UAAM,IAAI,YAAY,GAAG,GAAG;AAC5B,QAAI,EAAG,QAAO,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAAA,EACrC;AACA,SAAO;AACT;","names":[]}
|