sanity-plugin-seofields 1.2.0 → 1.2.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/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +223 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +223 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/SeoHealthDashboard.tsx +246 -8
- package/src/plugin.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanity-plugin-seofields",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A Sanity Studio plugin to manage SEO fields like meta titles, descriptions, and Open Graph tags for structured, search-optimized content.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -24,6 +24,22 @@ const PageTitle = styled.h1`
|
|
|
24
24
|
font-weight: 700;
|
|
25
25
|
color: #111827;
|
|
26
26
|
letter-spacing: -0.3px;
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 10px;
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
const PreviewBadge = styled.span`
|
|
33
|
+
display: inline-block;
|
|
34
|
+
background: #fef3c7;
|
|
35
|
+
color: #92400e;
|
|
36
|
+
font-size: 11px;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
padding: 4px 8px;
|
|
39
|
+
border-radius: 4px;
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
letter-spacing: 0.5px;
|
|
42
|
+
margin-left: 8px;
|
|
27
43
|
`
|
|
28
44
|
|
|
29
45
|
const PageSubtitle = styled.p`
|
|
@@ -245,14 +261,14 @@ const DocId = styled.div`
|
|
|
245
261
|
text-overflow: ellipsis;
|
|
246
262
|
`
|
|
247
263
|
|
|
248
|
-
const TypeBadge = styled.span
|
|
264
|
+
const TypeBadge = styled.span<{$bgColor?: string; $textColor?: string}>`
|
|
249
265
|
display: inline-block;
|
|
250
266
|
padding: 3px 8px;
|
|
251
267
|
border-radius: 5px;
|
|
252
268
|
font-size: 11px;
|
|
253
269
|
font-weight: 500;
|
|
254
|
-
background: #ede9fe;
|
|
255
|
-
color: #5b21b6;
|
|
270
|
+
background: ${(p) => p.$bgColor || '#ede9fe'};
|
|
271
|
+
color: ${(p) => p.$textColor || '#5b21b6'};
|
|
256
272
|
`
|
|
257
273
|
|
|
258
274
|
const TypeText = styled.span`
|
|
@@ -509,6 +525,47 @@ const EmptyState = styled.div`
|
|
|
509
525
|
font-size: 13px;
|
|
510
526
|
`
|
|
511
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Color palette for dynamic document type badges
|
|
530
|
+
* Colors are randomly assigned based on type hash for visual variety
|
|
531
|
+
* while maintaining consistency across sessions
|
|
532
|
+
*/
|
|
533
|
+
const TYPE_COLOR_PALETTE: Array<{bg: string; text: string}> = [
|
|
534
|
+
{bg: '#dbeafe', text: '#0c4a6e'}, // Blue
|
|
535
|
+
{bg: '#dcfce7', text: '#14532d'}, // Green
|
|
536
|
+
{bg: '#fce7f3', text: '#500724'}, // Pink
|
|
537
|
+
{bg: '#fed7aa', text: '#7c2d12'}, // Orange
|
|
538
|
+
{bg: '#e9d5ff', text: '#581c87'}, // Purple
|
|
539
|
+
{bg: '#f3e8ff', text: '#3f0f5c'}, // Deep Purple
|
|
540
|
+
{bg: '#ccfbf1', text: '#134e4a'}, // Teal
|
|
541
|
+
{bg: '#ddd6fe', text: '#3730a3'}, // Indigo
|
|
542
|
+
{bg: '#fca5a5', text: '#7f1d1d'}, // Red
|
|
543
|
+
{bg: '#a7f3d0', text: '#065f46'}, // Emerald
|
|
544
|
+
{bg: '#fbbf24', text: '#78350f'}, // Amber
|
|
545
|
+
{bg: '#c4b5fd', text: '#3b0764'}, // Violet
|
|
546
|
+
{bg: '#f0fdf4', text: '#15803d'}, // Light Green
|
|
547
|
+
{bg: '#fef2f2', text: '#991b1b'}, // Light Red
|
|
548
|
+
{bg: '#f5f3ff', text: '#5b21b6'}, // Light Purple
|
|
549
|
+
{bg: '#fffbeb', text: '#92400e'}, // Light Amber
|
|
550
|
+
]
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Get dynamic color for a document type based on type name hash
|
|
554
|
+
* Same type always gets the same color, but assignment is not fixed
|
|
555
|
+
*/
|
|
556
|
+
const getTypeColor = (type: string): {bg: string; text: string} => {
|
|
557
|
+
// Generate consistent hash from type string using simple arithmetic
|
|
558
|
+
let hash = 0
|
|
559
|
+
for (let i = 0; i < type.length; i += 1) {
|
|
560
|
+
const char = type.charCodeAt(i)
|
|
561
|
+
hash = Math.abs(hash * 31 + char)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Use modulo to get index within palette range
|
|
565
|
+
const colorIndex = hash % TYPE_COLOR_PALETTE.length
|
|
566
|
+
return TYPE_COLOR_PALETTE[colorIndex]
|
|
567
|
+
}
|
|
568
|
+
|
|
512
569
|
const getStatusCategory = (score: number): SeoHealthMetrics['status'] => {
|
|
513
570
|
if (score >= 80) return 'excellent'
|
|
514
571
|
if (score >= 60) return 'good'
|
|
@@ -760,6 +817,155 @@ export interface SeoHealthDashboardProps {
|
|
|
760
817
|
* Defaults to `"No documents found"`.
|
|
761
818
|
*/
|
|
762
819
|
noDocuments?: React.ReactNode
|
|
820
|
+
/**
|
|
821
|
+
* Enable preview/demo mode to show dummy data.
|
|
822
|
+
* Useful for testing, documentation, or showcasing the dashboard.
|
|
823
|
+
* When enabled, displays realistic sample documents with various SEO scores.
|
|
824
|
+
* Defaults to `false`.
|
|
825
|
+
*/
|
|
826
|
+
previewMode?: boolean
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Generate dummy data for preview mode showing various SEO health scenarios
|
|
831
|
+
*/
|
|
832
|
+
const generateDummyData = (): DocumentWithSeoHealth[] => {
|
|
833
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
834
|
+
const dummyDocs: any[] = [
|
|
835
|
+
{
|
|
836
|
+
_id: 'preview-post-1',
|
|
837
|
+
_type: 'post',
|
|
838
|
+
title: 'Getting Started with SEO Best Practices',
|
|
839
|
+
slug: {current: 'getting-started-seo'},
|
|
840
|
+
_updatedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
841
|
+
seo: {
|
|
842
|
+
title: 'Getting Started with SEO Best Practices | My Blog',
|
|
843
|
+
description:
|
|
844
|
+
'Learn the fundamentals of SEO optimization to improve your website visibility and search rankings.',
|
|
845
|
+
keywords: ['seo', 'best practices', 'optimization'],
|
|
846
|
+
metaImage: {_type: 'image', asset: {_ref: 'image-123', _type: 'reference'}},
|
|
847
|
+
openGraph: {
|
|
848
|
+
title: 'SEO Best Practices Guide',
|
|
849
|
+
description: 'Master SEO optimization',
|
|
850
|
+
image: {_type: 'image', asset: {_ref: 'image-123', _type: 'reference'}, alt: 'SEO Guide'},
|
|
851
|
+
type: 'article',
|
|
852
|
+
},
|
|
853
|
+
twitter: {
|
|
854
|
+
title: 'SEO Best Practices',
|
|
855
|
+
description: 'Learn SEO optimization',
|
|
856
|
+
image: {_type: 'image', asset: {_ref: 'image-123', _type: 'reference'}, alt: 'Guide'},
|
|
857
|
+
card: 'summary_large_image',
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
_id: 'preview-post-2',
|
|
863
|
+
_type: 'post',
|
|
864
|
+
title: 'Advanced Analytics Strategy',
|
|
865
|
+
slug: {current: 'advanced-analytics'},
|
|
866
|
+
_updatedAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
867
|
+
seo: {
|
|
868
|
+
title: 'Advanced Analytics',
|
|
869
|
+
description: 'Strategy tips',
|
|
870
|
+
keywords: ['analytics', 'data'],
|
|
871
|
+
openGraph: {
|
|
872
|
+
title: 'Analytics Guide',
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
_id: 'preview-page-1',
|
|
878
|
+
_type: 'page',
|
|
879
|
+
title: 'About Us',
|
|
880
|
+
slug: {current: 'about'},
|
|
881
|
+
_updatedAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
882
|
+
seo: {
|
|
883
|
+
title: 'About',
|
|
884
|
+
keywords: ['company', 'team'],
|
|
885
|
+
metaImage: {_type: 'image', asset: {_ref: 'image-456', _type: 'reference'}},
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
_id: 'preview-post-3',
|
|
890
|
+
_type: 'post',
|
|
891
|
+
title: 'Content Marketing Trends for 2024',
|
|
892
|
+
slug: {current: 'content-marketing-trends'},
|
|
893
|
+
_updatedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
|
894
|
+
seo: {
|
|
895
|
+
title: 'Content Marketing Trends 2024',
|
|
896
|
+
description:
|
|
897
|
+
'Discover the latest content marketing trends and strategies to engage your audience effectively.',
|
|
898
|
+
keywords: ['content marketing', 'trends', 'strategy', 'engagement'],
|
|
899
|
+
metaImage: {_type: 'image', asset: {_ref: 'image-789', _type: 'reference'}},
|
|
900
|
+
openGraph: {
|
|
901
|
+
title: 'Content Marketing Trends 2024',
|
|
902
|
+
description: 'Latest trends in content marketing',
|
|
903
|
+
image: {_type: 'image', asset: {_ref: 'image-789', _type: 'reference'}, alt: 'Trends'},
|
|
904
|
+
type: 'article',
|
|
905
|
+
},
|
|
906
|
+
twitter: {
|
|
907
|
+
title: 'Content Marketing Trends',
|
|
908
|
+
description: 'Discover the latest trends',
|
|
909
|
+
card: 'summary',
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
_id: 'preview-post-4',
|
|
915
|
+
_type: 'product',
|
|
916
|
+
title: 'Pro Plan',
|
|
917
|
+
slug: {current: 'pro-plan'},
|
|
918
|
+
_updatedAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
|
919
|
+
seo: {
|
|
920
|
+
title: 'Pro',
|
|
921
|
+
keywords: ['pricing'],
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
_id: 'preview-page-2',
|
|
926
|
+
_type: 'page',
|
|
927
|
+
title: 'Contact',
|
|
928
|
+
slug: {current: 'contact'},
|
|
929
|
+
_updatedAt: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString(),
|
|
930
|
+
seo: {
|
|
931
|
+
openGraph: {
|
|
932
|
+
title: 'Get in Touch',
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
_id: 'preview-post-5',
|
|
938
|
+
_type: 'post',
|
|
939
|
+
title: 'Mobile Optimization Guide',
|
|
940
|
+
slug: {current: 'mobile-optimization'},
|
|
941
|
+
_updatedAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
|
942
|
+
seo: {
|
|
943
|
+
title: 'Mobile Optimization Guide: Best Practices for Responsive Design',
|
|
944
|
+
description:
|
|
945
|
+
'Complete guide to mobile optimization including responsive design, performance tips, and user experience best practices for modern web development.',
|
|
946
|
+
keywords: ['mobile', 'optimization', 'responsive', 'performance'],
|
|
947
|
+
metaImage: {_type: 'image', asset: {_ref: 'image-mobile', _type: 'reference'}},
|
|
948
|
+
openGraph: {
|
|
949
|
+
title: 'Mobile Optimization Best Practices',
|
|
950
|
+
description: 'Master mobile web optimization',
|
|
951
|
+
image: {_type: 'image', asset: {_ref: 'image-mobile', _type: 'reference'}, alt: 'Mobile'},
|
|
952
|
+
type: 'article',
|
|
953
|
+
},
|
|
954
|
+
twitter: {
|
|
955
|
+
title: 'Mobile Optimization Tips',
|
|
956
|
+
description: 'Responsive design best practices',
|
|
957
|
+
image: {_type: 'image', asset: {_ref: 'image-mobile', _type: 'reference'}, alt: 'Mobile'},
|
|
958
|
+
card: 'summary_large_image',
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
},
|
|
962
|
+
]
|
|
963
|
+
|
|
964
|
+
// Calculate health scores and return
|
|
965
|
+
return dummyDocs.map((doc) => ({
|
|
966
|
+
...doc,
|
|
967
|
+
health: calculateHealthScore(doc),
|
|
968
|
+
}))
|
|
763
969
|
}
|
|
764
970
|
|
|
765
971
|
const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
|
|
@@ -780,6 +986,7 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
|
|
|
780
986
|
loadingLicense,
|
|
781
987
|
loadingDocuments,
|
|
782
988
|
noDocuments,
|
|
989
|
+
previewMode = false,
|
|
783
990
|
}) => {
|
|
784
991
|
const client = useClient({apiVersion})
|
|
785
992
|
const [licenseStatus, setLicenseStatus] = useState<'loading' | 'valid' | 'invalid'>('loading')
|
|
@@ -800,6 +1007,12 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
|
|
|
800
1007
|
|
|
801
1008
|
const validateLicense = useCallback(
|
|
802
1009
|
async (forceRefresh = false) => {
|
|
1010
|
+
// Preview mode bypasses license validation
|
|
1011
|
+
if (previewMode) {
|
|
1012
|
+
setLicenseStatus('valid')
|
|
1013
|
+
return
|
|
1014
|
+
}
|
|
1015
|
+
|
|
803
1016
|
// No key provided
|
|
804
1017
|
if (!licenseKey) {
|
|
805
1018
|
setLicenseStatus('invalid')
|
|
@@ -854,13 +1067,13 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
|
|
|
854
1067
|
}
|
|
855
1068
|
},
|
|
856
1069
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
857
|
-
[licenseKey],
|
|
1070
|
+
[licenseKey, previewMode],
|
|
858
1071
|
)
|
|
859
1072
|
|
|
860
1073
|
useEffect(() => {
|
|
861
1074
|
validateLicense()
|
|
862
1075
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
863
|
-
}, [licenseKey])
|
|
1076
|
+
}, [licenseKey, previewMode])
|
|
864
1077
|
|
|
865
1078
|
const handleMouseEnterIssues = (el: HTMLDivElement | null, issues: string[]) => {
|
|
866
1079
|
if (!el) return
|
|
@@ -881,6 +1094,12 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
|
|
|
881
1094
|
try {
|
|
882
1095
|
setLoading(true)
|
|
883
1096
|
|
|
1097
|
+
// Use dummy data in preview mode
|
|
1098
|
+
if (previewMode) {
|
|
1099
|
+
setDocuments(generateDummyData())
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1102
|
+
|
|
884
1103
|
let groqQuery: string
|
|
885
1104
|
let params: Record<string, unknown> = {}
|
|
886
1105
|
|
|
@@ -932,7 +1151,16 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
|
|
|
932
1151
|
|
|
933
1152
|
fetchDocuments()
|
|
934
1153
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
935
|
-
}, [
|
|
1154
|
+
}, [
|
|
1155
|
+
client,
|
|
1156
|
+
customQuery,
|
|
1157
|
+
queryRequireSeo,
|
|
1158
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1159
|
+
JSON.stringify(queryTypes),
|
|
1160
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1161
|
+
JSON.stringify(titleField),
|
|
1162
|
+
previewMode,
|
|
1163
|
+
])
|
|
936
1164
|
|
|
937
1165
|
const uniqueDocumentTypes = useMemo(() => {
|
|
938
1166
|
const types = new Set(documents.map((doc) => doc._type))
|
|
@@ -1060,7 +1288,10 @@ export default defineConfig({
|
|
|
1060
1288
|
{/* Header */}
|
|
1061
1289
|
<PageHeader>
|
|
1062
1290
|
<PageTitle>
|
|
1063
|
-
|
|
1291
|
+
<span>
|
|
1292
|
+
{icon} {title}
|
|
1293
|
+
</span>
|
|
1294
|
+
{previewMode && <PreviewBadge>Preview Mode</PreviewBadge>}
|
|
1064
1295
|
</PageTitle>
|
|
1065
1296
|
<PageSubtitle>{description}</PageSubtitle>
|
|
1066
1297
|
</PageHeader>
|
|
@@ -1190,7 +1421,14 @@ export default defineConfig({
|
|
|
1190
1421
|
{typeColumnMode === 'text' ? (
|
|
1191
1422
|
<TypeText>{resolveTypeLabel(doc._type, typeLabels)}</TypeText>
|
|
1192
1423
|
) : (
|
|
1193
|
-
|
|
1424
|
+
(() => {
|
|
1425
|
+
const typeColor = getTypeColor(doc._type)
|
|
1426
|
+
return (
|
|
1427
|
+
<TypeBadge $bgColor={typeColor.bg} $textColor={typeColor.text}>
|
|
1428
|
+
{resolveTypeLabel(doc._type, typeLabels)}
|
|
1429
|
+
</TypeBadge>
|
|
1430
|
+
)
|
|
1431
|
+
})()
|
|
1194
1432
|
)}
|
|
1195
1433
|
</ColType>
|
|
1196
1434
|
)}
|
package/src/plugin.ts
CHANGED
|
@@ -214,6 +214,16 @@ export interface SeoFieldsPluginConfig {
|
|
|
214
214
|
docBadge?: (
|
|
215
215
|
doc: DocumentWithSeoHealth & Record<string, unknown>,
|
|
216
216
|
) => {label: string; bgColor?: string; textColor?: string; fontSize?: string} | undefined
|
|
217
|
+
/**
|
|
218
|
+
* Enable preview/demo mode to show dummy data.
|
|
219
|
+
* Useful for testing, documentation, or showcasing the dashboard.
|
|
220
|
+
* When enabled, displays realistic sample documents with various SEO scores.
|
|
221
|
+
* Defaults to `false`.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* previewMode: true
|
|
225
|
+
*/
|
|
226
|
+
previewMode?: boolean
|
|
217
227
|
}
|
|
218
228
|
}
|
|
219
229
|
|
|
@@ -242,6 +252,7 @@ interface ResolvedDashboardConfig {
|
|
|
242
252
|
loadingLicense: string | undefined
|
|
243
253
|
loadingDocuments: string | undefined
|
|
244
254
|
noDocuments: string | undefined
|
|
255
|
+
previewMode: boolean | undefined
|
|
245
256
|
}
|
|
246
257
|
|
|
247
258
|
const resolveDashboardConfig = (
|
|
@@ -269,6 +280,7 @@ const resolveDashboardConfig = (
|
|
|
269
280
|
loadingLicense: cfg?.content?.loadingLicense,
|
|
270
281
|
loadingDocuments: cfg?.content?.loadingDocuments,
|
|
271
282
|
noDocuments: cfg?.content?.noDocuments,
|
|
283
|
+
previewMode: cfg?.previewMode,
|
|
272
284
|
}
|
|
273
285
|
}
|
|
274
286
|
|
|
@@ -295,6 +307,7 @@ const seofields = definePlugin<SeoFieldsPluginConfig | void>((config = {}) => {
|
|
|
295
307
|
loadingLicense: dash.loadingLicense,
|
|
296
308
|
loadingDocuments: dash.loadingDocuments,
|
|
297
309
|
noDocuments: dash.noDocuments,
|
|
310
|
+
previewMode: dash.previewMode,
|
|
298
311
|
})
|
|
299
312
|
|
|
300
313
|
return {
|