sanity-plugin-seofields 1.2.0 → 1.2.2

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.2",
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",
@@ -1,6 +1,7 @@
1
1
  import React, {useCallback, useEffect, useMemo, useState} from 'react'
2
2
  import {useClient} from 'sanity'
3
3
  import {useIntentLink} from 'sanity/router'
4
+ import {usePaneRouter} from 'sanity/structure'
4
5
  import styled, {keyframes} from 'styled-components'
5
6
 
6
7
  import {DocumentWithSeoHealth, SeoHealthMetrics} from '../types'
@@ -24,6 +25,22 @@ const PageTitle = styled.h1`
24
25
  font-weight: 700;
25
26
  color: #111827;
26
27
  letter-spacing: -0.3px;
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 10px;
31
+ `
32
+
33
+ const PreviewBadge = styled.span`
34
+ display: inline-block;
35
+ background: #fef3c7;
36
+ color: #92400e;
37
+ font-size: 11px;
38
+ font-weight: 600;
39
+ padding: 4px 8px;
40
+ border-radius: 4px;
41
+ text-transform: uppercase;
42
+ letter-spacing: 0.5px;
43
+ margin-left: 8px;
27
44
  `
28
45
 
29
46
  const PageSubtitle = styled.p`
@@ -34,16 +51,9 @@ const PageSubtitle = styled.p`
34
51
 
35
52
  const StatsGrid = styled.div`
36
53
  display: grid;
37
- grid-template-columns: repeat(6, 1fr);
54
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
38
55
  gap: 14px;
39
56
  margin-bottom: 20px;
40
-
41
- @media (max-width: 1100px) {
42
- grid-template-columns: repeat(3, 1fr);
43
- }
44
- @media (max-width: 600px) {
45
- grid-template-columns: repeat(2, 1fr);
46
- }
47
57
  `
48
58
 
49
59
  const StatCard = styled.div<{$accent?: string}>`
@@ -202,6 +212,14 @@ const TitleWrapper = styled.div`
202
212
  align-items: center;
203
213
  gap: 4px;
204
214
  flex-wrap: wrap;
215
+ min-width: 0;
216
+ `
217
+
218
+ /* Constrains the title + doc-id block so text-overflow works inside flex */
219
+ const TitleCell = styled.div`
220
+ min-width: 0;
221
+ overflow: hidden;
222
+ flex: 1;
205
223
  `
206
224
 
207
225
  const ColType = styled.div`
@@ -245,14 +263,14 @@ const DocId = styled.div`
245
263
  text-overflow: ellipsis;
246
264
  `
247
265
 
248
- const TypeBadge = styled.span`
266
+ const TypeBadge = styled.span<{$bgColor?: string; $textColor?: string}>`
249
267
  display: inline-block;
250
268
  padding: 3px 8px;
251
269
  border-radius: 5px;
252
270
  font-size: 11px;
253
271
  font-weight: 500;
254
- background: #ede9fe;
255
- color: #5b21b6;
272
+ background: ${(p) => p.$bgColor || '#ede9fe'};
273
+ color: ${(p) => p.$textColor || '#5b21b6'};
256
274
  `
257
275
 
258
276
  const TypeText = styled.span`
@@ -465,6 +483,48 @@ const DocTitleAnchor: React.FC<{id: string; type: string; children: React.ReactN
465
483
  )
466
484
  }
467
485
 
