From 7eb8914a840fc2a8db5d0c08a6c117fdd87558c7 Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Fri, 15 Mar 2024 17:03:22 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=9D=B8=EB=8D=B1?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=20API=20=EB=9D=BC=EC=9A=B0=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astro.config.ts | 3 - src/content-index.ts | 147 --------------------- src/pages/content-index/[fileName].json.ts | 84 ++++++++++++ 3 files changed, 84 insertions(+), 150 deletions(-) delete mode 100644 src/content-index.ts create mode 100644 src/pages/content-index/[fileName].json.ts diff --git a/astro.config.ts b/astro.config.ts index c8e3f51a9..d8e41f8b4 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -11,8 +11,6 @@ import { transformerMetaHighlight } from "@shikijs/transformers"; import { defineConfig } from "astro/config"; import unocss from "unocss/astro"; -import contentIndex from "./src/content-index"; - // https://astro.build/config export default defineConfig({ site: "https://developers.portone.io/", @@ -20,7 +18,6 @@ export default defineConfig({ preact({ compat: true }), mdx(), unocss({ injectReset: true }), - contentIndex, sitemap({ customPages: ["/api/rest-v1/", "/api/rest-v2/"].map( (url) => `https://developers.portone.io${url}`, diff --git a/src/content-index.ts b/src/content-index.ts deleted file mode 100644 index 08a21a7f0..000000000 --- a/src/content-index.ts +++ /dev/null @@ -1,147 +0,0 @@ -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import * as posixPath from "node:path/posix"; - -import { computed, effect, signal } from "@preact/signals"; -import type { AstroConfig, AstroIntegration } from "astro"; -import * as yaml from "js-yaml"; -import { debounce } from "lodash-es"; - -import { toPlainText } from "./misc/mdx"; - -const indexFilesMapping = { - "blog/": "blog.json", - "docs/en/": "docs-en.json", - "docs/ko/": "docs-ko.json", - "release-notes/": "release-notes.json", -} as const satisfies Record; - -const integration: AstroIntegration = { - name: "developers.portone.io:content-index", - hooks: { - "astro:config:setup": ({ config, updateConfig }) => { - updateConfig({ vite: { plugins: [vitePlugin(config)] } }); - }, - "astro:config:done": ({ config }) => { - const clientOut = config.build.client.pathname.replace(/^\/(\w:)/, "$1"); - const contentIndexDir = path.resolve(clientOut, "content-index"); - effect(() => { - const indexFiles = indexFilesSignal.value; - if (!indexFiles) return; - void fs.mkdir(contentIndexDir, { recursive: true }).then(() => { - for (const [filename, indexFile] of Object.entries(indexFiles)) { - const indexFilePath = path.resolve(contentIndexDir, filename); - void fs.writeFile(indexFilePath, indexFile); - } - }); - }); - }, - }, -}; - -export default integration; - -function vitePlugin(config: AstroConfig) { - const root = config.root.pathname.replace(/^\/(\w:)/, "$1"); - return { - name: "developers.portone.io:content-index", - async transform(_, id) { - const path = posixPath.relative(root, id); - const match = path.match(/^src\/content\/(.+)\.mdx$/); - if (!match) return; - const { frontmatter, md } = cutFrontmatter( - await fs.readFile(id, "utf-8"), - ); - if (!frontmatter || typeof frontmatter !== "object") return; - const slug = String("slug" in frontmatter ? frontmatter.slug : match[1]); - updateMdxTable(slug, { ...frontmatter, slug, md }); - }, - configureServer(server) { - server.middlewares.use((req, res, next) => { - if (!req.url?.startsWith("/content-index/")) return next(); - const filename = req.url.slice("/content-index/".length); - const indexFiles = indexFilesSignal.value; - if (!(filename in indexFiles)) return next(); - return res - .setHeader("Content-Type", "application/json") - .end(indexFiles[filename]); - }); - }, - } satisfies NonNullable[number]; -} - -interface CutFrontmatterResult { - frontmatter: unknown; - md: string; -} -function cutFrontmatter(md: string): CutFrontmatterResult { - const match = md.match( - /^---\r?\n((?:.|\r|\n)*?)\r?\n---\r?\n((?:.|\r|\n)*)$/, - ); - if (!match) return { frontmatter: {}, md }; - try { - const fm = match[1] || ""; - const md = match[2] || ""; - const frontmatter = yaml.load(fm); - return { frontmatter, md }; - } catch { - return { frontmatter: {}, md }; - } -} - -interface Mdx { - slug: string; - md: string; -} -interface MdxTable { - [slug: string]: Mdx; -} -const mdxTableSignal = signal({}); -function updateMdxTable(slug: string, mdx: Mdx) { - const mdxTable = mdxTableSignal.value; - if (mdxEq(mdxTable[slug], mdx)) return; - mdxTableSignal.value = { ...mdxTable, [slug]: mdx }; -} -function mdxEq(a?: Mdx, b?: Mdx): boolean { - if (a === b) return true; - if (!a || !b) return false; - if (a.slug !== b.slug) return false; - for (const key in a) - if (a[key as keyof Mdx] !== b[key as keyof Mdx]) return false; - return true; -} -type Index = IndexItem[]; -interface IndexItem { - slug: string; - text: string; -} -const indexSignal = signal([]); -const updateIndexSignal = debounce((mdxTable: MdxTable) => { - indexSignal.value = Object.values(mdxTable).map((mdx) => ({ - ...mdx, - text: toPlainText(mdx.md), - })); -}, 500); -const indexFilesSignal = computed(() => { - const index = indexSignal.value; - const result: Record = {}; - index: for (const item of index) { - for (const [slugPrefix, file] of Object.entries(indexFilesMapping)) { - if (item.slug.startsWith(slugPrefix)) { - (result[file] ??= []).push(item); - continue index; - } - } - } - return Object.fromEntries( - Object.entries(result).map(([filename, index]) => [ - filename, - JSON.stringify(index), - ]), - ); -}); - -effect(() => { - const mdxTable = mdxTableSignal.value; - updateIndexSignal(mdxTable); -}); diff --git a/src/pages/content-index/[fileName].json.ts b/src/pages/content-index/[fileName].json.ts new file mode 100644 index 000000000..34769cb78 --- /dev/null +++ b/src/pages/content-index/[fileName].json.ts @@ -0,0 +1,84 @@ +import type { APIRoute, GetStaticPaths } from "astro"; +import * as yaml from "js-yaml"; + +import { toPlainText } from "~/misc/mdx"; + +export const prerender = true; + +const indexFilesMapping = { + blog: "blog/", + "docs-en": "docs/en/", + "docs-ko": "docs/ko/", + "release-notes": "release-notes/", +} as const satisfies Record; +type IndexFileName = keyof typeof indexFilesMapping; +const isIndexFileName = (value: string): value is IndexFileName => + value in indexFilesMapping; + +export const getStaticPaths: GetStaticPaths = () => + Object.keys(indexFilesMapping).map((fileName) => ({ + params: { fileName }, + })); + +export const GET: APIRoute = async ({ params }) => { + const { fileName } = params; + const slug = + fileName && isIndexFileName(fileName) && indexFilesMapping[fileName]; + if (!slug) return new Response(null, { status: 404 }); + const entryMap = import.meta.glob("../../content/**/*.mdx", { + query: "?raw", + }); + const mdxTable = Object.fromEntries( + ( + await Promise.all( + Object.entries(entryMap).map(async ([path, importEntry]) => { + const match = path.match(/\/content\/(.+)\.mdx$/); + if (!match || !match[1]?.startsWith(slug)) return; + const entry = await importEntry(); + if ( + !entry || + typeof entry !== "object" || + !("default" in entry) || + typeof entry.default !== "string" + ) + return; + + const { frontmatter, md } = cutFrontmatter(entry.default); + if (!frontmatter || typeof frontmatter !== "object") return; + const entrySlug = String( + "slug" in frontmatter ? frontmatter.slug : match[1], + ); + return [entrySlug, { ...frontmatter, slug, md }] as const; + }), + ) + ).filter(Boolean), + ); + const indexes = Object.values(mdxTable).map((mdx) => ({ + ...mdx, + text: toPlainText(mdx.md), + })); + return new Response(JSON.stringify(indexes), { + headers: { + "Content-Type": "application/json", + }, + }); +}; + +interface CutFrontmatterResult { + frontmatter: unknown; + md: string; +} +function cutFrontmatter(md: string): CutFrontmatterResult { + const match = md.match( + /^---\r?\n((?:.|\r|\n)*?)\r?\n---\r?\n((?:.|\r|\n)*)$/, + ); + if (!match) return { frontmatter: {}, md }; + try { + const fm = match[1] || ""; + const md = match[2] || ""; + const frontmatter = yaml.load(fm); + return { frontmatter, md }; + } catch { + return { frontmatter: {}, md }; + } +}