@prenta/admin 1.15.0 → 1.16.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 +6 -0
- 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__/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-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/lib/seo-service.d.ts +94 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +33 -0
- 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/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/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__/lib/seo-service-geo.test.ts +118 -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-settings.render.test.tsx +28 -7
- package/src/__tests__/views/seo-technical-tab.render.test.tsx +15 -0
- package/src/lib/seo-service.ts +105 -0
- package/src/views/SEO.tsx +15 -6
- package/src/views/seo/AiVisibilityTab.tsx +616 -0
- package/src/views/seo/TechnicalTab.tsx +27 -8
- package/src/views/settings/SeoGeoSettingsCard.tsx +310 -0
- package/src/views/settings/SeoSettingsTab.tsx +2 -0
package/src/lib/seo-service.ts
CHANGED
|
@@ -1140,6 +1140,111 @@ export async function fetchAiCrawlerActivity(): Promise<AiCrawlerActivity> {
|
|
|
1140
1140
|
return res.data ?? { enabled: false, rows: [], totals: { hits: 0, bots: 0, honeypot: 0 } }
|
|
1141
1141
|
}
|
|
1142
1142
|
|
|
1143
|
+
// ─── AI visibility (GEO measurement) ────────────────────────────────────────
|
|
1144
|
+
// Mirrors `packages/cms-core/src/seo/geo-report.ts` (`SeoGeoReport`) and the
|
|
1145
|
+
// `GET /seo/geo?days=N` route. The server is the contract; keep in lock-step.
|
|
1146
|
+
|
|
1147
|
+
export type SeoGeoDays = 7 | 28 | 90
|
|
1148
|
+
export type SeoGeoBotCategory = 'training' | 'retrieval' | 'search'
|
|
1149
|
+
/**
|
|
1150
|
+
* `blocked_still_crawling` = robots.txt disallows this bot's category, yet it
|
|
1151
|
+
* was observed in the window — the signal the tab surfaces most prominently.
|
|
1152
|
+
*/
|
|
1153
|
+
export type SeoGeoBotPolicy = 'allowed' | 'blocked' | 'blocked_still_crawling'
|
|
1154
|
+
|
|
1155
|
+
/** A top-path row's optional join to the document behind it. */
|
|
1156
|
+
export interface SeoGeoEntityRef {
|
|
1157
|
+
entityId?: string
|
|
1158
|
+
entityTitle?: string
|
|
1159
|
+
/** Collection slug of the matched document; routes the editor link. */
|
|
1160
|
+
collection?: string
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Why `enabled` is `false`: the kill switch in Settings → SEO (`settings`) or
|
|
1165
|
+
* the GEO tables are not migrated on the CMS database (`unavailable`).
|
|
1166
|
+
*/
|
|
1167
|
+
export type SeoGeoDisabledReason = 'settings' | 'unavailable'
|
|
1168
|
+
|
|
1169
|
+
export interface SeoGeoReport {
|
|
1170
|
+
enabled: boolean
|
|
1171
|
+
/** Present only when `enabled` is `false`. */
|
|
1172
|
+
disabledReason?: SeoGeoDisabledReason
|
|
1173
|
+
window: { from: string; to: string; days: number }
|
|
1174
|
+
/** ISO date of the earliest recorded row, or null when nothing has arrived. */
|
|
1175
|
+
firstSeenAt: string | null
|
|
1176
|
+
crawl: {
|
|
1177
|
+
/** Whole-window crawl volume (incl. the capped "other" bucket). */
|
|
1178
|
+
total: { hits: number; prevHits: number | null }
|
|
1179
|
+
series: Array<{ date: string; hits: Record<string, number> }>
|
|
1180
|
+
bots: Array<{
|
|
1181
|
+
bot: string
|
|
1182
|
+
category: SeoGeoBotCategory
|
|
1183
|
+
hits: number
|
|
1184
|
+
/** `null` when the prior window is not fully covered by data. */
|
|
1185
|
+
prevHits: number | null
|
|
1186
|
+
lastSeenAt: string | null
|
|
1187
|
+
policy: SeoGeoBotPolicy
|
|
1188
|
+
}>
|
|
1189
|
+
topPaths: Array<SeoGeoEntityRef & { path: string; hits: number; bots: string[] }>
|
|
1190
|
+
/** Hits that rolled into the capped "other" bucket. */
|
|
1191
|
+
otherHits: number
|
|
1192
|
+
}
|
|
1193
|
+
referrals: {
|
|
1194
|
+
total: { visits: number; prevVisits: number | null }
|
|
1195
|
+
engines: Array<{ engine: string; visits: number; prevVisits: number | null }>
|
|
1196
|
+
topPaths: Array<SeoGeoEntityRef & { path: string; visits: number; engines: string[] }>
|
|
1197
|
+
/** Visits that rolled into the capped "other" bucket. */
|
|
1198
|
+
otherVisits: number
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
export async function fetchSeoGeo(days: SeoGeoDays): Promise<SeoGeoReport> {
|
|
1203
|
+
const res = await cmsApi<SeoGeoReport>(`/seo/geo?days=${days}`)
|
|
1204
|
+
throwIfError(res)
|
|
1205
|
+
// Validate the shape before handing it to the tab — an unexpected body must
|
|
1206
|
+
// render the error state, not crash in render. `crawl.total` is the newest
|
|
1207
|
+
// field on the wire; a `@prenta/core` deployed before it lands here.
|
|
1208
|
+
const d = res.data
|
|
1209
|
+
if (!d || typeof d.enabled !== 'boolean' || typeof d.crawl?.total?.hits !== 'number') {
|
|
1210
|
+
throw new Error('Unexpected AI visibility response')
|
|
1211
|
+
}
|
|
1212
|
+
return d
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Mirrors `GET|PUT /seo/geo-settings` (`routes/seo-geo.ts`). The server clamps
|
|
1216
|
+
// `retentionDays` to `SEO_GEO_RETENTION` itself; the card clamps first so the
|
|
1217
|
+
// value it shows is the value that lands.
|
|
1218
|
+
export const SEO_GEO_RETENTION = { min: 7, max: 365 } as const
|
|
1219
|
+
|
|
1220
|
+
export interface SeoGeoSettings {
|
|
1221
|
+
/** Kill switch: when off the server drops incoming beacons. */
|
|
1222
|
+
enabled: boolean
|
|
1223
|
+
/** Daily-bucket retention, 7–365 days. */
|
|
1224
|
+
retentionDays: number
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
export type SeoGeoSettingsPatch = Partial<SeoGeoSettings>
|
|
1228
|
+
|
|
1229
|
+
/** Resolves `null` on an error envelope or a malformed body so the card renders its error state. */
|
|
1230
|
+
export async function fetchGeoSettings(): Promise<SeoGeoSettings | null> {
|
|
1231
|
+
const res = await cmsApi<SeoGeoSettings>('/seo/geo-settings')
|
|
1232
|
+
const d = res.data
|
|
1233
|
+
if (!d || typeof d.enabled !== 'boolean' || typeof d.retentionDays !== 'number') return null
|
|
1234
|
+
return { enabled: d.enabled, retentionDays: d.retentionDays }
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
export async function updateGeoSettings(
|
|
1238
|
+
patch: SeoGeoSettingsPatch,
|
|
1239
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
1240
|
+
const res = await cmsApi<{ ok: boolean }>('/seo/geo-settings', {
|
|
1241
|
+
method: 'PUT',
|
|
1242
|
+
body: JSON.stringify(patch),
|
|
1243
|
+
})
|
|
1244
|
+
if (res.error) return { ok: false, error: res.error }
|
|
1245
|
+
return { ok: true }
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1143
1248
|
export interface RobotsConfig {
|
|
1144
1249
|
body: string
|
|
1145
1250
|
generated: boolean
|
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} />}
|