@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,213 @@
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 { 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
+ /** Count `<h1>` tags in an HTML string (case-insensitive). */
56
+ export function countH1(html) {
57
+ const matches = html.match(/<h1[\s>]/gi);
58
+ return matches ? matches.length : 0;
59
+ }
60
+ function wordCountOf(content) {
61
+ const text = stripHtmlTags(content);
62
+ if (!text)
63
+ return 0;
64
+ return text.split(/\s+/).filter((w) => w.length > 0).length;
65
+ }
66
+ /**
67
+ * Compute a page/post SEO score (0-100) with an explainable breakdown.
68
+ *
69
+ * Points are additive per {@link PAGE_SEO_WEIGHTS}; "warn" checks earn partial
70
+ * credit. The total is clamped to 0-100 and rounded.
71
+ */
72
+ export function calculatePageSeoScore(input) {
73
+ const checks = [];
74
+ const push = (id, label, max, status, message) => {
75
+ const points = status === 'pass' ? max : status === 'warn' ? Math.round(max * 0.5) : 0;
76
+ checks.push({ id, label, points, max, status, message });
77
+ };
78
+ // -- Meta title -----------------------------------------------------------
79
+ const metaTitle = (input.metaTitle ?? '').trim();
80
+ if (metaTitle.length === 0) {
81
+ push('meta-title', 'Meta title', PAGE_SEO_WEIGHTS.metaTitlePresent, 'fail', 'No meta title set.');
82
+ push('meta-title-length', 'Meta title length', PAGE_SEO_WEIGHTS.metaTitleLength, 'fail', 'Add a 30–60 character meta title.');
83
+ }
84
+ else {
85
+ push('meta-title', 'Meta title', PAGE_SEO_WEIGHTS.metaTitlePresent, 'pass', 'Meta title is set.');
86
+ const len = metaTitle.length;
87
+ if (len >= 30 && len <= 60) {
88
+ push('meta-title-length', 'Meta title length', PAGE_SEO_WEIGHTS.metaTitleLength, 'pass', `Meta title is ${len} characters — ideal.`);
89
+ }
90
+ else {
91
+ push('meta-title-length', 'Meta title length', PAGE_SEO_WEIGHTS.metaTitleLength, 'warn', `Meta title is ${len} characters. Aim for 30–60.`);
92
+ }
93
+ }
94
+ // -- Meta description -----------------------------------------------------
95
+ const metaDesc = (input.metaDescription ?? '').trim();
96
+ if (metaDesc.length === 0) {
97
+ push('meta-description', 'Meta description', PAGE_SEO_WEIGHTS.metaDescPresent, 'fail', 'No meta description set.');
98
+ push('meta-description-length', 'Meta description length', PAGE_SEO_WEIGHTS.metaDescLength, 'fail', 'Add a 120–160 character description.');
99
+ }
100
+ else {
101
+ push('meta-description', 'Meta description', PAGE_SEO_WEIGHTS.metaDescPresent, 'pass', 'Meta description is set.');
102
+ const len = metaDesc.length;
103
+ if (len >= 120 && len <= 160) {
104
+ push('meta-description-length', 'Meta description length', PAGE_SEO_WEIGHTS.metaDescLength, 'pass', `Meta description is ${len} characters — ideal.`);
105
+ }
106
+ else {
107
+ push('meta-description-length', 'Meta description length', PAGE_SEO_WEIGHTS.metaDescLength, 'warn', `Meta description is ${len} characters. Aim for 120–160.`);
108
+ }
109
+ }
110
+ // -- Focus keyword --------------------------------------------------------
111
+ 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.');
112
+ // -- Canonical ------------------------------------------------------------
113
+ push('canonical', 'Canonical URL', PAGE_SEO_WEIGHTS.canonical, present(input.canonicalUrl) ? 'pass' : 'warn', present(input.canonicalUrl) ? 'Canonical URL is set.' : 'No canonical URL set.');
114
+ // -- H1 -------------------------------------------------------------------
115
+ const content = input.content ?? '';
116
+ const h1Count = countH1(content);
117
+ if (h1Count === 1) {
118
+ push('h1', 'H1 heading', PAGE_SEO_WEIGHTS.h1, 'pass', 'Exactly one H1 heading.');
119
+ }
120
+ else if (h1Count === 0) {
121
+ push('h1', 'H1 heading', PAGE_SEO_WEIGHTS.h1, content.length === 0 ? 'warn' : 'fail', 'No H1 heading found.');
122
+ }
123
+ else {
124
+ push('h1', 'H1 heading', PAGE_SEO_WEIGHTS.h1, 'warn', `Found ${h1Count} H1 headings. Use exactly one.`);
125
+ }
126
+ // -- Content length -------------------------------------------------------
127
+ const words = typeof input.wordCount === 'number' ? input.wordCount : wordCountOf(content);
128
+ if (words >= MIN_CONTENT_WORDS) {
129
+ push('content-length', 'Content length', PAGE_SEO_WEIGHTS.contentLength, 'pass', `${words} words — meets the ${MIN_CONTENT_WORDS}-word minimum.`);
130
+ }
131
+ else if (words >= IMPROVE_CONTENT_WORDS) {
132
+ push('content-length', 'Content length', PAGE_SEO_WEIGHTS.contentLength, 'warn', `${words} words. Aim for ${MIN_CONTENT_WORDS}+.`);
133
+ }
134
+ else {
135
+ push('content-length', 'Content length', PAGE_SEO_WEIGHTS.contentLength, 'fail', `Only ${words} words. Aim for ${MIN_CONTENT_WORDS}+.`);
136
+ }
137
+ // -- OG image -------------------------------------------------------------
138
+ 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.');
139
+ // -- OG title / description ----------------------------------------------
140
+ 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)
141
+ ? 'OG title/description set or inherited.'
142
+ : 'No OG title/description set.');
143
+ // -- Structured data ------------------------------------------------------
144
+ push('structured-data', 'Structured data', PAGE_SEO_WEIGHTS.structuredData, present(input.structuredDataType) ? 'pass' : 'warn', present(input.structuredDataType)
145
+ ? `Schema type: ${input.structuredDataType}.`
146
+ : 'No structured data type set.');
147
+ // -- Internal links -------------------------------------------------------
148
+ const broken = input.brokenInternalLinks ?? 0;
149
+ push('internal-links', 'Internal links', PAGE_SEO_WEIGHTS.internalLinks, broken === 0 ? 'pass' : 'fail', broken === 0 ? 'No broken internal links.' : `${broken} broken internal link(s).`);
150
+ // -- Indexability ---------------------------------------------------------
151
+ const shouldIndex = input.shouldBeIndexable !== false;
152
+ const accidentalNoindex = shouldIndex && input.noindex === true;
153
+ push('indexable', 'Indexability', PAGE_SEO_WEIGHTS.indexable, accidentalNoindex ? 'fail' : 'pass', accidentalNoindex
154
+ ? 'Page is set to noindex but should be indexable.'
155
+ : 'Indexability is correct.');
156
+ const earned = checks.reduce((sum, c) => sum + c.points, 0);
157
+ const score = Math.max(0, Math.min(100, Math.round(earned)));
158
+ return { score, grade: gradeForSeoScore(score), checks };
159
+ }
160
+ function clampScore(n) {
161
+ if (!Number.isFinite(n))
162
+ return 0;
163
+ return Math.max(0, Math.min(100, n));
164
+ }
165
+ /**
166
+ * Combine per-category sub-scores into a weighted 0-100 site score.
167
+ *
168
+ * Performance defaults to 70 (neutral) when not supplied, so sites without a
169
+ * connected performance source aren't unfairly penalised — the weight is still
170
+ * applied so connecting Search Console can only improve the signal's accuracy.
171
+ */
172
+ export function calculateSiteSeoScore(input) {
173
+ const categories = [
174
+ {
175
+ category: 'content',
176
+ score: clampScore(input.content),
177
+ weight: SITE_SEO_WEIGHTS.content,
178
+ contribution: 0,
179
+ },
180
+ {
181
+ category: 'technical',
182
+ score: clampScore(input.technical),
183
+ weight: SITE_SEO_WEIGHTS.technical,
184
+ contribution: 0,
185
+ },
186
+ {
187
+ category: 'linkRedirect',
188
+ score: clampScore(input.linkRedirect),
189
+ weight: SITE_SEO_WEIGHTS.linkRedirect,
190
+ contribution: 0,
191
+ },
192
+ {
193
+ category: 'structuredSocial',
194
+ score: clampScore(input.structuredSocial),
195
+ weight: SITE_SEO_WEIGHTS.structuredSocial,
196
+ contribution: 0,
197
+ },
198
+ {
199
+ category: 'performance',
200
+ score: clampScore(input.performance ?? 70),
201
+ weight: SITE_SEO_WEIGHTS.performance,
202
+ contribution: 0,
203
+ },
204
+ ];
205
+ let total = 0;
206
+ for (const c of categories) {
207
+ c.contribution = (c.score * c.weight) / 100;
208
+ total += c.contribution;
209
+ }
210
+ const score = Math.max(0, Math.min(100, Math.round(total)));
211
+ return { score, grade: gradeForSeoScore(score), breakdown: categories };
212
+ }
213
+ //# 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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"}
@@ -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 raw count query to avoid loading the full user model.
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;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,CAOtE;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAoC/F;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAgBxE"}
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"}
@@ -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 raw count query to avoid loading the full user model.
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/cms-core",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/actuate-media/actuatecms.git",
@@ -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");
@@ -0,0 +1,143 @@
1
+ -- SEO Operations Center
2
+ --
3
+ -- Adds the persistence layer for the SEO module:
4
+ -- * Redirect: runtime + management columns (enabled, priority, preserveQuery,
5
+ -- source_kind, hits, lastHitAt, createdById).
6
+ -- * SeoAuditRun: one row per audit execution (score + issue-count snapshot).
7
+ -- * SeoIssue: detected SEO problems, status carried across runs via issueKey.
8
+ -- * Redirect404Hit: aggregated 404 traffic observed by the middleware.
9
+ -- * RedirectSuggestion: confidence-scored 404 recovery / chain suggestions.
10
+ --
11
+ -- Safe to apply to a populated database:
12
+ -- * Every new Redirect column is nullable or has a default — no backfill.
13
+ -- * New tables/enums are additive; no existing column is dropped or retyped.
14
+
15
+ -- ---------------------------------------------------------------------------
16
+ -- Enums
17
+ -- ---------------------------------------------------------------------------
18
+ CREATE TYPE "SeoAuditStatus" AS ENUM ('RUNNING', 'COMPLETED', 'FAILED');
19
+ CREATE TYPE "SeoIssueSeverity" AS ENUM ('critical', 'warning', 'info');
20
+ CREATE TYPE "SeoIssueStatus" AS ENUM ('open', 'ignored', 'resolved', 'recurring');
21
+ CREATE TYPE "SeoIssueCategory" AS ENUM (
22
+ 'content', 'technical', 'redirects', 'media',
23
+ 'performance', 'schema', 'social', 'indexing'
24
+ );
25
+
26
+ -- ---------------------------------------------------------------------------
27
+ -- actuate_redirects new columns
28
+ -- ---------------------------------------------------------------------------
29
+ ALTER TABLE "actuate_redirects"
30
+ ADD COLUMN "enabled" BOOLEAN NOT NULL DEFAULT true,
31
+ ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 0,
32
+ ADD COLUMN "preserveQuery" BOOLEAN NOT NULL DEFAULT false,
33
+ ADD COLUMN "source_kind" TEXT NOT NULL DEFAULT 'manual',
34
+ ADD COLUMN "hits" INTEGER NOT NULL DEFAULT 0,
35
+ ADD COLUMN "lastHitAt" TIMESTAMP(3),
36
+ ADD COLUMN "createdById" TEXT;
37
+
38
+ CREATE INDEX "actuate_redirects_enabled_idx" ON "actuate_redirects" ("enabled");
39
+ CREATE INDEX "actuate_redirects_lastHitAt_idx" ON "actuate_redirects" ("lastHitAt");
40
+
41
+ -- ---------------------------------------------------------------------------
42
+ -- actuate_seo_audit_runs
43
+ -- ---------------------------------------------------------------------------
44
+ CREATE TABLE "actuate_seo_audit_runs" (
45
+ "id" TEXT NOT NULL,
46
+ "status" "SeoAuditStatus" NOT NULL DEFAULT 'RUNNING',
47
+ "scope" TEXT NOT NULL DEFAULT 'full',
48
+ "startedById" TEXT,
49
+ "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
50
+ "finishedAt" TIMESTAMP(3),
51
+ "siteScoreBefore" INTEGER,
52
+ "siteScoreAfter" INTEGER,
53
+ "pagesScanned" INTEGER NOT NULL DEFAULT 0,
54
+ "issuesFound" INTEGER NOT NULL DEFAULT 0,
55
+ "criticalCount" INTEGER NOT NULL DEFAULT 0,
56
+ "warningCount" INTEGER NOT NULL DEFAULT 0,
57
+ "infoCount" INTEGER NOT NULL DEFAULT 0,
58
+ "errorMessage" TEXT,
59
+ "metadata" JSONB,
60
+ CONSTRAINT "actuate_seo_audit_runs_pkey" PRIMARY KEY ("id")
61
+ );
62
+
63
+ CREATE INDEX "actuate_seo_audit_runs_status_idx" ON "actuate_seo_audit_runs" ("status");
64
+ CREATE INDEX "actuate_seo_audit_runs_startedAt_idx" ON "actuate_seo_audit_runs" ("startedAt");
65
+
66
+ -- ---------------------------------------------------------------------------
67
+ -- actuate_seo_issues
68
+ -- ---------------------------------------------------------------------------
69
+ CREATE TABLE "actuate_seo_issues" (
70
+ "id" TEXT NOT NULL,
71
+ "auditRunId" TEXT,
72
+ "issueKey" TEXT NOT NULL,
73
+ "entityType" TEXT,
74
+ "entityId" TEXT,
75
+ "entityTitle" TEXT,
76
+ "url" TEXT,
77
+ "severity" "SeoIssueSeverity" NOT NULL,
78
+ "category" "SeoIssueCategory" NOT NULL,
79
+ "type" TEXT NOT NULL,
80
+ "title" TEXT NOT NULL,
81
+ "description" TEXT,
82
+ "impact" TEXT,
83
+ "recommendation" TEXT,
84
+ "status" "SeoIssueStatus" NOT NULL DEFAULT 'open',
85
+ "fixable" BOOLEAN NOT NULL DEFAULT false,
86
+ "fixActionType" TEXT,
87
+ "relatedEntityIds" JSONB,
88
+ "source" TEXT NOT NULL DEFAULT 'audit',
89
+ "firstDetectedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
90
+ "lastDetectedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
91
+ "resolvedAt" TIMESTAMP(3),
92
+ "resolvedById" TEXT,
93
+ "metadata" JSONB,
94
+ CONSTRAINT "actuate_seo_issues_pkey" PRIMARY KEY ("id")
95
+ );
96
+
97
+ CREATE INDEX "actuate_seo_issues_auditRunId_idx" ON "actuate_seo_issues" ("auditRunId");
98
+ CREATE INDEX "actuate_seo_issues_status_idx" ON "actuate_seo_issues" ("status");
99
+ CREATE INDEX "actuate_seo_issues_severity_idx" ON "actuate_seo_issues" ("severity");
100
+ CREATE INDEX "actuate_seo_issues_category_idx" ON "actuate_seo_issues" ("category");
101
+ CREATE INDEX "actuate_seo_issues_issueKey_idx" ON "actuate_seo_issues" ("issueKey");
102
+
103
+ ALTER TABLE "actuate_seo_issues"
104
+ ADD CONSTRAINT "actuate_seo_issues_auditRunId_fkey"
105
+ FOREIGN KEY ("auditRunId") REFERENCES "actuate_seo_audit_runs"("id")
106
+ ON DELETE SET NULL ON UPDATE CASCADE;
107
+
108
+ -- ---------------------------------------------------------------------------
109
+ -- actuate_redirect_404_hits
110
+ -- ---------------------------------------------------------------------------
111
+ CREATE TABLE "actuate_redirect_404_hits" (
112
+ "id" TEXT NOT NULL,
113
+ "path" TEXT NOT NULL,
114
+ "hits" INTEGER NOT NULL DEFAULT 1,
115
+ "referrer" TEXT,
116
+ "resolvedAt" TIMESTAMP(3),
117
+ "firstSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
118
+ "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
119
+ CONSTRAINT "actuate_redirect_404_hits_pkey" PRIMARY KEY ("id")
120
+ );
121
+
122
+ CREATE UNIQUE INDEX "actuate_redirect_404_hits_path_key" ON "actuate_redirect_404_hits" ("path");
123
+ CREATE INDEX "actuate_redirect_404_hits_lastSeenAt_idx" ON "actuate_redirect_404_hits" ("lastSeenAt");
124
+
125
+ -- ---------------------------------------------------------------------------
126
+ -- actuate_redirect_suggestions
127
+ -- ---------------------------------------------------------------------------
128
+ CREATE TABLE "actuate_redirect_suggestions" (
129
+ "id" TEXT NOT NULL,
130
+ "fromPath" TEXT NOT NULL,
131
+ "toPath" TEXT NOT NULL,
132
+ "confidence" INTEGER NOT NULL DEFAULT 0,
133
+ "reason" TEXT,
134
+ "status" TEXT NOT NULL DEFAULT 'open',
135
+ "source" TEXT NOT NULL DEFAULT 'ai-404-recovery',
136
+ "metadata" JSONB,
137
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
138
+ "updatedAt" TIMESTAMP(3) NOT NULL,
139
+ CONSTRAINT "actuate_redirect_suggestions_pkey" PRIMARY KEY ("id")
140
+ );
141
+
142
+ CREATE INDEX "actuate_redirect_suggestions_status_idx" ON "actuate_redirect_suggestions" ("status");
143
+ CREATE INDEX "actuate_redirect_suggestions_fromPath_idx" ON "actuate_redirect_suggestions" ("fromPath");