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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-seofields",
3
- "version": "1.2.0",
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
- }, [client, customQuery, queryRequireSeo, JSON.stringify(queryTypes), JSON.stringify(titleField)])
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
- {icon} {title}
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
- <TypeBadge>{resolveTypeLabel(doc._type, typeLabels)}</TypeBadge>
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 {