@pagebridge/sanity 0.0.2 → 0.1.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/LICENSE +21 -21
- package/README.md +228 -214
- package/dist/components/RefreshQueueTool.js +10 -10
- package/dist/components/SearchPerformancePane.js +10 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.d.ts +3 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2 -2
- package/package.json +20 -1
- package/.turbo/turbo-build.log +0 -2
- package/dist/components/DecayAlertCard.d.ts +0 -16
- package/dist/components/DecayAlertCard.d.ts.map +0 -1
- package/dist/components/DecayAlertCard.js +0 -24
- package/dist/components/DecayBadge.d.ts +0 -3
- package/dist/components/DecayBadge.d.ts.map +0 -1
- package/dist/components/DecayBadge.js +0 -19
- package/dist/components/PerformanceChart.d.ts +0 -12
- package/dist/components/PerformanceChart.d.ts.map +0 -1
- package/dist/components/PerformanceChart.js +0 -18
- package/dist/components/PerformanceInspector.d.ts +0 -3
- package/dist/components/PerformanceInspector.d.ts.map +0 -1
- package/dist/components/PerformanceInspector.js +0 -174
- package/dist/schemas/gscKeywordTarget.d.ts +0 -22
- package/dist/schemas/gscKeywordTarget.d.ts.map +0 -1
- package/dist/schemas/gscKeywordTarget.js +0 -43
- package/eslint.config.js +0 -3
- package/src/components/RefreshQueueTool.tsx +0 -196
- package/src/components/SearchPerformancePane.tsx +0 -237
- package/src/index.ts +0 -18
- package/src/plugin.ts +0 -120
- package/src/schemas/gscRefreshTask.ts +0 -203
- package/src/schemas/gscSite.ts +0 -71
- package/src/schemas/gscSnapshot.ts +0 -131
- package/src/schemas/index.ts +0 -11
- package/tsconfig.json +0 -23
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import { useClient } from "sanity";
|
|
3
|
-
import { Card, Stack, Text, Badge, Flex, Box, Spinner, Tooltip } from "@sanity/ui";
|
|
4
|
-
import { ArrowUpIcon, ArrowDownIcon, ArrowRightIcon, CheckmarkCircleIcon, CloseCircleIcon, WarningOutlineIcon } from "@sanity/icons";
|
|
5
|
-
|
|
6
|
-
interface IndexStatus {
|
|
7
|
-
verdict: "indexed" | "not_indexed" | "excluded";
|
|
8
|
-
coverageState: string | null;
|
|
9
|
-
lastCrawlTime: string | null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface PerformanceData {
|
|
13
|
-
clicks: number;
|
|
14
|
-
impressions: number;
|
|
15
|
-
ctr: number;
|
|
16
|
-
position: number;
|
|
17
|
-
positionDelta: number;
|
|
18
|
-
topQueries: { query: string; clicks: number; position: number }[];
|
|
19
|
-
lastUpdated: string;
|
|
20
|
-
indexStatus?: IndexStatus;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface SearchPerformancePaneProps {
|
|
24
|
-
documentId: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function SearchPerformancePane({
|
|
28
|
-
documentId,
|
|
29
|
-
}: SearchPerformancePaneProps) {
|
|
30
|
-
const client = useClient({ apiVersion: "2024-01-01" });
|
|
31
|
-
const [data, setData] = useState<PerformanceData | undefined>(undefined);
|
|
32
|
-
const [loading, setLoading] = useState(true);
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
async function fetchData() {
|
|
36
|
-
const snapshot = await client.fetch(
|
|
37
|
-
`*[_type == "gscSnapshot" && linkedDocument._ref == $id && period == "last28"][0]{
|
|
38
|
-
clicks,
|
|
39
|
-
impressions,
|
|
40
|
-
ctr,
|
|
41
|
-
position,
|
|
42
|
-
topQueries,
|
|
43
|
-
fetchedAt,
|
|
44
|
-
indexStatus
|
|
45
|
-
}`,
|
|
46
|
-
{ id: documentId },
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
const previousSnapshot = await client.fetch(
|
|
50
|
-
`*[_type == "gscSnapshot" && linkedDocument._ref == $id && period == "last28"] | order(fetchedAt desc)[1]{
|
|
51
|
-
position
|
|
52
|
-
}`,
|
|
53
|
-
{ id: documentId },
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
if (snapshot) {
|
|
57
|
-
setData({
|
|
58
|
-
...snapshot,
|
|
59
|
-
positionDelta: previousSnapshot
|
|
60
|
-
? snapshot.position - previousSnapshot.position
|
|
61
|
-
: 0,
|
|
62
|
-
lastUpdated: snapshot.fetchedAt,
|
|
63
|
-
indexStatus: snapshot.indexStatus,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
setLoading(false);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
fetchData();
|
|
70
|
-
}, [documentId, client]);
|
|
71
|
-
|
|
72
|
-
if (loading) {
|
|
73
|
-
return (
|
|
74
|
-
<Card padding={4}>
|
|
75
|
-
<Flex justify="center">
|
|
76
|
-
<Spinner />
|
|
77
|
-
</Flex>
|
|
78
|
-
</Card>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!data) {
|
|
83
|
-
return (
|
|
84
|
-
<Card padding={4} tone="caution">
|
|
85
|
-
<Text>No search data available for this document.</Text>
|
|
86
|
-
</Card>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<Card padding={4}>
|
|
92
|
-
<Stack space={4}>
|
|
93
|
-
<Flex justify="space-between" align="center">
|
|
94
|
-
<Text weight="semibold" size={2}>
|
|
95
|
-
Search Performance (Last 28 Days)
|
|
96
|
-
</Text>
|
|
97
|
-
{data.indexStatus && <IndexStatusBadge status={data.indexStatus} />}
|
|
98
|
-
</Flex>
|
|
99
|
-
|
|
100
|
-
<Flex gap={3} wrap="wrap">
|
|
101
|
-
<MetricCard label="Clicks" value={data.clicks.toLocaleString()} />
|
|
102
|
-
<MetricCard
|
|
103
|
-
label="Impressions"
|
|
104
|
-
value={data.impressions.toLocaleString()}
|
|
105
|
-
/>
|
|
106
|
-
<MetricCard label="CTR" value={`${(data.ctr * 100).toFixed(1)}%`} />
|
|
107
|
-
<MetricCard
|
|
108
|
-
label="Avg. Position"
|
|
109
|
-
value={data.position.toFixed(1)}
|
|
110
|
-
trend={data.positionDelta}
|
|
111
|
-
invertTrend
|
|
112
|
-
/>
|
|
113
|
-
</Flex>
|
|
114
|
-
|
|
115
|
-
{data.topQueries?.length > 0 && (
|
|
116
|
-
<Box>
|
|
117
|
-
<Text size={1} weight="semibold" muted style={{ marginBottom: 8 }}>
|
|
118
|
-
Top Queries
|
|
119
|
-
</Text>
|
|
120
|
-
<Stack space={2}>
|
|
121
|
-
{data.topQueries.slice(0, 5).map((q, i) => (
|
|
122
|
-
<Flex key={i} justify="space-between" align="center">
|
|
123
|
-
<Text size={1} style={{ flex: 1 }}>
|
|
124
|
-
{q.query}
|
|
125
|
-
</Text>
|
|
126
|
-
<Text size={1} muted>
|
|
127
|
-
{q.clicks} clicks · pos {q.position.toFixed(1)}
|
|
128
|
-
</Text>
|
|
129
|
-
</Flex>
|
|
130
|
-
))}
|
|
131
|
-
</Stack>
|
|
132
|
-
</Box>
|
|
133
|
-
)}
|
|
134
|
-
|
|
135
|
-
<Text size={0} muted>
|
|
136
|
-
Last updated: {new Date(data.lastUpdated).toLocaleDateString()}
|
|
137
|
-
</Text>
|
|
138
|
-
</Stack>
|
|
139
|
-
</Card>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
interface MetricCardProps {
|
|
144
|
-
label: string;
|
|
145
|
-
value: string;
|
|
146
|
-
trend?: number;
|
|
147
|
-
invertTrend?: boolean;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function MetricCard({ label, value, trend, invertTrend }: MetricCardProps) {
|
|
151
|
-
const TrendIcon =
|
|
152
|
-
trend === undefined || trend === 0
|
|
153
|
-
? ArrowRightIcon
|
|
154
|
-
: (invertTrend ? trend < 0 : trend > 0)
|
|
155
|
-
? ArrowUpIcon
|
|
156
|
-
: ArrowDownIcon;
|
|
157
|
-
|
|
158
|
-
const trendTone =
|
|
159
|
-
trend === undefined || trend === 0
|
|
160
|
-
? "default"
|
|
161
|
-
: (invertTrend ? trend < 0 : trend > 0)
|
|
162
|
-
? "positive"
|
|
163
|
-
: "critical";
|
|
164
|
-
|
|
165
|
-
return (
|
|
166
|
-
<Card padding={3} radius={2} shadow={1} style={{ minWidth: 100 }}>
|
|
167
|
-
<Stack space={2}>
|
|
168
|
-
<Text size={0} muted>
|
|
169
|
-
{label}
|
|
170
|
-
</Text>
|
|
171
|
-
<Flex align="center" gap={2}>
|
|
172
|
-
<Text size={3} weight="semibold">
|
|
173
|
-
{value}
|
|
174
|
-
</Text>
|
|
175
|
-
{trend !== undefined && trend !== 0 && (
|
|
176
|
-
<Badge tone={trendTone} fontSize={0}>
|
|
177
|
-
<TrendIcon />
|
|
178
|
-
{Math.abs(trend).toFixed(1)}
|
|
179
|
-
</Badge>
|
|
180
|
-
)}
|
|
181
|
-
</Flex>
|
|
182
|
-
</Stack>
|
|
183
|
-
</Card>
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
interface IndexStatusBadgeProps {
|
|
188
|
-
status: IndexStatus;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function IndexStatusBadge({ status }: IndexStatusBadgeProps) {
|
|
192
|
-
const config = {
|
|
193
|
-
indexed: {
|
|
194
|
-
tone: "positive" as const,
|
|
195
|
-
icon: CheckmarkCircleIcon,
|
|
196
|
-
label: "Indexed",
|
|
197
|
-
},
|
|
198
|
-
not_indexed: {
|
|
199
|
-
tone: "critical" as const,
|
|
200
|
-
icon: CloseCircleIcon,
|
|
201
|
-
label: "Not Indexed",
|
|
202
|
-
},
|
|
203
|
-
excluded: {
|
|
204
|
-
tone: "caution" as const,
|
|
205
|
-
icon: WarningOutlineIcon,
|
|
206
|
-
label: "Excluded",
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const { tone, icon: Icon, label } = config[status.verdict];
|
|
211
|
-
const tooltipContent = status.coverageState || label;
|
|
212
|
-
|
|
213
|
-
return (
|
|
214
|
-
<Tooltip
|
|
215
|
-
content={
|
|
216
|
-
<Box padding={2}>
|
|
217
|
-
<Stack space={2}>
|
|
218
|
-
<Text size={1}>{tooltipContent}</Text>
|
|
219
|
-
{status.lastCrawlTime && (
|
|
220
|
-
<Text size={0} muted>
|
|
221
|
-
Last crawled: {new Date(status.lastCrawlTime).toLocaleDateString()}
|
|
222
|
-
</Text>
|
|
223
|
-
)}
|
|
224
|
-
</Stack>
|
|
225
|
-
</Box>
|
|
226
|
-
}
|
|
227
|
-
placement="top"
|
|
228
|
-
>
|
|
229
|
-
<Badge tone={tone} fontSize={1} style={{ cursor: "help" }}>
|
|
230
|
-
<Flex align="center" gap={1}>
|
|
231
|
-
<Icon />
|
|
232
|
-
{label}
|
|
233
|
-
</Flex>
|
|
234
|
-
</Badge>
|
|
235
|
-
</Tooltip>
|
|
236
|
-
);
|
|
237
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Note: React components are not exported here to avoid loading them during schema extraction
|
|
2
|
-
// They are lazy-loaded within the plugin when needed
|
|
3
|
-
export {
|
|
4
|
-
gscPlugin,
|
|
5
|
-
createGscStructureResolver,
|
|
6
|
-
createPageBridgeStructure,
|
|
7
|
-
PAGEBRIDGE_TYPES,
|
|
8
|
-
type GscPluginConfig,
|
|
9
|
-
} from "./plugin";
|
|
10
|
-
export {
|
|
11
|
-
gscSite,
|
|
12
|
-
gscSnapshot,
|
|
13
|
-
gscRefreshTask,
|
|
14
|
-
createGscSnapshot,
|
|
15
|
-
createGscRefreshTask,
|
|
16
|
-
type GscSnapshotOptions,
|
|
17
|
-
type GscRefreshTaskOptions,
|
|
18
|
-
} from "./schemas";
|
package/src/plugin.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { definePlugin } from "sanity";
|
|
2
|
-
import type {
|
|
3
|
-
DefaultDocumentNodeResolver,
|
|
4
|
-
StructureBuilder,
|
|
5
|
-
} from "sanity/structure";
|
|
6
|
-
import { ChartUpwardIcon, EarthGlobeIcon } from "@sanity/icons";
|
|
7
|
-
import { gscSite } from "./schemas/gscSite";
|
|
8
|
-
import { createGscSnapshot } from "./schemas/gscSnapshot";
|
|
9
|
-
import { createGscRefreshTask } from "./schemas/gscRefreshTask";
|
|
10
|
-
import type { ComponentType } from "react";
|
|
11
|
-
|
|
12
|
-
export interface GscPluginConfig {
|
|
13
|
-
/**
|
|
14
|
-
* Array of Sanity document type names that represent your content.
|
|
15
|
-
* These will be available for linking in gscSnapshot and gscRefreshTask schemas.
|
|
16
|
-
* Example: ['post', 'article', 'page']
|
|
17
|
-
*/
|
|
18
|
-
contentTypes?: string[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Document type names registered by the PageBridge plugin */
|
|
22
|
-
export const PAGEBRIDGE_TYPES = [
|
|
23
|
-
"gscSite",
|
|
24
|
-
"gscSnapshot",
|
|
25
|
-
"gscRefreshTask",
|
|
26
|
-
] as const;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Creates a "PageBridge" folder list item for the desk structure.
|
|
30
|
-
* Use with structureTool's `structure` option to group PageBridge
|
|
31
|
-
* documents into a single folder and filter them from the default list.
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```ts
|
|
35
|
-
* structureTool({
|
|
36
|
-
* structure: (S, context) =>
|
|
37
|
-
* S.list()
|
|
38
|
-
* .title("Content")
|
|
39
|
-
* .items([
|
|
40
|
-
* createPageBridgeStructure(S),
|
|
41
|
-
* S.divider(),
|
|
42
|
-
* ...S.documentTypeListItems().filter(
|
|
43
|
-
* (item) => !PAGEBRIDGE_TYPES.includes(item.getId() as any),
|
|
44
|
-
* ),
|
|
45
|
-
* ]),
|
|
46
|
-
* })
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
|
-
export const createPageBridgeStructure = (S: StructureBuilder) =>
|
|
50
|
-
S.listItem()
|
|
51
|
-
.title("PageBridge")
|
|
52
|
-
.icon(EarthGlobeIcon)
|
|
53
|
-
.child(
|
|
54
|
-
S.list()
|
|
55
|
-
.title("PageBridge")
|
|
56
|
-
.items([
|
|
57
|
-
S.listItem()
|
|
58
|
-
.title("GSC Sites")
|
|
59
|
-
.schemaType("gscSite")
|
|
60
|
-
.child(S.documentTypeList("gscSite").title("GSC Sites")),
|
|
61
|
-
S.listItem()
|
|
62
|
-
.title("Snapshots")
|
|
63
|
-
.schemaType("gscSnapshot")
|
|
64
|
-
.child(S.documentTypeList("gscSnapshot").title("Snapshots")),
|
|
65
|
-
S.listItem()
|
|
66
|
-
.title("Refresh Tasks")
|
|
67
|
-
.schemaType("gscRefreshTask")
|
|
68
|
-
.child(S.documentTypeList("gscRefreshTask").title("Refresh Tasks")),
|
|
69
|
-
]),
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Creates a structure resolver that adds the Performance view to content types
|
|
74
|
-
* Use this with structureTool's defaultDocumentNode option
|
|
75
|
-
*/
|
|
76
|
-
export const createGscStructureResolver = (
|
|
77
|
-
contentTypes: string[] = [],
|
|
78
|
-
): DefaultDocumentNodeResolver => {
|
|
79
|
-
return (S, { schemaType }) => {
|
|
80
|
-
if (contentTypes.includes(schemaType)) {
|
|
81
|
-
// Lazy import to avoid loading React component during schema extraction
|
|
82
|
-
const SearchPerformancePane: ComponentType<any> =
|
|
83
|
-
require("./components/SearchPerformancePane").SearchPerformancePane;
|
|
84
|
-
|
|
85
|
-
return S.document().views([
|
|
86
|
-
S.view.form(),
|
|
87
|
-
S.view
|
|
88
|
-
.component(SearchPerformancePane)
|
|
89
|
-
.title("Performance")
|
|
90
|
-
.icon(ChartUpwardIcon),
|
|
91
|
-
]);
|
|
92
|
-
}
|
|
93
|
-
return S.document().views([S.view.form()]);
|
|
94
|
-
};
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
export const gscPlugin = definePlugin<GscPluginConfig | void>((config) => {
|
|
98
|
-
const contentTypes = config?.contentTypes ?? [];
|
|
99
|
-
|
|
100
|
-
const gscSnapshot = createGscSnapshot({ contentTypes });
|
|
101
|
-
const gscRefreshTask = createGscRefreshTask({ contentTypes });
|
|
102
|
-
|
|
103
|
-
// Lazy import to avoid loading React component during schema extraction
|
|
104
|
-
const RefreshQueueTool: ComponentType<any> =
|
|
105
|
-
require("./components/RefreshQueueTool").RefreshQueueTool;
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
name: "pagebridge-sanity",
|
|
109
|
-
schema: {
|
|
110
|
-
types: [gscSite, gscSnapshot, gscRefreshTask],
|
|
111
|
-
},
|
|
112
|
-
tools: [
|
|
113
|
-
{
|
|
114
|
-
name: "gsc-refresh-queue",
|
|
115
|
-
title: "Refresh Queue",
|
|
116
|
-
component: RefreshQueueTool,
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
};
|
|
120
|
-
});
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { defineType, defineField } from "sanity";
|
|
2
|
-
|
|
3
|
-
export interface GscRefreshTaskOptions {
|
|
4
|
-
contentTypes?: string[];
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export const createGscRefreshTask = (options: GscRefreshTaskOptions = {}) => {
|
|
8
|
-
const contentTypes = options.contentTypes ?? [];
|
|
9
|
-
|
|
10
|
-
return defineType({
|
|
11
|
-
name: "gscRefreshTask",
|
|
12
|
-
title: "Refresh Task",
|
|
13
|
-
type: "document",
|
|
14
|
-
fields: [
|
|
15
|
-
defineField({
|
|
16
|
-
name: "site",
|
|
17
|
-
type: "reference",
|
|
18
|
-
to: [{ type: "gscSite" }],
|
|
19
|
-
validation: (Rule) => Rule.required(),
|
|
20
|
-
}),
|
|
21
|
-
...(contentTypes.length > 0
|
|
22
|
-
? [
|
|
23
|
-
defineField({
|
|
24
|
-
name: "linkedDocument",
|
|
25
|
-
title: "Content to Refresh",
|
|
26
|
-
type: "reference",
|
|
27
|
-
to: contentTypes.map((type) => ({ type })),
|
|
28
|
-
validation: (Rule) => Rule.required(),
|
|
29
|
-
}),
|
|
30
|
-
]
|
|
31
|
-
: []),
|
|
32
|
-
defineField({
|
|
33
|
-
name: "reason",
|
|
34
|
-
title: "Reason",
|
|
35
|
-
type: "string",
|
|
36
|
-
options: {
|
|
37
|
-
list: [
|
|
38
|
-
{ title: "Position Decay", value: "position_decay" },
|
|
39
|
-
{ title: "Low CTR", value: "low_ctr" },
|
|
40
|
-
{ title: "Impressions Drop", value: "impressions_drop" },
|
|
41
|
-
{ title: "Manual", value: "manual" },
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
}),
|
|
45
|
-
defineField({
|
|
46
|
-
name: "severity",
|
|
47
|
-
title: "Severity",
|
|
48
|
-
type: "string",
|
|
49
|
-
options: {
|
|
50
|
-
list: [
|
|
51
|
-
{ title: "Low", value: "low" },
|
|
52
|
-
{ title: "Medium", value: "medium" },
|
|
53
|
-
{ title: "High", value: "high" },
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
}),
|
|
57
|
-
defineField({
|
|
58
|
-
name: "status",
|
|
59
|
-
title: "Status",
|
|
60
|
-
type: "string",
|
|
61
|
-
options: {
|
|
62
|
-
list: [
|
|
63
|
-
{ title: "Open", value: "open" },
|
|
64
|
-
{ title: "Snoozed", value: "snoozed" },
|
|
65
|
-
{ title: "In Progress", value: "in_progress" },
|
|
66
|
-
{ title: "Done", value: "done" },
|
|
67
|
-
{ title: "Dismissed", value: "dismissed" },
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
initialValue: "open",
|
|
71
|
-
}),
|
|
72
|
-
defineField({
|
|
73
|
-
name: "snoozedUntil",
|
|
74
|
-
title: "Snoozed Until",
|
|
75
|
-
type: "datetime",
|
|
76
|
-
hidden: ({ parent }) => parent?.status !== "snoozed",
|
|
77
|
-
}),
|
|
78
|
-
defineField({
|
|
79
|
-
name: "metrics",
|
|
80
|
-
title: "Metrics at Detection",
|
|
81
|
-
type: "object",
|
|
82
|
-
fields: [
|
|
83
|
-
defineField({
|
|
84
|
-
name: "positionBefore",
|
|
85
|
-
type: "number",
|
|
86
|
-
title: "Position (28 days ago)",
|
|
87
|
-
}),
|
|
88
|
-
defineField({
|
|
89
|
-
name: "positionNow",
|
|
90
|
-
type: "number",
|
|
91
|
-
title: "Position (current)",
|
|
92
|
-
}),
|
|
93
|
-
defineField({
|
|
94
|
-
name: "positionDelta",
|
|
95
|
-
type: "number",
|
|
96
|
-
title: "Position Change",
|
|
97
|
-
}),
|
|
98
|
-
defineField({
|
|
99
|
-
name: "ctrBefore",
|
|
100
|
-
type: "number",
|
|
101
|
-
title: "CTR (28 days ago)",
|
|
102
|
-
}),
|
|
103
|
-
defineField({
|
|
104
|
-
name: "ctrNow",
|
|
105
|
-
type: "number",
|
|
106
|
-
title: "CTR (current)",
|
|
107
|
-
}),
|
|
108
|
-
defineField({
|
|
109
|
-
name: "impressions",
|
|
110
|
-
type: "number",
|
|
111
|
-
title: "Impressions (last 28d)",
|
|
112
|
-
}),
|
|
113
|
-
],
|
|
114
|
-
}),
|
|
115
|
-
defineField({
|
|
116
|
-
name: "queryContext",
|
|
117
|
-
title: "Top Queries",
|
|
118
|
-
type: "array",
|
|
119
|
-
description: "What people searched to find this page",
|
|
120
|
-
of: [
|
|
121
|
-
{
|
|
122
|
-
type: "object",
|
|
123
|
-
fields: [
|
|
124
|
-
defineField({
|
|
125
|
-
name: "query",
|
|
126
|
-
type: "string",
|
|
127
|
-
title: "Query",
|
|
128
|
-
}),
|
|
129
|
-
defineField({
|
|
130
|
-
name: "impressions",
|
|
131
|
-
type: "number",
|
|
132
|
-
title: "Impressions",
|
|
133
|
-
}),
|
|
134
|
-
defineField({
|
|
135
|
-
name: "clicks",
|
|
136
|
-
type: "number",
|
|
137
|
-
title: "Clicks",
|
|
138
|
-
}),
|
|
139
|
-
defineField({
|
|
140
|
-
name: "position",
|
|
141
|
-
type: "number",
|
|
142
|
-
title: "Avg Position",
|
|
143
|
-
}),
|
|
144
|
-
],
|
|
145
|
-
preview: {
|
|
146
|
-
select: {
|
|
147
|
-
query: "query",
|
|
148
|
-
impressions: "impressions",
|
|
149
|
-
position: "position",
|
|
150
|
-
},
|
|
151
|
-
prepare({ query, impressions, position }) {
|
|
152
|
-
return {
|
|
153
|
-
title: query,
|
|
154
|
-
subtitle: `Pos ${position?.toFixed(1)} · ${impressions} impr`,
|
|
155
|
-
};
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
}),
|
|
161
|
-
defineField({
|
|
162
|
-
name: "notes",
|
|
163
|
-
title: "Notes",
|
|
164
|
-
type: "text",
|
|
165
|
-
description: "Why was this snoozed/dismissed?",
|
|
166
|
-
}),
|
|
167
|
-
defineField({
|
|
168
|
-
name: "createdAt",
|
|
169
|
-
type: "datetime",
|
|
170
|
-
}),
|
|
171
|
-
defineField({
|
|
172
|
-
name: "resolvedAt",
|
|
173
|
-
type: "datetime",
|
|
174
|
-
}),
|
|
175
|
-
],
|
|
176
|
-
orderings: [
|
|
177
|
-
{
|
|
178
|
-
title: "Severity",
|
|
179
|
-
name: "severityDesc",
|
|
180
|
-
by: [
|
|
181
|
-
{ field: "severity", direction: "desc" },
|
|
182
|
-
{ field: "createdAt", direction: "desc" },
|
|
183
|
-
],
|
|
184
|
-
},
|
|
185
|
-
],
|
|
186
|
-
preview: {
|
|
187
|
-
select: {
|
|
188
|
-
title: "linkedDocument.title",
|
|
189
|
-
reason: "reason",
|
|
190
|
-
severity: "severity",
|
|
191
|
-
},
|
|
192
|
-
prepare({ title, reason, severity }) {
|
|
193
|
-
return {
|
|
194
|
-
title: title || "Unknown document",
|
|
195
|
-
subtitle: `${severity} · ${reason.replace(/_/g, " ")}`,
|
|
196
|
-
};
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// Default export for backward compatibility (without linkedDocument)
|
|
203
|
-
export const gscRefreshTask = createGscRefreshTask();
|
package/src/schemas/gscSite.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { defineType, defineField } from "sanity";
|
|
2
|
-
|
|
3
|
-
export const gscSite = defineType({
|
|
4
|
-
name: "gscSite",
|
|
5
|
-
title: "GSC Site",
|
|
6
|
-
type: "document",
|
|
7
|
-
fields: [
|
|
8
|
-
defineField({
|
|
9
|
-
name: "siteUrl",
|
|
10
|
-
title: "Site URL",
|
|
11
|
-
type: "string",
|
|
12
|
-
description: "Exactly as it appears in GSC (e.g., sc-domain:example.com)",
|
|
13
|
-
validation: (Rule) => Rule.required(),
|
|
14
|
-
}),
|
|
15
|
-
defineField({
|
|
16
|
-
name: "slug",
|
|
17
|
-
title: "Slug",
|
|
18
|
-
type: "slug",
|
|
19
|
-
options: { source: "siteUrl" },
|
|
20
|
-
description: "Used for filtering in multi-site setups",
|
|
21
|
-
}),
|
|
22
|
-
defineField({
|
|
23
|
-
name: "defaultLocale",
|
|
24
|
-
title: "Default Locale",
|
|
25
|
-
type: "string",
|
|
26
|
-
initialValue: "en",
|
|
27
|
-
}),
|
|
28
|
-
defineField({
|
|
29
|
-
name: "pathPrefix",
|
|
30
|
-
title: "Path Prefix",
|
|
31
|
-
type: "string",
|
|
32
|
-
description: 'If your blog lives at /blog, enter "/blog"',
|
|
33
|
-
}),
|
|
34
|
-
defineField({
|
|
35
|
-
name: "contentTypes",
|
|
36
|
-
title: "Content Types",
|
|
37
|
-
type: "array",
|
|
38
|
-
of: [{ type: "string" }],
|
|
39
|
-
initialValue: ["post", "page"],
|
|
40
|
-
description:
|
|
41
|
-
"Sanity document types to match URLs against (e.g., post, page, article)",
|
|
42
|
-
}),
|
|
43
|
-
defineField({
|
|
44
|
-
name: "slugField",
|
|
45
|
-
title: "Slug Field",
|
|
46
|
-
type: "string",
|
|
47
|
-
initialValue: "slug",
|
|
48
|
-
description: "The field name containing the URL slug (default: slug)",
|
|
49
|
-
}),
|
|
50
|
-
defineField({
|
|
51
|
-
name: "lastSyncedAt",
|
|
52
|
-
title: "Last Synced",
|
|
53
|
-
type: "datetime",
|
|
54
|
-
readOnly: true,
|
|
55
|
-
}),
|
|
56
|
-
defineField({
|
|
57
|
-
name: "lastDiagnosticsAt",
|
|
58
|
-
title: "Last Diagnostics Run",
|
|
59
|
-
type: "datetime",
|
|
60
|
-
readOnly: true,
|
|
61
|
-
}),
|
|
62
|
-
defineField({
|
|
63
|
-
name: "unmatchedCount",
|
|
64
|
-
title: "Unmatched URLs",
|
|
65
|
-
type: "number",
|
|
66
|
-
readOnly: true,
|
|
67
|
-
description:
|
|
68
|
-
"Number of GSC URLs that could not be matched to Sanity documents",
|
|
69
|
-
}),
|
|
70
|
-
],
|
|
71
|
-
});
|