Add Sanity Studio served at /studio via Docker multi-stage build

Builds studio/ separately during Docker build and copies output to
public/studio/ so it is served as static files by the Astro Node adapter.
Catch-all route redirects deep studio links back to /studio for SPA routing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Calum Muir 2026-06-07 12:18:00 +00:00
parent fc644510af
commit 9ca50a1da8
8 changed files with 15357 additions and 291 deletions

7
.gitignore vendored
View file

@ -1,5 +1,12 @@
node_modules node_modules
dist dist
public/studio
.env
.env.local
.env.*.local
studio/.env
studio/.env.local
studio/.sanity
.DS_Store .DS_Store
server/public server/public
vite.config.ts.* vite.config.ts.*

45
Dockerfile Normal file
View file

@ -0,0 +1,45 @@
FROM node:20-alpine AS base
WORKDIR /app
# ── 1. Install main project deps ─────────────────────────────────────────────
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci
# ── 2. Install studio deps ────────────────────────────────────────────────────
FROM base AS studio-deps
COPY studio/package.json studio/package-lock.json* ./
RUN npm ci
# ── 3. Build Sanity Studio ────────────────────────────────────────────────────
FROM base AS studio-build
COPY --from=studio-deps /app/node_modules ./node_modules
COPY studio/ ./
ARG SANITY_STUDIO_PROJECT_ID
ARG SANITY_STUDIO_DATASET=production
ENV SANITY_STUDIO_PROJECT_ID=$SANITY_STUDIO_PROJECT_ID
ENV SANITY_STUDIO_DATASET=$SANITY_STUDIO_DATASET
RUN npm run build
# ── 4. Build Astro site (with studio output as static files) ──────────────────
FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
COPY --from=studio-build /app/dist ./public/studio
ARG SANITY_PROJECT_ID
ARG SANITY_DATASET=production
ARG SANITY_API_TOKEN
ENV SANITY_PROJECT_ID=$SANITY_PROJECT_ID
ENV SANITY_DATASET=$SANITY_DATASET
ENV SANITY_API_TOKEN=$SANITY_API_TOKEN
RUN npm run build
# ── 5. Runtime ────────────────────────────────────────────────────────────────
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]

1176
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
import type { APIRoute } from "astro";
// Redirect deep studio links (e.g. /studio/desk/post) back to /studio so the
// SPA can boot and handle its own routing. Static assets under /studio/assets/*
// are served before this route ever fires.
export const GET: APIRoute = () =>
new Response(null, { status: 302, headers: { Location: "/studio" } });

6
studio/.env.example Normal file
View file

@ -0,0 +1,6 @@
# ── Sanity Studio ────────────────────────────────────────────────────────────
# Copy to .env and fill in your values from https://www.sanity.io/manage
# SANITY_STUDIO_ prefix is required — Sanity injects these into the studio bundle.
SANITY_STUDIO_PROJECT_ID=
SANITY_STUDIO_DATASET=production

14402
studio/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"sanity": "^3.66.0" "sanity": "^3.66.0",
"styled-components": "^6.4.2"
} }
} }

View file

@ -1,6 +1,5 @@
import { defineConfig } from 'sanity'; import { defineConfig } from 'sanity';
import { structureTool } from 'sanity/structure'; import { structureTool } from 'sanity/structure';
import { visionTool } from '@sanity/vision';
import { schemaTypes } from './schemaTypes'; import { schemaTypes } from './schemaTypes';
export default defineConfig({ export default defineConfig({
@ -13,7 +12,6 @@ export default defineConfig({
plugins: [ plugins: [
structureTool(), structureTool(),
visionTool(), // GROQ query playground — remove in production if desired
], ],
schema: { schema: {