486
+ // Wrapper that applies DocTitleLink styles to the ChildLink <a> rendered by Sanity's pane router
487
+ const PaneLinkWrapper = styled.span`
488
+ display: block;
489
+ min-width: 0;
490
+ overflow: hidden;
491
+
492
+ a {
493
+ font-size: 13px;
494
+ font-weight: 600;
495
+ color: #4f46e5;
496
+ white-space: nowrap;
497
+ overflow: hidden;
498
+ text-overflow: ellipsis;
499
+ text-decoration: none;
500
+ display: block;
501
+ transition: color 0.15s;
502
+
503
+ &:hover {
504
+ color: #4338ca;
505
+ text-decoration: underline;
506
+ }
507
+ }
508
+ `
509
+
510
+ // Sub-component for desk-structure split-pane navigation.
511
+ // Uses ChildLink from usePaneRouter to open the document editor to the right
512
+ // while keeping the SEO Health pane visible on the left.
513
+ const DocTitleAnchorPane: React.FC<{id: string; type: string; children: React.ReactNode}> = ({
514
+ id,
515
+ type,
516
+ children,
517
+ }) => {
518
+ const {ChildLink} = usePaneRouter()
519
+ return (
520
+ <PaneLinkWrapper>
521
+ <ChildLink childId={id} childParameters={{type}}>
522
+ {children}
523
+ </ChildLink>
524
+ </PaneLinkWrapper>
525
+ )
526
+ }
527
+
468
528
  // Sub-component to safely call docBadge outside a .map expression
