Skip to content

Commit 277661c

Browse files
committed
local dev build works, but presentation view does not
1 parent a5da7bc commit 277661c

File tree

9 files changed

+222
-40
lines changed

9 files changed

+222
-40
lines changed

.vscode/launch.json

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Astro: Dev (debug server + sanity.ts)",
6+
"type": "node",
7+
"request": "launch",
8+
"runtimeExecutable": "node",
9+
"runtimeArgs": ["--inspect"],
10+
"program": "${workspaceFolder}/apps/web/node_modules/astro/bin/astro.mjs",
11+
"args": ["dev"],
12+
"cwd": "${workspaceFolder}/apps/web",
13+
"console": "integratedTerminal",
14+
"skipFiles": ["<node_internals>/**", "**/node_modules/**"]
15+
},
16+
{
17+
"name": "Astro: Attach to running dev server",
18+
"type": "node",
19+
"request": "attach",
20+
"port": 9229,
21+
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
22+
"sourceMaps": true,
23+
"restart": true
24+
},
25+
{
26+
"name": "Astro: Chrome (client-side)",
27+
"type": "chrome",
28+
"request": "launch",
29+
"url": "http://localhost:4321",
30+
"webRoot": "${workspaceFolder}/apps/web",
31+
"sourceMapPathOverrides": {
32+
"webpack:///./*": "${webRoot}/*",
33+
"webpack:///src/*": "${webRoot}/src/*",
34+
"webpack:///*": "*"
35+
}
36+
},
37+
{
38+
"name": "Astro: Dev + Chrome",
39+
"type": "node",
40+
"request": "launch",
41+
"runtimeExecutable": "node",
42+
"runtimeArgs": ["--inspect"],
43+
"program": "${workspaceFolder}/apps/web/node_modules/astro/bin/astro.mjs",
44+
"args": ["dev"],
45+
"cwd": "${workspaceFolder}/apps/web",
46+
"console": "integratedTerminal",
47+
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
48+
"serverReadyAction": {
49+
"pattern": "Local:.*(https?://[^\\s]+)",
50+
"uriFormat": "%s",
51+
"action": "debugWithChrome"
52+
}
53+
}
54+
],
55+
"compounds": [
56+
{
57+
"name": "Astro: Full (server + browser)",
58+
"configurations": ["Astro: Dev (debug server + sanity.ts)", "Astro: Chrome (client-side)"],
59+
"stopAll": true
60+
}
61+
]
62+
}

apps/sanity/sanity.config.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ const apiVersion = process.env.SANITY_STUDIO_API_VERSION || "2025-09-30";
5858
const presentationEnabled =
5959
process.env.SANITY_STUDIO_DISABLE_PRESENTATION !== "true";
6060

