@prenta/core 5.8.0 → 5.9.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/CHANGELOG.md +12 -0
- package/dist/api/handler-factory.d.ts.map +1 -1
- package/dist/api/handler-factory.js +15 -6
- package/dist/api/handler-factory.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +2 -0
- package/dist/api/handlers.js.map +1 -1
- package/dist/api/routes/scheduling.d.ts.map +1 -1
- package/dist/api/routes/scheduling.js +8 -1
- package/dist/api/routes/scheduling.js.map +1 -1
- package/dist/api/routes/seo-geo.d.ts +23 -0
- package/dist/api/routes/seo-geo.d.ts.map +1 -0
- package/dist/api/routes/seo-geo.js +188 -0
- package/dist/api/routes/seo-geo.js.map +1 -0
- package/dist/astro-redirects.d.ts +5 -0
- package/dist/astro-redirects.d.ts.map +1 -1
- package/dist/astro-redirects.js +13 -1
- package/dist/astro-redirects.js.map +1 -1
- package/dist/cron/index.d.ts +8 -0
- package/dist/cron/index.d.ts.map +1 -1
- package/dist/cron/index.js +21 -0
- package/dist/cron/index.js.map +1 -1
- package/dist/db/model-types.d.ts +21 -0
- package/dist/db/model-types.d.ts.map +1 -1
- package/dist/forms/attribution.d.ts +6 -2
- package/dist/forms/attribution.d.ts.map +1 -1
- package/dist/forms/attribution.js +5 -0
- package/dist/forms/attribution.js.map +1 -1
- package/dist/forms/index.d.ts +2 -2
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js +1 -1
- package/dist/forms/index.js.map +1 -1
- package/dist/generated/browser.d.ts +18 -0
- package/dist/generated/client.d.ts +18 -0
- package/dist/generated/internal/class.d.ts +22 -0
- package/dist/generated/internal/class.js +4 -4
- package/dist/generated/internal/prismaNamespace.d.ts +187 -1
- package/dist/generated/internal/prismaNamespace.js +32 -0
- package/dist/generated/internal/prismaNamespaceBrowser.d.ts +36 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +32 -0
- package/dist/generated/models/GeoCrawlDaily.d.ts +1144 -0
- package/dist/generated/models/GeoCrawlDaily.js +1 -0
- package/dist/generated/models/GeoReferralDaily.d.ts +1116 -0
- package/dist/generated/models/GeoReferralDaily.js +1 -0
- package/dist/generated/models.d.ts +2 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/public-geo.d.ts +39 -0
- package/dist/public-geo.d.ts.map +1 -0
- package/dist/public-geo.js +79 -0
- package/dist/public-geo.js.map +1 -0
- package/dist/security/rate-limit-policy.d.ts +23 -0
- package/dist/security/rate-limit-policy.d.ts.map +1 -1
- package/dist/security/rate-limit-policy.js +36 -2
- package/dist/security/rate-limit-policy.js.map +1 -1
- package/dist/seo/ai-bots.d.ts +38 -7
- package/dist/seo/ai-bots.d.ts.map +1 -1
- package/dist/seo/ai-bots.js +82 -27
- package/dist/seo/ai-bots.js.map +1 -1
- package/dist/seo/ai-crawler-log.d.ts +2 -0
- package/dist/seo/ai-crawler-log.d.ts.map +1 -1
- package/dist/seo/ai-crawler-log.js +19 -1
- package/dist/seo/ai-crawler-log.js.map +1 -1
- package/dist/seo/ai-referrers.d.ts +25 -0
- package/dist/seo/ai-referrers.d.ts.map +1 -0
- package/dist/seo/ai-referrers.js +96 -0
- package/dist/seo/ai-referrers.js.map +1 -0
- package/dist/seo/config-store.d.ts +4 -0
- package/dist/seo/config-store.d.ts.map +1 -1
- package/dist/seo/config-store.js +3 -0
- package/dist/seo/config-store.js.map +1 -1
- package/dist/seo/digest.d.ts +23 -0
- package/dist/seo/digest.d.ts.map +1 -1
- package/dist/seo/digest.js +89 -3
- package/dist/seo/digest.js.map +1 -1
- package/dist/seo/geo-record.d.ts +75 -0
- package/dist/seo/geo-record.d.ts.map +1 -0
- package/dist/seo/geo-record.js +252 -0
- package/dist/seo/geo-record.js.map +1 -0
- package/dist/seo/geo-report.d.ts +96 -0
- package/dist/seo/geo-report.d.ts.map +1 -0
- package/dist/seo/geo-report.js +315 -0
- package/dist/seo/geo-report.js.map +1 -0
- package/dist/seo/geo-retention.d.ts +19 -0
- package/dist/seo/geo-retention.d.ts.map +1 -0
- package/dist/seo/geo-retention.js +23 -0
- package/dist/seo/geo-retention.js.map +1 -0
- package/dist/seo/index.d.ts +9 -2
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +5 -1
- package/dist/seo/index.js.map +1 -1
- package/dist/seo/search-console/sync.d.ts +8 -0
- package/dist/seo/search-console/sync.d.ts.map +1 -1
- package/dist/seo/search-console/sync.js +6 -3
- package/dist/seo/search-console/sync.js.map +1 -1
- package/dist/site.d.ts +5 -0
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +3 -0
- package/dist/site.js.map +1 -1
- package/package.json +3 -3
- package/prisma/migrations/0018_geo_daily/migration.sql +35 -0
- package/prisma/schema.prisma +41 -0
package/dist/seo/ai-bots.js
CHANGED
|
@@ -63,26 +63,92 @@ export const AI_RETRIEVAL_BOTS = [
|
|
|
63
63
|
'DuckAssistBot',
|
|
64
64
|
];
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* Returns `null` for humans and unknown bots — never guesses.
|
|
66
|
+
* General web-search crawlers. Never blocked by the AI policy; recorded by GEO
|
|
67
|
+
* measurement as the baseline that makes AI crawl volume legible. Well-attested
|
|
68
|
+
* tokens only. `Applebot` is deliberately NOT here — it feeds Siri/Spotlight
|
|
69
|
+
* answers and stays in the retrieval list.
|
|
71
70
|
*/
|
|
72
|
-
export
|
|
71
|
+
export const SEARCH_BOTS = [
|
|
72
|
+
'Googlebot',
|
|
73
|
+
'Bingbot',
|
|
74
|
+
'DuckDuckBot',
|
|
75
|
+
'YandexBot',
|
|
76
|
+
'Baiduspider',
|
|
77
|
+
];
|
|
78
|
+
const REGISTRY = [
|
|
79
|
+
[AI_TRAINING_BOTS, 'training'],
|
|
80
|
+
[AI_RETRIEVAL_BOTS, 'retrieval'],
|
|
81
|
+
[SEARCH_BOTS, 'search'],
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* Identify a known crawler from a `User-Agent` string. Substring match
|
|
85
|
+
* (case-insensitive). Precedence is training → retrieval → search, so the more
|
|
86
|
+
* specific `*-Extended` variants win over their search-crawler namesakes
|
|
87
|
+
* (`Applebot-Extended` → training, `Applebot` → retrieval, `Google-Extended` →
|
|
88
|
+
* training, `Googlebot` → search). Returns `null` for humans and unknown bots.
|
|
89
|
+
*/
|
|
90
|
+
export function detectBot(userAgent) {
|
|
73
91
|
if (!userAgent)
|
|
74
92
|
return null;
|
|
75
93
|
const ua = userAgent.toLowerCase();
|
|
76
|
-
for (const
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
for (const [tokens, category] of REGISTRY) {
|
|
95
|
+
for (const token of tokens) {
|
|
96
|
+
if (ua.includes(token.toLowerCase()))
|
|
97
|
+
return { token, category };
|
|
98
|
+
}
|
|
79
99
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Identify a known **AI** crawler (training or retrieval) — the original
|
|
104
|
+
* contract used by robots.txt and the lifetime crawler log. Search bots return
|
|
105
|
+
* `null` here; use {@link detectBot} when you want them too.
|
|
106
|
+
*/
|
|
107
|
+
export function detectAiBot(userAgent) {
|
|
108
|
+
const found = detectBot(userAgent);
|
|
109
|
+
if (!found || found.category === 'search')
|
|
110
|
+
return null;
|
|
111
|
+
return { token: found.token, category: found.category };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Exact (case-insensitive) lookup of a canonical token. This is the server-side
|
|
115
|
+
* allowlist for beacon payloads: a middleware running a different core version
|
|
116
|
+
* can only ever record tokens this build knows about.
|
|
117
|
+
*/
|
|
118
|
+
export function botByToken(token) {
|
|
119
|
+
if (!token)
|
|
120
|
+
return null;
|
|
121
|
+
const needle = token.toLowerCase();
|
|
122
|
+
for (const [tokens, category] of REGISTRY) {
|
|
123
|
+
for (const candidate of tokens) {
|
|
124
|
+
if (candidate.toLowerCase() === needle)
|
|
125
|
+
return { token: candidate, category };
|
|
126
|
+
}
|
|
83
127
|
}
|
|
84
128
|
return null;
|
|
85
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Effective stance for one category. Precedence mirrors
|
|
132
|
+
* {@link buildAiBotRobotsLines}: explicit policy → legacy `blockAIBots` → allow.
|
|
133
|
+
* Search crawlers are never blocked by the AI policy.
|
|
134
|
+
*/
|
|
135
|
+
export function aiBotStanceFor(category, policy, legacyBlockAIBots) {
|
|
136
|
+
switch (category) {
|
|
137
|
+
case 'search':
|
|
138
|
+
return 'allow';
|
|
139
|
+
case 'training':
|
|
140
|
+
case 'retrieval': {
|
|
141
|
+
if (policy && (policy.training || policy.retrieval)) {
|
|
142
|
+
return policy[category] === 'block' ? 'block' : 'allow';
|
|
143
|
+
}
|
|
144
|
+
return legacyBlockAIBots ? 'block' : 'allow';
|
|
145
|
+
}
|
|
146
|
+
default: {
|
|
147
|
+
const _exhaustive = category;
|
|
148
|
+
return _exhaustive;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
86
152
|
/**
|
|
87
153
|
* Build the robots.txt User-agent/Disallow line groups for blocked AI bots.
|
|
88
154
|
*
|
|
@@ -96,24 +162,13 @@ export function detectAiBot(userAgent) {
|
|
|
96
162
|
* `User-agent` + `Disallow: /` + blank-line group). Pure + deterministic.
|
|
97
163
|
*/
|
|
98
164
|
export function buildAiBotRobotsLines(policy, legacyBlockAIBots) {
|
|
99
|
-
let blockTraining;
|
|
100
|
-
let blockRetrieval;
|
|
101
|
-
if (policy && (policy.training || policy.retrieval)) {
|
|
102
|
-
blockTraining = policy.training === 'block';
|
|
103
|
-
blockRetrieval = policy.retrieval === 'block';
|
|
104
|
-
}
|
|
105
|
-
else if (legacyBlockAIBots) {
|
|
106
|
-
blockTraining = true;
|
|
107
|
-
blockRetrieval = true;
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
return [];
|
|
111
|
-
}
|
|
112
165
|
const bots = [];
|
|
113
|
-
if (
|
|
166
|
+
if (aiBotStanceFor('training', policy, legacyBlockAIBots) === 'block') {
|
|
114
167
|
bots.push(...AI_TRAINING_BOTS);
|
|
115
|
-
|
|
168
|
+
}
|
|
169
|
+
if (aiBotStanceFor('retrieval', policy, legacyBlockAIBots) === 'block') {
|
|
116
170
|
bots.push(...AI_RETRIEVAL_BOTS);
|
|
171
|
+
}
|
|
117
172
|
const lines = [];
|
|
118
173
|
for (const bot of bots) {
|
|
119
174
|
lines.push(`User-agent: ${bot}`);
|
package/dist/seo/ai-bots.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-bots.js","sourceRoot":"","sources":["../../src/seo/ai-bots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,SAAS;IACT,QAAQ;IACR,YAAY;IACZ,WAAW;IACX,cAAc;IACd,2DAA2D;IAC3D,iBAAiB;IACjB,0CAA0C;IAC1C,OAAO;IACP,YAAY;IACZ,YAAY;IACZ,qEAAqE;IACrE,mBAAmB;IACnB,OAAO;IACP,oBAAoB;IACpB,aAAa;IACb,kCAAkC;IAClC,SAAS;IACT,WAAW;IACX,QAAQ;IACR,UAAU;IACV,UAAU;IACV,WAAW;IACX,cAAc;IACd,UAAU;CACX,CAAA;AAED,yEAAyE;AACzE,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,SAAS;IACT,eAAe;IACf,cAAc;IACd,aAAa;IACb,eAAe;IACf,iBAAiB;IACjB,sBAAsB;IACtB,YAAY;IACZ,aAAa;IACb,gCAAgC;IAChC,UAAU;IACV,iBAAiB;IACjB,WAAW;IACX,8BAA8B;IAC9B,QAAQ;IACR,eAAe;CAChB,CAAA;
|
|
1
|
+
{"version":3,"file":"ai-bots.js","sourceRoot":"","sources":["../../src/seo/ai-bots.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type AiCrawlerSurface = 'llms' | 'llms-full' | 'md' | 'honeypot';
|
|
2
|
+
/** Path recorded in the GEO daily trend for each lifetime-counter surface. */
|
|
3
|
+
export declare const AI_CRAWLER_SURFACE_PATH: Record<AiCrawlerSurface, string>;
|
|
2
4
|
/** Upsert one aggregated hit. Safe to await on any public route. */
|
|
3
5
|
export declare function recordAiCrawlerHit(db: any, entry: {
|
|
4
6
|
bot: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-crawler-log.d.ts","sourceRoot":"","sources":["../../src/seo/ai-crawler-log.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ai-crawler-log.d.ts","sourceRoot":"","sources":["../../src/seo/ai-crawler-log.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,IAAI,GAAG,UAAU,CAAA;AAIvE,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAKpE,CAAA;AAED,oEAAoE;AACpE,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,GAAG,EACP,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACnE,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,GAAG,EACP,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,IAAI,CAAC,CAQf"}
|
|
@@ -7,8 +7,16 @@
|
|
|
7
7
|
* `prenta_ai_crawler_hits` table — must never break a public surface.
|
|
8
8
|
*/
|
|
9
9
|
import { hasModel } from '../api/route-helpers.js';
|
|
10
|
-
import { detectAiBot } from './ai-bots.js';
|
|
10
|
+
import { botByToken, detectAiBot } from './ai-bots.js';
|
|
11
|
+
import { recordGeoCrawl } from './geo-record.js';
|
|
11
12
|
const MAX_BOT_LEN = 128;
|
|
13
|
+
/** Path recorded in the GEO daily trend for each lifetime-counter surface. */
|
|
14
|
+
export const AI_CRAWLER_SURFACE_PATH = {
|
|
15
|
+
llms: '/llms.txt',
|
|
16
|
+
'llms-full': '/llms-full.txt',
|
|
17
|
+
md: '/md',
|
|
18
|
+
honeypot: '/ai-crawlers/trap',
|
|
19
|
+
};
|
|
12
20
|
/** Upsert one aggregated hit. Safe to await on any public route. */
|
|
13
21
|
export async function recordAiCrawlerHit(db, entry) {
|
|
14
22
|
if (!db || !hasModel(db, 'aiCrawlerHit'))
|
|
@@ -29,6 +37,16 @@ export async function recordAiCrawlerHit(db, entry) {
|
|
|
29
37
|
catch {
|
|
30
38
|
// best-effort — swallow so logging can never break the response
|
|
31
39
|
}
|
|
40
|
+
// Keep the daily trend in step with the lifetime counter. Only registry
|
|
41
|
+
// tokens are recorded (the honeypot's `unknown` stays lifetime-only).
|
|
42
|
+
const known = botByToken(bot);
|
|
43
|
+
if (known) {
|
|
44
|
+
await recordGeoCrawl(db, {
|
|
45
|
+
bot: known.token,
|
|
46
|
+
category: known.category,
|
|
47
|
+
path: AI_CRAWLER_SURFACE_PATH[entry.surface],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
32
50
|
}
|
|
33
51
|
/**
|
|
34
52
|
* Detect a known AI crawler from the request's User-Agent and, if found, record
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-crawler-log.js","sourceRoot":"","sources":["../../src/seo/ai-crawler-log.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"ai-crawler-log.js","sourceRoot":"","sources":["../../src/seo/ai-crawler-log.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAClD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAIhD,MAAM,WAAW,GAAG,GAAG,CAAA;AAEvB,8EAA8E;AAC9E,MAAM,CAAC,MAAM,uBAAuB,GAAqC;IACvE,IAAI,EAAE,WAAW;IACjB,WAAW,EAAE,gBAAgB;IAC7B,EAAE,EAAE,KAAK;IACT,QAAQ,EAAE,mBAAmB;CAC9B,CAAA;AAED,oEAAoE;AACpE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,EAAO,EACP,KAAoE;IAEpE,IAAI,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAC;QAAE,OAAM;IAChD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC;SAChC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC;SACrB,IAAI,EAAE,CAAA;IACT,IAAI,CAAC,GAAG;QAAE,OAAM;IAChB,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,OAAO,CAAA;IAC1C,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC;YAC3B,KAAK,EAAE,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE;YACvD,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,QAAQ,EAAE;YACpE,MAAM,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE;SAC3D,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;IAClE,CAAC;IACD,wEAAwE;IACxE,sEAAsE;IACtE,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;IAC7B,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,cAAc,CAAC,EAAE,EAAE;YACvB,GAAG,EAAE,KAAK,CAAC,KAAK;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,IAAI,EAAE,uBAAuB,CAAC,KAAK,CAAC,OAAO,CAAC;SAC7C,CAAC,CAAA;IACJ,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,EAAO,EACP,OAAgB,EAChB,OAAyB;IAEzB,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAA;IAC/D,IAAI,CAAC,QAAQ;QAAE,OAAM;IACrB,MAAM,kBAAkB,CAAC,EAAE,EAAE;QAC3B,GAAG,EAAE,QAAQ,CAAC,KAAK;QACnB,OAAO;QACP,QAAQ,EAAE,QAAQ,CAAC,QAAQ;KAC5B,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface DetectedAiReferrer {
|
|
2
|
+
/** `KnownReferrer.name`, e.g. `ChatGPT`. */
|
|
3
|
+
engine: string;
|
|
4
|
+
via: 'referer' | 'utm';
|
|
5
|
+
}
|
|
6
|
+
/** Exact allowlist check for beacon payloads. */
|
|
7
|
+
export declare function isAiReferrerEngine(name: string | null | undefined): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Detects an AI-engine referral from the `Referer` header, falling back to the
|
|
10
|
+
* request URL's `utm_source`. Returns `null` (never throws) when neither signal
|
|
11
|
+
* matches or the inputs are malformed.
|
|
12
|
+
*
|
|
13
|
+
* @param input.referer - Raw `Referer` header value, if any.
|
|
14
|
+
* @param input.url - The current request URL.
|
|
15
|
+
* @param input.selfOrigin - The site's own origin, used to skip same-site
|
|
16
|
+
* referers. Must be a full origin including the scheme (e.g.
|
|
17
|
+
* `https://example.com`); bare hosts such as `example.com` do not parse as a
|
|
18
|
+
* URL and are ignored, so no same-site filtering is applied.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectAiReferrer(input: {
|
|
21
|
+
referer?: string | null;
|
|
22
|
+
url: string | URL;
|
|
23
|
+
selfOrigin?: string | null;
|
|
24
|
+
}): DetectedAiReferrer | null;
|
|
25
|
+
//# sourceMappingURL=ai-referrers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-referrers.d.ts","sourceRoot":"","sources":["../../src/seo/ai-referrers.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,SAAS,GAAG,KAAK,CAAA;CACvB;AA2CD,iDAAiD;AACjD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE3E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B,GAAG,kBAAkB,GAAG,IAAI,CAqB5B"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-engine referral detection for GEO measurement.
|
|
3
|
+
*
|
|
4
|
+
* Two signals, in order:
|
|
5
|
+
* 1. the `Referer` header's hostname, looked up in the shared attribution
|
|
6
|
+
* registry (`KNOWN_REFERRERS`, `channel: 'ai'`);
|
|
7
|
+
* 2. failing that, the request URL's `utm_source` — ChatGPT appends
|
|
8
|
+
* `?utm_source=chatgpt.com` to outbound links while sending no `Referer`,
|
|
9
|
+
* and other engines tag links with their name.
|
|
10
|
+
*
|
|
11
|
+
* Pure. Never returns anything derived from the raw referrer URL or query
|
|
12
|
+
* string other than the engine name.
|
|
13
|
+
*/
|
|
14
|
+
import { AI_REFERRER_NAMES, lookupKnownReferrer } from '../forms/attribution.js';
|
|
15
|
+
/**
|
|
16
|
+
* Explicit `utm_source` aliases (lowercased) → registry `name`. Only exact
|
|
17
|
+
* matches are accepted; word fragments of engine names (`google`, `microsoft`,
|
|
18
|
+
* `chat`, `studio`) are deliberately NOT matched — they are common paid-campaign
|
|
19
|
+
* tags and would corrupt the AI-referral metric.
|
|
20
|
+
*/
|
|
21
|
+
const UTM_ENGINE_ALIASES = {
|
|
22
|
+
copilot: 'Microsoft Copilot',
|
|
23
|
+
mistral: 'Le Chat (Mistral)',
|
|
24
|
+
};
|
|
25
|
+
function hostOf(value) {
|
|
26
|
+
try {
|
|
27
|
+
return new URL(value).hostname.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function aiEngineForHost(host) {
|
|
34
|
+
const meta = lookupKnownReferrer(host);
|
|
35
|
+
return meta && meta.channel === 'ai' ? meta.name : null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Matches a `utm_source` value against an engine name exactly (case-insensitive),
|
|
39
|
+
* against the name with non-alphanumerics removed (`chatgpt`, `deepseek`), or
|
|
40
|
+
* against an explicit alias. Never matches partial words.
|
|
41
|
+
*/
|
|
42
|
+
function aiEngineForWord(raw) {
|
|
43
|
+
const word = raw.trim().toLowerCase();
|
|
44
|
+
if (!word)
|
|
45
|
+
return null;
|
|
46
|
+
for (const name of AI_REFERRER_NAMES) {
|
|
47
|
+
const lowered = name.toLowerCase();
|
|
48
|
+
if (lowered === word)
|
|
49
|
+
return name;
|
|
50
|
+
if (lowered.replace(/[^a-z0-9]/g, '') === word)
|
|
51
|
+
return name;
|
|
52
|
+
}
|
|
53
|
+
const alias = Object.hasOwn(UTM_ENGINE_ALIASES, word) ? UTM_ENGINE_ALIASES[word] : undefined;
|
|
54
|
+
return alias ?? null;
|
|
55
|
+
}
|
|
56
|
+
/** Exact allowlist check for beacon payloads. */
|
|
57
|
+
export function isAiReferrerEngine(name) {
|
|
58
|
+
return typeof name === 'string' && name.length > 0 && AI_REFERRER_NAMES.includes(name);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Detects an AI-engine referral from the `Referer` header, falling back to the
|
|
62
|
+
* request URL's `utm_source`. Returns `null` (never throws) when neither signal
|
|
63
|
+
* matches or the inputs are malformed.
|
|
64
|
+
*
|
|
65
|
+
* @param input.referer - Raw `Referer` header value, if any.
|
|
66
|
+
* @param input.url - The current request URL.
|
|
67
|
+
* @param input.selfOrigin - The site's own origin, used to skip same-site
|
|
68
|
+
* referers. Must be a full origin including the scheme (e.g.
|
|
69
|
+
* `https://example.com`); bare hosts such as `example.com` do not parse as a
|
|
70
|
+
* URL and are ignored, so no same-site filtering is applied.
|
|
71
|
+
*/
|
|
72
|
+
export function detectAiReferrer(input) {
|
|
73
|
+
const refHost = input.referer ? hostOf(input.referer) : null;
|
|
74
|
+
if (refHost) {
|
|
75
|
+
const selfHost = input.selfOrigin ? hostOf(input.selfOrigin) : null;
|
|
76
|
+
if (!selfHost || refHost !== selfHost) {
|
|
77
|
+
const engine = aiEngineForHost(refHost);
|
|
78
|
+
if (engine)
|
|
79
|
+
return { engine, via: 'referer' };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let url;
|
|
83
|
+
try {
|
|
84
|
+
url = typeof input.url === 'string' ? new URL(input.url) : input.url;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const utm = url.searchParams.get('utm_source');
|
|
90
|
+
if (!utm)
|
|
91
|
+
return null;
|
|
92
|
+
const asHost = utm.includes('.') ? aiEngineForHost(utm.toLowerCase()) : null;
|
|
93
|
+
const engine = asHost ?? aiEngineForWord(utm);
|
|
94
|
+
return engine ? { engine, via: 'utm' } : null;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=ai-referrers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-referrers.js","sourceRoot":"","sources":["../../src/seo/ai-referrers.ts"],"names":[],"mappings":"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"}
|
|
@@ -99,6 +99,10 @@ export interface SeoModuleSettings {
|
|
|
99
99
|
* margin) halts `already_running` instead of racing the first past `maxPerDay`.
|
|
100
100
|
*/
|
|
101
101
|
autopilotRunStartedAt: string | null;
|
|
102
|
+
/** GEO measurement (spec 2026-09-08): accept crawl/referral beacons. On by default. */
|
|
103
|
+
geoMeasurementEnabled: boolean;
|
|
104
|
+
/** Days of GeoCrawlDaily / GeoReferralDaily to keep (7–365). */
|
|
105
|
+
geoRetentionDays: number;
|
|
102
106
|
}
|
|
103
107
|
/** Safe defaults applied when the overrides document has no `module` block. */
|
|
104
108
|
export declare const DEFAULT_SEO_MODULE_SETTINGS: SeoModuleSettings;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-store.d.ts","sourceRoot":"","sources":["../../src/seo/config-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,eAAe,EAEf,mBAAmB,EACnB,aAAa,EACd,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AACjE,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"config-store.d.ts","sourceRoot":"","sources":["../../src/seo/config-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,eAAe,EAEf,mBAAmB,EACnB,aAAa,EACd,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AACjE,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAA;AAG9B,eAAO,MAAM,qBAAqB,mBAAmB,CAAA;AAErD;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,IAAI,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;IAC7B,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAA;IAC1D,gFAAgF;IAChF,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAA;IACnC,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,iBAAiB,EAAE,OAAO,CAAA;IAC1B,cAAc,EAAE,OAAO,CAAA;IACvB,aAAa,EAAE,OAAO,CAAA;IACtB,8BAA8B,EAAE,OAAO,CAAA;IACvC,2EAA2E;IAC3E,0BAA0B,EAAE,OAAO,CAAA;IACnC,sBAAsB,EAAE,OAAO,CAAA;IAC/B,qBAAqB,EAAE,OAAO,CAAA;IAC9B,eAAe,EAAE,OAAO,CAAA;IACxB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;IACjB,+EAA+E;IAC/E,sBAAsB,EAAE,OAAO,CAAA;IAC/B,4EAA4E;IAC5E,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,8FAA8F;IAC9F,wBAAwB,EAAE,OAAO,CAAA;IACjC,+FAA+F;IAC/F,yBAAyB,EAAE,OAAO,CAAA;IAClC,sGAAsG;IACtG,sCAAsC,EAAE,MAAM,CAAA;IAC9C,sEAAsE;IACtE,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,yFAAyF;IACzF,cAAc,EAAE,MAAM,CAAA;IACtB,4FAA4F;IAC5F,oBAAoB,EAAE,MAAM,CAAA;IAC5B,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,oEAAoE;IACpE,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACzC,uFAAuF;IACvF,oBAAoB,EAAE,MAAM,CAAA;IAC5B,6EAA6E;IAC7E,aAAa,EAAE,OAAO,CAAA;IACtB,qEAAqE;IACrE,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,0EAA0E;IAC1E,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,gFAAgF;IAChF,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,0EAA0E;IAC1E,SAAS,EAAE,kBAAkB,CAAA;IAC7B,iEAAiE;IACjE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,4EAA4E;IAC5E,gBAAgB,EAAE,mBAAmB,GAAG,IAAI,CAAA;IAC5C;;;;OAIG;IACH,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,uFAAuF;IACvF,qBAAqB,EAAE,OAAO,CAAA;IAC9B,gEAAgE;IAChE,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAED,+EAA+E;AAC/E,eAAO,MAAM,2BAA2B,EAAE,iBAgCzC,CAAA;AAED,uEAAuE;AACvE,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,kBAAkB,GAAG,IAAI,GAAG,iBAAiB,CAgBhG;AAED,wDAAwD;AACxD,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,OAAO,EACX,KAAK,EAAE,OAAO,CAAC,iBAAiB,CAAC,EACjC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,iBAAiB,CAAC,CAY5B;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,oBAAoB,EAAE,aAAa,CAAC,GAAG;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAA;CACjE,CAAA;AAmBD;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,OAAO,EACX,KAAK,EAAE,oBAAoB,EAC3B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,iBAAiB,CAAC,CAe5B;AAED,4EAA4E;AAC5E,wBAAsB,eAAe,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAerF;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,OAAO,EACX,SAAS,EAAE,kBAAkB,EAC7B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,kBAAkB,CAAC,CA8B7B;AAwBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,IAAI,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,SAAS,GACvC,aAAa,CAiCf;AAED,qFAAqF;AACrF,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,mBAAmB,GAAG,SAAS,EACrC,IAAI,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAAG,SAAS,GAC7C,mBAAmB,CAiBrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,EAC1C,SAAS,EAAE,kBAAkB,GAAG,IAAI,GACnC,eAAe,GAAG,IAAI,CAkBxB"}
|
package/dist/seo/config-store.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* never destructive of code-level config.
|
|
18
18
|
*/
|
|
19
19
|
import { resolveAutopilotPolicy, } from './autopilot-policy.js';
|
|
20
|
+
import { GEO_RETENTION_DEFAULT_DAYS } from './geo-retention.js';
|
|
20
21
|
export const SEO_CONFIG_COLLECTION = '__seo_config__';
|
|
21
22
|
/** Safe defaults applied when the overrides document has no `module` block. */
|
|
22
23
|
export const DEFAULT_SEO_MODULE_SETTINGS = {
|
|
@@ -49,6 +50,8 @@ export const DEFAULT_SEO_MODULE_SETTINGS = {
|
|
|
49
50
|
autopilotLastRunAt: null,
|
|
50
51
|
autopilotLastRun: null,
|
|
51
52
|
autopilotRunStartedAt: null,
|
|
53
|
+
geoMeasurementEnabled: true,
|
|
54
|
+
geoRetentionDays: GEO_RETENTION_DEFAULT_DAYS,
|
|
52
55
|
};
|
|
53
56
|
/** Merge stored module settings over the defaults (field-by-field). */
|
|
54
57
|
export function resolveSeoModuleSettings(overrides) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-store.js","sourceRoot":"","sources":["../../src/seo/config-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH,OAAO,EACL,sBAAsB,GAKvB,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"config-store.js","sourceRoot":"","sources":["../../src/seo/config-store.ts"],"names":[],"mappings":"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"}
|
package/dist/seo/digest.d.ts
CHANGED
|
@@ -64,6 +64,27 @@ export interface SeoDigestAutopilot {
|
|
|
64
64
|
/** Newest applied rows in the window, capped at `AUTOPILOT_DIGEST_TOP`. */
|
|
65
65
|
top: SeoDigestAutopilotRow[];
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Trailing-7-day GEO measurement summary (spec
|
|
69
|
+
* 2026-09-08-geo-measurement-design.md §8), derived from
|
|
70
|
+
* `buildSeoGeoReport(days: 7)`. `null` when measurement is off, the tables are
|
|
71
|
+
* absent, or nothing was recorded in the 14-day range.
|
|
72
|
+
*/
|
|
73
|
+
export interface SeoDigestGeo {
|
|
74
|
+
crawlHits7d: number;
|
|
75
|
+
crawlHitsPrev7d: number | null;
|
|
76
|
+
/** Distinct retrieval (answer-engine) bots seen in the window. */
|
|
77
|
+
retrievalBots: number;
|
|
78
|
+
aiVisits7d: number;
|
|
79
|
+
aiVisitsPrev7d: number | null;
|
|
80
|
+
/** Most-cited paths, ≤ GEO_DIGEST_TOP. */
|
|
81
|
+
topCited: Array<{
|
|
82
|
+
path: string;
|
|
83
|
+
visits: number;
|
|
84
|
+
}>;
|
|
85
|
+
/** Bots whose category is blocked in robots but that still crawled. */
|
|
86
|
+
blockedStillCrawling: string[];
|
|
87
|
+
}
|
|
67
88
|
export interface SeoDigest {
|
|
68
89
|
generatedAt: string;
|
|
69
90
|
audit: SeoDigestAudit;
|
|
@@ -79,6 +100,8 @@ export interface SeoDigest {
|
|
|
79
100
|
};
|
|
80
101
|
/** `null` when the consumer schema has no `SeoAutoApply` model. */
|
|
81
102
|
autopilot: SeoDigestAutopilot | null;
|
|
103
|
+
/** `null` when GEO measurement is off/absent or recorded nothing in 14 days. */
|
|
104
|
+
geo: SeoDigestGeo | null;
|
|
82
105
|
}
|
|
83
106
|
export declare function buildSeoDigest(db: PrentaDB, options?: {
|
|
84
107
|
now?: Date;
|
package/dist/seo/digest.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/seo/digest.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/seo/digest.ts"],"names":[],"mappings":"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"}
|
package/dist/seo/digest.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { hasModel } from '../api/route-helpers.js';
|
|
1
2
|
import { getPrentaConfig } from '../config/runtime.js';
|
|
2
3
|
import { resolveEmailSenderAsync } from '../email/resolve.js';
|
|
3
4
|
import { buildQuickWins } from './audit-engine.js';
|
|
4
5
|
import { getSeoOverrides, patchSeoModuleSettings, resolveSeoModuleSettings, } from './config-store.js';
|
|
6
|
+
import { buildSeoGeoReport } from './geo-report.js';
|
|
5
7
|
import { pendingFixFromMetadata } from './issue-fix-ai.js';
|
|
6
8
|
import { gradeForSeoScore } from './score.js';
|
|
7
9
|
/** Latest COMPLETED run older than this ⇒ prominent staleness callout. */
|
|
@@ -12,6 +14,10 @@ const SEVERITY_RANK = { critical: 3, warning: 2, info: 1 };
|
|
|
12
14
|
const AUTOPILOT_WINDOW_MS = 7 * DAY_MS;
|
|
13
15
|
/** Max applied rows listed in the autopilot block. */
|
|
14
16
|
const AUTOPILOT_DIGEST_TOP = 5;
|
|
17
|
+
/** Max cited paths listed in the AI visibility block. */
|
|
18
|
+
const GEO_DIGEST_TOP = 3;
|
|
19
|
+
/** AI visibility block window (current 7 days; the prior 7 form the baseline). */
|
|
20
|
+
const GEO_WINDOW_DAYS = 7;
|
|
15
21
|
export async function buildSeoDigest(db, options = {}) {
|
|
16
22
|
const now = options.now ?? new Date();
|
|
17
23
|
if (!db?.seoAuditRun?.findFirst)
|
|
@@ -118,11 +124,17 @@ export async function buildSeoDigest(db, options = {}) {
|
|
|
118
124
|
redirectSuggestions = (await db.redirectSuggestion.count({ where: { status: 'open' } })) ?? 0;
|
|
119
125
|
}
|
|
120
126
|
const issueFixes = openIssues.filter((i) => pendingFixFromMetadata(i.metadata) !== null).length;
|
|
127
|
+
// Module settings feed both optional blocks below (autopilot policy, GEO
|
|
128
|
+
// measurement toggle) — resolved once.
|
|
129
|
+
const hasAutopilot = hasModel(db, 'seoAutoApply');
|
|
130
|
+
const hasGeo = hasModel(db, 'geoCrawlDaily') && hasModel(db, 'geoReferralDaily');
|
|
131
|
+
const settings = hasAutopilot || hasGeo
|
|
132
|
+
? resolveSeoModuleSettings(await getSeoOverrides(db).catch(() => null))
|
|
133
|
+
: null;
|
|
121
134
|
// Autopilot ledger (optional model — consumer schemas may predate it).
|
|
122
135
|
let autopilot = null;
|
|
123
|
-
if (
|
|
136
|
+
if (hasAutopilot && settings) {
|
|
124
137
|
const since = new Date(now.getTime() - AUTOPILOT_WINDOW_MS);
|
|
125
|
-
const settings = resolveSeoModuleSettings(await getSeoOverrides(db).catch(() => null));
|
|
126
138
|
const applied = { status: 'applied', appliedAt: { gte: since } };
|
|
127
139
|
const [applied7d, verifyFailed7d, reverted7d, regressed7d, conflicts7d, topRows] = await Promise.all([
|
|
128
140
|
db.seoAutoApply.count({ where: applied }),
|
|
@@ -173,6 +185,50 @@ export async function buildSeoDigest(db, options = {}) {
|
|
|
173
185
|
})),
|
|
174
186
|
};
|
|
175
187
|
}
|
|
188
|
+
// GEO measurement (optional tables; block is null when nothing to say).
|
|
189
|
+
// Best-effort: a failing report logs and yields `null`, never a failed digest.
|
|
190
|
+
let geo = null;
|
|
191
|
+
if (hasGeo && settings) {
|
|
192
|
+
try {
|
|
193
|
+
const report = await buildSeoGeoReport(db, {
|
|
194
|
+
days: GEO_WINDOW_DAYS,
|
|
195
|
+
now,
|
|
196
|
+
settings,
|
|
197
|
+
config: getPrentaConfig(),
|
|
198
|
+
});
|
|
199
|
+
const { total: crawl } = report.crawl;
|
|
200
|
+
const { total: referrals } = report.referrals;
|
|
201
|
+
// "Nothing recorded in the 14-day range" ⇒ no block. Prior-window rows
|
|
202
|
+
// count even when the baseline is incomparable (prev* null): in that
|
|
203
|
+
// case the first-ever row is inside the range, so `firstSeenAt` tells.
|
|
204
|
+
const rangeStart = Date.parse(report.window.from) - GEO_WINDOW_DAYS * DAY_MS;
|
|
205
|
+
const firstSeenInRange = report.firstSeenAt !== null && Date.parse(report.firstSeenAt) >= rangeStart;
|
|
206
|
+
const anyData = crawl.hits > 0 ||
|
|
207
|
+
referrals.visits > 0 ||
|
|
208
|
+
(crawl.prevHits ?? 0) > 0 ||
|
|
209
|
+
(referrals.prevVisits ?? 0) > 0 ||
|
|
210
|
+
firstSeenInRange;
|
|
211
|
+
if (report.enabled && anyData) {
|
|
212
|
+
geo = {
|
|
213
|
+
crawlHits7d: crawl.hits,
|
|
214
|
+
crawlHitsPrev7d: crawl.prevHits,
|
|
215
|
+
retrievalBots: report.crawl.bots.filter((b) => b.category === 'retrieval' && b.hits > 0)
|
|
216
|
+
.length,
|
|
217
|
+
aiVisits7d: referrals.visits,
|
|
218
|
+
aiVisitsPrev7d: referrals.prevVisits,
|
|
219
|
+
topCited: report.referrals.topPaths
|
|
220
|
+
.slice(0, GEO_DIGEST_TOP)
|
|
221
|
+
.map((p) => ({ path: p.path, visits: p.visits })),
|
|
222
|
+
blockedStillCrawling: report.crawl.bots
|
|
223
|
+
.filter((b) => b.policy === 'blocked_still_crawling')
|
|
224
|
+
.map((b) => b.bot),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
console.warn('[seo/digest] geo block skipped; report failed', err);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
176
232
|
return {
|
|
177
233
|
generatedAt: now.toISOString(),
|
|
178
234
|
audit: {
|
|
@@ -202,6 +258,7 @@ export async function buildSeoDigest(db, options = {}) {
|
|
|
202
258
|
quickWins,
|
|
203
259
|
pending: { redirectSuggestions, issueFixes },
|
|
204
260
|
autopilot,
|
|
261
|
+
geo,
|
|
205
262
|
};
|
|
206
263
|
}
|
|
207
264
|
// ── Email rendering ─────────────────────────────────────────────────────────
|
|
@@ -218,6 +275,15 @@ function pct(current, prev) {
|
|
|
218
275
|
const change = Math.round(((current - prev) / prev) * 100);
|
|
219
276
|
return `${change >= 0 ? '+' : ''}${change}%`;
|
|
220
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Period-over-period delta for the GEO block: `prev === null` means the prior
|
|
280
|
+
* window is not comparable (rendered as "—"); a zero prior window has no
|
|
281
|
+
* meaningful percentage and renders nothing.
|
|
282
|
+
*/
|
|
283
|
+
function geoDelta(current, prev) {
|
|
284
|
+
return prev === null ? '—' : pct(current, prev);
|
|
285
|
+
}
|
|
286
|
+
const plural = (n, one, many = `${one}s`) => (n === 1 ? one : many);
|
|
221
287
|
function fmtDate(iso) {
|
|
222
288
|
return new Date(iso).toISOString().slice(0, 10);
|
|
223
289
|
}
|
|
@@ -231,7 +297,7 @@ export function seoAdminUrl() {
|
|
|
231
297
|
return `${siteUrl.replace(/\/$/, '')}${adminPath}/seo`;
|
|
232
298
|
}
|
|
233
299
|
export function renderSeoDigestEmail(digest, opts) {
|
|
234
|
-
const { audit, search, newIssues, quickWins, pending, autopilot } = digest;
|
|
300
|
+
const { audit, search, newIssues, quickWins, pending, autopilot, geo } = digest;
|
|
235
301
|
const alert = audit.stale || audit.lastRunFailed;
|
|
236
302
|
const deltaPart = audit.delta !== null ? ` (${audit.delta >= 0 ? '+' : ''}${audit.delta})` : '';
|
|
237
303
|
const winsPart = `${quickWins.length} quick win${quickWins.length === 1 ? '' : 's'}`;
|
|
@@ -261,6 +327,26 @@ export function renderSeoDigestEmail(digest, opts) {
|
|
|
261
327
|
<p style="margin:0;font-size:14px;color:#374151;">Clicks: <strong>${search.clicks7d}</strong>${clicksPct ? ` (${clicksPct})` : ''} · Impressions: <strong>${search.impressions7d}</strong>${imprPct ? ` (${imprPct})` : ''}</p>`);
|
|
262
328
|
textParts.push(`Search (7d): ${search.clicks7d} clicks${clicksPct ? ` (${clicksPct})` : ''}, ${search.impressions7d} impressions${imprPct ? ` (${imprPct})` : ''}`);
|
|
263
329
|
}
|
|
330
|
+
if (geo) {
|
|
331
|
+
const crawlPct = geoDelta(geo.crawlHits7d, geo.crawlHitsPrev7d);
|
|
332
|
+
const visitsPct = geoDelta(geo.aiVisits7d, geo.aiVisitsPrev7d);
|
|
333
|
+
const crawlPart = crawlPct ? ` (${crawlPct})` : '';
|
|
334
|
+
const visitsPart = visitsPct ? ` (${visitsPct})` : '';
|
|
335
|
+
const botsPart = `${geo.retrievalBots} retrieval ${plural(geo.retrievalBots, 'bot')}`;
|
|
336
|
+
const hitsUnit = `crawler ${plural(geo.crawlHits7d, 'hit')}`;
|
|
337
|
+
const visitsUnit = `AI referral ${plural(geo.aiVisits7d, 'visit')}`;
|
|
338
|
+
// Paths and bot names originate from the public beacon — always escape.
|
|
339
|
+
const cited = geo.topCited
|
|
340
|
+
.map((c) => `<li style="margin:0 0 4px;font-size:14px;color:#374151;">${escapeHtml(c.path)} — ${c.visits} ${plural(c.visits, 'visit')}</li>`)
|
|
341
|
+
.join('');
|
|
342
|
+
const blockedLine = geo.blockedStillCrawling.length > 0
|
|
343
|
+
? `<p style="margin:8px 0 0;font-size:14px;color:#92400e;">Blocked in robots.txt but still crawling: ${escapeHtml(geo.blockedStillCrawling.join(', '))}.</p>`
|
|
344
|
+
: '';
|
|
345
|
+
sections.push(`<h3 style="margin:20px 0 8px;font-size:13px;font-weight:600;color:#555;text-transform:uppercase;letter-spacing:0.04em;">AI visibility (7 days)</h3>
|
|
346
|
+
<p style="margin:0;font-size:14px;color:#374151;"><strong>${geo.crawlHits7d}</strong> ${hitsUnit}${crawlPart} · ${botsPart} · <strong>${geo.aiVisits7d}</strong> ${visitsUnit}${visitsPart} (${link('?tab=ai-visibility', 'view trends →')})</p>
|
|
347
|
+
${cited ? `<ul style="margin:8px 0 0;padding-left:20px;">${cited}</ul>` : ''}${blockedLine}`);
|
|
348
|
+
textParts.push(`AI visibility: ${geo.crawlHits7d} ${hitsUnit}${crawlPart}, ${botsPart}, ${geo.aiVisits7d} ${visitsUnit}${visitsPart}${geo.blockedStillCrawling.length > 0 ? `\nBlocked but still crawling: ${geo.blockedStillCrawling.join(', ')}` : ''}`);
|
|
349
|
+
}
|
|
264
350
|
if (quickWins.length > 0) {
|
|
265
351
|
const rows = quickWins
|
|
266
352
|
.map((w) => `<li style="margin:0 0 6px;font-size:14px;color:#374151;"><strong>${escapeHtml(w.title)}</strong> — ${escapeHtml(w.explanation)} <span style="color:#9ca3af;">(${escapeHtml(w.impact)} impact)</span></li>`)
|