@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
package/src/lib/seo-service.ts
CHANGED
|
@@ -501,15 +501,30 @@ export async function fetchSeoIssueFixSuggestion(issueId: string): Promise<{
|
|
|
501
501
|
return { suggestion: res.data?.suggestion }
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Per-approval refinements for an `add-section` (FAQ) fix. Both are ignored by
|
|
506
|
+
* the server for every other fix strategy.
|
|
507
|
+
*/
|
|
508
|
+
export interface SeoApplyFixOptions {
|
|
509
|
+
/** Indices into `section.content.items` to keep; the server requires at least two. */
|
|
510
|
+
includeItems?: number[]
|
|
511
|
+
/** Replacement section heading (server trims, then enforces `FAQ_HEADING_MAX`, mirrored from core). */
|
|
512
|
+
headingOverride?: string
|
|
513
|
+
}
|
|
514
|
+
|
|
504
515
|
export async function applySeoIssueFix(
|
|
505
516
|
issueId: string,
|
|
506
517
|
fingerprint: string,
|
|
518
|
+
opts?: SeoApplyFixOptions,
|
|
507
519
|
): Promise<{ issue?: SeoIssue; error?: string }> {
|
|
520
|
+
const body: Record<string, unknown> = { fingerprint }
|
|
521
|
+
if (opts?.includeItems) body.includeItems = opts.includeItems
|
|
522
|
+
if (opts?.headingOverride !== undefined) body.headingOverride = opts.headingOverride
|
|
508
523
|
const res = await cmsApi<{ issue: SeoIssue }>(
|
|
509
524
|
`/seo/issues/${encodeURIComponent(issueId)}/apply-fix`,
|
|
510
525
|
{
|
|
511
526
|
method: 'POST',
|
|
512
|
-
body: JSON.stringify(
|
|
527
|
+
body: JSON.stringify(body),
|
|
513
528
|
},
|
|
514
529
|
)
|
|
515
530
|
if (res.error) return { error: res.error }
|
|
@@ -561,6 +576,7 @@ export const SEO_INLINE_FIX_ISSUE_TYPES = new Set([
|
|
|
561
576
|
'duplicate-meta-title',
|
|
562
577
|
'duplicate-meta-description',
|
|
563
578
|
'missing-structured-data',
|
|
579
|
+
'faq-schema-missing',
|
|
564
580
|
'broken-internal-link',
|
|
565
581
|
'orphan-page',
|
|
566
582
|
])
|
|
@@ -843,6 +859,7 @@ export type SeoAutopilotIssueType =
|
|
|
843
859
|
| 'missing-canonical'
|
|
844
860
|
| 'accidental-noindex'
|
|
845
861
|
| 'missing-structured-data'
|
|
862
|
+
| 'faq-schema-missing'
|
|
846
863
|
| 'redirect'
|
|
847
864
|
|
|
848
865
|
export const SEO_AUTOPILOT_ISSUE_TYPES: readonly SeoAutopilotIssueType[] = [
|
|
@@ -855,6 +872,7 @@ export const SEO_AUTOPILOT_ISSUE_TYPES: readonly SeoAutopilotIssueType[] = [
|
|
|
855
872
|
'missing-canonical',
|
|
856
873
|
'accidental-noindex',
|
|
857
874
|
'missing-structured-data',
|
|
875
|
+
'faq-schema-missing',
|
|
858
876
|
'redirect',
|
|
859
877
|
]
|
|
860
878
|
|
|
@@ -1140,6 +1158,111 @@ export async function fetchAiCrawlerActivity(): Promise<AiCrawlerActivity> {
|
|
|
1140
1158
|
return res.data ?? { enabled: false, rows: [], totals: { hits: 0, bots: 0, honeypot: 0 } }
|
|
1141
1159
|
}
|
|
1142
1160
|
|
|
1161
|
+
// ─── AI visibility (GEO measurement) ────────────────────────────────────────
|
|
1162
|
+
// Mirrors `packages/cms-core/src/seo/geo-report.ts` (`SeoGeoReport`) and the
|
|
1163
|
+
// `GET /seo/geo?days=N` route. The server is the contract; keep in lock-step.
|
|
1164
|
+
|
|
1165
|
+
export type SeoGeoDays = 7 | 28 | 90
|
|
1166
|
+
export type SeoGeoBotCategory = 'training' | 'retrieval' | 'search'
|
|
1167
|
+
/**
|
|
1168
|
+
* `blocked_still_crawling` = robots.txt disallows this bot's category, yet it
|
|
1169
|
+
* was observed in the window — the signal the tab surfaces most prominently.
|
|
1170
|
+
*/
|
|
1171
|
+
export type SeoGeoBotPolicy = 'allowed' | 'blocked' | 'blocked_still_crawling'
|
|
1172
|
+
|
|
1173
|
+
/** A top-path row's optional join to the document behind it. */
|
|
1174
|
+
export interface SeoGeoEntityRef {
|
|
1175
|
+
entityId?: string
|
|
1176
|
+
entityTitle?: string
|
|
1177
|
+
/** Collection slug of the matched document; routes the editor link. */
|
|
1178
|
+
collection?: string
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Why `enabled` is `false`: the kill switch in Settings → SEO (`settings`) or
|
|
1183
|
+
* the GEO tables are not migrated on the CMS database (`unavailable`).
|
|
1184
|
+
*/
|
|
1185
|
+
export type SeoGeoDisabledReason = 'settings' | 'unavailable'
|
|
1186
|
+
|
|
1187
|
+
export interface SeoGeoReport {
|
|
1188
|
+
enabled: boolean
|
|
1189
|
+
/** Present only when `enabled` is `false`. */
|
|
1190
|
+
disabledReason?: SeoGeoDisabledReason
|
|
1191
|
+
window: { from: string; to: string; days: number }
|
|
1192
|
+
/** ISO date of the earliest recorded row, or null when nothing has arrived. */
|
|
1193
|
+
firstSeenAt: string | null
|
|
1194
|
+
crawl: {
|
|
1195
|
+
/** Whole-window crawl volume (incl. the capped "other" bucket). */
|
|
1196
|
+
total: { hits: number; prevHits: number | null }
|
|
1197
|
+
series: Array<{ date: string; hits: Record<string, number> }>
|
|
1198
|
+
bots: Array<{
|
|
1199
|
+
bot: string
|
|
1200
|
+
category: SeoGeoBotCategory
|
|
1201
|
+
hits: number
|
|
1202
|
+
/** `null` when the prior window is not fully covered by data. */
|
|
1203
|
+
prevHits: number | null
|
|
1204
|
+
lastSeenAt: string | null
|
|
1205
|
+
policy: SeoGeoBotPolicy
|
|
1206
|
+
}>
|
|
1207
|
+
topPaths: Array<SeoGeoEntityRef & { path: string; hits: number; bots: string[] }>
|
|
1208
|
+
/** Hits that rolled into the capped "other" bucket. */
|
|
1209
|
+
otherHits: number
|
|
1210
|
+
}
|
|
1211
|
+
referrals: {
|
|
1212
|
+
total: { visits: number; prevVisits: number | null }
|
|
1213
|
+
engines: Array<{ engine: string; visits: number; prevVisits: number | null }>
|
|
1214
|
+
topPaths: Array<SeoGeoEntityRef & { path: string; visits: number; engines: string[] }>
|
|
1215
|
+
/** Visits that rolled into the capped "other" bucket. */
|
|
1216
|
+
otherVisits: number
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
export async function fetchSeoGeo(days: SeoGeoDays): Promise<SeoGeoReport> {
|
|
1221
|
+
const res = await cmsApi<SeoGeoReport>(`/seo/geo?days=${days}`)
|
|
1222
|
+
throwIfError(res)
|
|
1223
|
+
// Validate the shape before handing it to the tab — an unexpected body must
|
|
1224
|
+
// render the error state, not crash in render. `crawl.total` is the newest
|
|
1225
|
+
// field on the wire; a `@prenta/core` deployed before it lands here.
|
|
1226
|
+
const d = res.data
|
|
1227
|
+
if (!d || typeof d.enabled !== 'boolean' || typeof d.crawl?.total?.hits !== 'number') {
|
|
1228
|
+
throw new Error('Unexpected AI visibility response')
|
|
1229
|
+
}
|
|
1230
|
+
return d
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Mirrors `GET|PUT /seo/geo-settings` (`routes/seo-geo.ts`). The server clamps
|
|
1234
|
+
// `retentionDays` to `SEO_GEO_RETENTION` itself; the card clamps first so the
|
|
1235
|
+
// value it shows is the value that lands.
|
|
1236
|
+
export const SEO_GEO_RETENTION = { min: 7, max: 365 } as const
|
|
1237
|
+
|
|
1238
|
+
export interface SeoGeoSettings {
|
|
1239
|
+
/** Kill switch: when off the server drops incoming beacons. */
|
|
1240
|
+
enabled: boolean
|
|
1241
|
+
/** Daily-bucket retention, 7–365 days. */
|
|
1242
|
+
retentionDays: number
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export type SeoGeoSettingsPatch = Partial<SeoGeoSettings>
|
|
1246
|
+
|
|
1247
|
+
/** Resolves `null` on an error envelope or a malformed body so the card renders its error state. */
|
|
1248
|
+
export async function fetchGeoSettings(): Promise<SeoGeoSettings | null> {
|
|
1249
|
+
const res = await cmsApi<SeoGeoSettings>('/seo/geo-settings')
|
|
1250
|
+
const d = res.data
|
|
1251
|
+
if (!d || typeof d.enabled !== 'boolean' || typeof d.retentionDays !== 'number') return null
|
|
1252
|
+
return { enabled: d.enabled, retentionDays: d.retentionDays }
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
export async function updateGeoSettings(
|
|
1256
|
+
patch: SeoGeoSettingsPatch,
|
|
1257
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
1258
|
+
const res = await cmsApi<{ ok: boolean }>('/seo/geo-settings', {
|
|
1259
|
+
method: 'PUT',
|
|
1260
|
+
body: JSON.stringify(patch),
|
|
1261
|
+
})
|
|
1262
|
+
if (res.error) return { ok: false, error: res.error }
|
|
1263
|
+
return { ok: true }
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1143
1266
|
export interface RobotsConfig {
|
|
1144
1267
|
body: string
|
|
1145
1268
|
generated: boolean
|
|
@@ -1642,6 +1765,28 @@ export async function fetchLinkHealthIssues(): Promise<{
|
|
|
1642
1765
|
}
|
|
1643
1766
|
|
|
1644
1767
|
// ─── AI proposals inbox ─────────────────────────────────────────────────
|
|
1768
|
+
// Mirrors `packages/cms-core/src/seo/proposals.ts` (`SeoProposal`) and
|
|
1769
|
+
// `seo/faq-signals.ts` (`FaqSignals`). The server is the contract.
|
|
1770
|
+
|
|
1771
|
+
/** Same union as core's `SeoProposalFixStrategy`; `null` for plain metadata writes. */
|
|
1772
|
+
export type SeoProposalFixStrategy = 'redirect' | 'fix-link' | 'insert-link' | 'add-section'
|
|
1773
|
+
|
|
1774
|
+
/** Why a page got a `missing-faq` proposal (28-day window). */
|
|
1775
|
+
export interface SeoFaqSignals {
|
|
1776
|
+
/**
|
|
1777
|
+
* Answer-engine activity. `engines` carries the referrer registry's display
|
|
1778
|
+
* names (`ChatGPT`, `Perplexity`, …), sorted; empty when only retrieval
|
|
1779
|
+
* crawls were seen.
|
|
1780
|
+
*/
|
|
1781
|
+
geo: { referralVisits: number; retrievalHits: number; engines: string[] } | null
|
|
1782
|
+
/** Question-shaped Search Console queries, highest impressions first. */
|
|
1783
|
+
queries: Array<{ query: string; impressions: number; clicks: number }>
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
export interface SeoFaqSectionDraft {
|
|
1787
|
+
sectionType: 'faq'
|
|
1788
|
+
content: { heading: string; items: Array<{ question: string; answer: string }> }
|
|
1789
|
+
}
|
|
1645
1790
|
|
|
1646
1791
|
export interface SeoProposal {
|
|
1647
1792
|
/** `issue:<issueId>` | `redirect:<suggestionId>` */
|
|
@@ -1665,6 +1810,22 @@ export interface SeoProposal {
|
|
|
1665
1810
|
severity: SeoSeverity | null
|
|
1666
1811
|
issueId: string | null
|
|
1667
1812
|
suggestionId: string | null
|
|
1813
|
+
/** Always `'redirect'` for redirect rows; `null` when the cached patch carries none. */
|
|
1814
|
+
fixStrategy: SeoProposalFixStrategy | null
|
|
1815
|
+
/**
|
|
1816
|
+
* `add-section` only: the section approval will insert. `null` when the
|
|
1817
|
+
* cached patch failed core's structural checks — the row must still render
|
|
1818
|
+
* (badge + signals) but cannot be approved from the inbox.
|
|
1819
|
+
*/
|
|
1820
|
+
section: SeoFaqSectionDraft | null
|
|
1821
|
+
/**
|
|
1822
|
+
* `add-section` only: verbatim page quotes parallel to `section.content.items`.
|
|
1823
|
+
* A malformed row arrives as `sourceQuote: ''` — render as "no quote", never
|
|
1824
|
+
* as an empty blockquote.
|
|
1825
|
+
*/
|
|
1826
|
+
grounding: Array<{ sourceQuote: string }> | null
|
|
1827
|
+
/** `missing-faq` only. */
|
|
1828
|
+
signals: SeoFaqSignals | null
|
|
1668
1829
|
}
|
|
1669
1830
|
|
|
1670
1831
|
export interface SeoProposalsPayload {
|
package/src/views/SEO.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* SEO Operations Center —
|
|
5
|
-
* Links / Technical / Redirects / Audit / Proposals). Tab +
|
|
6
|
-
* driven by the `?tab=` query string so audits, issues,
|
|
7
|
-
* and content editors are all linkable. All data flows
|
|
8
|
-
* `lib/seo-service.ts`.
|
|
4
|
+
* SEO Operations Center — eight-tab workflow surface (Overview / Content /
|
|
5
|
+
* Links / Technical / AI visibility / Redirects / Audit / Proposals). Tab +
|
|
6
|
+
* deep-link state is driven by the `?tab=` query string so audits, issues,
|
|
7
|
+
* redirects, proposals, and content editors are all linkable. All data flows
|
|
8
|
+
* through `lib/seo-service.ts`.
|
|
9
9
|
*/
|
|
10
10
|
import * as Tabs from '@radix-ui/react-tabs'
|
|
11
11
|
import {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
Bot,
|
|
19
19
|
Link2,
|
|
20
20
|
Inbox,
|
|
21
|
+
Radar,
|
|
21
22
|
} from 'lucide-react'
|
|
22
23
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
23
24
|
import { toast } from 'sonner'
|
|
@@ -35,6 +36,7 @@ import { RedirectsTab } from './seo/RedirectsTab.js'
|
|
|
35
36
|
import { AuditTab } from './seo/AuditTab.js'
|
|
36
37
|
import { LinksTab } from './seo/LinksTab.js'
|
|
37
38
|
import { ProposalsTab } from './seo/ProposalsTab.js'
|
|
39
|
+
import { AiVisibilityTab } from './seo/AiVisibilityTab.js'
|
|
38
40
|
|
|
39
41
|
export interface SEOProps {
|
|
40
42
|
onNavigate?: (path: string) => void
|
|
@@ -46,6 +48,7 @@ const TABS = [
|
|
|
46
48
|
{ id: 'content', label: 'Content SEO', icon: FileText },
|
|
47
49
|
{ id: 'links', label: 'Links', icon: Link2 },
|
|
48
50
|
{ id: 'technical', label: 'Technical', icon: Settings2 },
|
|
51
|
+
{ id: 'ai-visibility', label: 'AI visibility', icon: Radar },
|
|
49
52
|
{ id: 'redirects', label: 'Redirects', icon: ArrowRightLeft },
|
|
50
53
|
{ id: 'audit', label: 'Audit', icon: ClipboardCheck },
|
|
51
54
|
{ id: 'proposals', label: 'Proposals', icon: Inbox },
|
|
@@ -74,6 +77,7 @@ export const SEO_DEEP_PATHS = [
|
|
|
74
77
|
'/seo/links',
|
|
75
78
|
'/seo/audit',
|
|
76
79
|
'/seo/proposals',
|
|
80
|
+
'/seo/ai-visibility',
|
|
77
81
|
'/seo',
|
|
78
82
|
] as const
|
|
79
83
|
|
|
@@ -96,6 +100,8 @@ export function seoTabForPath(pathname: string): TabId | null {
|
|
|
96
100
|
return 'audit'
|
|
97
101
|
case '/seo/proposals':
|
|
98
102
|
return 'proposals'
|
|
103
|
+
case '/seo/ai-visibility':
|
|
104
|
+
return 'ai-visibility'
|
|
99
105
|
case '/seo':
|
|
100
106
|
return 'overview'
|
|
101
107
|
default:
|
|
@@ -281,7 +287,10 @@ export function SEO({ onNavigate, initialTab = 'overview' }: SEOProps) {
|
|
|
281
287
|
{activeTab === 'links' && <LinksTab onNavigate={onNavigate} planInfo={planInfo} />}
|
|
282
288
|
</Tabs.Content>
|
|
283
289
|
<Tabs.Content value="technical" tabIndex={-1}>
|
|
284
|
-
{activeTab === 'technical' && <TechnicalTab />}
|
|
290
|
+
{activeTab === 'technical' && <TechnicalTab onNavigate={onNavigate} />}
|
|
291
|
+
</Tabs.Content>
|
|
292
|
+
<Tabs.Content value="ai-visibility" tabIndex={-1}>
|
|
293
|
+
{activeTab === 'ai-visibility' && <AiVisibilityTab onNavigate={onNavigate} />}
|
|
285
294
|
</Tabs.Content>
|
|
286
295
|
<Tabs.Content value="redirects" tabIndex={-1}>
|
|
287
296
|
{activeTab === 'redirects' && <RedirectsTab onNavigate={onNavigate} />}
|