@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.
Files changed (37) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +228 -214
  3. package/dist/components/RefreshQueueTool.js +10 -10
  4. package/dist/components/SearchPerformancePane.js +10 -10
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/plugin.d.ts +3 -3
  9. package/dist/plugin.d.ts.map +1 -1
  10. package/dist/plugin.js +2 -2
  11. package/package.json +20 -1
  12. package/.turbo/turbo-build.log +0 -2
  13. package/dist/components/DecayAlertCard.d.ts +0 -16
  14. package/dist/components/DecayAlertCard.d.ts.map +0 -1
  15. package/dist/components/DecayAlertCard.js +0 -24
  16. package/dist/components/DecayBadge.d.ts +0 -3
  17. package/dist/components/DecayBadge.d.ts.map +0 -1
  18. package/dist/components/DecayBadge.js +0 -19
  19. package/dist/components/PerformanceChart.d.ts +0 -12
  20. package/dist/components/PerformanceChart.d.ts.map +0 -1
  21. package/dist/components/PerformanceChart.js +0 -18
  22. package/dist/components/PerformanceInspector.d.ts +0 -3
  23. package/dist/components/PerformanceInspector.d.ts.map +0 -1
  24. package/dist/components/PerformanceInspector.js +0 -174
  25. package/dist/schemas/gscKeywordTarget.d.ts +0 -22
  26. package/dist/schemas/gscKeywordTarget.d.ts.map +0 -1
  27. package/dist/schemas/gscKeywordTarget.js +0 -43
  28. package/eslint.config.js +0 -3
  29. package/src/components/RefreshQueueTool.tsx +0 -196
  30. package/src/components/SearchPerformancePane.tsx +0 -237
  31. package/src/index.ts +0 -18
  32. package/src/plugin.ts +0 -120
  33. package/src/schemas/gscRefreshTask.ts +0 -203
  34. package/src/schemas/gscSite.ts +0 -71
  35. package/src/schemas/gscSnapshot.ts +0 -131
  36. package/src/schemas/index.ts +0 -11
  37. package/tsconfig.json +0 -23
