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 (