469
529
  const DocBadgeRenderer: React.FC<{
470
530
  doc: DocumentWithSeoHealth & Record<string, unknown>
@@ -509,6 +569,47 @@ const EmptyState = styled.div`
509
569
  font-size: 13px;
510
570
  `
511
571
 
572
+ /**
573
+ * Color palette for dynamic document type badges
574
+ * Colors are randomly assigned based on type hash for visual variety
575
+ * while maintaining consistency across sessions
576
+ */
577
+ const TYPE_COLOR_PALETTE: Array<{bg: string; text: string}> = [
578
+ {bg: '#dbeafe', text: '#0c4a6e'}, // Blue
579
+ {bg: '#dcfce7', text: '#14532d'}, // Green
580
+ {bg: '#fce7f3', text: '#500724'}, // Pink
581
+ {bg: '#fed7aa', text: '#7c2d12'}, // Orange
582
+ {bg: '#e9d5ff', text: '#581c87'}, // Purple
583
+ {bg: '#f3e8ff', text: '#3f0f5c'}, // Deep Purple
584
+ {bg: '#ccfbf1', text: '#134e4a'}, // Teal
585
+ {bg: '#ddd6fe', text: '#3730a3'}, // Indigo
586
+ {bg: '#fca5a5', text: '#7f1d1d'}, // Red
587
+ {bg: '#a7f3d0', text: '#065f46'}, // Emerald
588
+ {bg: '#fbbf24', text: '#78350f'}, // Amber
589
+ {bg: '#c4b5fd', text: '#3b0764'}, // Violet
590
+ {bg: '#f0fdf4', text: '#15803d'}, // Light Green
591
+ {bg: '#fef2f2', text: '#991b1b'}, // Light Red
592
+ {bg: '#f5f3ff', text: '#5b21b6'}, // Light Purple
593
+ {bg: '#fffbeb', text: '#92400e'}, // Light Amber
594
+ ]
595
+
596
+ /**
597
+ * Get dynamic color for a document type based on type name hash
598
+ * Same type always gets the same color, but assignment is not fixed
599
+ */
600
+ const getTypeColor = (type: string): {bg: string; text: string} => {
601
+ // Generate consistent hash from type string using simple arithmetic
602
+ let hash = 0
603
+ for (let i = 0; i < type.length; i += 1) {
604
+ const char = type.charCodeAt(i)
605
+ hash = Math.abs(hash * 31 + char)
606
+ }
607
+
608
+ // Use modulo to get index within palette range
609
+ const colorIndex = hash % TYPE_COLOR_PALETTE.length
610
+ return TYPE_COLOR_PALETTE[colorIndex]
611
+ }
612
+
512
613
  const getStatusCategory = (score: number): SeoHealthMetrics['status'] => {
513
614
  if (score >= 80) return 'excellent'
514
615
  if (score >= 60) return 'good'
@@ -760,6 +861,167 @@ export interface SeoHealthDashboardProps {
760
861
  * Defaults to `"No documents found"`.
761
862
  */
762
863
  noDocuments?: React.ReactNode
864
+ /**
865
+ * Enable preview/demo mode to show dummy data.
866
+ * Useful for testing, documentation, or showcasing the dashboard.
867
+ * When enabled, displays realistic sample documents with various SEO scores.
868
+ * Defaults to `false`.
869
+ */
870
+ previewMode?: boolean
871
+ /**
872
+ * When `true`, clicking a document title opens the document editor as a split
873
+ * pane to the right, keeping the SEO Health pane visible on the left.
874
+ * This uses Sanity's pane router and requires the component to be rendered
875
+ * inside a desk-structure pane context (i.e. via `createSeoHealthPane`).
876
+ *
877
+ * When `false` (default), clicking navigates to the document via the standard
878
+ * intent-link system (full navigation).
879
+ *
880
+ * This is set to `true` automatically by `createSeoHealthPane`.
881
+ */
882
+ openInPane?: boolean
883
+ }
884
+
885
+ /**
886
+ * Generate dummy data for preview mode showing various SEO health scenarios
887
+ */
888
+ const generateDummyData = (): DocumentWithSeoHealth[] => {
889
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
890
+ const dummyDocs: any[] = [
891
+ {
892
+ _id: 'preview-post-1',
893
+ _type: 'post',
894
+ title: 'Getting Started with SEO Best Practices',
895
+ slug: {current: 'getting-started-seo'},
896
+ _updatedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
897
+ seo: {
898
+ title: 'Getting Started with SEO Best Practices | My Blog',
899
+ description:
900
+ 'Learn the fundamentals of SEO optimization to improve your website visibility and search rankings.',
901
+ keywords: ['seo', 'best practices', 'optimization'],
902
+ metaImage: {_type: 'image', asset: {_ref: 'image-123', _type: 'reference'}},
903
+ openGraph: {
904
+ title: 'SEO Best Practices Guide',
905
+ description: 'Master SEO optimization',
906
+ image: {_type: 'image', asset: {_ref: 'image-123', _type: 'reference'}, alt: 'SEO Guide'},
907
+ type: 'article',
908
+ },
909
+ twitter: {
910
+ title: 'SEO Best Practices',
911
+ description: 'Learn SEO optimization',
912
+ image: {_type: 'image', asset: {_ref: 'image-123', _type: 'reference'}, alt: 'Guide'},
913
+ card: 'summary_large_image',
914
+ },
915
+ },
916
+ },
917
+ {
918
+ _id: 'preview-post-2',
919
+ _type: 'post',
920
+ title: 'Advanced Analytics Strategy',
921
+ slug: {current: 'advanced-analytics'},
922
+ _updatedAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
923
+ seo: {
924
+ title: 'Advanced Analytics',
925
+ description: 'Strategy tips',
926
+ keywords: ['analytics', 'data'],
927
+ openGraph: {
928
+ title: 'Analytics Guide',
929
+ },
930
+ },
931
+ },
932
+ {
933
+ _id: 'preview-page-1',
934
+ _type: 'page',
935
+ title: 'About Us',
936
+ slug: {current: 'about'},
937
+ _updatedAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
938
+ seo: {
939
+ title: 'About',
940
+ keywords: ['company', 'team'],
941
+ metaImage: {_type: 'image', asset: {_ref: 'image-456', _type: 'reference'}},
942
+ },
943
+ },
944
+ {
945
+ _id: 'preview-post-3',
946
+ _type: 'post',
947
+ title: 'Content Marketing Trends for 2024',
948
+ slug: {current: 'content-marketing-trends'},
949
+ _updatedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
950
+ seo: {
951
+ title: 'Content Marketing Trends 2024',
952
+ description:
953
+ 'Discover the latest content marketing trends and strategies to engage your audience effectively.',
954
+ keywords: ['content marketing', 'trends', 'strategy', 'engagement'],
955
+ metaImage: {_type: 'image', asset: {_ref: 'image-789', _type: 'reference'}},
956
+ openGraph: {
957
+ title: 'Content Marketing Trends 2024',
958
+ description: 'Latest trends in content marketing',
959
+ image: {_type: 'image', asset: {_ref: 'image-789', _type: 'reference'}, alt: 'Trends'},
960
+ type: 'article',
961
+ },
962
+ twitter: {
963
+ title: 'Content Marketing Trends',
964
+ description: 'Discover the latest trends',
965
+ card: 'summary',
966
+ },
967
+ },
968
+ },
969
+ {
970
+ _id: 'preview-post-4',
971
+ _type: 'product',
972
+ title: 'Pro Plan',
973
+ slug: {current: 'pro-plan'},
974
+ _updatedAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(),
975
+ seo: {
976
+ title: 'Pro',
977
+ keywords: ['pricing'],
978
+ },
979
+ },
980
+ {
981
+ _id: 'preview-page-2',
982
+ _type: 'page',
983
+ title: 'Contact',
984
+ slug: {current: 'contact'},
985
+ _updatedAt: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString(),
986
+ seo: {
987
+ openGraph: {
988
+ title: 'Get in Touch',
989
+ },
990
+ },
991
+ },
992
+ {
993
+ _id: 'preview-post-5',
994
+ _type: 'post',
995
+ title: 'Mobile Optimization Guide',
996
+ slug: {current: 'mobile-optimization'},
997
+ _updatedAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
998
+ seo: {
999
+ title: 'Mobile Optimization Guide: Best Practices for Responsive Design',
1000
+ description:
1001
+ 'Complete guide to mobile optimization including responsive design, performance tips, and user experience best practices for modern web development.',
1002
+ keywords: ['mobile', 'optimization', 'responsive', 'performance'],
1003
+ metaImage: {_type: 'image', asset: {_ref: 'image-mobile', _type: 'reference'}},
1004
+ openGraph: {
1005
+ title: 'Mobile Optimization Best Practices',
1006
+ description: 'Master mobile web optimization',
1007
+ image: {_type: 'image', asset: {_ref: 'image-mobile', _type: 'reference'}, alt: 'Mobile'},
1008
+ type: 'article',
1009
+ },
1010
+ twitter: {
1011
+ title: 'Mobile Optimization Tips',
1012
+ description: 'Responsive design best practices',
1013
+ image: {_type: 'image', asset: {_ref: 'image-mobile', _type: 'reference'}, alt: 'Mobile'},
1014
+ card: 'summary_large_image',
1015
+ },
1016
+ },
1017
+ },
1018
+ ]
1019
+
1020
+ // Calculate health scores and return
1021
+ return dummyDocs.map((doc) => ({
1022
+ ...doc,
1023
+ health: calculateHealthScore(doc),
1024
+ }))
763
1025
  }
764
1026
 
765
1027
  const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
@@ -780,6 +1042,8 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
780
1042
  loadingLicense,
781
1043
  loadingDocuments,
782
1044
  noDocuments,
1045
+ previewMode = false,
1046
+ openInPane = false,
783
1047
  }) => {
784
1048
  const client = useClient({apiVersion})
785
1049
  const [licenseStatus, setLicenseStatus] = useState<'loading' | 'valid' | 'invalid'>('loading')
@@ -800,6 +1064,12 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
800
1064
 
801
1065
  const validateLicense = useCallback(
802
1066
  async (forceRefresh = false) => {
1067
+ // Preview mode bypasses license validation
1068
+ if (previewMode) {
1069
+ setLicenseStatus('valid')
1070
+ return
1071
+ }
1072
+
803
1073
  // No key provided
804
1074
  if (!licenseKey) {
805
1075
  setLicenseStatus('invalid')
@@ -854,13 +1124,13 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
854
1124
  }
855
1125
  },
856
1126
  // eslint-disable-next-line react-hooks/exhaustive-deps
857
- [licenseKey],
1127
+ [licenseKey, previewMode],
858
1128
  )
859
1129
 
860
1130
  useEffect(() => {
861
1131
  validateLicense()
862
1132
  // eslint-disable-next-line react-hooks/exhaustive-deps
863
- }, [licenseKey])
1133
+ }, [licenseKey, previewMode])
864
1134
 
