@prenta/admin 0.97.5 → 0.98.1
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/CHANGELOG.md +16 -0
- package/dist/__tests__/components/seo-score-bands.test.js +0 -7
- package/dist/__tests__/components/seo-score-bands.test.js.map +1 -1
- package/dist/__tests__/lib/page-audit-input.test.js +77 -1
- package/dist/__tests__/lib/page-audit-input.test.js.map +1 -1
- package/dist/__tests__/lib/seo-service-errors.test.js +34 -3
- package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -1
- package/dist/__tests__/views/page-builder-seo-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-builder-seo-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-builder-seo-panel.render.test.js +65 -0
- package/dist/__tests__/views/page-builder-seo-panel.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-audit-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-audit-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-audit-tab.render.test.js +100 -0
- package/dist/__tests__/views/seo-audit-tab.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-technical-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-technical-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-technical-tab.render.test.js +121 -0
- package/dist/__tests__/views/seo-technical-tab.render.test.js.map +1 -0
- package/dist/lib/page-audit-input.d.ts +14 -1
- package/dist/lib/page-audit-input.d.ts.map +1 -1
- package/dist/lib/page-audit-input.js +19 -0
- package/dist/lib/page-audit-input.js.map +1 -1
- package/dist/lib/seo-service.d.ts +30 -1
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-builder/ContextPanel.d.ts.map +1 -1
- package/dist/views/page-builder/ContextPanel.js +1 -1
- package/dist/views/page-builder/ContextPanel.js.map +1 -1
- package/dist/views/page-builder/SEOPanel.d.ts +1 -2
- package/dist/views/page-builder/SEOPanel.d.ts.map +1 -1
- package/dist/views/page-builder/SEOPanel.js +10 -75
- package/dist/views/page-builder/SEOPanel.js.map +1 -1
- package/dist/views/seo/AuditTab.d.ts.map +1 -1
- package/dist/views/seo/AuditTab.js +3 -2
- package/dist/views/seo/AuditTab.js.map +1 -1
- package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
- package/dist/views/seo/TechnicalTab.js +5 -0
- package/dist/views/seo/TechnicalTab.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/components/seo-score-bands.test.ts +0 -7
- package/src/__tests__/lib/page-audit-input.test.ts +91 -1
- package/src/__tests__/lib/seo-service-errors.test.ts +55 -1
- package/src/__tests__/views/page-builder-seo-panel.render.test.tsx +77 -0
- package/src/__tests__/views/seo-audit-tab.render.test.tsx +113 -0
- package/src/__tests__/views/seo-technical-tab.render.test.tsx +156 -0
- package/src/lib/page-audit-input.ts +36 -1
- package/src/lib/seo-service.ts +30 -2
- package/src/views/page-builder/ContextPanel.tsx +0 -1
- package/src/views/page-builder/SEOPanel.tsx +40 -282
- package/src/views/seo/AuditTab.tsx +60 -27
- package/src/views/seo/TechnicalTab.tsx +5 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { render, screen } from '@testing-library/react'
|
|
4
|
+
import type { PageNode } from '@prenta/core'
|
|
5
|
+
import { BuilderSEOPanel } from '../../views/page-builder/SEOPanel.js'
|
|
6
|
+
|
|
7
|
+
const tree = {
|
|
8
|
+
id: 'page-1',
|
|
9
|
+
type: 'page',
|
|
10
|
+
children: [
|
|
11
|
+
{
|
|
12
|
+
id: 'sec-1',
|
|
13
|
+
type: 'section',
|
|
14
|
+
settings: {},
|
|
15
|
+
children: [
|
|
16
|
+
{
|
|
17
|
+
id: 'row-1',
|
|
18
|
+
type: 'row',
|
|
19
|
+
settings: {},
|
|
20
|
+
children: [
|
|
21
|
+
{
|
|
22
|
+
id: 'col-1',
|
|
23
|
+
type: 'column',
|
|
24
|
+
settings: { width: 12 },
|
|
25
|
+
children: [
|
|
26
|
+
{
|
|
27
|
+
id: 'blk-hero',
|
|
28
|
+
type: 'block',
|
|
29
|
+
settings: { blockType: 'hero' },
|
|
30
|
+
data: {
|
|
31
|
+
title: 'Welcome to Our Service',
|
|
32
|
+
image: { url: '/hero.webp', alt: 'Hero banner' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
} as PageNode
|
|
43
|
+
|
|
44
|
+
const pageSettings = {
|
|
45
|
+
title: 'Home',
|
|
46
|
+
slug: 'home',
|
|
47
|
+
metaTitle: 'Welcome to Our Service',
|
|
48
|
+
metaDescription: 'The public description.',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('BuilderSEOPanel', () => {
|
|
52
|
+
it('grades the live tree with the shared page audit panel and keeps meta fields', () => {
|
|
53
|
+
render(
|
|
54
|
+
<BuilderSEOPanel tree={tree} pageSettings={pageSettings} onPageSettingsChange={vi.fn()} />,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
expect(screen.getByRole('img', { name: /SEO score:/i })).toBeTruthy()
|
|
58
|
+
expect(screen.getByRole('img', { name: /GEO score:/i })).toBeTruthy()
|
|
59
|
+
expect(screen.getByText('SEO checks')).toBeTruthy()
|
|
60
|
+
expect(screen.getByText('GEO checks')).toBeTruthy()
|
|
61
|
+
expect(screen.getByLabelText('Meta Title')).toBeTruthy()
|
|
62
|
+
expect(screen.queryByText('Readability')).toBeNull()
|
|
63
|
+
expect(screen.queryByText('Block Hints')).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('keeps the empty-tree copy when the canvas has no children', () => {
|
|
67
|
+
render(
|
|
68
|
+
<BuilderSEOPanel
|
|
69
|
+
tree={{ id: 'page-1', type: 'page', children: [] } as PageNode}
|
|
70
|
+
pageSettings={pageSettings}
|
|
71
|
+
onPageSettingsChange={vi.fn()}
|
|
72
|
+
/>,
|
|
73
|
+
)
|
|
74
|
+
expect(screen.getByText('Add content to your page to see SEO analysis')).toBeTruthy()
|
|
75
|
+
expect(screen.queryByRole('img', { name: /SEO score:/i })).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
4
|
+
|
|
5
|
+
const baseRun = {
|
|
6
|
+
id: 'audit-1',
|
|
7
|
+
status: 'COMPLETED',
|
|
8
|
+
scope: 'full',
|
|
9
|
+
startedAt: '2026-08-11T12:00:00.000Z',
|
|
10
|
+
finishedAt: '2026-08-11T12:00:01.000Z',
|
|
11
|
+
siteScoreBefore: 70,
|
|
12
|
+
siteScoreAfter: 82,
|
|
13
|
+
pagesScanned: 10,
|
|
14
|
+
issuesFound: 3,
|
|
15
|
+
criticalCount: 0,
|
|
16
|
+
warningCount: 2,
|
|
17
|
+
infoCount: 1,
|
|
18
|
+
errorMessage: null,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let latestRun: Record<string, unknown> = { ...baseRun }
|
|
22
|
+
|
|
23
|
+
const cmsApi = vi.fn(async (endpoint: string) => {
|
|
24
|
+
if (endpoint === '/seo/audits') {
|
|
25
|
+
return { data: { runs: [latestRun] }, status: 200 }
|
|
26
|
+
}
|
|
27
|
+
if (endpoint.startsWith('/seo/issues')) {
|
|
28
|
+
return { data: { issues: [] }, status: 200 }
|
|
29
|
+
}
|
|
30
|
+
return { data: {}, status: 200 }
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
vi.mock('../../lib/api.js', () => ({
|
|
34
|
+
cmsApi: (endpoint: string) => cmsApi(endpoint),
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
vi.mock('sonner', () => ({
|
|
38
|
+
toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn(), message: vi.fn() },
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
const { AuditTab } = await import('../../views/seo/AuditTab.js')
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
cmsApi.mockClear()
|
|
45
|
+
latestRun = { ...baseRun }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('AuditTab rendered audit coverage', () => {
|
|
49
|
+
it('shows safe coverage details and a warning for partial runs', async () => {
|
|
50
|
+
latestRun = {
|
|
51
|
+
...baseRun,
|
|
52
|
+
metadata: {
|
|
53
|
+
renderedAudit: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
originSource: 'override',
|
|
56
|
+
verified: 8,
|
|
57
|
+
unverified: 2,
|
|
58
|
+
notAttempted: 0,
|
|
59
|
+
partial: true,
|
|
60
|
+
bytesRead: 2048,
|
|
61
|
+
durationMs: 50,
|
|
62
|
+
reasons: [{ reason: 'timeout', count: 2 }],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render(<AuditTab />)
|
|
68
|
+
|
|
69
|
+
expect(await screen.findByText('8 verified')).toBeTruthy()
|
|
70
|
+
expect(screen.getByText('2 unverified')).toBeTruthy()
|
|
71
|
+
const warning = screen.getByRole('status')
|
|
72
|
+
expect(warning.textContent).toContain('The score covers 8 verified pages.')
|
|
73
|
+
expect(warning.textContent).toContain('their prior rendered-dependent issues were preserved.')
|
|
74
|
+
expect(screen.queryByText(/timeout/i)).toBeNull()
|
|
75
|
+
expect(screen.queryByText(/https?:\/\//i)).toBeNull()
|
|
76
|
+
expect(screen.queryByText(/transport error/i)).toBeNull()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('does not show a partial warning for complete coverage', async () => {
|
|
80
|
+
latestRun = {
|
|
81
|
+
...baseRun,
|
|
82
|
+
metadata: {
|
|
83
|
+
renderedAudit: {
|
|
84
|
+
enabled: true,
|
|
85
|
+
originSource: 'config',
|
|
86
|
+
verified: 10,
|
|
87
|
+
unverified: 0,
|
|
88
|
+
notAttempted: 0,
|
|
89
|
+
partial: false,
|
|
90
|
+
bytesRead: 4096,
|
|
91
|
+
durationMs: 40,
|
|
92
|
+
reasons: [],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render(<AuditTab />)
|
|
98
|
+
|
|
99
|
+
expect(await screen.findByText('10 verified')).toBeTruthy()
|
|
100
|
+
expect(screen.getByText('0 unverified')).toBeTruthy()
|
|
101
|
+
expect(screen.queryByText('Partial rendered audit')).toBeNull()
|
|
102
|
+
expect(screen.queryByRole('status')).toBeNull()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('renders legacy audit runs without coverage or warnings', async () => {
|
|
106
|
+
render(<AuditTab />)
|
|
107
|
+
|
|
108
|
+
await waitFor(() => expect(screen.getByText('10')).toBeTruthy())
|
|
109
|
+
expect(screen.queryByText(/verified$/i)).toBeNull()
|
|
110
|
+
expect(screen.queryByText('Partial rendered audit')).toBeNull()
|
|
111
|
+
expect(screen.queryByRole('status')).toBeNull()
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
4
|
+
import { toast } from 'sonner'
|
|
5
|
+
|
|
6
|
+
const initialCrawlSettings = {
|
|
7
|
+
indexEntireSite: true,
|
|
8
|
+
includeImagesInSitemap: true,
|
|
9
|
+
includePostsInSitemap: true,
|
|
10
|
+
sitemapEnabled: true,
|
|
11
|
+
autoRegenerateSitemapOnPublish: true,
|
|
12
|
+
pingSearchEnginesOnPublish: true,
|
|
13
|
+
auditRenderedHtml: true,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let crawlSettings = { ...initialCrawlSettings }
|
|
17
|
+
let failNextPut = false
|
|
18
|
+
let pendingPut: Promise<{ data?: unknown; error?: string }> | null = null
|
|
19
|
+
|
|
20
|
+
const cmsApi = vi.fn(async (endpoint: string, options?: RequestInit) => {
|
|
21
|
+
const method = options?.method ?? 'GET'
|
|
22
|
+
if (endpoint === '/seo/crawl-settings' && method === 'GET') {
|
|
23
|
+
return { data: { ...crawlSettings }, status: 200 }
|
|
24
|
+
}
|
|
25
|
+
if (endpoint === '/seo/crawl-settings' && method === 'PUT') {
|
|
26
|
+
if (pendingPut) await pendingPut
|
|
27
|
+
if (failNextPut) {
|
|
28
|
+
failNextPut = false
|
|
29
|
+
return { error: 'Unable to save crawl settings.', status: 500 }
|
|
30
|
+
}
|
|
31
|
+
crawlSettings = {
|
|
32
|
+
...crawlSettings,
|
|
33
|
+
...(JSON.parse(String(options?.body)) as Partial<typeof initialCrawlSettings>),
|
|
34
|
+
}
|
|
35
|
+
return { data: { ...crawlSettings }, status: 200 }
|
|
36
|
+
}
|
|
37
|
+
if (endpoint === '/seo/sitemap/status') {
|
|
38
|
+
return {
|
|
39
|
+
data: {
|
|
40
|
+
sitemapUrl: 'https://example.test/sitemap.xml',
|
|
41
|
+
lastGeneratedAt: null,
|
|
42
|
+
pagesIncluded: 10,
|
|
43
|
+
pagesExcluded: 0,
|
|
44
|
+
autoRegenerateOnPublish: true,
|
|
45
|
+
errors: [],
|
|
46
|
+
warnings: [],
|
|
47
|
+
},
|
|
48
|
+
status: 200,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (endpoint === '/seo/robots') {
|
|
52
|
+
return { data: { body: 'User-agent: *\nAllow: /', generated: true }, status: 200 }
|
|
53
|
+
}
|
|
54
|
+
if (endpoint === '/ai-crawlers') {
|
|
55
|
+
return {
|
|
56
|
+
data: { enabled: false, rows: [], totals: { hits: 0, bots: 0, honeypot: 0 } },
|
|
57
|
+
status: 200,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { data: {}, status: 200 }
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
vi.mock('../../lib/api.js', () => ({
|
|
64
|
+
cmsApi: (endpoint: string, options?: RequestInit) => cmsApi(endpoint, options),
|
|
65
|
+
}))
|
|
66
|
+
|
|
67
|
+
vi.mock('sonner', () => ({
|
|
68
|
+
toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn(), message: vi.fn() },
|
|
69
|
+
}))
|
|
70
|
+
|
|
71
|
+
const { TechnicalTab } = await import('../../views/seo/TechnicalTab.js')
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
cmsApi.mockClear()
|
|
75
|
+
vi.mocked(toast.error).mockClear()
|
|
76
|
+
crawlSettings = { ...initialCrawlSettings }
|
|
77
|
+
failNextPut = false
|
|
78
|
+
pendingPut = null
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('TechnicalTab rendered audit setting', () => {
|
|
82
|
+
it('renders the accessible setting and persists changes', async () => {
|
|
83
|
+
render(<TechnicalTab />)
|
|
84
|
+
|
|
85
|
+
const renderedAudit = await screen.findByRole('switch', {
|
|
86
|
+
name: /Audit rendered public HTML/i,
|
|
87
|
+
description: 'Fetch public page output so audits grade crawler-visible headings and content.',
|
|
88
|
+
})
|
|
89
|
+
expect(renderedAudit.getAttribute('aria-checked')).toBe('true')
|
|
90
|
+
|
|
91
|
+
fireEvent.click(renderedAudit)
|
|
92
|
+
|
|
93
|
+
await waitFor(() =>
|
|
94
|
+
expect(cmsApi).toHaveBeenCalledWith(
|
|
95
|
+
'/seo/crawl-settings',
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
method: 'PUT',
|
|
98
|
+
body: JSON.stringify({ auditRenderedHtml: false }),
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('disables crawl switches while a setting is saving', async () => {
|
|
105
|
+
let resolvePut: (() => void) | undefined
|
|
106
|
+
pendingPut = new Promise((resolve) => {
|
|
107
|
+
resolvePut = () => resolve({})
|
|
108
|
+
})
|
|
109
|
+
render(<TechnicalTab />)
|
|
110
|
+
|
|
111
|
+
fireEvent.click(await screen.findByRole('switch', { name: /Audit rendered public HTML/i }))
|
|
112
|
+
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
for (const toggle of screen.getAllByRole('switch')) {
|
|
115
|
+
expect((toggle as HTMLButtonElement).disabled).toBe(true)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
resolvePut?.()
|
|
120
|
+
await waitFor(() =>
|
|
121
|
+
expect(
|
|
122
|
+
(
|
|
123
|
+
screen.getByRole('switch', {
|
|
124
|
+
name: /Audit rendered public HTML/i,
|
|
125
|
+
}) as HTMLButtonElement
|
|
126
|
+
).disabled,
|
|
127
|
+
).toBe(false),
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('reports save failures and refetches the server value', async () => {
|
|
132
|
+
failNextPut = true
|
|
133
|
+
render(<TechnicalTab />)
|
|
134
|
+
|
|
135
|
+
const renderedAudit = await screen.findByRole('switch', {
|
|
136
|
+
name: /Audit rendered public HTML/i,
|
|
137
|
+
})
|
|
138
|
+
const getCallsBeforeSave = cmsApi.mock.calls.filter(
|
|
139
|
+
([endpoint, options]) =>
|
|
140
|
+
endpoint === '/seo/crawl-settings' &&
|
|
141
|
+
((options as RequestInit | undefined)?.method ?? 'GET') === 'GET',
|
|
142
|
+
).length
|
|
143
|
+
|
|
144
|
+
fireEvent.click(renderedAudit)
|
|
145
|
+
|
|
146
|
+
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Unable to save crawl settings.'))
|
|
147
|
+
await waitFor(() => expect(renderedAudit.getAttribute('aria-checked')).toBe('true'))
|
|
148
|
+
expect(
|
|
149
|
+
cmsApi.mock.calls.filter(
|
|
150
|
+
([endpoint, options]) =>
|
|
151
|
+
endpoint === '/seo/crawl-settings' &&
|
|
152
|
+
((options as RequestInit | undefined)?.method ?? 'GET') === 'GET',
|
|
153
|
+
).length,
|
|
154
|
+
).toBeGreaterThan(getCallsBeforeSave)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveRenderedMetaTitle } from '@prenta/core/client'
|
|
2
|
-
import type
|
|
2
|
+
import { layoutTreeToSections, type PageSection } from '@prenta/core/page-sections'
|
|
3
3
|
import type { PageAuditInput } from '@prenta/core/seo/page-audit'
|
|
4
4
|
import type { SeoContentRecord } from './seo-service.js'
|
|
5
5
|
|
|
@@ -102,3 +102,38 @@ export function metaFromRecord(record: SeoContentRecord): AuditMetaSource {
|
|
|
102
102
|
hasOgImage: Boolean(record.ogImage),
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
export interface BuilderAuditSource {
|
|
107
|
+
tree: unknown
|
|
108
|
+
pageSettings: {
|
|
109
|
+
title: string
|
|
110
|
+
slug: string
|
|
111
|
+
metaTitle?: string
|
|
112
|
+
metaDescription?: string
|
|
113
|
+
ogImage?: string
|
|
114
|
+
focusKeyphrase?: string
|
|
115
|
+
schemaType?: string
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildPageAuditInputFromBuilder({
|
|
120
|
+
tree,
|
|
121
|
+
pageSettings,
|
|
122
|
+
}: BuilderAuditSource): PageAuditInput {
|
|
123
|
+
return buildPageAuditInput({
|
|
124
|
+
sections: layoutTreeToSections(tree),
|
|
125
|
+
meta: {
|
|
126
|
+
metaTitle: pageSettings.metaTitle ?? '',
|
|
127
|
+
metaDescription: pageSettings.metaDescription ?? '',
|
|
128
|
+
focusKeyword: pageSettings.focusKeyphrase ?? '',
|
|
129
|
+
structuredDataType: pageSettings.schemaType ?? '',
|
|
130
|
+
noindex: false,
|
|
131
|
+
nosnippet: false,
|
|
132
|
+
llmsInclude: 'AUTO',
|
|
133
|
+
hasOgImage: Boolean(pageSettings.ogImage),
|
|
134
|
+
},
|
|
135
|
+
slug: pageSettings.slug,
|
|
136
|
+
record: null,
|
|
137
|
+
fallbackTitle: pageSettings.title,
|
|
138
|
+
})
|
|
139
|
+
}
|
package/src/lib/seo-service.ts
CHANGED
|
@@ -299,6 +299,18 @@ export function applyContentSeoQuery(
|
|
|
299
299
|
|
|
300
300
|
export type SeoAuditScope = 'full' | 'content' | 'technical' | 'redirect'
|
|
301
301
|
|
|
302
|
+
export interface RenderedAuditSummary {
|
|
303
|
+
enabled: boolean
|
|
304
|
+
originSource: 'override' | 'config' | null
|
|
305
|
+
verified: number
|
|
306
|
+
unverified: number
|
|
307
|
+
notAttempted: number
|
|
308
|
+
partial: boolean
|
|
309
|
+
bytesRead: number
|
|
310
|
+
durationMs: number
|
|
311
|
+
reasons: Array<{ reason: string; count: number }>
|
|
312
|
+
}
|
|
313
|
+
|
|
302
314
|
export interface SeoAuditRun {
|
|
303
315
|
id: string
|
|
304
316
|
status: 'RUNNING' | 'COMPLETED' | 'FAILED'
|
|
@@ -313,12 +325,27 @@ export interface SeoAuditRun {
|
|
|
313
325
|
warningCount: number
|
|
314
326
|
infoCount: number
|
|
315
327
|
errorMessage: string | null
|
|
328
|
+
metadata: {
|
|
329
|
+
renderedAudit?: RenderedAuditSummary
|
|
330
|
+
} | null
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface SeoAuditRunResponse {
|
|
334
|
+
id: string
|
|
335
|
+
status: 'COMPLETED'
|
|
336
|
+
siteScoreAfter: number
|
|
337
|
+
issuesFound: number
|
|
338
|
+
criticalCount: number
|
|
339
|
+
warningCount: number
|
|
340
|
+
infoCount: number
|
|
341
|
+
pagesScanned: number
|
|
342
|
+
renderedAudit: RenderedAuditSummary
|
|
316
343
|
}
|
|
317
344
|
|
|
318
345
|
export async function runSeoAudit(
|
|
319
346
|
scope: SeoAuditScope = 'full',
|
|
320
|
-
): Promise<{ run?:
|
|
321
|
-
const res = await cmsApi<
|
|
347
|
+
): Promise<{ run?: SeoAuditRunResponse; error?: string }> {
|
|
348
|
+
const res = await cmsApi<SeoAuditRunResponse>('/seo/audit', {
|
|
322
349
|
method: 'POST',
|
|
323
350
|
body: JSON.stringify({ scope }),
|
|
324
351
|
})
|
|
@@ -662,6 +689,7 @@ export interface CrawlSettings {
|
|
|
662
689
|
sitemapEnabled: boolean
|
|
663
690
|
autoRegenerateSitemapOnPublish: boolean
|
|
664
691
|
pingSearchEnginesOnPublish: boolean
|
|
692
|
+
auditRenderedHtml: boolean
|
|
665
693
|
}
|
|
666
694
|
|
|
667
695
|
export async function fetchSitemapStatus(): Promise<SitemapStatus | null> {
|