sanity-plugin-seofields 1.2.1 → 1.2.3

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.1",
3
+ "version": "1.2.3",
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
- import {useClient} from 'sanity'
2
+ import {useClient, useWorkspace} 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'
@@ -50,16 +51,9 @@ const PageSubtitle = styled.p`
50
51
 
51
52
  const StatsGrid = styled.div`
52
53
  display: grid;
53
- grid-template-columns: repeat(6, 1fr);
54
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
54
55
  gap: 14px;
55
56
  margin-bottom: 20px;
56
-
57
- @media (max-width: 1100px) {
58
- grid-template-columns: repeat(3, 1fr);
59
- }
60
- @media (max-width: 600px) {
61
- grid-template-columns: repeat(2, 1fr);
62
- }
63
57
  `
64
58
 
65
59
  const StatCard = styled.div<{$accent?: string}>`
@@ -218,6 +212,14 @@ const TitleWrapper = styled.div`
218
212
  align-items: center;
219
213
  gap: 4px;
220
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;
221
223
  `
222
224
 
223
225
  const ColType = styled.div`
@@ -468,16 +470,66 @@ const ReloadButton = styled.button`
468
470
  `
469
471
 
470
472
  // Sub-component so useIntentLink can be called at the top level of a component (not inside .map)
471
- const DocTitleAnchor: React.FC<{id: string; type: string; children: React.ReactNode}> = ({
473
+ const DocTitleAnchor: React.FC<{
474
+ id: string
475
+ type: string
476
+ structureTool?: string
477
+ children: React.ReactNode
478
+ }> = ({id, type, structureTool, children}) => {
479
+ const {basePath} = useWorkspace()
480
+ const {onClick: intentOnClick, href: intentHref} = useIntentLink({intent: 'edit', params: {id, type}})
481
+ // When a specific structure tool name is provided, build a tool-scoped intent URL so that
482
+ // Sanity routes directly to that tool instead of letting the router pick the first match.
483
+ const href = structureTool
484
+ ? `${basePath}/${structureTool}/intent/edit/id=${id};type=${type}/`
485
+ : intentHref
486
+ const onClick = structureTool ? undefined : intentOnClick
487
+ return (
488
+ <DocTitleLink href={href} onClick={onClick} title="Open document">
489
+ {children}
490
+ </DocTitleLink>
491
+ )
492
+ }
493
+
494
+ // Wrapper that applies DocTitleLink styles to the ChildLink <a> rendered by Sanity's pane router
495
+ const PaneLinkWrapper = styled.span`
496
+ display: block;
497
+ min-width: 0;
498
+ overflow: hidden;
499
+
500
+ a {
501
+ font-size: 13px;
502
+ font-weight: 600;
503
+ color: #4f46e5;
504
+ white-space: nowrap;
505
+ overflow: hidden;
506
+ text-overflow: ellipsis;
507
+ text-decoration: none;
508
+ display: block;
509
+ transition: color 0.15s;
510
+
511
+ &:hover {
512
+ color: #4338ca;
513
+ text-decoration: underline;
514
+ }
515
+ }
516
+ `
517
+
518
+ // Sub-component for desk-structure split-pane navigation.
519
+ // Uses ChildLink from usePaneRouter to open the document editor to the right
520
+ // while keeping the SEO Health pane visible on the left.
521
+ const DocTitleAnchorPane: React.FC<{id: string; type: string; children: React.ReactNode}> = ({
472
522
  id,
473
523
  type,
474
524
  children,
475
525
  }) => {
476
- const {onClick, href} = useIntentLink({intent: 'edit', params: {id, type}})
526
+ const {ChildLink} = usePaneRouter()
477
527
  return (
478
- <DocTitleLink href={href} onClick={onClick} title="Open document">
479
- {children}
480
- </DocTitleLink>
528
+ <PaneLinkWrapper>
529
+ <ChildLink childId={id} childParameters={{type}}>
530
+ {children}
531
+ </ChildLink>
532
+ </PaneLinkWrapper>
481
533
  )
482
534
  }
483
535
 
@@ -824,6 +876,31 @@ export interface SeoHealthDashboardProps {
824
876
  * Defaults to `false`.
825
877
  */
826
878
  previewMode?: boolean
879
+ /**
880
+ * When `true`, clicking a document title opens the document editor as a split
881
+ * pane to the right, keeping the SEO Health pane visible on the left.
882
+ * This uses Sanity's pane router and requires the component to be rendered
883
+ * inside a desk-structure pane context (i.e. via `createSeoHealthPane`).
884
+ *
885
+ * When `false` (default), clicking navigates to the document via the standard
886
+ * intent-link system (full navigation).
887
+ *
888
+ * This is set to `true` automatically by `createSeoHealthPane`.
889
+ */
890
+ openInPane?: boolean
891
+ /**
892
+ * The `name` of the Sanity structure tool that contains the monitored documents.
893
+ * When provided, clicking a document title navigates directly to that tool's
894
+ * intent URL (`/{basePath}/{structureTool}/intent/edit/id=…;type=…/`) instead of
895
+ * using the generic intent resolver, which always picks the first registered tool.
896
+ *
897
+ * Required when you have multiple structure tools and the documents live in a
898
+ * non-default one (e.g. `name: 'common'`).
899
+ *
900
+ * @example
901
+ * structureTool: 'common'
902
+ */
903
+ structureTool?: string
827
904
  }
828
905
 
829
906
  /**
@@ -987,6 +1064,8 @@ const SeoHealthDashboard: React.FC<SeoHealthDashboardProps> = ({
987
1064
  loadingDocuments,
988
1065
  noDocuments,
989
1066
  previewMode = false,
1067
+ openInPane = false,
1068
+ structureTool,
990
1069
  }) => {
991
1070
  const client = useClient({apiVersion})
992
1071
  const [licenseStatus, setLicenseStatus] = useState<'loading' | 'valid' | 'invalid'>('loading')
@@ -1402,12 +1481,18 @@ export default defineConfig({
1402
1481
  <TableRow key={doc._id}>
1403
1482
  <ColTitle>
1404
1483
  <TitleWrapper>
1405
- <div>
1406
- <DocTitleAnchor id={doc._id} type={doc._type}>
1407
- {doc.title || 'Untitled'}
1408
- </DocTitleAnchor>
1484
+ <TitleCell>
1485
+ {openInPane ? (
1486
+ <DocTitleAnchorPane id={doc._id} type={doc._type}>
1487
+ {doc.title || 'Untitled'}
1488
+ </DocTitleAnchorPane>
1489
+ ) : (
1490
+ <DocTitleAnchor id={doc._id} type={doc._type} structureTool={structureTool}>
1491
+ {doc.title || 'Untitled'}
1492
+ </DocTitleAnchor>
1493
+ )}
1409
1494
  {showDocumentId && <DocId>{doc._id}</DocId>}
1410
- </div>
1495
+ </TitleCell>
1411
1496
  {docBadge && (
1412
1497
  <DocBadgeRenderer
1413
1498
  doc={doc as DocumentWithSeoHealth & Record<string, unknown>}
@@ -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
+ * The `name` of the Sanity structure tool that contains the monitored documents.
219
+ * Required when you have multiple structure tools and the documents live in a
220
+ * non-default one. Clicking a title will navigate to
221
+ * `/{basePath}/{structureTool}/intent/edit/…` directly.
222
+ *
223
+ * @example
224
+ * structureTool: 'common'
225
+ */
226
+ structureTool?: string
217
227
  /**
218
228
  * Enable preview/demo mode to show dummy data.
219
229
  * Useful for testing, documentation, or showcasing the dashboard.
@@ -253,6 +263,7 @@ interface ResolvedDashboardConfig {
253
263
  loadingDocuments: string | undefined
254
264
  noDocuments: string | undefined
255
265
  previewMode: boolean | undefined
266
+ structureTool: string | undefined
256
267
  }
257
268
 
258
269
  const resolveDashboardConfig = (
@@ -281,6 +292,7 @@ const resolveDashboardConfig = (
281
292
  loadingDocuments: cfg?.content?.loadingDocuments,
282
293
  noDocuments: cfg?.content?.noDocuments,
283
294
  previewMode: cfg?.previewMode,
295
+ structureTool: cfg?.structureTool,
284
296
  }
285
297
  }
286
298
 
@@ -308,6 +320,7 @@ const seofields = definePlugin<SeoFieldsPluginConfig | void>((config = {}) => {
308
320
  loadingDocuments: dash.loadingDocuments,
309
321
  noDocuments: dash.noDocuments,
310
322
  previewMode: dash.previewMode,
323
+ structureTool: dash.structureTool,
311
324
  })
312
325
 
313
326
  return {