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/README.md CHANGED
@@ -21,6 +21,7 @@ A comprehensive Sanity Studio v3 plugin to manage SEO fields like meta titles, d
21
21
  - ✅ **Validation**: Built-in character limits and best practices
22
22
  - 🎛️ **Field Visibility**: Hide sitewide fields on specific content types
23
23
  - 📊 **SEO Health Dashboard**: Studio-wide overview of SEO completeness with scores, issue highlights, and direct document links
24
+ - 🗂️ **Desk Structure Pane**: Embed the dashboard inside the Structure tool with `createSeoHealthPane` — supports split-pane document editing
24
25
 
25
26
  ## 📦 Installation
26
27
 
@@ -531,6 +532,65 @@ seofields({
531
532
 
532
533
  > **Scoring logic:** each field earns its full points when a non-empty value is present, zero when missing. `query.groq` lets you control exactly which documents are included in the audit.
533
534
 
535
+ ## 🗂️ Desk Structure Pane
536
+
537
+ Embed the SEO Health Dashboard **directly inside the Structure tool** as a pane with split-pane document editing — clicking any row opens the document editor to the right.
538
+
539
+ ### Import
540
+
541
+ ```typescript
542
+ import {createSeoHealthPane} from 'sanity-plugin-seofields'
543
+ ```
544
+
545
+ ### Usage
546
+
547
+ `createSeoHealthPane(S, options)` requires both arguments: Sanity's structure builder `S` and an options object with a required `licenseKey`. It returns a **`ComponentBuilder`** — use it **directly** as the `.child()` value.
548
+
549
+ > ⚠️ **Do NOT wrap in `S.component()`.** The function already calls `S.component()` internally. Wrapping it again causes: _"component is required for component structure item"_.
550
+
551
+ ```typescript
552
+ // sanity.config.ts
553
+ import {defineConfig} from 'sanity'
554
+ import {structureTool} from 'sanity/structure'
555
+ import seofields, {createSeoHealthPane} from 'sanity-plugin-seofields'
556
+
557
+ export default defineConfig({
558
+ plugins: [
559
+ seofields({healthDashboard: false}), // optional: hide the top-level tool tab
560
+ structureTool({
561
+ structure: (S) =>
562
+ S.list()
563
+ .title('Content')
564
+ .items([
565
+ S.documentTypeListItem('post').title('Posts'),
566
+ S.divider(),
567
+ S.listItem()
568
+ .title('SEO Health')
569
+ .child(
570
+ createSeoHealthPane(S, {
571
+ licenseKey: 'SEOF-XXXX-XXXX-XXXX',
572
+ query: `*[_type == "post" && defined(seo)]{
573
+ _id, _type, title, slug, seo, _updatedAt
574
+ }`,
575
+ title: 'Posts SEO Health',
576
+ }),
577
+ ),
578
+ ]),
579
+ }),
580
+ ],
581
+ })
582
+ ```
583
+
584
+ ### `createSeoHealthPane` options
585
+
586
+ | Option | Type | Default | Description |
587
+ | ------------ | --------- | -------------- | --------------------------------------------------------------------- |
588
+ | `licenseKey` | `string` | **required** | License key (format `SEOF-XXXX-XXXX-XXXX`). |
589
+ | `query` | `string` | — | GROQ query. Must return `_id`, `_type`, `title`, `seo`, `_updatedAt`. |
590
+ | `title` | `string` | `'SEO Health'` | Pane title shown in breadcrumb |
591
+ | `openInPane` | `boolean` | `true` | Enable row links that open the document editor as a pane to the right. |
592
+ | `...rest` | — | — | All other `SeoHealthDashboardProps` |
593
+
534
594
  ## 🌐 Frontend Integration
535
595
 
536
596
  ### Next.js Example
package/dist/index.d.mts CHANGED
@@ -1,14 +1,44 @@
1
+ import type {ComponentBuilder} from 'sanity/structure'
1
2
  import {JSX} from 'react'
2
3
  import {ObjectDefinition} from 'sanity'
3
4
  import {Plugin as Plugin_2} from 'sanity'
4
5
  import {PreviewConfig} from 'sanity'
5
6
  import {default as React_2} from 'react'
6
7
  import {SchemaTypeDefinition} from 'sanity'
8
+ import type {StructureBuilder} from 'sanity/structure'
7
9
 
8
10
  export declare type AllFieldKeys = SeoFieldKeys | openGraphFieldKeys | twitterFieldKeys
9
11
 
10
12
  export declare function allSchemas(config?: SeoFieldsPluginConfig): SchemaTypeDefinition[]
11
13
 
14
+ /**
15
+ * Creates a desk-structure pane for the SEO Health Dashboard.
16
+ *
17
+ * Returns a **`ComponentBuilder`** with a built-in `.child()` resolver so that
18
+ * clicking any document row opens the document editor as a split pane to the right.
19
+ *
20
+ * Use it **directly** as the `.child()` value — do **not** wrap it in `S.component()`.
21
+ *
22
+ * ```ts
23
+ * // sanity.config.ts
24
+ * structure: (S) =>
25
+ * S.list().items([
26
+ * S.listItem()
27
+ * .title('SEO Health')
28
+ * .child(
29
+ * createSeoHealthPane(S, {
30
+ * licenseKey: 'SEOF-XXXX-XXXX-XXXX',
31
+ * query: `*[_type == "post" && defined(seo)]{ _id, _type, title, slug, seo, _updatedAt }`,
32
+ * })
33
+ * ),
34
+ * ])
35
+ * ```
36
+ */
37
+ export declare function createSeoHealthPane(
38
+ optionsOrS: StructureBuilder,
39
+ optionsWhenS: SeoHealthPaneOptions,
40
+ ): ComponentBuilder
41
+
12
42
  export declare interface DocumentWithSeoHealth {
13
43
  _id: string
14
44
  _type: string
@@ -308,6 +338,16 @@ export declare interface SeoFieldsPluginConfig {
308
338
  fontSize?: string
309
339
  }
310
340
  | undefined
341
+ /**
342
+ * The `name` of the Sanity structure tool that contains the monitored documents.
343
+ * Required when you have multiple structure tools and the documents live in a
344
+ * non-default one. Clicking a title will navigate to
345
+ * `/{basePath}/{structureTool}/intent/edit/…` directly.
346
+ *
347
+ * @example
348
+ * structureTool: 'common'
349
+ */
350
+ structureTool?: string
311
351
  /**
312
352
  * Enable preview/demo mode to show dummy data.
313
353
  * Useful for testing, documentation, or showcasing the dashboard.
@@ -430,6 +470,31 @@ declare interface SeoHealthDashboardProps {
430
470
  * Defaults to `false`.
431
471
  */
432
472
  previewMode?: boolean
473
+ /**
474
+ * When `true`, clicking a document title opens the document editor as a split
475
+ * pane to the right, keeping the SEO Health pane visible on the left.
476
+ * This uses Sanity's pane router and requires the component to be rendered
477
+ * inside a desk-structure pane context (i.e. via `createSeoHealthPane`).
478
+ *
479
+ * When `false` (default), clicking navigates to the document via the standard
480
+ * intent-link system (full navigation).
481
+ *
482
+ * This is set to `true` automatically by `createSeoHealthPane`.
483
+ */
484
+ openInPane?: boolean
485
+ /**
486
+ * The `name` of the Sanity structure tool that contains the monitored documents.
487
+ * When provided, clicking a document title navigates directly to that tool's
488
+ * intent URL (`/{basePath}/{structureTool}/intent/edit/id=…;type=…/`) instead of
489
+ * using the generic intent resolver, which always picks the first registered tool.
490
+ *
491
+ * Required when you have multiple structure tools and the documents live in a
492
+ * non-default one (e.g. `name: 'common'`).
493
+ *
494
+ * @example
495
+ * structureTool: 'common'
496
+ */
497
+ structureTool?: string
433
498
  }
434
499
 
435
500
  export declare interface SeoHealthMetrics {
@@ -438,6 +503,27 @@ export declare interface SeoHealthMetrics {
438
503
  issues: string[]
439
504
  }
440
505
 
506
+ /**
507
+ * Options accepted by `createSeoHealthPane`.
508
+ * All props from `SeoHealthDashboardProps` are supported.
509
+ *
510
+ * `licenseKey` is **required** — the dashboard will not render without it.
511
+ */
512
+ export declare interface SeoHealthPaneOptions extends Omit<SeoHealthDashboardProps, 'customQuery'> {
513
+ /** Required license key (format: `SEOF-XXXX-XXXX-XXXX`). */
514
+ licenseKey: string
515
+ /**
516
+ * A fully custom GROQ query used to fetch documents for the dashboard.
517
+ * The query must return documents with at least: `_id`, `_type`, `title`, `seo`, `_updatedAt`.
518
+ *
519
+ * Takes precedence over `queryTypes` when both are provided.
520
+ *
521
+ * @example
522
+ * query: `*[_type in ["post","page"] && defined(seo)]{ _id, _type, title, slug, seo, _updatedAt }`
523
+ */
524
+ query?: string
525
+ }
526
+
441
527
  export declare type SeoHealthStatus = 'excellent' | 'good' | 'fair' | 'poor' | 'missing'
442
528
 
443
529
  /**
package/dist/index.d.ts CHANGED
@@ -1,14 +1,44 @@
1
+ import type {ComponentBuilder} from 'sanity/structure'
1
2
  import {JSX} from 'react'
2
3
  import {ObjectDefinition} from 'sanity'
3
4
  import {Plugin as Plugin_2} from 'sanity'
4
5
  import {PreviewConfig} from 'sanity'
5
6
  import {default as React_2} from 'react'
6
7
  import {SchemaTypeDefinition} from 'sanity'
8
+ import type {StructureBuilder} from 'sanity/structure'
7
9
 
8
10
  export declare type AllFieldKeys = SeoFieldKeys | openGraphFieldKeys | twitterFieldKeys
9
11
 
10
12
  export declare function allSchemas(config?: SeoFieldsPluginConfig): SchemaTypeDefinition[]
11
13
 
14
+ /**
15
+ * Creates a desk-structure pane for the SEO Health Dashboard.
16
+ *
17
+ * Returns a **`ComponentBuilder`** with a built-in `.child()` resolver so that
18
+ * clicking any document row opens the document editor as a split pane to the right.
19
+ *
20
+ * Use it **directly** as the `.child()` value — do **not** wrap it in `S.component()`.
21
+ *
22
+ * ```ts
23
+ * // sanity.config.ts
24
+ * structure: (S) =>
25
+ * S.list().items([
26
+ * S.listItem()
27
+ * .title('SEO Health')
28
+ * .child(
29
+ * createSeoHealthPane(S, {
30
+ * licenseKey: 'SEOF-XXXX-XXXX-XXXX',
31
+ * query: `*[_type == "post" && defined(seo)]{ _id, _type, title, slug, seo, _updatedAt }`,
32
+ * })
33
+ * ),
34
+ * ])
35
+ * ```
36
+ */
37
+ export declare function createSeoHealthPane(
38
+ optionsOrS: StructureBuilder,
39
+ optionsWhenS: SeoHealthPaneOptions,
40
+ ): ComponentBuilder
41
+
12
42
  export declare interface DocumentWithSeoHealth {
13
43
  _id: string
14
44
  _type: string
@@ -308,6 +338,16 @@ export declare interface SeoFieldsPluginConfig {
308
338
  fontSize?: string
309
339
  }
310
340
  | undefined
341
+ /**
342
+ * The `name` of the Sanity structure tool that contains the monitored documents.
343
+ * Required when you have multiple structure tools and the documents live in a
344
+ * non-default one. Clicking a title will navigate to
345
+ * `/{basePath}/{structureTool}/intent/edit/…` directly.
346
+ *
347
+ * @example
348
+ * structureTool: 'common'
349
+ */
350
+ structureTool?: string
311
351
  /**
312
352
  * Enable preview/demo mode to show dummy data.
313
353
  * Useful for testing, documentation, or showcasing the dashboard.
@@ -430,6 +470,31 @@ declare interface SeoHealthDashboardProps {
430
470
  * Defaults to `false`.
431
471
  */
432
472
  previewMode?: boolean
473
+ /**
474
+ * When `true`, clicking a document title opens the document editor as a split
475
+ * pane to the right, keeping the SEO Health pane visible on the left.
476
+ * This uses Sanity's pane router and requires the component to be rendered
477
+ * inside a desk-structure pane context (i.e. via `createSeoHealthPane`).
478
+ *
479
+ * When `false` (default), clicking navigates to the document via the standard
480
+ * intent-link system (full navigation).
481
+ *
482
+ * This is set to `true` automatically by `createSeoHealthPane`.
483
+ */
484
+ openInPane?: boolean
485
+ /**
486
+ * The `name` of the Sanity structure tool that contains the monitored documents.
487
+ * When provided, clicking a document title navigates directly to that tool's
488
+ * intent URL (`/{basePath}/{structureTool}/intent/edit/id=…;type=…/`) instead of
489
+ * using the generic intent resolver, which always picks the first registered tool.
490
+ *
491
+ * Required when you have multiple structure tools and the documents live in a
492
+ * non-default one (e.g. `name: 'common'`).
493
+ *
494
+ * @example
495
+ * structureTool: 'common'
496
+ */
497
+ structureTool?: string
433
498
  }
434
499
 
435
500
  export declare interface SeoHealthMetrics {
@@ -438,6 +503,27 @@ export declare interface SeoHealthMetrics {
438
503
  issues: string[]
439
504
  }
440
505
 
506
+ /**
507
+ * Options accepted by `createSeoHealthPane`.
508
+ * All props from `SeoHealthDashboardProps` are supported.
509
+ *
510
+ * `licenseKey` is **required** — the dashboard will not render without it.
511
+ */
512
+ export declare interface SeoHealthPaneOptions extends Omit<SeoHealthDashboardProps, 'customQuery'> {
513
+ /** Required license key (format: `SEOF-XXXX-XXXX-XXXX`). */
514
+ licenseKey: string
515
+ /**
516
+ * A fully custom GROQ query used to fetch documents for the dashboard.
517
+ * The query must return documents with at least: `_id`, `_type`, `title`, `seo`, `_updatedAt`.
518
+ *
519
+ * Takes precedence over `queryTypes` when both are provided.
520
+ *
521
+ * @example
522
+ * query: `*[_type in ["post","page"] && defined(seo)]{ _id, _type, title, slug, seo, _updatedAt }`
523
+ */
524
+ query?: string
525
+ }
526
+
441
527
  export declare type SeoHealthStatus = 'excellent' | 'good' | 'fair' | 'poor' | 'missing'
442
528
 
443
529
  /**
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: !0 });
3
- var o = require("react"), sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), router = require("sanity/router"), ui = require("@sanity/ui");
3
+ var o = require("react"), sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), router = require("sanity/router"), structure = require("sanity/structure"), ui = require("@sanity/ui");
4
4
  function _interopDefaultCompat(e) {
5
5
  return e && typeof e == "object" && "default" in e ? e : { default: e };
6
6
  }
@@ -1192,16 +1192,9 @@ const DashboardContainer = dt.div`
1192
1192
  color: #6b7280;
1193
1193
  `, StatsGrid = dt.div`
1194
1194
  display: grid;
1195
- grid-template-columns: repeat(6, 1fr);
1195
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
1196
1196
  gap: 14px;
1197
1197
  margin-bottom: 20px;
1198
-
1199
- @media (max-width: 1100px) {
1200
- grid-template-columns: repeat(3, 1fr);
1201
- }
1202
- @media (max-width: 600px) {
1203
- grid-template-columns: repeat(2, 1fr);
1204
- }
1205
1198
  `, StatCard = dt.div`
1206
1199
  background: #ffffff;
1207
1200
  border-radius: 10px;
@@ -1334,6 +1327,11 @@ const DashboardContainer = dt.div`
1334
1327
  align-items: center;
1335
1328
  gap: 4px;
1336
1329
  flex-wrap: wrap;
1330
+ min-width: 0;
1331
+ `, TitleCell = dt.div`
1332
+ min-width: 0;
1333
+ overflow: hidden;
1334
+ flex: 1;
1337
1335
  `, ColType = dt.div`
1338
1336
  flex: 0.8;
1339
1337
  min-width: 80px;
@@ -1525,13 +1523,37 @@ const DashboardContainer = dt.div`
1525
1523
  color: #374151;
1526
1524
  border-color: #9ca3af;
1527
1525
  }
1528
- `, DocTitleAnchor = ({
1526
+ `, DocTitleAnchor = ({ id, type, structureTool, children }) => {
1527
+ const { basePath } = sanity.useWorkspace(), { onClick: intentOnClick, href: intentHref } = router.useIntentLink({ intent: "edit", params: { id, type } }), href = structureTool ? `${basePath}/${structureTool}/intent/edit/id=${id};type=${type}/` : intentHref;
1528
+ return /* @__PURE__ */ jsxRuntime.jsx(DocTitleLink, { href, onClick: structureTool ? void 0 : intentOnClick, title: "Open document", children });
1529
+ }, PaneLinkWrapper = dt.span`
1530
+ display: block;
1531
+ min-width: 0;
1532
+ overflow: hidden;
1533
+
1534
+ a {
1535
+ font-size: 13px;
1536
+ font-weight: 600;
1537
+ color: #4f46e5;
1538
+ white-space: nowrap;
1539
+ overflow: hidden;
1540
+ text-overflow: ellipsis;
1541
+ text-decoration: none;
1542
+ display: block;
1543
+ transition: color 0.15s;
1544
+
1545
+ &:hover {
1546
+ color: #4338ca;
1547
+ text-decoration: underline;
1548
+ }
1549
+ }
1550
+ `, DocTitleAnchorPane = ({
1529
1551
  id,
1530
1552
  type,
1531
1553
  children
1532
1554
  }) => {
1533
- const { onClick, href } = router.useIntentLink({ intent: "edit", params: { id, type } });
1534
- return /* @__PURE__ */ jsxRuntime.jsx(DocTitleLink, { href, onClick, title: "Open document", children });
1555
+ const { ChildLink } = structure.usePaneRouter();
1556
+ return /* @__PURE__ */ jsxRuntime.jsx(PaneLinkWrapper, { children: /* @__PURE__ */ jsxRuntime.jsx(ChildLink, { childId: id, childParameters: { type }, children }) });
1535
1557
  }, DocBadgeRenderer = ({ doc, docBadge }) => {
1536
1558
  const badge = docBadge(doc);
1537
1559
  return badge ? /* @__PURE__ */ jsxRuntime.jsx(CustomBadge, { $bgColor: badge.bgColor, $textColor: badge.textColor, $fontSize: badge.fontSize, children: badge.label }) : null;
@@ -1773,7 +1795,9 @@ const DashboardContainer = dt.div`
1773
1795
  loadingLicense,
1774
1796
  loadingDocuments,
1775
1797
  noDocuments,
1776
- previewMode = !1
1798
+ previewMode = !1,
1799
+ openInPane = !1,
1800
+ structureTool
1777
1801
  }) => {
1778
1802
  const client = sanity.useClient({ apiVersion }), [licenseStatus, setLicenseStatus] = o.useState("loading"), [documents, setDocuments] = o.useState([]), [loading, setLoading] = o.useState(!0), [searchQuery, setSearchQuery] = o.useState(""), [filterStatus, setFilterStatus] = o.useState("all"), [filterType, setFilterType] = o.useState("all"), [sortBy, setSortBy] = o.useState("score"), [activePopover, setActivePopover] = o.useState(null), VALIDATION_ENDPOINT = "https://sanity-plugin-seofields.thehardik.in/api/validate-license", CACHE_TTL_MS = 3600 * 1e3, validateLicense = o.useCallback(
1779
1803
  async (forceRefresh = !1) => {
@@ -2058,8 +2082,8 @@ export default defineConfig({
2058
2082
  ] }),
2059
2083
  filteredAndSortedDocs.map((doc) => /* @__PURE__ */ jsxRuntime.jsxs(TableRow, { children: [
2060
2084
  /* @__PURE__ */ jsxRuntime.jsx(ColTitle, { children: /* @__PURE__ */ jsxRuntime.jsxs(TitleWrapper, { children: [
2061
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2062
- /* @__PURE__ */ jsxRuntime.jsx(DocTitleAnchor, { id: doc._id, type: doc._type, children: doc.title || "Untitled" }),
2085
+ /* @__PURE__ */ jsxRuntime.jsxs(TitleCell, { children: [
2086
+ openInPane ? /* @__PURE__ */ jsxRuntime.jsx(DocTitleAnchorPane, { id: doc._id, type: doc._type, children: doc.title || "Untitled" }) : /* @__PURE__ */ jsxRuntime.jsx(DocTitleAnchor, { id: doc._id, type: doc._type, structureTool, children: doc.title || "Untitled" }),
2063
2087
  showDocumentId && /* @__PURE__ */ jsxRuntime.jsx(DocId, { children: doc._id })
2064
2088
  ] }),
2065
2089
  docBadge && /* @__PURE__ */ jsxRuntime.jsx(
@@ -3081,7 +3105,8 @@ const resolveDashboardConfig = (healthDashboard) => {
3081
3105
  loadingLicense: cfg?.content?.loadingLicense,
3082
3106
  loadingDocuments: cfg?.content?.loadingDocuments,
3083
3107
  noDocuments: cfg?.content?.noDocuments,
3084
- previewMode: cfg?.previewMode
3108
+ previewMode: cfg?.previewMode,
3109
+ structureTool: cfg?.structureTool
3085
3110
  };
3086
3111
  }, seofields = sanity.definePlugin((config = {}) => {
3087
3112
  const { healthDashboard = !0 } = config, dash = resolveDashboardConfig(healthDashboard), BoundSeoHealthTool = () => o__default.default.createElement(SeoHealthTool, {
@@ -3102,7 +3127,8 @@ const resolveDashboardConfig = (healthDashboard) => {
3102
3127
  loadingLicense: dash.loadingLicense,
3103
3128
  loadingDocuments: dash.loadingDocuments,
3104
3129
  noDocuments: dash.noDocuments,
3105
- previewMode: dash.previewMode
3130
+ previewMode: dash.previewMode,
3131
+ structureTool: dash.structureTool
3106
3132
  });
3107
3133
  return {
3108
3134
  name: "sanity-plugin-seofields",
@@ -3121,9 +3147,17 @@ const resolveDashboardConfig = (healthDashboard) => {
3121
3147
  }
3122
3148
  };
3123
3149
  });
3150
+ function createSeoHealthPane(optionsOrS, optionsWhenS) {
3151
+ const S2 = optionsOrS, { query, openInPane = !0, title: paneTitle, ...rest } = optionsWhenS ?? {}, SeoHealthPane = () => /* @__PURE__ */ jsxRuntime.jsx(SeoHealthDashboard, { customQuery: query, openInPane, title: paneTitle, ...rest });
3152
+ return SeoHealthPane.displayName = "SeoHealthPane", S2.component(SeoHealthPane).title(paneTitle ?? "SEO Health").child((docId, { params }) => {
3153
+ const builder = S2.document().documentId(docId);
3154
+ return params?.type ? builder.schemaType(params.type) : builder;
3155
+ });
3156
+ }
3124
3157
  exports.SeoHealthDashboard = SeoHealthDashboard;
3125
3158
  exports.SeoHealthTool = SeoHealthTool;
3126
3159
  exports.allSchemas = types;
3160
+ exports.createSeoHealthPane = createSeoHealthPane;
3127
3161
  exports.default = seofields;
3128
3162
  exports.metaAttributeSchema = metaAttribute;
3129
3163
  exports.metaTagSchema = metaTag;