@actuate-media/cms-admin 0.73.0 → 0.75.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 +28 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +3 -2
- package/dist/AdminRoot.js.map +1 -1
- package/dist/LoginRoot.d.ts.map +1 -1
- package/dist/LoginRoot.js +2 -1
- package/dist/LoginRoot.js.map +1 -1
- package/dist/__tests__/components/theme-provider-accent.test.d.ts +2 -0
- package/dist/__tests__/components/theme-provider-accent.test.d.ts.map +1 -0
- package/dist/__tests__/components/theme-provider-accent.test.js +64 -0
- package/dist/__tests__/components/theme-provider-accent.test.js.map +1 -0
- package/dist/__tests__/views/portal-container.test.js +13 -0
- package/dist/__tests__/views/portal-container.test.js.map +1 -1
- package/dist/__tests__/views/post-fields-modal.render.test.js +3 -6
- package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
- package/dist/__tests__/views/redirects-pane.render.test.js +6 -0
- package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-content-pane.render.test.js +15 -4
- package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/CoauthorResultPopover.js +1 -1
- package/dist/components/EditorSeoAside.d.ts +23 -0
- package/dist/components/EditorSeoAside.d.ts.map +1 -0
- package/dist/components/EditorSeoAside.js +21 -0
- package/dist/components/EditorSeoAside.js.map +1 -0
- package/dist/components/MediaPickerModal.js +1 -1
- package/dist/components/SEOConfigPanel.js +1 -1
- package/dist/components/SchedulePublishDialog.js +1 -1
- package/dist/components/SharePreviewLinkDialog.js +1 -1
- package/dist/components/ThemeProvider.d.ts.map +1 -1
- package/dist/components/ThemeProvider.js +2 -1
- package/dist/components/ThemeProvider.js.map +1 -1
- package/dist/components/VersionHistory.js +1 -1
- package/dist/components/seo/EditorSeoScoreBadge.d.ts +7 -0
- package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -0
- package/dist/components/seo/EditorSeoScoreBadge.js +9 -0
- package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -0
- package/dist/components/seo/SeoAccordion.d.ts +16 -0
- package/dist/components/seo/SeoAccordion.d.ts.map +1 -0
- package/dist/components/seo/SeoAccordion.js +21 -0
- package/dist/components/seo/SeoAccordion.js.map +1 -0
- package/dist/components/seo/SeoAnalysisList.d.ts +7 -0
- package/dist/components/seo/SeoAnalysisList.d.ts.map +1 -0
- package/dist/components/seo/SeoAnalysisList.js +32 -0
- package/dist/components/seo/SeoAnalysisList.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.d.ts +15 -4
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorPane.js +155 -35
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/seo/SeoInsightsMetrics.d.ts +6 -0
- package/dist/components/seo/SeoInsightsMetrics.d.ts.map +1 -0
- package/dist/components/seo/SeoInsightsMetrics.js +16 -0
- package/dist/components/seo/SeoInsightsMetrics.js.map +1 -0
- package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js +13 -3
- package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
- package/dist/components/seo/SeoLinkingSection.d.ts +9 -0
- package/dist/components/seo/SeoLinkingSection.d.ts.map +1 -0
- package/dist/components/seo/SeoLinkingSection.js +44 -0
- package/dist/components/seo/SeoLinkingSection.js.map +1 -0
- package/dist/components/seo/primitives.d.ts.map +1 -1
- package/dist/components/seo/primitives.js +3 -3
- package/dist/components/seo/primitives.js.map +1 -1
- package/dist/components/ui/Card.js +5 -5
- package/dist/components/ui/Card.js.map +1 -1
- package/dist/components/ui/InspectorPane.js +3 -3
- package/dist/components/ui/InspectorPane.js.map +1 -1
- package/dist/components/ui/PanelSection.d.ts +16 -0
- package/dist/components/ui/PanelSection.d.ts.map +1 -0
- package/dist/components/ui/PanelSection.js +15 -0
- package/dist/components/ui/PanelSection.js.map +1 -0
- package/dist/lib/admin-root-element.d.ts +9 -0
- package/dist/lib/admin-root-element.d.ts.map +1 -0
- package/dist/lib/admin-root-element.js +14 -0
- package/dist/lib/admin-root-element.js.map +1 -0
- package/dist/lib/editor-seo-content.d.ts +16 -0
- package/dist/lib/editor-seo-content.d.ts.map +1 -0
- package/dist/lib/editor-seo-content.js +139 -0
- package/dist/lib/editor-seo-content.js.map +1 -0
- package/dist/lib/portal-container.d.ts.map +1 -1
- package/dist/lib/portal-container.js +2 -3
- package/dist/lib/portal-container.js.map +1 -1
- package/dist/lib/seo-service.d.ts +91 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +55 -2
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/ApiKeys.js +2 -2
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +12 -14
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +35 -6
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/PageEditor.js +1 -1
- package/dist/views/Posts/PostTypeEditor.js +3 -3
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +4 -2
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/ScriptTags.js +1 -1
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/Users.js +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +6 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +5 -2
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +36 -1
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.js +2 -13
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +2 -2
- package/dist/views/page-editor/SectionsPanel.js +1 -1
- package/dist/views/post-editor/PostFieldsModal.d.ts +1 -1
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
- package/dist/views/post-editor/PostFieldsModal.js +17 -79
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
- package/dist/views/post-editor/PostHeaderPanel.js +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +37 -1
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/seo/ContentTab.d.ts.map +1 -1
- package/dist/views/seo/ContentTab.js +7 -1
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/seo/LinksTab.d.ts +4 -0
- package/dist/views/seo/LinksTab.d.ts.map +1 -0
- package/dist/views/seo/LinksTab.js +111 -0
- package/dist/views/seo/LinksTab.js.map +1 -0
- package/dist/views/seo/OverviewTab.js +1 -1
- package/dist/views/seo/OverviewTab.js.map +1 -1
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
- package/dist/views/seo/RedirectsTab.js +39 -6
- package/dist/views/seo/RedirectsTab.js.map +1 -1
- package/dist/views/seo/TechnicalTab.js +2 -2
- package/dist/views/settings/AISettingsTab.js +1 -1
- package/dist/views/settings/AiFeaturesCard.js +1 -1
- package/dist/views/settings/AiFeaturesCard.js.map +1 -1
- package/dist/views/settings/AppearanceTab.js +1 -1
- package/dist/views/settings/BrandVoiceCard.js +1 -1
- package/dist/views/settings/EmailSettingsTab.js +2 -2
- package/dist/views/settings/GeneralSettingsTab.js +1 -1
- package/dist/views/settings/SecurityStatusCard.js +1 -1
- package/dist/views/settings/SeoSettingsTab.js +4 -4
- package/dist/views/settings/UpdatesTab.js +2 -2
- package/dist/views/settings/components.js +3 -3
- package/dist/views/settings/components.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +3 -2
- package/src/LoginRoot.tsx +2 -1
- package/src/__tests__/components/theme-provider-accent.test.tsx +84 -0
- package/src/__tests__/views/portal-container.test.tsx +14 -0
- package/src/__tests__/views/post-fields-modal.render.test.tsx +3 -9
- package/src/__tests__/views/redirects-pane.render.test.tsx +6 -0
- package/src/__tests__/views/seo-content-pane.render.test.tsx +16 -4
- package/src/components/CoauthorResultPopover.tsx +1 -1
- package/src/components/EditorSeoAside.tsx +68 -0
- package/src/components/MediaPickerModal.tsx +1 -1
- package/src/components/SEOConfigPanel.tsx +2 -2
- package/src/components/SchedulePublishDialog.tsx +1 -1
- package/src/components/SharePreviewLinkDialog.tsx +1 -1
- package/src/components/ThemeProvider.tsx +3 -1
- package/src/components/VersionHistory.tsx +1 -1
- package/src/components/seo/EditorSeoScoreBadge.tsx +26 -0
- package/src/components/seo/SeoAccordion.tsx +76 -0
- package/src/components/seo/SeoAnalysisList.tsx +113 -0
- package/src/components/seo/SeoEditorPane.tsx +532 -250
- package/src/components/seo/SeoInsightsMetrics.tsx +109 -0
- package/src/components/seo/SeoIssueFixPanel.tsx +16 -4
- package/src/components/seo/SeoLinkingSection.tsx +151 -0
- package/src/components/seo/primitives.tsx +7 -3
- package/src/components/ui/Card.tsx +5 -5
- package/src/components/ui/InspectorPane.tsx +3 -3
- package/src/components/ui/PanelSection.tsx +53 -0
- package/src/lib/admin-root-element.ts +15 -0
- package/src/lib/editor-seo-content.ts +144 -0
- package/src/lib/portal-container.ts +3 -2
- package/src/lib/seo-service.ts +150 -2
- package/src/views/ApiKeys.tsx +2 -2
- package/src/views/Dashboard.tsx +40 -53
- package/src/views/MediaBrowser.tsx +62 -12
- package/src/views/PageEditor.tsx +1 -1
- package/src/views/Posts/PostTypeEditor.tsx +3 -3
- package/src/views/SEO.tsx +6 -0
- package/src/views/ScriptTags.tsx +2 -2
- package/src/views/Users.tsx +1 -1
- package/src/views/page-editor/EditorTopBar.tsx +27 -0
- package/src/views/page-editor/PageSectionEditor.tsx +58 -3
- package/src/views/page-editor/PageSettingsModal.tsx +1 -45
- package/src/views/page-editor/SectionInspector.tsx +2 -2
- package/src/views/page-editor/SectionsPanel.tsx +1 -1
- package/src/views/post-editor/PostFieldsModal.tsx +19 -138
- package/src/views/post-editor/PostHeaderPanel.tsx +1 -1
- package/src/views/post-editor/PostSectionEditor.tsx +60 -3
- package/src/views/seo/ContentTab.tsx +11 -5
- package/src/views/seo/LinksTab.tsx +462 -0
- package/src/views/seo/OverviewTab.tsx +1 -1
- package/src/views/seo/RedirectsTab.tsx +97 -4
- package/src/views/seo/TechnicalTab.tsx +2 -2
- package/src/views/settings/AISettingsTab.tsx +3 -3
- package/src/views/settings/AiFeaturesCard.tsx +1 -1
- package/src/views/settings/AppearanceTab.tsx +1 -1
- package/src/views/settings/BrandVoiceCard.tsx +2 -2
- package/src/views/settings/EmailSettingsTab.tsx +2 -2
- package/src/views/settings/GeneralSettingsTab.tsx +3 -3
- package/src/views/settings/SecurityStatusCard.tsx +1 -1
- package/src/views/settings/SeoSettingsTab.tsx +6 -6
- package/src/views/settings/UpdatesTab.tsx +3 -3
- package/src/views/settings/components.tsx +3 -3
package/src/lib/seo-service.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* `packages/cms-core/src/api/handlers.ts` across phases 2-3 and 6.
|
|
10
10
|
*/
|
|
11
11
|
import type { BrandAlignmentScoreResult } from '@actuate-media/cms-core/brand-voice'
|
|
12
|
+
import type { SEOAnalysisResult } from '@actuate-media/cms-core/seo/analysis'
|
|
12
13
|
import { cmsApi } from './api.js'
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -163,7 +164,12 @@ export interface SeoContentRecord {
|
|
|
163
164
|
title: string
|
|
164
165
|
url: string
|
|
165
166
|
canonicalUrl: string | null
|
|
167
|
+
/** Rendered public title (after site title template). */
|
|
166
168
|
metaTitle: string | null
|
|
169
|
+
/** Stored page-specific portion for the `%page%` token. */
|
|
170
|
+
metaTitlePage?: string | null
|
|
171
|
+
siteName?: string | null
|
|
172
|
+
titleTemplate?: string | null
|
|
167
173
|
metaDescription: string | null
|
|
168
174
|
focusKeyword: string | null
|
|
169
175
|
keyTakeaway: string | null
|
|
@@ -234,6 +240,12 @@ export interface SeoFieldPayload {
|
|
|
234
240
|
llmsOptional?: boolean
|
|
235
241
|
}
|
|
236
242
|
|
|
243
|
+
export async function fetchDocumentSeoAnalysis(documentId: string): Promise<SEOAnalysisResult> {
|
|
244
|
+
const res = await cmsApi<SEOAnalysisResult>(`/seo/analysis/${encodeURIComponent(documentId)}`)
|
|
245
|
+
throwIfError(res)
|
|
246
|
+
return res.data ?? { score: 0, checks: [] }
|
|
247
|
+
}
|
|
248
|
+
|
|
237
249
|
export async function updateSeoFields(
|
|
238
250
|
entityType: string,
|
|
239
251
|
entityId: string,
|
|
@@ -368,6 +380,7 @@ export interface SeoFixFieldChange {
|
|
|
368
380
|
export interface SeoIssueFixSuggestion {
|
|
369
381
|
autoFixable: boolean
|
|
370
382
|
source?: 'deterministic' | 'ai'
|
|
383
|
+
fixStrategy?: 'redirect' | 'fix-link'
|
|
371
384
|
aiUnavailable?: boolean
|
|
372
385
|
fingerprint: string
|
|
373
386
|
patch: Record<string, unknown>
|
|
@@ -447,6 +460,7 @@ export const SEO_INLINE_FIX_ISSUE_TYPES = new Set([
|
|
|
447
460
|
'duplicate-meta-title',
|
|
448
461
|
'duplicate-meta-description',
|
|
449
462
|
'missing-structured-data',
|
|
463
|
+
'broken-internal-link',
|
|
450
464
|
])
|
|
451
465
|
|
|
452
466
|
export function isInlineSeoFixIssue(type: string): boolean {
|
|
@@ -890,11 +904,39 @@ export async function fetchRedirectSuggestions(): Promise<RedirectSuggestion[]>
|
|
|
890
904
|
return res.data?.suggestions ?? []
|
|
891
905
|
}
|
|
892
906
|
|
|
893
|
-
export
|
|
894
|
-
|
|
907
|
+
export interface RedirectRecoverySettings {
|
|
908
|
+
aiRerank: boolean
|
|
909
|
+
autoApply: boolean
|
|
910
|
+
autoApplyMinConfidence: number
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
export async function fetchRedirectRecoverySettings(): Promise<RedirectRecoverySettings | null> {
|
|
914
|
+
const res = await cmsApi<RedirectRecoverySettings>('/seo/redirects/recovery-settings')
|
|
915
|
+
throwIfError(res)
|
|
916
|
+
return res.data ?? null
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export async function saveRedirectRecoverySettings(
|
|
920
|
+
settings: Partial<RedirectRecoverySettings>,
|
|
921
|
+
): Promise<{ error?: string }> {
|
|
922
|
+
const res = await cmsApi<RedirectRecoverySettings>('/seo/redirects/recovery-settings', {
|
|
923
|
+
method: 'PUT',
|
|
924
|
+
body: JSON.stringify(settings),
|
|
925
|
+
})
|
|
895
926
|
return res.error ? { error: res.error } : {}
|
|
896
927
|
}
|
|
897
928
|
|
|
929
|
+
export async function generate404RecoverySuggestions(): Promise<{
|
|
930
|
+
error?: string
|
|
931
|
+
autoApplied?: number
|
|
932
|
+
}> {
|
|
933
|
+
const res = await cmsApi<{ created?: number; autoApplied?: number }>('/seo/redirects/suggest', {
|
|
934
|
+
method: 'POST',
|
|
935
|
+
})
|
|
936
|
+
if (res.error) return { error: res.error }
|
|
937
|
+
return { autoApplied: res.data?.autoApplied ?? 0 }
|
|
938
|
+
}
|
|
939
|
+
|
|
898
940
|
export async function acceptRedirectSuggestion(suggestionId: string): Promise<{ error?: string }> {
|
|
899
941
|
const res = await cmsApi(
|
|
900
942
|
`/seo/redirects/suggestions/${encodeURIComponent(suggestionId)}/accept`,
|
|
@@ -995,3 +1037,109 @@ export async function summarizeSeoAudit(): Promise<SeoAiResult> {
|
|
|
995
1037
|
if (isAiUnavailable(res.status)) return { unavailable: true }
|
|
996
1038
|
return res.data ?? {}
|
|
997
1039
|
}
|
|
1040
|
+
|
|
1041
|
+
// ─── Link graph & AI suggestions ───────────────────────────────────────────
|
|
1042
|
+
|
|
1043
|
+
export interface LinkGraphEntityRef {
|
|
1044
|
+
id: string
|
|
1045
|
+
title: string
|
|
1046
|
+
url: string
|
|
1047
|
+
path: string
|
|
1048
|
+
collection: string
|
|
1049
|
+
entityType: 'page' | 'post'
|
|
1050
|
+
isHome: boolean
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export interface LinkGraphSnapshot {
|
|
1054
|
+
scannedAt: string
|
|
1055
|
+
postsCrawled: number
|
|
1056
|
+
internalLinks: number
|
|
1057
|
+
externalLinks: number
|
|
1058
|
+
internalRatio: number
|
|
1059
|
+
orphanCount: number
|
|
1060
|
+
orphaned: LinkGraphEntityRef[]
|
|
1061
|
+
linkCoveragePercent: number
|
|
1062
|
+
underLinked: Array<LinkGraphEntityRef & { outboundInternal: number }>
|
|
1063
|
+
brokenLinkCount: number
|
|
1064
|
+
aiIndexLastRunAt: string | null
|
|
1065
|
+
aiIndexReady: boolean
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
export interface LinkHealthIssue {
|
|
1069
|
+
id: string
|
|
1070
|
+
page: string
|
|
1071
|
+
url: string
|
|
1072
|
+
status: number
|
|
1073
|
+
type: string
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
export interface LinkSuggestion {
|
|
1077
|
+
sourceEntityId: string
|
|
1078
|
+
sourceTitle: string
|
|
1079
|
+
sourceUrl: string
|
|
1080
|
+
anchorText: string
|
|
1081
|
+
confidence: number
|
|
1082
|
+
excerpt: string
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
export async function fetchLinkGraphSnapshot(): Promise<LinkGraphSnapshot> {
|
|
1086
|
+
const res = await cmsApi<LinkGraphSnapshot>('/seo/links/snapshot')
|
|
1087
|
+
throwIfError(res)
|
|
1088
|
+
if (!res.data) throw new Error('No link snapshot data')
|
|
1089
|
+
return res.data
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
export async function runLinkAiIndex(): Promise<{
|
|
1093
|
+
data?: {
|
|
1094
|
+
indexedAt: string
|
|
1095
|
+
entitiesIndexed: number
|
|
1096
|
+
aiAvailable: boolean
|
|
1097
|
+
message: string
|
|
1098
|
+
}
|
|
1099
|
+
error?: string
|
|
1100
|
+
}> {
|
|
1101
|
+
const res = await cmsApi<{
|
|
1102
|
+
indexedAt: string
|
|
1103
|
+
entitiesIndexed: number
|
|
1104
|
+
aiAvailable: boolean
|
|
1105
|
+
message: string
|
|
1106
|
+
}>('/seo/links/ai-index', { method: 'POST' })
|
|
1107
|
+
if (res.error) return { error: res.error }
|
|
1108
|
+
return { data: res.data }
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export async function fetchLinkSuggestions(input: {
|
|
1112
|
+
targetEntityId: string
|
|
1113
|
+
targetEntityType: 'page' | 'post'
|
|
1114
|
+
limit?: number
|
|
1115
|
+
}): Promise<{
|
|
1116
|
+
data?: {
|
|
1117
|
+
target: { id: string; title: string; url: string }
|
|
1118
|
+
suggestions: LinkSuggestion[]
|
|
1119
|
+
}
|
|
1120
|
+
error?: string
|
|
1121
|
+
}> {
|
|
1122
|
+
const res = await cmsApi<{
|
|
1123
|
+
target: { id: string; title: string; url: string }
|
|
1124
|
+
suggestions: LinkSuggestion[]
|
|
1125
|
+
}>('/seo/links/suggest', { method: 'POST', body: JSON.stringify(input) })
|
|
1126
|
+
if (res.error) return { error: res.error }
|
|
1127
|
+
return { data: res.data }
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
export async function fetchLinkHealthIssues(): Promise<{
|
|
1131
|
+
data?: { issues: LinkHealthIssue[]; checked: number; truncated: boolean }
|
|
1132
|
+
error?: string
|
|
1133
|
+
}> {
|
|
1134
|
+
const res = await cmsApi<{ truncated: boolean; checked: number; issues: LinkHealthIssue[] }>(
|
|
1135
|
+
'/seo/link-health',
|
|
1136
|
+
)
|
|
1137
|
+
if (res.error) return { error: res.error }
|
|
1138
|
+
return {
|
|
1139
|
+
data: {
|
|
1140
|
+
issues: res.data?.issues ?? [],
|
|
1141
|
+
checked: res.data?.checked ?? 0,
|
|
1142
|
+
truncated: res.data?.truncated ?? false,
|
|
1143
|
+
},
|
|
1144
|
+
}
|
|
1145
|
+
}
|
package/src/views/ApiKeys.tsx
CHANGED
|
@@ -134,7 +134,7 @@ export function ApiKeys({ embedded = false }: ApiKeysProps = {}) {
|
|
|
134
134
|
{error && (
|
|
135
135
|
<div
|
|
136
136
|
role="alert"
|
|
137
|
-
className="border-destructive/30 bg-destructive/10 text-destructive mb-4 flex items-center gap-3 rounded-lg border p-
|
|
137
|
+
className="border-destructive/30 bg-destructive/10 text-destructive mb-4 flex items-center gap-3 rounded-lg border p-4 text-sm"
|
|
138
138
|
>
|
|
139
139
|
<AlertTriangle size={20} className="shrink-0" aria-hidden="true" />
|
|
140
140
|
<span className="flex-1">{error}</span>
|
|
@@ -506,7 +506,7 @@ function RevealKeyDialog({
|
|
|
506
506
|
Copy this key now — it will not be shown again. Treat it like a password.
|
|
507
507
|
</Dialog.Description>
|
|
508
508
|
|
|
509
|
-
<div className="border-warning/30 bg-warning/10 mb-4 flex items-start gap-2 rounded-lg border p-
|
|
509
|
+
<div className="border-warning/30 bg-warning/10 mb-4 flex items-start gap-2 rounded-lg border p-4">
|
|
510
510
|
<AlertTriangle className="text-warning mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
|
511
511
|
<p className="text-foreground text-xs">
|
|
512
512
|
We only store a hash of this key. If you lose it, you'll need to revoke and
|
package/src/views/Dashboard.tsx
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
import { useApiData } from '../lib/useApiData.js'
|
|
43
43
|
import { editPathForDoc } from '../lib/collection-routes.js'
|
|
44
44
|
import { authorAvatar } from '../components/ui/AuthorAvatar.js'
|
|
45
|
+
import { PanelSection, PANEL_LIST_ROW_CLASS } from '../components/ui/PanelSection.js'
|
|
45
46
|
import { OnboardingBlock, shouldShowOnboarding } from './dashboard/OnboardingBlock.js'
|
|
46
47
|
import type { OnboardingSignals } from './dashboard/OnboardingBlock.js'
|
|
47
48
|
|
|
@@ -542,7 +543,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
542
543
|
<div className="w-full space-y-5 p-4 sm:p-6 lg:px-8">
|
|
543
544
|
{/* Health banners ────────────────────────────────────────────── */}
|
|
544
545
|
{health && health.status === 'degraded' && (
|
|
545
|
-
<div className="border-info/30 bg-info/10 flex items-start gap-3 rounded-lg border p-
|
|
546
|
+
<div className="border-info/30 bg-info/10 flex items-start gap-3 rounded-lg border p-4">
|
|
546
547
|
<Database className="text-info mt-0.5 h-5 w-5 shrink-0" />
|
|
547
548
|
<div className="min-w-0 flex-1">
|
|
548
549
|
<span className="text-info text-sm font-medium">Database setup required</span>
|
|
@@ -561,7 +562,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
561
562
|
)}
|
|
562
563
|
|
|
563
564
|
{error && exhausted && (
|
|
564
|
-
<div className="border-warning/30 bg-warning/10 flex items-center gap-3 rounded-lg border p-
|
|
565
|
+
<div className="border-warning/30 bg-warning/10 flex items-center gap-3 rounded-lg border p-4">
|
|
565
566
|
<AlertTriangle className="text-warning h-5 w-5 shrink-0" />
|
|
566
567
|
<span className="text-warning flex-1 text-sm">
|
|
567
568
|
Some dashboard data may be unavailable.
|
|
@@ -685,23 +686,20 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
685
686
|
) : (
|
|
686
687
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_320px]">
|
|
687
688
|
{/* Recent Activity ───────────────────────────────────────── */}
|
|
688
|
-
<
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
Last 7 days across all content
|
|
694
|
-
</p>
|
|
695
|
-
</div>
|
|
696
|
-
{(stats?.recentDocuments?.length ?? 0) > 8 && (
|
|
689
|
+
<PanelSection
|
|
690
|
+
title="Recent Activity"
|
|
691
|
+
description="Last 7 days across all content"
|
|
692
|
+
action={
|
|
693
|
+
(stats?.recentDocuments?.length ?? 0) > 8 ? (
|
|
697
694
|
<button
|
|
698
695
|
className="text-brand text-xs font-medium hover:underline"
|
|
699
696
|
onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
|
|
700
697
|
>
|
|
701
698
|
{activityLimit >= 20 ? 'Show less' : 'Show more'}
|
|
702
699
|
</button>
|
|
703
|
-
)
|
|
704
|
-
|
|
700
|
+
) : undefined
|
|
701
|
+
}
|
|
702
|
+
>
|
|
705
703
|
{activity.length === 0 ? (
|
|
706
704
|
<EmptyState
|
|
707
705
|
icon={Activity}
|
|
@@ -715,7 +713,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
715
713
|
<button
|
|
716
714
|
type="button"
|
|
717
715
|
onClick={() => nav(it.editPath)}
|
|
718
|
-
className=
|
|
716
|
+
className={PANEL_LIST_ROW_CLASS}
|
|
719
717
|
>
|
|
720
718
|
<div className="flex min-w-0 items-start gap-3">
|
|
721
719
|
<div
|
|
@@ -749,33 +747,27 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
749
747
|
))}
|
|
750
748
|
</ul>
|
|
751
749
|
)}
|
|
752
|
-
</
|
|
750
|
+
</PanelSection>
|
|
753
751
|
|
|
754
752
|
{/* Right column ─────────────────────────────────────────── */}
|
|
755
753
|
<aside className="flex min-w-0 flex-col gap-4">
|
|
756
754
|
{/* Publishing Queue */}
|
|
757
|
-
<
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
762
|
-
{publishQueue.length} item{publishQueue.length === 1 ? '' : 's'} scheduled
|
|
763
|
-
</p>
|
|
764
|
-
</div>
|
|
755
|
+
<PanelSection
|
|
756
|
+
title="Publishing Queue"
|
|
757
|
+
description={`${publishQueue.length} item${publishQueue.length === 1 ? '' : 's'} scheduled`}
|
|
758
|
+
action={
|
|
765
759
|
<button
|
|
766
760
|
className="text-brand text-xs font-medium hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50"
|
|
767
761
|
disabled={publishQueue.length === 0}
|
|
768
762
|
onClick={() => {
|
|
769
|
-
// We don't have a dedicated "scheduled" admin page, so deep-
|
|
770
|
-
// link to the first scheduled item — that's where authors
|
|
771
|
-
// typically need to land to reschedule or cancel.
|
|
772
763
|
const first = publishQueue[0]
|
|
773
764
|
if (first) nav(first.editPath)
|
|
774
765
|
}}
|
|
775
766
|
>
|
|
776
767
|
Manage
|
|
777
768
|
</button>
|
|
778
|
-
|
|
769
|
+
}
|
|
770
|
+
>
|
|
779
771
|
{publishQueue.length === 0 ? (
|
|
780
772
|
<EmptyState
|
|
781
773
|
icon={Clock}
|
|
@@ -790,7 +782,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
790
782
|
<button
|
|
791
783
|
type="button"
|
|
792
784
|
onClick={() => nav(q.editPath)}
|
|
793
|
-
className=
|
|
785
|
+
className={`${PANEL_LIST_ROW_CLASS} flex min-w-0 cursor-pointer items-center gap-2.5`}
|
|
794
786
|
>
|
|
795
787
|
<span className="bg-warning h-2 w-2 shrink-0 rounded-full" aria-hidden />
|
|
796
788
|
<div className="min-w-0 flex-1">
|
|
@@ -807,23 +799,21 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
807
799
|
))}
|
|
808
800
|
</ul>
|
|
809
801
|
)}
|
|
810
|
-
</
|
|
802
|
+
</PanelSection>
|
|
811
803
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
<h2 className="text-foreground text-sm font-medium">Content Health</h2>
|
|
817
|
-
<p className="text-muted-foreground mt-0.5 text-xs">SEO & quality issues</p>
|
|
818
|
-
</div>
|
|
804
|
+
<PanelSection
|
|
805
|
+
title="Content Health"
|
|
806
|
+
description="SEO & quality issues"
|
|
807
|
+
action={
|
|
819
808
|
<button
|
|
820
809
|
className="text-brand text-xs font-medium hover:underline"
|
|
821
810
|
onClick={() => nav('/seo')}
|
|
822
811
|
>
|
|
823
812
|
Fix issues
|
|
824
813
|
</button>
|
|
825
|
-
|
|
826
|
-
|
|
814
|
+
}
|
|
815
|
+
>
|
|
816
|
+
<div className="px-6 pt-4 pb-2.5">
|
|
827
817
|
<div className="mb-1.5 flex items-baseline justify-between">
|
|
828
818
|
<p className="text-foreground text-2xl leading-none font-medium tracking-tight">
|
|
829
819
|
{contentHealth.score > 0 ? contentHealth.score : '—'}
|
|
@@ -861,7 +851,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
861
851
|
</div>
|
|
862
852
|
</div>
|
|
863
853
|
{contentHealth.issues.length === 0 ? (
|
|
864
|
-
<div className="text-muted-foreground px-
|
|
854
|
+
<div className="text-muted-foreground px-6 pt-1 pb-4 text-xs">
|
|
865
855
|
No outstanding issues. Nice work.
|
|
866
856
|
</div>
|
|
867
857
|
) : (
|
|
@@ -871,7 +861,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
871
861
|
<button
|
|
872
862
|
type="button"
|
|
873
863
|
onClick={() => nav('/seo')}
|
|
874
|
-
className=
|
|
864
|
+
className={`${PANEL_LIST_ROW_CLASS} flex items-center gap-2.5 py-3.5`}
|
|
875
865
|
>
|
|
876
866
|
<span
|
|
877
867
|
className={`h-1.5 w-1.5 shrink-0 rounded-full ${
|
|
@@ -903,7 +893,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
903
893
|
))}
|
|
904
894
|
</ul>
|
|
905
895
|
)}
|
|
906
|
-
</
|
|
896
|
+
</PanelSection>
|
|
907
897
|
</aside>
|
|
908
898
|
</div>
|
|
909
899
|
)}
|
|
@@ -916,23 +906,20 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
916
906
|
collected enough data yet" from "we have data and it shows
|
|
917
907
|
0" so editors don't think the dashboard is broken on a
|
|
918
908
|
quiet weekend. */}
|
|
919
|
-
<
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
924
|
-
Headless content delivery · last 24 hours
|
|
925
|
-
</p>
|
|
926
|
-
</div>
|
|
909
|
+
<PanelSection
|
|
910
|
+
title="Delivery API"
|
|
911
|
+
description="Headless content delivery · last 24 hours"
|
|
912
|
+
action={
|
|
927
913
|
<button
|
|
928
914
|
className="text-brand inline-flex items-center gap-1 text-xs font-medium hover:underline"
|
|
929
915
|
onClick={() => nav('/settings?tab=api-keys')}
|
|
930
916
|
>
|
|
931
917
|
API docs <ExternalLink className="h-3 w-3" />
|
|
932
918
|
</button>
|
|
933
|
-
|
|
919
|
+
}
|
|
920
|
+
>
|
|
934
921
|
<DeliveryGrid delivery={delivery} loading={deliveryLoading} />
|
|
935
|
-
</
|
|
922
|
+
</PanelSection>
|
|
936
923
|
</div>
|
|
937
924
|
)
|
|
938
925
|
}
|
|
@@ -947,7 +934,7 @@ function DeliveryGrid({ delivery, loading }: { delivery: DeliveryStats | null; l
|
|
|
947
934
|
aria-busy
|
|
948
935
|
>
|
|
949
936
|
{[0, 1, 2, 3].map((i) => (
|
|
950
|
-
<div key={i} className="px-
|
|
937
|
+
<div key={i} className="px-6 py-4">
|
|
951
938
|
<div className="bg-muted mb-2 h-3 w-24 rounded" />
|
|
952
939
|
<div className="bg-muted mb-1.5 h-7 w-20 rounded" />
|
|
953
940
|
<div className="bg-muted h-3 w-28 rounded" />
|
|
@@ -1087,7 +1074,7 @@ function DeliveryTile({
|
|
|
1087
1074
|
? 'text-destructive dark:text-red-400'
|
|
1088
1075
|
: 'text-muted-foreground'
|
|
1089
1076
|
return (
|
|
1090
|
-
<div className="px-
|
|
1077
|
+
<div className="px-6 py-4">
|
|
1091
1078
|
<div className="text-muted-foreground mb-1 flex items-center gap-1.5">
|
|
1092
1079
|
<Icon className="h-3.5 w-3.5" aria-hidden />
|
|
1093
1080
|
<span className="text-[11px] font-medium">{label}</span>
|
|
@@ -47,6 +47,8 @@ interface MediaItem {
|
|
|
47
47
|
dimensions?: string
|
|
48
48
|
format?: string
|
|
49
49
|
altTag?: string
|
|
50
|
+
/** Pending AI alt suggestion (`aiAltText` column) awaiting editor approval. */
|
|
51
|
+
aiAltSuggestion?: string
|
|
50
52
|
title?: string
|
|
51
53
|
caption?: string
|
|
52
54
|
description?: string
|
|
@@ -321,10 +323,13 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
321
323
|
return
|
|
322
324
|
}
|
|
323
325
|
|
|
324
|
-
const res = await cmsApi<{
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
const res = await cmsApi<{
|
|
327
|
+
alt?: string
|
|
328
|
+
title?: string
|
|
329
|
+
alternatives?: string[]
|
|
330
|
+
isDecorative?: boolean
|
|
331
|
+
usedVision?: boolean
|
|
332
|
+
}>('/ai/media-metadata', { method: 'POST', body: JSON.stringify({ mediaId: activeItem.id }) })
|
|
328
333
|
|
|
329
334
|
if (res.error) {
|
|
330
335
|
// AI plugin/provider not configured (e.g. local dev): fall back to a
|
|
@@ -344,8 +349,17 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
344
349
|
}
|
|
345
350
|
|
|
346
351
|
if (field === 'alt') {
|
|
347
|
-
|
|
348
|
-
|
|
352
|
+
if (res.data?.isDecorative) {
|
|
353
|
+
setEditAlt('')
|
|
354
|
+
toast.success('AI marked this image as decorative (empty alt)')
|
|
355
|
+
} else {
|
|
356
|
+
setEditAlt(res.data?.alt ?? '')
|
|
357
|
+
toast.success(
|
|
358
|
+
res.data?.usedVision
|
|
359
|
+
? 'Alt tag generated from image analysis'
|
|
360
|
+
: 'Alt tag suggested — review before saving',
|
|
361
|
+
)
|
|
362
|
+
}
|
|
349
363
|
} else {
|
|
350
364
|
setEditTitle(res.data?.title ?? '')
|
|
351
365
|
toast.success('Title generated by AI')
|
|
@@ -395,6 +409,24 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
395
409
|
toast.success(`Uploaded ${file.name}`)
|
|
396
410
|
}
|
|
397
411
|
successCount++
|
|
412
|
+
|
|
413
|
+
const uploaded = res.data
|
|
414
|
+
const isImage =
|
|
415
|
+
file.type.startsWith('image/') || (uploaded?.type ?? '').startsWith('image/')
|
|
416
|
+
if (isImage && uploaded?.id) {
|
|
417
|
+
void cmsApi<{ alt?: string; isDecorative?: boolean }>('/ai/media-metadata', {
|
|
418
|
+
method: 'POST',
|
|
419
|
+
body: JSON.stringify({ mediaId: String(uploaded.id) }),
|
|
420
|
+
}).then((altRes) => {
|
|
421
|
+
if (altRes.error) return
|
|
422
|
+
if (altRes.data?.isDecorative) {
|
|
423
|
+
toast.info(`${file.name}: AI suggests decorative (empty alt). Open to review.`)
|
|
424
|
+
} else if (altRes.data?.alt) {
|
|
425
|
+
toast.info(`${file.name}: AI alt text ready. Open the item to review and save.`)
|
|
426
|
+
}
|
|
427
|
+
refetch()
|
|
428
|
+
})
|
|
429
|
+
}
|
|
398
430
|
}
|
|
399
431
|
}
|
|
400
432
|
|
|
@@ -467,7 +499,7 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
467
499
|
return (
|
|
468
500
|
<div className="flex h-full flex-col p-3 pr-6 sm:p-4 sm:pr-8">
|
|
469
501
|
{error && (
|
|
470
|
-
<div className="border-destructive/30 bg-destructive/10 mb-4 flex items-center gap-3 rounded-lg border p-
|
|
502
|
+
<div className="border-destructive/30 bg-destructive/10 mb-4 flex items-center gap-3 rounded-lg border p-4">
|
|
471
503
|
<AlertTriangle className="text-destructive h-5 w-5 shrink-0" />
|
|
472
504
|
<span className="text-destructive flex-1 text-sm">{error}</span>
|
|
473
505
|
<button
|
|
@@ -635,7 +667,7 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
635
667
|
</div>
|
|
636
668
|
|
|
637
669
|
{selectedMedia.length > 0 && (
|
|
638
|
-
<div className="border-primary/30 bg-primary/10 mb-4 rounded-lg border p-
|
|
670
|
+
<div className="border-primary/30 bg-primary/10 mb-4 rounded-lg border p-4">
|
|
639
671
|
<div className="flex items-center justify-between">
|
|
640
672
|
<span className="text-primary text-sm">
|
|
641
673
|
{selectedMedia.length} file{selectedMedia.length !== 1 ? 's' : ''} selected
|
|
@@ -901,7 +933,7 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
901
933
|
</div>
|
|
902
934
|
|
|
903
935
|
{issues.length > 0 && (
|
|
904
|
-
<div className="border-warning/30 bg-warning/10 mx-4 mt-4 rounded-lg border p-
|
|
936
|
+
<div className="border-warning/30 bg-warning/10 mx-4 mt-4 rounded-lg border p-4">
|
|
905
937
|
<div className="flex items-start gap-2">
|
|
906
938
|
<AlertTriangle className="text-warning mt-0.5 h-4 w-4 shrink-0" />
|
|
907
939
|
<div>
|
|
@@ -1006,6 +1038,24 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
1006
1038
|
rows={2}
|
|
1007
1039
|
className="border-input focus:ring-ring w-full resize-none rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
1008
1040
|
/>
|
|
1041
|
+
{activeItem.aiAltSuggestion !== undefined &&
|
|
1042
|
+
editAlt !== activeItem.aiAltSuggestion && (
|
|
1043
|
+
<div className="border-warning/40 bg-warning/10 mt-2 flex items-start justify-between gap-2 rounded-lg border p-4 text-xs">
|
|
1044
|
+
<span className="text-foreground">
|
|
1045
|
+
AI suggestion:{' '}
|
|
1046
|
+
{activeItem.aiAltSuggestion.trim()
|
|
1047
|
+
? activeItem.aiAltSuggestion
|
|
1048
|
+
: '(decorative — empty alt)'}
|
|
1049
|
+
</span>
|
|
1050
|
+
<button
|
|
1051
|
+
type="button"
|
|
1052
|
+
onClick={() => setEditAlt(activeItem.aiAltSuggestion ?? '')}
|
|
1053
|
+
className="text-primary shrink-0 font-medium hover:opacity-80"
|
|
1054
|
+
>
|
|
1055
|
+
Apply
|
|
1056
|
+
</button>
|
|
1057
|
+
</div>
|
|
1058
|
+
)}
|
|
1009
1059
|
{!editAlt && (
|
|
1010
1060
|
<p className="text-destructive mt-1 text-xs">
|
|
1011
1061
|
Required for accessibility and SEO
|
|
@@ -1121,7 +1171,7 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
1121
1171
|
))}
|
|
1122
1172
|
</div>
|
|
1123
1173
|
) : (
|
|
1124
|
-
<div className="border-warning/30 bg-warning/10 rounded-lg border p-
|
|
1174
|
+
<div className="border-warning/30 bg-warning/10 rounded-lg border p-4">
|
|
1125
1175
|
<div className="flex items-start gap-2">
|
|
1126
1176
|
<AlertTriangle className="text-warning mt-0.5 h-4 w-4 shrink-0" />
|
|
1127
1177
|
<div>
|
|
@@ -1144,7 +1194,7 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
1144
1194
|
type="button"
|
|
1145
1195
|
onClick={() => handleAiGenerate('optimize')}
|
|
1146
1196
|
disabled={aiGenerating === 'optimize'}
|
|
1147
|
-
className="border-primary/30 bg-primary/10 hover:bg-primary/15 flex w-full items-center gap-2 rounded-lg border p-
|
|
1197
|
+
className="border-primary/30 bg-primary/10 hover:bg-primary/15 flex w-full items-center gap-2 rounded-lg border p-4 text-left transition-colors disabled:opacity-50"
|
|
1148
1198
|
>
|
|
1149
1199
|
<Sparkles
|
|
1150
1200
|
className={`text-primary h-5 w-5 shrink-0 ${aiGenerating === 'optimize' ? 'animate-spin' : ''}`}
|
|
@@ -1160,7 +1210,7 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
1160
1210
|
</button>
|
|
1161
1211
|
<button
|
|
1162
1212
|
type="button"
|
|
1163
|
-
className="border-border hover:bg-muted flex w-full items-center gap-2 rounded-lg border p-
|
|
1213
|
+
className="border-border hover:bg-muted flex w-full items-center gap-2 rounded-lg border p-4 text-left transition-colors"
|
|
1164
1214
|
>
|
|
1165
1215
|
<Bot className="text-muted-foreground h-5 w-5 shrink-0" />
|
|
1166
1216
|
<div className="flex-1">
|
package/src/views/PageEditor.tsx
CHANGED
|
@@ -100,7 +100,7 @@ export function PageEditor({ id, onNavigate }: PageEditorProps) {
|
|
|
100
100
|
</div>
|
|
101
101
|
)}
|
|
102
102
|
|
|
103
|
-
<div className="border-border border-b px-
|
|
103
|
+
<div className="border-border border-b px-6 py-4">
|
|
104
104
|
<div className="flex items-center justify-between">
|
|
105
105
|
<button
|
|
106
106
|
onClick={() => onNavigate?.('/pages')}
|
|
@@ -136,7 +136,7 @@ function EditorToolbar({
|
|
|
136
136
|
}) {
|
|
137
137
|
const palette = paletteFor(detail.color)
|
|
138
138
|
return (
|
|
139
|
-
<div className="border-border bg-card flex flex-wrap items-center gap-3 border-b px-
|
|
139
|
+
<div className="border-border bg-card flex flex-wrap items-center gap-3 border-b px-6 py-4 sm:px-6">
|
|
140
140
|
<nav aria-label="Breadcrumb" className="flex min-w-0 items-center gap-1.5 text-sm">
|
|
141
141
|
<button
|
|
142
142
|
type="button"
|
|
@@ -284,7 +284,7 @@ function FieldBuilder({ detail }: { detail: PostTypeDetail }) {
|
|
|
284
284
|
function FieldList({ detail }: { detail: PostTypeDetail }) {
|
|
285
285
|
return (
|
|
286
286
|
<div className="bg-card border-border rounded-xl border shadow-sm">
|
|
287
|
-
<div className="border-border flex items-center gap-2 border-b px-
|
|
287
|
+
<div className="border-border flex items-center gap-2 border-b px-6 py-4">
|
|
288
288
|
<Lock className="text-muted-foreground h-3.5 w-3.5" aria-hidden />
|
|
289
289
|
<span className="text-foreground text-sm font-medium">Fields</span>
|
|
290
290
|
<span className="bg-muted text-muted-foreground ml-auto rounded-full px-2 py-0.5 text-xs font-medium">
|
|
@@ -525,7 +525,7 @@ function AddFieldCard({ detail }: { detail: PostTypeDetail }) {
|
|
|
525
525
|
function EditorSkeleton() {
|
|
526
526
|
return (
|
|
527
527
|
<div className="flex h-full flex-col">
|
|
528
|
-
<div className="border-border bg-card border-b px-
|
|
528
|
+
<div className="border-border bg-card border-b px-6 py-4 sm:px-6">
|
|
529
529
|
<div className="bg-muted h-5 w-48 animate-pulse rounded" />
|
|
530
530
|
</div>
|
|
531
531
|
<div className="flex flex-col gap-6 p-4 sm:p-6 lg:flex-row">
|
package/src/views/SEO.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
ClipboardCheck,
|
|
16
16
|
RefreshCw,
|
|
17
17
|
Bot,
|
|
18
|
+
Link2,
|
|
18
19
|
} from 'lucide-react'
|
|
19
20
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
20
21
|
import { toast } from 'sonner'
|
|
@@ -30,6 +31,7 @@ import { ContentTab } from './seo/ContentTab.js'
|
|
|
30
31
|
import { TechnicalTab } from './seo/TechnicalTab.js'
|
|
31
32
|
import { RedirectsTab } from './seo/RedirectsTab.js'
|
|
32
33
|
import { AuditTab } from './seo/AuditTab.js'
|
|
34
|
+
import { LinksTab } from './seo/LinksTab.js'
|
|
33
35
|
|
|
34
36
|
export interface SEOProps {
|
|
35
37
|
onNavigate?: (path: string) => void
|
|
@@ -39,6 +41,7 @@ export interface SEOProps {
|
|
|
39
41
|
const TABS = [
|
|
40
42
|
{ id: 'overview', label: 'Overview', icon: LayoutDashboard },
|
|
41
43
|
{ id: 'content', label: 'Content SEO', icon: FileText },
|
|
44
|
+
{ id: 'links', label: 'Links', icon: Link2 },
|
|
42
45
|
{ id: 'technical', label: 'Technical', icon: Settings2 },
|
|
43
46
|
{ id: 'redirects', label: 'Redirects', icon: ArrowRightLeft },
|
|
44
47
|
{ id: 'audit', label: 'Audit', icon: ClipboardCheck },
|
|
@@ -212,6 +215,9 @@ export function SEO({ onNavigate, initialTab = 'overview' }: SEOProps) {
|
|
|
212
215
|
/>
|
|
213
216
|
)}
|
|
214
217
|
</Tabs.Content>
|
|
218
|
+
<Tabs.Content value="links" tabIndex={-1}>
|
|
219
|
+
{activeTab === 'links' && <LinksTab onNavigate={onNavigate} />}
|
|
220
|
+
</Tabs.Content>
|
|
215
221
|
<Tabs.Content value="technical" tabIndex={-1}>
|
|
216
222
|
{activeTab === 'technical' && <TechnicalTab />}
|
|
217
223
|
</Tabs.Content>
|