@actuate-media/cms-admin 0.8.0 → 0.8.2
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/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +44 -42
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/lib/search.test.js +10 -10
- package/dist/__tests__/lib/search.test.js.map +1 -1
- package/dist/__tests__/lib/utils.test.js.map +1 -1
- package/dist/__tests__/router/match-route.test.js.map +1 -1
- package/dist/__tests__/router/strip-base.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.js +2 -4
- package/dist/components/Breadcrumbs.js.map +1 -1
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +7 -3
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/ContentOverviewChart.d.ts.map +1 -1
- package/dist/components/ContentOverviewChart.js.map +1 -1
- package/dist/components/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/ErrorBoundary.js.map +1 -1
- package/dist/components/FocalPointPicker.d.ts.map +1 -1
- package/dist/components/FocalPointPicker.js +4 -2
- package/dist/components/FocalPointPicker.js.map +1 -1
- package/dist/components/FolderTree.d.ts.map +1 -1
- package/dist/components/FolderTree.js +18 -10
- package/dist/components/FolderTree.js.map +1 -1
- package/dist/components/LivePreview.d.ts +1 -1
- package/dist/components/LivePreview.d.ts.map +1 -1
- package/dist/components/LivePreview.js +6 -2
- package/dist/components/LivePreview.js.map +1 -1
- package/dist/components/LocaleProvider.d.ts.map +1 -1
- package/dist/components/LocaleProvider.js.map +1 -1
- package/dist/components/LocaleSwitcher.d.ts.map +1 -1
- package/dist/components/LocaleSwitcher.js +1 -1
- package/dist/components/LocaleSwitcher.js.map +1 -1
- package/dist/components/MediaPickerModal.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.js.map +1 -1
- package/dist/components/PresenceIndicator.d.ts.map +1 -1
- package/dist/components/PresenceIndicator.js +5 -2
- package/dist/components/PresenceIndicator.js.map +1 -1
- package/dist/components/SEOPanel.d.ts +1 -1
- package/dist/components/SEOPanel.d.ts.map +1 -1
- package/dist/components/SEOPanel.js +110 -24
- package/dist/components/SEOPanel.js.map +1 -1
- package/dist/components/SEOPerformance.d.ts.map +1 -1
- package/dist/components/SEOPerformance.js +2 -2
- package/dist/components/SEOPerformance.js.map +1 -1
- package/dist/components/ThemeProvider.d.ts.map +1 -1
- package/dist/components/ThemeProvider.js.map +1 -1
- package/dist/components/TipTapEditor.d.ts.map +1 -1
- package/dist/components/TipTapEditor.js +5 -1
- package/dist/components/TipTapEditor.js.map +1 -1
- package/dist/components/VersionHistory.d.ts +1 -1
- package/dist/components/VersionHistory.d.ts.map +1 -1
- package/dist/components/VersionHistory.js +1 -1
- package/dist/components/VersionHistory.js.map +1 -1
- package/dist/components/ui/Avatar.d.ts.map +1 -1
- package/dist/components/ui/Avatar.js.map +1 -1
- package/dist/components/ui/Badge.d.ts.map +1 -1
- package/dist/components/ui/Badge.js.map +1 -1
- package/dist/components/ui/Button.d.ts.map +1 -1
- package/dist/components/ui/Button.js.map +1 -1
- package/dist/components/ui/CommandPalette.d.ts.map +1 -1
- package/dist/components/ui/CommandPalette.js +8 -2
- package/dist/components/ui/CommandPalette.js.map +1 -1
- package/dist/components/ui/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/ui/ConfirmDialog.js.map +1 -1
- package/dist/components/ui/DataTable.d.ts.map +1 -1
- package/dist/components/ui/DataTable.js +1 -3
- package/dist/components/ui/DataTable.js.map +1 -1
- package/dist/components/ui/EmptyState.d.ts.map +1 -1
- package/dist/components/ui/EmptyState.js +1 -1
- package/dist/components/ui/EmptyState.js.map +1 -1
- package/dist/components/ui/Modal.d.ts.map +1 -1
- package/dist/components/ui/Modal.js.map +1 -1
- package/dist/components/ui/Pagination.d.ts +1 -1
- package/dist/components/ui/Pagination.d.ts.map +1 -1
- package/dist/components/ui/Pagination.js +7 -2
- package/dist/components/ui/Pagination.js.map +1 -1
- package/dist/components/ui/SearchInput.d.ts.map +1 -1
- package/dist/components/ui/SearchInput.js.map +1 -1
- package/dist/components/ui/Skeleton.d.ts.map +1 -1
- package/dist/components/ui/Skeleton.js.map +1 -1
- package/dist/components/ui/Toast.d.ts.map +1 -1
- package/dist/components/ui/Toast.js.map +1 -1
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js.map +1 -1
- package/dist/fields/ArrayField.d.ts.map +1 -1
- package/dist/fields/ArrayField.js +1 -1
- package/dist/fields/ArrayField.js.map +1 -1
- package/dist/fields/BlockBuilderField.d.ts.map +1 -1
- package/dist/fields/BlockBuilderField.js +7 -7
- package/dist/fields/BlockBuilderField.js.map +1 -1
- package/dist/fields/DateField.d.ts.map +1 -1
- package/dist/fields/DateField.js +1 -1
- package/dist/fields/DateField.js.map +1 -1
- package/dist/fields/FieldRenderer.d.ts.map +1 -1
- package/dist/fields/FieldRenderer.js.map +1 -1
- package/dist/fields/GroupField.d.ts.map +1 -1
- package/dist/fields/GroupField.js +1 -1
- package/dist/fields/GroupField.js.map +1 -1
- package/dist/fields/MediaField.d.ts.map +1 -1
- package/dist/fields/MediaField.js +1 -1
- package/dist/fields/MediaField.js.map +1 -1
- package/dist/fields/NavBuilderField.d.ts.map +1 -1
- package/dist/fields/NavBuilderField.js +2 -5
- package/dist/fields/NavBuilderField.js.map +1 -1
- package/dist/fields/NumberField.d.ts +1 -1
- package/dist/fields/NumberField.d.ts.map +1 -1
- package/dist/fields/NumberField.js +2 -2
- package/dist/fields/NumberField.js.map +1 -1
- package/dist/fields/RelationshipField.d.ts.map +1 -1
- package/dist/fields/RelationshipField.js +7 -3
- package/dist/fields/RelationshipField.js.map +1 -1
- package/dist/fields/RichTextField.d.ts +1 -1
- package/dist/fields/RichTextField.d.ts.map +1 -1
- package/dist/fields/RichTextField.js +2 -2
- package/dist/fields/RichTextField.js.map +1 -1
- package/dist/fields/SelectField.d.ts.map +1 -1
- package/dist/fields/SelectField.js +9 -7
- package/dist/fields/SelectField.js.map +1 -1
- package/dist/fields/SlugField.d.ts.map +1 -1
- package/dist/fields/SlugField.js +1 -1
- package/dist/fields/SlugField.js.map +1 -1
- package/dist/fields/TextField.d.ts +1 -1
- package/dist/fields/TextField.d.ts.map +1 -1
- package/dist/fields/TextField.js +2 -2
- package/dist/fields/TextField.js.map +1 -1
- package/dist/fields/ToggleField.d.ts.map +1 -1
- package/dist/fields/ToggleField.js +1 -1
- package/dist/fields/ToggleField.js.map +1 -1
- package/dist/fields/block-types.d.ts.map +1 -1
- package/dist/fields/block-types.js +28 -8
- package/dist/fields/block-types.js.map +1 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js.map +1 -1
- package/dist/hooks/useBuilderState.d.ts.map +1 -1
- package/dist/hooks/useBuilderState.js.map +1 -1
- package/dist/hooks/useContentLock.d.ts.map +1 -1
- package/dist/hooks/useContentLock.js.map +1 -1
- package/dist/hooks/useDebounce.js.map +1 -1
- package/dist/hooks/useKeyboardShortcuts.d.ts.map +1 -1
- package/dist/hooks/useKeyboardShortcuts.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/layout/Header.d.ts.map +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +5 -8
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +3 -5
- package/dist/lib/search.js.map +1 -1
- package/dist/lib/useApiData.d.ts.map +1 -1
- package/dist/lib/useApiData.js.map +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +1 -3
- package/dist/router/index.js.map +1 -1
- package/dist/views/CollectionList.d.ts.map +1 -1
- package/dist/views/CollectionList.js +56 -17
- package/dist/views/CollectionList.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +26 -13
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/DocumentEdit.d.ts +1 -1
- package/dist/views/DocumentEdit.d.ts.map +1 -1
- package/dist/views/DocumentEdit.js +33 -15
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/ForgotPassword.d.ts.map +1 -1
- package/dist/views/ForgotPassword.js.map +1 -1
- package/dist/views/FormEditor.d.ts.map +1 -1
- package/dist/views/FormEditor.js +8 -2
- package/dist/views/FormEditor.js.map +1 -1
- package/dist/views/FormSubmissions.d.ts.map +1 -1
- package/dist/views/FormSubmissions.js +6 -6
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.d.ts.map +1 -1
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/Login.d.ts.map +1 -1
- package/dist/views/Login.js +5 -2
- package/dist/views/Login.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +39 -19
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/PageEditor.d.ts.map +1 -1
- package/dist/views/PageEditor.js.map +1 -1
- package/dist/views/Pages.d.ts.map +1 -1
- package/dist/views/Pages.js +20 -10
- package/dist/views/Pages.js.map +1 -1
- package/dist/views/PostEditor.d.ts.map +1 -1
- package/dist/views/PostEditor.js.map +1 -1
- package/dist/views/Posts.d.ts.map +1 -1
- package/dist/views/Posts.js +13 -7
- package/dist/views/Posts.js.map +1 -1
- package/dist/views/Redirects.d.ts.map +1 -1
- package/dist/views/Redirects.js +17 -5
- package/dist/views/Redirects.js.map +1 -1
- package/dist/views/ResetPassword.d.ts.map +1 -1
- package/dist/views/ResetPassword.js.map +1 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +40 -17
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/ScriptTagEditor.d.ts.map +1 -1
- package/dist/views/ScriptTagEditor.js +2 -1
- package/dist/views/ScriptTagEditor.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +38 -11
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/SetupWizard.d.ts.map +1 -1
- package/dist/views/SetupWizard.js.map +1 -1
- package/dist/views/Users.d.ts.map +1 -1
- package/dist/views/Users.js +5 -3
- package/dist/views/Users.js.map +1 -1
- package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -1
- package/dist/views/page-builder/AIBlockAssist.js +1 -1
- package/dist/views/page-builder/AIBlockAssist.js.map +1 -1
- package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -1
- package/dist/views/page-builder/AIGenerateDialog.js +4 -1
- package/dist/views/page-builder/AIGenerateDialog.js.map +1 -1
- package/dist/views/page-builder/BlockEditor.d.ts.map +1 -1
- package/dist/views/page-builder/BlockEditor.js +1 -1
- package/dist/views/page-builder/BlockEditor.js.map +1 -1
- package/dist/views/page-builder/BlockPicker.d.ts.map +1 -1
- package/dist/views/page-builder/BlockPicker.js.map +1 -1
- package/dist/views/page-builder/BottomBar.d.ts.map +1 -1
- package/dist/views/page-builder/BottomBar.js.map +1 -1
- package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -1
- package/dist/views/page-builder/BuilderToolbar.js.map +1 -1
- package/dist/views/page-builder/ContextPanel.d.ts.map +1 -1
- package/dist/views/page-builder/ContextPanel.js +4 -1
- package/dist/views/page-builder/ContextPanel.js.map +1 -1
- package/dist/views/page-builder/DesignScore.d.ts.map +1 -1
- package/dist/views/page-builder/DesignScore.js.map +1 -1
- package/dist/views/page-builder/NodeSettings.d.ts.map +1 -1
- package/dist/views/page-builder/NodeSettings.js +1 -1
- package/dist/views/page-builder/NodeSettings.js.map +1 -1
- package/dist/views/page-builder/PageBuilder.d.ts +1 -1
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
- package/dist/views/page-builder/PageBuilder.js +4 -2
- package/dist/views/page-builder/PageBuilder.js.map +1 -1
- package/dist/views/page-builder/PageSettings.d.ts.map +1 -1
- package/dist/views/page-builder/PageSettings.js.map +1 -1
- package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
- package/dist/views/page-builder/PageTemplates.js.map +1 -1
- package/dist/views/page-builder/SEOPanel.d.ts.map +1 -1
- package/dist/views/page-builder/SEOPanel.js +1 -3
- package/dist/views/page-builder/SEOPanel.js.map +1 -1
- package/dist/views/page-builder/SavedSections.d.ts.map +1 -1
- package/dist/views/page-builder/SavedSections.js +3 -7
- package/dist/views/page-builder/SavedSections.js.map +1 -1
- package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -1
- package/dist/views/page-builder/TemplatePicker.js.map +1 -1
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/CTAPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/CodePreview.js +1 -5
- package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.js +4 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/FormPreview.js +2 -2
- package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +1 -3
- package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/TextPreview.js +2 -6
- package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/VideoPreview.js +2 -5
- package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/index.js.map +1 -1
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/BlockRenderer.js +1 -5
- package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -1
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -1
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/ColumnRenderer.js +1 -5
- package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -1
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/ContainerRenderer.js +1 -5
- package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -1
- package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/RowRenderer.js +1 -5
- package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -1
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/SectionRenderer.js +1 -5
- package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -1
- package/dist/views/page-builder/canvas/index.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/index.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +263 -191
- package/src/__tests__/lib/search.test.ts +60 -69
- package/src/__tests__/lib/utils.test.ts +12 -12
- package/src/__tests__/router/match-route.test.ts +24 -26
- package/src/__tests__/router/strip-base.test.ts +15 -15
- package/src/components/Breadcrumbs.tsx +27 -24
- package/src/components/CommandPalette.tsx +115 -99
- package/src/components/ContentOverviewChart.tsx +19 -14
- package/src/components/ErrorBoundary.tsx +13 -13
- package/src/components/FocalPointPicker.tsx +31 -20
- package/src/components/FolderTree.tsx +172 -139
- package/src/components/LivePreview.tsx +68 -41
- package/src/components/LocaleProvider.tsx +26 -20
- package/src/components/LocaleSwitcher.tsx +9 -11
- package/src/components/MediaPickerModal.tsx +46 -45
- package/src/components/PresenceIndicator.tsx +30 -27
- package/src/components/SEOPanel.tsx +378 -228
- package/src/components/SEOPerformance.tsx +52 -30
- package/src/components/ThemeProvider.tsx +46 -46
- package/src/components/TipTapEditor.tsx +60 -64
- package/src/components/VersionHistory.tsx +63 -52
- package/src/components/ui/Avatar.tsx +8 -8
- package/src/components/ui/Badge.tsx +7 -5
- package/src/components/ui/Button.tsx +24 -13
- package/src/components/ui/CommandPalette.tsx +56 -42
- package/src/components/ui/ConfirmDialog.tsx +14 -14
- package/src/components/ui/DataTable.tsx +37 -39
- package/src/components/ui/EmptyState.tsx +9 -11
- package/src/components/ui/Modal.tsx +21 -15
- package/src/components/ui/Pagination.tsx +34 -19
- package/src/components/ui/SearchInput.tsx +17 -7
- package/src/components/ui/Skeleton.tsx +7 -7
- package/src/components/ui/Toast.tsx +29 -22
- package/src/components/ui/index.ts +24 -24
- package/src/fields/ArrayField.tsx +43 -25
- package/src/fields/BlockBuilderField.tsx +80 -99
- package/src/fields/DateField.tsx +20 -12
- package/src/fields/FieldRenderer.tsx +34 -34
- package/src/fields/GroupField.tsx +8 -10
- package/src/fields/MediaField.tsx +8 -10
- package/src/fields/NavBuilderField.tsx +24 -25
- package/src/fields/NumberField.tsx +21 -14
- package/src/fields/RelationshipField.tsx +105 -91
- package/src/fields/RichTextField.tsx +16 -12
- package/src/fields/SelectField.tsx +42 -34
- package/src/fields/SlugField.tsx +29 -17
- package/src/fields/TextField.tsx +24 -16
- package/src/fields/ToggleField.tsx +7 -9
- package/src/fields/block-types.ts +50 -24
- package/src/fields/index.ts +17 -17
- package/src/hooks/useBuilderState.ts +260 -221
- package/src/hooks/useContentLock.ts +23 -20
- package/src/hooks/useDebounce.ts +7 -7
- package/src/hooks/useKeyboardShortcuts.ts +16 -16
- package/src/index.ts +69 -58
- package/src/layout/Header.tsx +21 -20
- package/src/layout/Layout.tsx +22 -24
- package/src/layout/Sidebar.tsx +107 -72
- package/src/lib/api.ts +34 -34
- package/src/lib/search.ts +30 -34
- package/src/lib/useApiData.ts +65 -62
- package/src/lib/utils.ts +3 -3
- package/src/router/index.ts +33 -35
- package/src/styles/build-input.css +2 -2
- package/src/styles/tailwind.css +1 -1
- package/src/styles/theme.css +26 -2
- package/src/views/CollectionList.tsx +275 -121
- package/src/views/Dashboard.tsx +164 -117
- package/src/views/DocumentEdit.tsx +298 -253
- package/src/views/ForgotPassword.tsx +27 -23
- package/src/views/FormEditor.tsx +165 -99
- package/src/views/FormSubmissions.tsx +261 -117
- package/src/views/Forms.tsx +56 -26
- package/src/views/Login.tsx +107 -84
- package/src/views/MediaBrowser.tsx +717 -523
- package/src/views/PageEditor.tsx +44 -46
- package/src/views/Pages.tsx +312 -149
- package/src/views/PostEditor.tsx +57 -51
- package/src/views/Posts.tsx +206 -74
- package/src/views/Redirects.tsx +173 -117
- package/src/views/ResetPassword.tsx +43 -32
- package/src/views/SEO.tsx +607 -160
- package/src/views/ScriptTagEditor.tsx +69 -69
- package/src/views/ScriptTags.tsx +54 -42
- package/src/views/Settings.tsx +430 -220
- package/src/views/SetupWizard.tsx +69 -46
- package/src/views/Users.tsx +154 -120
- package/src/views/page-builder/AIBlockAssist.tsx +21 -25
- package/src/views/page-builder/AIGenerateDialog.tsx +134 -127
- package/src/views/page-builder/BlockEditor.tsx +94 -96
- package/src/views/page-builder/BlockPicker.tsx +73 -88
- package/src/views/page-builder/BottomBar.tsx +15 -11
- package/src/views/page-builder/BuilderToolbar.tsx +32 -29
- package/src/views/page-builder/ContextPanel.tsx +57 -57
- package/src/views/page-builder/DesignScore.tsx +52 -59
- package/src/views/page-builder/NodeSettings.tsx +59 -59
- package/src/views/page-builder/PageBuilder.tsx +156 -155
- package/src/views/page-builder/PageSettings.tsx +16 -15
- package/src/views/page-builder/PageTemplates.tsx +23 -17
- package/src/views/page-builder/SEOPanel.tsx +90 -111
- package/src/views/page-builder/SavedSections.tsx +99 -105
- package/src/views/page-builder/TemplatePicker.tsx +44 -48
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +11 -13
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +13 -15
- package/src/views/page-builder/block-renderers/CodePreview.tsx +16 -16
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +20 -23
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +5 -5
- package/src/views/page-builder/block-renderers/FormPreview.tsx +9 -13
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +22 -28
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +17 -30
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +12 -12
- package/src/views/page-builder/block-renderers/TextPreview.tsx +22 -22
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +13 -18
- package/src/views/page-builder/block-renderers/index.ts +17 -17
- package/src/views/page-builder/canvas/BlockRenderer.tsx +19 -23
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +17 -20
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +22 -26
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +20 -24
- package/src/views/page-builder/canvas/RowRenderer.tsx +19 -23
- package/src/views/page-builder/canvas/SectionRenderer.tsx +30 -34
- package/src/views/page-builder/canvas/index.ts +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
'use client'
|
|
1
|
+
'use client'
|
|
2
2
|
|
|
3
|
-
import * as Select from '@radix-ui/react-select'
|
|
4
|
-
import { useState, useMemo } from 'react'
|
|
3
|
+
import * as Select from '@radix-ui/react-select'
|
|
4
|
+
import { useState, useMemo } from 'react'
|
|
5
5
|
import {
|
|
6
6
|
Search,
|
|
7
7
|
RefreshCw,
|
|
@@ -19,84 +19,92 @@ import {
|
|
|
19
19
|
XCircle,
|
|
20
20
|
BarChart3,
|
|
21
21
|
Star,
|
|
22
|
-
} from 'lucide-react'
|
|
22
|
+
} from 'lucide-react'
|
|
23
23
|
|
|
24
24
|
export interface SEOData {
|
|
25
|
-
metaTitle?: string
|
|
26
|
-
metaDescription?: string
|
|
27
|
-
focusKeyphrase?: string
|
|
28
|
-
canonical?: string
|
|
29
|
-
robotsPolicy?:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
metaTitle?: string
|
|
26
|
+
metaDescription?: string
|
|
27
|
+
focusKeyphrase?: string
|
|
28
|
+
canonical?: string
|
|
29
|
+
robotsPolicy?:
|
|
30
|
+
| 'inherit'
|
|
31
|
+
| 'index-follow'
|
|
32
|
+
| 'noindex-follow'
|
|
33
|
+
| 'index-nofollow'
|
|
34
|
+
| 'noindex-nofollow'
|
|
35
|
+
noIndex?: boolean
|
|
36
|
+
noFollow?: boolean
|
|
37
|
+
ogTitle?: string
|
|
38
|
+
ogDescription?: string
|
|
39
|
+
ogImage?: string
|
|
40
|
+
twitterTitle?: string
|
|
41
|
+
twitterDescription?: string
|
|
42
|
+
twitterImage?: string
|
|
43
|
+
isCornerstone?: boolean
|
|
44
|
+
schemaType?: string
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
export interface SEOPanelProps {
|
|
43
|
-
title: string
|
|
44
|
-
slug: string
|
|
45
|
-
content?: string
|
|
46
|
-
seoData: SEOData
|
|
47
|
-
onChange: (data: SEOData) => void
|
|
48
|
-
siteUrl?: string
|
|
48
|
+
title: string
|
|
49
|
+
slug: string
|
|
50
|
+
content?: string
|
|
51
|
+
seoData: SEOData
|
|
52
|
+
onChange: (data: SEOData) => void
|
|
53
|
+
siteUrl?: string
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
const ROBOTS_POLICY_OPTIONS: Array<{ value: NonNullable<SEOData['robotsPolicy']>; label: string }> =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const ROBOTS_POLICY_OPTIONS: Array<{ value: NonNullable<SEOData['robotsPolicy']>; label: string }> =
|
|
57
|
+
[
|
|
58
|
+
{ value: 'inherit', label: 'Inherit site default' },
|
|
59
|
+
{ value: 'index-follow', label: 'Force index, follow' },
|
|
60
|
+
{ value: 'noindex-follow', label: 'Force noindex, follow' },
|
|
61
|
+
{ value: 'index-nofollow', label: 'Force index, nofollow' },
|
|
62
|
+
{ value: 'noindex-nofollow', label: 'Force noindex, nofollow' },
|
|
63
|
+
]
|
|
58
64
|
|
|
59
65
|
function getRobotsPolicy(seoData: SEOData): NonNullable<SEOData['robotsPolicy']> {
|
|
60
|
-
if (seoData.robotsPolicy) return seoData.robotsPolicy
|
|
66
|
+
if (seoData.robotsPolicy) return seoData.robotsPolicy
|
|
61
67
|
if (typeof seoData.noIndex === 'boolean' || typeof seoData.noFollow === 'boolean') {
|
|
62
|
-
const noIndex = seoData.noIndex === true
|
|
63
|
-
const noFollow = seoData.noFollow === true
|
|
64
|
-
if (noIndex && noFollow) return 'noindex-nofollow'
|
|
65
|
-
if (noIndex) return 'noindex-follow'
|
|
66
|
-
if (noFollow) return 'index-nofollow'
|
|
67
|
-
return 'index-follow'
|
|
68
|
+
const noIndex = seoData.noIndex === true
|
|
69
|
+
const noFollow = seoData.noFollow === true
|
|
70
|
+
if (noIndex && noFollow) return 'noindex-nofollow'
|
|
71
|
+
if (noIndex) return 'noindex-follow'
|
|
72
|
+
if (noFollow) return 'index-nofollow'
|
|
73
|
+
return 'index-follow'
|
|
68
74
|
}
|
|
69
|
-
return 'inherit'
|
|
75
|
+
return 'inherit'
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
function robotsPolicyToBooleans(
|
|
78
|
+
function robotsPolicyToBooleans(
|
|
79
|
+
policy: NonNullable<SEOData['robotsPolicy']>,
|
|
80
|
+
): Pick<SEOData, 'noIndex' | 'noFollow'> {
|
|
73
81
|
switch (policy) {
|
|
74
82
|
case 'index-follow':
|
|
75
|
-
return { noIndex: false, noFollow: false }
|
|
83
|
+
return { noIndex: false, noFollow: false }
|
|
76
84
|
case 'noindex-follow':
|
|
77
|
-
return { noIndex: true, noFollow: false }
|
|
85
|
+
return { noIndex: true, noFollow: false }
|
|
78
86
|
case 'index-nofollow':
|
|
79
|
-
return { noIndex: false, noFollow: true }
|
|
87
|
+
return { noIndex: false, noFollow: true }
|
|
80
88
|
case 'noindex-nofollow':
|
|
81
|
-
return { noIndex: true, noFollow: true }
|
|
89
|
+
return { noIndex: true, noFollow: true }
|
|
82
90
|
default:
|
|
83
|
-
return { noIndex: undefined, noFollow: undefined }
|
|
91
|
+
return { noIndex: undefined, noFollow: undefined }
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
interface SEOCheck {
|
|
88
|
-
id: string
|
|
89
|
-
label: string
|
|
90
|
-
status: 'good' | 'ok' | 'bad'
|
|
91
|
-
detail: string
|
|
96
|
+
id: string
|
|
97
|
+
label: string
|
|
98
|
+
status: 'good' | 'ok' | 'bad'
|
|
99
|
+
detail: string
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
interface ReadabilityResult {
|
|
95
|
-
fleschScore: number
|
|
96
|
-
avgSentenceLength: number
|
|
97
|
-
wordCount: number
|
|
98
|
-
readingTime: number
|
|
99
|
-
passiveEstimate: number
|
|
103
|
+
fleschScore: number
|
|
104
|
+
avgSentenceLength: number
|
|
105
|
+
wordCount: number
|
|
106
|
+
readingTime: number
|
|
107
|
+
passiveEstimate: number
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
const SCHEMA_TYPES = [
|
|
@@ -109,216 +117,284 @@ const SCHEMA_TYPES = [
|
|
|
109
117
|
'LocalBusiness',
|
|
110
118
|
'HowTo',
|
|
111
119
|
'Recipe',
|
|
112
|
-
] as const
|
|
120
|
+
] as const
|
|
113
121
|
|
|
114
122
|
function stripHtml(html: string): string {
|
|
115
|
-
return html
|
|
123
|
+
return html
|
|
124
|
+
.replace(/<[^>]*>/g, ' ')
|
|
125
|
+
.replace(/\s+/g, ' ')
|
|
126
|
+
.trim()
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
function countWords(text: string): number {
|
|
119
|
-
if (!text.trim()) return 0
|
|
120
|
-
return text.trim().split(/\s+/).length
|
|
130
|
+
if (!text.trim()) return 0
|
|
131
|
+
return text.trim().split(/\s+/).length
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
function countSentences(text: string): number {
|
|
124
|
-
if (!text.trim()) return 0
|
|
125
|
-
const matches = text.match(/[.!?]+/g)
|
|
126
|
-
return matches ? matches.length : 1
|
|
135
|
+
if (!text.trim()) return 0
|
|
136
|
+
const matches = text.match(/[.!?]+/g)
|
|
137
|
+
return matches ? matches.length : 1
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
function countSyllables(word: string): number {
|
|
130
|
-
const w = word.toLowerCase().replace(/[^a-z]/g, '')
|
|
131
|
-
if (w.length <= 3) return 1
|
|
132
|
-
let count = 0
|
|
133
|
-
const vowels = 'aeiouy'
|
|
134
|
-
let prevVowel = false
|
|
141
|
+
const w = word.toLowerCase().replace(/[^a-z]/g, '')
|
|
142
|
+
if (w.length <= 3) return 1
|
|
143
|
+
let count = 0
|
|
144
|
+
const vowels = 'aeiouy'
|
|
145
|
+
let prevVowel = false
|
|
135
146
|
for (let i = 0; i < w.length; i++) {
|
|
136
|
-
const isVowel = vowels.includes(w[i] ?? '')
|
|
137
|
-
if (isVowel && !prevVowel) count
|
|
138
|
-
prevVowel = isVowel
|
|
147
|
+
const isVowel = vowels.includes(w[i] ?? '')
|
|
148
|
+
if (isVowel && !prevVowel) count++
|
|
149
|
+
prevVowel = isVowel
|
|
139
150
|
}
|
|
140
|
-
if (w.endsWith('e') && count > 1) count
|
|
141
|
-
return Math.max(count, 1)
|
|
151
|
+
if (w.endsWith('e') && count > 1) count--
|
|
152
|
+
return Math.max(count, 1)
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
function analyzeReadability(text: string): ReadabilityResult {
|
|
145
|
-
const plainText = stripHtml(text)
|
|
146
|
-
const wordCount = countWords(plainText)
|
|
147
|
-
const sentenceCount = countSentences(plainText)
|
|
148
|
-
const avgSentenceLength = sentenceCount > 0 ? wordCount / sentenceCount : 0
|
|
149
|
-
const readingTime = Math.max(1, Math.ceil(wordCount / 200))
|
|
156
|
+
const plainText = stripHtml(text)
|
|
157
|
+
const wordCount = countWords(plainText)
|
|
158
|
+
const sentenceCount = countSentences(plainText)
|
|
159
|
+
const avgSentenceLength = sentenceCount > 0 ? wordCount / sentenceCount : 0
|
|
160
|
+
const readingTime = Math.max(1, Math.ceil(wordCount / 200))
|
|
150
161
|
|
|
151
|
-
let fleschScore = 0
|
|
162
|
+
let fleschScore = 0
|
|
152
163
|
if (wordCount > 0 && sentenceCount > 0) {
|
|
153
|
-
const words = plainText.split(/\s+/)
|
|
154
|
-
const totalSyllables = words.reduce((sum, w) => sum + countSyllables(w), 0)
|
|
164
|
+
const words = plainText.split(/\s+/)
|
|
165
|
+
const totalSyllables = words.reduce((sum, w) => sum + countSyllables(w), 0)
|
|
155
166
|
fleschScore = Math.round(
|
|
156
|
-
206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount)
|
|
157
|
-
)
|
|
158
|
-
fleschScore = Math.max(0, Math.min(100, fleschScore))
|
|
167
|
+
206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount),
|
|
168
|
+
)
|
|
169
|
+
fleschScore = Math.max(0, Math.min(100, fleschScore))
|
|
159
170
|
}
|
|
160
171
|
|
|
161
|
-
const passivePatterns = /\b(is|are|was|were|been|being|be)\s+\w+ed\b/gi
|
|
162
|
-
const passiveMatches = plainText.match(passivePatterns)
|
|
172
|
+
const passivePatterns = /\b(is|are|was|were|been|being|be)\s+\w+ed\b/gi
|
|
173
|
+
const passiveMatches = plainText.match(passivePatterns)
|
|
163
174
|
const passiveEstimate = passiveMatches
|
|
164
175
|
? Math.round((passiveMatches.length / Math.max(sentenceCount, 1)) * 100)
|
|
165
|
-
: 0
|
|
176
|
+
: 0
|
|
166
177
|
|
|
167
|
-
return { fleschScore, avgSentenceLength, wordCount, readingTime, passiveEstimate }
|
|
178
|
+
return { fleschScore, avgSentenceLength, wordCount, readingTime, passiveEstimate }
|
|
168
179
|
}
|
|
169
180
|
|
|
170
|
-
function runSEOChecks(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
const plainText = stripHtml(content);
|
|
178
|
-
const wordCount = countWords(plainText);
|
|
179
|
-
const keyphrase = (seoData.focusKeyphrase ?? '').toLowerCase().trim();
|
|
180
|
-
const metaTitle = seoData.metaTitle ?? '';
|
|
181
|
-
const metaDesc = seoData.metaDescription ?? '';
|
|
181
|
+
function runSEOChecks(seoData: SEOData, title: string, slug: string, content: string): SEOCheck[] {
|
|
182
|
+
const checks: SEOCheck[] = []
|
|
183
|
+
const plainText = stripHtml(content)
|
|
184
|
+
const wordCount = countWords(plainText)
|
|
185
|
+
const keyphrase = (seoData.focusKeyphrase ?? '').toLowerCase().trim()
|
|
186
|
+
const metaTitle = seoData.metaTitle ?? ''
|
|
187
|
+
const metaDesc = seoData.metaDescription ?? ''
|
|
182
188
|
|
|
183
189
|
// Meta title length
|
|
184
190
|
if (!metaTitle) {
|
|
185
|
-
checks.push({
|
|
191
|
+
checks.push({
|
|
192
|
+
id: 'title-missing',
|
|
193
|
+
label: 'Meta title',
|
|
194
|
+
status: 'bad',
|
|
195
|
+
detail: 'No meta title set',
|
|
196
|
+
})
|
|
186
197
|
} else if (metaTitle.length >= 30 && metaTitle.length <= 60) {
|
|
187
|
-
checks.push({
|
|
198
|
+
checks.push({
|
|
199
|
+
id: 'title-length',
|
|
200
|
+
label: 'Meta title length',
|
|
201
|
+
status: 'good',
|
|
202
|
+
detail: `${metaTitle.length} chars (ideal: 30-60)`,
|
|
203
|
+
})
|
|
188
204
|
} else if (metaTitle.length > 60) {
|
|
189
|
-
checks.push({
|
|
205
|
+
checks.push({
|
|
206
|
+
id: 'title-length',
|
|
207
|
+
label: 'Meta title length',
|
|
208
|
+
status: 'ok',
|
|
209
|
+
detail: `${metaTitle.length} chars — too long, may be truncated`,
|
|
210
|
+
})
|
|
190
211
|
} else {
|
|
191
|
-
checks.push({
|
|
212
|
+
checks.push({
|
|
213
|
+
id: 'title-length',
|
|
214
|
+
label: 'Meta title length',
|
|
215
|
+
status: 'ok',
|
|
216
|
+
detail: `${metaTitle.length} chars — quite short`,
|
|
217
|
+
})
|
|
192
218
|
}
|
|
193
219
|
|
|
194
220
|
// Meta description length
|
|
195
221
|
if (!metaDesc) {
|
|
196
|
-
checks.push({
|
|
222
|
+
checks.push({
|
|
223
|
+
id: 'desc-missing',
|
|
224
|
+
label: 'Meta description',
|
|
225
|
+
status: 'bad',
|
|
226
|
+
detail: 'No meta description set',
|
|
227
|
+
})
|
|
197
228
|
} else if (metaDesc.length >= 120 && metaDesc.length <= 160) {
|
|
198
|
-
checks.push({
|
|
229
|
+
checks.push({
|
|
230
|
+
id: 'desc-length',
|
|
231
|
+
label: 'Meta description length',
|
|
232
|
+
status: 'good',
|
|
233
|
+
detail: `${metaDesc.length} chars (ideal: 120-160)`,
|
|
234
|
+
})
|
|
199
235
|
} else if (metaDesc.length > 160) {
|
|
200
|
-
checks.push({
|
|
236
|
+
checks.push({
|
|
237
|
+
id: 'desc-length',
|
|
238
|
+
label: 'Meta description length',
|
|
239
|
+
status: 'ok',
|
|
240
|
+
detail: `${metaDesc.length} chars — may be truncated`,
|
|
241
|
+
})
|
|
201
242
|
} else {
|
|
202
|
-
checks.push({
|
|
243
|
+
checks.push({
|
|
244
|
+
id: 'desc-length',
|
|
245
|
+
label: 'Meta description length',
|
|
246
|
+
status: 'ok',
|
|
247
|
+
detail: `${metaDesc.length} chars — could be longer`,
|
|
248
|
+
})
|
|
203
249
|
}
|
|
204
250
|
|
|
205
251
|
// Content length
|
|
206
252
|
if (wordCount >= 300) {
|
|
207
|
-
checks.push({
|
|
253
|
+
checks.push({
|
|
254
|
+
id: 'content-length',
|
|
255
|
+
label: 'Content length',
|
|
256
|
+
status: 'good',
|
|
257
|
+
detail: `${wordCount} words`,
|
|
258
|
+
})
|
|
208
259
|
} else if (wordCount >= 150) {
|
|
209
|
-
checks.push({
|
|
260
|
+
checks.push({
|
|
261
|
+
id: 'content-length',
|
|
262
|
+
label: 'Content length',
|
|
263
|
+
status: 'ok',
|
|
264
|
+
detail: `${wordCount} words — aim for 300+`,
|
|
265
|
+
})
|
|
210
266
|
} else {
|
|
211
|
-
checks.push({
|
|
267
|
+
checks.push({
|
|
268
|
+
id: 'content-length',
|
|
269
|
+
label: 'Content length',
|
|
270
|
+
status: 'bad',
|
|
271
|
+
detail: `${wordCount} words — too short`,
|
|
272
|
+
})
|
|
212
273
|
}
|
|
213
274
|
|
|
214
275
|
// Focus keyphrase checks
|
|
215
276
|
if (!keyphrase) {
|
|
216
|
-
checks.push({
|
|
277
|
+
checks.push({
|
|
278
|
+
id: 'keyphrase-missing',
|
|
279
|
+
label: 'Focus keyphrase',
|
|
280
|
+
status: 'bad',
|
|
281
|
+
detail: 'No focus keyphrase set',
|
|
282
|
+
})
|
|
217
283
|
} else {
|
|
218
|
-
const inTitle = metaTitle.toLowerCase().includes(keyphrase)
|
|
284
|
+
const inTitle = metaTitle.toLowerCase().includes(keyphrase)
|
|
219
285
|
checks.push({
|
|
220
286
|
id: 'keyphrase-title',
|
|
221
287
|
label: 'Keyphrase in title',
|
|
222
288
|
status: inTitle ? 'good' : 'bad',
|
|
223
289
|
detail: inTitle ? 'Found in meta title' : 'Not found in meta title',
|
|
224
|
-
})
|
|
290
|
+
})
|
|
225
291
|
|
|
226
|
-
const inDesc = metaDesc.toLowerCase().includes(keyphrase)
|
|
292
|
+
const inDesc = metaDesc.toLowerCase().includes(keyphrase)
|
|
227
293
|
checks.push({
|
|
228
294
|
id: 'keyphrase-desc',
|
|
229
295
|
label: 'Keyphrase in description',
|
|
230
296
|
status: inDesc ? 'good' : 'ok',
|
|
231
297
|
detail: inDesc ? 'Found in meta description' : 'Not found in meta description',
|
|
232
|
-
})
|
|
298
|
+
})
|
|
233
299
|
|
|
234
|
-
const inSlug = slug.toLowerCase().includes(keyphrase.replace(/\s+/g, '-'))
|
|
300
|
+
const inSlug = slug.toLowerCase().includes(keyphrase.replace(/\s+/g, '-'))
|
|
235
301
|
checks.push({
|
|
236
302
|
id: 'keyphrase-slug',
|
|
237
303
|
label: 'Keyphrase in slug',
|
|
238
304
|
status: inSlug ? 'good' : 'ok',
|
|
239
305
|
detail: inSlug ? 'Found in URL slug' : 'Not found in URL slug',
|
|
240
|
-
})
|
|
306
|
+
})
|
|
241
307
|
|
|
242
|
-
const firstParagraph = plainText.slice(0, 300).toLowerCase()
|
|
243
|
-
const inIntro = firstParagraph.includes(keyphrase)
|
|
308
|
+
const firstParagraph = plainText.slice(0, 300).toLowerCase()
|
|
309
|
+
const inIntro = firstParagraph.includes(keyphrase)
|
|
244
310
|
checks.push({
|
|
245
311
|
id: 'keyphrase-intro',
|
|
246
312
|
label: 'Keyphrase in introduction',
|
|
247
313
|
status: inIntro ? 'good' : 'ok',
|
|
248
314
|
detail: inIntro ? 'Found in first paragraph' : 'Not found in first paragraph',
|
|
249
|
-
})
|
|
315
|
+
})
|
|
250
316
|
|
|
251
317
|
if (wordCount > 0) {
|
|
252
|
-
const kpWords = keyphrase.split(/\s+/).length
|
|
253
|
-
const regex = new RegExp(keyphrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
|
|
254
|
-
const matches = plainText.match(regex)
|
|
255
|
-
const occurrences = matches ? matches.length : 0
|
|
256
|
-
const density = (occurrences * kpWords) / wordCount * 100
|
|
257
|
-
const densityOk = density >= 0.5 && density <= 3
|
|
318
|
+
const kpWords = keyphrase.split(/\s+/).length
|
|
319
|
+
const regex = new RegExp(keyphrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
|
|
320
|
+
const matches = plainText.match(regex)
|
|
321
|
+
const occurrences = matches ? matches.length : 0
|
|
322
|
+
const density = ((occurrences * kpWords) / wordCount) * 100
|
|
323
|
+
const densityOk = density >= 0.5 && density <= 3
|
|
258
324
|
checks.push({
|
|
259
325
|
id: 'keyphrase-density',
|
|
260
326
|
label: 'Keyphrase density',
|
|
261
327
|
status: densityOk ? 'good' : density === 0 ? 'bad' : 'ok',
|
|
262
328
|
detail: `${density.toFixed(1)}% (aim for 0.5-3%)`,
|
|
263
|
-
})
|
|
329
|
+
})
|
|
264
330
|
}
|
|
265
331
|
}
|
|
266
332
|
|
|
267
333
|
// Image alt text
|
|
268
|
-
const imgTags = content.match(/<img[^>]*>/gi) ?? []
|
|
334
|
+
const imgTags = content.match(/<img[^>]*>/gi) ?? []
|
|
269
335
|
if (imgTags.length > 0) {
|
|
270
|
-
const withoutAlt = imgTags.filter(tag => !tag.match(/alt\s*=\s*["'][^"']+["']/i))
|
|
336
|
+
const withoutAlt = imgTags.filter((tag) => !tag.match(/alt\s*=\s*["'][^"']+["']/i))
|
|
271
337
|
if (withoutAlt.length === 0) {
|
|
272
|
-
checks.push({
|
|
338
|
+
checks.push({
|
|
339
|
+
id: 'img-alt',
|
|
340
|
+
label: 'Image alt text',
|
|
341
|
+
status: 'good',
|
|
342
|
+
detail: 'All images have alt text',
|
|
343
|
+
})
|
|
273
344
|
} else {
|
|
274
|
-
checks.push({
|
|
345
|
+
checks.push({
|
|
346
|
+
id: 'img-alt',
|
|
347
|
+
label: 'Image alt text',
|
|
348
|
+
status: 'ok',
|
|
349
|
+
detail: `${withoutAlt.length} image(s) missing alt text`,
|
|
350
|
+
})
|
|
275
351
|
}
|
|
276
352
|
}
|
|
277
353
|
|
|
278
354
|
// OG checks
|
|
279
|
-
const hasOgTitle = !!(seoData.ogTitle || metaTitle)
|
|
280
|
-
const hasOgDesc = !!(seoData.ogDescription || metaDesc)
|
|
281
|
-
const hasOgImage = !!seoData.ogImage
|
|
355
|
+
const hasOgTitle = !!(seoData.ogTitle || metaTitle)
|
|
356
|
+
const hasOgDesc = !!(seoData.ogDescription || metaDesc)
|
|
357
|
+
const hasOgImage = !!seoData.ogImage
|
|
282
358
|
checks.push({
|
|
283
359
|
id: 'og-title',
|
|
284
360
|
label: 'Social title',
|
|
285
361
|
status: hasOgTitle ? 'good' : 'ok',
|
|
286
362
|
detail: hasOgTitle ? 'Set' : 'Missing — will fall back to page title',
|
|
287
|
-
})
|
|
363
|
+
})
|
|
288
364
|
checks.push({
|
|
289
365
|
id: 'og-desc',
|
|
290
366
|
label: 'Social description',
|
|
291
367
|
status: hasOgDesc ? 'good' : 'ok',
|
|
292
368
|
detail: hasOgDesc ? 'Set' : 'Missing — will fall back to meta description',
|
|
293
|
-
})
|
|
369
|
+
})
|
|
294
370
|
checks.push({
|
|
295
371
|
id: 'og-image',
|
|
296
372
|
label: 'Social image',
|
|
297
373
|
status: hasOgImage ? 'good' : 'bad',
|
|
298
374
|
detail: hasOgImage ? 'Set' : 'No OG image set — strongly recommended',
|
|
299
|
-
})
|
|
375
|
+
})
|
|
300
376
|
|
|
301
|
-
return checks
|
|
377
|
+
return checks
|
|
302
378
|
}
|
|
303
379
|
|
|
304
380
|
function computeOverallScore(checks: SEOCheck[]): number {
|
|
305
|
-
if (checks.length === 0) return 0
|
|
306
|
-
const scoreMap = { good: 100, ok: 50, bad: 0 }
|
|
307
|
-
const total = checks.reduce((sum, c) => sum + scoreMap[c.status], 0)
|
|
308
|
-
return Math.round(total / checks.length)
|
|
381
|
+
if (checks.length === 0) return 0
|
|
382
|
+
const scoreMap = { good: 100, ok: 50, bad: 0 }
|
|
383
|
+
const total = checks.reduce((sum, c) => sum + scoreMap[c.status], 0)
|
|
384
|
+
return Math.round(total / checks.length)
|
|
309
385
|
}
|
|
310
386
|
|
|
311
387
|
function StatusDot({ status }: { status: 'good' | 'ok' | 'bad' }) {
|
|
312
|
-
if (status === 'good') return <CheckCircle2 className="h-4 w-4 shrink-0 text-green-500"
|
|
313
|
-
if (status === 'ok') return <AlertCircle className="h-4 w-4 shrink-0 text-amber-500"
|
|
314
|
-
return <XCircle className="h-4 w-4 shrink-0 text-red-500"
|
|
388
|
+
if (status === 'good') return <CheckCircle2 className="h-4 w-4 shrink-0 text-green-500" />
|
|
389
|
+
if (status === 'ok') return <AlertCircle className="h-4 w-4 shrink-0 text-amber-500" />
|
|
390
|
+
return <XCircle className="h-4 w-4 shrink-0 text-red-500" />
|
|
315
391
|
}
|
|
316
392
|
|
|
317
393
|
function ScoreRing({ score }: { score: number }) {
|
|
318
|
-
const radius = 28
|
|
319
|
-
const circumference = 2 * Math.PI * radius
|
|
320
|
-
const offset = circumference - (score / 100) * circumference
|
|
321
|
-
const color = score >= 70 ? '#22c55e' : score >= 40 ? '#f59e0b' : '#ef4444'
|
|
394
|
+
const radius = 28
|
|
395
|
+
const circumference = 2 * Math.PI * radius
|
|
396
|
+
const offset = circumference - (score / 100) * circumference
|
|
397
|
+
const color = score >= 70 ? '#22c55e' : score >= 40 ? '#f59e0b' : '#ef4444'
|
|
322
398
|
|
|
323
399
|
return (
|
|
324
400
|
<div className="relative inline-flex items-center justify-center">
|
|
@@ -337,9 +413,11 @@ function ScoreRing({ score }: { score: number }) {
|
|
|
337
413
|
className="transition-all duration-500"
|
|
338
414
|
/>
|
|
339
415
|
</svg>
|
|
340
|
-
<span className="absolute text-sm font-bold" style={{ color }}>
|
|
416
|
+
<span className="absolute text-sm font-bold" style={{ color }}>
|
|
417
|
+
{score}
|
|
418
|
+
</span>
|
|
341
419
|
</div>
|
|
342
|
-
)
|
|
420
|
+
)
|
|
343
421
|
}
|
|
344
422
|
|
|
345
423
|
function Section({
|
|
@@ -351,13 +429,13 @@ function Section({
|
|
|
351
429
|
children,
|
|
352
430
|
badge,
|
|
353
431
|
}: {
|
|
354
|
-
id: string
|
|
355
|
-
title: string
|
|
356
|
-
icon: React.ReactNode
|
|
357
|
-
expanded: boolean
|
|
358
|
-
onToggle: (id: string) => void
|
|
359
|
-
children: React.ReactNode
|
|
360
|
-
badge?: React.ReactNode
|
|
432
|
+
id: string
|
|
433
|
+
title: string
|
|
434
|
+
icon: React.ReactNode
|
|
435
|
+
expanded: boolean
|
|
436
|
+
onToggle: (id: string) => void
|
|
437
|
+
children: React.ReactNode
|
|
438
|
+
badge?: React.ReactNode
|
|
361
439
|
}) {
|
|
362
440
|
return (
|
|
363
441
|
<div className="border border-[var(--border)] rounded-lg overflow-hidden">
|
|
@@ -370,11 +448,15 @@ function Section({
|
|
|
370
448
|
{title}
|
|
371
449
|
{badge}
|
|
372
450
|
</span>
|
|
373
|
-
{expanded ?
|
|
451
|
+
{expanded ? (
|
|
452
|
+
<ChevronUp className="h-4 w-4 text-[var(--muted-foreground)]" />
|
|
453
|
+
) : (
|
|
454
|
+
<ChevronDown className="h-4 w-4 text-[var(--muted-foreground)]" />
|
|
455
|
+
)}
|
|
374
456
|
</button>
|
|
375
457
|
{expanded && <div className="px-4 pb-4 pt-1">{children}</div>}
|
|
376
458
|
</div>
|
|
377
|
-
)
|
|
459
|
+
)
|
|
378
460
|
}
|
|
379
461
|
|
|
380
462
|
function ToggleSwitch({
|
|
@@ -383,10 +465,10 @@ function ToggleSwitch({
|
|
|
383
465
|
checked,
|
|
384
466
|
onChange,
|
|
385
467
|
}: {
|
|
386
|
-
label: string
|
|
387
|
-
description: string
|
|
388
|
-
checked: boolean
|
|
389
|
-
onChange: (v: boolean) => void
|
|
468
|
+
label: string
|
|
469
|
+
description: string
|
|
470
|
+
checked: boolean
|
|
471
|
+
onChange: (v: boolean) => void
|
|
390
472
|
}) {
|
|
391
473
|
return (
|
|
392
474
|
<div className="flex items-center justify-between gap-3">
|
|
@@ -401,10 +483,12 @@ function ToggleSwitch({
|
|
|
401
483
|
onClick={() => onChange(!checked)}
|
|
402
484
|
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition-colors ${checked ? 'bg-[var(--primary)]' : 'bg-[var(--muted)]'}`}
|
|
403
485
|
>
|
|
404
|
-
<span
|
|
486
|
+
<span
|
|
487
|
+
className={`pointer-events-none block h-5 w-5 rounded-full bg-white shadow-sm transition-transform mt-0.5 ${checked ? 'translate-x-[22px]' : 'translate-x-0.5'}`}
|
|
488
|
+
/>
|
|
405
489
|
</button>
|
|
406
490
|
</div>
|
|
407
|
-
)
|
|
491
|
+
)
|
|
408
492
|
}
|
|
409
493
|
|
|
410
494
|
function InputField({
|
|
@@ -416,17 +500,19 @@ function InputField({
|
|
|
416
500
|
charCount,
|
|
417
501
|
charTarget,
|
|
418
502
|
}: {
|
|
419
|
-
label: string
|
|
420
|
-
value: string
|
|
421
|
-
onChange: (v: string) => void
|
|
422
|
-
placeholder?: string
|
|
423
|
-
type?: string
|
|
424
|
-
charCount?: number
|
|
425
|
-
charTarget?: string
|
|
503
|
+
label: string
|
|
504
|
+
value: string
|
|
505
|
+
onChange: (v: string) => void
|
|
506
|
+
placeholder?: string
|
|
507
|
+
type?: string
|
|
508
|
+
charCount?: number
|
|
509
|
+
charTarget?: string
|
|
426
510
|
}) {
|
|
427
511
|
return (
|
|
428
512
|
<div>
|
|
429
|
-
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
|
|
513
|
+
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
|
|
514
|
+
{label}
|
|
515
|
+
</label>
|
|
430
516
|
<input
|
|
431
517
|
type={type}
|
|
432
518
|
value={value}
|
|
@@ -435,10 +521,12 @@ function InputField({
|
|
|
435
521
|
placeholder={placeholder}
|
|
436
522
|
/>
|
|
437
523
|
{charCount !== undefined && charTarget && (
|
|
438
|
-
<p className="text-xs mt-1 text-[var(--muted-foreground)]">
|
|
524
|
+
<p className="text-xs mt-1 text-[var(--muted-foreground)]">
|
|
525
|
+
{charCount} chars {charTarget}
|
|
526
|
+
</p>
|
|
439
527
|
)}
|
|
440
528
|
</div>
|
|
441
|
-
)
|
|
529
|
+
)
|
|
442
530
|
}
|
|
443
531
|
|
|
444
532
|
function TextareaField({
|
|
@@ -450,17 +538,19 @@ function TextareaField({
|
|
|
450
538
|
charCount,
|
|
451
539
|
charTarget,
|
|
452
540
|
}: {
|
|
453
|
-
label: string
|
|
454
|
-
value: string
|
|
455
|
-
onChange: (v: string) => void
|
|
456
|
-
placeholder?: string
|
|
457
|
-
rows?: number
|
|
458
|
-
charCount?: number
|
|
459
|
-
charTarget?: string
|
|
541
|
+
label: string
|
|
542
|
+
value: string
|
|
543
|
+
onChange: (v: string) => void
|
|
544
|
+
placeholder?: string
|
|
545
|
+
rows?: number
|
|
546
|
+
charCount?: number
|
|
547
|
+
charTarget?: string
|
|
460
548
|
}) {
|
|
461
549
|
return (
|
|
462
550
|
<div>
|
|
463
|
-
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
|
|
551
|
+
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
|
|
552
|
+
{label}
|
|
553
|
+
</label>
|
|
464
554
|
<textarea
|
|
465
555
|
value={value}
|
|
466
556
|
onChange={(e) => onChange(e.target.value)}
|
|
@@ -469,42 +559,65 @@ function TextareaField({
|
|
|
469
559
|
placeholder={placeholder}
|
|
470
560
|
/>
|
|
471
561
|
{charCount !== undefined && charTarget && (
|
|
472
|
-
<p className="text-xs mt-1 text-[var(--muted-foreground)]">
|
|
562
|
+
<p className="text-xs mt-1 text-[var(--muted-foreground)]">
|
|
563
|
+
{charCount} chars {charTarget}
|
|
564
|
+
</p>
|
|
473
565
|
)}
|
|
474
566
|
</div>
|
|
475
|
-
)
|
|
567
|
+
)
|
|
476
568
|
}
|
|
477
569
|
|
|
478
|
-
export function SEOPanel({
|
|
479
|
-
|
|
570
|
+
export function SEOPanel({
|
|
571
|
+
title,
|
|
572
|
+
slug,
|
|
573
|
+
content = '',
|
|
574
|
+
seoData,
|
|
575
|
+
onChange,
|
|
576
|
+
siteUrl = 'https://example.com',
|
|
577
|
+
}: SEOPanelProps) {
|
|
578
|
+
const [expandedSections, setExpandedSections] = useState<string[]>(['analysis'])
|
|
480
579
|
|
|
481
580
|
const update = (partial: Partial<SEOData>) => {
|
|
482
|
-
onChange({ ...seoData, ...partial })
|
|
483
|
-
}
|
|
581
|
+
onChange({ ...seoData, ...partial })
|
|
582
|
+
}
|
|
484
583
|
|
|
485
584
|
const toggleSection = (id: string) => {
|
|
486
585
|
setExpandedSections((prev) =>
|
|
487
|
-
prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id]
|
|
488
|
-
)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const checks = useMemo(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
const
|
|
586
|
+
prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id],
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const checks = useMemo(
|
|
591
|
+
() => runSEOChecks(seoData, title, slug, content),
|
|
592
|
+
[seoData, title, slug, content],
|
|
593
|
+
)
|
|
594
|
+
const readability = useMemo(() => analyzeReadability(content), [content])
|
|
595
|
+
const score = useMemo(() => computeOverallScore(checks), [checks])
|
|
596
|
+
|
|
597
|
+
const goodCount = checks.filter((c) => c.status === 'good').length
|
|
598
|
+
const okCount = checks.filter((c) => c.status === 'ok').length
|
|
599
|
+
const badCount = checks.filter((c) => c.status === 'bad').length
|
|
600
|
+
|
|
601
|
+
const metaTitle = seoData.metaTitle ?? ''
|
|
602
|
+
const metaDesc = seoData.metaDescription ?? ''
|
|
603
|
+
const robotsPolicy = getRobotsPolicy(seoData)
|
|
604
|
+
const displayTitle = seoData.ogTitle || metaTitle || title || 'Page Title'
|
|
605
|
+
const displayDesc =
|
|
606
|
+
seoData.ogDescription ||
|
|
607
|
+
metaDesc ||
|
|
608
|
+
'Add a meta description to see how this page will appear in search results.'
|
|
504
609
|
const fleschLabel =
|
|
505
|
-
readability.fleschScore >= 60
|
|
610
|
+
readability.fleschScore >= 60
|
|
611
|
+
? 'Easy to read'
|
|
612
|
+
: readability.fleschScore >= 30
|
|
613
|
+
? 'Fairly difficult'
|
|
614
|
+
: 'Very difficult'
|
|
506
615
|
const fleschColor =
|
|
507
|
-
readability.fleschScore >= 60
|
|
616
|
+
readability.fleschScore >= 60
|
|
617
|
+
? 'text-green-500'
|
|
618
|
+
: readability.fleschScore >= 30
|
|
619
|
+
? 'text-amber-500'
|
|
620
|
+
: 'text-red-500'
|
|
508
621
|
|
|
509
622
|
return (
|
|
510
623
|
<div className="rounded-lg border border-[var(--border)] bg-[var(--card)]">
|
|
@@ -527,9 +640,15 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
|
|
|
527
640
|
<div className="flex items-center gap-4 px-4 py-3 border-b border-[var(--border)]">
|
|
528
641
|
<ScoreRing score={score} />
|
|
529
642
|
<div className="text-xs text-[var(--muted-foreground)] space-y-0.5">
|
|
530
|
-
<div className="flex items-center gap-1.5"
|
|
531
|
-
|
|
532
|
-
|
|
643
|
+
<div className="flex items-center gap-1.5">
|
|
644
|
+
<CheckCircle2 className="h-3 w-3 text-green-500" /> {goodCount} passed
|
|
645
|
+
</div>
|
|
646
|
+
<div className="flex items-center gap-1.5">
|
|
647
|
+
<AlertCircle className="h-3 w-3 text-amber-500" /> {okCount} improvements
|
|
648
|
+
</div>
|
|
649
|
+
<div className="flex items-center gap-1.5">
|
|
650
|
+
<XCircle className="h-3 w-3 text-red-500" /> {badCount} issues
|
|
651
|
+
</div>
|
|
533
652
|
</div>
|
|
534
653
|
</div>
|
|
535
654
|
|
|
@@ -582,18 +701,28 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
|
|
|
582
701
|
</div>
|
|
583
702
|
<div className="rounded-lg bg-[var(--muted)] p-2.5">
|
|
584
703
|
<p className="text-xs text-[var(--muted-foreground)]">Avg. Sentence</p>
|
|
585
|
-
<p className="text-lg font-bold text-[var(--foreground)]">
|
|
704
|
+
<p className="text-lg font-bold text-[var(--foreground)]">
|
|
705
|
+
{readability.avgSentenceLength.toFixed(1)}
|
|
706
|
+
</p>
|
|
586
707
|
<p className="text-[10px] text-[var(--muted-foreground)]">words/sentence</p>
|
|
587
708
|
</div>
|
|
588
709
|
<div className="rounded-lg bg-[var(--muted)] p-2.5">
|
|
589
710
|
<p className="text-xs text-[var(--muted-foreground)]">Reading Time</p>
|
|
590
|
-
<p className="text-lg font-bold text-[var(--foreground)]">
|
|
711
|
+
<p className="text-lg font-bold text-[var(--foreground)]">
|
|
712
|
+
{readability.readingTime}
|
|
713
|
+
</p>
|
|
591
714
|
<p className="text-[10px] text-[var(--muted-foreground)]">min</p>
|
|
592
715
|
</div>
|
|
593
716
|
</div>
|
|
594
717
|
<div className="mt-3 flex items-center gap-2 text-xs text-[var(--muted-foreground)]">
|
|
595
718
|
<span>Passive voice est.:</span>
|
|
596
|
-
<span
|
|
719
|
+
<span
|
|
720
|
+
className={
|
|
721
|
+
readability.passiveEstimate > 15
|
|
722
|
+
? 'text-amber-500 font-medium'
|
|
723
|
+
: 'text-green-500 font-medium'
|
|
724
|
+
}
|
|
725
|
+
>
|
|
597
726
|
{readability.passiveEstimate}%
|
|
598
727
|
</span>
|
|
599
728
|
</div>
|
|
@@ -650,14 +779,17 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
|
|
|
650
779
|
>
|
|
651
780
|
<div className="space-y-4">
|
|
652
781
|
<div>
|
|
653
|
-
<label
|
|
782
|
+
<label
|
|
783
|
+
id="robots-policy-label"
|
|
784
|
+
className="mb-1 block text-xs font-medium text-muted-foreground"
|
|
785
|
+
>
|
|
654
786
|
Robots Policy
|
|
655
787
|
</label>
|
|
656
788
|
<Select.Root
|
|
657
789
|
value={robotsPolicy}
|
|
658
790
|
onValueChange={(value) => {
|
|
659
|
-
const policy = value as NonNullable<SEOData['robotsPolicy']
|
|
660
|
-
update({ robotsPolicy: policy, ...robotsPolicyToBooleans(policy) })
|
|
791
|
+
const policy = value as NonNullable<SEOData['robotsPolicy']>
|
|
792
|
+
update({ robotsPolicy: policy, ...robotsPolicyToBooleans(policy) })
|
|
661
793
|
}}
|
|
662
794
|
>
|
|
663
795
|
<Select.Trigger
|
|
@@ -689,7 +821,8 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
|
|
|
689
821
|
</Select.Portal>
|
|
690
822
|
</Select.Root>
|
|
691
823
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
692
|
-
Use inheritance for most pages. Override only when a page needs different
|
|
824
|
+
Use inheritance for most pages. Override only when a page needs different
|
|
825
|
+
index/follow behavior.
|
|
693
826
|
</p>
|
|
694
827
|
</div>
|
|
695
828
|
</div>
|
|
@@ -711,7 +844,8 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
|
|
|
711
844
|
{siteUrl}/{slug}
|
|
712
845
|
</div>
|
|
713
846
|
<div className="text-sm text-[var(--muted-foreground)] mt-1 line-clamp-2">
|
|
714
|
-
{metaDesc ||
|
|
847
|
+
{metaDesc ||
|
|
848
|
+
'Add a meta description to see how this page will appear in search results.'}
|
|
715
849
|
</div>
|
|
716
850
|
</div>
|
|
717
851
|
</Section>
|
|
@@ -747,7 +881,9 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
|
|
|
747
881
|
/>
|
|
748
882
|
|
|
749
883
|
<div className="border-t border-[var(--border)] pt-3 mt-3">
|
|
750
|
-
<p className="text-xs font-medium text-[var(--muted-foreground)] mb-2">
|
|
884
|
+
<p className="text-xs font-medium text-[var(--muted-foreground)] mb-2">
|
|
885
|
+
Twitter / X Overrides
|
|
886
|
+
</p>
|
|
751
887
|
<div className="space-y-3">
|
|
752
888
|
<InputField
|
|
753
889
|
label="Twitter Title"
|
|
@@ -774,11 +910,17 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
|
|
|
774
910
|
|
|
775
911
|
{/* Social preview card */}
|
|
776
912
|
<div className="mt-3">
|
|
777
|
-
<p className="text-xs font-medium text-[var(--muted-foreground)] mb-2">
|
|
913
|
+
<p className="text-xs font-medium text-[var(--muted-foreground)] mb-2">
|
|
914
|
+
Social Preview
|
|
915
|
+
</p>
|
|
778
916
|
<div className="rounded-lg border border-[var(--border)] overflow-hidden bg-[var(--muted)]">
|
|
779
917
|
{seoData.ogImage ? (
|
|
780
918
|
<div className="aspect-video bg-[var(--muted)] flex items-center justify-center overflow-hidden">
|
|
781
|
-
<img
|
|
919
|
+
<img
|
|
920
|
+
src={seoData.ogImage}
|
|
921
|
+
alt="OG preview"
|
|
922
|
+
className="w-full h-full object-cover"
|
|
923
|
+
/>
|
|
782
924
|
</div>
|
|
783
925
|
) : (
|
|
784
926
|
<div className="aspect-video bg-[var(--muted)] flex items-center justify-center text-[var(--muted-foreground)] text-sm">
|
|
@@ -786,11 +928,15 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
|
|
|
786
928
|
</div>
|
|
787
929
|
)}
|
|
788
930
|
<div className="p-3">
|
|
789
|
-
<div className="text-sm font-medium text-[var(--foreground)] line-clamp-1">
|
|
931
|
+
<div className="text-sm font-medium text-[var(--foreground)] line-clamp-1">
|
|
932
|
+
{displayTitle}
|
|
933
|
+
</div>
|
|
790
934
|
<div className="text-xs text-[var(--muted-foreground)] mt-1 line-clamp-2">
|
|
791
935
|
{displayDesc.slice(0, 100)}
|
|
792
936
|
</div>
|
|
793
|
-
<div className="text-xs text-[var(--muted-foreground)] mt-1 truncate">
|
|
937
|
+
<div className="text-xs text-[var(--muted-foreground)] mt-1 truncate">
|
|
938
|
+
{siteUrl}
|
|
939
|
+
</div>
|
|
794
940
|
</div>
|
|
795
941
|
</div>
|
|
796
942
|
</div>
|
|
@@ -819,14 +965,18 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
|
|
|
819
965
|
</div>
|
|
820
966
|
)}
|
|
821
967
|
<div>
|
|
822
|
-
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
|
|
968
|
+
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
|
|
969
|
+
Schema Type
|
|
970
|
+
</label>
|
|
823
971
|
<select
|
|
824
972
|
value={seoData.schemaType ?? 'Article'}
|
|
825
973
|
onChange={(e) => update({ schemaType: e.target.value })}
|
|
826
974
|
className="w-full px-3 py-1.5 text-sm border border-[var(--border)] rounded-lg bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
|
|
827
975
|
>
|
|
828
976
|
{SCHEMA_TYPES.map((t) => (
|
|
829
|
-
<option key={t} value={t}>
|
|
977
|
+
<option key={t} value={t}>
|
|
978
|
+
{t}
|
|
979
|
+
</option>
|
|
830
980
|
))}
|
|
831
981
|
</select>
|
|
832
982
|
</div>
|
|
@@ -834,5 +984,5 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
|
|
|
834
984
|
</Section>
|
|
835
985
|
</div>
|
|
836
986
|
</div>
|
|
837
|
-
)
|
|
987
|
+
)
|
|
838
988
|
}
|