865
1135
  const handleMouseEnterIssues = (el: HTMLDivElement | null, issues: string[]) => {
866
1136
  if (!el) return
@@ -881,6 +1151,12 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
881
1151
  try {
882
1152
  setLoading(true)
883
1153
 
1154
+ // Use dummy data in preview mode
1155
+ if (previewMode) {
1156
+ setDocuments(generateDummyData())
1157
+ return
1158
+ }
1159
+
884
1160
  let groqQuery: string
885
1161
  let params: Record<string, unknown> = {}
886
1162
 
@@ -932,7 +1208,16 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
932
1208
 
933
1209
  fetchDocuments()
934
1210
  // eslint-disable-next-line react-hooks/exhaustive-deps
935
- }, [client, customQuery, queryRequireSeo, JSON.stringify(queryTypes), JSON.stringify(titleField)])
1211
+ }, [
1212
+ client,
1213
+ customQuery,
1214
+ queryRequireSeo,
1215
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1216
+ JSON.stringify(queryTypes),
1217
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1218
+ JSON.stringify(titleField),
1219
+ previewMode,
1220
+ ])
936
1221
 
937
1222
  const uniqueDocumentTypes = useMemo(() => {
938
1223
  const types = new Set(documents.map((doc) => doc._type))
@@ -1060,7 +1345,10 @@ export default defineConfig({
1060
1345
  {/* Header */}
1061
1346
  <PageHeader>
1062
1347
  <PageTitle>
1063
- {icon} {title}
1348
+ <span>
1349
+ {icon} {title}
1350
+ </span>
1351
+ {previewMode && <PreviewBadge>Preview Mode</PreviewBadge>}
1064
1352
  </PageTitle>
1065
1353
  <PageSubtitle>{description}</PageSubtitle>
1066
1354
  </PageHeader>
@@ -1171,12 +1459,18 @@ export default defineConfig({
1171
1459
  <TableRow key={doc._id}>
1172
1460
  <ColTitle>
1173
1461
  <TitleWrapper>
1174
- <div>
1175
- <DocTitleAnchor id={doc._id} type={doc._type}>
1176
- {doc.title || 'Untitled'}
1177
- </DocTitleAnchor>
1462
+ <TitleCell>
1463
+ {openInPane ? (
1464
+ <DocTitleAnchorPane id={doc._id} type={doc._type}>
1465
+ {doc.title || 'Untitled'}
1466
+ </DocTitleAnchorPane>
1467
+ ) : (
1468
+ <DocTitleAnchor id={doc._id} type={doc._type}>
1469
+ {doc.title || 'Untitled'}
1470
+ </DocTitleAnchor>
1471
+ )}
1178
1472
  {showDocumentId && <DocId>{doc._id}</DocId>}
1179
- </div>
1473
+ </TitleCell>
1180
1474
  {docBadge && (
1181
1475
  <DocBadgeRenderer
1182
1476
  doc={doc as DocumentWithSeoHealth & Record<string, unknown>}
@@ -1190,7 +1484,14 @@ export default defineConfig({
1190
1484
  {typeColumnMode === 'text' ? (
1191
1485
  <TypeText>{resolveTypeLabel(doc._type, typeLabels)}</TypeText>
1192
1486
  ) : (
1193
- <TypeBadge>{resolveTypeLabel(doc._type, typeLabels)}</TypeBadge>
1487
+ (() => {
1488
+ const typeColor = getTypeColor(doc._type)
1489
+ return (
1490
+ <TypeBadge $bgColor={typeColor.bg} $textColor={typeColor.text}>
1491
+ {resolveTypeLabel(doc._type, typeLabels)}
1492
+ </TypeBadge>
1493
+ )
1494
+ })()
1194
1495
  )}
1195
1496
  </ColType>
1196
1497
  )}
@@ -0,0 +1,81 @@
1
+ import React from 'react'
2
+ import type {ComponentBuilder, StructureBuilder} from 'sanity/structure'
3
+
4
+ import SeoHealthDashboard, {SeoHealthDashboardProps} from './SeoHealthDashboard'
5
+
6
+ /**
7
+ * Options accepted by `createSeoHealthPane`.
8
+ * All props from `SeoHealthDashboardProps` are supported.
9
+ *
10
+ * `licenseKey` is **required** — the dashboard will not render without it.
11
+ */
12
+ export interface SeoHealthPaneOptions extends Omit<SeoHealthDashboardProps, 'customQuery'> {
13
+ /** Required license key (format: `SEOF-XXXX-XXXX-XXXX`). */
14
+ licenseKey: string
15
+ /**
16
+ * A fully custom GROQ query used to fetch documents for the dashboard.
17
+ * The query must return documents with at least: `_id`, `_type`, `title`, `seo`, `_updatedAt`.
18
+ *
19
+ * Takes precedence over `queryTypes` when both are provided.
20
+ *
21
+ * @example
22
+ * query: `*[_type in ["post","page"] && defined(seo)]{ _id, _type, title, slug, seo, _updatedAt }`
23
+ */
24
+ query?: string
25
+ }
26
+
27
+ // function isStructureBuilder(arg: unknown): arg is StructureBuilder {
28
+ // return (
29
+ // arg !== null &&
30
+ // typeof arg === 'object' &&
31
+ // typeof (arg as StructureBuilder).component === 'function' &&
32
+ // typeof (arg as StructureBuilder).document === 'function'
33
+ // )
34
+ // }
35
+
36
+ /**
37
+ * Creates a desk-structure pane for the SEO Health Dashboard.
38
+ *
39
+ * Returns a **`ComponentBuilder`** with a built-in `.child()` resolver so that
40
+ * clicking any document row opens the document editor as a split pane to the right.
41
+ *
42
+ * Use it **directly** as the `.child()` value — do **not** wrap it in `S.component()`.
43
+ *
44
+ * ```ts
45
+ * // sanity.config.ts
46
+ * structure: (S) =>
47
+ * S.list().items([
48
+ * S.listItem()
49
+ * .title('SEO Health')
50
+ * .child(
51
+ * createSeoHealthPane(S, {
52
+ * licenseKey: 'SEOF-XXXX-XXXX-XXXX',
53
+ * query: `*[_type == "post" && defined(seo)]{ _id, _type, title, slug, seo, _updatedAt }`,
54
+ * })
55
+ * ),
56
+ * ])
57
+ * ```
58
+ */
59
+ export function createSeoHealthPane(
60
+ optionsOrS: StructureBuilder,
61
+ optionsWhenS: SeoHealthPaneOptions,
62
+ ): ComponentBuilder {
63
+ // ── Two-arg form: structure builder passed as first arg ──────────────────
64
+ const S = optionsOrS
65
+ const {query, openInPane = true, title: paneTitle, ...rest} = optionsWhenS ?? {}
66
+
67
+ const SeoHealthPane: React.FC = () => (
68
+ <SeoHealthDashboard customQuery={query} openInPane={openInPane} title={paneTitle} {...rest} />
69
+ )
70
+ SeoHealthPane.displayName = 'SeoHealthPane'
71
+
72
+ // Wire up the child resolver so ChildLink URLs resolve to the document editor
73
+ return (S.component(SeoHealthPane) as ComponentBuilder)
74
+ .title(paneTitle ?? 'SEO Health')
75
+ .child((docId: string, {params}: {params: Record<string, string | undefined>}) => {
76
+ const builder = S.document().documentId(docId)
77
+ return params?.type ? builder.schemaType(params.type) : builder
78
+ })
79
+ }
80
+
81
+ export default createSeoHealthPane
package/src/index.ts CHANGED
@@ -19,6 +19,8 @@ export {default as twitterSchema} from './schemas/types/twitter'
19
19
  // Export dashboard components and types
20
20
  export {default as SeoHealthDashboard} from './components/SeoHealthDashboard'
21
21
  export {default as SeoHealthTool} from './components/SeoHealthTool'
22
+ export {createSeoHealthPane} from './components/SeoHealthPane'
23
+ export type {SeoHealthPaneOptions} from './components/SeoHealthPane'
22
24
 
23
25
  // Export types
24
26
  export type {DocumentWithSeoHealth, SeoHealthMetrics, SeoHealthStatus} from './types'
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 {