61+
// Use local Astro dev server for presentation preview when running Studio locally
62+
const isLocal = typeof import.meta !== "undefined" && import.meta.env?.DEV;
63+
const localPreviewOrigin = "http://localhost:4321";
64+
6165
// ── Shared helpers ───────────────────────────────────────────────────
6266
function resolveHref(type: string, slug?: string): string | undefined {
6367
switch (type) {
@@ -258,7 +262,9 @@ export default defineConfig([
258262
basePath: "/production",
259263
schema: { types: schemaTypes },
260264
document: { actions: documentActions },
261-
plugins: buildPlugins("https://codingcat.dev"),
265+
plugins: buildPlugins(
266+
isLocal ? localPreviewOrigin : "https://codingcat.dev",
267+
),
262268
},
263269
{
264270
name: "dev",
@@ -268,6 +274,8 @@ export default defineConfig([
268274
basePath: "/dev",
269275
schema: { types: schemaTypes },
270276
document: { actions: documentActions },
271-
plugins: buildPlugins("https://dev.codingcat.dev"),
277+
plugins: buildPlugins(
278+
isLocal ? localPreviewOrigin : "https://dev.codingcat.dev",
279+
),
272280
},
273281
]);

apps/web/.dev.vars.example

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,5 @@ BETTER_AUTH_URL=http://localhost:4321
99
GOOGLE_CLIENT_ID=your-google-client-id
1010
GOOGLE_CLIENT_SECRET=your-google-client-secret
1111

12-
# Sanity (for preview/draft mode — read queries use public API)
13-
SANITY_API_TOKEN=your-sanity-api-token
14-
SANITY_PREVIEW_SECRET=your-preview-secret
12+
# Sanity (viewer token for draft mode / Visual Editing — Presentation tool validates via API)
13+
SANITY_API_READ_TOKEN=your-sanity-viewer-token

apps/web/.env.example

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
SANITY_PROJECT_ID=hfh83o0w
33
SANITY_DATASET=production
44
SANITY_API_TOKEN=
5-
6-
# Preview
7-
SANITY_PREVIEW_SECRET=
5+
# SANITY_API_READ_TOKEN = viewer token for draft mode / Visual Editing (optional; required for Presentation tool)
86

97
# Auth (better-auth)
108
BETTER_AUTH_SECRET=

apps/web/astro.config.mjs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,31 @@ import tailwindcss from "@tailwindcss/vite";
66
import fs from "node:fs";
77
import path from "node:path";
88

9-
// Sanity config — dataset comes from env var (set by wrangler vars or .env)
10-
// In astro.config.mjs, .env files are NOT loaded — use process.env
9+
// Load .env and .env.local into process.env before config runs.
10+
// (Using a function config with loadEnv broke virtual module resolution.)
11+
function loadEnvIntoProcess(dir) {
12+
for (const name of [".env", ".env.local"]) {
13+
const file = path.join(dir, name);
14+
try {
15+
const raw = fs.readFileSync(file, "utf8");
16+
for (const line of raw.split("\n")) {
17+
const trimmed = line.trim();
18+
if (!trimmed || trimmed.startsWith("#")) continue;
19+
const eq = trimmed.indexOf("=");
20+
if (eq === -1) continue;
21+
const key = trimmed.slice(0, eq).trim();
22+
let value = trimmed.slice(eq + 1).trim();
23+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
24+
value = value.slice(1, -1).replace(/\\(.)/g, "$1");
25+
if (!Object.prototype.hasOwnProperty.call(process.env, key)) process.env[key] = value;
26+
}
27+
} catch {
28+
// ignore missing file
29+
}
30+
}
31+
}
32+
loadEnvIntoProcess(process.cwd());
33+
1134
const sanityProjectId = process.env.SANITY_PROJECT_ID || "hfh83o0w";
1235
const sanityDataset = process.env.SANITY_DATASET || "production";
1336

@@ -48,7 +71,7 @@ export default defineConfig({
4871
projectId: sanityProjectId,
4972
dataset: sanityDataset,
5073
useCdn: false,
51-
apiVersion: "2024-01-01",
74+
apiVersion: "2026-03-17",
5275
// Visual Editing: stega encodes edit markers in strings
5376
// Studio is standalone (apps/sanity), not embedded — no studioBasePath
5477
stega: {
@@ -61,6 +84,23 @@ export default defineConfig({
6184
plugins: [tailwindcss(), rawFonts([".ttf", ".otf"])],
6285
assetsInclude: ["**/*.wasm"],
6386
assetsExclude: ["**/*.ttf", "**/*.otf"],
87+
resolve: {
88+
alias: {
89+
// Sanity/visual-editing deps pull Node built-ins into client bundle; polyfill for browser
90+
stream: "stream-browserify",
91+
timers: "timers-browserify",
92+
},
93+
dedupe: ["react", "react-dom", "react-is", "react-compiler-runtime"],
94+
},
95+
optimizeDeps: {
96+
// Force pre-bundle CJS deps so ESM default/named exports work in client (Sanity visual-editing chain)
97+
include: [
98+
"react-is",
99+
"react-compiler-runtime",
100+
"lodash",
101+
"lodash/isObject",
102+
],
103+
},
64104
ssr: {
65105
external: ["buffer", "path", "fs"].map((i) => `node:${i}`),
66106
},

apps/web/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@sanity/astro": "^3.3.1",
1616
"@sanity/client": "^7.17.0",
1717
"@sanity/image-url": "^2.0.3",
18+
"@sanity/preview-url-secret": "^4.0.3",
1819
"@tailwindcss/vite": "^4.2.1",
1920
"@types/react": "^19.2.14",
2021
"@types/react-dom": "^19.2.3",
@@ -24,11 +25,15 @@
2425
"drizzle-orm": "^0.45.1",
2526
"groq": "^5.16.0",
2627
"react": "^19.2.4",
28+
"react-compiler-runtime": "^1.0.0",
2729
"react-dom": "^19.2.4",
30+
"react-is": "^19.2.4",
2831
"tailwindcss": "^4.2.1",
2932
"workers-og": "^0.0.27"
3033
},
3134
"devDependencies": {
32-
"drizzle-kit": "^0.31.9"
35+
"drizzle-kit": "^0.31.9",
36+
"stream-browserify": "^3.0.0",
37+
"timers-browserify": "^2.0.12"
3338
}
3439
}

apps/web/src/pages/api/draft-mode/enable.ts

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,60 @@
22
* Enable draft mode for Sanity Visual Editing.
33
*
44
* Called by the Studio's Presentation Tool when loading the preview iframe.
5-
* Validates SANITY_PREVIEW_SECRET, sets a __sanity_preview cookie, and
6-
* redirects to the requested page.
5+
* Validates the request URL with @sanity/preview-url-secret (secrets live in
6+
* the Sanity dataset). Sets a __sanity_preview cookie and redirects.
77
*
8-
* Usage: /api/draft-mode/enable?secret=<value>&slug=/post/my-post
8+
* Requires SANITY_API_READ_TOKEN (viewer rights). No SANITY_PREVIEW_SECRET needed.
99
*/
1010
import type { APIRoute } from "astro";
11+
import { createClient } from "@sanity/client";
12+
import { validatePreviewUrl } from "@sanity/preview-url-secret";
1113
import { env } from "cloudflare:workers";
1214

1315
export const prerender = false;
1416

15-
export const GET: APIRoute = async ({ url, cookies, redirect }) => {
16-
const secret = url.searchParams.get("secret");
17-
const slug = url.searchParams.get("slug") || "/";
17+
const PROJECT_ID = "hfh83o0w";
18+
const DATASETS = ["production", "dev"] as const;
1819

19-
// Fail-closed: require SANITY_PREVIEW_SECRET to be configured
20-
const expectedSecret = (env as Record<string, string>).SANITY_PREVIEW_SECRET;
21-
if (!expectedSecret) {
22-
return new Response("Draft mode not configured — SANITY_PREVIEW_SECRET is missing", {
23-
status: 503,
24-
});
20+
export const GET: APIRoute = async ({ url, cookies, redirect, request }) => {
21+
const token = (env as Record<string, string>).SANITY_API_READ_TOKEN;
22+
if (!token) {
23+
return new Response(
24+
"Draft mode not configured — SANITY_API_READ_TOKEN is missing",
25+
{ status: 503 },
26+
);
2527
}
2628

27-
// Validate the secret
28-
if (!secret || secret !== expectedSecret) {
29-
return new Response("Invalid preview secret", { status: 401 });
29+
const requestUrl = request.url;
30+
31+
for (const dataset of DATASETS) {
32+
const client = createClient({
33+
projectId: (env as Record<string, string>).SANITY_PROJECT_ID || PROJECT_ID,
34+
dataset,
35+
apiVersion: "2026-03-17",
36+
useCdn: false,
37+
token,
38+
});
39+
40+
const { isValid, redirectTo = "/" } = await validatePreviewUrl(
41+
client,
42+
requestUrl,
43+
);
44+
45+
if (isValid) {
46+
const isLocal = url.hostname === "localhost";
47+
48+
cookies.set("__sanity_preview", "1", {
49+
path: "/",
50+
httpOnly: true,
51+
sameSite: isLocal ? "lax" : "none",
52+
secure: !isLocal,
53+
maxAge: 60 * 60,
54+
});
55+
56+
return redirect(redirectTo, 307);
57+
}
3058
}
3159

32-
// Set the preview cookie — httpOnly so client JS can't tamper with it
33-
cookies.set("__sanity_preview", "1", {
34-
path: "/",
35-
httpOnly: true,
36-
sameSite: "none",
37-
secure: true,
38-
// 1 hour — long enough for an editing session
39-
maxAge: 60 * 60,
40-
});
41-
42-
// Redirect to the requested page
43-
return redirect(slug, 307);
60+
return new Response("Invalid preview secret", { status: 401 });
4461
};

apps/web/src/utils/sanity.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,27 @@
1717
* - No token needed (public API)
1818
*/
1919
import type { QueryParams } from "sanity";
20-
import { sanityClient } from "sanity:client";
20+
import { createClient } from "@sanity/client";
2121
import imageUrlBuilder from "@sanity/image-url";
2222
import type { SanityImageSource } from "@sanity/image-url/lib/types/types";
2323

24+
// Studio URL required for stega encoding (click-to-edit overlays in Presentation tool).
25+
// Set PUBLIC_SANITY_STUDIO_URL to http://localhost:3333 when running Studio locally.
26+
const studioUrl =
27+
import.meta.env.PUBLIC_SANITY_STUDIO_URL || "https://codingcat.dev.sanity.studio";
28+
29+
// Create client explicitly so we don't rely on the "sanity:client" virtual module,
30+
// which can fail to resolve in some Vite/Astro contexts (e.g. config loading).
31+
const sanityClient = createClient({
32+
projectId: import.meta.env.SANITY_PROJECT_ID || "hfh83o0w",
33+
dataset: import.meta.env.SANITY_DATASET || "production",
34+
apiVersion: "2026-03-17",
35+
useCdn: false,
36+
stega: {
37+
studioUrl,
38+
},
39+
});
40+
2441
const builder = imageUrlBuilder(sanityClient);
2542

2643
export function urlForImage(source: SanityImageSource) {

0 commit comments

Comments
 (0)