From 1ef41de8649578837196d513c77bec81484519d4 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Fri, 16 Oct 2026 17:01:50 -0400 Subject: [PATCH] fix(site,sanity): enable Sanity Presentation iframe preview and visual editing --- apps/sanity/sanity.config.ts | 8 +++++-- .../site/src/components/DraftModeToggle.astro | 14 ++++++++++++ apps/site/src/lib/sanity/config.ts | 2 +- apps/site/src/middleware.ts | 6 +++-- apps/site/src/pages/api/draft-mode/disable.ts | 20 +++++++++++++++-- apps/site/src/pages/api/draft-mode/enable.ts | 22 ++++++++++++++----- apps/site/tests/tier1-features.test.mjs | 3 ++- apps/site/wrangler.jsonc | 4 ++-- 8 files changed, 64 insertions(+), 15 deletions(-) diff --git a/apps/sanity/sanity.config.ts b/apps/sanity/sanity.config.ts index 1b712b1d5..56ec16fe9 100644 --- a/apps/sanity/sanity.config.ts +++ b/apps/sanity/sanity.config.ts @@ -63,11 +63,11 @@ const apiVersion = process.env.SANITY_STUDIO_API_VERSION || "2025-09-30"; const presentationEnabled = process.env.SANITY_STUDIO_DISABLE_PRESENTATION !== "true"; -// Use local Next.js dev server for presentation preview when running Studio locally +// Use local Astro dev server for presentation preview when running Studio locally const isLocal = typeof import.meta !== "undefined" && (import.meta as unknown as { env?: { DEV?: boolean } }).env?.DEV; -const localPreviewOrigin = "http://localhost:3000"; +const localPreviewOrigin = "http://localhost:4321"; // ── Shared helpers ─────────────────────────────────────────────────── function resolveHref(type: string, slug?: string): string | undefined { @@ -242,6 +242,10 @@ function buildPlugins(previewUrl: string): PluginOptions[] { route: "/post/:slug", filter: `_type == "post" && slug.current == $slug`, }, + { + route: "/podcast/:slug", + filter: `_type == "podcast" && slug.current == $slug`, + }, ]), locations: { settings: defineLocations({ diff --git a/apps/site/src/components/DraftModeToggle.astro b/apps/site/src/components/DraftModeToggle.astro index 999e8eae5..17bf0a03d 100644 --- a/apps/site/src/components/DraftModeToggle.astro +++ b/apps/site/src/components/DraftModeToggle.astro @@ -20,3 +20,17 @@ const returnTo = Astro.url.pathname + Astro.url.search; Exit draft mode + + diff --git a/apps/site/src/lib/sanity/config.ts b/apps/site/src/lib/sanity/config.ts index 813cb87c9..5b2a02e03 100644 --- a/apps/site/src/lib/sanity/config.ts +++ b/apps/site/src/lib/sanity/config.ts @@ -28,7 +28,7 @@ const DEFAULTS = { projectId: "hfh83o0w", dataset: "production", apiVersion: "2025-09-30", - studioUrl: "https://codingcat.dev.sanity.studio/production", + studioUrl: "https://codingcat-prod.sanity.studio", } as const; function read(env: EnvLike | undefined, key: string): string | undefined { diff --git a/apps/site/src/middleware.ts b/apps/site/src/middleware.ts index 6e0ea4414..0f7d25edb 100644 --- a/apps/site/src/middleware.ts +++ b/apps/site/src/middleware.ts @@ -90,9 +90,11 @@ export const onRequest = defineMiddleware(async (context, next) => { "max-age=31536000; includeSubDomains; preload", ); response.headers.set("X-Content-Type-Options", "nosniff"); - response.headers.set("X-Frame-Options", "SAMEORIGIN"); response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); - response.headers.set("Content-Security-Policy", "frame-ancestors 'self'"); + response.headers.set( + "Content-Security-Policy", + "frame-ancestors 'self' https://*.sanity.studio http://localhost:3333", + ); // AIPREF / AI Preference Headers response.headers.set("Content-Usage", "train-ai=n, search=y"); diff --git a/apps/site/src/pages/api/draft-mode/disable.ts b/apps/site/src/pages/api/draft-mode/disable.ts index 360316393..dee57280c 100644 --- a/apps/site/src/pages/api/draft-mode/disable.ts +++ b/apps/site/src/pages/api/draft-mode/disable.ts @@ -45,17 +45,33 @@ export const POST: APIRoute = async ({ request, url, cookies, redirect }) => { returnTo = url.searchParams.get("returnTo"); } + const isHttps = url.protocol === "https:"; cookies.delete(PREVIEW_COOKIE, { path: "/" }); - cookies.set(PREVIEW_COOKIE, "", { maxAge: 0, path: "/" }); + cookies.set(PREVIEW_COOKIE, "", { + maxAge: 0, + path: "/", + httpOnly: true, + secure: isHttps, + sameSite: isHttps ? "none" : "lax", + partitioned: isHttps, + }); return redirect(resolveDestination(returnTo), 303); }; export const GET: APIRoute = async ({ url, cookies, redirect }) => { const returnTo = url.searchParams.get("returnTo"); + const isHttps = url.protocol === "https:"; cookies.delete(PREVIEW_COOKIE, { path: "/" }); - cookies.set(PREVIEW_COOKIE, "", { maxAge: 0, path: "/" }); + cookies.set(PREVIEW_COOKIE, "", { + maxAge: 0, + path: "/", + httpOnly: true, + secure: isHttps, + sameSite: isHttps ? "none" : "lax", + partitioned: isHttps, + }); return redirect(resolveDestination(returnTo), 307); }; diff --git a/apps/site/src/pages/api/draft-mode/enable.ts b/apps/site/src/pages/api/draft-mode/enable.ts index 2b240725e..805907a8a 100644 --- a/apps/site/src/pages/api/draft-mode/enable.ts +++ b/apps/site/src/pages/api/draft-mode/enable.ts @@ -10,10 +10,15 @@ export const GET: APIRoute = async ({ url, cookies, redirect }) => { (import.meta.env as Record) ?.SANITY_API_READ_TOKEN; - // Query params supported: secret, slug, returnTo - const secret = url.searchParams.get("secret"); + // Query params supported: sanity-preview-secret/secret, sanity-preview-pathname/returnTo, slug, sanity-preview-perspective + const secret = + url.searchParams.get("sanity-preview-secret") ?? + url.searchParams.get("secret"); const slug = url.searchParams.get("slug"); - const returnTo = url.searchParams.get("returnTo"); + const returnTo = + url.searchParams.get("sanity-preview-pathname") ?? + url.searchParams.get("returnTo"); + const perspective = url.searchParams.get("sanity-preview-perspective"); if (!readToken) { console.warn( @@ -35,11 +40,18 @@ export const GET: APIRoute = async ({ url, cookies, redirect }) => { destination = "/"; } + if (perspective && !destination.includes("sanity-preview-perspective=")) { + const sep = destination.includes("?") ? "&" : "?"; + destination = `${destination}${sep}sanity-preview-perspective=${encodeURIComponent(perspective)}`; + } + + const isHttps = url.protocol === "https:"; cookies.set(PREVIEW_COOKIE, "true", { path: "/", httpOnly: true, - secure: true, - sameSite: "lax", + secure: isHttps, + sameSite: isHttps ? "none" : "lax", + partitioned: isHttps, }); return redirect(destination, 307); diff --git a/apps/site/tests/tier1-features.test.mjs b/apps/site/tests/tier1-features.test.mjs index 7c03d9e66..a04b5cd68 100644 --- a/apps/site/tests/tier1-features.test.mjs +++ b/apps/site/tests/tier1-features.test.mjs @@ -877,7 +877,8 @@ describe("Tier 1 - Feature Coverage", () => { assert.equal(setCookieVal, "true"); assert.equal(setCookieOpts.httpOnly, true); assert.equal(setCookieOpts.secure, true); - assert.equal(setCookieOpts.sameSite, "lax"); + assert.equal(setCookieOpts.sameSite, "none"); + assert.equal(setCookieOpts.partitioned, true); }); it("6.7 POST and GET /api/draft-mode/disable clears __sanity_preview cookie and redirects", async () => { diff --git a/apps/site/wrangler.jsonc b/apps/site/wrangler.jsonc index 573c03a28..1954017ad 100644 --- a/apps/site/wrangler.jsonc +++ b/apps/site/wrangler.jsonc @@ -37,7 +37,7 @@ "PUBLIC_SANITY_PROJECT_ID": "hfh83o0w", "PUBLIC_SANITY_DATASET": "dev", "PUBLIC_SANITY_API_VERSION": "2025-09-30", - "PUBLIC_SANITY_STUDIO_URL": "https://codingcat-dev.sanity.studio/dev", + "PUBLIC_SANITY_STUDIO_URL": "https://codingcat-dev.sanity.studio", "SITE_URL": "https://dev.codingcat.dev", "FLIPCORE_API_URL": "https://api.flipfeeds.dev", "PUBLIC_FLIPCORE_WEB_URL": "https://flipfeeds.dev" @@ -76,7 +76,7 @@ "PUBLIC_SANITY_PROJECT_ID": "hfh83o0w", "PUBLIC_SANITY_DATASET": "production", "PUBLIC_SANITY_API_VERSION": "2025-09-30", - "PUBLIC_SANITY_STUDIO_URL": "https://codingcat-dev.sanity.studio/production", + "PUBLIC_SANITY_STUDIO_URL": "https://codingcat-prod.sanity.studio", "SITE_URL": "https://codingcat.dev", "FLIPCORE_API_URL": "https://api.flipfeeds.com", "PUBLIC_FLIPCORE_WEB_URL": "https://flipfeeds.com"