@actuate-media/cms-admin 0.19.0 → 0.21.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 (129) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +13 -9
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/layout/sidebar-posts.test.js +6 -4
  5. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -1
  6. package/dist/__tests__/lib/seo-service-errors.test.d.ts +2 -0
  7. package/dist/__tests__/lib/seo-service-errors.test.d.ts.map +1 -0
  8. package/dist/__tests__/lib/seo-service-errors.test.js +46 -0
  9. package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -0
  10. package/dist/__tests__/views/use-seo-resource.test.d.ts +2 -0
  11. package/dist/__tests__/views/use-seo-resource.test.d.ts.map +1 -0
  12. package/dist/__tests__/views/use-seo-resource.test.js +56 -0
  13. package/dist/__tests__/views/use-seo-resource.test.js.map +1 -0
  14. package/dist/actuate-admin.css +1 -1
  15. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  16. package/dist/components/Breadcrumbs.js +4 -1
  17. package/dist/components/Breadcrumbs.js.map +1 -1
  18. package/dist/components/seo/Drawer.d.ts +11 -0
  19. package/dist/components/seo/Drawer.d.ts.map +1 -0
  20. package/dist/components/seo/Drawer.js +17 -0
  21. package/dist/components/seo/Drawer.js.map +1 -0
  22. package/dist/components/seo/RedirectEditorDrawer.d.ts +9 -0
  23. package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -0
  24. package/dist/components/seo/RedirectEditorDrawer.js +94 -0
  25. package/dist/components/seo/RedirectEditorDrawer.js.map +1 -0
  26. package/dist/components/seo/SeoCopilotDrawer.d.ts +10 -0
  27. package/dist/components/seo/SeoCopilotDrawer.d.ts.map +1 -0
  28. package/dist/components/seo/SeoCopilotDrawer.js +34 -0
  29. package/dist/components/seo/SeoCopilotDrawer.js.map +1 -0
  30. package/dist/components/seo/SeoEditorDrawer.d.ts +8 -0
  31. package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -0
  32. package/dist/components/seo/SeoEditorDrawer.js +120 -0
  33. package/dist/components/seo/SeoEditorDrawer.js.map +1 -0
  34. package/dist/components/seo/SeoIssueFixDrawer.d.ts +8 -0
  35. package/dist/components/seo/SeoIssueFixDrawer.d.ts.map +1 -0
  36. package/dist/components/seo/SeoIssueFixDrawer.js +111 -0
  37. package/dist/components/seo/SeoIssueFixDrawer.js.map +1 -0
  38. package/dist/components/seo/primitives.d.ts +53 -0
  39. package/dist/components/seo/primitives.d.ts.map +1 -0
  40. package/dist/components/seo/primitives.js +101 -0
  41. package/dist/components/seo/primitives.js.map +1 -0
  42. package/dist/layout/Sidebar.d.ts.map +1 -1
  43. package/dist/layout/Sidebar.js +8 -5
  44. package/dist/layout/Sidebar.js.map +1 -1
  45. package/dist/lib/seo-service.d.ts +322 -0
  46. package/dist/lib/seo-service.d.ts.map +1 -0
  47. package/dist/lib/seo-service.js +331 -0
  48. package/dist/lib/seo-service.js.map +1 -0
  49. package/dist/router/index.d.ts.map +1 -1
  50. package/dist/router/index.js +4 -1
  51. package/dist/router/index.js.map +1 -1
  52. package/dist/views/ApiKeys.d.ts +4 -1
  53. package/dist/views/ApiKeys.d.ts.map +1 -1
  54. package/dist/views/ApiKeys.js +5 -3
  55. package/dist/views/ApiKeys.js.map +1 -1
  56. package/dist/views/Dashboard.js +2 -2
  57. package/dist/views/Dashboard.js.map +1 -1
  58. package/dist/views/MediaBrowser.d.ts.map +1 -1
  59. package/dist/views/MediaBrowser.js +36 -25
  60. package/dist/views/MediaBrowser.js.map +1 -1
  61. package/dist/views/Posts/NewPostTypeInstructions.js +2 -2
  62. package/dist/views/Posts/post-type-presets.d.ts +5 -0
  63. package/dist/views/Posts/post-type-presets.d.ts.map +1 -1
  64. package/dist/views/Posts/post-type-presets.js +10 -64
  65. package/dist/views/Posts/post-type-presets.js.map +1 -1
  66. package/dist/views/SEO.d.ts.map +1 -1
  67. package/dist/views/SEO.js +89 -132
  68. package/dist/views/SEO.js.map +1 -1
  69. package/dist/views/Settings.d.ts +4 -1
  70. package/dist/views/Settings.d.ts.map +1 -1
  71. package/dist/views/Settings.js +45 -6
  72. package/dist/views/Settings.js.map +1 -1
  73. package/dist/views/Users.d.ts +4 -1
  74. package/dist/views/Users.d.ts.map +1 -1
  75. package/dist/views/Users.js +5 -3
  76. package/dist/views/Users.js.map +1 -1
  77. package/dist/views/seo/AuditTab.d.ts +6 -0
  78. package/dist/views/seo/AuditTab.d.ts.map +1 -0
  79. package/dist/views/seo/AuditTab.js +57 -0
  80. package/dist/views/seo/AuditTab.js.map +1 -0
  81. package/dist/views/seo/ContentTab.d.ts +5 -0
  82. package/dist/views/seo/ContentTab.d.ts.map +1 -0
  83. package/dist/views/seo/ContentTab.js +27 -0
  84. package/dist/views/seo/ContentTab.js.map +1 -0
  85. package/dist/views/seo/OverviewTab.d.ts +8 -0
  86. package/dist/views/seo/OverviewTab.d.ts.map +1 -0
  87. package/dist/views/seo/OverviewTab.js +35 -0
  88. package/dist/views/seo/OverviewTab.js.map +1 -0
  89. package/dist/views/seo/RedirectsTab.d.ts +2 -0
  90. package/dist/views/seo/RedirectsTab.d.ts.map +1 -0
  91. package/dist/views/seo/RedirectsTab.js +130 -0
  92. package/dist/views/seo/RedirectsTab.js.map +1 -0
  93. package/dist/views/seo/TechnicalTab.d.ts +2 -0
  94. package/dist/views/seo/TechnicalTab.d.ts.map +1 -0
  95. package/dist/views/seo/TechnicalTab.js +105 -0
  96. package/dist/views/seo/TechnicalTab.js.map +1 -0
  97. package/dist/views/seo/useSeoResource.d.ts +8 -0
  98. package/dist/views/seo/useSeoResource.d.ts.map +1 -0
  99. package/dist/views/seo/useSeoResource.js +55 -0
  100. package/dist/views/seo/useSeoResource.js.map +1 -0
  101. package/package.json +2 -2
  102. package/src/AdminRoot.tsx +13 -9
  103. package/src/__tests__/layout/sidebar-posts.test.ts +6 -4
  104. package/src/__tests__/lib/seo-service-errors.test.ts +60 -0
  105. package/src/__tests__/views/use-seo-resource.test.tsx +68 -0
  106. package/src/components/Breadcrumbs.tsx +4 -1
  107. package/src/components/seo/Drawer.tsx +65 -0
  108. package/src/components/seo/RedirectEditorDrawer.tsx +235 -0
  109. package/src/components/seo/SeoCopilotDrawer.tsx +76 -0
  110. package/src/components/seo/SeoEditorDrawer.tsx +359 -0
  111. package/src/components/seo/SeoIssueFixDrawer.tsx +244 -0
  112. package/src/components/seo/primitives.tsx +240 -0
  113. package/src/layout/Sidebar.tsx +7 -10
  114. package/src/lib/seo-service.ts +660 -0
  115. package/src/router/index.ts +4 -1
  116. package/src/views/ApiKeys.tsx +10 -5
  117. package/src/views/Dashboard.tsx +2 -2
  118. package/src/views/MediaBrowser.tsx +89 -67
  119. package/src/views/Posts/NewPostTypeInstructions.tsx +2 -2
  120. package/src/views/Posts/post-type-presets.ts +15 -64
  121. package/src/views/SEO.tsx +186 -863
  122. package/src/views/Settings.tsx +93 -40
  123. package/src/views/Users.tsx +10 -4
  124. package/src/views/seo/AuditTab.tsx +229 -0
  125. package/src/views/seo/ContentTab.tsx +164 -0
  126. package/src/views/seo/OverviewTab.tsx +257 -0
  127. package/src/views/seo/RedirectsTab.tsx +398 -0
  128. package/src/views/seo/TechnicalTab.tsx +315 -0
  129. package/src/views/seo/useSeoResource.ts +66 -0
