@smalk/nextjs-ads 0.1.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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/app.cjs +191 -0
- package/dist/app.cjs.map +1 -0
- package/dist/app.d.cts +15 -0
- package/dist/app.d.ts +15 -0
- package/dist/app.js +54 -0
- package/dist/app.js.map +1 -0
- package/dist/chunk-HX4ZGDYQ.js +31 -0
- package/dist/chunk-HX4ZGDYQ.js.map +1 -0
- package/dist/chunk-YA6M2IA4.js +92 -0
- package/dist/chunk-YA6M2IA4.js.map +1 -0
- package/dist/index.cjs +58 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.cjs +185 -0
- package/dist/middleware.cjs.map +1 -0
- package/dist/middleware.d.cts +8 -0
- package/dist/middleware.d.ts +8 -0
- package/dist/middleware.js +48 -0
- package/dist/middleware.js.map +1 -0
- package/dist/pages.cjs +196 -0
- package/dist/pages.cjs.map +1 -0
- package/dist/pages.d.cts +21 -0
- package/dist/pages.d.ts +21 -0
- package/dist/pages.js +58 -0
- package/dist/pages.js.map +1 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Smalk SAS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @smalk/nextjs-ads
|
|
2
|
+
|
|
3
|
+
Server-side Smalk ad injection for Next.js publisher sites.
|
|
4
|
+
|
|
5
|
+
- App Router (`<SmalkAd>` Server Component)
|
|
6
|
+
- Pages Router (`getSmalkAd()` + `<AdHtml>`)
|
|
7
|
+
- Middleware variant (`withSmalkAds()` for raw `<div smalk-ads>` placeholders)
|
|
8
|
+
|
|
9
|
+
Compat: **Next 13.4+**, **Node 18+**.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @smalk/nextjs-ads
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Set two env vars:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
SMALK_PROJECT_KEY=your-workspace-uuid
|
|
21
|
+
SMALK_API_KEY=your-api-key
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
(`SMALK_API_BASE_URL` is optional — defaults to `https://api.smalk.ai`.)
|
|
25
|
+
|
|
26
|
+
## Usage — App Router
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
// app/blog/[slug]/page.tsx
|
|
30
|
+
import { SmalkAd } from '@smalk/nextjs-ads/app';
|
|
31
|
+
|
|
32
|
+
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
|
33
|
+
const { slug } = await params;
|
|
34
|
+
return (
|
|
35
|
+
<article>
|
|
36
|
+
<h1>{slug}</h1>
|
|
37
|
+
<SmalkAd pathname={`/blog/${slug}`} />
|
|
38
|
+
</article>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage — Pages Router
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// pages/blog/[slug].tsx
|
|
47
|
+
import { getSmalkAd, AdHtml } from '@smalk/nextjs-ads/pages';
|
|
48
|
+
|
|
49
|
+
export const getServerSideProps = async (ctx) => {
|
|
50
|
+
const ad = await getSmalkAd(ctx.req);
|
|
51
|
+
return { props: { ad } };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default function Page({ ad }) {
|
|
55
|
+
return (
|
|
56
|
+
<article>
|
|
57
|
+
<h1>Title</h1>
|
|
58
|
+
<AdHtml ad={ad} />
|
|
59
|
+
</article>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage — Middleware (raw `<div smalk-ads>`)
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// middleware.ts
|
|
68
|
+
import { withSmalkAds } from '@smalk/nextjs-ads/middleware';
|
|
69
|
+
|
|
70
|
+
export default withSmalkAds();
|
|
71
|
+
|
|
72
|
+
export const config = {
|
|
73
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then put `<div smalk-ads></div>` anywhere in your page HTML — middleware regex-replaces it with API HTML at request time.
|
|
78
|
+
|
|
79
|
+
## Caching
|
|
80
|
+
|
|
81
|
+
- `fetch` data cache: 20 min `revalidate` (override with `<SmalkAd revalidate={N} />`)
|
|
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
|
+
- HTTP: callers should not add CDN caching to ad-bearing pages (`Cache-Control: private, no-cache, must-revalidate` recommended at the edge)
|
|
84
|
+
|
|
85
|
+
## Trust Boundary
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
## Troubleshooting
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
curl -sA "Mozilla/5.0 ChatGPT-User/1.0" https://yoursite.com/blog/article | grep -iE 'smalk-ads|booking'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If the response only contains `<!-- smalk: no ad -->`, either no booking is active or the API timed out (100 ms). Check publisher dashboard inventory status.
|
|
96
|
+
|
|
97
|
+
## Middleware variant — important caveat
|
|
98
|
+
|
|
99
|
+
The `withSmalkAds()` helper is exposed for completeness but **does not work as a Next.js middleware** for HTML body transformation. Next.js Edge middleware runs BEFORE the route renders; `NextResponse.next()` is a sentinel and its body is empty. The helper is reusable in non-Next.js custom-server / Express-style setups (e.g., a Node fronting reverse proxy). For in-Next.js usage, prefer `<SmalkAd>` (App Router) or `getSmalkAd()` (Pages Router).
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
package/dist/app.cjs
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/app.tsx
|
|
31
|
+
var app_exports = {};
|
|
32
|
+
__export(app_exports, {
|
|
33
|
+
SmalkAd: () => SmalkAd
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(app_exports);
|
|
36
|
+
var import_server_only = require("server-only");
|
|
37
|
+
var import_headers = require("next/headers");
|
|
38
|
+
|
|
39
|
+
// src/index.ts
|
|
40
|
+
var DEFAULT_TIMEOUT_MS = 100;
|
|
41
|
+
var DEFAULT_REVALIDATE_S = 1200;
|
|
42
|
+
var API_PATH = "/api/v1/transform/ads/content/";
|
|
43
|
+
function loadConfig() {
|
|
44
|
+
const projectKey = process.env.SMALK_PROJECT_KEY;
|
|
45
|
+
const apiKey = process.env.SMALK_API_KEY;
|
|
46
|
+
const apiBaseUrl = process.env.SMALK_API_BASE_URL || "https://api.smalk.ai";
|
|
47
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
48
|
+
if (!projectKey) {
|
|
49
|
+
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production");
|
|
50
|
+
console.warn("[smalk] SMALK_PROJECT_KEY not set \u2014 ad fetch will be skipped");
|
|
51
|
+
}
|
|
52
|
+
if (!apiKey) {
|
|
53
|
+
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_API_KEY env var is required in production");
|
|
54
|
+
console.warn("[smalk] SMALK_API_KEY not set \u2014 ad fetch will be skipped");
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
projectKey: projectKey ?? "",
|
|
58
|
+
apiKey: apiKey ?? "",
|
|
59
|
+
apiBaseUrl
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/client.ts
|
|
64
|
+
var lastHashByUrl = /* @__PURE__ */ new Map();
|
|
65
|
+
async function fetchAd(input, opts = {}) {
|
|
66
|
+
const cfg = loadConfig();
|
|
67
|
+
if (!cfg.projectKey || !cfg.apiKey) return null;
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
70
|
+
try {
|
|
71
|
+
const body = {
|
|
72
|
+
project_key: cfg.projectKey,
|
|
73
|
+
page_url: input.pageUrl,
|
|
74
|
+
user_agent: input.userAgent,
|
|
75
|
+
referer: input.referer,
|
|
76
|
+
client_ip: input.clientIp
|
|
77
|
+
};
|
|
78
|
+
if (opts.preview !== void 0 && opts.preview !== false) {
|
|
79
|
+
body.preview = opts.preview;
|
|
80
|
+
}
|
|
81
|
+
const res = await fetch(`${cfg.apiBaseUrl}${API_PATH}`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"Authorization": `Api-Key ${cfg.apiKey}`,
|
|
85
|
+
"Content-Type": "application/json"
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
signal: controller.signal,
|
|
89
|
+
// `next` is a Next.js fetch extension; type provided by next types when present.
|
|
90
|
+
next: {
|
|
91
|
+
revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,
|
|
92
|
+
tags: ["smalk-ad", `smalk-ad:${input.pageUrl}`]
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const data = await res.json();
|
|
100
|
+
if (!data.html) return null;
|
|
101
|
+
const hash = await sha256Hex(data.html);
|
|
102
|
+
const prev = lastHashByUrl.get(input.pageUrl);
|
|
103
|
+
if (prev && prev !== hash) {
|
|
104
|
+
await maybeRevalidatePath(input.pageUrl);
|
|
105
|
+
}
|
|
106
|
+
lastHashByUrl.set(input.pageUrl, hash);
|
|
107
|
+
return { html: data.html, bookingId: data.booking_id ?? null };
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
110
|
+
logErrorOnce(`smalk-ads fetch error: ${err.message}`);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
} finally {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function maybeRevalidatePath(pageUrl) {
|
|
118
|
+
try {
|
|
119
|
+
const mod = await import("next/cache");
|
|
120
|
+
if (typeof mod.revalidatePath === "function") {
|
|
121
|
+
const pathname = new URL(pageUrl).pathname;
|
|
122
|
+
mod.revalidatePath(pathname);
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
var seenErrors = /* @__PURE__ */ new Set();
|
|
128
|
+
function logErrorOnce(msg) {
|
|
129
|
+
if (seenErrors.has(msg)) return;
|
|
130
|
+
seenErrors.add(msg);
|
|
131
|
+
console.error(`[smalk-nextjs-ads] ${msg}`);
|
|
132
|
+
}
|
|
133
|
+
async function sha256Hex(input) {
|
|
134
|
+
const data = new TextEncoder().encode(input);
|
|
135
|
+
const buf = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
136
|
+
const bytes = new Uint8Array(buf);
|
|
137
|
+
let hex = "";
|
|
138
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
139
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
140
|
+
}
|
|
141
|
+
return hex;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/app.tsx
|
|
145
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
146
|
+
async function SmalkAd(props = {}) {
|
|
147
|
+
const h = await readHeadersCompat();
|
|
148
|
+
const userAgent = h.get("user-agent") ?? "";
|
|
149
|
+
const referer = h.get("referer") ?? "";
|
|
150
|
+
const clientIp = pickClientIp(h);
|
|
151
|
+
const host = h.get("host") ?? "";
|
|
152
|
+
const pathname = props.pathname ?? "/";
|
|
153
|
+
const pageUrl = host ? `https://${host}${pathname}` : pathname;
|
|
154
|
+
const ad = await fetchAd(
|
|
155
|
+
{ pageUrl, userAgent, referer, clientIp },
|
|
156
|
+
{ revalidate: props.revalidate, preview: props.preview }
|
|
157
|
+
);
|
|
158
|
+
if (!ad) {
|
|
159
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SmalkAdComment, { reason: "no ad" });
|
|
160
|
+
}
|
|
161
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
162
|
+
"div",
|
|
163
|
+
{
|
|
164
|
+
className: props.className ?? "smalk-ads",
|
|
165
|
+
"data-smalk-booking": ad.bookingId ?? void 0,
|
|
166
|
+
dangerouslySetInnerHTML: { __html: ad.html }
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
function SmalkAdComment({ reason }) {
|
|
171
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-smalk": "comment", dangerouslySetInnerHTML: { __html: `<!-- smalk: ${reason} -->` } });
|
|
172
|
+
}
|
|
173
|
+
async function readHeadersCompat() {
|
|
174
|
+
const result = (0, import_headers.headers)();
|
|
175
|
+
if (result && typeof result.then === "function") {
|
|
176
|
+
return await result;
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
function pickClientIp(h) {
|
|
181
|
+
for (const key of ["cf-connecting-ip", "x-real-ip", "x-forwarded-for"]) {
|
|
182
|
+
const v = h.get(key);
|
|
183
|
+
if (v) return v.split(",")[0].trim();
|
|
184
|
+
}
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
188
|
+
0 && (module.exports = {
|
|
189
|
+
SmalkAd
|
|
190
|
+
});
|
|
191
|
+
//# sourceMappingURL=app.cjs.map
|
package/dist/app.cjs.map
ADDED
|
@@ -0,0 +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":"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","names":[]}
|
package/dist/app.d.cts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface SmalkAdProps {
|
|
4
|
+
/** Override revalidation TTL in seconds (default 1200 = 20 min). */
|
|
5
|
+
revalidate?: number;
|
|
6
|
+
/** Page pathname; if omitted, derived from request headers / `/`. */
|
|
7
|
+
pathname?: string;
|
|
8
|
+
/** Optional className applied to the wrapper element. */
|
|
9
|
+
className?: string;
|
|
10
|
+
/** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */
|
|
11
|
+
preview?: boolean | string;
|
|
12
|
+
}
|
|
13
|
+
declare function SmalkAd(props?: SmalkAdProps): Promise<React.ReactElement>;
|
|
14
|
+
|
|
15
|
+
export { SmalkAd, type SmalkAdProps };
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface SmalkAdProps {
|
|
4
|
+
/** Override revalidation TTL in seconds (default 1200 = 20 min). */
|
|
5
|
+
revalidate?: number;
|
|
6
|
+
/** Page pathname; if omitted, derived from request headers / `/`. */
|
|
7
|
+
pathname?: string;
|
|
8
|
+
/** Optional className applied to the wrapper element. */
|
|
9
|
+
className?: string;
|
|
10
|
+
/** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */
|
|
11
|
+
preview?: boolean | string;
|
|
12
|
+
}
|
|
13
|
+
declare function SmalkAd(props?: SmalkAdProps): Promise<React.ReactElement>;
|
|
14
|
+
|
|
15
|
+
export { SmalkAd, type SmalkAdProps };
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchAd
|
|
3
|
+
} from "./chunk-YA6M2IA4.js";
|
|
4
|
+
import "./chunk-HX4ZGDYQ.js";
|
|
5
|
+
|
|
6
|
+
// src/app.tsx
|
|
7
|
+
import "server-only";
|
|
8
|
+
import { headers } from "next/headers";
|
|
9
|
+
import { jsx } from "react/jsx-runtime";
|
|
10
|
+
async function SmalkAd(props = {}) {
|
|
11
|
+
const h = await readHeadersCompat();
|
|
12
|
+
const userAgent = h.get("user-agent") ?? "";
|
|
13
|
+
const referer = h.get("referer") ?? "";
|
|
14
|
+
const clientIp = pickClientIp(h);
|
|
15
|
+
const host = h.get("host") ?? "";
|
|
16
|
+
const pathname = props.pathname ?? "/";
|
|
17
|
+
const pageUrl = host ? `https://${host}${pathname}` : pathname;
|
|
18
|
+
const ad = await fetchAd(
|
|
19
|
+
{ pageUrl, userAgent, referer, clientIp },
|
|
20
|
+
{ revalidate: props.revalidate, preview: props.preview }
|
|
21
|
+
);
|
|
22
|
+
if (!ad) {
|
|
23
|
+
return /* @__PURE__ */ jsx(SmalkAdComment, { reason: "no ad" });
|
|
24
|
+
}
|
|
25
|
+
return /* @__PURE__ */ jsx(
|
|
26
|
+
"div",
|
|
27
|
+
{
|
|
28
|
+
className: props.className ?? "smalk-ads",
|
|
29
|
+
"data-smalk-booking": ad.bookingId ?? void 0,
|
|
30
|
+
dangerouslySetInnerHTML: { __html: ad.html }
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function SmalkAdComment({ reason }) {
|
|
35
|
+
return /* @__PURE__ */ jsx("span", { "data-smalk": "comment", dangerouslySetInnerHTML: { __html: `<!-- smalk: ${reason} -->` } });
|
|
36
|
+
}
|
|
37
|
+
async function readHeadersCompat() {
|
|
38
|
+
const result = headers();
|
|
39
|
+
if (result && typeof result.then === "function") {
|
|
40
|
+
return await result;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
function pickClientIp(h) {
|
|
45
|
+
for (const key of ["cf-connecting-ip", "x-real-ip", "x-forwarded-for"]) {
|
|
46
|
+
const v = h.get(key);
|
|
47
|
+
if (v) return v.split(",")[0].trim();
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
SmalkAd
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
ADDED
|
@@ -0,0 +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 = 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"],"mappings":";;;;;;AAAA,OAAO;AAEP,SAAS,eAAe;AA6Bb;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,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,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":[]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULT_TIMEOUT_MS = 100;
|
|
3
|
+
var DEFAULT_REVALIDATE_S = 1200;
|
|
4
|
+
var API_PATH = "/api/v1/transform/ads/content/";
|
|
5
|
+
function loadConfig() {
|
|
6
|
+
const projectKey = process.env.SMALK_PROJECT_KEY;
|
|
7
|
+
const apiKey = process.env.SMALK_API_KEY;
|
|
8
|
+
const apiBaseUrl = process.env.SMALK_API_BASE_URL || "https://api.smalk.ai";
|
|
9
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
10
|
+
if (!projectKey) {
|
|
11
|
+
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production");
|
|
12
|
+
console.warn("[smalk] SMALK_PROJECT_KEY not set \u2014 ad fetch will be skipped");
|
|
13
|
+
}
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_API_KEY env var is required in production");
|
|
16
|
+
console.warn("[smalk] SMALK_API_KEY not set \u2014 ad fetch will be skipped");
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
projectKey: projectKey ?? "",
|
|
20
|
+
apiKey: apiKey ?? "",
|
|
21
|
+
apiBaseUrl
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_TIMEOUT_MS,
|
|
27
|
+
DEFAULT_REVALIDATE_S,
|
|
28
|
+
API_PATH,
|
|
29
|
+
loadConfig
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=chunk-HX4ZGDYQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["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"],"mappings":";AAwBO,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;","names":[]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
API_PATH,
|
|
3
|
+
DEFAULT_REVALIDATE_S,
|
|
4
|
+
DEFAULT_TIMEOUT_MS,
|
|
5
|
+
loadConfig
|
|
6
|
+
} from "./chunk-HX4ZGDYQ.js";
|
|
7
|
+
|
|
8
|
+
// src/client.ts
|
|
9
|
+
var lastHashByUrl = /* @__PURE__ */ new Map();
|
|
10
|
+
async function fetchAd(input, opts = {}) {
|
|
11
|
+
const cfg = loadConfig();
|
|
12
|
+
if (!cfg.projectKey || !cfg.apiKey) return null;
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
15
|
+
try {
|
|
16
|
+
const body = {
|
|
17
|
+
project_key: cfg.projectKey,
|
|
18
|
+
page_url: input.pageUrl,
|
|
19
|
+
user_agent: input.userAgent,
|
|
20
|
+
referer: input.referer,
|
|
21
|
+
client_ip: input.clientIp
|
|
22
|
+
};
|
|
23
|
+
if (opts.preview !== void 0 && opts.preview !== false) {
|
|
24
|
+
body.preview = opts.preview;
|
|
25
|
+
}
|
|
26
|
+
const res = await fetch(`${cfg.apiBaseUrl}${API_PATH}`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Authorization": `Api-Key ${cfg.apiKey}`,
|
|
30
|
+
"Content-Type": "application/json"
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
// `next` is a Next.js fetch extension; type provided by next types when present.
|
|
35
|
+
next: {
|
|
36
|
+
revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,
|
|
37
|
+
tags: ["smalk-ad", `smalk-ad:${input.pageUrl}`]
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
if (!data.html) return null;
|
|
46
|
+
const hash = await sha256Hex(data.html);
|
|
47
|
+
const prev = lastHashByUrl.get(input.pageUrl);
|
|
48
|
+
if (prev && prev !== hash) {
|
|
49
|
+
await maybeRevalidatePath(input.pageUrl);
|
|
50
|
+
}
|
|
51
|
+
lastHashByUrl.set(input.pageUrl, hash);
|
|
52
|
+
return { html: data.html, bookingId: data.booking_id ?? null };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
55
|
+
logErrorOnce(`smalk-ads fetch error: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function maybeRevalidatePath(pageUrl) {
|
|
63
|
+
try {
|
|
64
|
+
const mod = await import("next/cache");
|
|
65
|
+
if (typeof mod.revalidatePath === "function") {
|
|
66
|
+
const pathname = new URL(pageUrl).pathname;
|
|
67
|
+
mod.revalidatePath(pathname);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var seenErrors = /* @__PURE__ */ new Set();
|
|
73
|
+
function logErrorOnce(msg) {
|
|
74
|
+
if (seenErrors.has(msg)) return;
|
|
75
|
+
seenErrors.add(msg);
|
|
76
|
+
console.error(`[smalk-nextjs-ads] ${msg}`);
|
|
77
|
+
}
|
|
78
|
+
async function sha256Hex(input) {
|
|
79
|
+
const data = new TextEncoder().encode(input);
|
|
80
|
+
const buf = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
81
|
+
const bytes = new Uint8Array(buf);
|
|
82
|
+
let hex = "";
|
|
83
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
84
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
85
|
+
}
|
|
86
|
+
return hex;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
fetchAd
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=chunk-YA6M2IA4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["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":";;;;;;;;AAEA,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;","names":[]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
API_PATH: () => API_PATH,
|
|
24
|
+
DEFAULT_REVALIDATE_S: () => DEFAULT_REVALIDATE_S,
|
|
25
|
+
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
26
|
+
loadConfig: () => loadConfig
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(src_exports);
|
|
29
|
+
var DEFAULT_TIMEOUT_MS = 100;
|
|
30
|
+
var DEFAULT_REVALIDATE_S = 1200;
|
|
31
|
+
var API_PATH = "/api/v1/transform/ads/content/";
|
|
32
|
+
function loadConfig() {
|
|
33
|
+
const projectKey = process.env.SMALK_PROJECT_KEY;
|
|
34
|
+
const apiKey = process.env.SMALK_API_KEY;
|
|
35
|
+
const apiBaseUrl = process.env.SMALK_API_BASE_URL || "https://api.smalk.ai";
|
|
36
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
37
|
+
if (!projectKey) {
|
|
38
|
+
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production");
|
|
39
|
+
console.warn("[smalk] SMALK_PROJECT_KEY not set \u2014 ad fetch will be skipped");
|
|
40
|
+
}
|
|
41
|
+
if (!apiKey) {
|
|
42
|
+
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_API_KEY env var is required in production");
|
|
43
|
+
console.warn("[smalk] SMALK_API_KEY not set \u2014 ad fetch will be skipped");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
projectKey: projectKey ?? "",
|
|
47
|
+
apiKey: apiKey ?? "",
|
|
48
|
+
apiBaseUrl
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
52
|
+
0 && (module.exports = {
|
|
53
|
+
API_PATH,
|
|
54
|
+
DEFAULT_REVALIDATE_S,
|
|
55
|
+
DEFAULT_TIMEOUT_MS,
|
|
56
|
+
loadConfig
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBO,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;","names":[]}
|