@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/__tests__/views/post-fields-modal.render.test.js +3 -6
  3. package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
  4. package/dist/__tests__/views/seo-content-pane.render.test.js +2 -0
  5. package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -1
  6. package/dist/actuate-admin.css +1 -1
  7. package/dist/components/EditorSeoAside.d.ts +7 -4
  8. package/dist/components/EditorSeoAside.d.ts.map +1 -1
  9. package/dist/components/EditorSeoAside.js +7 -16
  10. package/dist/components/EditorSeoAside.js.map +1 -1
  11. package/dist/components/seo/EditorSeoScoreBadge.d.ts +7 -0
  12. package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -0
  13. package/dist/components/seo/EditorSeoScoreBadge.js +9 -0
  14. package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -0
  15. package/dist/components/seo/SeoAccordion.d.ts +16 -0
  16. package/dist/components/seo/SeoAccordion.d.ts.map +1 -0
  17. package/dist/components/seo/SeoAccordion.js +21 -0
  18. package/dist/components/seo/SeoAccordion.js.map +1 -0
  19. package/dist/components/seo/SeoAnalysisList.d.ts +7 -0
  20. package/dist/components/seo/SeoAnalysisList.d.ts.map +1 -0
  21. package/dist/components/seo/SeoAnalysisList.js +32 -0
  22. package/dist/components/seo/SeoAnalysisList.js.map +1 -0
  23. package/dist/components/seo/SeoEditorPane.d.ts +15 -4
  24. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
  25. package/dist/components/seo/SeoEditorPane.js +131 -31
  26. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  27. package/dist/components/seo/SeoInsightsMetrics.d.ts +6 -0
  28. package/dist/components/seo/SeoInsightsMetrics.d.ts.map +1 -0
  29. package/dist/components/seo/SeoInsightsMetrics.js +16 -0
  30. package/dist/components/seo/SeoInsightsMetrics.js.map +1 -0
  31. package/dist/components/seo/SeoLinkingSection.d.ts +9 -0
  32. package/dist/components/seo/SeoLinkingSection.d.ts.map +1 -0
  33. package/dist/components/seo/SeoLinkingSection.js +44 -0
  34. package/dist/components/seo/SeoLinkingSection.js.map +1 -0
  35. package/dist/lib/editor-seo-content.d.ts +16 -0
  36. package/dist/lib/editor-seo-content.d.ts.map +1 -0
  37. package/dist/lib/editor-seo-content.js +139 -0
  38. package/dist/lib/editor-seo-content.js.map +1 -0
  39. package/dist/lib/seo-service.d.ts +2 -0
  40. package/dist/lib/seo-service.d.ts.map +1 -1
  41. package/dist/lib/seo-service.js +5 -0
  42. package/dist/lib/seo-service.js.map +1 -1
  43. package/dist/views/page-editor/EditorTopBar.d.ts +3 -1
  44. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  45. package/dist/views/page-editor/EditorTopBar.js +5 -4
  46. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  47. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  48. package/dist/views/page-editor/PageSectionEditor.js +13 -1
  49. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  50. package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
  51. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
  52. package/dist/views/page-editor/PageSettingsModal.js +2 -13
  53. package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
  54. package/dist/views/post-editor/PostFieldsModal.d.ts +1 -1
  55. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
  56. package/dist/views/post-editor/PostFieldsModal.js +17 -79
  57. package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
  58. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  59. package/dist/views/post-editor/PostSectionEditor.js +14 -1
  60. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  61. package/dist/views/seo/ContentTab.d.ts.map +1 -1
  62. package/dist/views/seo/ContentTab.js +7 -1
  63. package/dist/views/seo/ContentTab.js.map +1 -1
  64. package/package.json +2 -2
  65. package/src/__tests__/views/post-fields-modal.render.test.tsx +3 -9
  66. package/src/__tests__/views/seo-content-pane.render.test.tsx +2 -0
  67. package/src/components/EditorSeoAside.tsx +27 -108
  68. package/src/components/seo/EditorSeoScoreBadge.tsx +26 -0
  69. package/src/components/seo/SeoAccordion.tsx +76 -0
  70. package/src/components/seo/SeoAnalysisList.tsx +113 -0
  71. package/src/components/seo/SeoEditorPane.tsx +497 -244
  72. package/src/components/seo/SeoInsightsMetrics.tsx +109 -0
  73. package/src/components/seo/SeoLinkingSection.tsx +151 -0
  74. package/src/lib/editor-seo-content.ts +144 -0
  75. package/src/lib/seo-service.ts +7 -0
  76. package/src/views/page-editor/EditorTopBar.tsx +7 -4
  77. package/src/views/page-editor/PageSectionEditor.tsx +15 -0
  78. package/src/views/page-editor/PageSettingsModal.tsx +1 -45
  79. package/src/views/post-editor/PostFieldsModal.tsx +19 -138
  80. package/src/views/post-editor/PostSectionEditor.tsx +17 -0
  81. 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, shows a draft-only meta form. */
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, title, seoTitle, seoDescription, canEdit, onClose, onDraftChange, onSaved, }: EditorSeoAsideProps): import("react/jsx-runtime").JSX.Element;
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":"AAkBA,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3B,qEAAqE;IACrE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,yEAAyE;IACzE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IAC/E,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAuDD,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,EAC7B,UAAU,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,cAAc,EACd,OAAO,EACP,OAAO,EACP,aAAa,EACb,OAAO,GACR,EAAE,mBAAmB,2CAmDrB"}
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, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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 { InspectorExpandButton, ResizableAside } from './ui/ResizableAside.js';
8
- const SEO_TITLE_MIN = 30;
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, title, seoTitle, seoDescription, canEdit, onClose, onDraftChange, onSaved, }) {
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: entityId ? (_jsx(SeoEditorPane, { entityType: entityType, entityId: entityId, onClose: onClose, onSaved: onSaved })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "border-border flex items-center justify-between border-b px-6 py-4", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Search, { className: "text-muted-foreground h-4 w-4 shrink-0", "aria-hidden": true }), _jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-foreground truncate text-sm font-medium", children: "SEO" }), _jsx("p", { className: "text-muted-foreground truncate text-xs", children: title || 'Untitled' })] })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [_jsx(InspectorExpandButton, {}), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close SEO panel", className: "text-muted-foreground hover:text-foreground hover:bg-accent flex h-9 w-9 items-center justify-center rounded-md md:h-7 md:w-7", children: _jsx(X, { className: "h-4 w-4", "aria-hidden": true }) })] })] }), _jsx(DraftSeoForm, { title: title, seoTitle: seoTitle, seoDescription: seoDescription, canEdit: canEdit, onDraftChange: onDraftChange })] })) }));
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":"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"}
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: string;
3
- entityId: string;
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":"AA8HA,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED,wBAAgB,aAAa,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,kBAAkB,2CA2a3F"}
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 design-parity replacement for the old
5
- * `SeoEditorDrawer` overlay. Rendered inside an `<InspectorPane>` beside the
6
- * Content SEO list (380px in the handoff). Same fields, validation, AI
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 { Sparkles, Check, Loader2, Wand2 } from 'lucide-react';
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 { fetchSeoContentRecord, updateSeoFields, generateSeoField, SEO_SCHEMA_TYPES, } from '../../lib/seo-service.js';
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, InspectorPaneSection, } from '../ui/InspectorPane.js';
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(true);
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
- const update = (patch) => setForm((f) => ({ ...f, ...patch }));
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
- const result = await generateSeoField(field, { entityType, entityId });
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 — OG inherits from meta unless you override.'
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 || 'No meta description set. Search engines may generate one.';
182
- const url = record?.url ?? '';
183
- function aiButton(field) {
184
- return (_jsxs("button", { type: "button", onClick: () => void handleAiGenerate(field), disabled: aiBusy !== null, 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'] }));
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
- 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 ?? 'Edit SEO' }), url && _jsx("div", { className: "mt-0.5 truncate text-[11.5px] text-[var(--muted)]", children: url })] }), _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 }) })) : record ? (_jsxs(_Fragment, { children: [_jsx(InspectorPaneSection, { title: "Google Preview", children: _jsxs("div", { className: "rounded-[7px] border border-[var(--bdr)] bg-[var(--card)] p-3", children: [_jsx("p", { className: "truncate text-[11.5px] text-[var(--suc)]", children: url }), _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 })] }) }), _jsxs(InspectorPaneSection, { title: "Meta Tags", children: [_jsx(PaneField, { label: "Meta 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)" }) }), _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(InspectorPaneSection, { title: "Focus Keyword", children: [_jsx(PaneField, { label: "Target keyword", hint: aiButton('focusKeyword'), children: _jsx("input", { className: inputCls, value: form.focusKeyword ?? '', onChange: (e) => update({ focusKeyword: e.target.value }), placeholder: "Primary keyword this page targets" }) }), _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')] }), children: _jsx("textarea", { className: `${inputCls} min-h-16 resize-y`, value: form.keyTakeaway ?? '', onChange: (e) => update({ keyTakeaway: e.target.value }), placeholder: "One or two sentences AI answer engines can quote \u2014 what is this page about?" }) })] }), _jsxs(InspectorPaneSection, { title: "Open Graph", subtitle: "Social sharing", 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 || 'Title when shared on social media' }) }), _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 || 'Description when shared on social media' }) }), alignmentRow('ogDescription'), _jsxs("div", { className: "mt-3 overflow-hidden rounded-[7px] border border-[var(--bdr)]", children: [record.ogImage ? (
187
- // eslint-disable-next-line @next/next/no-img-element
188
- _jsx("img", { src: record.ogImage, alt: `Social preview for ${record.title}`, 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 OG image set" })), _jsxs("div", { className: "p-2.5", children: [_jsx("p", { className: "truncate text-[10.5px] text-[var(--muted)] uppercase", children: url.replace(/^https?:\/\//, '').split('/')[0] }), _jsx("p", { className: "truncate text-[12.5px] font-medium text-[var(--txt)]", children: form.ogTitle || renderedTitle }), _jsx("p", { className: "line-clamp-1 text-[12px] text-[var(--sub)]", children: form.ogDescription || form.metaDescription || desc })] })] })] }), _jsxs(InspectorPaneSection, { title: "Technical SEO", children: [_jsx(PaneField, { label: "Canonical URL", children: _jsx("input", { className: inputCls, value: form.canonicalUrl ?? '', onChange: (e) => update({ canonicalUrl: e.target.value }), placeholder: url || 'https://example.com/page' }) }), !form.canonicalUrl?.trim() && url ? (_jsxs("p", { className: "mb-3 text-[11px] text-[var(--muted)]", children: ["Auto-canonical: ", record.canonicalUrl ?? url] })) : null, _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: "No-index", hint: "Hide this page from search engines", checked: !!form.noindex, onCheckedChange: (v) => update({ noindex: v }) }), _jsx(PaneToggleRow, { label: "No-follow", hint: "Tell crawlers not to follow links on this page", checked: !!form.nofollow, onCheckedChange: (v) => update({ nofollow: v }) })] }), _jsxs(InspectorPaneSection, { title: "AI Discovery", subtitle: "llms.txt", children: [_jsxs("p", { className: "mb-3 text-[11px] text-[var(--muted)]", children: ["Controls how this page appears in the curated ", _jsx("code", { children: "llms.txt" }), " guide that AI agents read. It does not affect search ranking."] }), _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 ||
189
- form.metaDescription ||
190
- 'One line for an AI agent — what this page covers and why it matters.' }) }), _jsx(PaneField, { label: "Section", children: _jsx("input", { className: inputCls, value: form.llmsSection ?? '', onChange: (e) => update({ llmsSection: e.target.value }), placeholder: "Defaults to the content type" }) }), _jsx(PaneToggleRow, { label: "Optional", hint: "Group under \u201COptional\u201D \u2014 agents skip these first when context is tight", checked: !!form.llmsOptional, onCheckedChange: (v) => update({ llmsOptional: v }) })] }), _jsx(InspectorPaneSection, { children: (() => {
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 (_jsxs("div", { className: `flex items-center gap-2 rounded-[7px] px-3 py-2 text-[12.5px] ${meta.bg} ${meta.text}`, children: [_jsx(Check, { className: "h-4 w-4 shrink-0", "aria-hidden": true }), _jsxs("span", { children: ["Current SEO score: ", _jsx("span", { className: "font-medium", children: record.seoScore }), " (", meta.label, ")"] })] }));
193
- })() })] })) : null }), _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…' : 'Save Changes' }) })] }));
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