feat: serve Storybook via Next.js catch-all route handler

Serves static storybook files from public/storybook/ through
a Next.js route at /storybook, avoiding the need for separate
nginx or server configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Temuulen Ankhbayar 2026-03-07 15:41:32 +09:00
parent e704488eb9
commit 1a6242e642

View File

@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from "next/server";
import path from "node:path";
import fs from "node:fs";
const MIME_TYPES: Record<string, string> = {
".html": "text/html",
".js": "application/javascript",
".css": "text/css",
".json": "application/json",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".woff2": "font/woff2",
".woff": "font/woff",
".ico": "image/x-icon",
};
function getStorybookRoot(): string {
// In standalone mode, public files are at ../public relative to server.js
// In dev mode, they're in the project's public directory
const candidates = [
path.join(process.cwd(), "public", "storybook"),
path.join(process.cwd(), "apps", "portal", "public", "storybook"),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return candidates[0]!;
}
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const resolvedParams = await params;
const segments = resolvedParams.path;
const filePath = segments?.length ? segments.join("/") : "index.html";
const root = getStorybookRoot();
const resolved = path.resolve(root, filePath);
// Prevent path traversal
if (!resolved.startsWith(root)) {
return new NextResponse("Forbidden", { status: 403 });
}
if (!fs.existsSync(resolved)) {
return new NextResponse("Not found", { status: 404 });
}
const ext = path.extname(resolved);
const contentType = MIME_TYPES[ext] || "application/octet-stream";
const content = fs.readFileSync(resolved);
return new NextResponse(content, {
headers: {
"Content-Type": contentType,
"Cache-Control": ext === ".html" ? "no-cache" : "public, max-age=31536000, immutable",
},
});
}