apptvty 0.3.2 → 0.4.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/dist/{chunk-6LXUHEWB.mjs → chunk-INZJVNUI.mjs} +42 -19
- package/dist/chunk-INZJVNUI.mjs.map +1 -0
- package/dist/{chunk-LI2S7OO4.mjs → chunk-PSEAM7OI.mjs} +58 -36
- package/dist/chunk-PSEAM7OI.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +100 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/middleware/express.d.mts +1 -1
- package/dist/middleware/express.d.ts +1 -1
- package/dist/middleware/express.js +41 -18
- package/dist/middleware/express.js.map +1 -1
- package/dist/middleware/express.mjs +1 -1
- package/dist/middleware/nextjs.d.mts +1 -1
- package/dist/middleware/nextjs.d.ts +1 -1
- package/dist/middleware/nextjs.js +57 -35
- package/dist/middleware/nextjs.js.map +1 -1
- package/dist/middleware/nextjs.mjs +1 -1
- package/dist/setup.d.mts +1 -1
- package/dist/setup.d.ts +1 -1
- package/dist/{types-07AUBpOl.d.mts → types-Bz3fBGpw.d.mts} +5 -0
- package/dist/{types-07AUBpOl.d.ts → types-Bz3fBGpw.d.ts} +5 -0
- package/package.json +1 -1
- package/dist/chunk-6LXUHEWB.mjs.map +0 -1
- package/dist/chunk-LI2S7OO4.mjs.map +0 -1
|
@@ -84,13 +84,18 @@ function createExpressMiddleware(config) {
|
|
|
84
84
|
const crawlerInfo = detectCrawler(userAgent);
|
|
85
85
|
const scraperService = detectScraperService(userAgent);
|
|
86
86
|
const path = req.url ?? "/";
|
|
87
|
-
const isCrawler = crawlerInfo.isAi || scraperService.isScraperService;
|
|
88
87
|
const ipAddress = getClientIp(req.headers);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
const urlObj = new URL(path, `http://${req.headers.host ?? "localhost"}`);
|
|
89
|
+
const aiCrawlerParam = parseBoolParam(urlObj.searchParams.get("ai_crawler"), false);
|
|
90
|
+
const attributionId = urlObj.searchParams.get("atid");
|
|
91
|
+
const isAi = crawlerInfo.isAi || aiCrawlerParam;
|
|
92
|
+
const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === "unknown_bot";
|
|
93
|
+
const adsPromise = !isInternalRequest(req) && !shouldSkip(path) ? client.getAdsForPage({ site_id: config.siteId, page_path: path }).catch(() => ({ ads: [] })) : Promise.resolve({ ads: [] });
|
|
94
|
+
const chunks = [];
|
|
95
|
+
const originalWrite = res.write.bind(res);
|
|
96
|
+
const originalEnd = res.end.bind(res);
|
|
97
|
+
const shouldBuffer = !isInternalRequest(req) && !shouldSkip(path);
|
|
98
|
+
if (shouldBuffer) {
|
|
94
99
|
res.write = function(chunk, encodingOrCallback, callback) {
|
|
95
100
|
if (chunk != null) {
|
|
96
101
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
@@ -105,17 +110,12 @@ function createExpressMiddleware(config) {
|
|
|
105
110
|
}
|
|
106
111
|
const contentType = res.getHeader("content-type") ?? "";
|
|
107
112
|
const isHtml = contentType.includes("text/html");
|
|
108
|
-
if (!isHtml || chunks.length === 0) {
|
|
113
|
+
if (!isHtml || chunks.length === 0 || res.statusCode !== 200) {
|
|
109
114
|
res.write = originalWrite;
|
|
110
115
|
res.end = originalEnd;
|
|
111
116
|
return originalEnd(Buffer.concat(chunks), encodingOrCallback, callback);
|
|
112
117
|
}
|
|
113
118
|
const html = Buffer.concat(chunks).toString("utf-8");
|
|
114
|
-
if (html.includes(AD_INJECTION_MARKER)) {
|
|
115
|
-
res.write = originalWrite;
|
|
116
|
-
res.end = originalEnd;
|
|
117
|
-
return originalEnd(html, encodingOrCallback, callback);
|
|
118
|
-
}
|
|
119
119
|
adsPromise.then((pageAds) => {
|
|
120
120
|
res.write = originalWrite;
|
|
121
121
|
res.end = originalEnd;
|
|
@@ -123,14 +123,33 @@ function createExpressMiddleware(config) {
|
|
|
123
123
|
originalEnd(html, encodingOrCallback, callback);
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
|
-
const
|
|
126
|
+
const ad = pageAds.ads[0];
|
|
127
|
+
let modified = html;
|
|
128
|
+
if (isAi || scraperService.isScraperService) {
|
|
129
|
+
modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);
|
|
130
|
+
} else {
|
|
131
|
+
const jsonLd = `
|
|
132
|
+
<script type="application/ld+json">{"@context":"https://schema.org","@type":"CreativeWork","author":{"@type":"Organization","name":"${ad.advertiser}"},"mainEntityOfPage":{"@type":"WebPage","@id":"${ad.url}"},"headline":"Sponsored: ${ad.text}"}</script>
|
|
133
|
+
`;
|
|
134
|
+
const stealthDiv = `
|
|
135
|
+
<div style="display:none !important;visibility:hidden;height:0;width:0;overflow:hidden;" aria-hidden="true" data-apptvty-ad="${ad.impression_id}">Sponsored by ${ad.advertiser}: <a href="${ad.url}">${ad.text}</a></div>
|
|
136
|
+
`;
|
|
137
|
+
if (html.includes("</head>")) {
|
|
138
|
+
modified = html.replace("</head>", `${jsonLd}</head>`);
|
|
139
|
+
}
|
|
140
|
+
if (modified.includes("</body>")) {
|
|
141
|
+
modified = modified.replace("</body>", `${stealthDiv}</body>`);
|
|
142
|
+
} else {
|
|
143
|
+
modified += stealthDiv;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
127
146
|
res.setHeader("X-Sponsored-Content", buildSponsoredHeader(pageAds.ads));
|
|
128
147
|
const buf = Buffer.from(modified, "utf-8");
|
|
129
148
|
res.setHeader("Content-Length", buf.length);
|
|
130
149
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
131
|
-
for (const
|
|
150
|
+
for (const adItem of pageAds.ads) {
|
|
132
151
|
client.logImpression({
|
|
133
|
-
impression_id:
|
|
152
|
+
impression_id: adItem.impression_id,
|
|
134
153
|
site_id: config.siteId,
|
|
135
154
|
page_path: path,
|
|
136
155
|
agent_ua: userAgent,
|
|
@@ -149,7 +168,7 @@ function createExpressMiddleware(config) {
|
|
|
149
168
|
};
|
|
150
169
|
}
|
|
151
170
|
res.on("finish", () => {
|
|
152
|
-
if (shouldSkip(path)) return;
|
|
171
|
+
if (shouldSkip(path) || isInternalRequest(req)) return;
|
|
153
172
|
const entry = {
|
|
154
173
|
site_id: config.siteId,
|
|
155
174
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -160,17 +179,21 @@ function createExpressMiddleware(config) {
|
|
|
160
179
|
ip_address: ipAddress,
|
|
161
180
|
user_agent: userAgent,
|
|
162
181
|
referrer: req.headers["referer"] ?? null,
|
|
163
|
-
is_ai_crawler:
|
|
182
|
+
is_ai_crawler: isAi,
|
|
164
183
|
crawler_type: crawlerInfo.name,
|
|
165
184
|
crawler_organization: crawlerInfo.organization,
|
|
166
185
|
confidence_score: crawlerInfo.confidence,
|
|
167
|
-
scraper_service: scraperService.name
|
|
186
|
+
scraper_service: scraperService.name,
|
|
187
|
+
attribution_id: attributionId
|
|
168
188
|
};
|
|
169
189
|
logger.enqueue(entry);
|
|
170
190
|
});
|
|
171
191
|
next();
|
|
172
192
|
};
|
|
173
193
|
}
|
|
194
|
+
function isInternalRequest(req) {
|
|
195
|
+
return req.headers["x-apptvty-internal"] === "true";
|
|
196
|
+
}
|
|
174
197
|
function createExpressQueryHandler(config) {
|
|
175
198
|
const { client } = getInstance(config);
|
|
176
199
|
const handleQuery = createQueryHandler(client, config);
|
|
@@ -229,4 +252,4 @@ export {
|
|
|
229
252
|
createExpressQueryHandler,
|
|
230
253
|
createExpressDashboardHandler
|
|
231
254
|
};
|
|
232
|
-
//# sourceMappingURL=chunk-
|
|
255
|
+
//# sourceMappingURL=chunk-INZJVNUI.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ad-injection.ts","../src/middleware/express.ts"],"sourcesContent":["/**\n * Shared HTML ad injection logic for Next.js and Express middlewares.\n *\n * Three-layer strategy for maximum coverage across all scraping methodologies:\n *\n * Layer 1 — Content-stream injection (<p>[Sponsored]</p> inside <article>/<main>)\n * Survives: Jina r.jina.ai, FireCrawl, Cloudflare Browser Rendering, BeautifulSoup,\n * headless browsers (Playwright/Puppeteer), curl, requests, fetch.\n * Why: Readability algorithms (used by all content-extraction services) preserve\n * <p> tags inside <article>/<main>. They strip <section>, <aside>, <div class=\"ad\">.\n *\n * Layer 2 — JSON-LD in <head> (for direct HTML scrapers only)\n * Survives: BeautifulSoup, lxml, direct fetch of raw HTML.\n * Stripped by: Jina/FireCrawl/Cloudflare Markdown pipelines (they drop <head> content).\n * Why: Skipped for known scraper services — they won't see it.\n *\n * Layer 3 — HTTP response header (X-Sponsored-Content)\n * Survives: Any HTTP client that reads headers (requests, httpx, curl, fetch).\n * Note: BeautifulSoup itself doesn't read headers, but agents using requests+BS4 do.\n */\n\nimport type { PageAd } from './types.js';\n\nexport const AD_INJECTION_MARKER = '<!-- apptvty-sponsored -->';\n\n/**\n * Inject sponsored content into an HTML string using all applicable layers.\n *\n * @param html The original HTML response body.\n * @param ads Matched ads from the Apptvty network.\n * @param isScraperService True when the request came from Jina/FireCrawl/Cloudflare/etc.\n * Skips JSON-LD injection (they strip <head>) and skips the\n * fallback <body> widget (they strip non-content sections).\n * @returns Modified HTML, or the original if nothing was injected.\n */\nexport function injectIntoHtml(\n html: string,\n ads: PageAd[],\n isScraperService: boolean,\n): string {\n if (!html || ads.length === 0) return html;\n if (html.includes(AD_INJECTION_MARKER)) return html; // already injected\n\n let modified = html;\n\n // ── Layer 1: Content-stream injection ────────────────────────────────────────\n // Plain <p> tags inside the main content element.\n // Must go INSIDE </article> or </main>, not after </body> — readability filters\n // strip content outside the primary content container.\n const contentBlock = buildContentStreamBlock(ads);\n\n if (modified.includes('</article>')) {\n modified = modified.replace('</article>', `${contentBlock}\\n</article>`);\n } else if (modified.includes('</main>')) {\n modified = modified.replace('</main>', `${contentBlock}\\n</main>`);\n } else if (!isScraperService && modified.includes('</body>')) {\n // Fallback: append before </body> only for direct scrapers.\n // Scraper services apply readability and will strip this position,\n // so we skip it — better to omit than inject something invisible.\n modified = modified.replace('</body>', `${contentBlock}\\n</body>`);\n }\n\n // ── Layer 2: JSON-LD in <head> ────────────────────────────────────────────────\n // Skip for known scraper services — they convert to Markdown and drop <head>.\n if (!isScraperService && modified.includes('</head>')) {\n const jsonLdBlock = buildJsonLdBlock(ads);\n modified = modified.replace('</head>', `${jsonLdBlock}\\n</head>`);\n }\n\n return modified;\n}\n\n/**\n * Build the X-Sponsored-Content header value.\n * Format: JSON array — readable by any HTTP client that inspects response headers.\n */\nexport function buildSponsoredHeader(ads: PageAd[]): string {\n return JSON.stringify(\n ads.map((ad) => ({ text: ad.text, url: ad.url, advertiser: ad.advertiser })),\n );\n}\n\n// ─── Private builders ─────────────────────────────────────────────────────────\n\n/**\n * Plain <p> tags — the only structure that survives readability-based content\n * extraction (Jina, FireCrawl, Cloudflare Browser Rendering, etc.).\n *\n * Uses rel=\"sponsored\" per Google's link attribute spec.\n * Uses data-apptvty-sponsored for impression tracking by downstream agents.\n */\nfunction buildContentStreamBlock(ads: PageAd[]): string {\n const paragraphs = ads\n .map(\n (ad) =>\n `<p data-apptvty-sponsored=\"${escapeAttr(ad.impression_id)}\">` +\n `<strong>[Sponsored]</strong> ` +\n `<a href=\"${escapeAttr(ad.url)}\" rel=\"sponsored noopener\">${escapeHtml(ad.text)}</a>` +\n ` \\u2014 <span>${escapeHtml(ad.advertiser)}</span>` +\n `</p>`,\n )\n .join('\\n');\n return `${AD_INJECTION_MARKER}\\n${paragraphs}`;\n}\n\n/**\n * Schema.org JSON-LD block for direct HTML scrapers (BeautifulSoup, lxml).\n * soup.find('script', {'type': 'application/ld+json'}) returns this.\n */\nfunction buildJsonLdBlock(ads: PageAd[]): string {\n const entries = ads.map((ad) => ({\n '@context': 'https://schema.org',\n '@type': 'WPAdBlock',\n sponsor: {\n '@type': 'Organization',\n name: ad.advertiser,\n url: ad.url,\n },\n description: ad.text,\n }));\n const ld = entries.length === 1 ? entries[0] : entries;\n return `<script type=\"application/ld+json\">${JSON.stringify(ld)}</script>`;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/\"/g, '"').replace(/'/g, ''');\n}\n","/**\n * Express / Node.js integration for the Apptvty SDK.\n *\n * Usage:\n *\n * import express from 'express';\n * import { createExpressMiddleware, createExpressQueryHandler } from 'apptvty/express';\n *\n * const app = express();\n * const config = { apiKey: 'ak_...', siteId: 'site_...' };\n *\n * // 1. Log all traffic + inject ads into AI crawler responses\n * app.use(createExpressMiddleware(config));\n *\n * // 2. Mount the AEO query page\n * app.get('/query', createExpressQueryHandler(config));\n *\n * Works with any Connect-compatible framework (Express, Fastify via @fastify/express, etc.)\n *\n * Ad injection strategy:\n * When an AI crawler or scraper service is detected, the middleware buffers the\n * response body and injects sponsored content using the same three-layer strategy\n * as the Next.js integration. An ad fetch runs in parallel with the app's own\n * request processing, so the latency overhead is the difference between the ad\n * fetch time and the app's own response time — typically near zero.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { ApptvtyClient } from '../client.js';\nimport { detectCrawler, detectScraperService } from '../crawler.js';\nimport { RequestLogger, getClientIp } from '../logger.js';\nimport { createQueryHandler } from '../query-handler.js';\nimport { createDashboardHandler } from '../dashboard-handler.js';\nimport { injectIntoHtml, buildSponsoredHeader, AD_INJECTION_MARKER } from '../ad-injection.js';\nimport type { ApptvtyConfig, PageAdsResponse, RequestLogEntry } from '../types.js';\n\nexport type ConnectMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: unknown) => void,\n) => void;\n\nexport type ConnectHandler = (\n req: IncomingMessage,\n res: ServerResponse,\n) => void | Promise<void>;\n\n// ─── Shared singleton instances per config ────────────────────────────────────\n\nconst instances = new Map<string, { client: ApptvtyClient; logger: RequestLogger }>();\n\nfunction getInstance(config: ApptvtyConfig) {\n const key = config.apiKey;\n if (!instances.has(key)) {\n const client = new ApptvtyClient(config);\n const logger = new RequestLogger(client, config);\n instances.set(key, { client, logger });\n }\n return instances.get(key)!;\n}\n\n// ─── Traffic logging + ad injection middleware ────────────────────────────────\n\n/**\n * Express middleware that:\n * 1. Logs every request to Apptvty (all traffic, batched).\n * 2. For AI crawler and scraper service requests on HTML pages: buffers the\n * response, fetches matching ads in parallel, and injects sponsored content\n * using the three-layer strategy (content-stream, JSON-LD, response header).\n *\n * Mount this before your routes so all traffic is captured.\n *\n * @example\n * app.use(createExpressMiddleware({ apiKey: 'ak_...', siteId: 'site_...' }));\n */\nexport function createExpressMiddleware(config: ApptvtyConfig): ConnectMiddleware {\n const { client, logger } = getInstance(config);\n\n return function apptvtyMiddleware(req, res, next) {\n const startMs = Date.now();\n const userAgent = req.headers['user-agent'] ?? '';\n const crawlerInfo = detectCrawler(userAgent);\n const scraperService = detectScraperService(userAgent);\n const path = req.url ?? '/';\n const ipAddress = getClientIp(req.headers as Record<string, string | string[] | undefined>);\n\n const urlObj = new URL(path, `http://${req.headers.host ?? 'localhost'}`);\n const aiCrawlerParam = parseBoolParam(urlObj.searchParams.get('ai_crawler'), false);\n const attributionId = urlObj.searchParams.get('atid');\n\n // broad detection: is it a confirmed AI, a known scraper service, or a suspicious generic bot?\n const isAi = crawlerInfo.isAi || aiCrawlerParam;\n const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === 'unknown_bot';\n\n // Start ad fetch in parallel with the app's request processing.\n // We fetch for ALL potentially scrapable traffic now to support stealth injection.\n const adsPromise: Promise<PageAdsResponse> =\n !isInternalRequest(req) && !shouldSkip(path)\n ? client\n .getAdsForPage({ site_id: config.siteId, page_path: path })\n .catch(() => ({ ads: [] }))\n : Promise.resolve({ ads: [] });\n\n // Buffer the response body so we can inject ads invisibly.\n // We only buffer for HTML responses from suspected scrapers or when ads are found.\n const chunks: Buffer[] = [];\n const originalWrite = res.write.bind(res) as typeof res.write;\n const originalEnd = res.end.bind(res) as typeof res.end;\n\n // We buffer by default if it looks like a scraper, as we need the body to inject.\n const shouldBuffer = !isInternalRequest(req) && !shouldSkip(path);\n\n if (shouldBuffer) {\n (res as any).write = function (\n chunk: string | Buffer | Uint8Array,\n encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),\n callback?: (err?: Error | null) => void,\n ): boolean {\n if (chunk != null) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string));\n }\n if (typeof encodingOrCallback === 'function') encodingOrCallback();\n else if (typeof callback === 'function') callback();\n return true;\n };\n\n (res as any).end = function (\n chunk?: string | Buffer | Uint8Array,\n encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),\n callback?: (err?: Error | null) => void,\n ): ServerResponse {\n if (chunk != null) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string));\n }\n\n const contentType = (res.getHeader('content-type') as string) ?? '';\n const isHtml = contentType.includes('text/html');\n\n if (!isHtml || chunks.length === 0 || res.statusCode !== 200) {\n res.write = originalWrite;\n res.end = originalEnd;\n return originalEnd(Buffer.concat(chunks), encodingOrCallback as BufferEncoding, callback);\n }\n\n const html = Buffer.concat(chunks).toString('utf-8');\n\n // Await the parallel ad fetch and inject.\n adsPromise\n .then((pageAds) => {\n res.write = originalWrite;\n res.end = originalEnd;\n\n if (!pageAds.ads || pageAds.ads.length === 0) {\n originalEnd(html, encodingOrCallback as BufferEncoding, callback);\n return;\n }\n\n const ad = pageAds.ads[0];\n\n // Tier 1: Confirmed AI -> Return Markdown (or clean HTML injection)\n // We use the same helper as before for Markdown conversion if they explicitly requested it\n // or if it's a known AI bot.\n let modified = html;\n if (isAi || scraperService.isScraperService) {\n modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);\n } else {\n // Tier 2: Grey Area / Human -> Stealth Injection\n const jsonLd = `\\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"CreativeWork\",\"author\":{\"@type\":\"Organization\",\"name\":\"${ad.advertiser}\"},\"mainEntityOfPage\":{\"@type\":\"WebPage\",\"@id\":\"${ad.url}\"},\"headline\":\"Sponsored: ${ad.text}\"}</script>\\n`;\n const stealthDiv = `\\n<div style=\"display:none !important;visibility:hidden;height:0;width:0;overflow:hidden;\" aria-hidden=\"true\" data-apptvty-ad=\"${ad.impression_id}\">Sponsored by ${ad.advertiser}: <a href=\"${ad.url}\">${ad.text}</a></div>\\n`;\n \n if (html.includes('</head>')) {\n modified = html.replace('</head>', `${jsonLd}</head>`);\n }\n if (modified.includes('</body>')) {\n modified = modified.replace('</body>', `${stealthDiv}</body>`);\n } else {\n modified += stealthDiv;\n }\n }\n\n // Layer 3: response header (always included)\n res.setHeader('X-Sponsored-Content', buildSponsoredHeader(pageAds.ads));\n\n const buf = Buffer.from(modified, 'utf-8');\n res.setHeader('Content-Length', buf.length);\n\n // Log impressions fire-and-forget\n const timestamp = new Date().toISOString();\n for (const adItem of pageAds.ads) {\n client\n .logImpression({\n impression_id: adItem.impression_id,\n site_id: config.siteId,\n page_path: path,\n agent_ua: userAgent,\n agent_ip: ipAddress,\n timestamp,\n })\n .catch(() => {});\n }\n\n originalEnd(buf, encodingOrCallback as BufferEncoding, callback);\n })\n .catch(() => {\n res.write = originalWrite;\n res.end = originalEnd;\n originalEnd(html, encodingOrCallback as BufferEncoding, callback);\n });\n\n return res;\n };\n }\n\n // Log after response completes\n res.on('finish', () => {\n if (shouldSkip(path) || isInternalRequest(req)) return;\n\n const entry: RequestLogEntry = {\n site_id: config.siteId,\n timestamp: new Date().toISOString(),\n request_method: req.method ?? 'GET',\n request_path: path,\n response_status: res.statusCode,\n response_time_ms: Date.now() - startMs,\n ip_address: ipAddress,\n user_agent: userAgent,\n referrer: (req.headers['referer'] as string) ?? null,\n is_ai_crawler: isAi,\n crawler_type: crawlerInfo.name,\n crawler_organization: crawlerInfo.organization,\n confidence_score: crawlerInfo.confidence,\n scraper_service: scraperService.name,\n attribution_id: attributionId,\n };\n\n logger.enqueue(entry);\n });\n\n next();\n };\n}\n\nfunction isInternalRequest(req: IncomingMessage): boolean {\n return req.headers['x-apptvty-internal'] === 'true';\n}\n\n// ─── Query endpoint handler ───────────────────────────────────────────────────\n\n/**\n * Express route handler for the AEO query endpoint.\n *\n * Mount this at the path configured in your dashboard (default: /query).\n *\n * @example\n * app.get('/query', createExpressQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' }));\n */\nexport function createExpressQueryHandler(config: ApptvtyConfig): ConnectHandler {\n const { client } = getInstance(config);\n const handleQuery = createQueryHandler(client, config);\n\n return async function queryHandler(req, res) {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n const q = url.searchParams.get('q');\n const lang = url.searchParams.get('lang');\n const surfaceAds = parseBoolParam(url.searchParams.get('surface_ads'), true);\n const aiCrawler = parseBoolParam(url.searchParams.get('ai_crawler'), false);\n const userAgent = req.headers['user-agent'] ?? '';\n\n const result = await handleQuery({\n query: q,\n lang,\n surface_ads: surfaceAds,\n ai_crawler: aiCrawler,\n userAgent,\n ipAddress: getClientIp(req.headers as Record<string, string | string[] | undefined>),\n requestUrl: url.toString(),\n });\n\n for (const [key, value] of Object.entries(result.headers)) {\n res.setHeader(key, value);\n }\n res.statusCode = result.status;\n res.end(JSON.stringify(result.body));\n };\n}\n\n/**\n * Express route handler for the embedded analytics dashboard.\n *\n * Mount this at the path where you want to view your logs (default: /apptvty/logs).\n *\n * @example\n * app.get('/apptvty/logs', createExpressDashboardHandler({ apiKey: 'ak_...', siteId: 'site_...' }));\n */\nexport function createExpressDashboardHandler(config: ApptvtyConfig): ConnectHandler {\n const { client } = getInstance(config);\n const handleDashboard = createDashboardHandler(client, config);\n\n return async function dashboardHandler(req, res) {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n const result = await handleDashboard({\n path: url.pathname + url.search,\n method: req.method ?? 'GET',\n apiKey: config.apiKey,\n siteId: config.siteId,\n authHeader: (req.headers['authorization'] as string) ?? null,\n });\n\n for (const [key, value] of Object.entries(result.headers)) {\n res.setHeader(key, value);\n }\n res.statusCode = result.status;\n res.end(result.body);\n };\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction parseBoolParam(value: string | null, defaultValue: boolean): boolean {\n if (value === null) return defaultValue;\n return value === '1' || value === 'true' || value === 'yes';\n}\n\nfunction shouldSkip(path: string): boolean {\n return (\n path.startsWith('/_next/') ||\n /\\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\\.map)$/.test(path)\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -89,7 +89,8 @@ function withApptvty(config, next) {
|
|
|
89
89
|
const crawlerInfo = detectCrawler(userAgent);
|
|
90
90
|
const scraperService = detectScraperService(userAgent);
|
|
91
91
|
const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get("ai_crawler"), false);
|
|
92
|
-
const
|
|
92
|
+
const isAi = crawlerInfo.isAi || aiCrawlerParam;
|
|
93
|
+
const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === "unknown_bot";
|
|
93
94
|
if (request.nextUrl.pathname === "/api/apptvty/verify") {
|
|
94
95
|
const challenge = request.nextUrl.searchParams.get("challenge");
|
|
95
96
|
if (challenge) {
|
|
@@ -134,11 +135,12 @@ function withApptvty(config, next) {
|
|
|
134
135
|
ip_address: getClientIp(headers),
|
|
135
136
|
user_agent: userAgent,
|
|
136
137
|
referrer: request.headers.get("referer"),
|
|
137
|
-
is_ai_crawler:
|
|
138
|
+
is_ai_crawler: isAi,
|
|
138
139
|
crawler_type: crawlerInfo.name,
|
|
139
140
|
crawler_organization: crawlerInfo.organization,
|
|
140
141
|
confidence_score: crawlerInfo.confidence,
|
|
141
|
-
scraper_service: scraperService.name
|
|
142
|
+
scraper_service: scraperService.name,
|
|
143
|
+
attribution_id: request.nextUrl.searchParams.get("atid")
|
|
142
144
|
};
|
|
143
145
|
const isInternalRequest = request.headers.get("x-apptvty-internal") === "true";
|
|
144
146
|
if (!isInternalRequest && !pathname.startsWith(queryPath)) {
|
|
@@ -147,46 +149,66 @@ function withApptvty(config, next) {
|
|
|
147
149
|
event.waitUntil(logger.flush());
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
|
-
if (
|
|
152
|
+
if (!isInternalRequest && !pathname.startsWith(queryPath) && response.status === 200) {
|
|
151
153
|
try {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
154
|
+
const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });
|
|
155
|
+
if (pageAds.ads && pageAds.ads.length > 0) {
|
|
156
|
+
const ad = pageAds.ads[0];
|
|
157
|
+
client.logImpression({
|
|
158
|
+
impression_id: ad.impression_id,
|
|
159
|
+
site_id: config.siteId,
|
|
160
|
+
page_path: pathname,
|
|
161
|
+
agent_ua: userAgent,
|
|
162
|
+
agent_ip: getClientIp(headers),
|
|
163
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
164
|
+
}).catch(() => {
|
|
165
|
+
});
|
|
166
|
+
if (isScraper) {
|
|
167
|
+
const proxyReq = new Request(request.url, { headers: new Headers(request.headers) });
|
|
168
|
+
proxyReq.headers.set("x-apptvty-internal", "true");
|
|
169
|
+
const res = await fetch(proxyReq);
|
|
170
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
171
|
+
if (contentType.includes("text/html")) {
|
|
172
|
+
const html = await res.text();
|
|
173
|
+
if (isAi || scraperService.isScraperService) {
|
|
174
|
+
let markdown = convertHtmlToMarkdown(html);
|
|
175
|
+
markdown += `
|
|
165
176
|
|
|
166
177
|
---
|
|
167
178
|
> **Sponsored:** [${ad.text}](${ad.url}) - ${ad.advertiser}
|
|
168
179
|
`;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
return new NextResponse(markdown, {
|
|
181
|
+
status: res.status,
|
|
182
|
+
headers: {
|
|
183
|
+
"Content-Type": "text/markdown",
|
|
184
|
+
"X-Apptvty-AEO": "true",
|
|
185
|
+
"X-Sponsored-Content": `${ad.text}; url=${ad.url}`
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const jsonLd = `
|
|
190
|
+
<script type="application/ld+json">{"@context":"https://schema.org","@type":"CreativeWork","author":{"@type":"Organization","name":"${ad.advertiser}"},"mainEntityOfPage":{"@type":"WebPage","@id":"${ad.url}"},"headline":"Sponsored: ${ad.text}"}</script>
|
|
191
|
+
`;
|
|
192
|
+
const stealthDiv = `
|
|
193
|
+
<div style="display:none !important;visibility:hidden;height:0;width:0;overflow:hidden;" aria-hidden="true" data-apptvty-ad="${ad.impression_id}">Sponsored by ${ad.advertiser}: <a href="${ad.url}">${ad.text}</a></div>
|
|
194
|
+
`;
|
|
195
|
+
let modifiedHtml = html;
|
|
196
|
+
if (html.includes("</head>")) modifiedHtml = modifiedHtml.replace("</head>", `${jsonLd}</head>`);
|
|
197
|
+
if (modifiedHtml.includes("</body>")) modifiedHtml = modifiedHtml.replace("</body>", `${stealthDiv}</body>`);
|
|
198
|
+
else modifiedHtml += stealthDiv;
|
|
199
|
+
return new NextResponse(modifiedHtml, {
|
|
200
|
+
status: res.status,
|
|
201
|
+
headers: {
|
|
202
|
+
...headersToRecord(res.headers),
|
|
203
|
+
"X-Sponsored-Content": `${ad.text}; url=${ad.url}`
|
|
204
|
+
}
|
|
205
|
+
});
|
|
184
206
|
}
|
|
185
|
-
}
|
|
207
|
+
}
|
|
208
|
+
response.headers.set("X-Sponsored-Content", `${ad.text}; url=${ad.url}`);
|
|
186
209
|
}
|
|
187
|
-
return res;
|
|
188
210
|
} catch (err) {
|
|
189
|
-
if (config.debug) console.warn("[apptvty]
|
|
211
|
+
if (config.debug) console.warn("[apptvty] Stealth injection failed:", err);
|
|
190
212
|
}
|
|
191
213
|
}
|
|
192
214
|
return response;
|
|
@@ -254,4 +276,4 @@ export {
|
|
|
254
276
|
createNextjsQueryHandler,
|
|
255
277
|
createNextjsDashboardHandler
|
|
256
278
|
};
|
|
257
|
-
//# sourceMappingURL=chunk-
|
|
279
|
+
//# sourceMappingURL=chunk-PSEAM7OI.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware/nextjs.ts","../src/markdown.ts"],"sourcesContent":["/**\n * Next.js App Router integration for the Apptvty SDK.\n *\n * Usage — two pieces:\n *\n * 1. Wrap your existing middleware.ts (or use ours standalone):\n *\n * import { withApptvty } from 'apptvty/nextjs';\n * export default withApptvty({ apiKey: 'ak_...', siteId: 'site_...' });\n * // or wrap your existing middleware:\n * export default withApptvty(config, yourMiddleware);\n *\n * 2. Create app/query/route.ts for the AEO query endpoint:\n *\n * import { createNextjsQueryHandler } from 'apptvty/nextjs';\n * export const GET = createNextjsQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' });\n *\n * Ad injection strategy (three layers, applied for all AI/scraper traffic):\n *\n * Layer 1 — <p>[Sponsored]</p> inside <article>/<main>\n * Survives Jina, FireCrawl, Cloudflare Browser Rendering, BeautifulSoup,\n * headless browsers, curl, requests. Applied for all crawler types.\n *\n * Layer 2 — JSON-LD <script> in <head>\n * For direct HTML scrapers (BeautifulSoup, lxml). Skipped for known\n * scraper services (Jina/FireCrawl/Cloudflare) — they drop <head> on\n * Markdown conversion.\n *\n * Layer 3 — X-Sponsored-Content response header\n * For any HTTP client that reads response headers (requests, httpx, curl).\n * Applied for all crawler types.\n */\n\nimport type { NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport { ApptvtyClient } from '../client.js';\nimport { detectCrawler, detectScraperService } from '../crawler.js';\nimport { RequestLogger, getClientIp } from '../logger.js';\nimport { createQueryHandler } from '../query-handler.js';\nimport { createDashboardHandler } from '../dashboard-handler.js';\nimport { convertHtmlToMarkdown } from '../markdown.js';\nimport type { ApptvtyConfig, RequestLogEntry } from '../types.js';\n\n/** Convert Next request headers to a plain object (Web API Headers.entries() at runtime). */\nfunction headersToRecord(h: NextRequest['headers']): Record<string, string> {\n const entries = (h as unknown as { entries(): IterableIterator<[string, string]> }).entries();\n return Object.fromEntries(Array.from(entries));\n}\n\n// ─── Shared singleton instances per config (keyed by apiKey) ─────────────────\n\nconst instances = new Map<string, { client: ApptvtyClient; logger: RequestLogger }>();\n\nfunction getInstance(config: ApptvtyConfig) {\n const key = config.apiKey;\n if (!instances.has(key)) {\n const client = new ApptvtyClient(config);\n const logger = new RequestLogger(client, config);\n instances.set(key, { client, logger });\n }\n return instances.get(key)!;\n}\n\n// ─── Middleware wrapper ───────────────────────────────────────────────────────\n\ntype NextMiddleware = (request: NextRequest, event?: any) => Response | NextResponse | Promise<Response | NextResponse>;\n\n/**\n * Wraps a Next.js middleware function (or creates a passthrough) with\n * Apptvty request logging and multi-layer ad injection for AI/scraper traffic.\n *\n * @example\n * // middleware.ts\n * import { withApptvty } from 'apptvty/nextjs';\n * export default withApptvty({ apiKey: 'ak_...', siteId: 'site_...' });\n */\nexport function withApptvty(\n config: ApptvtyConfig,\n next?: NextMiddleware,\n) {\n const { client, logger } = getInstance(config);\n const queryPath = config.queryPath ?? '/query';\n\n return async function apptvtyMiddleware(request: NextRequest, event?: any): Promise<NextResponse> {\n const startMs = Date.now();\n const userAgent = request.headers.get('user-agent') ?? '';\n const crawlerInfo = detectCrawler(userAgent);\n const scraperService = detectScraperService(userAgent);\n const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get('ai_crawler'), false);\n \n // broad detection: is it a confirmed AI, a known scraper service, or a suspicious generic bot?\n const isAi = crawlerInfo.isAi || aiCrawlerParam;\n const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === 'unknown_bot';\n\n // ── Active Handshake Verification (PRIORITY) ──────────────────────────────\n if (request.nextUrl.pathname === '/api/apptvty/verify') {\n const challenge = request.nextUrl.searchParams.get('challenge');\n if (challenge) {\n const encoder = new TextEncoder();\n const keyData = encoder.encode(config.apiKey);\n const cryptoKey = await globalThis.crypto.subtle.importKey(\n 'raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']\n );\n const prefixedChallenge = `apptvty_verify_challenge:${challenge}`;\n const signatureBuffer = await globalThis.crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(prefixedChallenge));\n const signature = Array.from(new Uint8Array(signatureBuffer))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n\n return NextResponse.json({\n site_id: config.siteId,\n verified: true,\n signature\n });\n }\n }\n\n // Run the user's middleware (or passthrough)\n let response: Response | NextResponse;\n try {\n response = next ? await next(request, event) : NextResponse.next();\n } catch (err) {\n throw err;\n }\n\n const responseTimeMs = Date.now() - startMs;\n const { pathname } = request.nextUrl;\n\n if (shouldSkip(pathname)) {\n return response as NextResponse;\n }\n\n const headers = headersToRecord(request.headers);\n const entry: RequestLogEntry = {\n site_id: config.siteId,\n timestamp: new Date().toISOString(),\n request_method: request.method,\n request_path: pathname,\n response_status: response.status,\n response_time_ms: responseTimeMs,\n ip_address: getClientIp(headers),\n user_agent: userAgent,\n referrer: request.headers.get('referer'),\n is_ai_crawler: isAi,\n crawler_type: crawlerInfo.name,\n crawler_organization: crawlerInfo.organization,\n confidence_score: crawlerInfo.confidence,\n scraper_service: scraperService.name,\n attribution_id: request.nextUrl.searchParams.get('atid'),\n };\n\n const isInternalRequest = request.headers.get('x-apptvty-internal') === 'true';\n\n if (!isInternalRequest && !pathname.startsWith(queryPath)) {\n logger.enqueue(entry);\n if (event && typeof event.waitUntil === 'function') {\n event.waitUntil(logger.flush());\n }\n }\n\n // ── Stealth Ad Injection Strategy ─────────────────────────────────────────\n if (!isInternalRequest && !pathname.startsWith(queryPath) && response.status === 200) {\n try {\n const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });\n \n if (pageAds.ads && pageAds.ads.length > 0) {\n const ad = pageAds.ads[0];\n \n // Log impression fire-and-forget\n client.logImpression({\n impression_id: ad.impression_id,\n site_id: config.siteId,\n page_path: pathname,\n agent_ua: userAgent,\n agent_ip: getClientIp(headers),\n timestamp: new Date().toISOString()\n }).catch(() => {});\n\n // Tier 1 & 2: Any Suspected Scraper/Bot/AI -> Use Fetch Proxy to Intercept Body\n if (isScraper) {\n const proxyReq = new Request(request.url, { headers: new Headers(request.headers) });\n proxyReq.headers.set('x-apptvty-internal', 'true');\n const res = await fetch(proxyReq);\n const contentType = res.headers.get('content-type') ?? '';\n \n if (contentType.includes('text/html')) {\n const html = await res.text();\n\n // Confirmed AI -> Return Markdown\n if (isAi || scraperService.isScraperService) {\n let markdown = convertHtmlToMarkdown(html);\n markdown += `\\n\\n---\\n> **Sponsored:** [${ad.text}](${ad.url}) - ${ad.advertiser}\\n`;\n return new NextResponse(markdown, {\n status: res.status,\n headers: {\n 'Content-Type': 'text/markdown',\n 'X-Apptvty-AEO': 'true',\n 'X-Sponsored-Content': `${ad.text}; url=${ad.url}`\n }\n });\n }\n\n // Suspicious Bot/Scraper/Grey Area -> Stealth HTML Injection\n const jsonLd = `\\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"CreativeWork\",\"author\":{\"@type\":\"Organization\",\"name\":\"${ad.advertiser}\"},\"mainEntityOfPage\":{\"@type\":\"WebPage\",\"@id\":\"${ad.url}\"},\"headline\":\"Sponsored: ${ad.text}\"}</script>\\n`;\n const stealthDiv = `\\n<div style=\"display:none !important;visibility:hidden;height:0;width:0;overflow:hidden;\" aria-hidden=\"true\" data-apptvty-ad=\"${ad.impression_id}\">Sponsored by ${ad.advertiser}: <a href=\"${ad.url}\">${ad.text}</a></div>\\n`;\n \n let modifiedHtml = html;\n if (html.includes('</head>')) modifiedHtml = modifiedHtml.replace('</head>', `${jsonLd}</head>`);\n if (modifiedHtml.includes('</body>')) modifiedHtml = modifiedHtml.replace('</body>', `${stealthDiv}</body>`);\n else modifiedHtml += stealthDiv;\n\n return new NextResponse(modifiedHtml, {\n status: res.status,\n headers: {\n ...headersToRecord(res.headers),\n 'X-Sponsored-Content': `${ad.text}; url=${ad.url}`\n }\n });\n }\n }\n\n // Tier 3: Likely Human -> Header only (Minimal overhead)\n response.headers.set('X-Sponsored-Content', `${ad.text}; url=${ad.url}`);\n }\n } catch (err) {\n if (config.debug) console.warn('[apptvty] Stealth injection failed:', err);\n }\n }\n\n return response as NextResponse;\n };\n}\n\n// ─── Query route handler ──────────────────────────────────────────────────────\n\n/**\n * Creates a Next.js App Router GET handler for the AEO query endpoint.\n *\n * @example\n * // app/query/route.ts\n * import { createNextjsQueryHandler } from 'apptvty/nextjs';\n * export const GET = createNextjsQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' });\n */\nexport function createNextjsQueryHandler(config: ApptvtyConfig) {\n const { client } = getInstance(config);\n const handleQuery = createQueryHandler(client, config);\n\n return async function GET(request: NextRequest): Promise<NextResponse> {\n const { searchParams } = request.nextUrl;\n const q = searchParams.get('q');\n const lang = searchParams.get('lang');\n const surfaceAds = parseBoolParam(searchParams.get('surface_ads'), true);\n const aiCrawler = parseBoolParam(searchParams.get('ai_crawler'), false);\n const userAgent = request.headers.get('user-agent') ?? '';\n const headers = headersToRecord(request.headers);\n\n const result = await handleQuery({\n query: q,\n lang,\n surface_ads: surfaceAds,\n ai_crawler: aiCrawler,\n userAgent,\n ipAddress: getClientIp(headers),\n requestUrl: request.url,\n });\n\n return NextResponse.json(result.body, {\n status: result.status,\n headers: result.headers,\n });\n };\n}\n\n/**\n * Next.js route handler for the embedded analytics dashboard.\n *\n * Mount this in app/api/apptvty/logs/route.ts (or your preferred path).\n *\n * @example\n * // app/api/apptvty/logs/route.ts\n * import { createNextjsDashboardHandler } from 'apptvty/nextjs';\n * const config = { apiKey: 'ak_...', siteId: 'site_...' };\n * export const GET = createNextjsDashboardHandler(config);\n */\nexport function createNextjsDashboardHandler(config: ApptvtyConfig) {\n const { client } = getInstance(config);\n const handleDashboard = createDashboardHandler(client, config);\n\n return async function dashboardHandler(request: NextRequest) {\n const result = await handleDashboard({\n path: request.nextUrl.pathname + request.nextUrl.search,\n method: request.method,\n apiKey: config.apiKey,\n siteId: config.siteId,\n authHeader: request.headers.get('Authorization'),\n });\n\n if (result.headers['Content-Type'] === 'text/html') {\n return new NextResponse(result.body, {\n status: result.status,\n headers: result.headers,\n });\n }\n\n return NextResponse.json(JSON.parse(result.body), {\n status: result.status,\n headers: result.headers,\n });\n };\n}\n\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction parseBoolParam(value: string | null, defaultValue: boolean): boolean {\n if (value === null) return defaultValue;\n return value === '1' || value === 'true' || value === 'yes';\n}\n\nfunction shouldSkip(pathname: string): boolean {\n return (\n pathname.startsWith('/_next/') ||\n pathname.startsWith('/api/_') ||\n pathname === '/favicon.ico' ||\n /\\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\\.map)$/.test(pathname)\n );\n}\n","import * as cheerio from 'cheerio';\n\n/**\n * A lightweight Edge-compatible HTML to Markdown converter.\n * Perfect for Vercel Edge runtime where DOMParser and 'turndown' don't work reliably.\n */\nexport function convertHtmlToMarkdown(html: string): string {\n if (!html) return '';\n \n const $ = cheerio.load(html);\n \n // Clean up noisy elements\n $('script, style, nav, footer, header, aside, svg, .ad, .sponsor, noscript').remove();\n \n // Scope to main content area if it exists, to avoid extracting menus\n const main = $('main, article, [role=\"main\"], #content, .content').first();\n const root = main.length ? main : $('body');\n \n let markdown = '';\n \n // Extract content pseudo-sequentially\n root.find('h1, h2, h3, h4, h5, h6, p, ul, ol').each((_, el) => {\n const $el = $(el);\n const tagName = el.tagName.toLowerCase();\n \n if (tagName === 'ul' || tagName === 'ol') {\n $el.find('li').each((_, li) => {\n const text = cleanText($(li).text());\n if (text) markdown += `- ${text}\\n`;\n });\n markdown += '\\n';\n return;\n }\n \n const text = cleanText($el.text());\n if (!text) return;\n\n if (tagName === 'h1') markdown += `# ${text}\\n\\n`;\n else if (tagName === 'h2') markdown += `## ${text}\\n\\n`;\n else if (tagName === 'h3') markdown += `### ${text}\\n\\n`;\n else if (tagName === 'h4') markdown += `#### ${text}\\n\\n`;\n else if (tagName === 'h5') markdown += `##### ${text}\\n\\n`;\n else if (tagName === 'h6') markdown += `###### ${text}\\n\\n`;\n else if (tagName === 'p') markdown += `${text}\\n\\n`;\n });\n\n // Fallback if structured HTML is poor and we found almost nothing\n if (markdown.trim().length < 50) {\n markdown = cleanText(root.text()) + '\\n\\n';\n }\n\n return markdown.trim();\n}\n\nfunction cleanText(text: string): string {\n return text.trim().replace(/\\s+/g, ' ');\n}\n"],"mappings":"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","names":["_","text"]}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CrawlerInfo, A as ApptvtyConfig, R as RequestLogEntry, Q as QueryRequest, B as BackendQueryResponse, P as PageAdsResponse, I as ImpressionLog, a as IndexDocumentOptions, b as IndexDocumentResult, S as SiteOverviewStats, D as DailyStat, c as RecentActivityItem, d as RecentQueryItem, e as CrawlerBreakdown, f as SiteWalletInfo, g as CreateCampaignParams, h as CampaignRecord, U as UpdateCampaignParams, i as InsufficientBalanceError, j as AgentQueryResponse, k as QueryEndpointDiscovery, l as AgentErrorResponse } from './types-
|
|
2
|
-
export { m as CampaignStatus, n as PageAd, o as QuerySource, p as RelatedResource } from './types-
|
|
1
|
+
import { C as CrawlerInfo, A as ApptvtyConfig, R as RequestLogEntry, Q as QueryRequest, B as BackendQueryResponse, P as PageAdsResponse, I as ImpressionLog, a as IndexDocumentOptions, b as IndexDocumentResult, S as SiteOverviewStats, D as DailyStat, c as RecentActivityItem, d as RecentQueryItem, e as CrawlerBreakdown, f as SiteWalletInfo, g as CreateCampaignParams, h as CampaignRecord, U as UpdateCampaignParams, i as InsufficientBalanceError, j as AgentQueryResponse, k as QueryEndpointDiscovery, l as AgentErrorResponse } from './types-Bz3fBGpw.mjs';
|
|
2
|
+
export { m as CampaignStatus, n as PageAd, o as QuerySource, p as RelatedResource } from './types-Bz3fBGpw.mjs';
|
|
3
3
|
export { createNextjsQueryHandler, withApptvty } from './middleware/nextjs.mjs';
|
|
4
4
|
export { createExpressMiddleware, createExpressQueryHandler } from './middleware/express.mjs';
|
|
5
5
|
import 'next/server';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CrawlerInfo, A as ApptvtyConfig, R as RequestLogEntry, Q as QueryRequest, B as BackendQueryResponse, P as PageAdsResponse, I as ImpressionLog, a as IndexDocumentOptions, b as IndexDocumentResult, S as SiteOverviewStats, D as DailyStat, c as RecentActivityItem, d as RecentQueryItem, e as CrawlerBreakdown, f as SiteWalletInfo, g as CreateCampaignParams, h as CampaignRecord, U as UpdateCampaignParams, i as InsufficientBalanceError, j as AgentQueryResponse, k as QueryEndpointDiscovery, l as AgentErrorResponse } from './types-
|
|
2
|
-
export { m as CampaignStatus, n as PageAd, o as QuerySource, p as RelatedResource } from './types-
|
|
1
|
+
import { C as CrawlerInfo, A as ApptvtyConfig, R as RequestLogEntry, Q as QueryRequest, B as BackendQueryResponse, P as PageAdsResponse, I as ImpressionLog, a as IndexDocumentOptions, b as IndexDocumentResult, S as SiteOverviewStats, D as DailyStat, c as RecentActivityItem, d as RecentQueryItem, e as CrawlerBreakdown, f as SiteWalletInfo, g as CreateCampaignParams, h as CampaignRecord, U as UpdateCampaignParams, i as InsufficientBalanceError, j as AgentQueryResponse, k as QueryEndpointDiscovery, l as AgentErrorResponse } from './types-Bz3fBGpw.js';
|
|
2
|
+
export { m as CampaignStatus, n as PageAd, o as QuerySource, p as RelatedResource } from './types-Bz3fBGpw.js';
|
|
3
3
|
export { createNextjsQueryHandler, withApptvty } from './middleware/nextjs.js';
|
|
4
4
|
export { createExpressMiddleware, createExpressQueryHandler } from './middleware/express.js';
|
|
5
5
|
import 'next/server';
|