@actuate-media/cms-core 0.34.0 → 0.35.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__/seo/index-status.test.d.ts +2 -0
- package/dist/__tests__/seo/index-status.test.d.ts.map +1 -0
- package/dist/__tests__/seo/index-status.test.js +140 -0
- package/dist/__tests__/seo/index-status.test.js.map +1 -0
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +71 -10
- package/dist/api/handlers.js.map +1 -1
- package/dist/seo/index-status.d.ts +56 -0
- package/dist/seo/index-status.d.ts.map +1 -0
- package/dist/seo/index-status.js +193 -0
- package/dist/seo/index-status.js.map +1 -0
- package/dist/seo/index.d.ts +2 -0
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +1 -0
- package/dist/seo/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-status.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/seo/index-status.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { computeSiteIndexStatus, getSiteIndexStatus, robotsTxtBlocksEntireSite, } from '../../seo/index-status.js';
|
|
3
|
+
import { DEFAULT_SEO_MODULE_SETTINGS, SEO_CONFIG_COLLECTION } from '../../seo/config-store.js';
|
|
4
|
+
import { setActuateConfig } from '../../config/runtime.js';
|
|
5
|
+
/** Minimal config stub — index-status only reads `seo`. */
|
|
6
|
+
function stubConfig(seo) {
|
|
7
|
+
setActuateConfig({ collections: {}, ...(seo ? { seo } : {}) });
|
|
8
|
+
}
|
|
9
|
+
function inputs(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
environment: 'production',
|
|
12
|
+
seo: { siteUrl: 'https://example.com' },
|
|
13
|
+
module: { ...DEFAULT_SEO_MODULE_SETTINGS },
|
|
14
|
+
hasSeoConfig: true,
|
|
15
|
+
lastAuditAt: null,
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
describe('robotsTxtBlocksEntireSite', () => {
|
|
20
|
+
it('detects a blanket Disallow: /', () => {
|
|
21
|
+
expect(robotsTxtBlocksEntireSite('User-agent: *\nDisallow: /')).toBe(true);
|
|
22
|
+
expect(robotsTxtBlocksEntireSite('User-agent: *\nDisallow: / # block all')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it('does not flag scoped disallow rules', () => {
|
|
25
|
+
expect(robotsTxtBlocksEntireSite('User-agent: *\nDisallow: /admin\nDisallow: /api/')).toBe(false);
|
|
26
|
+
expect(robotsTxtBlocksEntireSite('')).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('computeSiteIndexStatus', () => {
|
|
30
|
+
it('reports indexable in production with a site URL and no blocks', () => {
|
|
31
|
+
const result = computeSiteIndexStatus(inputs());
|
|
32
|
+
expect(result.status).toBe('indexable');
|
|
33
|
+
expect(result.severity).toBe('success');
|
|
34
|
+
expect(result.label).toBe('Indexable');
|
|
35
|
+
});
|
|
36
|
+
it('reports blocked in production when global noindex is on', () => {
|
|
37
|
+
const result = computeSiteIndexStatus(inputs({ seo: { siteUrl: 'https://example.com', robots: { defaultNoIndex: true } } }));
|
|
38
|
+
expect(result.status).toBe('blocked');
|
|
39
|
+
expect(result.severity).toBe('danger');
|
|
40
|
+
expect(result.checks.find((c) => c.key === 'globalNoindex')?.status).toBe('fail');
|
|
41
|
+
});
|
|
42
|
+
it('reports blocked in production when robots.txt disallows everything', () => {
|
|
43
|
+
const result = computeSiteIndexStatus(inputs({
|
|
44
|
+
module: { ...DEFAULT_SEO_MODULE_SETTINGS, robotsTxt: 'User-agent: *\nDisallow: /' },
|
|
45
|
+
}));
|
|
46
|
+
expect(result.status).toBe('blocked');
|
|
47
|
+
expect(result.severity).toBe('danger');
|
|
48
|
+
});
|
|
49
|
+
it('reports blocked in production when the robots route is disabled', () => {
|
|
50
|
+
const result = computeSiteIndexStatus(inputs({ module: { ...DEFAULT_SEO_MODULE_SETTINGS, robotsEnabled: false } }));
|
|
51
|
+
expect(result.status).toBe('blocked');
|
|
52
|
+
});
|
|
53
|
+
it('reports protected when non-production noindex guards a preview env', () => {
|
|
54
|
+
const result = computeSiteIndexStatus(inputs({
|
|
55
|
+
environment: 'preview',
|
|
56
|
+
seo: { siteUrl: 'https://preview.example.com', robots: { noIndexNonProduction: true } },
|
|
57
|
+
}));
|
|
58
|
+
expect(result.status).toBe('protected');
|
|
59
|
+
expect(result.severity).toBe('warning');
|
|
60
|
+
expect(result.label).toBe('Protected');
|
|
61
|
+
});
|
|
62
|
+
it('warns (partially blocked) when a non-production env is NOT protected', () => {
|
|
63
|
+
const result = computeSiteIndexStatus(inputs({ environment: 'preview', seo: { siteUrl: 'https://preview.example.com' } }));
|
|
64
|
+
expect(result.status).toBe('partially_blocked');
|
|
65
|
+
expect(result.severity).toBe('warning');
|
|
66
|
+
expect(result.checks.find((c) => c.key === 'nonProdNoindex')?.status).toBe('warning');
|
|
67
|
+
});
|
|
68
|
+
it('flags a missing site URL as needs-attention', () => {
|
|
69
|
+
const result = computeSiteIndexStatus(inputs({ seo: { siteUrl: '' } }));
|
|
70
|
+
expect(result.status).toBe('unknown');
|
|
71
|
+
expect(result.severity).toBe('warning');
|
|
72
|
+
expect(result.checks.find((c) => c.key === 'siteUrl')?.status).toBe('warning');
|
|
73
|
+
});
|
|
74
|
+
it('reports partially blocked when the sitemap is disabled but the site is indexable', () => {
|
|
75
|
+
const result = computeSiteIndexStatus(inputs({ module: { ...DEFAULT_SEO_MODULE_SETTINGS, sitemapEnabled: false } }));
|
|
76
|
+
expect(result.status).toBe('partially_blocked');
|
|
77
|
+
expect(result.checks.find((c) => c.key === 'sitemap')?.status).toBe('warning');
|
|
78
|
+
});
|
|
79
|
+
it('reports unknown when no SEO config exists at all', () => {
|
|
80
|
+
const result = computeSiteIndexStatus(inputs({ hasSeoConfig: false, seo: null }));
|
|
81
|
+
expect(result.status).toBe('unknown');
|
|
82
|
+
expect(result.severity).toBe('neutral');
|
|
83
|
+
});
|
|
84
|
+
it('always exposes the three SEO actions and a full set of checks', () => {
|
|
85
|
+
const result = computeSiteIndexStatus(inputs());
|
|
86
|
+
expect(result.actions.map((a) => a.label)).toEqual([
|
|
87
|
+
'Run SEO Audit',
|
|
88
|
+
'View robots.txt',
|
|
89
|
+
'View sitemap.xml',
|
|
90
|
+
]);
|
|
91
|
+
expect(result.checks.map((c) => c.key)).toEqual([
|
|
92
|
+
'environment',
|
|
93
|
+
'siteUrl',
|
|
94
|
+
'robotsBlockAll',
|
|
95
|
+
'globalNoindex',
|
|
96
|
+
'nonProdNoindex',
|
|
97
|
+
'sitemap',
|
|
98
|
+
'lastAudit',
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
it('surfaces the last audit date', () => {
|
|
102
|
+
const result = computeSiteIndexStatus(inputs({ lastAuditAt: '2026-01-02T03:04:05.000Z' }));
|
|
103
|
+
expect(result.checks.find((c) => c.key === 'lastAudit')?.value).toBe('2026-01-02');
|
|
104
|
+
expect(result.checks.find((c) => c.key === 'lastAudit')?.status).toBe('pass');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('getSiteIndexStatus', () => {
|
|
108
|
+
const origVercel = process.env.VERCEL_ENV;
|
|
109
|
+
const origNode = process.env.NODE_ENV;
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
process.env.VERCEL_ENV = origVercel;
|
|
112
|
+
process.env.NODE_ENV = origNode;
|
|
113
|
+
stubConfig();
|
|
114
|
+
});
|
|
115
|
+
it('reads effective config + last audit from the DB', async () => {
|
|
116
|
+
process.env.VERCEL_ENV = 'production';
|
|
117
|
+
stubConfig({ siteUrl: 'https://example.com' });
|
|
118
|
+
const db = {
|
|
119
|
+
document: {
|
|
120
|
+
findFirst: async ({ where }) => where.collection === SEO_CONFIG_COLLECTION ? { data: { site: {}, module: {} } } : null,
|
|
121
|
+
},
|
|
122
|
+
seoAuditRun: {
|
|
123
|
+
findFirst: async () => ({ finishedAt: new Date('2026-02-03T00:00:00.000Z') }),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const result = await getSiteIndexStatus(db);
|
|
127
|
+
expect(result.status).toBe('indexable');
|
|
128
|
+
expect(result.environment).toBe('production');
|
|
129
|
+
expect(result.lastAuditAt).toBe('2026-02-03T00:00:00.000Z');
|
|
130
|
+
});
|
|
131
|
+
it('tolerates a missing audit table', async () => {
|
|
132
|
+
process.env.VERCEL_ENV = 'production';
|
|
133
|
+
stubConfig({ siteUrl: 'https://example.com' });
|
|
134
|
+
const db = { document: { findFirst: async () => null } };
|
|
135
|
+
const result = await getSiteIndexStatus(db);
|
|
136
|
+
expect(result.lastAuditAt).toBeNull();
|
|
137
|
+
expect(result.status).toBe('indexable');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
//# sourceMappingURL=index-status.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-status.test.js","sourceRoot":"","sources":["../../../src/__tests__/seo/index-status.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AA+/B5C,iFAAiF;AACjF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AA8JD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AA+/B5C,iFAAiF;AACjF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AA8JD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AAyRD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAwtUzD"}
|
package/dist/api/handlers.js
CHANGED
|
@@ -1102,6 +1102,27 @@ async function checkRateLimitAsync(limiter, key) {
|
|
|
1102
1102
|
function clientIp(request) {
|
|
1103
1103
|
return getClientIp(request);
|
|
1104
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Compute a before/after diff of top-level scalar (string/number/boolean) keys
|
|
1107
|
+
* between a stored record and an incoming patch. Used to audit settings changes
|
|
1108
|
+
* without ever logging nested objects (which may hold secret blobs).
|
|
1109
|
+
*/
|
|
1110
|
+
function diffScalarKeys(previous, next) {
|
|
1111
|
+
const isScalar = (v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
|
|
1112
|
+
const changes = [];
|
|
1113
|
+
for (const key of Object.keys(next)) {
|
|
1114
|
+
const to = next[key];
|
|
1115
|
+
if (!isScalar(to))
|
|
1116
|
+
continue;
|
|
1117
|
+
const from = previous[key];
|
|
1118
|
+
if (isScalar(from) && from === to)
|
|
1119
|
+
continue;
|
|
1120
|
+
if (!isScalar(from) && from !== undefined)
|
|
1121
|
+
continue;
|
|
1122
|
+
changes.push({ key, from: isScalar(from) ? from : undefined, to });
|
|
1123
|
+
}
|
|
1124
|
+
return changes;
|
|
1125
|
+
}
|
|
1105
1126
|
const MAX_CONCURRENT_SESSIONS = 5;
|
|
1106
1127
|
/**
|
|
1107
1128
|
* After a successful primary-credential check, prune the user's active sessions
|
|
@@ -6886,20 +6907,18 @@ export function registerCMSRoutes(router) {
|
|
|
6886
6907
|
return accessErr;
|
|
6887
6908
|
const { generateMetaTags } = await import('../seo/meta-tags.js');
|
|
6888
6909
|
const { resolveRobotsDirectives } = await import('../seo/robots.js');
|
|
6910
|
+
const { getSeoOverrides, applySeoOverrides } = await import('../seo/config-store.js');
|
|
6889
6911
|
const data = doc.data || {};
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
const settingsData = settingsGlobal?.data && typeof settingsGlobal.data === 'object'
|
|
6895
|
-
? settingsGlobal.data
|
|
6896
|
-
: {};
|
|
6912
|
+
// Read robots defaults from the SAME effective SEO config the public
|
|
6913
|
+
// renderer (`composePageMeta`) and General Settings use — never the
|
|
6914
|
+
// legacy `settings` global — so this preview can't drift from reality.
|
|
6915
|
+
const effectiveSeo = applySeoOverrides(getActuateConfig(), await getSeoOverrides(db()))?.seo;
|
|
6897
6916
|
const robots = resolveRobotsDirectives({
|
|
6898
6917
|
pageData: data,
|
|
6899
6918
|
settings: {
|
|
6900
|
-
defaultNoIndex:
|
|
6901
|
-
defaultNoFollow:
|
|
6902
|
-
noIndexNonProduction:
|
|
6919
|
+
defaultNoIndex: effectiveSeo?.robots?.defaultNoIndex,
|
|
6920
|
+
defaultNoFollow: effectiveSeo?.robots?.defaultNoFollow,
|
|
6921
|
+
noIndexNonProduction: effectiveSeo?.robots?.noIndexNonProduction,
|
|
6903
6922
|
},
|
|
6904
6923
|
environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV,
|
|
6905
6924
|
});
|
|
@@ -7138,6 +7157,25 @@ export function registerCMSRoutes(router) {
|
|
|
7138
7157
|
return internalError(err, 'seo/config PUT');
|
|
7139
7158
|
}
|
|
7140
7159
|
});
|
|
7160
|
+
// Effective, computed crawl/index status for the General Settings card.
|
|
7161
|
+
// Reads the SAME effective SEO config the public renderer uses, so the badge
|
|
7162
|
+
// shown to editors never drifts from what search engines actually see.
|
|
7163
|
+
router.get('/seo/index-status', async (request) => {
|
|
7164
|
+
try {
|
|
7165
|
+
const auth = await requireAuth(request);
|
|
7166
|
+
if (auth.error)
|
|
7167
|
+
return auth.error;
|
|
7168
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
7169
|
+
if (roleErr)
|
|
7170
|
+
return roleErr;
|
|
7171
|
+
const { getSiteIndexStatus } = await import('../seo/index-status.js');
|
|
7172
|
+
const status = await getSiteIndexStatus(db());
|
|
7173
|
+
return json({ data: status });
|
|
7174
|
+
}
|
|
7175
|
+
catch (err) {
|
|
7176
|
+
return internalError(err, 'seo/index-status GET');
|
|
7177
|
+
}
|
|
7178
|
+
});
|
|
7141
7179
|
// ---------------------------------------------------------------------------
|
|
7142
7180
|
// Technical SEO — sitemap status, crawl settings, robots.txt editor.
|
|
7143
7181
|
// All backed by the `module` block of the SEO overrides document.
|
|
@@ -8900,7 +8938,30 @@ export function registerCMSRoutes(router) {
|
|
|
8900
8938
|
}
|
|
8901
8939
|
const body = (await request.json());
|
|
8902
8940
|
const ctx = buildActionContext(auth.session, db());
|
|
8941
|
+
const previous = await getGlobal(params.slug, ctx).catch(() => null);
|
|
8903
8942
|
const global = await updateGlobal(params.slug, body, ctx);
|
|
8943
|
+
// Audit settings changes (title/tagline/URL/locale/timezone/etc.) with a
|
|
8944
|
+
// per-key before/after diff so the audit trail records what changed and
|
|
8945
|
+
// who changed it. Scalar values only — we never log nested secret blobs.
|
|
8946
|
+
try {
|
|
8947
|
+
const changes = diffScalarKeys(previous ?? {}, body);
|
|
8948
|
+
if (changes.length > 0) {
|
|
8949
|
+
void logEvent({
|
|
8950
|
+
event: 'settings_changed',
|
|
8951
|
+
userId: auth.session.userId,
|
|
8952
|
+
ipAddress: getClientIp(request),
|
|
8953
|
+
details: {
|
|
8954
|
+
global: params.slug,
|
|
8955
|
+
source: 'database',
|
|
8956
|
+
environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? null,
|
|
8957
|
+
changes,
|
|
8958
|
+
},
|
|
8959
|
+
});
|
|
8960
|
+
}
|
|
8961
|
+
}
|
|
8962
|
+
catch {
|
|
8963
|
+
// Audit failures must never block the write.
|
|
8964
|
+
}
|
|
8904
8965
|
return json({ data: global });
|
|
8905
8966
|
}
|
|
8906
8967
|
catch (err) {
|