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 +60 -0
- package/dist/index.d.mts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +51 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +52 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/SeoHealthDashboard.tsx +104 -19
- package/src/components/SeoHealthPane.tsx +81 -0
- package/src/index.ts +2 -0
- 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.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(
|
|
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<{
|
|
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 {
|
|
526
|
+
const {ChildLink} = usePaneRouter()
|
|
477
527
|
return (
|
|
478
|
-
<
|
|
479
|
-
{
|
|
480
|
-
|
|
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
|
-
<
|
|
1406
|
-
|
|
1407
|
-
{doc.
|
|
1408
|
-
|
|
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
|
-
</
|
|
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 {
|