@actuate-media/plugin-seo 0.0.17 → 0.0.19

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 (37) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/__tests__/canonical.test.d.ts +2 -0
  3. package/dist/__tests__/canonical.test.d.ts.map +1 -0
  4. package/dist/__tests__/canonical.test.js +67 -0
  5. package/dist/__tests__/canonical.test.js.map +1 -0
  6. package/dist/__tests__/hooks.test.d.ts +2 -0
  7. package/dist/__tests__/hooks.test.d.ts.map +1 -0
  8. package/dist/__tests__/hooks.test.js +106 -0
  9. package/dist/__tests__/hooks.test.js.map +1 -0
  10. package/dist/__tests__/link-health.test.d.ts +2 -0
  11. package/dist/__tests__/link-health.test.d.ts.map +1 -0
  12. package/dist/__tests__/link-health.test.js +126 -0
  13. package/dist/__tests__/link-health.test.js.map +1 -0
  14. package/dist/__tests__/og-image.test.d.ts +2 -0
  15. package/dist/__tests__/og-image.test.d.ts.map +1 -0
  16. package/dist/__tests__/og-image.test.js +45 -0
  17. package/dist/__tests__/og-image.test.js.map +1 -0
  18. package/dist/__tests__/robots.test.d.ts +2 -0
  19. package/dist/__tests__/robots.test.d.ts.map +1 -0
  20. package/dist/__tests__/robots.test.js +27 -0
  21. package/dist/__tests__/robots.test.js.map +1 -0
  22. package/dist/__tests__/schema-composer.test.d.ts +2 -0
  23. package/dist/__tests__/schema-composer.test.d.ts.map +1 -0
  24. package/dist/__tests__/schema-composer.test.js +47 -0
  25. package/dist/__tests__/schema-composer.test.js.map +1 -0
  26. package/dist/__tests__/schema-validator.test.d.ts +2 -0
  27. package/dist/__tests__/schema-validator.test.d.ts.map +1 -0
  28. package/dist/__tests__/schema-validator.test.js +129 -0
  29. package/dist/__tests__/schema-validator.test.js.map +1 -0
  30. package/dist/__tests__/sitemap.test.d.ts +2 -0
  31. package/dist/__tests__/sitemap.test.d.ts.map +1 -0
  32. package/dist/__tests__/sitemap.test.js +173 -0
  33. package/dist/__tests__/sitemap.test.js.map +1 -0
  34. package/dist/schema/composer.d.ts.map +1 -1
  35. package/dist/schema/composer.js +5 -1
  36. package/dist/schema/composer.js.map +1 -1
  37. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @actuate-media/plugin-seo
2
2
 
3
+ ## 0.0.19
4
+
5
+ ### Patch Changes
6
+
7
+ - daf91b4: Fix `composeSchema` never coercing a bare-string nested field into a typed entity.
8
+
9
+ The generic data-copy loop already placed the raw string on the schema, so the
10
+ `!schema[field]` guard on the coercion branch was always false and the branch was
11
+ dead code — e.g. `Article.author: 'Jane'` shipped as the raw string `"Jane"`
12
+ instead of `{ '@type': 'Person', name: 'Jane' }`. A bare string on a
13
+ nested-entity field is now wrapped into the correct entity type.
14
+
15
+ ## 0.0.18
16
+
17
+ ### Patch Changes
18
+
19
+ - e25cd48: Standardize React peer ranges to `^18.0.0 || ^19.0.0`.
20
+
21
+ Plugin React peer ranges were inconsistent: `plugin-ai`, `plugin-commerce`, and
22
+ `plugin-seo` pinned `react: ^19.2.0` (excluding React 18, which `cms-core`
23
+ supports via `react >=18.0.0`), while the others used `^18.0.0 || ^19.2.0`
24
+ (excluding React 19.0/19.1). Either produced an `ERESOLVE` install failure for
25
+ consumers on the excluded versions. All bundled plugins now declare `react`
26
+ (and `react-dom` where already peered) as `^18.0.0 || ^19.0.0`.
27
+
3
28
  ## 0.0.17
4
29
 
