@actuate-media/cms-core 0.28.1 → 0.30.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/dist/__tests__/actions/seo-fields-update.test.d.ts +2 -0
- package/dist/__tests__/actions/seo-fields-update.test.d.ts.map +1 -0
- package/dist/__tests__/actions/seo-fields-update.test.js +93 -0
- package/dist/__tests__/actions/seo-fields-update.test.js.map +1 -0
- package/dist/__tests__/api/llms-txt.test.d.ts +2 -0
- package/dist/__tests__/api/llms-txt.test.d.ts.map +1 -0
- package/dist/__tests__/api/llms-txt.test.js +77 -0
- package/dist/__tests__/api/llms-txt.test.js.map +1 -0
- package/dist/__tests__/api/seo-read-access.test.d.ts +2 -0
- package/dist/__tests__/api/seo-read-access.test.d.ts.map +1 -0
- package/dist/__tests__/api/seo-read-access.test.js +101 -0
- package/dist/__tests__/api/seo-read-access.test.js.map +1 -0
- package/dist/__tests__/api/seo-summary-ssrf.test.d.ts +2 -0
- package/dist/__tests__/api/seo-summary-ssrf.test.d.ts.map +1 -0
- package/dist/__tests__/api/seo-summary-ssrf.test.js +80 -0
- package/dist/__tests__/api/seo-summary-ssrf.test.js.map +1 -0
- package/dist/__tests__/api/sitemap-pagination.test.d.ts +2 -0
- package/dist/__tests__/api/sitemap-pagination.test.d.ts.map +1 -0
- package/dist/__tests__/api/sitemap-pagination.test.js +100 -0
- package/dist/__tests__/api/sitemap-pagination.test.js.map +1 -0
- package/dist/__tests__/media/seo.test.d.ts +2 -0
- package/dist/__tests__/media/seo.test.d.ts.map +1 -0
- package/dist/__tests__/media/seo.test.js +186 -0
- package/dist/__tests__/media/seo.test.js.map +1 -0
- package/dist/__tests__/middleware.test.d.ts +2 -0
- package/dist/__tests__/middleware.test.d.ts.map +1 -0
- package/dist/__tests__/middleware.test.js +50 -0
- package/dist/__tests__/middleware.test.js.map +1 -0
- package/dist/__tests__/redirects/graph.test.d.ts +2 -0
- package/dist/__tests__/redirects/graph.test.d.ts.map +1 -0
- package/dist/__tests__/redirects/graph.test.js +104 -0
- package/dist/__tests__/redirects/graph.test.js.map +1 -0
- package/dist/__tests__/redirects/suggest.test.d.ts +2 -0
- package/dist/__tests__/redirects/suggest.test.d.ts.map +1 -0
- package/dist/__tests__/redirects/suggest.test.js +28 -0
- package/dist/__tests__/redirects/suggest.test.js.map +1 -0
- package/dist/__tests__/seo/analysis-markdown.test.d.ts +2 -0
- package/dist/__tests__/seo/analysis-markdown.test.d.ts.map +1 -0
- package/dist/__tests__/seo/analysis-markdown.test.js +57 -0
- package/dist/__tests__/seo/analysis-markdown.test.js.map +1 -0
- package/dist/__tests__/seo/audit-engine.test.d.ts +2 -0
- package/dist/__tests__/seo/audit-engine.test.d.ts.map +1 -0
- package/dist/__tests__/seo/audit-engine.test.js +152 -0
- package/dist/__tests__/seo/audit-engine.test.js.map +1 -0
- package/dist/__tests__/seo/audit-runner.test.d.ts +2 -0
- package/dist/__tests__/seo/audit-runner.test.d.ts.map +1 -0
- package/dist/__tests__/seo/audit-runner.test.js +82 -0
- package/dist/__tests__/seo/audit-runner.test.js.map +1 -0
- package/dist/__tests__/seo/config-store.test.js +24 -160
- package/dist/__tests__/seo/config-store.test.js.map +1 -1
- package/dist/__tests__/seo/internal-collections.test.d.ts +2 -0
- package/dist/__tests__/seo/internal-collections.test.d.ts.map +1 -0
- package/dist/__tests__/seo/internal-collections.test.js +27 -0
- package/dist/__tests__/seo/internal-collections.test.js.map +1 -0
- package/dist/__tests__/seo/page-meta.test.js +34 -2
- package/dist/__tests__/seo/page-meta.test.js.map +1 -1
- package/dist/__tests__/seo/robots.test.js +23 -2
- package/dist/__tests__/seo/robots.test.js.map +1 -1
- package/dist/__tests__/seo/score.test.d.ts +2 -0
- package/dist/__tests__/seo/score.test.d.ts.map +1 -0
- package/dist/__tests__/seo/score.test.js +152 -0
- package/dist/__tests__/seo/score.test.js.map +1 -0
- package/dist/__tests__/setup/index.test.d.ts +2 -0
- package/dist/__tests__/setup/index.test.d.ts.map +1 -0
- package/dist/__tests__/setup/index.test.js +82 -0
- package/dist/__tests__/setup/index.test.js.map +1 -0
- package/dist/actions.d.ts +24 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +63 -0
- package/dist/actions.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +2339 -274
- package/dist/api/handlers.js.map +1 -1
- package/dist/config/types.d.ts +7 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/media/index.d.ts +2 -0
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +1 -0
- package/dist/media/index.js.map +1 -1
- package/dist/media/seo.d.ts +88 -0
- package/dist/media/seo.d.ts.map +1 -0
- package/dist/media/seo.js +215 -0
- package/dist/media/seo.js.map +1 -0
- package/dist/middleware.js +12 -2
- package/dist/middleware.js.map +1 -1
- package/dist/redirects/graph.d.ts +68 -0
- package/dist/redirects/graph.d.ts.map +1 -0
- package/dist/redirects/graph.js +136 -0
- package/dist/redirects/graph.js.map +1 -0
- package/dist/redirects/index.d.ts +5 -0
- package/dist/redirects/index.d.ts.map +1 -0
- package/dist/redirects/index.js +3 -0
- package/dist/redirects/index.js.map +1 -0
- package/dist/redirects/suggest.d.ts +21 -0
- package/dist/redirects/suggest.d.ts.map +1 -0
- package/dist/redirects/suggest.js +55 -0
- package/dist/redirects/suggest.js.map +1 -0
- package/dist/seo/analysis.d.ts +9 -0
- package/dist/seo/analysis.d.ts.map +1 -1
- package/dist/seo/analysis.js +45 -6
- package/dist/seo/analysis.js.map +1 -1
- package/dist/seo/audit-engine.d.ts +119 -0
- package/dist/seo/audit-engine.d.ts.map +1 -0
- package/dist/seo/audit-engine.js +402 -0
- package/dist/seo/audit-engine.js.map +1 -0
- package/dist/seo/audit-runner.d.ts +47 -0
- package/dist/seo/audit-runner.d.ts.map +1 -0
- package/dist/seo/audit-runner.js +252 -0
- package/dist/seo/audit-runner.js.map +1 -0
- package/dist/seo/config-store.d.ts +28 -0
- package/dist/seo/config-store.d.ts.map +1 -1
- package/dist/seo/config-store.js +22 -0
- package/dist/seo/config-store.js.map +1 -1
- package/dist/seo/index.d.ts +8 -3
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +5 -2
- package/dist/seo/index.js.map +1 -1
- package/dist/seo/page-meta.d.ts +6 -0
- package/dist/seo/page-meta.d.ts.map +1 -1
- package/dist/seo/page-meta.js +22 -5
- package/dist/seo/page-meta.js.map +1 -1
- package/dist/seo/robots.d.ts +7 -0
- package/dist/seo/robots.d.ts.map +1 -1
- package/dist/seo/robots.js +10 -1
- package/dist/seo/robots.js.map +1 -1
- package/dist/seo/score.d.ts +132 -0
- package/dist/seo/score.d.ts.map +1 -0
- package/dist/seo/score.js +216 -0
- package/dist/seo/score.js.map +1 -0
- package/dist/setup/index.d.ts +16 -1
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/index.js +52 -2
- package/dist/setup/index.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/0008_media_center/migration.sql +47 -0
- package/prisma/migrations/0009_seo/migration.sql +143 -0
- package/prisma/schema.prisma +183 -8
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEO scoring model.
|
|
3
|
+
*
|
|
4
|
+
* Pure, dependency-light functions that turn a page/post's metadata + content
|
|
5
|
+
* into an explainable 0-100 score, and aggregate per-category signals into a
|
|
6
|
+
* site-wide score. Nothing here is persisted directly — the audit engine
|
|
7
|
+
* (Phase 2) calls these and snapshots the results into `SeoAuditRun` /
|
|
8
|
+
* `SeoIssue` rows, while the Content SEO table recomputes page scores on read.
|
|
9
|
+
*
|
|
10
|
+
* The score is intentionally NOT a mystery number: every result carries a
|
|
11
|
+
* `checks` array describing exactly which points were earned/lost and why, so
|
|
12
|
+
* the admin UI can show editors what to fix. The unit tests in
|
|
13
|
+
* `__tests__/seo/score.test.ts` are the source of truth for the weights.
|
|
14
|
+
*/
|
|
15
|
+
export type SeoGrade = 'good' | 'fair' | 'poor' | 'critical';
|
|
16
|
+
export type SeoCheckStatus = 'pass' | 'warn' | 'fail';
|
|
17
|
+
/** One explainable line item in a page score. */
|
|
18
|
+
export interface SeoScoreCheck {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
/** Points earned (0..max). */
|
|
22
|
+
points: number;
|
|
23
|
+
/** Maximum points this check contributes. */
|
|
24
|
+
max: number;
|
|
25
|
+
status: SeoCheckStatus;
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PageSeoScoreResult {
|
|
29
|
+
score: number;
|
|
30
|
+
grade: SeoGrade;
|
|
31
|
+
checks: SeoScoreCheck[];
|
|
32
|
+
}
|
|
33
|
+
/** The metadata + content subset a page score depends on. */
|
|
34
|
+
export interface PageSeoScoreInput {
|
|
35
|
+
title?: string | null;
|
|
36
|
+
slug?: string | null;
|
|
37
|
+
url?: string | null;
|
|
38
|
+
/** Raw HTML or plain content body, used for H1 + length checks. */
|
|
39
|
+
content?: string | null;
|
|
40
|
+
metaTitle?: string | null;
|
|
41
|
+
metaDescription?: string | null;
|
|
42
|
+
focusKeyword?: string | null;
|
|
43
|
+
canonicalUrl?: string | null;
|
|
44
|
+
ogTitle?: string | null;
|
|
45
|
+
ogDescription?: string | null;
|
|
46
|
+
ogImage?: string | null;
|
|
47
|
+
structuredDataType?: string | null;
|
|
48
|
+
noindex?: boolean | null;
|
|
49
|
+
/** True when this content is intended to be public/indexable. */
|
|
50
|
+
shouldBeIndexable?: boolean;
|
|
51
|
+
/** Number of internal links that failed validation, when known. */
|
|
52
|
+
brokenInternalLinks?: number;
|
|
53
|
+
/** Pre-computed word count; falls back to counting `content`. */
|
|
54
|
+
wordCount?: number | null;
|
|
55
|
+
}
|
|
56
|
+
/** Page score weights. Must sum to 100. */
|
|
57
|
+
export declare const PAGE_SEO_WEIGHTS: {
|
|
58
|
+
readonly metaTitlePresent: 10;
|
|
59
|
+
readonly metaTitleLength: 5;
|
|
60
|
+
readonly metaDescPresent: 10;
|
|
61
|
+
readonly metaDescLength: 5;
|
|
62
|
+
readonly focusKeyword: 8;
|
|
63
|
+
readonly canonical: 7;
|
|
64
|
+
readonly h1: 10;
|
|
65
|
+
readonly contentLength: 10;
|
|
66
|
+
readonly ogImage: 8;
|
|
67
|
+
readonly ogTitleDesc: 5;
|
|
68
|
+
readonly structuredData: 7;
|
|
69
|
+
readonly internalLinks: 5;
|
|
70
|
+
readonly indexable: 10;
|
|
71
|
+
};
|
|
72
|
+
/** Site score category weights (spec). Must sum to 100. */
|
|
73
|
+
export declare const SITE_SEO_WEIGHTS: {
|
|
74
|
+
readonly content: 30;
|
|
75
|
+
readonly technical: 25;
|
|
76
|
+
readonly linkRedirect: 20;
|
|
77
|
+
readonly structuredSocial: 15;
|
|
78
|
+
readonly performance: 10;
|
|
79
|
+
};
|
|
80
|
+
/** Map a 0-100 score to a grade. Aligns with the Content SEO table colours. */
|
|
81
|
+
export declare function gradeForSeoScore(score: number): SeoGrade;
|
|
82
|
+
/**
|
|
83
|
+
* Count H1 headings in content that may be HTML or Markdown (case-insensitive
|
|
84
|
+
* for HTML tags, ATX `#`/Setext `===` for Markdown), so markdown-bodied posts
|
|
85
|
+
* aren't penalised for a "missing" H1 that is actually present.
|
|
86
|
+
*/
|
|
87
|
+
export declare function countH1(html: string): number;
|
|
88
|
+
/**
|
|
89
|
+
* Compute a page/post SEO score (0-100) with an explainable breakdown.
|
|
90
|
+
*
|
|
91
|
+
* Points are additive per {@link PAGE_SEO_WEIGHTS}; "warn" checks earn partial
|
|
92
|
+
* credit. The total is clamped to 0-100 and rounded.
|
|
93
|
+
*/
|
|
94
|
+
export declare function calculatePageSeoScore(input: PageSeoScoreInput): PageSeoScoreResult;
|
|
95
|
+
export interface SiteScoreCategory {
|
|
96
|
+
/** 0-100 sub-score for this category. */
|
|
97
|
+
score: number;
|
|
98
|
+
weight: number;
|
|
99
|
+
}
|
|
100
|
+
export interface SiteSeoScoreInput {
|
|
101
|
+
/** 0-100 — metadata completeness/uniqueness across content. */
|
|
102
|
+
content: number;
|
|
103
|
+
/** 0-100 — crawl/index health (sitemap, robots, canonicals, noindex). */
|
|
104
|
+
technical: number;
|
|
105
|
+
/** 0-100 — internal link + redirect health. */
|
|
106
|
+
linkRedirect: number;
|
|
107
|
+
/** 0-100 — structured data + social metadata coverage. */
|
|
108
|
+
structuredSocial: number;
|
|
109
|
+
/** 0-100 — performance + search signals (defaults neutral when unknown). */
|
|
110
|
+
performance?: number;
|
|
111
|
+
}
|
|
112
|
+
export interface SiteSeoScoreBreakdownItem {
|
|
113
|
+
category: keyof typeof SITE_SEO_WEIGHTS;
|
|
114
|
+
score: number;
|
|
115
|
+
weight: number;
|
|
116
|
+
/** Weighted contribution to the final score. */
|
|
117
|
+
contribution: number;
|
|
118
|
+
}
|
|
119
|
+
export interface SiteSeoScoreResult {
|
|
120
|
+
score: number;
|
|
121
|
+
grade: SeoGrade;
|
|
122
|
+
breakdown: SiteSeoScoreBreakdownItem[];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Combine per-category sub-scores into a weighted 0-100 site score.
|
|
126
|
+
*
|
|
127
|
+
* Performance defaults to 70 (neutral) when not supplied, so sites without a
|
|
128
|
+
* connected performance source aren't unfairly penalised — the weight is still
|
|
129
|
+
* applied so connecting Search Console can only improve the signal's accuracy.
|
|
130
|
+
*/
|
|
131
|
+
export declare function calculateSiteSeoScore(input: SiteSeoScoreInput): SiteSeoScoreResult;
|
|
132
|
+
//# sourceMappingURL=score.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.d.ts","sourceRoot":"","sources":["../../src/seo/score.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEO scoring model.
|
|
3
|
+
*
|
|
4
|
+
* Pure, dependency-light functions that turn a page/post's metadata + content
|
|
5
|
+
* into an explainable 0-100 score, and aggregate per-category signals into a
|
|
6
|
+
* site-wide score. Nothing here is persisted directly — the audit engine
|
|
7
|
+
* (Phase 2) calls these and snapshots the results into `SeoAuditRun` /
|
|
8
|
+
* `SeoIssue` rows, while the Content SEO table recomputes page scores on read.
|
|
9
|
+
*
|
|
10
|
+
* The score is intentionally NOT a mystery number: every result carries a
|
|
11
|
+
* `checks` array describing exactly which points were earned/lost and why, so
|
|
12
|
+
* the admin UI can show editors what to fix. The unit tests in
|
|
13
|
+
* `__tests__/seo/score.test.ts` are the source of truth for the weights.
|
|
14
|
+
*/
|
|
15
|
+
import { extractHeadings, stripHtmlTags } from './analysis.js';
|
|
16
|
+
/** Page score weights. Must sum to 100. */
|
|
17
|
+
export const PAGE_SEO_WEIGHTS = {
|
|
18
|
+
metaTitlePresent: 10,
|
|
19
|
+
metaTitleLength: 5,
|
|
20
|
+
metaDescPresent: 10,
|
|
21
|
+
metaDescLength: 5,
|
|
22
|
+
focusKeyword: 8,
|
|
23
|
+
canonical: 7,
|
|
24
|
+
h1: 10,
|
|
25
|
+
contentLength: 10,
|
|
26
|
+
ogImage: 8,
|
|
27
|
+
ogTitleDesc: 5,
|
|
28
|
+
structuredData: 7,
|
|
29
|
+
internalLinks: 5,
|
|
30
|
+
indexable: 10,
|
|
31
|
+
};
|
|
32
|
+
/** Site score category weights (spec). Must sum to 100. */
|
|
33
|
+
export const SITE_SEO_WEIGHTS = {
|
|
34
|
+
content: 30,
|
|
35
|
+
technical: 25,
|
|
36
|
+
linkRedirect: 20,
|
|
37
|
+
structuredSocial: 15,
|
|
38
|
+
performance: 10,
|
|
39
|
+
};
|
|
40
|
+
const MIN_CONTENT_WORDS = 300;
|
|
41
|
+
const IMPROVE_CONTENT_WORDS = 150;
|
|
42
|
+
function present(value) {
|
|
43
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
44
|
+
}
|
|
45
|
+
/** Map a 0-100 score to a grade. Aligns with the Content SEO table colours. */
|
|
46
|
+
export function gradeForSeoScore(score) {
|
|
47
|
+
if (score >= 80)
|
|
48
|
+
return 'good';
|
|
49
|
+
if (score >= 60)
|
|
50
|
+
return 'fair';
|
|
51
|
+
if (score >= 1)
|
|
52
|
+
return 'poor';
|
|
53
|
+
return 'critical';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Count H1 headings in content that may be HTML or Markdown (case-insensitive
|
|
57
|
+
* for HTML tags, ATX `#`/Setext `===` for Markdown), so markdown-bodied posts
|
|
58
|
+
* aren't penalised for a "missing" H1 that is actually present.
|
|
59
|
+
*/
|
|
60
|
+
export function countH1(html) {
|
|
61
|
+
return extractHeadings(html, 1).length;
|
|
62
|
+
}
|
|
63
|
+
function wordCountOf(content) {
|
|
64
|
+
const text = stripHtmlTags(content);
|
|
65
|
+
if (!text)
|
|
66
|
+
return 0;
|
|
67
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Compute a page/post SEO score (0-100) with an explainable breakdown.
|
|
71
|
+
*
|
|
72
|
+
* Points are additive per {@link PAGE_SEO_WEIGHTS}; "warn" checks earn partial
|
|
73
|
+
* credit. The total is clamped to 0-100 and rounded.
|
|
74
|
+
*/
|
|
75
|
+
export function calculatePageSeoScore(input) {
|
|
76
|
+
const checks = [];
|
|
77
|
+
const push = (id, label, max, status, message) => {
|
|
78
|
+
const points = status === 'pass' ? max : status === 'warn' ? Math.round(max * 0.5) : 0;
|
|
79
|
+
checks.push({ id, label, points, max, status, message });
|
|
80
|
+
};
|
|
81
|
+
// -- Meta title -----------------------------------------------------------
|
|
82
|
+
const metaTitle = (input.metaTitle ?? '').trim();
|
|
83
|
+
if (metaTitle.length === 0) {
|
|
84
|
+
push('meta-title', 'Meta title', PAGE_SEO_WEIGHTS.metaTitlePresent, 'fail', 'No meta title set.');
|
|
85
|
+
push('meta-title-length', 'Meta title length', PAGE_SEO_WEIGHTS.metaTitleLength, 'fail', 'Add a 30–60 character meta title.');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
push('meta-title', 'Meta title', PAGE_SEO_WEIGHTS.metaTitlePresent, 'pass', 'Meta title is set.');
|
|
89
|
+
const len = metaTitle.length;
|
|
90
|
+
if (len >= 30 && len <= 60) {
|
|
91
|
+
push('meta-title-length', 'Meta title length', PAGE_SEO_WEIGHTS.metaTitleLength, 'pass', `Meta title is ${len} characters — ideal.`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
push('meta-title-length', 'Meta title length', PAGE_SEO_WEIGHTS.metaTitleLength, 'warn', `Meta title is ${len} characters. Aim for 30–60.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// -- Meta description -----------------------------------------------------
|
|
98
|
+
const metaDesc = (input.metaDescription ?? '').trim();
|
|
99
|
+
if (metaDesc.length === 0) {
|
|
100
|
+
push('meta-description', 'Meta description', PAGE_SEO_WEIGHTS.metaDescPresent, 'fail', 'No meta description set.');
|
|
101
|
+
push('meta-description-length', 'Meta description length', PAGE_SEO_WEIGHTS.metaDescLength, 'fail', 'Add a 120–160 character description.');
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
push('meta-description', 'Meta description', PAGE_SEO_WEIGHTS.metaDescPresent, 'pass', 'Meta description is set.');
|
|
105
|
+
const len = metaDesc.length;
|
|
106
|
+
if (len >= 120 && len <= 160) {
|
|
107
|
+
push('meta-description-length', 'Meta description length', PAGE_SEO_WEIGHTS.metaDescLength, 'pass', `Meta description is ${len} characters — ideal.`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
push('meta-description-length', 'Meta description length', PAGE_SEO_WEIGHTS.metaDescLength, 'warn', `Meta description is ${len} characters. Aim for 120–160.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// -- Focus keyword --------------------------------------------------------
|
|
114
|
+
push('focus-keyword', 'Focus keyword', PAGE_SEO_WEIGHTS.focusKeyword, present(input.focusKeyword) ? 'pass' : 'warn', present(input.focusKeyword) ? 'Focus keyword is set.' : 'No focus keyword set.');
|
|
115
|
+
// -- Canonical ------------------------------------------------------------
|
|
116
|
+
push('canonical', 'Canonical URL', PAGE_SEO_WEIGHTS.canonical, present(input.canonicalUrl) ? 'pass' : 'warn', present(input.canonicalUrl) ? 'Canonical URL is set.' : 'No canonical URL set.');
|
|
117
|
+
// -- H1 -------------------------------------------------------------------
|
|
118
|
+
const content = input.content ?? '';
|
|
119
|
+
const h1Count = countH1(content);
|
|
120
|
+
if (h1Count === 1) {
|
|
121
|
+
push('h1', 'H1 heading', PAGE_SEO_WEIGHTS.h1, 'pass', 'Exactly one H1 heading.');
|
|
122
|
+
}
|
|
123
|
+
else if (h1Count === 0) {
|
|
124
|
+
push('h1', 'H1 heading', PAGE_SEO_WEIGHTS.h1, content.length === 0 ? 'warn' : 'fail', 'No H1 heading found.');
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
push('h1', 'H1 heading', PAGE_SEO_WEIGHTS.h1, 'warn', `Found ${h1Count} H1 headings. Use exactly one.`);
|
|
128
|
+
}
|
|
129
|
+
// -- Content length -------------------------------------------------------
|
|
130
|
+
const words = typeof input.wordCount === 'number' ? input.wordCount : wordCountOf(content);
|
|
131
|
+
if (words >= MIN_CONTENT_WORDS) {
|
|
132
|
+
push('content-length', 'Content length', PAGE_SEO_WEIGHTS.contentLength, 'pass', `${words} words — meets the ${MIN_CONTENT_WORDS}-word minimum.`);
|
|
133
|
+
}
|
|
134
|
+
else if (words >= IMPROVE_CONTENT_WORDS) {
|
|
135
|
+
push('content-length', 'Content length', PAGE_SEO_WEIGHTS.contentLength, 'warn', `${words} words. Aim for ${MIN_CONTENT_WORDS}+.`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
push('content-length', 'Content length', PAGE_SEO_WEIGHTS.contentLength, 'fail', `Only ${words} words. Aim for ${MIN_CONTENT_WORDS}+.`);
|
|
139
|
+
}
|
|
140
|
+
// -- OG image -------------------------------------------------------------
|
|
141
|
+
push('og-image', 'Open Graph image', PAGE_SEO_WEIGHTS.ogImage, present(input.ogImage) ? 'pass' : 'warn', present(input.ogImage) ? 'OG image is set.' : 'No OG image — social shares show a placeholder.');
|
|
142
|
+
// -- OG title / description ----------------------------------------------
|
|
143
|
+
push('og-title-desc', 'Open Graph title & description', PAGE_SEO_WEIGHTS.ogTitleDesc, present(input.ogTitle) || present(input.ogDescription) ? 'pass' : 'warn', present(input.ogTitle) || present(input.ogDescription)
|
|
144
|
+
? 'OG title/description set or inherited.'
|
|
145
|
+
: 'No OG title/description set.');
|
|
146
|
+
// -- Structured data ------------------------------------------------------
|
|
147
|
+
push('structured-data', 'Structured data', PAGE_SEO_WEIGHTS.structuredData, present(input.structuredDataType) ? 'pass' : 'warn', present(input.structuredDataType)
|
|
148
|
+
? `Schema type: ${input.structuredDataType}.`
|
|
149
|
+
: 'No structured data type set.');
|
|
150
|
+
// -- Internal links -------------------------------------------------------
|
|
151
|
+
const broken = input.brokenInternalLinks ?? 0;
|
|
152
|
+
push('internal-links', 'Internal links', PAGE_SEO_WEIGHTS.internalLinks, broken === 0 ? 'pass' : 'fail', broken === 0 ? 'No broken internal links.' : `${broken} broken internal link(s).`);
|
|
153
|
+
// -- Indexability ---------------------------------------------------------
|
|
154
|
+
const shouldIndex = input.shouldBeIndexable !== false;
|
|
155
|
+
const accidentalNoindex = shouldIndex && input.noindex === true;
|
|
156
|
+
push('indexable', 'Indexability', PAGE_SEO_WEIGHTS.indexable, accidentalNoindex ? 'fail' : 'pass', accidentalNoindex
|
|
157
|
+
? 'Page is set to noindex but should be indexable.'
|
|
158
|
+
: 'Indexability is correct.');
|
|
159
|
+
const earned = checks.reduce((sum, c) => sum + c.points, 0);
|
|
160
|
+
const score = Math.max(0, Math.min(100, Math.round(earned)));
|
|
161
|
+
return { score, grade: gradeForSeoScore(score), checks };
|
|
162
|
+
}
|
|
163
|
+
function clampScore(n) {
|
|
164
|
+
if (!Number.isFinite(n))
|
|
165
|
+
return 0;
|
|
166
|
+
return Math.max(0, Math.min(100, n));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Combine per-category sub-scores into a weighted 0-100 site score.
|
|
170
|
+
*
|
|
171
|
+
* Performance defaults to 70 (neutral) when not supplied, so sites without a
|
|
172
|
+
* connected performance source aren't unfairly penalised — the weight is still
|
|
173
|
+
* applied so connecting Search Console can only improve the signal's accuracy.
|
|
174
|
+
*/
|
|
175
|
+
export function calculateSiteSeoScore(input) {
|
|
176
|
+
const categories = [
|
|
177
|
+
{
|
|
178
|
+
category: 'content',
|
|
179
|
+
score: clampScore(input.content),
|
|
180
|
+
weight: SITE_SEO_WEIGHTS.content,
|
|
181
|
+
contribution: 0,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
category: 'technical',
|
|
185
|
+
score: clampScore(input.technical),
|
|
186
|
+
weight: SITE_SEO_WEIGHTS.technical,
|
|
187
|
+
contribution: 0,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
category: 'linkRedirect',
|
|
191
|
+
score: clampScore(input.linkRedirect),
|
|
192
|
+
weight: SITE_SEO_WEIGHTS.linkRedirect,
|
|
193
|
+
contribution: 0,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
category: 'structuredSocial',
|
|
197
|
+
score: clampScore(input.structuredSocial),
|
|
198
|
+
weight: SITE_SEO_WEIGHTS.structuredSocial,
|
|
199
|
+
contribution: 0,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
category: 'performance',
|
|
203
|
+
score: clampScore(input.performance ?? 70),
|
|
204
|
+
weight: SITE_SEO_WEIGHTS.performance,
|
|
205
|
+
contribution: 0,
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
let total = 0;
|
|
209
|
+
for (const c of categories) {
|
|
210
|
+
c.contribution = (c.score * c.weight) / 100;
|
|
211
|
+
total += c.contribution;
|
|
212
|
+
}
|
|
213
|
+
const score = Math.max(0, Math.min(100, Math.round(total)));
|
|
214
|
+
return { score, grade: gradeForSeoScore(score), breakdown: categories };
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=score.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.js","sourceRoot":"","sources":["../../src/seo/score.ts"],"names":[],"mappings":"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"}
|
package/dist/setup/index.d.ts
CHANGED
|
@@ -19,9 +19,24 @@ export interface SetupResult {
|
|
|
19
19
|
error?: string;
|
|
20
20
|
userId?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Detect errors that mean the database is unreachable / not initialized,
|
|
24
|
+
* as opposed to an empty-but-healthy database.
|
|
25
|
+
*
|
|
26
|
+
* We must NOT treat these as "needs setup": doing so makes a DB outage
|
|
27
|
+
* silently render the first-run account-creation wizard, and the
|
|
28
|
+
* subsequent create-admin write then fails with an opaque 400. Surfacing
|
|
29
|
+
* the error instead lets callers return a proper 5xx and lets operators
|
|
30
|
+
* see the real cause (e.g. `ECONNREFUSED`, Prisma `P1001`).
|
|
31
|
+
*/
|
|
32
|
+
export declare function isDatabaseUnavailableError(err: unknown): boolean;
|
|
22
33
|
/**
|
|
23
34
|
* Check if the CMS needs first-run setup (no admin users exist).
|
|
24
|
-
* Uses a
|
|
35
|
+
* Uses a count query to avoid loading the full user model.
|
|
36
|
+
*
|
|
37
|
+
* A connection/initialization failure is rethrown so callers can report
|
|
38
|
+
* it as an infrastructure error — only a successful query that returns
|
|
39
|
+
* zero users counts as "setup required".
|
|
25
40
|
*/
|
|
26
41
|
export declare function checkSetupRequired(db: any): Promise<SetupStatus>;
|
|
27
42
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,OAAO,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,OAAO,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CA4BhE;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,CAQtE;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAuC/F;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAgBxE"}
|
package/dist/setup/index.js
CHANGED
|
@@ -6,16 +6,62 @@
|
|
|
6
6
|
* 2. Automated — env-var-based admin creation for CI/headless deploys
|
|
7
7
|
*/
|
|
8
8
|
import { hashPassword } from '../auth/password.js';
|
|
9
|
+
/**
|
|
10
|
+
* Detect errors that mean the database is unreachable / not initialized,
|
|
11
|
+
* as opposed to an empty-but-healthy database.
|
|
12
|
+
*
|
|
13
|
+
* We must NOT treat these as "needs setup": doing so makes a DB outage
|
|
14
|
+
* silently render the first-run account-creation wizard, and the
|
|
15
|
+
* subsequent create-admin write then fails with an opaque 400. Surfacing
|
|
16
|
+
* the error instead lets callers return a proper 5xx and lets operators
|
|
17
|
+
* see the real cause (e.g. `ECONNREFUSED`, Prisma `P1001`).
|
|
18
|
+
*/
|
|
19
|
+
export function isDatabaseUnavailableError(err) {
|
|
20
|
+
if (!err || typeof err !== 'object')
|
|
21
|
+
return false;
|
|
22
|
+
const code = err.code;
|
|
23
|
+
const name = err.name;
|
|
24
|
+
// Prisma connection/init error codes: can't reach server, timeouts,
|
|
25
|
+
// auth failures, db-does-not-exist, and the raw socket error.
|
|
26
|
+
const connectionCodes = new Set([
|
|
27
|
+
'P1000',
|
|
28
|
+
'P1001',
|
|
29
|
+
'P1002',
|
|
30
|
+
'P1003',
|
|
31
|
+
'P1008',
|
|
32
|
+
'P1017',
|
|
33
|
+
'ECONNREFUSED',
|
|
34
|
+
'ETIMEDOUT',
|
|
35
|
+
'ENOTFOUND',
|
|
36
|
+
]);
|
|
37
|
+
if (typeof code === 'string' && connectionCodes.has(code))
|
|
38
|
+
return true;
|
|
39
|
+
if (name === 'PrismaClientInitializationError')
|
|
40
|
+
return true;
|
|
41
|
+
const message = err.message;
|
|
42
|
+
if (typeof message === 'string') {
|
|
43
|
+
return (message.includes('ECONNREFUSED') ||
|
|
44
|
+
message.includes("Can't reach database server") ||
|
|
45
|
+
message.includes('Authentication failed against database'));
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
9
49
|
/**
|
|
10
50
|
* Check if the CMS needs first-run setup (no admin users exist).
|
|
11
|
-
* Uses a
|
|
51
|
+
* Uses a count query to avoid loading the full user model.
|
|
52
|
+
*
|
|
53
|
+
* A connection/initialization failure is rethrown so callers can report
|
|
54
|
+
* it as an infrastructure error — only a successful query that returns
|
|
55
|
+
* zero users counts as "setup required".
|
|
12
56
|
*/
|
|
13
57
|
export async function checkSetupRequired(db) {
|
|
14
58
|
try {
|
|
15
59
|
const userCount = await db.user.count();
|
|
16
60
|
return { setupRequired: userCount === 0, userCount };
|
|
17
61
|
}
|
|
18
|
-
catch {
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (isDatabaseUnavailableError(err))
|
|
64
|
+
throw err;
|
|
19
65
|
return { setupRequired: true, userCount: 0 };
|
|
20
66
|
}
|
|
21
67
|
}
|
|
@@ -52,6 +98,10 @@ export async function createInitialAdmin(db, input) {
|
|
|
52
98
|
return { success: true, userId: user.id };
|
|
53
99
|
}
|
|
54
100
|
catch (err) {
|
|
101
|
+
// A DB outage is an infrastructure failure, not a bad request —
|
|
102
|
+
// rethrow so the caller can return a 5xx instead of a confusing 400.
|
|
103
|
+
if (isDatabaseUnavailableError(err))
|
|
104
|
+
throw err;
|
|
55
105
|
const message = err instanceof Error ? err.message : 'Setup failed';
|
|
56
106
|
return { success: false, error: message };
|
|
57
107
|
}
|
package/dist/setup/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAmBlD
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- Media Center
|
|
2
|
+
--
|
|
3
|
+
-- Extends actuate_media with the metadata the Media Center needs: SEO fields
|
|
4
|
+
-- (caption / description / focusKeyword), provenance (originalFileName), a
|
|
5
|
+
-- normalised storage split (publicUrl / storageProvider), media processing
|
|
6
|
+
-- artefacts (thumbnailUrl / responsiveVariants / durationSeconds), a tags
|
|
7
|
+
-- bag, soft-delete (deletedAt), and a processing status.
|
|
8
|
+
--
|
|
9
|
+
-- SEO score and issues are deliberately NOT persisted — they are derived on
|
|
10
|
+
-- read by `calculateMediaSeoScore()` so they never drift from the metadata.
|
|
11
|
+
--
|
|
12
|
+
-- Safe to apply to a populated database:
|
|
13
|
+
-- * Every new column is nullable or has a default — no backfill required.
|
|
14
|
+
-- * `status` defaults to READY so existing rows are immediately valid.
|
|
15
|
+
-- * No existing column is dropped or retyped.
|
|
16
|
+
|
|
17
|
+
-- ---------------------------------------------------------------------------
|
|
18
|
+
-- MediaStatus enum
|
|
19
|
+
-- ---------------------------------------------------------------------------
|
|
20
|
+
CREATE TYPE "MediaStatus" AS ENUM (
|
|
21
|
+
'PROCESSING',
|
|
22
|
+
'READY',
|
|
23
|
+
'FAILED'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
-- ---------------------------------------------------------------------------
|
|
27
|
+
-- actuate_media new columns
|
|
28
|
+
-- ---------------------------------------------------------------------------
|
|
29
|
+
ALTER TABLE "actuate_media"
|
|
30
|
+
ADD COLUMN "originalFileName" TEXT,
|
|
31
|
+
ADD COLUMN "caption" TEXT,
|
|
32
|
+
ADD COLUMN "description" TEXT,
|
|
33
|
+
ADD COLUMN "focusKeyword" TEXT,
|
|
34
|
+
ADD COLUMN "tags" JSONB,
|
|
35
|
+
ADD COLUMN "publicUrl" TEXT,
|
|
36
|
+
ADD COLUMN "storageProvider" TEXT,
|
|
37
|
+
ADD COLUMN "durationSeconds" DOUBLE PRECISION,
|
|
38
|
+
ADD COLUMN "thumbnailUrl" TEXT,
|
|
39
|
+
ADD COLUMN "responsiveVariants" JSONB,
|
|
40
|
+
ADD COLUMN "status" "MediaStatus" NOT NULL DEFAULT 'READY',
|
|
41
|
+
ADD COLUMN "deletedAt" TIMESTAMP(3);
|
|
42
|
+
|
|
43
|
+
-- ---------------------------------------------------------------------------
|
|
44
|
+
-- Indexes backing the new query filters (soft-delete + type filtering)
|
|
45
|
+
-- ---------------------------------------------------------------------------
|
|
46
|
+
CREATE INDEX "actuate_media_deletedAt_idx" ON "actuate_media" ("deletedAt");
|
|
47
|
+
CREATE INDEX "actuate_media_mimeType_idx" ON "actuate_media" ("mimeType");
|