@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,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 +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 +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,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
|
-
}
|