feat: make the site database authoritative for documents and catalog
Verify database-backed documents / verify (pull_request) Failing after 6m6s

This commit is contained in:
2026-10-30 16:23:54 +08:00
parent 25c5e83d08
commit d57e9b77d7
79 changed files with 562 additions and 5222 deletions
@@ -0,0 +1,22 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { readDocument } from "@/lib/docs";
// The proxy protects Markdown just like the rendered document and search index.
export default function handler(request: NextApiRequest, response: NextApiResponse) {
response.setHeader("Cache-Control", "private, no-store");
if (request.method !== "GET") {
response.setHeader("Allow", "GET");
return response.status(405).end();
}
const slug = request.query.slug;
if (typeof slug !== "string") return response.status(400).json({ error: "缺少文档地址" });
try {
const doc = readDocument(slug);
if (!doc) return response.status(404).end();
response.setHeader("Content-Type", "text/plain; charset=utf-8");
response.setHeader("X-Content-Type-Options", "nosniff");
return response.status(200).send(doc.content);
} catch {
return response.status(503).json({ error: "文档数据库暂时不可用" });
}
}
+7 -6
View File
@@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { listDocs, readDoc } from "@/lib/docs";
import { allDocuments } from "@/lib/docs";
// The proxy authenticates this endpoint, including index content.
export default async function handler(request: NextApiRequest, response: NextApiResponse) {
@@ -8,9 +8,10 @@ export default async function handler(request: NextApiRequest, response: NextApi
response.setHeader("Allow", "GET");
return response.status(405).end();
}
const docs = await listDocs();
const index = await Promise.all(docs.map(async (doc) => ({
...doc, searchText: (await readDoc(doc.slug)).content,
})));
response.status(200).json(index);
try {
const index = allDocuments().map(({ content, ...doc }) => ({ ...doc, searchText: content }));
return response.status(200).json(index);
} catch {
return response.status(503).json({ error: "文档数据库暂时不可用" });
}
}
+15 -15
View File
@@ -1,7 +1,6 @@
import type {
GetStaticPaths,
GetStaticProps,
InferGetStaticPropsType,
GetServerSideProps,
InferGetServerSidePropsType,
} from "next";
import Head from "next/head";
import dynamic from "next/dynamic";
@@ -15,7 +14,7 @@ import {
} from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { listDocs, readDoc, type Doc, type DocLink } from "@/lib/docs";
import { listDocs, readDocument, type Doc, type DocLink } from "@/lib/docs";
import { Shell, Navigation } from "@/components/Shell";
import { ArchitectureFigure } from "@/components/ArchitectureFigure";
import { FigureGallery } from "@/components/FigureGallery";
@@ -26,16 +25,17 @@ import { headingsFor } from "@/lib/navigation";
type Props = { doc: Doc; docs: DocLink[] };
const AICompanyResearch = dynamic(() => import("@/components/AICompanyResearch"));
export const getStaticPaths: GetStaticPaths = async () => ({
paths: (await listDocs()).map(({ slug }) => ({ params: { slug: slug.split("/") } })),
fallback: false,
});
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
export const getServerSideProps: GetServerSideProps<Props> = async ({ params, res, resolvedUrl }) => {
res.setHeader("Cache-Control", "private, no-store");
const slug = Array.isArray(params?.slug) ? params.slug.join("/") : params?.slug;
if (typeof slug !== "string") return { notFound: true };
const docs = await listDocs();
if (!docs.some((d) => d.slug === slug)) return { notFound: true };
return { props: { doc: await readDoc(slug), docs } };
const doc = readDocument(slug);
if (!doc) return { notFound: true };
if (slug !== doc.slug) {
const query = resolvedUrl.includes("?") ? resolvedUrl.slice(resolvedUrl.indexOf("?")) : "";
return { redirect: { destination: `/docs/${doc.slug}${query}`, permanent: true } };
}
return { props: { doc, docs: await listDocs() } };
};
function textOf(value: ReactNode): string {
return Children.toArray(value)
@@ -49,7 +49,7 @@ function textOf(value: ReactNode): string {
export default function DocPage({
doc,
docs,
}: InferGetStaticPropsType<typeof getStaticProps>) {
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
const headings = headingsFor(doc.content);
const [active, setActive] = useState("");
@@ -231,9 +231,9 @@ export default function DocPage({
<div className="article-end">
<a
href={`https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs/src/branch/main/content/docs/${doc.sourceSlug}.md`}
href={`/api/documents/source?slug=${encodeURIComponent(doc.slug)}`}
>
查看文档源码 ↗
查看 Markdown 原文 ↗
</a>
</div>
<nav className="pagination" aria-label="相邻文档">
+6 -5
View File
@@ -1,4 +1,4 @@
import type { GetStaticProps, InferGetStaticPropsType } from "next";
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
import Head from "next/head";
import Link from "next/link";
import { useState } from "react";
@@ -7,11 +7,12 @@ import { Shell, Navigation } from "@/components/Shell";
import { Icon } from "@/components/Icon";
import { collectionEntries, collectionIcon, collectionLabel, collectionTone } from "@/lib/collections";
export const getStaticProps: GetStaticProps<{ docs: DocLink[] }> = async () => ({
props: { docs: await listDocs() },
});
export const getServerSideProps: GetServerSideProps<{ docs: DocLink[] }> = async ({ res }) => {
res.setHeader("Cache-Control", "private, no-store");
return { props: { docs: await listDocs() } };
};
const PAGE_SIZE = 8;
export default function Home({ docs }: InferGetStaticPropsType<typeof getStaticProps>) {
export default function Home({ docs }: InferGetServerSidePropsType<typeof getServerSideProps>) {
const [collection, setCollection] = useState("");
const [query, setQuery] = useState("");
const [page, setPage] = useState(0);