From 394daca38e1bbd6aca20f063015dfa7b72907533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Wed, 25 Mar 2026 02:01:27 +0100 Subject: [PATCH] Serve static assets in planner's custom server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custom server.ts only had the React Router request handler — static assets in build/client/ were never served, causing 404s for all JS/CSS in production. Add a static file handler with path traversal protection and immutable caching for hashed assets. Verified locally: HTML 200, assets 200, Cache-Control: immutable on /assets/*. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/planner/server.ts | 46 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/apps/planner/server.ts b/apps/planner/server.ts index 097ddad..d876aef 100644 --- a/apps/planner/server.ts +++ b/apps/planner/server.ts @@ -1,14 +1,56 @@ import { createRequestListener } from "@react-router/node"; -import { createServer } from "node:http"; +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import { createReadStream, statSync } from "node:fs"; +import { join, extname, resolve } from "node:path"; import { setupYjsWebSocket } from "./app/lib/yjs-server.ts"; const port = Number(process.env.PORT ?? 3001); +const CLIENT_DIR = resolve(import.meta.dirname, "build", "client"); + +const MIME: Record = { + ".js": "application/javascript", + ".css": "text/css", + ".html": "text/html", + ".json": "application/json", + ".png": "image/png", + ".jpg": "image/jpeg", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".woff": "font/woff", + ".woff2": "font/woff2", +}; + +function serveStatic(req: IncomingMessage, res: ServerResponse): boolean { + if (req.method !== "GET" && req.method !== "HEAD") return false; + + const url = new URL(req.url ?? "/", `http://${req.headers.host}`); + const filePath = resolve(join(CLIENT_DIR, url.pathname)); + + if (!filePath.startsWith(CLIENT_DIR)) return false; + + try { + if (!statSync(filePath).isFile()) return false; + } catch { + return false; + } + + res.setHeader("Content-Type", MIME[extname(filePath)] ?? "application/octet-stream"); + if (url.pathname.startsWith("/assets/")) { + res.setHeader("Cache-Control", "public, immutable, max-age=31536000"); + } + createReadStream(filePath).pipe(res); + return true; +} const listener = createRequestListener({ build: () => import("./build/server/index.js") as never, }); -const server = createServer(listener); +const server = createServer((req, res) => { + if (!serveStatic(req, res)) { + listener(req, res); + } +}); setupYjsWebSocket(server);