@prenta/admin 1.15.0 → 1.17.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/CHANGELOG.md +16 -0
- package/dist/__tests__/components/faq-proposal-preview.render.test.d.ts +2 -0
- package/dist/__tests__/components/faq-proposal-preview.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/faq-proposal-preview.render.test.js +97 -0
- package/dist/__tests__/components/faq-proposal-preview.render.test.js.map +1 -0
- package/dist/__tests__/components/seo-issue-fix-panel.render.test.js +37 -11
- package/dist/__tests__/components/seo-issue-fix-panel.render.test.js.map +1 -1
- package/dist/__tests__/lib/seo-service-errors.test.js +3 -0
- package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -1
- package/dist/__tests__/lib/seo-service-geo.test.d.ts +2 -0
- package/dist/__tests__/lib/seo-service-geo.test.d.ts.map +1 -0
- package/dist/__tests__/lib/seo-service-geo.test.js +103 -0
- package/dist/__tests__/lib/seo-service-geo.test.js.map +1 -0
- package/dist/__tests__/lib/seo-service-proposals.test.js +24 -0
- package/dist/__tests__/lib/seo-service-proposals.test.js.map +1 -1
- package/dist/__tests__/router/seo-tab-for-path.test.js +4 -0
- package/dist/__tests__/router/seo-tab-for-path.test.js.map +1 -1
- package/dist/__tests__/views/seo-ai-visibility.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-ai-visibility.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-ai-visibility.render.test.js +244 -0
- package/dist/__tests__/views/seo-ai-visibility.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-geo-settings-card.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-geo-settings-card.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-geo-settings-card.render.test.js +227 -0
- package/dist/__tests__/views/seo-geo-settings-card.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-proposals-tab.render.test.js +277 -0
- package/dist/__tests__/views/seo-proposals-tab.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-settings.render.test.js +16 -4
- package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-technical-tab.render.test.js +13 -0
- package/dist/__tests__/views/seo-technical-tab.render.test.js.map +1 -1
- package/dist/components/seo/FaqProposalPreview.d.ts +39 -0
- package/dist/components/seo/FaqProposalPreview.d.ts.map +1 -0
- package/dist/components/seo/FaqProposalPreview.js +62 -0
- package/dist/components/seo/FaqProposalPreview.js.map +1 -0
- package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js +7 -2
- package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
- package/dist/lib/seo-service.d.ts +155 -2
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +42 -2
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/SEO.d.ts +5 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +12 -7
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/seo/AiVisibilityTab.d.ts +4 -0
- package/dist/views/seo/AiVisibilityTab.d.ts.map +1 -0
- package/dist/views/seo/AiVisibilityTab.js +165 -0
- package/dist/views/seo/AiVisibilityTab.js.map +1 -0
- package/dist/views/seo/ProposalsTab.d.ts.map +1 -1
- package/dist/views/seo/ProposalsTab.js +160 -7
- package/dist/views/seo/ProposalsTab.js.map +1 -1
- package/dist/views/seo/TechnicalTab.d.ts +3 -1
- package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
- package/dist/views/seo/TechnicalTab.js +4 -4
- package/dist/views/seo/TechnicalTab.js.map +1 -1
- package/dist/views/settings/SeoAutopilotCard.d.ts.map +1 -1
- package/dist/views/settings/SeoAutopilotCard.js +6 -0
- package/dist/views/settings/SeoAutopilotCard.js.map +1 -1
- package/dist/views/settings/SeoGeoSettingsCard.d.ts +12 -0
- package/dist/views/settings/SeoGeoSettingsCard.d.ts.map +1 -0
- package/dist/views/settings/SeoGeoSettingsCard.js +178 -0
- package/dist/views/settings/SeoGeoSettingsCard.js.map +1 -0
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/SeoSettingsTab.js +2 -1
- package/dist/views/settings/SeoSettingsTab.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/components/faq-proposal-preview.render.test.tsx +137 -0
- package/src/__tests__/components/seo-issue-fix-panel.render.test.tsx +42 -11
- package/src/__tests__/lib/seo-service-errors.test.ts +4 -0
- package/src/__tests__/lib/seo-service-geo.test.ts +118 -0
- package/src/__tests__/lib/seo-service-proposals.test.ts +30 -0
- package/src/__tests__/router/seo-tab-for-path.test.ts +5 -0
- package/src/__tests__/views/seo-ai-visibility.render.test.tsx +279 -0
- package/src/__tests__/views/seo-geo-settings-card.render.test.tsx +253 -0
- package/src/__tests__/views/seo-proposals-tab.render.test.tsx +358 -0
- package/src/__tests__/views/seo-settings.render.test.tsx +28 -7
- package/src/__tests__/views/seo-technical-tab.render.test.tsx +15 -0
- package/src/components/seo/FaqProposalPreview.tsx +134 -0
- package/src/components/seo/SeoIssueFixPanel.tsx +7 -2
- package/src/lib/seo-service.ts +162 -1
- package/src/views/SEO.tsx +15 -6
- package/src/views/seo/AiVisibilityTab.tsx +616 -0
- package/src/views/seo/ProposalsTab.tsx +279 -13
- package/src/views/seo/TechnicalTab.tsx +27 -8
- package/src/views/settings/SeoAutopilotCard.tsx +6 -0
- package/src/views/settings/SeoGeoSettingsCard.tsx +310 -0
- package/src/views/settings/SeoSettingsTab.tsx +2 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useId, useRef, useState } from 'react'
|
|
4
|
+
import { AlertTriangle, RefreshCw } from 'lucide-react'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import {
|
|
7
|
+
SEO_GEO_RETENTION,
|
|
8
|
+
fetchGeoSettings,
|
|
9
|
+
updateGeoSettings,
|
|
10
|
+
type SeoGeoSettings,
|
|
11
|
+
type SeoGeoSettingsPatch,
|
|
12
|
+
} from '../../lib/seo-service.js'
|
|
13
|
+
import { Skeleton } from '../../components/ui/Skeleton.js'
|
|
14
|
+
import { SettingsCard, SettingsToggleRow } from './components.js'
|
|
15
|
+
|
|
16
|
+
const INPUT_CLASS =
|
|
17
|
+
'w-32 rounded-md border border-border bg-input-background px-3 py-2 text-base text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60 aria-invalid:border-destructive'
|
|
18
|
+
/** Horizontally scrollable, so focusable (`tabIndex={0}`) for keyboard scrolling. */
|
|
19
|
+
const SNIPPET_CLASS =
|
|
20
|
+
'bg-muted text-foreground focus-visible:ring-ring overflow-x-auto rounded-md p-3 text-sm focus-visible:ring-2 focus-visible:outline-none'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The two host-side one-liners. Kept as plain strings so the test can pin the
|
|
24
|
+
* identifiers (`observeGeoTraffic`, `createAstroRedirectMiddleware`) against
|
|
25
|
+
* the `@prenta/core/site` and `@prenta/core/astro/redirects` exports.
|
|
26
|
+
*/
|
|
27
|
+
const NEXT_SNIPPET = `import { applyPublicRedirect, observeGeoTraffic } from '@prenta/core/site'
|
|
28
|
+
// inside proxy(), just before the applyPublicRedirect call:
|
|
29
|
+
observeGeoTraffic(request, { baseUrl: origin, adminPaths: ['/admin'] })`
|
|
30
|
+
const ASTRO_SNIPPET = `// src/middleware.ts — GEO capture is on by default:
|
|
31
|
+
import { createAstroRedirectMiddleware } from '@prenta/core/astro/redirects'
|
|
32
|
+
export const onRequest = createAstroRedirectMiddleware({ baseUrl: import.meta.env.PRENTA_CMS_URL })`
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Settings → SEO "AI visibility" card (spec 2026-09-08 §6): the measurement
|
|
36
|
+
* kill switch, daily-bucket retention, and the one-line middleware setup for
|
|
37
|
+
* Next and Astro hosts. Owns its own fetch/save like `SeoAutopilotCard`; every
|
|
38
|
+
* control PUTs a sparse patch immediately, saves queue behind each other, and
|
|
39
|
+
* the card re-reads the server's resolved settings after each success. Takes
|
|
40
|
+
* no `onNavigate`, so it names the AI visibility tab in copy without linking.
|
|
41
|
+
*/
|
|
42
|
+
export function SeoGeoSettingsCard({ canEdit }: { canEdit: boolean }) {
|
|
43
|
+
const baseId = useId()
|
|
44
|
+
const [settings, setSettings] = useState<SeoGeoSettings | null>(null)
|
|
45
|
+
/** Mirrors `settings` synchronously so a queued save reads the latest resolved values. */
|
|
46
|
+
const settingsRef = useRef<SeoGeoSettings | null>(null)
|
|
47
|
+
const [loading, setLoading] = useState(true)
|
|
48
|
+
const [busy, setBusy] = useState(false)
|
|
49
|
+
/** Saves are serialised through this chain instead of being dropped when one is in flight. */
|
|
50
|
+
const saveChainRef = useRef<Promise<unknown>>(Promise.resolve())
|
|
51
|
+
const mountedRef = useRef(true)
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
mountedRef.current = true
|
|
55
|
+
return () => {
|
|
56
|
+
mountedRef.current = false
|
|
57
|
+
}
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
const commitSettings = useCallback((next: SeoGeoSettings) => {
|
|
61
|
+
settingsRef.current = next
|
|
62
|
+
setSettings(next)
|
|
63
|
+
}, [])
|
|
64
|
+
|
|
65
|
+
const load = useCallback(async () => {
|
|
66
|
+
setLoading(true)
|
|
67
|
+
const s = await fetchGeoSettings()
|
|
68
|
+
if (!mountedRef.current) return
|
|
69
|
+
if (s) commitSettings(s)
|
|
70
|
+
setLoading(false)
|
|
71
|
+
}, [commitSettings])
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
void load()
|
|
75
|
+
}, [load])
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Re-read without the skeleton after a successful save. The PUT answers only
|
|
79
|
+
* `{ ok }`, so the server-resolved values come from a fresh GET; if that GET
|
|
80
|
+
* fails, the accepted patch is applied locally — it is exactly what the
|
|
81
|
+
* server stored (the card clamps retention the way the server does).
|
|
82
|
+
*/
|
|
83
|
+
const refresh = useCallback(
|
|
84
|
+
async (accepted: SeoGeoSettingsPatch) => {
|
|
85
|
+
const s = await fetchGeoSettings()
|
|
86
|
+
if (!mountedRef.current) return
|
|
87
|
+
if (s) commitSettings(s)
|
|
88
|
+
else if (settingsRef.current) commitSettings({ ...settingsRef.current, ...accepted })
|
|
89
|
+
},
|
|
90
|
+
[commitSettings],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
/** Resolves `true` when the server accepted the patch. */
|
|
94
|
+
async function performSave(patch: SeoGeoSettingsPatch, successMessage: string): Promise<boolean> {
|
|
95
|
+
if (!settingsRef.current || !mountedRef.current) return false
|
|
96
|
+
setBusy(true)
|
|
97
|
+
try {
|
|
98
|
+
const res = await updateGeoSettings(patch)
|
|
99
|
+
if (!mountedRef.current) return false
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
toast.error(res.error ?? 'Failed to update AI visibility settings')
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
toast.success(successMessage)
|
|
105
|
+
await refresh(patch)
|
|
106
|
+
return true
|
|
107
|
+
} finally {
|
|
108
|
+
if (mountedRef.current) setBusy(false)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Queue a sparse patch behind any save already in flight. Callers await the
|
|
114
|
+
* result to know whether to keep or reset a local draft.
|
|
115
|
+
*/
|
|
116
|
+
function save(patch: SeoGeoSettingsPatch, successMessage: string): Promise<boolean> {
|
|
117
|
+
const next = saveChainRef.current.then(() => performSave(patch, successMessage))
|
|
118
|
+
saveChainRef.current = next.catch(() => undefined)
|
|
119
|
+
return next
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (loading) {
|
|
123
|
+
return (
|
|
124
|
+
<SettingsCard title="AI visibility">
|
|
125
|
+
<div aria-busy="true" aria-live="polite">
|
|
126
|
+
<span className="sr-only">Loading AI visibility settings…</span>
|
|
127
|
+
<Skeleton lines={4} />
|
|
128
|
+
</div>
|
|
129
|
+
</SettingsCard>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
if (!settings) {
|
|
133
|
+
return (
|
|
134
|
+
<SettingsCard title="AI visibility">
|
|
135
|
+
<div
|
|
136
|
+
role="alert"
|
|
137
|
+
className="border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-3 rounded-lg border p-4 text-sm"
|
|
138
|
+
>
|
|
139
|
+
<AlertTriangle size={16} className="shrink-0" aria-hidden="true" />
|
|
140
|
+
<span className="flex-1">Failed to load AI visibility settings.</span>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={() => void load()}
|
|
144
|
+
className="border-destructive/40 focus-visible:ring-ring inline-flex items-center gap-2 rounded-md border px-3 py-1 transition-colors hover:opacity-80 focus-visible:ring-2 focus-visible:outline-none"
|
|
145
|
+
>
|
|
146
|
+
<RefreshCw size={14} aria-hidden="true" />
|
|
147
|
+
Retry
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
</SettingsCard>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Controls lock only for read-only users; a save in flight must not steal
|
|
155
|
+
// focus (saves queue), so the card is marked busy instead.
|
|
156
|
+
const disabled = !canEdit
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<SettingsCard
|
|
160
|
+
title="AI visibility"
|
|
161
|
+
description="Record which AI and search crawlers fetch your pages and which answer engines send visitors. Only bot names, engine names, and your own paths are stored — never IPs, user agents, or referrer URLs."
|
|
162
|
+
busy={busy}
|
|
163
|
+
>
|
|
164
|
+
<div className="flex flex-col gap-5">
|
|
165
|
+
<SettingsToggleRow
|
|
166
|
+
label="Record AI crawler and referral activity"
|
|
167
|
+
description="When off, the server drops incoming beacons and the AI visibility tab in SEO shows a disabled state. Existing data stays until retention prunes it."
|
|
168
|
+
checked={settings.enabled}
|
|
169
|
+
onChange={(v) =>
|
|
170
|
+
void save(
|
|
171
|
+
{ enabled: v },
|
|
172
|
+
v ? 'AI visibility measurement enabled' : 'AI visibility measurement disabled',
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
<RetentionField
|
|
179
|
+
id={`${baseId}-retention`}
|
|
180
|
+
value={settings.retentionDays}
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
onCommit={(v) => save({ retentionDays: v }, `Retention set to ${v} days`)}
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
<div className="border-border flex flex-col gap-3 border-t pt-4">
|
|
186
|
+
<div>
|
|
187
|
+
<p className="text-foreground text-base font-medium">Site middleware setup</p>
|
|
188
|
+
<p className="text-muted-foreground mt-0.5 text-sm">
|
|
189
|
+
Counts arrive only once the public site reports them. Add one line to the host that
|
|
190
|
+
serves your pages.
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
<div>
|
|
194
|
+
<p className="text-muted-foreground mb-1 text-sm">
|
|
195
|
+
Next.js — <code className="text-foreground">proxy.ts</code>
|
|
196
|
+
</p>
|
|
197
|
+
<pre className={SNIPPET_CLASS} tabIndex={0} aria-label="Next.js middleware snippet">
|
|
198
|
+
<code>{NEXT_SNIPPET}</code>
|
|
199
|
+
</pre>
|
|
200
|
+
</div>
|
|
201
|
+
<div>
|
|
202
|
+
<p className="text-muted-foreground mb-1 text-sm">Astro</p>
|
|
203
|
+
<pre className={SNIPPET_CLASS} tabIndex={0} aria-label="Astro middleware snippet">
|
|
204
|
+
<code>{ASTRO_SNIPPET}</code>
|
|
205
|
+
</pre>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</SettingsCard>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Retention input that commits on blur (or Enter). The draft is local so typing
|
|
215
|
+
* never fires a PUT per keystroke; an unchanged blur restores the saved value
|
|
216
|
+
* without a request. Out-of-range integers are clamped to 7–365 before they are
|
|
217
|
+
* sent (the server clamps identically). An empty, non-numeric, or fractional
|
|
218
|
+
* draft is a client-side validation failure: it shows an inline error and sends
|
|
219
|
+
* nothing. A rejected commit (`onCommit` resolves `false`) falls back to the
|
|
220
|
+
* last saved value so the input never shows text the server refused.
|
|
221
|
+
*/
|
|
222
|
+
function RetentionField({
|
|
223
|
+
id,
|
|
224
|
+
value,
|
|
225
|
+
disabled,
|
|
226
|
+
onCommit,
|
|
227
|
+
}: {
|
|
228
|
+
id: string
|
|
229
|
+
value: number
|
|
230
|
+
disabled: boolean
|
|
231
|
+
onCommit: (v: number) => Promise<boolean>
|
|
232
|
+
}) {
|
|
233
|
+
const [draft, setDraft] = useState(String(value))
|
|
234
|
+
const [syncedValue, setSyncedValue] = useState(value)
|
|
235
|
+
const [error, setError] = useState<string | null>(null)
|
|
236
|
+
// Adopt a new server value without an effect: reset the draft during render
|
|
237
|
+
// when the prop changes (React's recommended "derive state from props" form).
|
|
238
|
+
if (value !== syncedValue) {
|
|
239
|
+
setSyncedValue(value)
|
|
240
|
+
setDraft(String(value))
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Latest confirmed value, readable from a commit's `.then` after later
|
|
244
|
+
* renders. Saves queue, so a second commit can fail after an earlier one
|
|
245
|
+
* changed `value`; reverting to the closed-over `value` would show the
|
|
246
|
+
* pre-first-save number while the server holds the first save.
|
|
247
|
+
*/
|
|
248
|
+
const valueRef = useRef(value)
|
|
249
|
+
valueRef.current = value
|
|
250
|
+
const hintId = `${id}-hint`
|
|
251
|
+
const errorId = `${id}-error`
|
|
252
|
+
const { min, max } = SEO_GEO_RETENTION
|
|
253
|
+
|
|
254
|
+
function commit() {
|
|
255
|
+
const n = Number(draft)
|
|
256
|
+
if (draft.trim() === '' || !Number.isInteger(n)) {
|
|
257
|
+
setError('Retention must be a whole number of days')
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
const next = Math.max(min, Math.min(max, n))
|
|
261
|
+
if (next === valueRef.current) {
|
|
262
|
+
setDraft(String(valueRef.current))
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
void onCommit(next).then((ok) => {
|
|
266
|
+
// On success the parent adopts the server value and the derive-from-props
|
|
267
|
+
// branch above resets the draft; on failure revert to the latest confirmed
|
|
268
|
+
// value (not the one closed over when this commit started).
|
|
269
|
+
if (!ok) setDraft(String(valueRef.current))
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<div>
|
|
275
|
+
<label htmlFor={id} className="text-foreground mb-1 block text-sm font-medium">
|
|
276
|
+
Retention (days)
|
|
277
|
+
</label>
|
|
278
|
+
<input
|
|
279
|
+
id={id}
|
|
280
|
+
type="number"
|
|
281
|
+
inputMode="numeric"
|
|
282
|
+
className={INPUT_CLASS}
|
|
283
|
+
min={min}
|
|
284
|
+
max={max}
|
|
285
|
+
step={1}
|
|
286
|
+
value={draft}
|
|
287
|
+
disabled={disabled}
|
|
288
|
+
aria-invalid={error ? true : undefined}
|
|
289
|
+
aria-describedby={error ? `${hintId} ${errorId}` : hintId}
|
|
290
|
+
onChange={(e) => {
|
|
291
|
+
setDraft(e.target.value)
|
|
292
|
+
if (error) setError(null)
|
|
293
|
+
}}
|
|
294
|
+
onBlur={commit}
|
|
295
|
+
onKeyDown={(e) => {
|
|
296
|
+
if (e.key === 'Enter') e.currentTarget.blur()
|
|
297
|
+
}}
|
|
298
|
+
/>
|
|
299
|
+
<p id={hintId} className="text-muted-foreground mt-1 text-sm">
|
|
300
|
+
Daily counts older than this are deleted by the nightly cleanup. {min}–{max} days.
|
|
301
|
+
</p>
|
|
302
|
+
{error ? (
|
|
303
|
+
<p id={errorId} className="text-destructive mt-1 flex items-center gap-1 text-sm">
|
|
304
|
+
<AlertTriangle size={14} className="shrink-0" aria-hidden="true" />
|
|
305
|
+
{error}
|
|
306
|
+
</p>
|
|
307
|
+
) : null}
|
|
308
|
+
</div>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
@@ -19,6 +19,7 @@ import { SEOConfigPanel } from '../../components/SEOConfigPanel.js'
|
|
|
19
19
|
import { SearchDataCard } from './SearchDataCard.js'
|
|
20
20
|
import { SeoAutopilotCard } from './SeoAutopilotCard.js'
|
|
21
21
|
import { SeoDigestCard } from './SeoDigestCard.js'
|
|
22
|
+
import { SeoGeoSettingsCard } from './SeoGeoSettingsCard.js'
|
|
22
23
|
import {
|
|
23
24
|
ConfirmDangerousSettingDialog,
|
|
24
25
|
SettingsCard,
|
|
@@ -153,6 +154,7 @@ export function SeoSettingsTab({ onNavigate, role }: SeoSettingsTabProps) {
|
|
|
153
154
|
<OrganizationCard seo={seo} canEdit={canEdit} />
|
|
154
155
|
<SearchEngineVerificationCard seo={seo} canEdit={canEdit} />
|
|
155
156
|
<SearchDataCard canEdit={canEdit} />
|
|
157
|
+
<SeoGeoSettingsCard canEdit={canEdit} />
|
|
156
158
|
<SeoAutopilotCard canEdit={canEdit} onNavigate={onNavigate} />
|
|
157
159
|
<SeoDigestCard canEdit={canEdit} />
|
|
158
160
|
<SitemapDefaultsCard seo={seo} canEdit={canEdit} onNavigate={onNavigate} />
|