@@ -0,0 +1,130 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRef, useState } from 'react';
4
+ import { Plus, Pencil, Trash2, Upload, Download, Sparkles, ArrowRight, GitBranch, Check, X, Link2, } from 'lucide-react';
5
+ import { toast } from 'sonner';
6
+ import { fetchRedirectOverview, fetchRedirectRules, fetchRedirectChains, fetchRedirectSuggestions, deleteRedirectRule, flattenRedirectChain, generate404RecoverySuggestions, acceptRedirectSuggestion, dismissRedirectSuggestion, exportRedirectsCsv, importRedirectsCsv, } from '../../lib/seo-service.js';
7
+ import { SectionCard, SeoLoading, SeoEmptyState, SeoErrorState, SeoStatusBadge, btnPrimary, btnSecondary, btnGhostIcon, } from '../../components/seo/primitives.js';
8
+ import { RedirectEditorDrawer } from '../../components/seo/RedirectEditorDrawer.js';
9
+ import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js';
10
+ import { useSeoResource } from './useSeoResource.js';
11
+ function OverviewCards({ onRefetchKey }) {
12
+ const { data, loading, error, refetch } = useSeoResource(fetchRedirectOverview, [onRefetchKey]);
13
+ if (loading)
14
+ return _jsx(SeoLoading, { label: "Loading redirect overview\u2026" });
15
+ if (error)
16
+ return _jsx(SeoErrorState, { message: error, onRetry: refetch });
17
+ if (!data)
18
+ return null;
19
+ const cards = [
20
+ { label: 'Active redirects', value: data.activeRedirects },
21
+ { label: 'Stale redirects', value: data.staleRedirects },
22
+ { label: '404s tracked', value: data.notFoundCount },
23
+ { label: 'Chains & loops', value: data.chainsAndLoops },
24
+ ];
25
+ return (_jsx("div", { className: "grid grid-cols-2 gap-4 lg:grid-cols-4", children: cards.map((c) => (_jsxs("div", { className: "border-border bg-card rounded-lg border p-4", children: [_jsx("p", { className: "text-muted-foreground text-sm", children: c.label }), _jsx("p", { className: "text-foreground mt-1 text-2xl font-medium tabular-nums", children: c.value })] }, c.label))) }));
26
+ }
27
+ function Ai404RecoveryCard({ onApplied }) {
28
+ const { data, loading, error, refetch } = useSeoResource(fetchRedirectSuggestions, []);
29
+ const [generating, setGenerating] = useState(false);
30
+ async function handleGenerate() {
31
+ setGenerating(true);
32
+ const res = await generate404RecoverySuggestions();
33
+ setGenerating(false);
34
+ if (res.error) {
35
+ toast.error(res.error);
36
+ return;
37
+ }
38
+ toast.success('Generated 404 recovery suggestions.');
39
+ refetch();
40
+ }
41
+ async function handleAccept(id) {
42
+ const res = await acceptRedirectSuggestion(id);
43
+ if (res.error)
44
+ return toast.error(res.error);
45
+ toast.success('Redirect created from suggestion.');
46
+ refetch();
47
+ onApplied();
48
+ }
49
+ async function handleDismiss(id) {
50
+ const res = await dismissRedirectSuggestion(id);
51
+ if (res.error)
52
+ return toast.error(res.error);
53
+ refetch();
54
+ }
55
+ return (_jsx(SectionCard, { title: "AI 404 Recovery", description: "Suggested redirects for paths returning 404s", action: _jsxs("button", { className: btnSecondary, onClick: handleGenerate, disabled: generating, children: [_jsx(Sparkles, { className: `h-4 w-4 ${generating ? 'animate-pulse' : ''}`, "aria-hidden": true }), generating ? 'Analyzing…' : 'Generate suggestions'] }), children: loading ? (_jsx(SeoLoading, {})) : error ? (_jsx(SeoErrorState, { message: error, onRetry: refetch })) : (data ?? []).length === 0 ? (_jsx(SeoEmptyState, { icon: _jsx(Sparkles, { size: 24 }), title: "No suggestions", description: "Generate suggestions to recover traffic from broken URLs." })) : (_jsx("ul", { className: "divide-border divide-y", children: data.map((s) => (_jsxs("li", { className: "flex items-center justify-between gap-3 py-3 first:pt-0", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("p", { className: "text-foreground flex items-center gap-2 text-sm", children: [_jsx("span", { className: "truncate font-medium", children: s.fromPath }), _jsx(ArrowRight, { className: "text-muted-foreground h-3.5 w-3.5 shrink-0", "aria-hidden": true }), _jsx("span", { className: "text-muted-foreground truncate", children: s.toPath })] }), s.reason && _jsx("p", { className: "text-muted-foreground mt-0.5 text-sm", children: s.reason })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsx(SeoStatusBadge, { label: `${Math.round(s.confidence * 100)}%`, tone: s.confidence >= 0.7 ? 'good' : s.confidence >= 0.4 ? 'fair' : 'poor' }), _jsx("button", { onClick: () => handleAccept(s.id), className: "text-success hover:bg-accent focus-visible:ring-ring rounded-md p-1.5 focus-visible:ring-2 focus-visible:outline-none", "aria-label": `Accept redirect from ${s.fromPath}`, children: _jsx(Check, { className: "h-4 w-4", "aria-hidden": true }) }), _jsx("button", { onClick: () => handleDismiss(s.id), className: "text-muted-foreground hover:bg-accent focus-visible:ring-ring rounded-md p-1.5 focus-visible:ring-2 focus-visible:outline-none", "aria-label": `Dismiss suggestion for ${s.fromPath}`, children: _jsx(X, { className: "h-4 w-4", "aria-hidden": true }) })] })] }, s.id))) })) }));
56
+ }
57
+ function ChainMapCard({ onFlattened }) {
58
+ const { data, loading, error, refetch } = useSeoResource(fetchRedirectChains, []);
59
+ async function handleFlatten(id) {
60
+ const res = await flattenRedirectChain(id);
61
+ if (res.error)
62
+ return toast.error(res.error);
63
+ toast.success('Chain flattened.');
64
+ refetch();
65
+ onFlattened();
66
+ }
67
+ if (loading)
68
+ return null;
69
+ if (error)
70
+ return null;
71
+ if (!data || data.length === 0)
72
+ return null;
73
+ return (_jsx(SectionCard, { title: "Redirect Chains & Loops", description: "Multi-hop redirects slow crawlers and lose link equity", children: _jsx("ul", { className: "space-y-3", children: data.map((chain) => (_jsxs("li", { className: "border-border flex items-center justify-between gap-3 rounded-md border p-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "mb-1 flex items-center gap-2", children: [_jsx(GitBranch, { className: "text-muted-foreground h-4 w-4", "aria-hidden": true }), _jsx(SeoStatusBadge, { label: chain.kind === 'loop' ? 'Loop' : 'Chain', tone: chain.kind === 'loop' ? 'critical' : 'fair' })] }), _jsx("p", { className: "text-foreground flex flex-wrap items-center gap-1 text-sm", children: chain.hops.map((hop, i) => (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "truncate", children: hop }), i < chain.hops.length - 1 && (_jsx(ArrowRight, { className: "text-muted-foreground h-3 w-3", "aria-hidden": true }))] }, i))) })] }), chain.kind === 'chain' && (_jsx("button", { className: btnSecondary, onClick: () => handleFlatten(chain.id), children: "Flatten" }))] }, chain.id))) }) }));
74
+ }
75
+ export function RedirectsTab() {
76
+ const [refetchKey, setRefetchKey] = useState(0);
77
+ const { data, loading, error, refetch } = useSeoResource(fetchRedirectRules, [refetchKey]);
78
+ const [editor, setEditor] = useState(null);
79
+ const [confirmDelete, setConfirmDelete] = useState(null);
80
+ const fileRef = useRef(null);
81
+ const bump = () => setRefetchKey((k) => k + 1);
82
+ const refetchAll = () => {
83
+ refetch();
84
+ bump();
85
+ };
86
+ async function handleDelete(rule) {
87
+ const res = await deleteRedirectRule(rule.id);
88
+ if (res.error)
89
+ return toast.error(res.error);
90
+ toast.success('Redirect deleted.');
91
+ refetchAll();
92
+ }
93
+ async function handleExport() {
94
+ const res = await exportRedirectsCsv();
95
+ if (res.error || !res.csv)
96
+ return toast.error(res.error ?? 'Export failed.');
97
+ const blob = new Blob([res.csv], { type: 'text/csv' });
98
+ const url = URL.createObjectURL(blob);
99
+ const link = document.createElement('a');
100
+ link.href = url;
101
+ link.download = 'redirects.csv';
102
+ // Firefox requires the anchor to be in the document for programmatic clicks.
103
+ document.body.appendChild(link);
104
+ link.click();
105
+ document.body.removeChild(link);
106
+ // Defer revocation so the browser has a chance to start the download —
107
+ // revoking synchronously can cancel it before it begins.
108
+ setTimeout(() => URL.revokeObjectURL(url), 0);
109
+ }
110
+ async function handleImportFile(file) {
111
+ const csv = await file.text();
112
+ const res = await importRedirectsCsv(csv);
113
+ if (res.error)
114
+ return toast.error(res.error);
115
+ if (res.errors && res.errors.length > 0) {
116
+ toast.error(`Imported ${res.imported ?? 0}, ${res.errors.length} row(s) skipped.`);
117
+ }
118
+ else {
119
+ toast.success(`Imported ${res.imported ?? 0} redirect(s).`);
120
+ }
121
+ refetchAll();
122
+ }
123
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(OverviewCards, { onRefetchKey: refetchKey }), _jsxs("div", { className: "grid grid-cols-1 gap-4 xl:grid-cols-2", children: [_jsx(Ai404RecoveryCard, { onApplied: refetchAll }), _jsx(ChainMapCard, { onFlattened: refetchAll })] }), _jsx(SectionCard, { title: "Redirect Rules", action: _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx("input", { ref: fileRef, type: "file", accept: ".csv,text/csv", className: "sr-only", "aria-hidden": true, tabIndex: -1, onChange: (e) => {
124
+ const f = e.target.files?.[0];
125
+ if (f)
126
+ handleImportFile(f);
127
+ e.target.value = '';
128
+ } }), _jsxs("button", { className: btnSecondary, onClick: () => fileRef.current?.click(), children: [_jsx(Upload, { className: "h-4 w-4", "aria-hidden": true }), " Import"] }), _jsxs("button", { className: btnSecondary, onClick: handleExport, children: [_jsx(Download, { className: "h-4 w-4", "aria-hidden": true }), " Export"] }), _jsxs("button", { className: btnPrimary, onClick: () => setEditor({ rule: null }), children: [_jsx(Plus, { className: "h-4 w-4", "aria-hidden": true }), " New redirect"] })] }), children: loading ? (_jsx(SeoLoading, { label: "Loading redirects\u2026" })) : error ? (_jsx(SeoErrorState, { message: error, onRetry: refetch })) : (data ?? []).length === 0 ? (_jsx(SeoEmptyState, { icon: _jsx(Link2, { size: 24 }), title: "No redirects yet", description: "Create a redirect or import a CSV to get started.", action: _jsxs("button", { className: btnPrimary, onClick: () => setEditor({ rule: null }), children: [_jsx(Plus, { className: "h-4 w-4", "aria-hidden": true }), " New redirect"] }) })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", "aria-label": "Redirect rules", children: [_jsx("thead", { children: _jsxs("tr", { className: "text-muted-foreground border-border border-b text-left", children: [_jsx("th", { className: "py-2 pr-4 font-medium", children: "Source" }), _jsx("th", { className: "py-2 pr-4 font-medium", children: "Destination" }), _jsx("th", { className: "py-2 pr-4 font-medium", children: "Type" }), _jsx("th", { className: "py-2 pr-4 text-right font-medium", children: "Hits" }), _jsx("th", { className: "py-2 pr-4 font-medium", children: "Status" }), _jsx("th", { className: "py-2 text-right font-medium", children: _jsx("span", { className: "sr-only", children: "Actions" }) })] }) }), _jsx("tbody", { className: "divide-border divide-y", children: data.map((r) => (_jsxs("tr", { className: "hover:bg-accent/40", children: [_jsx("td", { className: "text-foreground py-2.5 pr-4 font-mono", children: r.fromPath }), _jsx("td", { className: "text-muted-foreground py-2.5 pr-4 font-mono", children: r.type === 410 ? _jsx("span", { className: "italic", children: "\u2014 (gone)" }) : r.toPath }), _jsx("td", { className: "text-foreground py-2.5 pr-4 tabular-nums", children: r.type }), _jsx("td", { className: "text-foreground py-2.5 pr-4 text-right tabular-nums", children: r.hits }), _jsx("td", { className: "py-2.5 pr-4", children: _jsxs("span", { className: "flex flex-wrap gap-1.5", children: [_jsx(SeoStatusBadge, { label: r.enabled ? 'Enabled' : 'Disabled', tone: r.enabled ? 'good' : 'neutral' }), r.stale && _jsx(SeoStatusBadge, { label: "Stale", tone: "fair" })] }) }), _jsx("td", { className: "py-2.5 text-right", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsx("button", { className: btnGhostIcon, onClick: () => setEditor({ rule: r }), "aria-label": `Edit redirect ${r.fromPath}`, children: _jsx(Pencil, { className: "h-4 w-4", "aria-hidden": true }) }), _jsx("button", { className: btnGhostIcon, onClick: () => setConfirmDelete(r), "aria-label": `Delete redirect ${r.fromPath}`, children: _jsx(Trash2, { className: "h-4 w-4", "aria-hidden": true }) })] }) })] }, r.id))) })] }) })) }), _jsx(RedirectEditorDrawer, { open: editor !== null, onOpenChange: (o) => !o && setEditor(null), rule: editor?.rule ?? null, onSaved: refetchAll }), _jsx(ConfirmDialog, { open: confirmDelete !== null, onClose: () => setConfirmDelete(null), onConfirm: () => confirmDelete && handleDelete(confirmDelete), title: "Delete redirect", description: `Delete the redirect from "${confirmDelete?.fromPath}"? This cannot be undone.`, confirmLabel: "Delete", destructive: true })] }));
129
+ }
130
+ //# sourceMappingURL=RedirectsTab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RedirectsTab.js","sourceRoot":"","sources":["../../../src/views/seo/RedirectsTab.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function TechnicalTab(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=TechnicalTab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TechnicalTab.d.ts","sourceRoot":"","sources":["../../../src/views/seo/TechnicalTab.tsx"],"names":[],"mappings":"AAgTA,wBAAgB,YAAY,4CAU3B"}
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useState } from 'react';
4
+ import { RefreshCw, Map, AlertTriangle, CheckCircle2, ExternalLink } from 'lucide-react';
5
+ import * as Switch from '@radix-ui/react-switch';
6
+ import { toast } from 'sonner';
7
+ import { fetchSitemapStatus, regenerateSitemap, fetchCrawlSettings, updateCrawlSettings, fetchRobotsTxt, updateRobotsTxt, validateRobotsTxt, } from '../../lib/seo-service.js';
8
+ import { SectionCard, SeoLoading, SeoErrorState, btnPrimary, btnSecondary, } from '../../components/seo/primitives.js';
9
+ import { useSeoResource } from './useSeoResource.js';
10
+ const CRAWL_TOGGLES = [
11
+ {
12
+ key: 'indexEntireSite',
13
+ label: 'Index entire site',
14
+ help: 'Allow search engines to index all public pages.',
15
+ },
16
+ {
17
+ key: 'includeImagesInSitemap',
18
+ label: 'Include images in sitemap',
19
+ help: 'Add image entries to the XML sitemap.',
20
+ },
21
+ {
22
+ key: 'includePostsInSitemap',
23
+ label: 'Include posts in sitemap',
24
+ help: 'Add published posts to the sitemap.',
25
+ },
26
+ {
27
+ key: 'noindexPaginatedArchives',
28
+ label: 'No-index paginated archives',
29
+ help: 'Avoid duplicate-content penalties on page 2+.',
30
+ },
31
+ {
32
+ key: 'noindexTagPages',
33
+ label: 'No-index tag pages',
34
+ help: 'Keep thin tag archives out of the index.',
35
+ },
36
+ ];
37
+ function Toggle({ checked, onChange, label, help, disabled, }) {
38
+ const labelId = useId();
39
+ const helpId = useId();
40
+ return (_jsxs("div", { className: "flex items-center justify-between gap-3 py-2.5", children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { id: labelId, className: "text-foreground block text-sm font-medium", children: label }), _jsx("span", { id: helpId, className: "text-muted-foreground block text-sm", children: help })] }), _jsx(Switch.Root, { checked: checked, onCheckedChange: onChange, disabled: disabled, "aria-labelledby": labelId, "aria-describedby": helpId, className: "bg-input-background data-[state=checked]:bg-primary focus-visible:ring-ring relative h-[18px] w-8 shrink-0 rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", children: _jsx(Switch.Thumb, { className: "bg-background block h-3.5 w-3.5 translate-x-0.5 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-[14px]" }) })] }));
41
+ }
42
+ function SitemapCard() {
43
+ const { data, loading, error, refetch } = useSeoResource(fetchSitemapStatus, []);
44
+ const [busy, setBusy] = useState(false);
45
+ async function handleRegenerate() {
46
+ setBusy(true);
47
+ const res = await regenerateSitemap();
48
+ setBusy(false);
49
+ if (res.error) {
50
+ toast.error(res.error);
51
+ return;
52
+ }
53
+ toast.success('Sitemap regenerated.');
54
+ refetch();
55
+ }
56
+ return (_jsx(SectionCard, { title: "Sitemap", action: _jsxs("button", { className: btnSecondary, onClick: handleRegenerate, disabled: busy || loading, children: [_jsx(RefreshCw, { className: `h-4 w-4 ${busy ? 'animate-spin' : ''}`, "aria-hidden": true }), "Regenerate"] }), children: loading ? (_jsx(SeoLoading, { label: "Loading sitemap status\u2026" })) : error ? (_jsx(SeoErrorState, { message: error, onRetry: refetch })) : !data ? (_jsx("p", { className: "text-muted-foreground text-sm", children: "Sitemap status unavailable." })) : (_jsxs("div", { className: "space-y-3", children: [_jsxs("a", { href: data.sitemapUrl, target: "_blank", rel: "noopener noreferrer", className: "text-primary inline-flex items-center gap-1 text-sm hover:underline", children: [_jsx(Map, { className: "h-4 w-4", "aria-hidden": true }), data.sitemapUrl, _jsx(ExternalLink, { className: "h-3.5 w-3.5", "aria-hidden": true })] }), _jsxs("dl", { className: "grid grid-cols-2 gap-3 text-sm", children: [_jsxs("div", { children: [_jsx("dt", { className: "text-muted-foreground", children: "Pages included" }), _jsx("dd", { className: "text-foreground font-medium tabular-nums", children: data.pagesIncluded })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-muted-foreground", children: "Pages excluded" }), _jsx("dd", { className: "text-foreground font-medium tabular-nums", children: data.pagesExcluded })] }), _jsxs("div", { className: "col-span-2", children: [_jsx("dt", { className: "text-muted-foreground", children: "Last generated" }), _jsx("dd", { className: "text-foreground", children: data.lastGeneratedAt ? new Date(data.lastGeneratedAt).toLocaleString() : 'Never' })] })] }), data.warnings.length > 0 && (_jsx("ul", { className: "space-y-1", children: data.warnings.map((w, i) => (_jsxs("li", { className: "text-warning flex items-center gap-1.5 text-sm", children: [_jsx(AlertTriangle, { className: "h-3.5 w-3.5", "aria-hidden": true }), " ", w] }, i))) }))] })) }));
57
+ }
58
+ function CrawlCard() {
59
+ const { data, loading, error, refetch } = useSeoResource(fetchCrawlSettings, []);
60
+ const [settings, setSettings] = useState(null);
61
+ const [saving, setSaving] = useState(false);
62
+ useEffect(() => {
63
+ if (data)
64
+ setSettings(data);
65
+ }, [data]);
66
+ async function save(next, changed) {
67
+ setSettings(next);
68
+ setSaving(true);
69
+ const res = await updateCrawlSettings({ [changed]: next[changed] });
70
+ setSaving(false);
71
+ if (res.error) {
72
+ toast.error(res.error);
73
+ refetch();
74
+ }
75
+ }
76
+ return (_jsx(SectionCard, { title: "Crawl Settings", children: loading ? (_jsx(SeoLoading, { label: "Loading crawl settings\u2026" })) : error ? (_jsx(SeoErrorState, { message: error, onRetry: refetch })) : !settings ? (_jsx("p", { className: "text-muted-foreground text-sm", children: "Crawl settings unavailable." })) : (_jsx("div", { className: "divide-border divide-y", "aria-busy": saving, children: CRAWL_TOGGLES.map((t) => (_jsx(Toggle, { label: t.label, help: t.help, checked: settings[t.key], disabled: saving, onChange: (v) => save({ ...settings, [t.key]: v }, t.key) }, t.key))) })) }));
77
+ }
78
+ function RobotsCard() {
79
+ const { data, loading, error, refetch } = useSeoResource(fetchRobotsTxt, []);
80
+ const [body, setBody] = useState('');
81
+ const [saving, setSaving] = useState(false);
82
+ useEffect(() => {
83
+ if (data)
84
+ setBody(data.body);
85
+ }, [data]);
86
+ const validation = validateRobotsTxt(body);
87
+ async function handleSave() {
88
+ setSaving(true);
89
+ const res = await updateRobotsTxt(body);
90
+ setSaving(false);
91
+ if (res.error) {
92
+ toast.error(res.error);
93
+ return;
94
+ }
95
+ toast.success('robots.txt saved.');
96
+ refetch();
97
+ }
98
+ return (_jsx(SectionCard, { title: "robots.txt", description: "Controls which crawlers can access your site.", action: _jsx("button", { className: btnPrimary, onClick: handleSave, disabled: saving || loading || !validation.valid, children: saving ? 'Saving…' : 'Save' }), children: loading ? (_jsx(SeoLoading, { label: "Loading robots.txt\u2026" })) : error ? (_jsx(SeoErrorState, { message: error, onRetry: refetch })) : (_jsxs("div", { className: "space-y-3", children: [validation.blocksEntireSite && (_jsxs("div", { className: "border-destructive/30 bg-destructive/10 flex items-start gap-2 rounded-md border p-3", role: "alert", children: [_jsx(AlertTriangle, { className: "text-destructive mt-0.5 h-4 w-4 shrink-0", "aria-hidden": true }), _jsx("p", { className: "text-destructive text-sm", children: "This configuration blocks your entire site from all search engines. Pages will be removed from search results." })] })), _jsx("label", { className: "sr-only", htmlFor: "robots-editor", children: "robots.txt contents" }), _jsx("textarea", { id: "robots-editor", value: body, onChange: (e) => setBody(e.target.value), spellCheck: false, className: "border-border bg-input-background text-foreground focus-visible:ring-ring min-h-48 w-full rounded-md border p-3 font-mono text-sm focus-visible:ring-2 focus-visible:outline-none" }), validation.errors.length > 0 ? (_jsx("ul", { className: "space-y-1", children: validation.errors.map((e, i) => (_jsxs("li", { className: "text-destructive flex items-center gap-1.5 text-sm", children: [_jsx(AlertTriangle, { className: "h-3.5 w-3.5", "aria-hidden": true }), " ", e] }, i))) })) : (_jsxs("p", { className: "text-success flex items-center gap-1.5 text-sm", children: [_jsx(CheckCircle2, { className: "h-3.5 w-3.5", "aria-hidden": true }), " Valid robots.txt"] })), validation.warnings
99
+ .filter((w) => !validation.blocksEntireSite || !w.includes('blocks the entire site'))
100
+ .map((w, i) => (_jsxs("p", { className: "text-warning flex items-center gap-1.5 text-sm", children: [_jsx(AlertTriangle, { className: "h-3.5 w-3.5", "aria-hidden": true }), " ", w] }, i)))] })) }));
101
+ }
102
+ export function TechnicalTab() {
103
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 lg:grid-cols-2", children: [_jsx(SitemapCard, {}), _jsx(CrawlCard, {})] }), _jsx(RobotsCard, {})] }));
104
+ }
105
+ //# sourceMappingURL=TechnicalTab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TechnicalTab.js","sourceRoot":"","sources":["../../../src/views/seo/TechnicalTab.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export interface SeoResource<T> {
2
+ data: T | null;
3
+ loading: boolean;
4
+ error: string | null;
5
+ refetch: () => void;
6
+ }
7
+ export declare function useSeoResource<T>(fetcher: () => Promise<T>, deps?: ReadonlyArray<unknown>): SeoResource<T>;
8
+ //# sourceMappingURL=useSeoResource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSeoResource.d.ts","sourceRoot":"","sources":["../../../src/views/seo/useSeoResource.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,IAAI,EAAE,CAAC,GAAG,IAAI,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,wBAAgB,cAAc,CAAC,CAAC,EAC9B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,IAAI,GAAE,aAAa,CAAC,OAAO,CAAM,GAChC,WAAW,CAAC,CAAC,CAAC,CA8ChB"}
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+ /**
3
+ * Small async-resource hook for the SEO tabs: wraps a service fetcher with
4
+ * loading / error / data / refetch and abort-on-unmount safety. Keeps each
5
+ * tab declarative without re-implementing fetch plumbing.
6
+ */
7
+ import { useCallback, useEffect, useRef, useState } from 'react';
8
+ export function useSeoResource(fetcher, deps = []) {
9
+ const [data, setData] = useState(null);
10
+ const [loading, setLoading] = useState(true);
11
+ const [error, setError] = useState(null);
12
+ // Tracks whether the component is still mounted (guards setState after
13
+ // unmount). Set once on mount; never toggled by dependency-driven reloads.
14
+ const mountedRef = useRef(true);
15
+ // Monotonic id of the most recently *started* request. A response is only
16
+ // applied when its id still matches — otherwise a slow earlier request that
17
+ // resolves after a faster later one (rapid filter switches) would clobber
18
+ // the newer, correct data.
19
+ const requestIdRef = useRef(0);
20
+ // eslint-disable-next-line react-hooks/exhaustive-deps
21
+ const stableFetcher = useCallback(fetcher, deps);
22
+ const load = useCallback(async () => {
23
+ const requestId = ++requestIdRef.current;
24
+ const isCurrent = () => mountedRef.current && requestId === requestIdRef.current;
25
+ setLoading(true);
26
+ setError(null);
27
+ try {
28
+ const result = await stableFetcher();
29
+ if (isCurrent())
30
+ setData(result);
31
+ }
32
+ catch (err) {
33
+ if (isCurrent()) {
34
+ setError(err instanceof Error ? err.message : 'Failed to load data');
35
+ }
36
+ }
37
+ finally {
38
+ if (isCurrent())
39
+ setLoading(false);
40
+ }
41
+ }, [stableFetcher]);
42
+ // Mount/unmount guard only — independent of `load` so a dependency change
43
+ // (which recreates `load`) never flips `mountedRef` to false mid-flight.
44
+ useEffect(() => {
45
+ mountedRef.current = true;
46
+ return () => {
47
+ mountedRef.current = false;
48
+ };
49
+ }, []);
50
+ useEffect(() => {
51
+ load();
52
+ }, [load]);
53
+ return { data, loading, error, refetch: load };
54
+ }
55
+ //# sourceMappingURL=useSeoResource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSeoResource.js","sourceRoot":"","sources":["../../../src/views/seo/useSeoResource.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAShE,MAAM,UAAU,cAAc,CAC5B,OAAyB,EACzB,OAA+B,EAAE;IAEjC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAW,IAAI,CAAC,CAAA;IAChD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC5C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IACvD,uEAAuE;IACvE,2EAA2E;IAC3E,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC/B,0EAA0E;IAC1E,4EAA4E;IAC5E,0EAA0E;IAC1E,2BAA2B;IAC3B,MAAM,YAAY,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IAC9B,uDAAuD;IACvD,MAAM,aAAa,GAAG,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IAEhD,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAClC,MAAM,SAAS,GAAG,EAAE,YAAY,CAAC,OAAO,CAAA;QACxC,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,IAAI,SAAS,KAAK,YAAY,CAAC,OAAO,CAAA;QAChF,UAAU,CAAC,IAAI,CAAC,CAAA;QAChB,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,aAAa,EAAE,CAAA;YACpC,IAAI,SAAS,EAAE;gBAAE,OAAO,CAAC,MAAM,CAAC,CAAA;QAClC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,SAAS,EAAE,EAAE,CAAC;gBAChB,QAAQ,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAA;YACtE,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,SAAS,EAAE;gBAAE,UAAU,CAAC,KAAK,CAAC,CAAA;QACpC,CAAC;IACH,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAA;IAEnB,0EAA0E;IAC1E,yEAAyE;IACzE,SAAS,CAAC,GAAG,EAAE;QACb,UAAU,CAAC,OAAO,GAAG,IAAI,CAAA;QACzB,OAAO,GAAG,EAAE;YACV,UAAU,CAAC,OAAO,GAAG,KAAK,CAAA;QAC5B,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,EAAE,CAAA;IACR,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IAEV,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAChD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/cms-admin",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/actuate-media/actuatecms.git",
@@ -87,7 +87,7 @@
87
87
  "tailwindcss": "^4.0.0",
88
88
  "typescript": "^5.7.0",
89
89
  "vitest": "^3.0.0",
90
- "@actuate-media/cms-core": "0.28.0",
90
+ "@actuate-media/cms-core": "0.30.0",
91
91
  "@actuate-media/component-blocks": "0.2.0"
92
92
  },
93
93
  "scripts": {
package/src/AdminRoot.tsx CHANGED
@@ -13,8 +13,6 @@ import { Settings } from './views/Settings.js'
13
13
  import { Forms } from './views/Forms.js'
14
14
  import { FormEditor } from './views/FormEditor.js'
15
15
  import { FormSubmissions } from './views/FormSubmissions.js'
16
- import { Users } from './views/Users.js'
17
- import { ApiKeys } from './views/ApiKeys.js'
18
16
  import { SEO } from './views/SEO.js'
19
17
  import { ScriptTags } from './views/ScriptTags.js'
20
18
  import { ScriptTagEditor } from './views/ScriptTagEditor.js'
@@ -379,17 +377,21 @@ function AdminShell({
379
377
  return <Forms onNavigate={navigate} />
380
378
  }
381
379
 
380
+ // Legacy deep paths map onto the new five-tab IA.
382
381
  if (matchRoute('/seo/redirects')) {
383
382
  return <SEO onNavigate={navigate} initialTab="redirects" />
384
383
  }
385
- if (matchRoute('/seo/canonicals')) {
386
- return <SEO onNavigate={navigate} initialTab="canonicals" />
384
+ if (matchRoute('/seo/content') || matchRoute('/seo/canonicals')) {
385
+ return <SEO onNavigate={navigate} initialTab="content" />
387
386
  }
388
- if (matchRoute('/seo/links')) {
389
- return <SEO onNavigate={navigate} initialTab="links" />
387
+ if (matchRoute('/seo/technical') || matchRoute('/seo/links')) {
388
+ return <SEO onNavigate={navigate} initialTab="technical" />
389
+ }
390
+ if (matchRoute('/seo/audit')) {
391
+ return <SEO onNavigate={navigate} initialTab="audit" />
390
392
  }
391
393
  if (matchRoute('/seo')) {
392
- return <SEO onNavigate={navigate} initialTab="pages" />
394
+ return <SEO onNavigate={navigate} initialTab="overview" />
393
395
  }
394
396
 
395
397
  if (matchRoute('/script-tags/new')) {
@@ -415,12 +417,14 @@ function AdminShell({
415
417
  return <Widgets onNavigate={navigate} />
416
418
  }
417
419
 
420
+ // Users + API Keys moved into the Settings view as tabs. The legacy
421
+ // top-level routes still resolve — they just land on the matching tab.
418
422
  if (matchRoute('/users')) {
419
- return <Users onNavigate={navigate} />
423
+ return <Settings onNavigate={navigate} config={config} initialTab="users" />
420
424
  }
421
425
 
422
426
  if (matchRoute('/api-keys')) {
423
- return <ApiKeys onNavigate={navigate} />
427
+ return <Settings onNavigate={navigate} config={config} initialTab="api-keys" />
424
428
  }
425
429
 
426
430
  if (matchRoute('/settings')) {
@@ -156,9 +156,9 @@ describe('buildNavItems', () => {
156
156
  // Lands after the static Content destinations, before Settings.
157
157
  const menusIdx = items.findIndex((i) => i.path === '/navigations')
158
158
  const scriptIdx = items.findIndex((i) => i.path === '/script-tags')
159
- const usersIdx = items.findIndex((i) => i.path === '/users')
159
+ const settingsIdx = items.findIndex((i) => i.path === '/settings')
160
160
  expect(menusIdx).toBeGreaterThan(scriptIdx)
161
- expect(menusIdx).toBeLessThan(usersIdx)
161
+ expect(menusIdx).toBeLessThan(settingsIdx)
162
162
  })
163
163
 
164
164
  it('keeps standard Content/Settings entries after collections', () => {
@@ -172,8 +172,10 @@ describe('buildNavItems', () => {
172
172
  expect(paths).toContain('/forms')
173
173
  expect(paths).toContain('/seo')
174
174
  expect(paths).toContain('/script-tags')
175
- expect(paths).toContain('/users')
176
- expect(paths).toContain('/api-keys')
177
175
  expect(paths).toContain('/settings')
176
+ // Users + API Keys moved into Settings tabs — they are no longer
177
+ // standalone sidebar destinations.
178
+ expect(paths).not.toContain('/users')
179
+ expect(paths).not.toContain('/api-keys')
178
180
  })
179
181
  })
@@ -0,0 +1,60 @@
1
+ // @vitest-environment node
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ const cmsApi = vi.fn()
5
+ vi.mock('../../lib/api.js', () => ({
6
+ cmsApi: (...args: unknown[]) => cmsApi(...args),
7
+ }))
8
+
9
+ const {
10
+ fetchSeoOverview,
11
+ fetchSeoContentRecords,
12
+ fetchSeoIssues,
13
+ fetchRedirectRules,
14
+ updateSeoFields,
15
+ } = await import('../../lib/seo-service.js')
16
+
17
+ afterEach(() => {
18
+ cmsApi.mockReset()
19
+ })
20
+
21
+ /**
22
+ * Regression tests for the "swallowed read errors" blocker. `cmsApi` never
23
+ * throws — it returns `{ error }` on failure — so the SEO read fetchers must
24
+ * re-throw, otherwise `useSeoResource` can't distinguish "request failed" from
25
+ * "empty result" and renders a misleading fallback (e.g. a fake score of 0 /
26
+ * grade "Critical") with the error/retry UI permanently unreachable.
27
+ */
28
+ describe('SEO service read fetchers surface errors', () => {
29
+ it('fetchSeoOverview throws when the request fails (instead of returning a fake 0 score)', async () => {
30
+ cmsApi.mockResolvedValue({ error: 'Request failed (500)', status: 500 })
31
+ await expect(fetchSeoOverview()).rejects.toThrow('Request failed (500)')
32
+ })
33
+
34
+ it('fetchSeoContentRecords throws on error', async () => {
35
+ cmsApi.mockResolvedValue({ error: 'Network error', status: 0 })
36
+ await expect(fetchSeoContentRecords()).rejects.toThrow('Network error')
37
+ })
38
+
39
+ it('fetchSeoIssues throws on error', async () => {
40
+ cmsApi.mockResolvedValue({ error: 'Request failed (503)', status: 503 })
41
+ await expect(fetchSeoIssues()).rejects.toThrow('Request failed (503)')
42
+ })
43
+
44
+ it('fetchRedirectRules throws on error', async () => {
45
+ cmsApi.mockResolvedValue({ error: 'boom', status: 500 })
46
+ await expect(fetchRedirectRules()).rejects.toThrow('boom')
47
+ })
48
+
49
+ it('returns data normally on success', async () => {
50
+ cmsApi.mockResolvedValue({ data: { records: [{ id: 'r1' }] }, status: 200 })
51
+ await expect(fetchSeoContentRecords()).resolves.toEqual([{ id: 'r1' }])
52
+ })
53
+
54
+ it('mutations still return { error } rather than throwing', async () => {
55
+ cmsApi.mockResolvedValue({ error: 'forbidden', status: 403 })
56
+ await expect(updateSeoFields('page', 'doc-1', { metaTitle: 'x' })).resolves.toEqual({
57
+ error: 'forbidden',
58
+ })
59
+ })
60
+ })
@@ -0,0 +1,68 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, it } from 'vitest'
3
+ import { act, render, screen, waitFor } from '@testing-library/react'
4
+
5
+ import { useSeoResource } from '../../views/seo/useSeoResource.js'
6
+
7
+ /**
8
+ * Regression test for the `useSeoResource` stale-response race: when the
9
+ * dependency (and therefore the fetcher) changes rapidly, a slow earlier
10
+ * request that resolves AFTER a faster later one must not clobber the newer
11
+ * result. We drive this with two deferred promises whose resolution order is
12
+ * inverted relative to the request order.
13
+ */
14
+
15
+ function Probe({ dep, fetcher }: { dep: number; fetcher: () => Promise<string> }) {
16
+ const { data, loading, error } = useSeoResource<string>(fetcher, [dep])
17
+ return (
18
+ <div>
19
+ <span data-testid="state">{loading ? 'loading' : (error ?? data ?? 'empty')}</span>
20
+ </div>
21
+ )
22
+ }
23
+
24
+ function deferred<T>() {
25
+ let resolve!: (value: T) => void
26
+ let reject!: (reason?: unknown) => void
27
+ const promise = new Promise<T>((res, rej) => {
28
+ resolve = res
29
+ reject = rej
30
+ })
31
+ return { promise, resolve, reject }
32
+ }
33
+
34
+ describe('useSeoResource', () => {
35
+ it('ignores a stale (earlier) response that resolves after the latest one', async () => {
36
+ const first = deferred<string>()
37
+ const second = deferred<string>()
38
+
39
+ // First render uses the "first" fetcher (slow), then we switch deps so the
40
+ // hook re-runs with the "second" fetcher (fast).
41
+ const firstFetcher = () => first.promise
42
+ const secondFetcher = () => second.promise
43
+
44
+ const { rerender } = render(<Probe dep={1} fetcher={firstFetcher} />)
45
+ rerender(<Probe dep={2} fetcher={secondFetcher} />)
46
+
47
+ // The newer request resolves FIRST.
48
+ await act(async () => {
49
+ second.resolve('NEW')
50
+ })
51
+ await waitFor(() => expect(screen.getByTestId('state').textContent).toBe('NEW'))
52
+
53
+ // The stale earlier request resolves LATER — it must be ignored.
54
+ await act(async () => {
55
+ first.resolve('OLD')
56
+ })
57
+ expect(screen.getByTestId('state').textContent).toBe('NEW')
58
+ })
59
+
60
+ it('surfaces fetcher errors', async () => {
61
+ const d = deferred<string>()
62
+ render(<Probe dep={1} fetcher={() => d.promise} />)
63
+ await act(async () => {
64
+ d.reject(new Error('boom'))
65
+ })
66
+ await waitFor(() => expect(screen.getByTestId('state').textContent).toBe('boom'))
67
+ })
68
+ })
@@ -36,7 +36,10 @@ export interface BreadcrumbsProps {
36
36
  }
37
37
 
38
38
  export function Breadcrumbs({ currentPath, onNavigate }: BreadcrumbsProps) {
39
- const segments = currentPath.split('/').filter(Boolean)
39
+ // Strip query string + hash so tab-driven routes (e.g. `/settings?tab=users`,
40
+ // `/seo?tab=audit`) don't leak the raw query into a crumb label.
41
+ const cleanPath = currentPath.split('?')[0]!.split('#')[0]!
42
+ const segments = cleanPath.split('/').filter(Boolean)
40
43
 
41
44
  if (segments.length === 0) return null
42
45