@actuate-media/cms-admin 0.74.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 +12 -0
- 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/seo-content-pane.render.test.js +2 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/EditorSeoAside.d.ts +7 -4
- package/dist/components/EditorSeoAside.d.ts.map +1 -1
- package/dist/components/EditorSeoAside.js +7 -16
- package/dist/components/EditorSeoAside.js.map +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 +131 -31
- 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/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/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/seo-service.d.ts +2 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +5 -0
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +3 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +5 -4
- 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 +13 -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/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/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +14 -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/package.json +2 -2
- package/src/__tests__/views/post-fields-modal.render.test.tsx +3 -9
- package/src/__tests__/views/seo-content-pane.render.test.tsx +2 -0
- package/src/components/EditorSeoAside.tsx +27 -108
- 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 +497 -244
- package/src/components/seo/SeoInsightsMetrics.tsx +109 -0
- package/src/components/seo/SeoLinkingSection.tsx +151 -0
- package/src/lib/editor-seo-content.ts +144 -0
- package/src/lib/seo-service.ts +7 -0
- package/src/views/page-editor/EditorTopBar.tsx +7 -4
- package/src/views/page-editor/PageSectionEditor.tsx +15 -0
- package/src/views/page-editor/PageSettingsModal.tsx +1 -45
- package/src/views/post-editor/PostFieldsModal.tsx +19 -138
- package/src/views/post-editor/PostSectionEditor.tsx +17 -0
- package/src/views/seo/ContentTab.tsx +11 -5
|
@@ -1,20 +1,23 @@
|
|
|
1
|
+
import type { PageSection } from '@actuate-media/cms-core/page-sections';
|
|
1
2
|
export interface EditorSeoAsideProps {
|
|
2
3
|
entityType: 'page' | 'post';
|
|
3
|
-
/** Saved document id — when absent,
|
|
4
|
+
/** Saved document id — when absent, draft mode with live analysis. */
|
|
4
5
|
entityId: string | null;
|
|
5
6
|
title: string;
|
|
7
|
+
slug: string;
|
|
6
8
|
seoTitle: string;
|
|
7
9
|
seoDescription: string;
|
|
10
|
+
/** Post body HTML and/or section content for live SEO analysis. */
|
|
11
|
+
sections?: PageSection[];
|
|
12
|
+
bodyHtml?: string;
|
|
8
13
|
canEdit: boolean;
|
|
9
14
|
onClose: () => void;
|
|
10
|
-
/** Sync basic meta fields on the in-memory editor model (draft flow). */
|
|
11
15
|
onDraftChange?: (patch: {
|
|
12
16
|
seoTitle?: string;
|
|
13
17
|
seoDescription?: string;
|
|
14
18
|
}) => void;
|
|
15
|
-
/** Refetch the editor document after SEO API save (saved flow). */
|
|
16
19
|
onSaved?: () => void;
|
|
17
20
|
}
|
|
18
21
|
/** Right-side SEO panel for the Page / Post section editors. */
|
|
19
|
-
export declare function EditorSeoAside({ entityType, entityId,
|
|
22
|
+
export declare function EditorSeoAside({ entityType, entityId, slug, seoTitle, seoDescription, sections, bodyHtml, onClose, onDraftChange, onSaved, }: EditorSeoAsideProps): import("react/jsx-runtime").JSX.Element;
|
|
20
23
|
//# sourceMappingURL=EditorSeoAside.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorSeoAside.d.ts","sourceRoot":"","sources":["../../src/components/EditorSeoAside.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EditorSeoAside.d.ts","sourceRoot":"","sources":["../../src/components/EditorSeoAside.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAA;AAIxE,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IAC/E,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,EAC7B,UAAU,EACV,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,cAAc,EACd,QAAa,EACb,QAAa,EACb,OAAO,EACP,aAAa,EACb,OAAO,GACR,EAAE,mBAAmB,2CA4BrB"}
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx
|
|
3
|
-
import { useEffect } from 'react';
|
|
4
|
-
import { Search, X } from 'lucide-react';
|
|
5
|
-
import { SeoCharCount } from './seo/SeoCharCount.js';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo } from 'react';
|
|
6
4
|
import { SeoEditorPane } from './seo/SeoEditorPane.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
const SEO_TITLE_MAX = 60;
|
|
10
|
-
const SEO_DESC_MIN = 120;
|
|
11
|
-
const SEO_DESC_MAX = 160;
|
|
5
|
+
import { ResizableAside } from './ui/ResizableAside.js';
|
|
6
|
+
import { buildEditorContentHtml } from '../lib/editor-seo-content.js';
|
|
12
7
|
const INSPECTOR_SHELL = 'bg-card border-border flex h-full flex-col border-l';
|
|
13
|
-
const LABEL = 'text-foreground mb-1 block text-xs font-medium';
|
|
14
|
-
const INPUT = 'border-input bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none';
|
|
15
|
-
function DraftSeoForm({ title, seoTitle, seoDescription, canEdit, onDraftChange, }) {
|
|
16
|
-
return (_jsxs("div", { className: "flex-1 space-y-4 overflow-y-auto p-4", children: [_jsx("p", { className: "border-border bg-muted text-muted-foreground rounded-md border p-4 text-sm", children: "Save a draft first to unlock the full SEO editor \u2014 focus keyword, social previews, schema, robots, and AI assists. You can still set a basic title and description below; they are saved with the document." }), _jsxs("div", { children: [_jsxs("div", { className: "mb-1 flex items-center justify-between", children: [_jsx("label", { className: `${LABEL} mb-0`, htmlFor: "editor-seo-title", children: "SEO title" }), _jsx(SeoCharCount, { value: seoTitle, min: SEO_TITLE_MIN, max: SEO_TITLE_MAX })] }), _jsx("input", { id: "editor-seo-title", className: INPUT, value: seoTitle, disabled: !canEdit, placeholder: title || 'Defaults to the page title', onChange: (e) => onDraftChange?.({ seoTitle: e.target.value }) })] }), _jsxs("div", { children: [_jsxs("div", { className: "mb-1 flex items-center justify-between", children: [_jsx("label", { className: `${LABEL} mb-0`, htmlFor: "editor-seo-desc", children: "SEO description" }), _jsx(SeoCharCount, { value: seoDescription, min: SEO_DESC_MIN, max: SEO_DESC_MAX })] }), _jsx("textarea", { id: "editor-seo-desc", className: `${INPUT} min-h-[96px] resize-y`, value: seoDescription, disabled: !canEdit, placeholder: "Summarize this content for search results (120\u2013160 characters)", onChange: (e) => onDraftChange?.({ seoDescription: e.target.value }) })] })] }));
|
|
17
|
-
}
|
|
18
8
|
/** Right-side SEO panel for the Page / Post section editors. */
|
|
19
|
-
export function EditorSeoAside({ entityType, entityId,
|
|
9
|
+
export function EditorSeoAside({ entityType, entityId, slug, seoTitle, seoDescription, sections = [], bodyHtml = '', onClose, onDraftChange, onSaved, }) {
|
|
10
|
+
const contentHtml = useMemo(() => buildEditorContentHtml(sections, bodyHtml), [sections, bodyHtml]);
|
|
20
11
|
useEffect(() => {
|
|
21
12
|
function onKey(e) {
|
|
22
13
|
if (e.key === 'Escape')
|
|
@@ -25,6 +16,6 @@ export function EditorSeoAside({ entityType, entityId, title, seoTitle, seoDescr
|
|
|
25
16
|
document.addEventListener('keydown', onKey);
|
|
26
17
|
return () => document.removeEventListener('keydown', onKey);
|
|
27
18
|
}, [onClose]);
|
|
28
|
-
return (_jsx(ResizableAside, { role: "dialog", "aria-label": "SEO settings", className: INSPECTOR_SHELL, children:
|
|
19
|
+
return (_jsx(ResizableAside, { role: "dialog", "aria-label": "SEO settings", className: INSPECTOR_SHELL, children: _jsx(SeoEditorPane, { entityType: entityType, entityId: entityId, slug: slug, contentHtml: contentHtml, draftMeta: { seoTitle, seoDescription }, onDraftChange: onDraftChange, onClose: onClose, onSaved: onSaved }) }));
|
|
29
20
|
}
|
|
30
21
|
//# sourceMappingURL=EditorSeoAside.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorSeoAside.js","sourceRoot":"","sources":["../../src/components/EditorSeoAside.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"EditorSeoAside.js","sourceRoot":"","sources":["../../src/components/EditorSeoAside.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAA;AAGrE,MAAM,eAAe,GAAG,qDAAqD,CAAA;AAmB7E,gEAAgE;AAChE,MAAM,UAAU,cAAc,CAAC,EAC7B,UAAU,EACV,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,cAAc,EACd,QAAQ,GAAG,EAAE,EACb,QAAQ,GAAG,EAAE,EACb,OAAO,EACP,aAAa,EACb,OAAO,GACa;IACpB,MAAM,WAAW,GAAG,OAAO,CACzB,GAAG,EAAE,CAAC,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAChD,CAAC,QAAQ,EAAE,QAAQ,CAAC,CACrB,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,KAAK,CAAC,CAAgB;YAC7B,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ;gBAAE,OAAO,EAAE,CAAA;QACnC,CAAC;QACD,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QAC3C,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;IAC7D,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IAEb,OAAO,CACL,KAAC,cAAc,IAAC,IAAI,EAAC,QAAQ,gBAAY,cAAc,EAAC,SAAS,EAAE,eAAe,YAChF,KAAC,aAAa,IACZ,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,EACvC,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,OAAO,GAChB,GACa,CAClB,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Compact color-coded score for the page/post editor toolbar (`77/100` left of "SEO"). */
|
|
2
|
+
export declare function EditorSeoScoreBadge({ score, inverted, }: {
|
|
3
|
+
score: number;
|
|
4
|
+
/** When the SEO toggle uses `bg-primary`, render score in primary-foreground. */
|
|
5
|
+
inverted?: boolean;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=EditorSeoScoreBadge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditorSeoScoreBadge.d.ts","sourceRoot":"","sources":["../../../src/components/seo/EditorSeoScoreBadge.tsx"],"names":[],"mappings":"AAIA,2FAA2F;AAC3F,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAgB,GACjB,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,2CAaA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { gradeFromScore, gradeMeta } from './primitives.js';
|
|
4
|
+
/** Compact color-coded score for the page/post editor toolbar (`77/100` left of "SEO"). */
|
|
5
|
+
export function EditorSeoScoreBadge({ score, inverted = false, }) {
|
|
6
|
+
const meta = gradeMeta(gradeFromScore(score));
|
|
7
|
+
return (_jsxs("span", { className: `inline-flex items-baseline gap-0.5 text-xs font-medium tabular-nums ${inverted ? 'text-primary-foreground' : meta.text}`, "aria-hidden": true, children: [score, _jsx("span", { className: inverted ? 'text-primary-foreground/80' : 'opacity-70', children: "/100" })] }));
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=EditorSeoScoreBadge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditorSeoScoreBadge.js","sourceRoot":"","sources":["../../../src/components/seo/EditorSeoScoreBadge.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3D,2FAA2F;AAC3F,MAAM,UAAU,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,GAAG,KAAK,GAKjB;IACC,MAAM,IAAI,GAAG,SAAS,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IAC7C,OAAO,CACL,gBACE,SAAS,EAAE,uEACT,QAAQ,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,IAAI,CAAC,IAC9C,EAAE,kCAGD,KAAK,EACN,eAAM,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC,YAAY,qBAAa,IAC/E,CACR,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export declare function SeoAccordion({ children, defaultOpen, }: {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
defaultOpen?: string[];
|
|
5
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function SeoAccordionItem({ value, title, badge, children, }: {
|
|
7
|
+
value: string;
|
|
8
|
+
title: string;
|
|
9
|
+
badge?: ReactNode;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export declare function SeoAccordionBadge({ tone, children, }: {
|
|
13
|
+
tone: 'neutral' | 'good' | 'warn' | 'bad';
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=SeoAccordion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeoAccordion.d.ts","sourceRoot":"","sources":["../../../src/components/seo/SeoAccordion.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,WAAoD,GACrD,EAAE;IACD,QAAQ,EAAE,SAAS,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB,2CAUA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,QAAQ,EAAE,SAAS,CAAA;CACpB,2CAqBA;AAED,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;IACzC,QAAQ,EAAE,SAAS,CAAA;CACpB,2CAYA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as Accordion from '@radix-ui/react-accordion';
|
|
4
|
+
import { ChevronDown } from 'lucide-react';
|
|
5
|
+
export function SeoAccordion({ children, defaultOpen = ['focus-keyword', 'search-appearance'], }) {
|
|
6
|
+
return (_jsx(Accordion.Root, { type: "multiple", defaultValue: defaultOpen, className: "divide-y divide-[var(--bdr)]", children: children }));
|
|
7
|
+
}
|
|
8
|
+
export function SeoAccordionItem({ value, title, badge, children, }) {
|
|
9
|
+
return (_jsxs(Accordion.Item, { value: value, className: "border-[var(--bdr)] bg-[var(--sb)]", children: [_jsx(Accordion.Header, { children: _jsxs(Accordion.Trigger, { className: "group flex w-full items-center justify-between gap-2 px-5 py-3.5 text-left transition-colors duration-[var(--motion-fast)] hover:bg-[var(--bg)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "text-[13px] font-medium text-[var(--txt)]", children: title }), badge] }), _jsx(ChevronDown, { size: 16, "aria-hidden": true, className: "shrink-0 text-[var(--muted)] group-data-[state=open]:rotate-180 motion-safe:transition-transform motion-safe:duration-200" })] }) }), _jsx(Accordion.Content, { className: "overflow-hidden data-[state=closed]:hidden", children: _jsx("div", { className: "space-y-3 px-5 pt-1 pb-4", children: children }) })] }));
|
|
10
|
+
}
|
|
11
|
+
export function SeoAccordionBadge({ tone, children, }) {
|
|
12
|
+
const cls = tone === 'good'
|
|
13
|
+
? 'bg-[var(--suc-l)] text-[var(--suc)]'
|
|
14
|
+
: tone === 'warn'
|
|
15
|
+
? 'bg-[var(--warn-l)] text-[var(--warn)]'
|
|
16
|
+
: tone === 'bad'
|
|
17
|
+
? 'bg-[var(--err-l)] text-[var(--err)]'
|
|
18
|
+
: 'bg-[var(--bg)] text-[var(--sub)]';
|
|
19
|
+
return (_jsx("span", { className: `rounded-full px-2 py-0.5 text-[10.5px] font-medium ${cls}`, children: children }));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=SeoAccordion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeoAccordion.js","sourceRoot":"","sources":["../../../src/components/seo/SeoAccordion.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAGZ,OAAO,KAAK,SAAS,MAAM,2BAA2B,CAAA;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,UAAU,YAAY,CAAC,EAC3B,QAAQ,EACR,WAAW,GAAG,CAAC,eAAe,EAAE,mBAAmB,CAAC,GAIrD;IACC,OAAO,CACL,KAAC,SAAS,CAAC,IAAI,IACb,IAAI,EAAC,UAAU,EACf,YAAY,EAAE,WAAW,EACzB,SAAS,EAAC,8BAA8B,YAEvC,QAAQ,GACM,CAClB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,GAMT;IACC,OAAO,CACL,MAAC,SAAS,CAAC,IAAI,IAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAC,oCAAoC,aAC1E,KAAC,SAAS,CAAC,MAAM,cACf,MAAC,SAAS,CAAC,OAAO,IAAC,SAAS,EAAC,kOAAkO,aAC7P,gBAAM,SAAS,EAAC,iCAAiC,aAC/C,eAAM,SAAS,EAAC,2CAA2C,YAAE,KAAK,GAAQ,EACzE,KAAK,IACD,EACP,KAAC,WAAW,IACV,IAAI,EAAE,EAAE,uBAER,SAAS,EAAC,2HAA2H,GACrI,IACgB,GACH,EACnB,KAAC,SAAS,CAAC,OAAO,IAAC,SAAS,EAAC,4CAA4C,YACvE,cAAK,SAAS,EAAC,0BAA0B,YAAE,QAAQ,GAAO,GACxC,IACL,CAClB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAChC,IAAI,EACJ,QAAQ,GAIT;IACC,MAAM,GAAG,GACP,IAAI,KAAK,MAAM;QACb,CAAC,CAAC,qCAAqC;QACvC,CAAC,CAAC,IAAI,KAAK,MAAM;YACf,CAAC,CAAC,uCAAuC;YACzC,CAAC,CAAC,IAAI,KAAK,KAAK;gBACd,CAAC,CAAC,qCAAqC;gBACvC,CAAC,CAAC,kCAAkC,CAAA;IAC5C,OAAO,CACL,eAAM,SAAS,EAAE,sDAAsD,GAAG,EAAE,YAAG,QAAQ,GAAQ,CAChG,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SEOCheck } from '@actuate-media/cms-core/seo/analysis';
|
|
2
|
+
export declare function SeoAnalysisList({ checks, onAiFixField, aiFixField, }: {
|
|
3
|
+
checks: SEOCheck[];
|
|
4
|
+
onAiFixField?: (field: 'metaTitle' | 'metaDescription' | 'focusKeyword') => void;
|
|
5
|
+
aiFixField?: string | null;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=SeoAnalysisList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeoAnalysisList.d.ts","sourceRoot":"","sources":["../../../src/components/seo/SeoAnalysisList.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sCAAsC,CAAA;AAEpE,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,YAAY,EACZ,UAAU,GACX,EAAE;IACD,MAAM,EAAE,QAAQ,EAAE,CAAA;IAClB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,GAAG,iBAAiB,GAAG,cAAc,KAAK,IAAI,CAAA;IAChF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B,2CA6CA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AlertCircle, CheckCircle2, Sparkles, Wand2 } from 'lucide-react';
|
|
4
|
+
export function SeoAnalysisList({ checks, onAiFixField, aiFixField, }) {
|
|
5
|
+
const problems = checks.filter((c) => c.status === 'problem' || c.status === 'improvement');
|
|
6
|
+
const good = checks.filter((c) => c.status === 'good');
|
|
7
|
+
if (checks.length === 0) {
|
|
8
|
+
return (_jsx("p", { className: "text-[12px] text-[var(--muted)]", children: "Add content and a focus keyphrase to run SEO checks." }));
|
|
9
|
+
}
|
|
10
|
+
return (_jsxs("div", { className: "space-y-4", children: [problems.length > 0 && (_jsxs("div", { children: [_jsxs("h4", { className: "mb-2 text-[11px] font-semibold tracking-wide text-[var(--sub)] uppercase", children: ["Problems (", problems.length, ")"] }), _jsx("ul", { className: "space-y-2", children: problems.map((check) => (_jsx(AnalysisRow, { check: check, onAiFix: onAiFixField, aiBusy: aiFixField }, check.id))) })] })), good.length > 0 && (_jsxs("div", { children: [_jsxs("h4", { className: "mb-2 text-[11px] font-semibold tracking-wide text-[var(--sub)] uppercase", children: ["Good results (", good.length, ")"] }), _jsx("ul", { className: "space-y-2", children: good.map((check) => (_jsx(AnalysisRow, { check: check }, check.id))) })] }))] }));
|
|
11
|
+
}
|
|
12
|
+
function AnalysisRow({ check, onAiFix, aiBusy, }) {
|
|
13
|
+
const tone = check.status === 'good'
|
|
14
|
+
? 'text-[var(--suc)]'
|
|
15
|
+
: check.status === 'improvement'
|
|
16
|
+
? 'text-[var(--warn)]'
|
|
17
|
+
: 'text-[var(--err)]';
|
|
18
|
+
const Icon = check.status === 'good' ? CheckCircle2 : AlertCircle;
|
|
19
|
+
const aiField = aiFieldForCheck(check.id);
|
|
20
|
+
return (_jsxs("li", { className: "flex items-start gap-2 rounded-[7px] border border-[var(--bdr)] bg-[var(--card)] px-2.5 py-2", children: [_jsx(Icon, { className: `mt-0.5 h-3.5 w-3.5 shrink-0 ${tone}`, "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-[12.5px] font-medium text-[var(--txt)]", children: check.title }), _jsx("p", { className: "text-[11.5px] text-[var(--sub)]", children: check.description })] }), onAiFix && aiField && check.status !== 'good' && (_jsxs("button", { type: "button", "aria-label": `AI help for ${check.title}`, disabled: Boolean(aiBusy), onClick: () => onAiFix(aiField), className: "inline-flex shrink-0 items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium text-[var(--acc)] hover:bg-[var(--acc-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:opacity-60", children: [aiBusy === aiField ? (_jsx(Sparkles, { className: "h-3 w-3 animate-pulse", "aria-hidden": true })) : (_jsx(Wand2, { className: "h-3 w-3", "aria-hidden": true })), "AI"] }))] }));
|
|
21
|
+
}
|
|
22
|
+
function aiFieldForCheck(checkId) {
|
|
23
|
+
if (checkId.includes('meta-title') || checkId.includes('keyphrase-in-title'))
|
|
24
|
+
return 'metaTitle';
|
|
25
|
+
if (checkId.includes('meta-description') || checkId.includes('keyphrase-in-meta-description')) {
|
|
26
|
+
return 'metaDescription';
|
|
27
|
+
}
|
|
28
|
+
if (checkId.includes('keyphrase'))
|
|
29
|
+
return 'focusKeyword';
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=SeoAnalysisList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeoAnalysisList.js","sourceRoot":"","sources":["../../../src/components/seo/SeoAnalysisList.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
export interface SeoEditorPaneProps {
|
|
2
|
-
entityType:
|
|
3
|
-
|
|
2
|
+
entityType: 'page' | 'post';
|
|
3
|
+
/** When absent, draft-only mode — meta fields sync via `onDraftChange`. */
|
|
4
|
+
entityId: string | null;
|
|
4
5
|
onClose: () => void;
|
|
5
|
-
/** Called after a successful save so the list can refetch. */
|
|
6
6
|
onSaved?: () => void;
|
|
7
|
+
/** Live HTML from the section editor for real-time analysis. */
|
|
8
|
+
contentHtml?: string;
|
|
9
|
+
slug?: string;
|
|
10
|
+
draftMeta?: {
|
|
11
|
+
seoTitle: string;
|
|
12
|
+
seoDescription: string;
|
|
13
|
+
};
|
|
14
|
+
onDraftChange?: (patch: {
|
|
15
|
+
seoTitle?: string;
|
|
16
|
+
seoDescription?: string;
|
|
17
|
+
}) => void;
|
|
7
18
|
}
|
|
8
|
-
export declare function SeoEditorPane({ entityType, entityId, onClose, onSaved }: SeoEditorPaneProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export declare function SeoEditorPane({ entityType, entityId, onClose, onSaved, contentHtml, slug, draftMeta, onDraftChange, }: SeoEditorPaneProps): import("react/jsx-runtime").JSX.Element;
|
|
9
20
|
//# sourceMappingURL=SeoEditorPane.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SeoEditorPane.d.ts","sourceRoot":"","sources":["../../../src/components/seo/SeoEditorPane.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SeoEditorPane.d.ts","sourceRoot":"","sources":["../../../src/components/seo/SeoEditorPane.tsx"],"names":[],"mappings":"AAmIA,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3B,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAA;IACxD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CAChF;AAED,wBAAgB,aAAa,CAAC,EAC5B,UAAU,EACV,QAAQ,EACR,OAAO,EACP,OAAO,EACP,WAAgB,EAChB,IAAS,EACT,SAAS,EACT,aAAa,GACd,EAAE,kBAAkB,2CAqpBpB"}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
|
-
* Inline SEO editor pane
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* assists, and `updateSeoFields` save path as the drawer; only the
|
|
8
|
-
* presentation pattern changed.
|
|
4
|
+
* Inline SEO editor pane for the post/page section editors and the Content SEO
|
|
5
|
+
* list. Accordion sections mirror Yoast-style workflow: Focus Keyword → Search
|
|
6
|
+
* Appearance → Social → Analysis → Advanced → Insights → Linking.
|
|
9
7
|
*/
|
|
10
8
|
import { cloneElement, isValidElement, useEffect, useId, useMemo, useState, } from 'react';
|
|
11
|
-
import {
|
|
9
|
+
import { Loader2, Monitor, Smartphone, Sparkles, Wand2 } from 'lucide-react';
|
|
12
10
|
import { toast } from 'sonner';
|
|
13
11
|
import { resolveRenderedMetaTitle } from '@actuate-media/cms-core/client';
|
|
14
|
-
import {
|
|
12
|
+
import { analyzeContent, calculateReadability, stripHtmlTags, } from '@actuate-media/cms-core/seo/analysis';
|
|
13
|
+
import { fetchSeoContentRecord, updateSeoFields, generateSeoField, fetchDocumentSeoAnalysis, SEO_SCHEMA_TYPES, } from '../../lib/seo-service.js';
|
|
15
14
|
import { fixBrandVoiceContent } from '../../lib/brand-voice-client.js';
|
|
16
15
|
import { BrandVoiceAlignmentBadge } from '../BrandVoiceAlignmentBadge.js';
|
|
17
|
-
import { InspectorPaneBody, InspectorPaneFooter, InspectorPaneHeader
|
|
16
|
+
import { InspectorPaneBody, InspectorPaneFooter, InspectorPaneHeader } from '../ui/InspectorPane.js';
|
|
18
17
|
import { Toggle } from '../ui/Toggle.js';
|
|
19
18
|
import { SeoLoading, SeoErrorState, gradeMeta } from './primitives.js';
|
|
19
|
+
import { SeoAccordion, SeoAccordionBadge, SeoAccordionItem } from './SeoAccordion.js';
|
|
20
|
+
import { SeoAnalysisList } from './SeoAnalysisList.js';
|
|
21
|
+
import { SeoInsightsMetrics } from './SeoInsightsMetrics.js';
|
|
22
|
+
import { SeoLinkingSection } from './SeoLinkingSection.js';
|
|
20
23
|
const META_TITLE_MAX = 60;
|
|
21
24
|
const META_DESC_MAX = 160;
|
|
22
25
|
const KEY_TAKEAWAY_MAX = 240;
|
|
@@ -31,31 +34,54 @@ function CharCount({ value, max }) {
|
|
|
31
34
|
const over = value.length > max;
|
|
32
35
|
return (_jsxs("span", { className: `text-[11px] ${over ? 'text-[var(--err)]' : 'text-[var(--muted)]'}`, children: [value.length, "/", max] }));
|
|
33
36
|
}
|
|
34
|
-
function PaneField({ label, children, hint, }) {
|
|
35
|
-
// Associate the visible label with its control for screen readers. We inject
|
|
36
|
-
// a generated id onto the single child input/textarea (unless it already has
|
|
37
|
-
// one) and point the <label htmlFor> at it.
|
|
37
|
+
function PaneField({ label, children, hint, help, }) {
|
|
38
38
|
const generatedId = useId();
|
|
39
39
|
let control = children;
|
|
40
40
|
if (isValidElement(children) && children.props.id == null) {
|
|
41
41
|
control = cloneElement(children, { id: generatedId });
|
|
42
42
|
}
|
|
43
43
|
const controlId = isValidElement(control) ? control.props.id : undefined;
|
|
44
|
-
return (_jsxs("div", { className: "mb-3 last:mb-0", children: [_jsxs("div", { className: "mb-1 flex items-center justify-between gap-2", children: [_jsx("label", { htmlFor: controlId, className: "text-[11.5px] font-medium text-[var(--sub)]", children: label }), hint] }), control] }));
|
|
44
|
+
return (_jsxs("div", { className: "mb-3 last:mb-0", children: [_jsxs("div", { className: "mb-1 flex items-center justify-between gap-2", children: [_jsx("label", { htmlFor: controlId, className: "text-[11.5px] font-medium text-[var(--sub)]", children: label }), hint] }), control, help && _jsx("p", { className: "mt-1 text-[11px] text-[var(--muted)]", children: help })] }));
|
|
45
45
|
}
|
|
46
46
|
function PaneToggleRow({ label, hint, checked, onCheckedChange, }) {
|
|
47
47
|
return (_jsxs("div", { className: "flex items-center justify-between gap-3 py-1.5", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-[12.5px] font-medium text-[var(--txt)]", children: label }), _jsx("div", { className: "text-[11px] text-[var(--muted)]", children: hint })] }), _jsx(Toggle, { checked: checked, onCheckedChange: onCheckedChange, "aria-label": label })] }));
|
|
48
48
|
}
|
|
49
|
-
export function SeoEditorPane({ entityType, entityId, onClose, onSaved }) {
|
|
49
|
+
export function SeoEditorPane({ entityType, entityId, onClose, onSaved, contentHtml = '', slug = '', draftMeta, onDraftChange, }) {
|
|
50
|
+
const isDraft = !entityId;
|
|
50
51
|
const [record, setRecord] = useState(null);
|
|
51
|
-
const [loading, setLoading] = useState(
|
|
52
|
+
const [loading, setLoading] = useState(!isDraft);
|
|
52
53
|
const [error, setError] = useState(null);
|
|
53
54
|
const [saving, setSaving] = useState(false);
|
|
54
55
|
const [aiBusy, setAiBusy] = useState(null);
|
|
55
56
|
const [aiFixBusy, setAiFixBusy] = useState(null);
|
|
56
57
|
const [aiAlignments, setAiAlignments] = useState({});
|
|
57
58
|
const [form, setForm] = useState({});
|
|
59
|
+
const [previewDevice, setPreviewDevice] = useState('mobile');
|
|
60
|
+
const [socialPlatform, setSocialPlatform] = useState('facebook');
|
|
61
|
+
const [remoteAnalysis, setRemoteAnalysis] = useState(null);
|
|
58
62
|
useEffect(() => {
|
|
63
|
+
if (isDraft) {
|
|
64
|
+
setLoading(false);
|
|
65
|
+
setRecord(null);
|
|
66
|
+
setError(null);
|
|
67
|
+
setForm({
|
|
68
|
+
metaTitle: draftMeta?.seoTitle ?? '',
|
|
69
|
+
metaDescription: draftMeta?.seoDescription ?? '',
|
|
70
|
+
focusKeyword: '',
|
|
71
|
+
keyTakeaway: '',
|
|
72
|
+
ogTitle: '',
|
|
73
|
+
ogDescription: '',
|
|
74
|
+
canonicalUrl: '',
|
|
75
|
+
structuredDataType: 'WebPage',
|
|
76
|
+
noindex: false,
|
|
77
|
+
nofollow: false,
|
|
78
|
+
llmsInclude: 'AUTO',
|
|
79
|
+
llmsDescription: '',
|
|
80
|
+
llmsSection: '',
|
|
81
|
+
llmsOptional: false,
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
59
85
|
let alive = true;
|
|
60
86
|
setLoading(true);
|
|
61
87
|
setError(null);
|
|
@@ -76,6 +102,8 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }) {
|
|
|
76
102
|
keyTakeaway: rec.keyTakeaway ?? '',
|
|
77
103
|
ogTitle: rec.ogTitle ?? '',
|
|
78
104
|
ogDescription: rec.ogDescription ?? '',
|
|
105
|
+
twitterTitle: rec.twitterTitle ?? '',
|
|
106
|
+
twitterDescription: rec.twitterDescription ?? '',
|
|
79
107
|
structuredDataType: rec.structuredDataType ?? 'WebPage',
|
|
80
108
|
noindex: rec.noindex,
|
|
81
109
|
nofollow: rec.nofollow,
|
|
@@ -90,12 +118,42 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }) {
|
|
|
90
118
|
return () => {
|
|
91
119
|
alive = false;
|
|
92
120
|
};
|
|
93
|
-
}, [entityType, entityId]);
|
|
94
|
-
|
|
121
|
+
}, [entityType, entityId, isDraft]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (isDraft || contentHtml.trim() || !entityId)
|
|
124
|
+
return;
|
|
125
|
+
let alive = true;
|
|
126
|
+
fetchDocumentSeoAnalysis(entityId)
|
|
127
|
+
.then((result) => alive && setRemoteAnalysis(result))
|
|
128
|
+
.catch(() => alive && setRemoteAnalysis(null));
|
|
129
|
+
return () => {
|
|
130
|
+
alive = false;
|
|
131
|
+
};
|
|
132
|
+
}, [entityId, isDraft, contentHtml]);
|
|
133
|
+
const update = (patch) => {
|
|
134
|
+
setForm((f) => {
|
|
135
|
+
const next = { ...f, ...patch };
|
|
136
|
+
if (isDraft && onDraftChange) {
|
|
137
|
+
if ('metaTitle' in patch)
|
|
138
|
+
onDraftChange({ seoTitle: patch.metaTitle });
|
|
139
|
+
if ('metaDescription' in patch)
|
|
140
|
+
onDraftChange({ seoDescription: patch.metaDescription });
|
|
141
|
+
}
|
|
142
|
+
return next;
|
|
143
|
+
});
|
|
144
|
+
};
|
|
95
145
|
async function handleAiGenerate(field) {
|
|
146
|
+
if (isDraft && !contentHtml.trim() && field !== 'focusKeyword') {
|
|
147
|
+
toast.error('Add a title or body content before generating with AI.');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
96
150
|
setAiBusy(field);
|
|
97
151
|
try {
|
|
98
|
-
|
|
152
|
+
if (isDraft) {
|
|
153
|
+
toast.error('Save a draft first to use AI field generation with site knowledge.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const result = await generateSeoField(field, { entityType, entityId: entityId });
|
|
99
157
|
if (result.unavailable) {
|
|
100
158
|
toast.error('AI is not configured. Add an AI provider to enable suggestions.');
|
|
101
159
|
return;
|
|
@@ -112,7 +170,7 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }) {
|
|
|
112
170
|
setAiAlignments((prev) => ({ ...prev, [targetField]: result.brandAlignment }));
|
|
113
171
|
}
|
|
114
172
|
toast.success(field === 'ogTitle' || field === 'ogDescription'
|
|
115
|
-
? 'AI suggestion applied to meta tags —
|
|
173
|
+
? 'AI suggestion applied to meta tags — social fields inherit unless overridden.'
|
|
116
174
|
: 'AI suggestion applied — review before saving.');
|
|
117
175
|
}
|
|
118
176
|
else {
|
|
@@ -156,6 +214,11 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }) {
|
|
|
156
214
|
return (_jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [_jsx(BrandVoiceAlignmentBadge, { alignment: alignment, compact: true }), alignment.score < 85 && (_jsxs("button", { type: "button", onClick: () => void handleAiFix(field), disabled: fixing || aiBusy !== null, className: "inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[11.5px] font-medium text-[var(--acc)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--acc-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:opacity-60", children: [fixing ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": true })) : (_jsx(Wand2, { className: "h-3.5 w-3.5", "aria-hidden": true })), "Fix for brand voice"] }))] }));
|
|
157
215
|
}
|
|
158
216
|
async function handleSave() {
|
|
217
|
+
if (isDraft) {
|
|
218
|
+
toast.success('SEO fields will be saved with the document.');
|
|
219
|
+
onClose();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
159
222
|
setSaving(true);
|
|
160
223
|
const res = await updateSeoFields(entityType, entityId, form);
|
|
161
224
|
setSaving(false);
|
|
@@ -167,6 +230,8 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }) {
|
|
|
167
230
|
onSaved?.();
|
|
168
231
|
}
|
|
169
232
|
const renderedTitle = useMemo(() => {
|
|
233
|
+
if (isDraft)
|
|
234
|
+
return form.metaTitle?.trim() || draftMeta?.seoTitle || 'Untitled';
|
|
170
235
|
if (!record)
|
|
171
236
|
return form.metaTitle?.trim() || 'Untitled';
|
|
172
237
|
return resolveRenderedMetaTitle({
|
|
@@ -177,19 +242,54 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }) {
|
|
|
177
242
|
titleTemplate: record.titleTemplate ?? undefined,
|
|
178
243
|
},
|
|
179
244
|
});
|
|
180
|
-
}, [form.metaTitle, record]);
|
|
181
|
-
const desc = form.metaDescription ||
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
245
|
+
}, [form.metaTitle, record, isDraft, draftMeta?.seoTitle]);
|
|
246
|
+
const desc = form.metaDescription ||
|
|
247
|
+
'Please provide a meta description. Search engines may generate one from your content.';
|
|
248
|
+
const url = record?.url ?? (slug ? `/${slug.replace(/^\//, '')}` : '');
|
|
249
|
+
const displayUrl = record?.url ?? url;
|
|
250
|
+
const analysisInput = useMemo(() => ({
|
|
251
|
+
title: record?.title ?? draftMeta?.seoTitle ?? '',
|
|
252
|
+
slug: slug || record?.url?.split('/').filter(Boolean).pop() || '',
|
|
253
|
+
content: contentHtml,
|
|
254
|
+
metaTitle: form.metaTitle,
|
|
255
|
+
metaDescription: form.metaDescription,
|
|
256
|
+
focusKeyphrase: form.focusKeyword,
|
|
257
|
+
canonical: form.canonicalUrl,
|
|
258
|
+
ogTitle: form.ogTitle,
|
|
259
|
+
ogDescription: form.ogDescription,
|
|
260
|
+
ogImage: record?.ogImage ?? undefined,
|
|
261
|
+
}), [record, draftMeta, slug, contentHtml, form]);
|
|
262
|
+
const liveAnalysis = useMemo(() => {
|
|
263
|
+
if (contentHtml.trim())
|
|
264
|
+
return analyzeContent(analysisInput);
|
|
265
|
+
return null;
|
|
266
|
+
}, [analysisInput, contentHtml]);
|
|
267
|
+
const analysis = liveAnalysis ?? remoteAnalysis;
|
|
268
|
+
const plainText = stripHtmlTags(contentHtml);
|
|
269
|
+
const readability = useMemo(() => calculateReadability(plainText), [plainText]);
|
|
270
|
+
const problemCount = analysis?.checks.filter((c) => c.status === 'problem').length ?? 0;
|
|
271
|
+
const internalLinks = analysis?.checks.find((c) => c.id === 'internal-links')?.description.match(/\d+/)?.[0] ?? '0';
|
|
272
|
+
const externalLinks = analysis?.checks.find((c) => c.id === 'outbound-links')?.description.match(/\d+/)?.[0] ?? '0';
|
|
273
|
+
const internalOk = Number(internalLinks) >= 1;
|
|
274
|
+
const externalOk = Number(externalLinks) >= 1;
|
|
275
|
+
function aiButton(field, label) {
|
|
276
|
+
return (_jsxs("button", { type: "button", onClick: () => void handleAiGenerate(field), disabled: aiBusy !== null || isDraft, title: isDraft ? 'Save a draft first' : label, className: "inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium text-[var(--acc)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--acc-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:opacity-60", children: [_jsx(Sparkles, { className: `h-3 w-3 ${aiBusy === field ? 'animate-pulse' : ''}`, "aria-hidden": true }), aiBusy === field ? 'Generating…' : 'AI Generate'] }));
|
|
185
277
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
278
|
+
const socialTitle = socialPlatform === 'twitter'
|
|
279
|
+
? form.twitterTitle || form.ogTitle || renderedTitle
|
|
280
|
+
: form.ogTitle || renderedTitle;
|
|
281
|
+
const socialDesc = socialPlatform === 'twitter'
|
|
282
|
+
? form.twitterDescription || form.ogDescription || form.metaDescription || desc
|
|
283
|
+
: form.ogDescription || form.metaDescription || desc;
|
|
284
|
+
return (_jsxs(_Fragment, { children: [_jsxs(InspectorPaneHeader, { onClose: onClose, closeLabel: "Close SEO editor", children: [_jsx("div", { className: "truncate text-[13.5px] font-semibold text-[var(--txt)]", children: record?.title ?? draftMeta?.seoTitle ?? 'SEO' }), displayUrl && (_jsx("div", { className: "mt-0.5 truncate text-[11.5px] text-[var(--muted)]", children: displayUrl }))] }), _jsx(InspectorPaneBody, { children: loading ? (_jsx("div", { className: "px-5 py-4", children: _jsx(SeoLoading, {}) })) : error ? (_jsx("div", { className: "px-5 py-4", children: _jsx(SeoErrorState, { message: error }) })) : (_jsxs(_Fragment, { children: [isDraft && (_jsx("div", { className: "border-b border-[var(--bdr)] px-5 py-3", children: _jsx("p", { className: "text-[11.5px] text-[var(--sub)]", children: "Save a draft to unlock AI assists trained on your site knowledge and tone. You can still set focus keyword, search appearance, and review live analysis below." }) })), !isDraft && record && (_jsx("div", { className: "border-b border-[var(--bdr)] px-5 py-3", children: (() => {
|
|
191
285
|
const meta = gradeMeta(record.seoGrade);
|
|
192
|
-
return (
|
|
193
|
-
})() })] })) :
|
|
286
|
+
return (_jsx("div", { className: `flex items-center gap-2 rounded-[7px] px-3 py-2 text-[12px] ${meta.bg} ${meta.text}`, children: _jsxs("span", { children: ["SEO score: ", _jsx("span", { className: "font-medium", children: record.seoScore }), " (", meta.label, ")", analysis && ` · Live analysis ${analysis.score}`] }) }));
|
|
287
|
+
})() })), _jsxs(SeoAccordion, { children: [_jsxs(SeoAccordionItem, { value: "focus-keyword", title: "Focus keyword", badge: form.focusKeyword ? (_jsx(SeoAccordionBadge, { tone: "neutral", children: form.focusKeyword })) : (_jsx(SeoAccordionBadge, { tone: "warn", children: "Not set" })), children: [_jsx(PaneField, { label: "Focus keyphrase", hint: aiButton('focusKeyword', 'Suggest focus keyphrase'), help: "The main phrase you want this content found for in search and AI answers.", children: _jsx("input", { className: inputCls, value: form.focusKeyword ?? '', onChange: (e) => update({ focusKeyword: e.target.value }), placeholder: "e.g. modern headless cms" }) }), _jsx(PaneField, { label: "Key takeaway", hint: _jsxs("span", { className: "flex items-center gap-2", children: [_jsx(CharCount, { value: form.keyTakeaway ?? '', max: KEY_TAKEAWAY_MAX }), aiButton('keyTakeaway')] }), help: "One or two sentences AI answer engines can quote \u2014 what is this page about?", children: _jsx("textarea", { className: `${inputCls} min-h-16 resize-y`, value: form.keyTakeaway ?? '', onChange: (e) => update({ keyTakeaway: e.target.value }), placeholder: "Summarize the page's core value proposition" }) })] }), _jsxs(SeoAccordionItem, { value: "search-appearance", title: "Search appearance", children: [_jsx("p", { className: "text-[11.5px] text-[var(--sub)]", children: "Determine how this content should look in search results." }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[11.5px] font-medium text-[var(--sub)]", children: "Google preview" }), _jsxs("div", { className: "flex rounded-[7px] border border-[var(--bdr)] p-0.5", children: [_jsxs("button", { type: "button", "aria-pressed": previewDevice === 'mobile', onClick: () => setPreviewDevice('mobile'), className: `inline-flex items-center gap-1 rounded-[5px] px-2 py-1 text-[11px] ${previewDevice === 'mobile' ? 'bg-[var(--acc-l)] text-[var(--acc)]' : 'text-[var(--muted)]'}`, children: [_jsx(Smartphone, { size: 14, "aria-hidden": true }), "Mobile"] }), _jsxs("button", { type: "button", "aria-pressed": previewDevice === 'desktop', onClick: () => setPreviewDevice('desktop'), className: `inline-flex items-center gap-1 rounded-[5px] px-2 py-1 text-[11px] ${previewDevice === 'desktop' ? 'bg-[var(--acc-l)] text-[var(--acc)]' : 'text-[var(--muted)]'}`, children: [_jsx(Monitor, { size: 14, "aria-hidden": true }), "Desktop"] })] })] }), _jsxs("div", { className: `rounded-[7px] border border-[var(--bdr)] bg-[var(--card)] p-3 ${previewDevice === 'mobile' ? 'max-w-[320px]' : ''}`, children: [_jsx("p", { className: "truncate text-[11px] text-[var(--suc)]", children: displayUrl }), _jsx("p", { className: "text-[15px] leading-snug text-[var(--acc)]", children: renderedTitle }), _jsx("p", { className: "line-clamp-2 text-[12px] text-[var(--sub)]", children: desc })] }), _jsx(PaneField, { label: "SEO title", hint: _jsxs("span", { className: "flex items-center gap-2", children: [_jsx(CharCount, { value: form.metaTitle ?? '', max: META_TITLE_MAX }), aiButton('metaTitle')] }), children: _jsx("input", { className: inputCls, value: form.metaTitle ?? '', onChange: (e) => update({ metaTitle: e.target.value }), placeholder: "Page-specific title (%page% in your site template)" }) }), alignmentRow('metaTitle'), _jsx(PaneField, { label: "Slug", help: "URL slug for this content \u2014 edit in page/post settings.", children: _jsx("input", { className: `${inputCls} opacity-80`, value: slug, readOnly: true, disabled: true }) }), _jsx(PaneField, { label: "Meta description", hint: _jsxs("span", { className: "flex items-center gap-2", children: [_jsx(CharCount, { value: form.metaDescription ?? '', max: META_DESC_MAX }), aiButton('metaDescription')] }), children: _jsx("textarea", { className: `${inputCls} min-h-20 resize-y`, value: form.metaDescription ?? '', onChange: (e) => update({ metaDescription: e.target.value }), placeholder: "Short summary shown under the title in search results" }) }), alignmentRow('metaDescription')] }), _jsxs(SeoAccordionItem, { value: "social-appearance", title: "Social appearance", children: [_jsx("p", { className: "text-[11.5px] text-[var(--sub)]", children: "Customize title, description, and image used when sharing on social platforms." }), _jsx("div", { className: "flex rounded-[7px] border border-[var(--bdr)] p-0.5", children: ['facebook', 'twitter'].map((platform) => (_jsx("button", { type: "button", "aria-pressed": socialPlatform === platform, onClick: () => setSocialPlatform(platform), className: `flex-1 rounded-[5px] px-2 py-1.5 text-[11.5px] font-medium capitalize ${socialPlatform === platform ? 'bg-[var(--acc-l)] text-[var(--acc)]' : 'text-[var(--sub)]'}`, children: platform === 'twitter' ? 'X / Twitter' : 'Facebook' }, platform))) }), _jsxs("div", { className: "overflow-hidden rounded-[7px] border border-[var(--bdr)]", children: [record?.ogImage ? (
|
|
288
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
289
|
+
_jsx("img", { src: record.ogImage, alt: "", className: "aspect-[1200/630] w-full bg-[var(--bg)] object-cover" })) : (_jsx("div", { className: "flex aspect-[1200/630] w-full items-center justify-center bg-[var(--bg)] text-[12px] text-[var(--muted)]", children: "No social image \u2014 add a featured or OG image" })), _jsxs("div", { className: "p-2.5", children: [_jsx("p", { className: "truncate text-[10.5px] text-[var(--muted)] uppercase", children: displayUrl.replace(/^https?:\/\//, '').split('/')[0] || 'yoursite.com' }), _jsx("p", { className: "truncate text-[12.5px] font-medium text-[var(--txt)]", children: socialTitle }), _jsx("p", { className: "line-clamp-2 text-[12px] text-[var(--sub)]", children: socialDesc })] })] }), socialPlatform === 'facebook' ? (_jsxs(_Fragment, { children: [_jsx(PaneField, { label: "OG title", hint: aiButton('ogTitle'), children: _jsx("input", { className: inputCls, value: form.ogTitle ?? '', onChange: (e) => update({ ogTitle: e.target.value }), placeholder: form.metaTitle || 'Inherits meta title when empty' }) }), _jsx(PaneField, { label: "OG description", hint: aiButton('ogDescription'), children: _jsx("textarea", { className: `${inputCls} min-h-16 resize-y`, value: form.ogDescription ?? '', onChange: (e) => update({ ogDescription: e.target.value }), placeholder: form.metaDescription || 'Inherits meta description when empty' }) })] })) : (_jsxs(_Fragment, { children: [_jsx(PaneField, { label: "Twitter title", children: _jsx("input", { className: inputCls, value: form.twitterTitle ?? '', onChange: (e) => update({ twitterTitle: e.target.value }), placeholder: form.ogTitle || form.metaTitle || 'Inherits OG/meta title' }) }), _jsx(PaneField, { label: "Twitter description", children: _jsx("textarea", { className: `${inputCls} min-h-16 resize-y`, value: form.twitterDescription ?? '', onChange: (e) => update({ twitterDescription: e.target.value }), placeholder: form.ogDescription ||
|
|
290
|
+
form.metaDescription ||
|
|
291
|
+
'Inherits OG/meta description' }) })] })), alignmentRow('ogDescription')] }), _jsx(SeoAccordionItem, { value: "seo-analysis", title: "SEO analysis", badge: analysis ? (_jsx(SeoAccordionBadge, { tone: problemCount > 0 ? 'bad' : 'good', children: problemCount > 0 ? `${problemCount} issues` : 'Looking good' })) : undefined, children: _jsx(SeoAnalysisList, { checks: analysis?.checks ?? [], onAiFixField: isDraft ? undefined : (field) => void handleAiGenerate(field), aiFixField: aiBusy }) }), _jsxs(SeoAccordionItem, { value: "advanced", title: "Advanced", children: [_jsx(PaneField, { label: "Canonical URL", children: _jsx("input", { className: inputCls, value: form.canonicalUrl ?? '', onChange: (e) => update({ canonicalUrl: e.target.value }), placeholder: displayUrl || 'https://example.com/page' }) }), !form.canonicalUrl?.trim() && displayUrl && !isDraft && (_jsxs("p", { className: "mb-3 text-[11px] text-[var(--muted)]", children: ["Auto-canonical: ", record?.canonicalUrl ?? displayUrl] })), _jsx(PaneField, { label: "Schema type", children: _jsx("select", { className: inputCls, value: form.structuredDataType ?? 'WebPage', onChange: (e) => update({ structuredDataType: e.target.value }), children: SEO_SCHEMA_TYPES.map((t) => (_jsx("option", { value: t, children: t }, t))) }) }), _jsx(PaneToggleRow, { label: "Allow search engines to show this content?", hint: "When off, adds a noindex directive", checked: !form.noindex, onCheckedChange: (v) => update({ noindex: !v }) }), _jsx(PaneToggleRow, { label: "Should search engines follow links?", hint: "When off, adds a nofollow directive", checked: !form.nofollow, onCheckedChange: (v) => update({ nofollow: !v }) }), _jsxs("div", { className: "mt-2 border-t border-[var(--bdr)] pt-3", children: [_jsx("p", { className: "mb-2 text-[11px] font-medium text-[var(--sub)]", children: "AI discovery (llms.txt)" }), _jsx(PaneField, { label: "Include in llms.txt", children: _jsxs("select", { className: inputCls, value: form.llmsInclude ?? 'AUTO', onChange: (e) => update({ llmsInclude: e.target.value }), children: [_jsx("option", { value: "AUTO", children: "Auto \u2014 include when eligible" }), _jsx("option", { value: "ALWAYS", children: "Always \u2014 pin it in" }), _jsx("option", { value: "NEVER", children: "Never \u2014 exclude" })] }) }), _jsx(PaneField, { label: "Agent description", hint: _jsx(CharCount, { value: form.llmsDescription ?? '', max: LLMS_DESC_MAX }), children: _jsx("textarea", { className: `${inputCls} min-h-16 resize-y`, value: form.llmsDescription ?? '', onChange: (e) => update({ llmsDescription: e.target.value }), placeholder: form.keyTakeaway ||
|
|
292
|
+
form.metaDescription ||
|
|
293
|
+
'One line for an AI agent — what this page covers' }) })] })] }), _jsx(SeoAccordionItem, { value: "insights", title: "Insights", children: _jsx(SeoInsightsMetrics, { readability: readability, plainText: plainText }) }), _jsx(SeoAccordionItem, { value: "linking-data", title: "Linking data", children: _jsx(SeoLinkingSection, { entityType: entityType, entityId: entityId, internalLinks: Number(internalLinks), externalLinks: Number(externalLinks), internalLinkOk: internalOk, externalLinkOk: externalOk }) })] })] })) }), _jsx(InspectorPaneFooter, { children: _jsx("button", { type: "button", onClick: () => void handleSave(), disabled: saving || loading || !!error, className: "bg-primary text-primary-foreground w-full rounded-[7px] px-3 py-2 text-[13px] font-semibold transition-colors duration-[var(--motion-fast)] hover:opacity-90 focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", children: saving ? 'Saving…' : isDraft ? 'Done' : 'Save Changes' }) })] }));
|
|
194
294
|
}
|
|
195
295
|
//# sourceMappingURL=SeoEditorPane.js.map
|