@pagebridge/sanity 0.1.0 → 0.1.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": "@pagebridge/sanity",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Sanity Studio plugin for PageBridge — performance pane, decay alerts, and refresh queue powered by Google Search Console data",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -18,6 +18,11 @@
18
18
  "cms",
19
19
  "content-refresh"
20
20
  ],
21
+ "files": [
22
+ "dist",
23
+ "LICENSE",
24
+ "README.md"
25
+ ],
21
26
  "exports": {
22
27
  ".": {
23
28
  "types": "./dist/index.d.ts",
@@ -33,7 +38,7 @@
33
38
  }
34
39
  },
35
40
  "peerDependencies": {
36
- "sanity": "^5.0.0",
41
+ "sanity": "^4.0.0",
37
42
  "@sanity/ui": "^3.0.0",
38
43
  "@sanity/icons": "^3.0.0",
39
44
  "react": "^18 || ^19",
@@ -1,2 +0,0 @@
1
- [?9001h[?1004h[?25l> @pagebridge/sanity@0.1.0 build F:\Code\pagebridge\oss\packages\sanity-plugin
2
- > pnpm exec tsc]0;C:\WINDOWS\system32\cmd.exe[?25h[?9001l[?1004l
package/eslint.config.js DELETED
@@ -1,3 +0,0 @@
1
- import { config } from "@pagebridge/eslint-config/react-internal";
2
-
3
- export default [...config];
@@ -1,196 +0,0 @@
1
- import { useClient } from "sanity";
2
- import {
3
- Card,
4
- Stack,
5
- Text,
6
- Button,
7
- Flex,
8
- Badge,
9
- Menu,
10
- MenuButton,
11
- MenuItem,
12
- type BadgeTone,
13
- } from "@sanity/ui";
14
- import {
15
- EllipsisVerticalIcon,
16
- CheckmarkIcon,
17
- ClockIcon,
18
- CloseIcon,
19
- } from "@sanity/icons";
20
- import { useEffect, useState, useCallback } from "react";
21
-
22
- interface RefreshTask {
23
- _id: string;
24
- reason: "position_decay" | "low_ctr" | "impressions_drop" | "manual";
25
- severity: "low" | "medium" | "high";
26
- status: string;
27
- snoozedUntil?: string;
28
- metrics?: {
29
- positionBefore?: number;
30
- positionNow?: number;
31
- positionDelta?: number;
32
- };
33
- documentTitle?: string;
34
- documentSlug?: string;
35
- createdAt: string;
36
- }
37
-
38
- type FilterType = "open" | "snoozed" | "all";
39
-
40
- const severityTone: Record<string, BadgeTone> = {
41
- high: "critical",
42
- medium: "caution",
43
- low: "default",
44
- };
45
-
46
- const reasonLabels: Record<string, string> = {
47
- position_decay: "Position Drop",
48
- low_ctr: "Low CTR",
49
- impressions_drop: "Traffic Drop",
50
- manual: "Manual",
51
- };
52
-
53
- export function RefreshQueueTool() {
54
- const client = useClient({ apiVersion: "2024-01-01" });
55
- const [tasks, setTasks] = useState<RefreshTask[]>([]);
56
- const [filter, setFilter] = useState<FilterType>("open");
57
-
58
- const fetchTasks = useCallback(async () => {
59
- const query =
60
- filter === "all"
61
- ? `*[_type == "gscRefreshTask" && status != "done" && status != "dismissed"]`
62
- : `*[_type == "gscRefreshTask" && status == $status]`;
63
-
64
- const results = await client.fetch<RefreshTask[]>(
65
- `${query} | order(severity desc, createdAt desc) {
66
- _id,
67
- reason,
68
- severity,
69
- status,
70
- snoozedUntil,
71
- metrics,
72
- "documentTitle": linkedDocument->title,
73
- "documentSlug": linkedDocument->slug.current,
74
- createdAt
75
- }`,
76
- { status: filter },
77
- );
78
- setTasks(results);
79
- }, [filter, client]);
80
-
81
- useEffect(() => {
82
- fetchTasks();
83
- }, [fetchTasks]);
84
-
85
- const updateStatus = async (
86
- taskId: string,
87
- status: string,
88
- snoozeDays?: number,
89
- ) => {
90
- const patch: Record<string, unknown> = { status };
91
-
92
- if (status === "snoozed" && snoozeDays) {
93
- const until = new Date();
94
- until.setDate(until.getDate() + snoozeDays);
95
- patch.snoozedUntil = until.toISOString();
96
- }
97
-
98
- if (status === "done") {
99
- patch.resolvedAt = new Date().toISOString();
100
- }
101
-
102
- await client.patch(taskId).set(patch).commit();
103
- setTasks((prev) =>
104
- prev.filter((t) => t._id !== taskId || status === filter),
105
- );
106
- };
107
-
108
- return (
109
- <Card padding={4}>
110
- <Stack space={4}>
111
- <Flex justify="space-between" align="center">
112
- <Text size={3} weight="semibold">
113
- Content Refresh Queue
114
- </Text>
115
- <Flex gap={2}>
116
- {(["open", "snoozed", "all"] as const).map((f) => (
117
- <Button
118
- key={f}
119
- mode={filter === f ? "default" : "ghost"}
120
- text={f.charAt(0).toUpperCase() + f.slice(1)}
121
- onClick={() => setFilter(f)}
122
- fontSize={1}
123
- />
124
- ))}
125
- </Flex>
126
- </Flex>
127
-
128
- {tasks.length === 0 ? (
129
- <Card padding={5} tone="positive" radius={2}>
130
- <Text align="center">🎉 No pending refresh tasks!</Text>
131
- </Card>
132
- ) : (
133
- <Stack space={2}>
134
- {tasks.map((task) => (
135
- <Card key={task._id} padding={3} radius={2} shadow={1}>
136
- <Flex justify="space-between" align="flex-start">
137
- <Stack space={2} style={{ flex: 1 }}>
138
- <Flex gap={2} align="center">
139
- <Badge tone={severityTone[task.severity]}>
140
- {task.severity}
141
- </Badge>
142
- <Badge>{reasonLabels[task.reason]}</Badge>
143
- </Flex>
144
- <Text weight="semibold">
145
- {task.documentTitle || "Untitled"}
146
- </Text>
147
- <Text size={1} muted>
148
- /{task.documentSlug}
149
- </Text>
150
- {task.metrics && (
151
- <Text size={1} muted>
152
- Position: {task.metrics.positionBefore?.toFixed(1)} →{" "}
153
- {task.metrics.positionNow?.toFixed(1)} (
154
- {(task.metrics.positionDelta ?? 0) > 0 ? "+" : ""}
155
- {task.metrics.positionDelta?.toFixed(1)})
156
- </Text>
157
- )}
158
- </Stack>
159
- <MenuButton
160
- button={<Button icon={EllipsisVerticalIcon} mode="ghost" />}
161
- id={`menu-${task._id}`}
162
- menu={
163
- <Menu>
164
- <MenuItem
165
- icon={CheckmarkIcon}
166
- text="Mark as Done"
167
- onClick={() => updateStatus(task._id, "done")}
168
- />
169
- <MenuItem
170
- icon={ClockIcon}
171
- text="Snooze 7 days"
172
- onClick={() => updateStatus(task._id, "snoozed", 7)}
173
- />
174
- <MenuItem
175
- icon={ClockIcon}
176
- text="Snooze 30 days"
177
- onClick={() => updateStatus(task._id, "snoozed", 30)}
178
- />
179
- <MenuItem
180
- icon={CloseIcon}
181
- text="Dismiss"
182
- tone="critical"
183
- onClick={() => updateStatus(task._id, "dismissed")}
184
- />
185
- </Menu>
186
- }
187
- />
188
- </Flex>
189
- </Card>
190
- ))}
191
- </Stack>
192
- )}
193
- </Stack>
194
- </Card>
195
- );
196
- }
@@ -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
- pageBridgePlugin,
5
- createPageBridgeStructureResolver,
6
- createPageBridgeStructure,
7
- PAGEBRIDGE_TYPES,
8
- type PageBridgePluginConfig,
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 PageBridgePluginConfig {
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 createPageBridgeStructureResolver = (
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 pageBridgePlugin = definePlugin<PageBridgePluginConfig | 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();
@@ -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
- });
@@ -1,131 +0,0 @@
1
- import { defineType, defineField } from "sanity";
2
-
3
- export interface GscSnapshotOptions {
4
- contentTypes?: string[];
5
- }
6
-
7
- export const createGscSnapshot = (options: GscSnapshotOptions = {}) => {
8
- const contentTypes = options.contentTypes ?? [];
9
-
10
- return defineType({
11
- name: "gscSnapshot",
12
- title: "GSC Snapshot",
13
- type: "document",
14
- fields: [
15
- defineField({
16
- name: "site",
17
- title: "Site",
18
- type: "reference",
19
- to: [{ type: "gscSite" }],
20
- validation: (Rule) => Rule.required(),
21
- }),
22
- defineField({
23
- name: "page",
24
- title: "Page URL",
25
- type: "string",
26
- validation: (Rule) => Rule.required(),
27
- }),
28
- ...(contentTypes.length > 0
29
- ? [
30
- defineField({
31
- name: "linkedDocument",
32
- title: "Linked Document",
33
- type: "reference",
34
- to: contentTypes.map((type) => ({ type })),
35
- description: "Auto-matched Sanity document",
36
- }),
37
- ]
38
- : []),
39
- defineField({
40
- name: "period",
41
- title: "Period",
42
- type: "string",
43
- options: {
44
- list: ["last7", "last28", "last90"],
45
- },
46
- }),
47
- defineField({
48
- name: "clicks",
49
- type: "number",
50
- }),
51
- defineField({
52
- name: "impressions",
53
- type: "number",
54
- }),
55
- defineField({
56
- name: "ctr",
57
- title: "CTR",
58
- type: "number",
59
- description: "Stored as decimal (0.05 = 5%)",
60
- }),
61
- defineField({
62
- name: "position",
63
- title: "Average Position",
64
- type: "number",
65
- }),
66
- defineField({
67
- name: "topQueries",
68
- title: "Top Queries",
69
- type: "array",
70
- of: [
71
- {
72
- type: "object",
73
- fields: [
74
- defineField({ name: "query", type: "string" }),
75
- defineField({ name: "clicks", type: "number" }),
76
- defineField({ name: "impressions", type: "number" }),
77
- defineField({ name: "position", type: "number" }),
78
- ],
79
- },
80
- ],
81
- }),
82
- defineField({
83
- name: "fetchedAt",
84
- title: "Fetched At",
85
- type: "datetime",
86
- }),
87
- defineField({
88
- name: "indexStatus",
89
- title: "Index Status",
90
- type: "object",
91
- fields: [
92
- defineField({
93
- name: "verdict",
94
- title: "Verdict",
95
- type: "string",
96
- options: {
97
- list: ["indexed", "not_indexed", "excluded"],
98
- },
99
- }),
100
- defineField({
101
- name: "coverageState",
102
- title: "Coverage State",
103
- type: "string",
104
- description: "Human-readable index status from Google",
105
- }),
106
- defineField({
107
- name: "lastCrawlTime",
108
- title: "Last Crawl Time",
109
- type: "datetime",
110
- }),
111
- defineField({
112
- name: "robotsTxtState",
113
- title: "Robots.txt State",
114
- type: "string",
115
- }),
116
- defineField({
117
- name: "pageFetchState",
118
- title: "Page Fetch State",
119
- type: "string",
120
- }),
121
- ],
122
- }),
123
- ],
124
- preview: {
125
- select: { title: "page", subtitle: "period" },
126
- },
127
- });
128
- };
129
-
130
- // Default export for backward compatibility (without linkedDocument)
131
- export const gscSnapshot = createGscSnapshot();
@@ -1,11 +0,0 @@
1
- export { gscSite } from "./gscSite";
2
- export {
3
- gscSnapshot,
4
- createGscSnapshot,
5
- type GscSnapshotOptions,
6
- } from "./gscSnapshot";
7
- export {
8
- gscRefreshTask,
9
- createGscRefreshTask,
10
- type GscRefreshTaskOptions,
11
- } from "./gscRefreshTask";
package/tsconfig.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "outDir": "./dist",
4
- "rootDir": "./src",
5
- "jsx": "react-jsx",
6
- "declaration": true,
7
- "declarationMap": true,
8
- "esModuleInterop": true,
9
- "incremental": false,
10
- "isolatedModules": true,
11
- "lib": ["es2022", "DOM", "DOM.Iterable"],
12
- "module": "ESNext",
13
- "moduleDetection": "force",
14
- "moduleResolution": "Bundler",
15
- "noUncheckedIndexedAccess": true,
16
- "resolveJsonModule": true,
17
- "skipLibCheck": true,
18
- "strict": true,
19
- "target": "ES2022"
20
- },
21
- "include": ["src/**/*"],
22
- "exclude": ["node_modules", "dist"]
23
- }