@prenta/admin 0.98.0 → 0.98.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/__tests__/components/seo-score-bands.test.js +0 -7
- package/dist/__tests__/components/seo-score-bands.test.js.map +1 -1
- package/dist/__tests__/lib/page-audit-input.test.js +77 -1
- package/dist/__tests__/lib/page-audit-input.test.js.map +1 -1
- package/dist/__tests__/views/page-builder-seo-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-builder-seo-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-builder-seo-panel.render.test.js +65 -0
- package/dist/__tests__/views/page-builder-seo-panel.render.test.js.map +1 -0
- package/dist/lib/page-audit-input.d.ts +14 -1
- package/dist/lib/page-audit-input.d.ts.map +1 -1
- package/dist/lib/page-audit-input.js +19 -0
- package/dist/lib/page-audit-input.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-builder/ContextPanel.d.ts.map +1 -1
- package/dist/views/page-builder/ContextPanel.js +1 -1
- package/dist/views/page-builder/ContextPanel.js.map +1 -1
- package/dist/views/page-builder/SEOPanel.d.ts +1 -2
- package/dist/views/page-builder/SEOPanel.d.ts.map +1 -1
- package/dist/views/page-builder/SEOPanel.js +10 -75
- package/dist/views/page-builder/SEOPanel.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/components/seo-score-bands.test.ts +0 -7
- package/src/__tests__/lib/page-audit-input.test.ts +91 -1
- package/src/__tests__/views/page-builder-seo-panel.render.test.tsx +77 -0
- package/src/lib/page-audit-input.ts +36 -1
- package/src/views/page-builder/ContextPanel.tsx +0 -1
- package/src/views/page-builder/SEOPanel.tsx +40 -282
|
@@ -1,127 +1,17 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
BuilderReadabilityResult,
|
|
10
|
-
} from '@prenta/core'
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
|
+
import type { PageNode } from '@prenta/core'
|
|
5
|
+
import { auditPage } from '@prenta/core/seo/page-audit'
|
|
6
|
+
import { ChevronDown, ChevronUp, Eye, FileText, Globe, Search } from 'lucide-react'
|
|
7
|
+
import { PageAuditPanel } from '../../components/seo/PageAuditPanel.js'
|
|
8
|
+
import { buildPageAuditInputFromBuilder } from '../../lib/page-audit-input.js'
|
|
11
9
|
import type { PageSettings } from '../../hooks/useBuilderState.js'
|
|
12
|
-
import {
|
|
13
|
-
CheckCircle2,
|
|
14
|
-
AlertCircle,
|
|
15
|
-
XCircle,
|
|
16
|
-
ChevronDown,
|
|
17
|
-
ChevronUp,
|
|
18
|
-
Search,
|
|
19
|
-
Globe,
|
|
20
|
-
Type,
|
|
21
|
-
FileText,
|
|
22
|
-
Clock,
|
|
23
|
-
BarChart3,
|
|
24
|
-
Target,
|
|
25
|
-
Eye,
|
|
26
|
-
} from 'lucide-react'
|
|
27
10
|
|
|
28
11
|
export interface BuilderSEOPanelProps {
|
|
29
12
|
tree: PageNode
|
|
30
13
|
pageSettings: PageSettings
|
|
31
14
|
onPageSettingsChange: (settings: Partial<PageSettings>) => void
|
|
32
|
-
selectedNodeId: string | null
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function getScoreColor(score: number): string {
|
|
36
|
-
if (score >= 70) return 'text-success'
|
|
37
|
-
if (score >= 40) return 'text-warning'
|
|
38
|
-
return 'text-destructive'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getRingStrokeColor(score: number): string {
|
|
42
|
-
if (score >= 70) return 'rgb(34, 197, 94)'
|
|
43
|
-
if (score >= 40) return 'rgb(245, 158, 11)'
|
|
44
|
-
return 'rgb(239, 68, 68)'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getScoreLabel(score: number): string {
|
|
48
|
-
if (score >= 80) return 'Excellent'
|
|
49
|
-
if (score >= 70) return 'Good'
|
|
50
|
-
if (score >= 50) return 'Needs Work'
|
|
51
|
-
return 'Poor'
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function StatusIcon({ status }: { status: BuilderSEOCheck['status'] }) {
|
|
55
|
-
switch (status) {
|
|
56
|
-
case 'good':
|
|
57
|
-
return <CheckCircle2 size={14} className="text-success shrink-0" />
|
|
58
|
-
case 'warning':
|
|
59
|
-
return <AlertCircle size={14} className="text-warning shrink-0" />
|
|
60
|
-
case 'error':
|
|
61
|
-
return <XCircle size={14} className="text-destructive shrink-0" />
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function ScoreRing({ score }: { score: number }) {
|
|
66
|
-
const radius = 32
|
|
67
|
-
const strokeWidth = 6
|
|
68
|
-
const circumference = 2 * Math.PI * radius
|
|
69
|
-
const progress = Math.min(score, 100) / 100
|
|
70
|
-
const dashOffset = circumference * (1 - progress)
|
|
71
|
-
const size = (radius + strokeWidth) * 2
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div className="relative">
|
|
75
|
-
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="-rotate-90">
|
|
76
|
-
<circle
|
|
77
|
-
cx={size / 2}
|
|
78
|
-
cy={size / 2}
|
|
79
|
-
r={radius}
|
|
80
|
-
fill="none"
|
|
81
|
-
stroke="var(--border)"
|
|
82
|
-
strokeWidth={strokeWidth}
|
|
83
|
-
/>
|
|
84
|
-
<circle
|
|
85
|
-
cx={size / 2}
|
|
86
|
-
cy={size / 2}
|
|
87
|
-
r={radius}
|
|
88
|
-
fill="none"
|
|
89
|
-
stroke={getRingStrokeColor(score)}
|
|
90
|
-
strokeWidth={strokeWidth}
|
|
91
|
-
strokeDasharray={circumference}
|
|
92
|
-
strokeDashoffset={dashOffset}
|
|
93
|
-
strokeLinecap="round"
|
|
94
|
-
className="transition-all duration-700 ease-out"
|
|
95
|
-
/>
|
|
96
|
-
</svg>
|
|
97
|
-
<div className="absolute inset-0 flex items-center justify-center">
|
|
98
|
-
<span className={`text-lg font-medium ${getScoreColor(score)}`}>{score}</span>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function ScoreSummary({ checks }: { checks: BuilderSEOCheck[] }) {
|
|
105
|
-
const passed = checks.filter((c) => c.status === 'good').length
|
|
106
|
-
const warnings = checks.filter((c) => c.status === 'warning').length
|
|
107
|
-
const errors = checks.filter((c) => c.status === 'error').length
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<div className="flex flex-col gap-1">
|
|
111
|
-
<div className="flex items-center gap-1.5">
|
|
112
|
-
<CheckCircle2 size={12} className="text-success" />
|
|
113
|
-
<span className="text-muted-foreground text-xs">{passed} passed</span>
|
|
114
|
-
</div>
|
|
115
|
-
<div className="flex items-center gap-1.5">
|
|
116
|
-
<AlertCircle size={12} className="text-warning" />
|
|
117
|
-
<span className="text-muted-foreground text-xs">{warnings} warnings</span>
|
|
118
|
-
</div>
|
|
119
|
-
<div className="flex items-center gap-1.5">
|
|
120
|
-
<XCircle size={12} className="text-destructive" />
|
|
121
|
-
<span className="text-muted-foreground text-xs">{errors} issues</span>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
)
|
|
125
15
|
}
|
|
126
16
|
|
|
127
17
|
function CollapsibleSection({
|
|
@@ -160,95 +50,6 @@ function CollapsibleSection({
|
|
|
160
50
|
)
|
|
161
51
|
}
|
|
162
52
|
|
|
163
|
-
function SEOChecksList({ checks }: { checks: BuilderSEOCheck[] }) {
|
|
164
|
-
return (
|
|
165
|
-
<div className="flex flex-col gap-2">
|
|
166
|
-
{checks.map((check) => (
|
|
167
|
-
<div key={check.id} className="flex items-start gap-2">
|
|
168
|
-
<StatusIcon status={check.status} />
|
|
169
|
-
<div className="flex flex-col">
|
|
170
|
-
<span className="text-foreground text-xs font-medium">{check.label}</span>
|
|
171
|
-
<span className="text-muted-foreground text-xs">{check.detail}</span>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
))}
|
|
175
|
-
</div>
|
|
176
|
-
)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function ReadabilityGrid({ readability }: { readability: BuilderReadabilityResult }) {
|
|
180
|
-
const fleschLabel =
|
|
181
|
-
readability.fleschScore >= 60
|
|
182
|
-
? 'Easy'
|
|
183
|
-
: readability.fleschScore >= 30
|
|
184
|
-
? 'Moderate'
|
|
185
|
-
: 'Difficult'
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<div className="grid grid-cols-2 gap-3">
|
|
189
|
-
<div className="bg-muted/50 flex flex-col gap-1 rounded-md p-3">
|
|
190
|
-
<div className="flex items-center gap-1.5">
|
|
191
|
-
<BarChart3 size={12} className="text-muted-foreground" />
|
|
192
|
-
<span className="text-muted-foreground text-xs">Flesch Score</span>
|
|
193
|
-
</div>
|
|
194
|
-
<span className={`text-sm font-medium ${getScoreColor(readability.fleschScore)}`}>
|
|
195
|
-
{readability.fleschScore}
|
|
196
|
-
</span>
|
|
197
|
-
<span className="text-muted-foreground text-xs">{fleschLabel}</span>
|
|
198
|
-
</div>
|
|
199
|
-
|
|
200
|
-
<div className="bg-muted/50 flex flex-col gap-1 rounded-md p-3">
|
|
201
|
-
<div className="flex items-center gap-1.5">
|
|
202
|
-
<Type size={12} className="text-muted-foreground" />
|
|
203
|
-
<span className="text-muted-foreground text-xs">Word Count</span>
|
|
204
|
-
</div>
|
|
205
|
-
<span className="text-foreground text-sm font-medium">{readability.wordCount}</span>
|
|
206
|
-
<span className="text-muted-foreground text-xs">
|
|
207
|
-
{readability.wordCount >= 300 ? 'Good length' : 'Short'}
|
|
208
|
-
</span>
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
<div className="bg-muted/50 flex flex-col gap-1 rounded-md p-3">
|
|
212
|
-
<div className="flex items-center gap-1.5">
|
|
213
|
-
<FileText size={12} className="text-muted-foreground" />
|
|
214
|
-
<span className="text-muted-foreground text-xs">Avg Sentence</span>
|
|
215
|
-
</div>
|
|
216
|
-
<span className="text-foreground text-sm font-medium">
|
|
217
|
-
{readability.avgSentenceLength} words
|
|
218
|
-
</span>
|
|
219
|
-
<span className="text-muted-foreground text-xs">
|
|
220
|
-
{readability.avgSentenceLength <= 20 ? 'Good' : 'Long'}
|
|
221
|
-
</span>
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
<div className="bg-muted/50 flex flex-col gap-1 rounded-md p-3">
|
|
225
|
-
<div className="flex items-center gap-1.5">
|
|
226
|
-
<Clock size={12} className="text-muted-foreground" />
|
|
227
|
-
<span className="text-muted-foreground text-xs">Reading Time</span>
|
|
228
|
-
</div>
|
|
229
|
-
<span className="text-foreground text-sm font-medium">{readability.readingTime} min</span>
|
|
230
|
-
<span className="text-muted-foreground text-xs">~{readability.wordCount} words</span>
|
|
231
|
-
</div>
|
|
232
|
-
</div>
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function PerBlockHints({ hints }: { hints: BuilderSEOCheck[] }) {
|
|
237
|
-
return (
|
|
238
|
-
<div className="flex flex-col gap-2">
|
|
239
|
-
{hints.map((hint, index) => (
|
|
240
|
-
<div key={`${hint.id}-${index}`} className="flex items-start gap-2">
|
|
241
|
-
<StatusIcon status={hint.status} />
|
|
242
|
-
<div className="flex flex-col">
|
|
243
|
-
<span className="text-foreground text-xs font-medium">{hint.label}</span>
|
|
244
|
-
<span className="text-muted-foreground text-xs">{hint.detail}</span>
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
))}
|
|
248
|
-
</div>
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
53
|
function BasicSEOFields({
|
|
253
54
|
pageSettings,
|
|
254
55
|
onPageSettingsChange,
|
|
@@ -263,7 +64,9 @@ function BasicSEOFields({
|
|
|
263
64
|
<div className="flex flex-col gap-4">
|
|
264
65
|
<div className="flex flex-col gap-1.5">
|
|
265
66
|
<div className="flex items-center justify-between">
|
|
266
|
-
<label className="text-foreground text-xs font-medium">
|
|
67
|
+
<label htmlFor="builder-meta-title" className="text-foreground text-xs font-medium">
|
|
68
|
+
Meta Title
|
|
69
|
+
</label>
|
|
267
70
|
<span
|
|
268
71
|
className={`text-xs ${metaTitleLength > 60 ? 'text-destructive' : metaTitleLength > 0 ? 'text-muted-foreground' : 'text-muted-foreground'}`}
|
|
269
72
|
>
|
|
@@ -271,6 +74,7 @@ function BasicSEOFields({
|
|
|
271
74
|
</span>
|
|
272
75
|
</div>
|
|
273
76
|
<input
|
|
77
|
+
id="builder-meta-title"
|
|
274
78
|
type="text"
|
|
275
79
|
value={pageSettings.metaTitle || ''}
|
|
276
80
|
onChange={(e) => onPageSettingsChange({ metaTitle: e.target.value })}
|
|
@@ -358,14 +162,12 @@ export function BuilderSEOPanel({
|
|
|
358
162
|
tree,
|
|
359
163
|
pageSettings,
|
|
360
164
|
onPageSettingsChange,
|
|
361
|
-
selectedNodeId,
|
|
362
165
|
}: BuilderSEOPanelProps) {
|
|
363
|
-
const
|
|
364
|
-
() =>
|
|
166
|
+
const audit = useMemo(
|
|
167
|
+
() => auditPage(buildPageAuditInputFromBuilder({ tree, pageSettings })),
|
|
365
168
|
[tree, pageSettings],
|
|
366
169
|
)
|
|
367
|
-
|
|
368
|
-
const [expandedSections, setExpandedSections] = useState<string[]>(['checks'])
|
|
170
|
+
const [expandedSections, setExpandedSections] = useState<string[]>(['basicSeo'])
|
|
369
171
|
|
|
370
172
|
const toggleSection = (section: string) => {
|
|
371
173
|
setExpandedSections((prev) =>
|
|
@@ -373,11 +175,6 @@ export function BuilderSEOPanel({
|
|
|
373
175
|
)
|
|
374
176
|
}
|
|
375
177
|
|
|
376
|
-
const blockHints = useMemo(() => {
|
|
377
|
-
if (!selectedNodeId) return []
|
|
378
|
-
return analysis.perBlockHints.get(selectedNodeId) || []
|
|
379
|
-
}, [analysis.perBlockHints, selectedNodeId])
|
|
380
|
-
|
|
381
178
|
if (!tree.children || tree.children.length === 0) {
|
|
382
179
|
return (
|
|
383
180
|
<div className="flex min-h-[200px] flex-col items-center justify-center p-6 text-center">
|
|
@@ -392,73 +189,34 @@ export function BuilderSEOPanel({
|
|
|
392
189
|
|
|
393
190
|
return (
|
|
394
191
|
<div className="flex flex-col">
|
|
395
|
-
<
|
|
396
|
-
<ScoreRing score={analysis.score} />
|
|
397
|
-
<div className="flex flex-col gap-1">
|
|
398
|
-
<span className="text-foreground text-xs font-medium">
|
|
399
|
-
{getScoreLabel(analysis.score)}
|
|
400
|
-
</span>
|
|
401
|
-
<ScoreSummary checks={analysis.checks} />
|
|
402
|
-
</div>
|
|
403
|
-
</div>
|
|
404
|
-
|
|
405
|
-
<div className="flex flex-col">
|
|
406
|
-
<CollapsibleSection
|
|
407
|
-
title="SEO Checks"
|
|
408
|
-
icon={Search}
|
|
409
|
-
expanded={expandedSections.includes('checks')}
|
|
410
|
-
onToggle={() => toggleSection('checks')}
|
|
411
|
-
>
|
|
412
|
-
<SEOChecksList checks={analysis.checks} />
|
|
413
|
-
</CollapsibleSection>
|
|
414
|
-
|
|
415
|
-
<CollapsibleSection
|
|
416
|
-
title="Readability"
|
|
417
|
-
icon={BarChart3}
|
|
418
|
-
expanded={expandedSections.includes('readability')}
|
|
419
|
-
onToggle={() => toggleSection('readability')}
|
|
420
|
-
>
|
|
421
|
-
<ReadabilityGrid readability={analysis.readability} />
|
|
422
|
-
</CollapsibleSection>
|
|
192
|
+
<PageAuditPanel audit={audit} />
|
|
423
193
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
<SearchPreview pageSettings={pageSettings} />
|
|
451
|
-
</CollapsibleSection>
|
|
452
|
-
|
|
453
|
-
<CollapsibleSection
|
|
454
|
-
title="Social Preview"
|
|
455
|
-
icon={Globe}
|
|
456
|
-
expanded={expandedSections.includes('socialPreview')}
|
|
457
|
-
onToggle={() => toggleSection('socialPreview')}
|
|
458
|
-
>
|
|
459
|
-
<SocialPreview pageSettings={pageSettings} />
|
|
460
|
-
</CollapsibleSection>
|
|
461
|
-
</div>
|
|
194
|
+
<CollapsibleSection
|
|
195
|
+
title="Basic SEO"
|
|
196
|
+
icon={FileText}
|
|
197
|
+
expanded={expandedSections.includes('basicSeo')}
|
|
198
|
+
onToggle={() => toggleSection('basicSeo')}
|
|
199
|
+
>
|
|
200
|
+
<BasicSEOFields pageSettings={pageSettings} onPageSettingsChange={onPageSettingsChange} />
|
|
201
|
+
</CollapsibleSection>
|
|
202
|
+
|
|
203
|
+
<CollapsibleSection
|
|
204
|
+
title="Search Preview"
|
|
205
|
+
icon={Eye}
|
|
206
|
+
expanded={expandedSections.includes('searchPreview')}
|
|
207
|
+
onToggle={() => toggleSection('searchPreview')}
|
|
208
|
+
>
|
|
209
|
+
<SearchPreview pageSettings={pageSettings} />
|
|
210
|
+
</CollapsibleSection>
|
|
211
|
+
|
|
212
|
+
<CollapsibleSection
|
|
213
|
+
title="Social Preview"
|
|
214
|
+
icon={Globe}
|
|
215
|
+
expanded={expandedSections.includes('socialPreview')}
|
|
216
|
+
onToggle={() => toggleSection('socialPreview')}
|
|
217
|
+
>
|
|
218
|
+
<SocialPreview pageSettings={pageSettings} />
|
|
219
|
+
</CollapsibleSection>
|
|
462
220
|
</div>
|
|
463
221
|
)
|
|
464
222
|
}
|