|
| 1 | +import { NextResponse } from "next/server"; |
| 2 | +import { visibleIconMeta } from "@/registry/icon-meta.gen"; |
| 3 | + |
| 4 | +/** |
| 5 | + * The icon's AI prompt, read from Sanity. |
| 6 | + * |
| 7 | + * This is the site's ONLY read from Sanity, and it is deliberately confined to |
| 8 | + * one field on one route. `aiPrompt` is the one thing Sanity is authoritative |
| 9 | + * for — it exists nowhere in the repo — whereas the grid, ICON_COUNT and the |
| 10 | + * copy button stay build-time or static (/r/*.json) so a Sanity outage can never |
| 11 | + * take the site down. The blast radius of Sanity being unreachable is this route |
| 12 | + * returning 502 and one button showing an error toast. |
| 13 | + * |
| 14 | + * Server-side on purpose: the projectId/dataset never have to be shipped to the |
| 15 | + * browser, and the ISR cache means Sanity sees roughly one request per icon per |
| 16 | + * revalidate window rather than one per visitor. No token is needed — the |
| 17 | + * dataset is public and this reads published content only. |
| 18 | + */ |
| 19 | + |
| 20 | +const PROJECT_ID = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID; |
| 21 | +const DATASET = process.env.NEXT_PUBLIC_SANITY_DATASET; |
| 22 | +const API_VERSION = process.env.NEXT_PUBLIC_SANITY_API_VERSION ?? "2025-02-19"; |
| 23 | + |
| 24 | +// Studio edits appear within this window. Prompts change rarely, so 5 minutes |
| 25 | +// trades negligible staleness for a near-zero request rate against Sanity. |
| 26 | +const REVALIDATE_SECONDS = 300; |
| 27 | + |
| 28 | +export async function GET(_req: Request, ctx: { params: Promise<{ slug: string }> }) { |
| 29 | + const { slug } = await ctx.params; |
| 30 | + |
| 31 | + // Validate against the registry rather than passing the path straight into a |
| 32 | + // query: the slug is user-controlled, and this keeps an arbitrary string from |
| 33 | + // ever reaching GROQ. |
| 34 | + if (!visibleIconMeta.some((e) => e.slug === slug)) { |
| 35 | + return NextResponse.json({ error: `unknown icon "${slug}"` }, { status: 404 }); |
| 36 | + } |
| 37 | + |
| 38 | + if (!PROJECT_ID || !DATASET) { |
| 39 | + return NextResponse.json( |
| 40 | + { error: "Sanity is not configured (NEXT_PUBLIC_SANITY_PROJECT_ID / _DATASET)" }, |
| 41 | + { status: 501 }, |
| 42 | + ); |
| 43 | + } |
| 44 | + |
| 45 | + const query = `*[_type == "icon" && slug == $slug][0].aiPrompt`; |
| 46 | + const url = |
| 47 | + `https://${PROJECT_ID}.apicdn.sanity.io/v${API_VERSION}/data/query/${DATASET}` + |
| 48 | + `?query=${encodeURIComponent(query)}` + |
| 49 | + `&$slug=${encodeURIComponent(JSON.stringify(slug))}`; |
| 50 | + |
| 51 | + try { |
| 52 | + const res = await fetch(url, { next: { revalidate: REVALIDATE_SECONDS } }); |
| 53 | + if (!res.ok) { |
| 54 | + return NextResponse.json({ error: `Sanity responded ${res.status}` }, { status: 502 }); |
| 55 | + } |
| 56 | + const { result } = (await res.json()) as { result?: string | null }; |
| 57 | + if (!result) { |
| 58 | + return NextResponse.json({ error: `no prompt written for "${slug}" yet` }, { status: 404 }); |
| 59 | + } |
| 60 | + return NextResponse.json({ slug, prompt: result }); |
| 61 | + } catch { |
| 62 | + return NextResponse.json({ error: "could not reach Sanity" }, { status: 502 }); |
| 63 | + } |
| 64 | +} |
0 commit comments