import React from "react"; import { Stack, Card, Flex, Text, useTheme } from "@sanity/ui"; import CheckCircleIcon from "./icons/CheckCircleIcon"; import EmptyCircleIcon from "./icons/EmptyCircleIcon"; import ProgressRing from "./icons/ProgressRing"; interface GEOItem { label: string; pass: boolean; description: string; } /* eslint-disable @typescript-eslint/no-explicit-any */ export function buildGEOChecklist(value: Record | undefined): GEOItem[] { const v = value || {}; const kw = (v.focusKeyword || "").toLowerCase().trim(); return [ { label: "Structured data configured", pass: true, description: "Plugin generates JSON-LD automatically", }, { label: "Description is answer-ready", pass: (v.metaDescription || "").length >= 100 && (v.metaDescription || "").length <= 160, description: "Optimal length for AI snippet extraction (100–160 chars)", }, { label: "OG image present", pass: Boolean(v.openGraph?.image?.asset), description: "AI visual search and social sharing require an image", }, { label: "Keyword in title and description", pass: kw ? v.metaTitle?.toLowerCase().includes(kw) && v.metaDescription?.toLowerCase().includes(kw) : false, description: "Keyword consistency signals topical relevance to AI crawlers", }, { label: "Open Graph fully configured", pass: Boolean(v.openGraph?.title && v.openGraph?.description && v.openGraph?.image?.asset), description: "Required for social sharing and AI citation previews", }, ]; } function badgeTone(passCount: number, total: number): "positive" | "caution" | "critical" { if (passCount === total) return "positive"; if (passCount >= total / 2) return "caution"; return "critical"; } function getStatusColor(tone: "positive" | "caution" | "critical"): string { if (tone === "positive") return "#10b981"; if (tone === "caution") return "#f59e0b"; return "#ef4444"; } export default function GEOChecklist({ value }: { value: Record | undefined }) { const items = buildGEOChecklist(value); const passCount = items.filter((i) => i.pass).length; const total = items.length; const theme = useTheme(); const isDarkMode = theme.sanity.v2?.color._dark ?? (theme.sanity as unknown as { color: { dark: boolean } }).color.dark; const tone = badgeTone(passCount, total); const statusColor = getStatusColor(tone); return ( GEO Score AI Search Visibility How well this page is optimised for ChatGPT, Perplexity, and Google AI Overviews {items.map((item) => { const hasDescription = !item.pass && item.description; const Icon = item.pass ? ( ) : ( ); return ( {Icon} {item.label} {hasDescription && ( {item.description} )} ); })} ); }