From 050ca518c058c5d79b64b0efde8ea6c2e064c259 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Thu, 20 Aug 2026 17:06:16 +0700 Subject: [PATCH] fix(web): Sidebar as client component (no DB during SSG) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar.tsx is now "use client" — fetches /api/docs at runtime instead of calling listDocuments() during SSG (CI has no DB, causes ECONNREFUSED) - GET /api/docs added to route.ts (returns doc list for sidebar) - Removed SidebarContent.tsx (merged into Sidebar.tsx) - Fixed isAuthorized double-brace typo --- apps/web/app/api/docs/route.ts | 14 ++++++++-- apps/web/app/components/Sidebar.tsx | 43 +++++++++++++++++++---------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/apps/web/app/api/docs/route.ts b/apps/web/app/api/docs/route.ts index 21b3264..7dd611d 100644 --- a/apps/web/app/api/docs/route.ts +++ b/apps/web/app/api/docs/route.ts @@ -1,12 +1,15 @@ import { NextRequest, NextResponse } from "next/server"; import { createDocument, + updateDocument, + deleteDocument, + listDocuments, } from "@mcpedia/core"; import { WEBHOOK_SECRET } from "@mcpedia/config"; import { timingSafeEqual } from "node:crypto"; -// Web CRUD auth: either the `x-webhook-secret` header (API/MCP style) OR the -// `mcpedia_admin` cookie (web login). One of the two must be present + valid. +// Web CRUD auth: either the x-webhook-secret header (API/MCP style) OR the +// mcpedia_admin cookie (web login). One of the two must be valid. function isAuthorized(req: NextRequest): boolean { if (!WEBHOOK_SECRET) return false; const headerSecret = req.headers.get("x-webhook-secret") ?? ""; @@ -21,8 +24,13 @@ function unauthorized() { return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); } +// GET /api/docs — list all documents (for sidebar navigation). +export async function GET() { + const docs = await listDocuments(); + return NextResponse.json(docs); +} + // POST /api/docs — Create a new document. -// Body: { slug, title, section, body, type?, status?, author?, tags? } export async function POST(req: NextRequest) { if (!isAuthorized(req)) return unauthorized(); diff --git a/apps/web/app/components/Sidebar.tsx b/apps/web/app/components/Sidebar.tsx index 2d82af9..db38d13 100644 --- a/apps/web/app/components/Sidebar.tsx +++ b/apps/web/app/components/Sidebar.tsx @@ -1,5 +1,14 @@ +"use client"; + +import { useEffect, useState } from "react"; import Link from "next/link"; -import { listDocuments } from "@mcpedia/core"; + +interface Doc { + slug: string; + title: string; + section: string; + tags: string[]; +} const SECTIONS = [ { id: "docs", label: "Documentation" }, @@ -8,20 +17,25 @@ const SECTIONS = [ { id: "notes", label: "Notes" }, ] as const; -export default async function Sidebar() { - const docs = await listDocuments(); +/** Client-side sidebar (fetches doc list via /api/docs at runtime). + * Client component so `next build` SSG doesn't hit Postgres (CI has no DB). */ +export default function Sidebar() { + const [docs, setDocs] = useState([]); - // Build a tree: section → top-level slugs + children - const bySection: Record = {}; + useEffect(() => { + fetch("/api/docs") + .then((r) => r.json()) + .then((data) => { + if (Array.isArray(data)) setDocs(data); + else if (data?.docs) setDocs(data.docs); + }) + .catch(() => setDocs([])); + }, []); + + const bySection: Record = {}; for (const doc of docs) { if (!bySection[doc.section]) bySection[doc.section] = []; - // Count depth from dots in a flattened slug: docs/websocket/contract - // We'll show all docs at their depth level - bySection[doc.section].push({ - slug: doc.slug, - title: doc.title, - depth: doc.slug.split("/").length, - }); + bySection[doc.section].push(doc); } return ( @@ -37,13 +51,14 @@ export default async function Sidebar() {
    {sectionDocs.map((doc) => { - const parts = doc.slug.split("/").slice(1); // remove section prefix + const parts = doc.slug.split("/").slice(1); const label = parts[parts.length - 1]; const formatted = label .split(/[-_]/) .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); - const indent = doc.depth - 1; + const depth = doc.slug.split("/").length; + const indent = depth - 1; return (