@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/dist/index.d.cts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface SmalkAdsConfig {
|
|
2
|
+
projectKey: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
apiBaseUrl: string;
|
|
5
|
+
}
|
|
6
|
+
interface AdInput {
|
|
7
|
+
pageUrl: string;
|
|
8
|
+
userAgent: string;
|
|
9
|
+
referer: string;
|
|
10
|
+
clientIp: string;
|
|
11
|
+
}
|
|
12
|
+
interface AdResult {
|
|
13
|
+
html: string;
|
|
14
|
+
bookingId: string | null;
|
|
15
|
+
}
|
|
16
|
+
interface ApiResponse {
|
|
17
|
+
html?: string;
|
|
18
|
+
booking_id?: string;
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
declare const DEFAULT_TIMEOUT_MS = 100;
|
|
22
|
+
declare const DEFAULT_REVALIDATE_S = 1200;
|
|
23
|
+
declare const API_PATH = "/api/v1/transform/ads/content/";
|
|
24
|
+
declare function loadConfig(): SmalkAdsConfig;
|
|
25
|
+
|
|
26
|
+
export { API_PATH, type AdInput, type AdResult, type ApiResponse, DEFAULT_REVALIDATE_S, DEFAULT_TIMEOUT_MS, type SmalkAdsConfig, loadConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface SmalkAdsConfig {
|
|
2
|
+
projectKey: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
apiBaseUrl: string;
|
|
5
|
+
}
|
|
6
|
+
interface AdInput {
|
|
7
|
+
pageUrl: string;
|
|
8
|
+
userAgent: string;
|
|
9
|
+
referer: string;
|
|
10
|
+
clientIp: string;
|
|
11
|
+
}
|
|
12
|
+
interface AdResult {
|
|
13
|
+
html: string;
|
|
14
|
+
bookingId: string | null;
|
|
15
|
+
}
|
|
16
|
+
interface ApiResponse {
|
|
17
|
+
html?: string;
|
|
18
|
+
booking_id?: string;
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
declare const DEFAULT_TIMEOUT_MS = 100;
|
|
22
|
+
declare const DEFAULT_REVALIDATE_S = 1200;
|
|
23
|
+
declare const API_PATH = "/api/v1/transform/ads/content/";
|
|
24
|
+
declare function loadConfig(): SmalkAdsConfig;
|
|
25
|
+
|
|
26
|
+
export { API_PATH, type AdInput, type AdResult, type ApiResponse, DEFAULT_REVALIDATE_S, DEFAULT_TIMEOUT_MS, type SmalkAdsConfig, loadConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,185 @@
|
|
|
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/middleware.ts
|
|
31
|
+
var middleware_exports = {};
|
|
32
|
+
__export(middleware_exports, {
|
|
33
|
+
withSmalkAds: () => withSmalkAds
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(middleware_exports);
|
|
36
|
+
var import_server = require("next/server");
|
|
37
|
+
|
|
38
|
+
// src/index.ts
|
|
39
|
+
var DEFAULT_TIMEOUT_MS = 100;
|
|
40
|
+
var DEFAULT_REVALIDATE_S = 1200;
|
|
41
|
+
var API_PATH = "/api/v1/transform/ads/content/";
|
|
42
|
+
function loadConfig() {
|
|
43
|
+
const projectKey = process.env.SMALK_PROJECT_KEY;
|
|
44
|
+
const apiKey = process.env.SMALK_API_KEY;
|
|
45
|
+
const apiBaseUrl = process.env.SMALK_API_BASE_URL || "https://api.smalk.ai";
|
|
46
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
47
|
+
if (!projectKey) {
|
|
48
|
+
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production");
|
|
49
|
+
console.warn("[smalk] SMALK_PROJECT_KEY not set \u2014 ad fetch will be skipped");
|
|
50
|
+
}
|
|
51
|
+
if (!apiKey) {
|
|
52
|
+
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_API_KEY env var is required in production");
|
|
53
|
+
console.warn("[smalk] SMALK_API_KEY not set \u2014 ad fetch will be skipped");
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
projectKey: projectKey ?? "",
|
|
57
|
+
apiKey: apiKey ?? "",
|
|
58
|
+
apiBaseUrl
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/client.ts
|
|
63
|
+
var lastHashByUrl = /* @__PURE__ */ new Map();
|
|
64
|
+
async function fetchAd(input, opts = {}) {
|
|
65
|
+
const cfg = loadConfig();
|
|
66
|
+
if (!cfg.projectKey || !cfg.apiKey) return null;
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
69
|
+
try {
|
|
70
|
+
const body = {
|
|
71
|
+
project_key: cfg.projectKey,
|
|
72
|
+
page_url: input.pageUrl,
|
|
73
|
+
user_agent: input.userAgent,
|
|
74
|
+
referer: input.referer,
|
|
75
|
+
client_ip: input.clientIp
|
|
76
|
+
};
|
|
77
|
+
if (opts.preview !== void 0 && opts.preview !== false) {
|
|
78
|
+
body.preview = opts.preview;
|
|
79
|
+
}
|
|
80
|
+
const res = await fetch(`${cfg.apiBaseUrl}${API_PATH}`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"Authorization": `Api-Key ${cfg.apiKey}`,
|
|
84
|
+
"Content-Type": "application/json"
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
// `next` is a Next.js fetch extension; type provided by next types when present.
|
|
89
|
+
next: {
|
|
90
|
+
revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,
|
|
91
|
+
tags: ["smalk-ad", `smalk-ad:${input.pageUrl}`]
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
if (!data.html) return null;
|
|
100
|
+
const hash = await sha256Hex(data.html);
|
|
101
|
+
const prev = lastHashByUrl.get(input.pageUrl);
|
|
102
|
+
if (prev && prev !== hash) {
|
|
103
|
+
await maybeRevalidatePath(input.pageUrl);
|
|
104
|
+
}
|
|
105
|
+
lastHashByUrl.set(input.pageUrl, hash);
|
|
106
|
+
return { html: data.html, bookingId: data.booking_id ?? null };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
109
|
+
logErrorOnce(`smalk-ads fetch error: ${err.message}`);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function maybeRevalidatePath(pageUrl) {
|
|
117
|
+
try {
|
|
118
|
+
const mod = await import("next/cache");
|
|
119
|
+
if (typeof mod.revalidatePath === "function") {
|
|
120
|
+
const pathname = new URL(pageUrl).pathname;
|
|
121
|
+
mod.revalidatePath(pathname);
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
var seenErrors = /* @__PURE__ */ new Set();
|
|
127
|
+
function logErrorOnce(msg) {
|
|
128
|
+
if (seenErrors.has(msg)) return;
|
|
129
|
+
seenErrors.add(msg);
|
|
130
|
+
console.error(`[smalk-nextjs-ads] ${msg}`);
|
|
131
|
+
}
|
|
132
|
+
async function sha256Hex(input) {
|
|
133
|
+
const data = new TextEncoder().encode(input);
|
|
134
|
+
const buf = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
135
|
+
const bytes = new Uint8Array(buf);
|
|
136
|
+
let hex = "";
|
|
137
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
138
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
139
|
+
}
|
|
140
|
+
return hex;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/middleware.ts
|
|
144
|
+
var SMALK_ADS_PATTERN = /<(\w+)[^>]*\bsmalk-ads\b[^>]*>[\s\S]*?<\/\1>/i;
|
|
145
|
+
function withSmalkAds(opts = {}) {
|
|
146
|
+
return async function smalkAdsMiddleware(req) {
|
|
147
|
+
const res = import_server.NextResponse.next();
|
|
148
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
149
|
+
if (!contentType.includes("text/html")) return res;
|
|
150
|
+
const body = await res.text();
|
|
151
|
+
if (!body || body.indexOf("smalk-ads") === -1) return res;
|
|
152
|
+
const match = body.match(SMALK_ADS_PATTERN);
|
|
153
|
+
if (!match) return res;
|
|
154
|
+
const userAgent = req.headers.get("user-agent") ?? "";
|
|
155
|
+
const referer = req.headers.get("referer") ?? "";
|
|
156
|
+
const clientIp = pickClientIp(req.headers);
|
|
157
|
+
const pageUrl = req.url ?? `https://${req.headers.get("host") ?? ""}${req.nextUrl?.pathname ?? "/"}`;
|
|
158
|
+
const ad = await fetchAd({ pageUrl, userAgent, referer, clientIp }, { revalidate: opts.revalidate });
|
|
159
|
+
const headers = collectHeaders(res.headers);
|
|
160
|
+
if (!ad) {
|
|
161
|
+
return new import_server.NextResponse(body, { headers, status: res.status });
|
|
162
|
+
}
|
|
163
|
+
const replaced = body.replace(match[0], ad.html);
|
|
164
|
+
return new import_server.NextResponse(replaced, { headers, status: res.status });
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function collectHeaders(h) {
|
|
168
|
+
const out = {};
|
|
169
|
+
if (typeof h.entries === "function") {
|
|
170
|
+
for (const [k, v] of h.entries()) out[k] = v;
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
function pickClientIp(h) {
|
|
175
|
+
for (const key of ["cf-connecting-ip", "x-real-ip", "x-forwarded-for"]) {
|
|
176
|
+
const v = h.get(key);
|
|
177
|
+
if (v) return v.split(",")[0].trim();
|
|
178
|
+
}
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
182
|
+
0 && (module.exports = {
|
|
183
|
+
withSmalkAds
|
|
184
|
+
});
|
|
185
|
+
//# sourceMappingURL=middleware.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/index.ts","../src/client.ts"],"sourcesContent":["import { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\nimport { fetchAd } from './client';\n\nconst SMALK_ADS_PATTERN = /<(\\w+)[^>]*\\bsmalk-ads\\b[^>]*>[\\s\\S]*?<\\/\\1>/i;\n\nexport interface WithSmalkAdsOptions {\n revalidate?: number;\n}\n\nexport function withSmalkAds(opts: WithSmalkAdsOptions = {}) {\n return async function smalkAdsMiddleware(req: NextRequest): Promise<Response> {\n const res = NextResponse.next();\n const contentType = res.headers.get('content-type') ?? '';\n if (!contentType.includes('text/html')) return res;\n\n const body = await res.text();\n if (!body || body.indexOf('smalk-ads') === -1) return res;\n\n const match = body.match(SMALK_ADS_PATTERN);\n if (!match) return res;\n\n const userAgent = req.headers.get('user-agent') ?? '';\n const referer = req.headers.get('referer') ?? '';\n const clientIp = pickClientIp(req.headers);\n const pageUrl = req.url ?? `https://${req.headers.get('host') ?? ''}${req.nextUrl?.pathname ?? '/'}`;\n\n const ad = await fetchAd({ pageUrl, userAgent, referer, clientIp }, { revalidate: opts.revalidate });\n const headers = collectHeaders(res.headers);\n if (!ad) {\n return new NextResponse(body, { headers, status: res.status }) as unknown as Response;\n }\n const replaced = body.replace(match[0], ad.html);\n return new NextResponse(replaced, { headers, status: res.status }) as unknown as Response;\n };\n}\n\nfunction collectHeaders(h: { entries?: () => Iterable<[string, string]> } & { get?: (k: string) => string | null }): Record<string, string> {\n const out: Record<string, string> = {};\n if (typeof h.entries === 'function') {\n for (const [k, v] of h.entries()) out[k] = v;\n }\n return out;\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":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchAd
|
|
3
|
+
} from "./chunk-YA6M2IA4.js";
|
|
4
|
+
import "./chunk-HX4ZGDYQ.js";
|
|
5
|
+
|
|
6
|
+
// src/middleware.ts
|
|
7
|
+
import { NextResponse } from "next/server";
|
|
8
|
+
var SMALK_ADS_PATTERN = /<(\w+)[^>]*\bsmalk-ads\b[^>]*>[\s\S]*?<\/\1>/i;
|
|
9
|
+
function withSmalkAds(opts = {}) {
|
|
10
|
+
return async function smalkAdsMiddleware(req) {
|
|
11
|
+
const res = NextResponse.next();
|
|
12
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
13
|
+
if (!contentType.includes("text/html")) return res;
|
|
14
|
+
const body = await res.text();
|
|
15
|
+
if (!body || body.indexOf("smalk-ads") === -1) return res;
|
|
16
|
+
const match = body.match(SMALK_ADS_PATTERN);
|
|
17
|
+
if (!match) return res;
|
|
18
|
+
const userAgent = req.headers.get("user-agent") ?? "";
|
|
19
|
+
const referer = req.headers.get("referer") ?? "";
|
|
20
|
+
const clientIp = pickClientIp(req.headers);
|
|
21
|
+
const pageUrl = req.url ?? `https://${req.headers.get("host") ?? ""}${req.nextUrl?.pathname ?? "/"}`;
|
|
22
|
+
const ad = await fetchAd({ pageUrl, userAgent, referer, clientIp }, { revalidate: opts.revalidate });
|
|
23
|
+
const headers = collectHeaders(res.headers);
|
|
24
|
+
if (!ad) {
|
|
25
|
+
return new NextResponse(body, { headers, status: res.status });
|
|
26
|
+
}
|
|
27
|
+
const replaced = body.replace(match[0], ad.html);
|
|
28
|
+
return new NextResponse(replaced, { headers, status: res.status });
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function collectHeaders(h) {
|
|
32
|
+
const out = {};
|
|
33
|
+
if (typeof h.entries === "function") {
|
|
34
|
+
for (const [k, v] of h.entries()) out[k] = v;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function pickClientIp(h) {
|
|
39
|
+
for (const key of ["cf-connecting-ip", "x-real-ip", "x-forwarded-for"]) {
|
|
40
|
+
const v = h.get(key);
|
|
41
|
+
if (v) return v.split(",")[0].trim();
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
withSmalkAds
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["import { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\nimport { fetchAd } from './client';\n\nconst SMALK_ADS_PATTERN = /<(\\w+)[^>]*\\bsmalk-ads\\b[^>]*>[\\s\\S]*?<\\/\\1>/i;\n\nexport interface WithSmalkAdsOptions {\n revalidate?: number;\n}\n\nexport function withSmalkAds(opts: WithSmalkAdsOptions = {}) {\n return async function smalkAdsMiddleware(req: NextRequest): Promise<Response> {\n const res = NextResponse.next();\n const contentType = res.headers.get('content-type') ?? '';\n if (!contentType.includes('text/html')) return res;\n\n const body = await res.text();\n if (!body || body.indexOf('smalk-ads') === -1) return res;\n\n const match = body.match(SMALK_ADS_PATTERN);\n if (!match) return res;\n\n const userAgent = req.headers.get('user-agent') ?? '';\n const referer = req.headers.get('referer') ?? '';\n const clientIp = pickClientIp(req.headers);\n const pageUrl = req.url ?? `https://${req.headers.get('host') ?? ''}${req.nextUrl?.pathname ?? '/'}`;\n\n const ad = await fetchAd({ pageUrl, userAgent, referer, clientIp }, { revalidate: opts.revalidate });\n const headers = collectHeaders(res.headers);\n if (!ad) {\n return new NextResponse(body, { headers, status: res.status }) as unknown as Response;\n }\n const replaced = body.replace(match[0], ad.html);\n return new NextResponse(replaced, { headers, status: res.status }) as unknown as Response;\n };\n}\n\nfunction collectHeaders(h: { entries?: () => Iterable<[string, string]> } & { get?: (k: string) => string | null }): Record<string, string> {\n const out: Record<string, string> = {};\n if (typeof h.entries === 'function') {\n for (const [k, v] of h.entries()) out[k] = v;\n }\n return out;\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,SAAS,oBAAoB;AAI7B,IAAM,oBAAoB;AAMnB,SAAS,aAAa,OAA4B,CAAC,GAAG;AAC3D,SAAO,eAAe,mBAAmB,KAAqC;AAC5E,UAAM,MAAM,aAAa,KAAK;AAC9B,UAAM,cAAc,IAAI,QAAQ,IAAI,cAAc,KAAK;AACvD,QAAI,CAAC,YAAY,SAAS,WAAW,EAAG,QAAO;AAE/C,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAI,CAAC,QAAQ,KAAK,QAAQ,WAAW,MAAM,GAAI,QAAO;AAEtD,UAAM,QAAQ,KAAK,MAAM,iBAAiB;AAC1C,QAAI,CAAC,MAAO,QAAO;AAEnB,UAAM,YAAY,IAAI,QAAQ,IAAI,YAAY,KAAK;AACnD,UAAM,UAAU,IAAI,QAAQ,IAAI,SAAS,KAAK;AAC9C,UAAM,WAAW,aAAa,IAAI,OAAO;AACzC,UAAM,UAAU,IAAI,OAAO,WAAW,IAAI,QAAQ,IAAI,MAAM,KAAK,EAAE,GAAG,IAAI,SAAS,YAAY,GAAG;AAElG,UAAM,KAAK,MAAM,QAAQ,EAAE,SAAS,WAAW,SAAS,SAAS,GAAG,EAAE,YAAY,KAAK,WAAW,CAAC;AACnG,UAAM,UAAU,eAAe,IAAI,OAAO;AAC1C,QAAI,CAAC,IAAI;AACP,aAAO,IAAI,aAAa,MAAM,EAAE,SAAS,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC/D;AACA,UAAM,WAAW,KAAK,QAAQ,MAAM,CAAC,GAAG,GAAG,IAAI;AAC/C,WAAO,IAAI,aAAa,UAAU,EAAE,SAAS,QAAQ,IAAI,OAAO,CAAC;AAAA,EACnE;AACF;AAEA,SAAS,eAAe,GAAoH;AAC1I,QAAM,MAA8B,CAAC;AACrC,MAAI,OAAO,EAAE,YAAY,YAAY;AACnC,eAAW,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAG,KAAI,CAAC,IAAI;AAAA,EAC7C;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
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
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/pages.ts
|
|
31
|
+
var pages_exports = {};
|
|
32
|
+
__export(pages_exports, {
|
|
33
|
+
AdHtml: () => AdHtml,
|
|
34
|
+
getSmalkAd: () => getSmalkAd
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(pages_exports);
|
|
37
|
+
var React = __toESM(require("react"), 1);
|
|
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/pages.ts
|
|
145
|
+
async function getSmalkAd(req, opts = {}) {
|
|
146
|
+
const headers = req.headers ?? {};
|
|
147
|
+
const url = req.url ?? "/";
|
|
148
|
+
const host = headerValue(headers, "host") ?? "";
|
|
149
|
+
const pageUrl = host ? `https://${host}${url}` : url;
|
|
150
|
+
const userAgent = headerValue(headers, "user-agent") ?? "";
|
|
151
|
+
const referer = headerValue(headers, "referer") ?? "";
|
|
152
|
+
const clientIp = pickClientIp(headers);
|
|
153
|
+
let preview = opts.preview;
|
|
154
|
+
if (preview === void 0) {
|
|
155
|
+
const m = url.match(/[?&]preview=([^&]+)/);
|
|
156
|
+
if (m) {
|
|
157
|
+
const v = decodeURIComponent(m[1]);
|
|
158
|
+
preview = v === "1" || v === "true" ? true : v;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return fetchAd(
|
|
162
|
+
{ pageUrl, userAgent, referer, clientIp },
|
|
163
|
+
{ revalidate: opts.revalidate, preview }
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
function AdHtml({ ad, className }) {
|
|
167
|
+
if (!ad) {
|
|
168
|
+
return React.createElement("span", {
|
|
169
|
+
"data-smalk": "comment",
|
|
170
|
+
dangerouslySetInnerHTML: { __html: "<!-- smalk: no ad -->" }
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return React.createElement("div", {
|
|
174
|
+
className: className ?? "smalk-ads",
|
|
175
|
+
"data-smalk-booking": ad.bookingId ?? void 0,
|
|
176
|
+
dangerouslySetInnerHTML: { __html: ad.html }
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function headerValue(h, key) {
|
|
180
|
+
const v = h[key] ?? h[key.toLowerCase()];
|
|
181
|
+
if (Array.isArray(v)) return v[0] ?? null;
|
|
182
|
+
return v ?? null;
|
|
183
|
+
}
|
|
184
|
+
function pickClientIp(h) {
|
|
185
|
+
for (const key of ["cf-connecting-ip", "x-real-ip", "x-forwarded-for"]) {
|
|
186
|
+
const v = headerValue(h, key);
|
|
187
|
+
if (v) return v.split(",")[0].trim();
|
|
188
|
+
}
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
192
|
+
0 && (module.exports = {
|
|
193
|
+
AdHtml,
|
|
194
|
+
getSmalkAd
|
|
195
|
+
});
|
|
196
|
+
//# sourceMappingURL=pages.cjs.map
|
|
@@ -0,0 +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":"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","names":[]}
|
package/dist/pages.d.cts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { IncomingMessage } from 'node:http';
|
|
3
|
+
import { AdResult } from './index.cjs';
|
|
4
|
+
|
|
5
|
+
interface MinimalReq {
|
|
6
|
+
url?: string;
|
|
7
|
+
headers: Record<string, string | string[] | undefined>;
|
|
8
|
+
}
|
|
9
|
+
interface GetSmalkAdOptions {
|
|
10
|
+
revalidate?: number;
|
|
11
|
+
/** Preview mode: `true` (PARAGRAPH placeholder) or a format string. Bypasses booking lookup. */
|
|
12
|
+
preview?: boolean | string;
|
|
13
|
+
}
|
|
14
|
+
declare function getSmalkAd(req: IncomingMessage | MinimalReq, opts?: GetSmalkAdOptions): Promise<AdResult | null>;
|
|
15
|
+
interface AdHtmlProps {
|
|
16
|
+
ad: AdResult | null;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
declare function AdHtml({ ad, className }: AdHtmlProps): React.ReactElement;
|
|
20
|
+
|
|
21
|
+
export { AdHtml, type AdHtmlProps, type GetSmalkAdOptions, type MinimalReq, getSmalkAd };
|
package/dist/pages.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { IncomingMessage } from 'node:http';
|
|
3
|
+
import { AdResult } from './index.js';
|
|
4
|
+
|
|
5
|
+
interface MinimalReq {
|
|
6
|
+
url?: string;
|
|
7
|
+
headers: Record<string, string | string[] | undefined>;
|
|
8
|
+
}
|
|
9
|
+
interface GetSmalkAdOptions {
|
|
10
|
+
revalidate?: number;
|
|
11
|
+
/** Preview mode: `true` (PARAGRAPH placeholder) or a format string. Bypasses booking lookup. */
|
|
12
|
+
preview?: boolean | string;
|
|
13
|
+
}
|
|
14
|
+
declare function getSmalkAd(req: IncomingMessage | MinimalReq, opts?: GetSmalkAdOptions): Promise<AdResult | null>;
|
|
15
|
+
interface AdHtmlProps {
|
|
16
|
+
ad: AdResult | null;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
declare function AdHtml({ ad, className }: AdHtmlProps): React.ReactElement;
|
|
20
|
+
|
|
21
|
+
export { AdHtml, type AdHtmlProps, type GetSmalkAdOptions, type MinimalReq, getSmalkAd };
|