5
30
  ### Patch Changes
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=canonical.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/canonical.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { detectDuplicates, suggestCanonical } from '../canonical/index.js';
3
+ // Gap 4: detectDuplicates groups pages by Dice-coefficient similarity >= 0.85,
4
+ // marks members assigned (single-group membership), and attaches a
5
+ // suggestedCanonical. Unrelated / below-threshold pages form no group.
6
+ describe('detectDuplicates (gap 4)', () => {
7
+ it('groups the two near-identical pages and excludes the unrelated one', () => {
8
+ const shared = 'the quick brown fox jumps over the lazy dog while the sun rises today';
9
+ const groups = detectDuplicates([
10
+ { url: '/dup-a', content: shared },
11
+ { url: '/dup-b', content: shared },
12
+ {
13
+ url: '/other',
14
+ content: 'completely unrelated marketing copy about kitchen appliances',
15
+ },
16
+ ]);
17
+ expect(groups).toHaveLength(1);
18
+ const group = groups[0];
19
+ expect(group.urls).toEqual(['/dup-a', '/dup-b']);
20
+ expect(group.similarity).toBeGreaterThanOrEqual(0.85);
21
+ // The unrelated page is not a member of any group.
22
+ expect(group.urls).not.toContain('/other');
23
+ // A canonical is chosen from the group members.
24
+ expect(group.urls).toContain(group.suggestedCanonical);
25
+ });
26
+ it('does NOT group a pair whose similarity sits just below 0.85', () => {
27
+ // Two 10-token sets sharing 8 tokens -> Dice = 2*8/(10+10) = 0.80 < 0.85.
28
+ const a = 'alpha bravo charlie delta echo foxtrot golf hotel india juliet';
29
+ const b = 'alpha bravo charlie delta echo foxtrot golf hotel kilo lima';
30
+ const groups = detectDuplicates([
31
+ { url: '/a', content: a },
32
+ { url: '/b', content: b },
33
+ ]);
34
+ expect(groups).toEqual([]);
35
+ });
36
+ it('does not re-group a page once it belongs to a group (single membership)', () => {
37
+ // Three identical pages -> a single group of all three, not overlapping pairs.
38
+ const same = 'identical paragraph repeated across three distinct urls verbatim';
39
+ const groups = detectDuplicates([
40
+ { url: '/1', content: same },
41
+ { url: '/2', content: same },
42
+ { url: '/3', content: same },
43
+ ]);
44
+ expect(groups).toHaveLength(1);
45
+ expect(groups[0].urls).toEqual(['/1', '/2', '/3']);
46
+ });
47
+ it('returns no groups when a single page has no peer to compare', () => {
48
+ expect(detectDuplicates([{ url: '/solo', content: 'lonely unique content here' }])).toEqual([]);
49
+ });
50
+ });
51
+ // Gap 5: suggestCanonical ranks by path depth, then query-string penalty, then
52
+ // length. Each tie-breaker is a distinct comparator branch.
53
+ describe('suggestCanonical (gap 5)', () => {
54
+ it('prefers the shallowest path (fewest segments)', () => {
55
+ expect(suggestCanonical({ urls: ['/a/b/c', '/a'], similarity: 1 })).toBe('/a');
56
+ });
57
+ it('penalizes a URL that carries a query string at equal depth', () => {
58
+ expect(suggestCanonical({ urls: ['/a?x=1', '/a'], similarity: 1 })).toBe('/a');
59
+ });
60
+ it('breaks a depth+query tie by shortest length', () => {
61
+ expect(suggestCanonical({ urls: ['/aaaa', '/aa'], similarity: 1 })).toBe('/aa');
62
+ });
63
+ it('falls back to empty string for an empty url list', () => {
64
+ expect(suggestCanonical({ urls: [], similarity: 1 })).toBe('');
65
+ });
66
+ });
67
+ //# sourceMappingURL=canonical.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.test.js","sourceRoot":"","sources":["../../src/__tests__/canonical.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hooks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hooks.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { seoHooks } from '../hooks.js';
3
+ import { seoPlugin } from '../index.js';
4
+ function getCache() {
5
+ return globalThis.__sitemapCache;
6
+ }
7
+ function seedCache(entries) {
8
+ ;
9
+ globalThis.__sitemapCache = new Map(entries);
10
+ }
11
+ function docPayload(overrides = {}) {
12
+ return {
13
+ id: 'p1',
14
+ collection: 'posts',
15
+ slug: 'x',
16
+ status: 'published',
17
+ data: {},
18
+ ...overrides,
19
+ };
20
+ }
21
+ afterEach(() => {
22
+ delete globalThis.__sitemapCache;
23
+ });
24
+ describe('seoHooks afterUpdate — sitemap cache invalidation', () => {
25
+ // Gap 1: the hook's ONLY side effect is deleting sitemap:{collection} from
26
+ // globalThis.__sitemapCache. Prove the write drops exactly the written
27
+ // collection's fragment and leaves siblings intact.
28
+ describe('collection fragment invalidation (gap 1)', () => {
29
+ beforeEach(() => {
30
+ seedCache([
31
+ ['sitemap:posts', { xml: '<posts/>' }],
32
+ ['sitemap:pages', { xml: '<pages/>' }],
33
+ ]);
34
+ });
35
+ it('drops only the written collection fragment, keeping siblings', async () => {
36
+ await seoHooks[0].handler(docPayload({ id: 'p1', collection: 'posts' }));
37
+ const cache = getCache();
38
+ expect(cache.has('sitemap:posts')).toBe(false);
39
+ expect(cache.has('sitemap:pages')).toBe(true);
40
+ });
41
+ it('keys off the top-level collection, not payload.data', async () => {
42
+ // `data.collection` must be ignored — only the top-level collection counts.
43
+ await seoHooks[0].handler(docPayload({ collection: 'pages', data: { collection: 'posts' } }));
44
+ const cache = getCache();
45
+ expect(cache.has('sitemap:pages')).toBe(false);
46
+ // posts fragment untouched: the hook did NOT read data.collection.
47
+ expect(cache.has('sitemap:posts')).toBe(true);
48
+ });
49
+ });
50
+ // Gap 2: when the payload has no collection, cacheKey falls back to
51
+ // `sitemap:global` via `?? 'global'`.
52
+ describe('global fallback when collection is absent (gap 2)', () => {
53
+ it("deletes 'sitemap:global' for a payload omitting collection", async () => {
54
+ seedCache([
55
+ ['sitemap:global', { xml: '<global/>' }],
56
+ ['sitemap:posts', { xml: '<posts/>' }],
57
+ ]);
58
+ // Payload deliberately omits `collection` to exercise the ?? 'global' path.
59
+ const noCollection = {
60
+ id: 'g1',
61
+ slug: 'x',
62
+ status: 'published',
63
+ data: {},
64
+ };
65
+ await seoHooks[0].handler(noCollection);
66
+ const cache = getCache();
67
+ expect(cache.has('sitemap:global')).toBe(false);
68
+ // Only the global fragment was targeted.
69
+ expect(cache.has('sitemap:posts')).toBe(true);
70
+ });
71
+ });
72
+ // Gap 3: no-op safety — the null-payload guard and the cache?.delete
73
+ // optional-chaining when __sitemapCache is undefined.
74
+ describe('no-op safety (gap 3)', () => {
75
+ it('returns undefined and does not throw on a null/undefined payload', async () => {
76
+ seedCache([['sitemap:posts', {}]]);
77
+ await expect(seoHooks[0].handler(undefined)).resolves.toBeUndefined();
78
+ // Early return means nothing was deleted.
79
+ expect(getCache().has('sitemap:posts')).toBe(true);
80
+ });
81
+ it('does not throw when __sitemapCache is undefined', async () => {
82
+ // No cache seeded — cache?.delete must short-circuit safely.
83
+ expect(getCache()).toBeUndefined();
84
+ await expect(seoHooks[0].handler({ collection: 'posts' })).resolves.toBeUndefined();
85
+ });
86
+ });
87
+ // Gap 17: pin the current contract — _pluginConfig is INERT. The hook handler
88
+ // ignores it entirely; toggling sitemap.enabled changes nothing. This makes a
89
+ // future wiring change a deliberate, test-visible decision.
90
+ describe('config is inert / not enforced (gap 17)', () => {
91
+ it('deletes the same cache key regardless of sitemap.enabled', async () => {
92
+ for (const enabled of [true, false]) {
93
+ seedCache([['sitemap:posts', {}]]);
94
+ const plugin = seoPlugin({ sitemap: { enabled } });
95
+ const hook = plugin.hooks[0];
96
+ // The config is stamped onto the hook but never read by the handler.
97
+ expect(hook._pluginConfig).toEqual({
98
+ sitemap: { enabled },
99
+ });
100
+ await hook.handler(docPayload({ collection: 'posts' }));
101
+ expect(getCache().has('sitemap:posts')).toBe(false);
102
+ }
103
+ });
104
+ });
105
+ });
106
+ //# sourceMappingURL=hooks.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.test.js","sourceRoot":"","sources":["../../src/__tests__/hooks.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=link-health.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-health.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/link-health.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,126 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ // Mock only `safeFetch`; preserve SsrfBlockedError and every other cms-core
3
+ // export so the module under test resolves normally.
4
+ vi.mock('@actuate-media/cms-core', async (importOriginal) => {
5
+ const actual = await importOriginal();
6
+ return { ...actual, safeFetch: vi.fn() };
7
+ });
8
+ import { safeFetch } from '@actuate-media/cms-core';
9
+ import { detectBrokenLinks, detectOrphanPages, detectRedirectChains } from '../link-health/index.js';
10
+ const mockSafeFetch = vi.mocked(safeFetch);
11
+ function fakeResponse(status, ok, location) {
12
+ return {
13
+ status,
14
+ ok,
15
+ headers: {
16
+ get: (name) => (name.toLowerCase() === 'location' ? (location ?? null) : null),
17
+ },
18
+ };
19
+ }
20
+ // Gap 11: status classification + URL-extraction branches.
21
+ describe('detectBrokenLinks status classification (gap 11)', () => {
22
+ beforeEach(() => {
23
+ mockSafeFetch.mockReset();
24
+ });
25
+ afterEach(() => {
26
+ vi.restoreAllMocks();
27
+ });
28
+ it('maps 2xx -> ok, 4xx/5xx -> broken, 3xx -> redirect (with redirectTarget)', async () => {
29
+ mockSafeFetch.mockImplementation(async (url) => {
30
+ if (url.includes('/ok'))
31
+ return fakeResponse(200, true);
32
+ if (url.includes('/broken'))
33
+ return fakeResponse(404, false);
34
+ if (url.includes('/redir'))
35
+ return fakeResponse(301, false, '/new-location');
36
+ throw new Error(`unexpected url ${url}`);
37
+ });
38
+ const html = '<a href="https://a.test/ok">o</a>' +
39
+ '<a href="https://b.test/broken">b</a>' +
40
+ '<a href="https://c.test/redir">r</a>';
41
+ const results = await detectBrokenLinks(html, 'https://example.com');
42
+ expect(results).toHaveLength(3);
43
+ const byUrl = Object.fromEntries(results.map((r) => [r.url, r]));
44
+ expect(byUrl['https://a.test/ok'].status).toBe('ok');
45
+ expect(byUrl['https://a.test/ok'].statusCode).toBe(200);
46
+ expect(byUrl['https://b.test/broken'].status).toBe('broken');
47
+ expect(byUrl['https://b.test/broken'].statusCode).toBe(404);
48
+ expect(byUrl['https://c.test/redir'].status).toBe('redirect');
49
+ expect(byUrl['https://c.test/redir'].statusCode).toBe(301);
50
+ expect(byUrl['https://c.test/redir'].redirectTarget).toBe('/new-location');
51
+ });
52
+ it("classifies an AbortError/TimeoutError as 'timeout' and other throws as 'error'", async () => {
53
+ mockSafeFetch.mockImplementation(async (url) => {
54
+ if (url.includes('/slow'))
55
+ throw new DOMException('aborted', 'TimeoutError');
56
+ throw new Error('connection refused');
57
+ });
58
+ const html = '<a href="https://a.test/slow">s</a><a href="https://b.test/down">d</a>';
59
+ const results = await detectBrokenLinks(html, 'https://example.com');
60
+ const byUrl = Object.fromEntries(results.map((r) => [r.url, r]));
61
+ expect(byUrl['https://a.test/slow'].status).toBe('timeout');
62
+ expect(byUrl['https://b.test/down'].status).toBe('error');
63
+ });
64
+ it('resolves relative and root-relative hrefs against the base URL', async () => {
65
+ mockSafeFetch.mockResolvedValue(fakeResponse(200, true));
66
+ const html = '<a href="/root-rel">a</a>' +
67
+ '<a href="plain-rel">b</a>' +
68
+ '<a href="https://abs.test/x">c</a>';
69
+ const results = await detectBrokenLinks(html, 'https://example.com/');
70
+ const urls = results.map((r) => r.url);
71
+ expect(urls).toContain('https://example.com/root-rel');
72
+ expect(urls).toContain('https://example.com/plain-rel');
73
+ expect(urls).toContain('https://abs.test/x');
74
+ });
75
+ it('skips #, mailto:, and tel: hrefs entirely', async () => {
76
+ const html = '<a href="#section">a</a><a href="mailto:x@y.com">b</a><a href="tel:+1000">c</a>';
77
+ const results = await detectBrokenLinks(html, 'https://example.com');
78
+ expect(results).toEqual([]);
79
+ expect(mockSafeFetch).not.toHaveBeenCalled();
80
+ });
81
+ it('dedupes repeated hrefs into a single check', async () => {
82
+ mockSafeFetch.mockResolvedValue(fakeResponse(200, true));
83
+ const html = '<a href="https://a.test/same">1</a><a href="https://a.test/same">2</a>';
84
+ const results = await detectBrokenLinks(html, 'https://example.com');
85
+ expect(results).toHaveLength(1);
86
+ expect(mockSafeFetch).toHaveBeenCalledTimes(1);
87
+ });
88
+ });
89
+ // Gap 10: detectOrphanPages normalizes (lowercase + trailing-slash strip) the
90
+ // linked set, returning pages absent from it.
91
+ describe('detectOrphanPages (gap 10)', () => {
92
+ it('treats case and trailing slash as insensitive when matching', () => {
93
+ expect(detectOrphanPages(['/a', '/b', '/c'], ['/A/', '/b'])).toEqual(['/c']);
94
+ });
95
+ it('returns all pages when nothing is linked', () => {
96
+ expect(detectOrphanPages(['/x', '/y'], [])).toEqual(['/x', '/y']);
97
+ });
98
+ });
99
+ // Gap 12: sub-chain collapse + loop reporting.
100
+ describe('detectRedirectChains collapse/loop (gap 12)', () => {
101
+ it('collapses A->B->C->D (with a redundant B->C) into one 3-hop chain', () => {
102
+ const chains = detectRedirectChains([
103
+ { from: 'A', to: 'B' },
104
+ { from: 'B', to: 'C' },
105
+ { from: 'C', to: 'D' },
106
+ { from: 'B', to: 'C' }, // redundant tail, must not re-emit a B->C->D chain
107
+ ]);
108
+ expect(chains).toHaveLength(1);
109
+ const chain = chains[0];
110
+ expect(chain.kind).toBe('chain');
111
+ expect(chain.chain).toEqual(['A', 'B', 'C', 'D']);
112
+ expect(chain.length).toBe(3);
113
+ expect(chain.finalUrl).toBe('D');
114
+ });
115
+ it('reports a 3-node cycle once as a loop', () => {
116
+ const chains = detectRedirectChains([
117
+ { from: 'A', to: 'B' },
118
+ { from: 'B', to: 'C' },
119
+ { from: 'C', to: 'A' },
120
+ ]);
121
+ expect(chains).toHaveLength(1);
122
+ expect(chains[0].kind).toBe('loop');
123
+ expect(chains[0].chain).toEqual(['A', 'B', 'C', 'A']);
124
+ });
125
+ });
126
+ //# sourceMappingURL=link-health.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-health.test.js","sourceRoot":"","sources":["../../src/__tests__/link-health.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=og-image.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"og-image.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/og-image.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateOGImage } from '../og-image/index.js';
3
+ // Gap 16: theme colors, conditional blocks, default font size, XSS escaping,
4
+ // and response headers.
5
+ describe('generateOGImage (gap 16)', () => {
6
+ it('uses the dark palette and escapes the title (XSS guard)', async () => {
7
+ const res = await generateOGImage({ title: '<script>alert(1)</script>', theme: 'dark' });
8
+ const body = await res.text();
9
+ expect(body).toContain('#0a0a0a'); // dark background
10
+ expect(body).toContain('&lt;script&gt;'); // escaped
11
+ expect(body).not.toContain('<script>'); // raw markup never injected
12
+ });
13
+ it('uses the light palette by default', async () => {
14
+ const body = await (await generateOGImage({ title: 'Hello' })).text();
15
+ expect(body).toContain('#ffffff');
16
+ });
17
+ it('renders the default 56px title font size', async () => {
18
+ const body = await (await generateOGImage({ title: 'Hello' })).text();
19
+ expect(body).toContain('font-size:56px');
20
+ });
21
+ it('honors a custom fontSize', async () => {
22
+ const body = await (await generateOGImage({ title: 'Hello', fontSize: 72 })).text();
23
+ expect(body).toContain('font-size:72px');
24
+ expect(body).not.toContain('font-size:56px');
25
+ });
26
+ it('omits the siteName and description blocks when not provided', async () => {
27
+ const body = await (await generateOGImage({ title: 'Hello' })).text();
28
+ // siteName block is 24px; description block is 28px.
29
+ expect(body).not.toContain('font-size:24px');
30
+ expect(body).not.toContain('font-size:28px');
31
+ });
32
+ it('includes siteName and description blocks when provided', async () => {
33
+ const body = await (await generateOGImage({ title: 'Hello', siteName: 'Acme', description: 'A tagline' })).text();
34
+ expect(body).toContain('font-size:24px');
35
+ expect(body).toContain('Acme');
36
+ expect(body).toContain('font-size:28px');
37
+ expect(body).toContain('A tagline');
38
+ });
39
+ it('sets HTML content-type and a day-long cache-control', async () => {
40
+ const res = await generateOGImage({ title: 'Hello' });
41
+ expect(res.headers.get('Content-Type')).toBe('text/html');
42
+ expect(res.headers.get('Cache-Control')).toContain('s-maxage=86400');
43
+ });
44
+ });
45
+ //# sourceMappingURL=og-image.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"og-image.test.js","sourceRoot":"","sources":["../../src/__tests__/og-image.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,6EAA6E;AAC7E,wBAAwB;AACxB,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;QACvE,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;QACxF,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;QAE7B,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAA,CAAC,kBAAkB;QACpD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA,CAAC,UAAU;QACnD,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,CAAA,CAAC,4BAA4B;IACrE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QACjD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACrE,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAA;IACnC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACrE,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;QACxC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACnF,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA;QACxC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA;IAC9C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACrE,qDAAqD;QACrD,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA;QAC5C,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA;IAC9C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,IAAI,GAAG,MAAM,CACjB,MAAM,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC,CACtF,CAAC,IAAI,EAAE,CAAA;QACR,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA;QACxC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;QAC9B,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA;QACxC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;IACrC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAA;QACrD,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QACzD,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA;IACtE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=robots.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/robots.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateRobotsTxt } from '../robots/index.js';
3
+ // Gap 15: default-rule fallback, crawlDelay, and sitemap line emission.
4
+ describe('generateRobotsTxt core rules (gap 15)', () => {
5
+ it('emits the default * rule when config.rules is omitted', () => {
6
+ const txt = generateRobotsTxt({});
7
+ expect(txt).toContain('User-agent: *');
8
+ expect(txt).toContain('Allow: /');
9
+ expect(txt).toContain('Disallow: /admin');
10
+ expect(txt).toContain('Disallow: /api/');
11
+ });
12
+ it('emits an explicit rule with Crawl-delay and a Sitemap line', () => {
13
+ const txt = generateRobotsTxt({
14
+ rules: [{ userAgent: 'Bot', crawlDelay: 10 }],
15
+ sitemapUrl: 'https://x.com/sitemap.xml',
16
+ });
17
+ expect(txt).toContain('User-agent: Bot');
18
+ expect(txt).toContain('Crawl-delay: 10');
19
+ expect(txt).toContain('Sitemap: https://x.com/sitemap.xml');
20
+ // Providing explicit rules replaces the default * rule.
21
+ expect(txt).not.toContain('Disallow: /admin');
22
+ });
23
+ it('omits the Sitemap line when no sitemapUrl is configured', () => {
24
+ expect(generateRobotsTxt({})).not.toContain('Sitemap:');
25
+ });
26
+ });
27
+ //# sourceMappingURL=robots.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.test.js","sourceRoot":"","sources":["../../src/__tests__/robots.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEtD,wEAAwE;AACxE,QAAQ,CAAC,uCAAuC,EAAE,GAAG,EAAE;IACrD,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,GAAG,GAAG,iBAAiB,CAAC,EAAE,CAAC,CAAA;QACjC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAA;QACtC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAA;QACjC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAA;QACzC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,GAAG,GAAG,iBAAiB,CAAC;YAC5B,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;YAC7C,UAAU,EAAE,2BAA2B;SACxC,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAA;QACxC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAA;QACxC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,oCAAoC,CAAC,CAAA;QAC3D,wDAAwD;QACxD,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,CAAA;IACzD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schema-composer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-composer.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/schema-composer.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { composeSchema } from '../schema/composer.js';
3
+ // Gap 9: composeSchema field/entity handling.
4
+ describe('composeSchema (gap 9)', () => {
5
+ it('stamps @context and @type', () => {
6
+ const s = composeSchema('Article', { headline: 'H' });
7
+ expect(s['@context']).toBe('https://schema.org');
8
+ expect(s['@type']).toBe('Article');
9
+ });
10
+ it("strips '@'-prefixed keys and undefined/null/'' values from data", () => {
11
+ const s = composeSchema('Article', {
12
+ headline: 'H',
13
+ dropped: '',
14
+ ignored: null,
15
+ alsoIgnored: undefined,
16
+ '@bad': 1,
17
+ });
18
+ expect(s.headline).toBe('H');
19
+ expect('dropped' in s).toBe(false);
20
+ expect('ignored' in s).toBe(false);
21
+ expect('alsoIgnored' in s).toBe(false);
22
+ expect('@bad' in s).toBe(false);
23
+ });
24
+ it('wraps a nested-entity object with its mapped @type (publisher -> Organization)', () => {
25
+ const s = composeSchema('Article', {}, { publisher: { name: 'Acme' } });
26
+ expect(s.publisher).toEqual({ '@type': 'Organization', name: 'Acme' });
27
+ });
28
+ it("defaults an unmapped extra nested object to @type 'Thing'", () => {
29
+ const s = composeSchema('Article', {}, { sponsor: { name: 'X' } });
30
+ expect(s.sponsor).toEqual({ '@type': 'Thing', name: 'X' });
31
+ });
32
+ it('respects an explicit @type on an unmapped nested object', () => {
33
+ const s = composeSchema('Article', {}, { sponsor: { '@type': 'Corporation', name: 'X' } });
34
+ expect(s.sponsor['@type']).toBe('Corporation');
35
+ });
36
+ it('wraps a bare string on a nested-entity field as a typed entity', () => {
37
+ // Article.author is a NESTED_ENTITY_MAP field, so a bare string is coerced
38
+ // into a Person node rather than left as raw text.
39
+ const s = composeSchema('Article', { headline: 'H', author: 'Jane' });
40
+ expect(s.author).toEqual({ '@type': 'Person', name: 'Jane' });
41
+ });
42
+ it('prefers the explicit nested override object over data for the same field', () => {
43
+ const s = composeSchema('Article', { author: 'Jane' }, { author: { name: 'Jane Doe' } });
44
+ expect(s.author).toEqual({ '@type': 'Person', name: 'Jane Doe' });
45
+ });
46
+ });
47
+ //# sourceMappingURL=schema-composer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-composer.test.js","sourceRoot":"","sources":["../../src/__tests__/schema-composer.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAErD,8CAA8C;AAC9C,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,GAAG,aAAa,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAA;QACrD,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAA;QAChD,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACpC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,CAAC,GAAG,aAAa,CAAC,SAAS,EAAE;YACjC,QAAQ,EAAE,GAAG;YACb,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,IAAI;YACb,WAAW,EAAE,SAAS;YACtB,MAAM,EAAE,CAAC;SACV,CAAC,CAAA;QACF,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC5B,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAClC,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAClC,MAAM,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACtC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACjC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gFAAgF,EAAE,GAAG,EAAE;QACxF,MAAM,CAAC,GAAG,aAAa,CAAC,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,CAAA;QACvE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;IACxE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,GAAG,aAAa,CAAC,SAAS,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,CAAA;QAClE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAA;IAC5D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,GAAG,aAAa,CAAC,SAAS,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,CAAA;QAC1F,MAAM,CAAE,CAAC,CAAC,OAAmC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IAC7E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,2EAA2E;QAC3E,mDAAmD;QACnD,MAAM,CAAC,GAAG,aAAa,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;QACrE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;IAC/D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,CAAC,GAAG,aAAa,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;QACxF,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schema-validator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-validator.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/schema-validator.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validateSchema } from '../schema/validator.js';
3
+ // Gap 6: FAQPage nested-item validation. Each mainEntity item must have
4
+ // @type 'Question', a name, and acceptedAnswer.text, else per-index errors.
5
+ describe('validateSchema FAQPage mainEntity items (gap 6)', () => {
6
+ it('flags @type, name, and acceptedAnswer.text on a malformed FAQ item', () => {
7
+ const result = validateSchema({
8
+ '@context': 'https://schema.org',
9
+ '@type': 'FAQPage',
10
+ mainEntity: [{ '@type': 'Answer' }], // wrong type, no name, no acceptedAnswer
11
+ });
12
+ expect(result.valid).toBe(false);
13
+ const fields = result.errors.map((e) => e.field);
14
+ expect(fields).toContain('mainEntity[0].@type');
15
+ expect(fields).toContain('mainEntity[0].name');
16
+ expect(fields).toContain('mainEntity[0].acceptedAnswer.text');
17
+ // These per-item findings are hard errors, not warnings.
18
+ for (const field of [
19
+ 'mainEntity[0].@type',
20
+ 'mainEntity[0].name',
21
+ 'mainEntity[0].acceptedAnswer.text',
22
+ ]) {
23
+ expect(result.errors.find((e) => e.field === field).severity).toBe('error');
24
+ }
25
+ });
26
+ it('reports the correct index for the malformed item in a multi-item list', () => {
27
+ const result = validateSchema({
28
+ '@context': 'https://schema.org',
29
+ '@type': 'FAQPage',
30
+ mainEntity: [
31
+ { '@type': 'Question', name: 'Q1?', acceptedAnswer: { '@type': 'Answer', text: 'A1' } },
32
+ { '@type': 'Question', name: 'Q2?' }, // missing acceptedAnswer.text
33
+ ],
34
+ });
35
+ const fields = result.errors.map((e) => e.field);
36
+ expect(fields).toContain('mainEntity[1].acceptedAnswer.text');
37
+ // The valid item (index 0) produces no mainEntity errors.
38
+ expect(fields.some((f) => f.startsWith('mainEntity[0]'))).toBe(false);
39
+ });
40
+ it('accepts a well-formed Question item with no mainEntity errors', () => {
41
+ const result = validateSchema({
42
+ '@context': 'https://schema.org',
43
+ '@type': 'FAQPage',
44
+ mainEntity: [
45
+ {
46
+ '@type': 'Question',
47
+ name: 'What is X?',
48
+ acceptedAnswer: { '@type': 'Answer', text: 'X is Y' },
49
+ },
50
+ ],
51
+ });
52
+ expect(result.valid).toBe(true);
53
+ expect(result.errors.some((e) => e.field.startsWith('mainEntity'))).toBe(false);
54
+ });
55
+ });
56
+ // Gap 7: error-vs-warning severity semantics. Missing RECOMMENDED fields keep
57
+ // valid:true while emitting severity 'warning'; missing REQUIRED / @type flip
58
+ // valid:false. Pins `valid = !hasErrors`.
59
+ describe('validateSchema severity semantics (gap 7)', () => {
60
+ it('stays valid:true when only recommended fields are missing (all warnings)', () => {
61
+ const result = validateSchema({
62
+ '@context': 'https://schema.org',
63
+ '@type': 'Article',
64
+ headline: 'H',
65
+ author: 'A',
66
+ datePublished: '2026-01-01',
67
+ // recommended image/publisher/dateModified/description all absent
68
+ });
69
+ expect(result.valid).toBe(true);
70
+ expect(result.errors.length).toBeGreaterThan(0);
71
+ expect(result.errors.every((e) => e.severity === 'warning')).toBe(true);
72
+ const fields = result.errors.map((e) => e.field);
73
+ for (const field of ['image', 'publisher', 'dateModified', 'description']) {
74
+ expect(fields).toContain(field);
75
+ }
76
+ });
77
+ it('flips valid:false when a required field is missing', () => {
78
+ const result = validateSchema({
79
+ '@context': 'https://schema.org',
80
+ '@type': 'Article',
81
+ headline: 'H',
82
+ // author + datePublished missing
83
+ });
84
+ expect(result.valid).toBe(false);
85
+ expect(result.errors.filter((e) => e.severity === 'error').map((e) => e.field)).toEqual(expect.arrayContaining(['author', 'datePublished']));
86
+ });
87
+ it('short-circuits on a missing @type without checking required fields', () => {
88
+ const result = validateSchema({}); // no @context, no @type
89
+ expect(result.valid).toBe(false);
90
+ // Both top-level guards fire, and the function returns before any
91
+ // type-specific required-field checks run.
92
+ expect(result.errors.map((e) => e.field)).toEqual(['@context', '@type']);
93
+ });
94
+ });
95
+ // Gap 8: Article headline > 110 chars emits a warning but keeps valid:true.
96
+ describe('validateSchema Article long-headline warning (gap 8)', () => {
97
+ it('warns on a 120-char headline without invalidating the schema', () => {
98
+ const result = validateSchema({
99
+ '@context': 'https://schema.org',
100
+ '@type': 'Article',
101
+ headline: 'H'.repeat(120),
102
+ author: 'A',
103
+ datePublished: '2026-01-01',
104
+ image: 'https://x/i.png',
105
+ publisher: 'Acme',
106
+ dateModified: '2026-01-02',
107
+ description: 'desc',
108
+ });
109
+ expect(result.valid).toBe(true);
110
+ const headlineWarning = result.errors.find((e) => e.field === 'headline');
111
+ expect(headlineWarning).toBeDefined();
112
+ expect(headlineWarning.severity).toBe('warning');
113
+ });
114
+ it('does not warn for a headline at/under 110 chars', () => {
115
+ const result = validateSchema({
116
+ '@context': 'https://schema.org',
117
+ '@type': 'Article',
118
+ headline: 'H'.repeat(110),
119
+ author: 'A',
120
+ datePublished: '2026-01-01',
121
+ image: 'https://x/i.png',
122
+ publisher: 'Acme',
123
+ dateModified: '2026-01-02',
124
+ description: 'desc',
125
+ });
126
+ expect(result.errors.some((e) => e.field === 'headline')).toBe(false);
127
+ });
128
+ });
129
+ //# sourceMappingURL=schema-validator.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-validator.test.js","sourceRoot":"","sources":["../../src/__tests__/schema-validator.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sitemap.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sitemap.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildDocumentUrl, generateCollectionSitemap, generateImageSitemap, generateVideoSitemap, generateNewsSitemap, generateHreflangSitemap, } from '../sitemap/index.js';
3
+ // Gap 13: buildDocumentUrl + generateCollectionSitemap URL/priority/archive branches.
4
+ describe('buildDocumentUrl (gap 13)', () => {
5
+ it('joins a urlPrefix and strips a trailing slash on the base', () => {
6
+ expect(buildDocumentUrl('https://x.com/', 'blog', 'hello')).toBe('https://x.com/blog/hello');
7
+ });
8
+ it('omits the prefix segment when urlPrefix is undefined or empty', () => {
9
+ expect(buildDocumentUrl('https://x.com', undefined, 'hello')).toBe('https://x.com/hello');
10
+ expect(buildDocumentUrl('https://x.com', '', 'hello')).toBe('https://x.com/hello');
11
+ });
12
+ });
13
+ describe('generateCollectionSitemap URL resolution + priority (gap 13)', () => {
14
+ it('joins slugs via urlPrefix, injects a post archive entry, defaults priority 0.6', () => {
15
+ const xml = generateCollectionSitemap([{ loc: 'hello' }], 'https://x.com', {
16
+ slug: 'blog',
17
+ urlPrefix: 'blog',
18
+ type: 'post',
19
+ archivePath: '/blog',
20
+ });
21
+ expect(xml).toContain('<loc>https://x.com/blog/hello</loc>');
22
+ // archivePath injected as its own root-relative entry for post collections.
23
+ expect(xml).toContain('<loc>https://x.com/blog</loc>');
24
+ expect(xml).toContain('<priority>0.6</priority>');
25
+ });
26
+ it('defaults page-type priority to 0.8 and suppresses the archive entry', () => {
27
+ const xml = generateCollectionSitemap([{ loc: 'hello' }], 'https://x.com', {
28
+ slug: 'pages',
29
+ urlPrefix: '',
30
+ type: 'page',
31
+ archivePath: '/blog', // present but must be ignored for non-post collections
32
+ });
33
+ expect(xml).toContain('<loc>https://x.com/hello</loc>');
34
+ expect(xml).toContain('<priority>0.8</priority>');
35
+ expect(xml).not.toContain('<loc>https://x.com/blog</loc>');
36
+ });
37
+ it('passes through absolute http locs and joins root-relative and bare locs', () => {
38
+ const entries = [
39
+ { loc: 'https://other.com/x' },
40
+ { loc: '/abs' },
41
+ { loc: 'bare' },
42
+ ];
43
+ const xml = generateCollectionSitemap(entries, 'https://x.com');
44
+ expect(xml).toContain('<loc>https://other.com/x</loc>');
45
+ expect(xml).toContain('<loc>https://x.com/abs</loc>');
46
+ expect(xml).toContain('<loc>https://x.com/bare</loc>');
47
+ });
48
+ });
49
+ // Gap 14: sitemap extension generators.
50
+ describe('generateImageSitemap (gap 14)', () => {
51
+ it('renders optional image fields and xml-escapes the title', () => {
52
+ const xml = generateImageSitemap([
53
+ {
54
+ pageUrl: 'https://x.com/p',
55
+ images: [
56
+ {
57
+ loc: 'https://x.com/i.png',
58
+ title: 'A & B <c>',
59
+ caption: 'cap',
60
+ geoLocation: 'NYC',
61
+ license: 'https://x.com/lic',
62
+ },
63
+ ],
64
+ },
65
+ ]);
66
+ expect(xml).toContain('<image:title>A &amp; B &lt;c&gt;</image:title>');
67
+ expect(xml).toContain('<image:caption>cap</image:caption>');
68
+ expect(xml).toContain('<image:geo_location>NYC</image:geo_location>');
69
+ expect(xml).toContain('<image:license>https://x.com/lic</image:license>');
70
+ });
71
+ it('omits optional image tags when not provided', () => {
72
+ const xml = generateImageSitemap([
73
+ { pageUrl: 'https://x.com/p', images: [{ loc: 'https://x.com/i.png' }] },
74
+ ]);
75
+ expect(xml).toContain('<image:loc>https://x.com/i.png</image:loc>');
76
+ expect(xml).not.toContain('<image:title>');
77
+ expect(xml).not.toContain('<image:caption>');
78
+ expect(xml).not.toContain('<image:geo_location>');
79
+ expect(xml).not.toContain('<image:license>');
80
+ });
81
+ });
82
+ describe('generateVideoSitemap (gap 14)', () => {
83
+ it('renders duration, optional URLs, and family_friendly=no', () => {
84
+ const xml = generateVideoSitemap([
85
+ {
86
+ pageUrl: 'https://x.com/v',
87
+ video: {
88
+ thumbnailLoc: 'https://x.com/t.jpg',
89
+ title: 'T & T',
90
+ description: 'D',
91
+ contentLoc: 'https://x.com/c.mp4',
92
+ playerLoc: 'https://x.com/player',
93
+ duration: 120,
94
+ publicationDate: '2026-01-01',
95
+ familyFriendly: false,
96
+ },
97
+ },
98
+ ]);
99
+ expect(xml).toContain('<video:title>T &amp; T</video:title>');
100
+ expect(xml).toContain('<video:duration>120</video:duration>');
101
+ expect(xml).toContain('<video:content_loc>https://x.com/c.mp4</video:content_loc>');
102
+ expect(xml).toContain('<video:player_loc>https://x.com/player</video:player_loc>');
103
+ expect(xml).toContain('<video:publication_date>2026-01-01</video:publication_date>');
104
+ expect(xml).toContain('<video:family_friendly>no</video:family_friendly>');
105
+ });
106
+ it('renders family_friendly=yes and omits absent optional tags', () => {
107
+ const xml = generateVideoSitemap([
108
+ {
109
+ pageUrl: 'https://x.com/v',
110
+ video: {
111
+ thumbnailLoc: 'https://x.com/t.jpg',
112
+ title: 'T',
113
+ description: 'D',
114
+ familyFriendly: true,
115
+ },
116
+ },
117
+ ]);
118
+ expect(xml).toContain('<video:family_friendly>yes</video:family_friendly>');
119
+ expect(xml).not.toContain('<video:duration>');
120
+ expect(xml).not.toContain('<video:content_loc>');
121
+ expect(xml).not.toContain('<video:player_loc>');
122
+ expect(xml).not.toContain('<video:publication_date>');
123
+ });
124
+ });
125
+ describe('generateNewsSitemap (gap 14)', () => {
126
+ it('joins keywords with a comma and separator', () => {
127
+ const xml = generateNewsSitemap([
128
+ {
129
+ loc: 'https://x.com/n',
130
+ news: {
131
+ publicationName: 'Daily',
132
+ publicationLanguage: 'en',
133
+ publicationDate: '2026-01-01',
134
+ title: 'Headline',
135
+ keywords: ['a', 'b'],
136
+ },
137
+ },
138
+ ]);
139
+ expect(xml).toContain('<news:keywords>a, b</news:keywords>');
140
+ });
141
+ it('omits the keywords tag when none are provided', () => {
142
+ const xml = generateNewsSitemap([
143
+ {
144
+ loc: 'https://x.com/n',
145
+ news: {
146
+ publicationName: 'Daily',
147
+ publicationLanguage: 'en',
148
+ publicationDate: '2026-01-01',
149
+ title: 'Headline',
150
+ },
151
+ },
152
+ ]);
153
+ expect(xml).not.toContain('<news:keywords>');
154
+ });
155
+ });
156
+ describe('generateHreflangSitemap (gap 14)', () => {
157
+ it('emits one xhtml:link per alternate', () => {
158
+ const xml = generateHreflangSitemap([
159
+ {
160
+ loc: 'https://x.com/en',
161
+ alternates: [
162
+ { hreflang: 'en', href: 'https://x.com/en' },
163
+ { hreflang: 'fr', href: 'https://x.com/fr' },
164
+ ],
165
+ },
166
+ ]);
167
+ const linkCount = (xml.match(/<xhtml:link /g) ?? []).length;
168
+ expect(linkCount).toBe(2);
169
+ expect(xml).toContain('hreflang="fr"');
170
+ expect(xml).toContain('href="https://x.com/fr"');
171
+ });
172
+ });
173
+ //# sourceMappingURL=sitemap.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.test.js","sourceRoot":"","sources":["../../src/__tests__/sitemap.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/schema/composer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAwB3E,oFAAoF;AACpF,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA2CzB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,gBAAgB,CAAA;CAAE,CAGnE"}
1
+ {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/schema/composer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAwB3E,oFAAoF;AACpF,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA+CzB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,gBAAgB,CAAA;CAAE,CAGnE"}
@@ -42,7 +42,11 @@ export function composeSchema(type, data, nested) {
42
42
  ...nestedData,
43
43
  };
44
44
  }