@@ -1,16 +0,0 @@
1
- interface DecayMetrics {
2
- positionBefore?: number;
3
- positionNow?: number;
4
- positionDelta?: number;
5
- ctrBefore?: number;
6
- ctrNow?: number;
7
- impressions?: number;
8
- }
9
- interface DecayAlertCardProps {
10
- reason: "position_decay" | "low_ctr" | "impressions_drop" | "manual";
11
- severity: "low" | "medium" | "high";
12
- metrics?: DecayMetrics;
13
- }
14
- export declare function DecayAlertCard({ reason, severity, metrics, }: DecayAlertCardProps): import("react/jsx-runtime").JSX.Element;
15
- export {};
16
- //# sourceMappingURL=DecayAlertCard.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"DecayAlertCard.d.ts","sourceRoot":"","sources":["../../src/components/DecayAlertCard.tsx"],"names":[],"mappings":"AAGA,UAAU,YAAY;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,mBAAmB;IAC3B,MAAM,EAAE,gBAAgB,GAAG,SAAS,GAAG,kBAAkB,GAAG,QAAQ,CAAC;IACrE,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAwBD,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,QAAQ,EACR,OAAO,GACR,EAAE,mBAAmB,2CAkBrB"}
@@ -1,24 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Card, Stack, Text, Flex } from "@sanity/ui";
3
- import { WarningOutlineIcon } from "@sanity/icons";
4
- function getDecayMessage(reason, metrics) {
5
- switch (reason) {
6
- case "position_decay":
7
- if (metrics?.positionBefore != null && metrics?.positionNow != null) {
8
- return `Position dropped ${metrics.positionBefore.toFixed(0)} \u2192 ${metrics.positionNow.toFixed(0)} over 30 days`;
9
- }
10
- return "Search position has declined over 30 days";
11
- case "low_ctr":
12
- if (metrics?.ctrNow != null) {
13
- return `CTR at ${(metrics.ctrNow * 100).toFixed(1)}% despite top-10 position`;
14
- }
15
- return "Click-through rate is below threshold";
16
- case "impressions_drop":
17
- return "Impressions dropped significantly over 30 days";
18
- case "manual":
19
- return "Manually flagged for review";
20
- }
21
- }
22
- export function DecayAlertCard({ reason, severity, metrics, }) {
23
- return (_jsx(Card, { padding: 3, radius: 2, tone: "critical", children: _jsxs(Stack, { space: 2, children: [_jsxs(Flex, { align: "center", gap: 2, children: [_jsx(Text, { size: 1, children: _jsx(WarningOutlineIcon, {}) }), _jsx(Text, { size: 1, weight: "semibold", children: "Decay Detected" })] }), _jsx(Text, { size: 1, muted: true, children: getDecayMessage(reason, metrics) })] }) }));
24
- }
@@ -1,3 +0,0 @@
1
- import type { DocumentBadgeComponent } from "sanity";
2
- export declare const DecayBadge: DocumentBadgeComponent;
3
- //# sourceMappingURL=DecayBadge.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"DecayBadge.d.ts","sourceRoot":"","sources":["../../src/components/DecayBadge.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAErD,eAAO,MAAM,UAAU,EAAE,sBAqBxB,CAAC"}
@@ -1,19 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { useClient } from "sanity";
3
- export const DecayBadge = (props) => {
4
- const client = useClient({ apiVersion: "2025-02-07" });
5
- const [hasDecay, setHasDecay] = useState(false);
6
- useEffect(() => {
7
- client
8
- .fetch(`count(*[_type == "gscRefreshTask" && linkedDocument._ref == $id && status == "open"])`, { id: props.id })
9
- .then((count) => setHasDecay(count > 0))
10
- .catch(() => setHasDecay(false));
11
- }, [props.id, client]);
12
- if (!hasDecay)
13
- return null;
14
- return {
15
- label: "Decay detected",
16
- title: "Content decay has been detected for this document",
17
- color: "danger",
18
- };
19
- };
@@ -1,12 +0,0 @@
1
- export interface ChartBar {
2
- /** Weekly click value for this bar */
3
- value: number;
4
- /** Whether this segment is trending down relative to the longer-term average */
5
- declining: boolean;
6
- }
7
- interface PerformanceChartProps {
8
- bars: ChartBar[];
9
- }
10
- export declare function PerformanceChart({ bars }: PerformanceChartProps): import("react/jsx-runtime").JSX.Element;
11
- export {};
12
- //# sourceMappingURL=PerformanceChart.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PerformanceChart.d.ts","sourceRoot":"","sources":["../../src/components/PerformanceChart.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACvB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,qBAAqB;IAC7B,IAAI,EAAE,QAAQ,EAAE,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,EAAE,qBAAqB,2CA+C/D"}
@@ -1,18 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Card, Stack, Text, Flex } from "@sanity/ui";
3
- export function PerformanceChart({ bars }) {
4
- const maxValue = Math.max(...bars.map((b) => b.value), 1);
5
- return (_jsx(Card, { padding: 3, radius: 2, shadow: 1, children: _jsxs(Stack, { space: 3, children: [_jsxs(Stack, { space: 1, children: [_jsx(Text, { size: 0, weight: "semibold", muted: true, children: "PERFORMANCE (30D)" }), _jsx(Text, { size: 0, muted: true, children: "Weekly clicks trend" })] }), _jsx("div", { style: {
6
- display: "flex",
7
- alignItems: "flex-end",
8
- height: 120,
9
- gap: 3,
10
- padding: "0 4px",
11
- }, children: bars.map((bar, i) => (_jsx("div", { style: {
12
- flex: 1,
13
- height: `${Math.max((bar.value / maxValue) * 100, 4)}%`,
14
- backgroundColor: bar.declining ? "#f4726d" : "#43d675",
15
- borderRadius: "3px 3px 0 0",
16
- minHeight: 4,
17
- } }, i))) }), _jsxs(Flex, { justify: "space-between", style: { padding: "0 4px" }, children: [_jsx(Text, { size: 0, muted: true, children: "older" }), _jsx(Text, { size: 0, muted: true, children: "recent" })] })] }) }));
18
- }
@@ -1,3 +0,0 @@
1
- import type { DocumentInspectorProps } from "sanity";
2
- export declare function PerformanceInspector({ documentId, }: DocumentInspectorProps): import("react/jsx-runtime").JSX.Element;
3
- //# sourceMappingURL=PerformanceInspector.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PerformanceInspector.d.ts","sourceRoot":"","sources":["../../src/components/PerformanceInspector.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAiJrD,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,GACX,EAAE,sBAAsB,2CA+RxB"}
@@ -1,174 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState, useCallback } from "react";
3
- import { useClient } from "sanity";
4
- import { Card, Stack, Text, Badge, Flex, Spinner, Box, TextInput, Button, } from "@sanity/ui";
5
- import { AddIcon, TrashIcon, CheckmarkCircleIcon, CircleIcon, } from "@sanity/icons";
6
- import { PerformanceChart } from "./PerformanceChart";
7
- import { DecayAlertCard } from "./DecayAlertCard";
8
- /**
9
- * Deduplicate snapshots — keep only the most recent per period.
10
- */
11
- function deduplicateSnapshots(snapshots) {
12
- const byPeriod = new Map();
13
- for (const snap of snapshots) {
14
- const existing = byPeriod.get(snap.period);
15
- if (!existing || snap.fetchedAt > existing.fetchedAt) {
16
- byPeriod.set(snap.period, snap);
17
- }
18
- }
19
- return byPeriod;
20
- }
21
- /**
22
- * Build ~12 weekly bars from period aggregates.
23
- *
24
- * Derives weekly click totals from the 3 period snapshots, then colors
25
- * each bar based on the overall trend direction:
26
- * - If the page is declining (recent < older), older bars are green
27
- * and recent bars are red — showing the transition from healthy to declining.
28
- * - If the page is stable/growing, all bars are green.
29
- */
30
- function buildChartBars(snapshots) {
31
- const s7 = snapshots.get("last7");
32
- const s28 = snapshots.get("last28");
33
- const s90 = snapshots.get("last90");
34
- if (!s28 && !s7)
35
- return [];
36
- const clicks7 = s7?.clicks ?? 0;
37
- const clicks28 = s28?.clicks ?? clicks7;
38
- const clicks90 = s90?.clicks ?? clicks28;
39
- // Derive weekly values for each segment
40
- const recentWeekly = clicks7; // most recent week
41
- const midWeekly = Math.max(clicks28 - clicks7, 0) / 3; // weeks 2-4 avg
42
- const olderWeekly = s90 ? Math.max(clicks90 - clicks28, 0) / 8 : 0; // weeks 5-12 avg
43
- // Determine overall trend: is recent worse than historical?
44
- const historicalWeeklyAvg = s90
45
- ? (clicks90 - clicks7) / 11
46
- : s28
47
- ? (clicks28 - clicks7) / 3
48
- : clicks7;
49
- const isDeclining = recentWeekly < historicalWeeklyAvg * 0.85;
50
- const bars = [];
51
- // Older weeks (5-12) — 8 bars — green if declining (these were the good days)
52
- if (s90) {
53
- for (let i = 0; i < 8; i++) {
54
- bars.push({ value: olderWeekly, declining: false });
55
- }
56
- }
57
- // Middle weeks (2-4) — 3 bars — transition zone
58
- if (s28) {
59
- for (let i = 0; i < 3; i++) {
60
- bars.push({
61
- value: midWeekly,
62
- declining: isDeclining && midWeekly < historicalWeeklyAvg * 0.85,
63
- });
64
- }
65
- }
66
- // Most recent week — 1 bar — red if the page is declining
67
- bars.push({ value: recentWeekly, declining: isDeclining });
68
- return bars;
69
- }
70
- /**
71
- * Check if a target keyword matches any of the top queries.
72
- * Uses case-insensitive substring matching.
73
- */
74
- function isKeywordInQueries(keyword, queries) {
75
- const lower = keyword.toLowerCase();
76
- return queries.some((q) => q.query.toLowerCase().includes(lower));
77
- }
78
- /**
79
- * Check if a query matches any of the target keywords.
80
- */
81
- function isQueryInTargets(query, targets) {
82
- const lower = query.toLowerCase();
83
- return targets.some((t) => lower.includes(t.toLowerCase()));
84
- }
85
- export function PerformanceInspector({ documentId, }) {
86
- const client = useClient({ apiVersion: "2024-01-01" });
87
- const [snapshots, setSnapshots] = useState(new Map());
88
- const [decayTask, setDecayTask] = useState(null);
89
- const [keywordTarget, setKeywordTarget] = useState(null);
90
- const [newKeyword, setNewKeyword] = useState("");
91
- const [loading, setLoading] = useState(true);
92
- const fetchData = useCallback(async () => {
93
- const [snapshotResults, taskResult, keywordResult] = await Promise.all([
94
- client.fetch(`*[_type == "gscSnapshot" && linkedDocument._ref == $id] | order(fetchedAt desc) {
95
- period,
96
- clicks,
97
- impressions,
98
- ctr,
99
- position,
100
- topQueries,
101
- fetchedAt
102
- }`, { id: documentId }),
103
- client.fetch(`*[_type == "gscRefreshTask" && linkedDocument._ref == $id && status == "open"] | order(severity desc)[0] {
104
- reason,
105
- severity,
106
- metrics
107
- }`, { id: documentId }),
108
- client.fetch(`*[_type == "gscKeywordTarget" && linkedDocument._ref == $id][0] {
109
- _id,
110
- keywords
111
- }`, { id: documentId }),
112
- ]);
113
- setSnapshots(deduplicateSnapshots(snapshotResults ?? []));
114
- setDecayTask(taskResult);
115
- setKeywordTarget(keywordResult);
116
- setLoading(false);
117
- }, [documentId, client]);
118
- useEffect(() => {
119
- fetchData();
120
- }, [fetchData]);
121
- const addKeyword = useCallback(async () => {
122
- const keyword = newKeyword.trim();
123
- if (!keyword)
124
- return;
125
- if (keywordTarget) {
126
- const updated = [...(keywordTarget.keywords ?? []), keyword];
127
- await client.patch(keywordTarget._id).set({ keywords: updated }).commit();
128
- setKeywordTarget({ ...keywordTarget, keywords: updated });
129
- }
130
- else {
131
- const doc = await client.create({
132
- _type: "gscKeywordTarget",
133
- linkedDocument: { _type: "reference", _ref: documentId },
134
- keywords: [keyword],
135
- });
136
- setKeywordTarget({ _id: doc._id, keywords: [keyword] });
137
- }
138
- setNewKeyword("");
139
- }, [newKeyword, keywordTarget, documentId, client]);
140
- const removeKeyword = useCallback(async (keyword) => {
141
- if (!keywordTarget)
142
- return;
143
- const updated = keywordTarget.keywords.filter((k) => k !== keyword);
144
- await client.patch(keywordTarget._id).set({ keywords: updated }).commit();
145
- setKeywordTarget({ ...keywordTarget, keywords: updated });
146
- }, [keywordTarget, client]);
147
- if (loading) {
148
- return (_jsx(Card, { padding: 4, children: _jsx(Flex, { justify: "center", align: "center", style: { minHeight: 200 }, children: _jsx(Spinner, {}) }) }));
149
- }
150
- const snapshot28 = snapshots.get("last28");
151
- const chartBars = buildChartBars(snapshots);
152
- const topQueries = snapshot28?.topQueries?.slice(0, 5) ?? [];
153
- const targetKeywords = keywordTarget?.keywords ?? [];
154
- return (_jsx(Card, { padding: 4, style: { height: "100%", overflowY: "auto" }, children: _jsxs(Stack, { space: 4, children: [_jsxs(Flex, { justify: "space-between", align: "center", children: [_jsxs(Flex, { align: "center", gap: 2, children: [_jsx("div", { style: {
155
- width: 10,
156
- height: 10,
157
- borderRadius: "50%",
158
- backgroundColor: "#43d675",
159
- flexShrink: 0,
160
- } }), _jsx(Text, { weight: "semibold", size: 2, children: "PageBridge" })] }), _jsx(Badge, { tone: "primary", fontSize: 0, children: "Live GSC data" })] }), chartBars.length > 0 ? (_jsx(PerformanceChart, { bars: chartBars })) : (_jsx(Card, { padding: 4, tone: "caution", radius: 2, children: _jsx(Text, { size: 1, children: "No performance data available yet." }) })), decayTask && (_jsx(DecayAlertCard, { reason: decayTask.reason, severity: decayTask.severity, metrics: decayTask.metrics })), snapshot28 && (_jsxs(Flex, { gap: 3, children: [_jsx(Card, { padding: 3, radius: 2, shadow: 1, style: { flex: 1 }, children: _jsxs(Stack, { space: 2, children: [_jsx(Text, { size: 0, muted: true, children: "Clicks" }), _jsx(Text, { size: 3, weight: "semibold", children: snapshot28.clicks.toLocaleString() })] }) }), _jsx(Card, { padding: 3, radius: 2, shadow: 1, style: { flex: 1 }, children: _jsxs(Stack, { space: 2, children: [_jsx(Text, { size: 0, muted: true, children: "CTR" }), _jsxs(Text, { size: 3, weight: "semibold", children: [(snapshot28.ctr * 100).toFixed(1), "%"] })] }) })] })), topQueries.length > 0 && (_jsxs(Box, { children: [_jsx(Text, { size: 1, weight: "semibold", style: { marginBottom: 8 }, children: "Top Queries" }), _jsx(Stack, { space: 2, children: topQueries.map((q, i) => {
161
- const isTarget = isQueryInTargets(q.query, targetKeywords);
162
- return (_jsx(Card, { padding: 2, radius: 2, tone: isTarget ? "positive" : "default", shadow: isTarget ? 1 : 0, children: _jsxs(Flex, { justify: "space-between", align: "center", children: [_jsxs(Flex, { align: "center", gap: 2, style: { flex: 1, minWidth: 0 }, children: [isTarget && (_jsx(Text, { size: 0, children: _jsx(CheckmarkCircleIcon, {}) })), _jsx(Text, { size: 1, style: {
163
- overflow: "hidden",
164
- textOverflow: "ellipsis",
165
- whiteSpace: "nowrap",
166
- }, children: q.query })] }), _jsxs(Text, { size: 0, muted: true, style: { flexShrink: 0, marginLeft: 8 }, children: [q.clicks, "cl \u00B7 pos ", q.position.toFixed(1)] })] }) }, i));
167
- }) })] })), _jsxs(Box, { children: [_jsx(Text, { size: 1, weight: "semibold", style: { marginBottom: 8 }, children: "Target Keywords" }), _jsxs(Stack, { space: 2, children: [targetKeywords.map((kw) => {
168
- const isRanking = isKeywordInQueries(kw, topQueries);
169
- return (_jsx(Card, { padding: 2, radius: 2, tone: isRanking ? "positive" : "caution", shadow: 1, children: _jsxs(Flex, { justify: "space-between", align: "center", children: [_jsxs(Flex, { align: "center", gap: 2, style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { size: 0, children: isRanking ? _jsx(CheckmarkCircleIcon, {}) : _jsx(CircleIcon, {}) }), _jsx(Text, { size: 1, children: kw })] }), _jsxs(Flex, { align: "center", gap: 2, style: { flexShrink: 0 }, children: [isRanking && (_jsx(Badge, { tone: "positive", fontSize: 0, children: "Ranking" })), _jsx(Button, { icon: TrashIcon, mode: "ghost", tone: "critical", fontSize: 0, padding: 1, onClick: () => removeKeyword(kw) })] })] }) }, kw));
170
- }), targetKeywords.length === 0 && (_jsx(Text, { size: 1, muted: true, children: "No target keywords set." })), _jsxs(Flex, { gap: 2, children: [_jsx(Box, { style: { flex: 1 }, children: _jsx(TextInput, { fontSize: 1, placeholder: "Add target keyword...", value: newKeyword, onChange: (e) => setNewKeyword(e.target.value), onKeyDown: (e) => {
171
- if (e.key === "Enter")
172
- addKeyword();
173
- } }) }), _jsx(Button, { icon: AddIcon, mode: "ghost", fontSize: 1, padding: 2, onClick: addKeyword, disabled: !newKeyword.trim() })] })] })] })] }) }));
174
- }
@@ -1,22 +0,0 @@
1
- export interface GscKeywordTargetOptions {
2
- contentTypes?: string[];
3
- }
4
- export declare const createGscKeywordTarget: (options?: GscKeywordTargetOptions) => {
5
- type: "document";
6
- name: "gscKeywordTarget";
7
- } & Omit<import("sanity").DocumentDefinition, "preview"> & {
8
- preview?: import("sanity").PreviewConfig<{
9
- title: string;
10
- keywords: string;
11
- }, Record<"title" | "keywords", any>> | undefined;
12
- };
13
- export declare const gscKeywordTarget: {
14
- type: "document";
15
- name: "gscKeywordTarget";
16
- } & Omit<import("sanity").DocumentDefinition, "preview"> & {
17
- preview?: import("sanity").PreviewConfig<{
18
- title: string;
19
- keywords: string;
20
- }, Record<"title" | "keywords", any>> | undefined;
21
- };
22
- //# sourceMappingURL=gscKeywordTarget.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gscKeywordTarget.d.ts","sourceRoot":"","sources":["../../src/schemas/gscKeywordTarget.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,uBAAuB;IACtC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,eAAO,MAAM,sBAAsB,GACjC,UAAS,uBAA4B;;;;;;;;CA0CtC,CAAC;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;CAA2B,CAAC"}
@@ -1,43 +0,0 @@
1
- import { defineType, defineField } from "sanity";
2
- export const createGscKeywordTarget = (options = {}) => {
3
- const contentTypes = options.contentTypes ?? [];
4
- return defineType({
5
- name: "gscKeywordTarget",
6
- title: "GSC Keyword Target",
7
- type: "document",
8
- fields: [
9
- ...(contentTypes.length > 0
10
- ? [
11
- defineField({
12
- name: "linkedDocument",
13
- title: "Linked Document",
14
- type: "reference",
15
- to: contentTypes.map((type) => ({ type })),
16
- validation: (Rule) => Rule.required(),
17
- }),
18
- ]
19
- : []),
20
- defineField({
21
- name: "keywords",
22
- title: "Target Keywords",
23
- type: "array",
24
- of: [{ type: "string" }],
25
- description: "Keywords you want this page to rank for",
26
- }),
27
- ],
28
- preview: {
29
- select: {
30
- title: "linkedDocument.title",
31
- keywords: "keywords",
32
- },
33
- prepare({ title, keywords }) {
34
- const count = keywords?.length ?? 0;
35
- return {
36
- title: title || "Untitled",
37
- subtitle: `${count} target keyword${count !== 1 ? "s" : ""}`,
38
- };
39
- },
40
- },
41
- });
42
- };
43
- export const gscKeywordTarget = createGscKeywordTarget();
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
- }