@actuate-media/cms-core 0.28.0 → 0.29.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.
Files changed (86) hide show
  1. package/dist/__tests__/diagnostics/env.test.js +18 -0
  2. package/dist/__tests__/diagnostics/env.test.js.map +1 -1
  3. package/dist/__tests__/media/seo.test.d.ts +2 -0
  4. package/dist/__tests__/media/seo.test.d.ts.map +1 -0
  5. package/dist/__tests__/media/seo.test.js +186 -0
  6. package/dist/__tests__/media/seo.test.js.map +1 -0
  7. package/dist/__tests__/redirects/graph.test.d.ts +2 -0
  8. package/dist/__tests__/redirects/graph.test.d.ts.map +1 -0
  9. package/dist/__tests__/redirects/graph.test.js +104 -0
  10. package/dist/__tests__/redirects/graph.test.js.map +1 -0
  11. package/dist/__tests__/redirects/suggest.test.d.ts +2 -0
  12. package/dist/__tests__/redirects/suggest.test.d.ts.map +1 -0
  13. package/dist/__tests__/redirects/suggest.test.js +28 -0
  14. package/dist/__tests__/redirects/suggest.test.js.map +1 -0
  15. package/dist/__tests__/seo/audit-engine.test.d.ts +2 -0
  16. package/dist/__tests__/seo/audit-engine.test.d.ts.map +1 -0
  17. package/dist/__tests__/seo/audit-engine.test.js +152 -0
  18. package/dist/__tests__/seo/audit-engine.test.js.map +1 -0
  19. package/dist/__tests__/seo/config-store.test.js +24 -160
  20. package/dist/__tests__/seo/config-store.test.js.map +1 -1
  21. package/dist/__tests__/seo/score.test.d.ts +2 -0
  22. package/dist/__tests__/seo/score.test.d.ts.map +1 -0
  23. package/dist/__tests__/seo/score.test.js +152 -0
  24. package/dist/__tests__/seo/score.test.js.map +1 -0
  25. package/dist/__tests__/setup/index.test.d.ts +2 -0
  26. package/dist/__tests__/setup/index.test.d.ts.map +1 -0
  27. package/dist/__tests__/setup/index.test.js +82 -0
  28. package/dist/__tests__/setup/index.test.js.map +1 -0
  29. package/dist/api/handlers.d.ts.map +1 -1
  30. package/dist/api/handlers.js +2095 -228
  31. package/dist/api/handlers.js.map +1 -1
  32. package/dist/diagnostics/env.d.ts.map +1 -1
  33. package/dist/diagnostics/env.js +9 -5
  34. package/dist/diagnostics/env.js.map +1 -1
  35. package/dist/index.d.ts +4 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +4 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/media/index.d.ts +2 -0
  40. package/dist/media/index.d.ts.map +1 -1
  41. package/dist/media/index.js +1 -0
  42. package/dist/media/index.js.map +1 -1
  43. package/dist/media/seo.d.ts +88 -0
  44. package/dist/media/seo.d.ts.map +1 -0
  45. package/dist/media/seo.js +215 -0
  46. package/dist/media/seo.js.map +1 -0
  47. package/dist/redirects/graph.d.ts +68 -0
  48. package/dist/redirects/graph.d.ts.map +1 -0
  49. package/dist/redirects/graph.js +136 -0
  50. package/dist/redirects/graph.js.map +1 -0
  51. package/dist/redirects/index.d.ts +5 -0
  52. package/dist/redirects/index.d.ts.map +1 -0
  53. package/dist/redirects/index.js +3 -0
  54. package/dist/redirects/index.js.map +1 -0
  55. package/dist/redirects/suggest.d.ts +21 -0
  56. package/dist/redirects/suggest.d.ts.map +1 -0
  57. package/dist/redirects/suggest.js +55 -0
  58. package/dist/redirects/suggest.js.map +1 -0
  59. package/dist/seo/audit-engine.d.ts +119 -0
  60. package/dist/seo/audit-engine.d.ts.map +1 -0
  61. package/dist/seo/audit-engine.js +402 -0
  62. package/dist/seo/audit-engine.js.map +1 -0
  63. package/dist/seo/audit-runner.d.ts +41 -0
  64. package/dist/seo/audit-runner.d.ts.map +1 -0
  65. package/dist/seo/audit-runner.js +223 -0
  66. package/dist/seo/audit-runner.js.map +1 -0
  67. package/dist/seo/config-store.d.ts +28 -0
  68. package/dist/seo/config-store.d.ts.map +1 -1
  69. package/dist/seo/config-store.js +22 -0
  70. package/dist/seo/config-store.js.map +1 -1
  71. package/dist/seo/index.d.ts +7 -2
  72. package/dist/seo/index.d.ts.map +1 -1
  73. package/dist/seo/index.js +4 -1
  74. package/dist/seo/index.js.map +1 -1
  75. package/dist/seo/score.d.ts +128 -0
  76. package/dist/seo/score.d.ts.map +1 -0
  77. package/dist/seo/score.js +213 -0
  78. package/dist/seo/score.js.map +1 -0
  79. package/dist/setup/index.d.ts +16 -1
  80. package/dist/setup/index.d.ts.map +1 -1
  81. package/dist/setup/index.js +52 -2
  82. package/dist/setup/index.js.map +1 -1
  83. package/package.json +1 -1
  84. package/prisma/migrations/0008_media_center/migration.sql +47 -0
  85. package/prisma/migrations/0009_seo/migration.sql +143 -0
  86. package/prisma/schema.prisma +183 -8
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Redirect graph utilities — pure functions for chain/loop detection,
3
+ * loop-prevention on write, path normalization, and stale detection.
4
+ *
5
+ * Lives in cms-core (not plugin-redirects) because the API layer needs it and
6
+ * the dependency direction forbids core importing a plugin. The plugin's
7
+ * request-time middleware can stay independent.
8
+ */
9
+ /**
10
+ * Normalize a path for matching: drop query/hash, ensure a single leading
11
+ * slash, strip a trailing slash (except root), and lowercase. External
12
+ * absolute URLs are returned lowercased without a forced leading slash.
13
+ */
14
+ export function normalizeRedirectPath(input) {
15
+ if (!input)
16
+ return '/';
17
+ let p = String(input).trim();
18
+ if (/^https?:\/\//i.test(p))
19
+ return p.toLowerCase();
20
+ // Strip query + hash.
21
+ p = p.split('#')[0].split('?')[0];
22
+ if (!p.startsWith('/'))
23
+ p = `/${p}`;
24
+ if (p.length > 1 && p.endsWith('/'))
25
+ p = p.slice(0, -1);
26
+ return p.toLowerCase();
27
+ }
28
+ /** True when a redirect points to an external (absolute) destination. */
29
+ export function isExternal(to) {
30
+ return /^https?:\/\//i.test(to.trim());
31
+ }
32
+ /** Build a normalized from -> edge map of enabled rules. */
33
+ export function buildEdgeMap(rules) {
34
+ const map = new Map();
35
+ for (const r of rules) {
36
+ if (r.enabled === false)
37
+ continue;
38
+ const fromNorm = normalizeRedirectPath(r.from);
39
+ if (map.has(fromNorm))
40
+ continue; // first rule wins (priority handled upstream)
41
+ map.set(fromNorm, { to: r.to, toNorm: normalizeRedirectPath(r.to), id: r.id });
42
+ }
43
+ return map;
44
+ }
45
+ /**
46
+ * Follow the redirect map from `start` until a path with no further mapping
47
+ * (terminal) or a cycle is reached. Bounded by `maxHops`.
48
+ */
49
+ export function resolveChain(map, start, maxHops = 10) {
50
+ const startNorm = normalizeRedirectPath(start);
51
+ const hops = [startNorm];
52
+ const seen = new Set([startNorm]);
53
+ let current = startNorm;
54
+ for (let i = 0; i < maxHops; i++) {
55
+ const edge = map.get(current);
56
+ if (!edge) {
57
+ return { hops, terminal: current, loop: false };
58
+ }
59
+ const next = edge.toNorm;
60
+ hops.push(next);
61
+ if (seen.has(next)) {
62
+ return { hops, terminal: next, loop: true };
63
+ }
64
+ seen.add(next);
65
+ current = next;
66
+ }
67
+ return { hops, terminal: current, loop: false };
68
+ }
69
+ /**
70
+ * Detect multi-hop chains (3+ nodes => 2+ redirects) and loops across the
71
+ * rule set. Returns one entry per chain/loop keyed by a stable id derived from
72
+ * the starting source.
73
+ */
74
+ export function detectChainsAndLoops(rules) {
75
+ const map = buildEdgeMap(rules);
76
+ const results = [];
77
+ const reported = new Set();
78
+ for (const [from] of map) {
79
+ if (reported.has(from))
80
+ continue;
81
+ const res = resolveChain(map, from);
82
+ if (res.loop) {
83
+ // Only report loops once per cycle (mark all hops reported).
84
+ res.hops.forEach((h) => reported.add(h));
85
+ const breakTo = res.hops.find((h) => h !== from && h !== res.terminal) ?? res.terminal;
86
+ results.push({
87
+ id: `loop:${from}`,
88
+ kind: 'loop',
89
+ hops: res.hops,
90
+ suggestedFrom: from,
91
+ suggestedTo: breakTo,
92
+ });
93
+ }
94
+ else if (res.hops.length >= 3) {
95
+ // Mark intermediate sources reported so we don't emit sub-chains.
96
+ res.hops.slice(0, -1).forEach((h) => reported.add(h));
97
+ results.push({
98
+ id: `chain:${from}`,
99
+ kind: 'chain',
100
+ hops: res.hops,
101
+ suggestedFrom: from,
102
+ suggestedTo: res.terminal,
103
+ });
104
+ }
105
+ }
106
+ return results;
107
+ }
108
+ /**
109
+ * Would adding (or updating to) `from -> to` create a loop or self-redirect?
110
+ * Simulates the new edge over the existing rule set.
111
+ */
112
+ export function wouldCreateLoop(rules, from, to, ignoreId) {
113
+ const fromNorm = normalizeRedirectPath(from);
114
+ const toNorm = normalizeRedirectPath(to);
115
+ if (fromNorm === toNorm)
116
+ return true;
117
+ if (isExternal(to))
118
+ return false; // external targets can't loop internally
119
+ const others = rules.filter((r) => r.id !== ignoreId);
120
+ const map = buildEdgeMap(others);
121
+ map.set(fromNorm, { to, toNorm, id: ignoreId ?? '__new__' });
122
+ const res = resolveChain(map, fromNorm);
123
+ return res.loop;
124
+ }
125
+ const STALE_DAYS = 60;
126
+ /** A redirect is stale when it hasn't been hit in 60+ days. */
127
+ export function isRedirectStale(lastHitAt, now = new Date()) {
128
+ if (!lastHitAt)
129
+ return true;
130
+ const last = typeof lastHitAt === 'string' ? new Date(lastHitAt) : lastHitAt;
131
+ if (Number.isNaN(last.getTime()))
132
+ return true;
133
+ const ageMs = now.getTime() - last.getTime();
134
+ return ageMs > STALE_DAYS * 24 * 60 * 60 * 1000;
135
+ }
136
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/redirects/graph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { normalizeRedirectPath, isExternal, buildEdgeMap, resolveChain, detectChainsAndLoops, wouldCreateLoop, isRedirectStale, } from './graph.js';
2
+ export type { RedirectRecord, ChainResolution, DetectedChain } from './graph.js';
3
+ export { suggestRedirectTarget } from './suggest.js';
4
+ export type { SuggestCandidate, RedirectSuggestionResult } from './suggest.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/redirects/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEhF,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAEpD,YAAY,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { normalizeRedirectPath, isExternal, buildEdgeMap, resolveChain, detectChainsAndLoops, wouldCreateLoop, isRedirectStale, } from './graph.js';
2
+ export { suggestRedirectTarget } from './suggest.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/redirects/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAA;AAInB,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 404 recovery suggestions — pure heuristic matching of a dead path to the
3
+ * best live URL using path-segment + title token similarity. Used as the
4
+ * always-available baseline; the API can layer AI ranking on top when a
5
+ * provider is configured.
6
+ */
7
+ export interface SuggestCandidate {
8
+ url: string;
9
+ title?: string;
10
+ }
11
+ export interface RedirectSuggestionResult {
12
+ toPath: string;
13
+ confidence: number;
14
+ reason: string;
15
+ }
16
+ /**
17
+ * Suggest the best redirect target for a dead `fromPath`. Returns null when no
18
+ * candidate clears the minimum confidence threshold (default 30).
19
+ */
20
+ export declare function suggestRedirectTarget(fromPath: string, candidates: SuggestCandidate[], minConfidence?: number): RedirectSuggestionResult | null;
21
+ //# sourceMappingURL=suggest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../../src/redirects/suggest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;CACf;AAmBD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,gBAAgB,EAAE,EAC9B,aAAa,SAAK,GACjB,wBAAwB,GAAG,IAAI,CA0BjC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 404 recovery suggestions — pure heuristic matching of a dead path to the
3
+ * best live URL using path-segment + title token similarity. Used as the
4
+ * always-available baseline; the API can layer AI ranking on top when a
5
+ * provider is configured.
6
+ */
7
+ function tokens(input) {
8
+ return input
9
+ .toLowerCase()
10
+ .replace(/^https?:\/\/[^/]+/i, '')
11
+ .replace(/[^a-z0-9]+/g, ' ')
12
+ .split(/\s+/)
13
+ .filter((t) => t.length > 1);
14
+ }
15
+ function jaccard(a, b) {
16
+ if (a.size === 0 && b.size === 0)
17
+ return 0;
18
+ let inter = 0;
19
+ for (const x of a)
20
+ if (b.has(x))
21
+ inter++;
22
+ const union = a.size + b.size - inter;
23
+ return union === 0 ? 0 : inter / union;
24
+ }
25
+ /**
26
+ * Suggest the best redirect target for a dead `fromPath`. Returns null when no
27
+ * candidate clears the minimum confidence threshold (default 30).
28
+ */
29
+ export function suggestRedirectTarget(fromPath, candidates, minConfidence = 30) {
30
+ const fromTokens = new Set(tokens(fromPath));
31
+ if (fromTokens.size === 0 || candidates.length === 0)
32
+ return null;
33
+ let best = null;
34
+ for (const c of candidates) {
35
+ const pathTokens = new Set(tokens(c.url));
36
+ const titleTokens = new Set(tokens(c.title ?? ''));
37
+ const pathSim = jaccard(fromTokens, pathTokens);
38
+ const titleSim = jaccard(fromTokens, titleTokens);
39
+ // Path match weighted higher than title match.
40
+ const score = Math.round((pathSim * 0.7 + titleSim * 0.3) * 100);
41
+ if (!best || score > best.confidence) {
42
+ best = {
43
+ toPath: c.url,
44
+ confidence: score,
45
+ reason: pathSim >= titleSim
46
+ ? `URL path similarity to ${c.url}`
47
+ : `Title similarity to "${c.title ?? c.url}"`,
48
+ };
49
+ }
50
+ }
51
+ if (!best || best.confidence < minConfidence)
52
+ return null;
53
+ return best;
54
+ }
55
+ //# sourceMappingURL=suggest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggest.js","sourceRoot":"","sources":["../../src/redirects/suggest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAaH,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,KAAK;SACT,WAAW,EAAE;SACb,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC;SACjC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AAChC,CAAC;AAED,SAAS,OAAO,CAAC,CAAc,EAAE,CAAc;IAC7C,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAC1C,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,KAAK,EAAE,CAAA;IACxC,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,GAAG,KAAK,CAAA;IACrC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,KAAK,CAAA;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAAgB,EAChB,UAA8B,EAC9B,aAAa,GAAG,EAAE;IAElB,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5C,IAAI,UAAU,CAAC,IAAI,KAAK,CAAC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEjE,IAAI,IAAI,GAAoC,IAAI,CAAA;IAChD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QACzC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAA;QAClD,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,UAAU,CAAC,CAAA;QAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC,CAAA;QACjD,+CAA+C;QAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,GAAG,GAAG,QAAQ,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAA;QAChE,IAAI,CAAC,IAAI,IAAI,KAAK,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACrC,IAAI,GAAG;gBACL,MAAM,EAAE,CAAC,CAAC,GAAG;gBACb,UAAU,EAAE,KAAK;gBACjB,MAAM,EACJ,OAAO,IAAI,QAAQ;oBACjB,CAAC,CAAC,0BAA0B,CAAC,CAAC,GAAG,EAAE;oBACnC,CAAC,CAAC,wBAAwB,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,GAAG;aAClD,CAAA;QACH,CAAC;IACH,CAAC;IAED,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,GAAG,aAAa;QAAE,OAAO,IAAI,CAAA;IACzD,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * SEO audit engine.
3
+ *
4
+ * Pure analysis: given a set of content entities (pages/posts) plus optional
5
+ * cross-cutting signals (broken links, redirect chains/loops, sitemap/robots
6
+ * health), it produces:
7
+ * - per-page SEO scores (via `calculatePageSeoScore`),
8
+ * - a flat list of detected issues with stable `issueKey`s,
9
+ * - an explainable site score (via `calculateSiteSeoScore`),
10
+ * - issue counts by severity, and
11
+ * - priority-ranked quick wins.
12
+ *
13
+ * The handler layer fetches the data, calls `runSeoAuditAnalysis`, and
14
+ * snapshots the result into `SeoAuditRun` / `SeoIssue` rows. Keeping detection
15
+ * pure means it is fully unit-testable with no DB.
16
+ */
17
+ import { gradeForSeoScore, type SeoGrade, type SiteSeoScoreResult } from './score.js';
18
+ export type SeoSeverity = 'critical' | 'warning' | 'info';
19
+ export type SeoIssueCategory = 'content' | 'technical' | 'redirects' | 'media' | 'performance' | 'schema' | 'social' | 'indexing';
20
+ export interface AuditEntityInput {
21
+ entityType: 'page' | 'post';
22
+ entityId: string;
23
+ title: string;
24
+ url: string;
25
+ content?: string;
26
+ metaTitle?: string | null;
27
+ metaDescription?: string | null;
28
+ focusKeyword?: string | null;
29
+ canonicalUrl?: string | null;
30
+ ogTitle?: string | null;
31
+ ogDescription?: string | null;
32
+ ogImage?: string | null;
33
+ structuredDataType?: string | null;
34
+ noindex?: boolean | null;
35
+ shouldBeIndexable?: boolean;
36
+ wordCount?: number | null;
37
+ }
38
+ export interface AuditExtraSignals {
39
+ /** Broken internal links discovered by the link checker. */
40
+ brokenLinks?: {
41
+ url: string;
42
+ fromEntityId: string;
43
+ fromTitle: string;
44
+ fromUrl: string;
45
+ }[];
46
+ /** Pages flagged as slow by an external performance source. */
47
+ slowPages?: {
48
+ entityId: string;
49
+ title: string;
50
+ url: string;
51
+ note?: string;
52
+ }[];
53
+ redirectChains?: number;
54
+ redirectLoops?: number;
55
+ sitemapEnabled?: boolean;
56
+ robotsEnabled?: boolean;
57
+ }
58
+ export interface DetectedIssue {
59
+ issueKey: string;
60
+ entityType: string | null;
61
+ entityId: string | null;
62
+ entityTitle: string | null;
63
+ url: string | null;
64
+ severity: SeoSeverity;
65
+ category: SeoIssueCategory;
66
+ type: string;
67
+ title: string;
68
+ description: string;
69
+ impact: string;
70
+ recommendation: string;
71
+ fixable: boolean;
72
+ fixActionType: string | null;
73
+ relatedEntityIds: string[];
74
+ }
75
+ export interface AuditPageRecord {
76
+ entityType: 'page' | 'post';
77
+ entityId: string;
78
+ title: string;
79
+ url: string;
80
+ score: number;
81
+ grade: SeoGrade;
82
+ metaTitleSet: boolean;
83
+ metaDescriptionSet: boolean;
84
+ }
85
+ export interface QuickWin {
86
+ id: string;
87
+ title: string;
88
+ explanation: string;
89
+ impact: 'High' | 'Medium' | 'Low';
90
+ issueType: string;
91
+ fixActionType: string | null;
92
+ count: number;
93
+ }
94
+ export interface SeoAuditResult {
95
+ pages: AuditPageRecord[];
96
+ issues: DetectedIssue[];
97
+ siteScore: SiteSeoScoreResult;
98
+ counts: {
99
+ total: number;
100
+ critical: number;
101
+ warning: number;
102
+ info: number;
103
+ };
104
+ quickWins: QuickWin[];
105
+ }
106
+ /** Stable per-issue key so status carries across audit runs. */
107
+ export declare function issueKeyFor(type: string, identifier: string): string;
108
+ /**
109
+ * Run the full SEO audit analysis over a set of entities + signals.
110
+ */
111
+ export declare function runSeoAuditAnalysis(entities: AuditEntityInput[], signals?: AuditExtraSignals): SeoAuditResult;
112
+ /**
113
+ * Derive priority-ranked quick wins by grouping issues by type and ranking by
114
+ * (impact, severity, count). Never hardcoded — purely a function of detected
115
+ * issues, so the Overview "Quick Wins" card always reflects reality.
116
+ */
117
+ export declare function buildQuickWins(issues: DetectedIssue[]): QuickWin[];
118
+ export { gradeForSeoScore };
119
+ //# sourceMappingURL=audit-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-engine.d.ts","sourceRoot":"","sources":["../../src/seo/audit-engine.ts"],"names":[],"mappings":"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"}