45
- else if (!schema[def.field] && data[def.field] && typeof data[def.field] === 'string') {
45
+ else if (typeof data[def.field] === 'string' && data[def.field] !== '') {
46
+ // The generic copy loop above already put the bare string on `schema`, so
47
+ // a `!schema[def.field]` guard here would never fire. Overwrite it with a
48
+ // proper nested entity so e.g. Article.author: 'Jane' becomes
49
+ // { '@type': 'Person', name: 'Jane' }.
46
50
  schema[def.field] = {
47
51
  '@type': def.type,
48
52
  name: data[def.field],
@@ -1 +1 @@
1
- {"version":3,"file":"composer.js","sourceRoot":"","sources":["../../src/schema/composer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"composer.js","sourceRoot":"","sources":["../../src/schema/composer.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/plugin-seo",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,11 +28,11 @@
28
28
  "@types/react": "^19.0.0",
29
29
  "typescript": "^5.7.0",
30
30
  "vitest": "^4.1.0",
31
- "@actuate-media/cms-core": "0.93.1"
31
+ "@actuate-media/cms-core": "0.95.1"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@actuate-media/cms-core": ">=0.1.0",
35
- "react": "^19.2.0"
35
+ "react": "^18.0.0 || ^19.0.0"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsc --project tsconfig.json",