diff --git a/package.json b/package.json index b8016651d..a2780d862 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "yaml-eslint-parser": "^1.2.3", "zod": "^3.23.8" }, - "packageManager": "pnpm@9.6.0", + "packageManager": "pnpm@9.12.1", "pnpm": { "patchedDependencies": { "eslint-plugin-prettier@5.2.1": "patches/eslint-plugin-prettier@5.2.1.patch", diff --git a/packages/lint-local-links-valid/package.json b/packages/lint-local-links-valid/package.json index 5012331b8..6f639f4ce 100644 --- a/packages/lint-local-links-valid/package.json +++ b/packages/lint-local-links-valid/package.json @@ -24,6 +24,5 @@ }, "peerDependencies": { "eslint": ">=9.0.0" - }, - "packageManager": "pnpm@9.0.6" + } } diff --git a/src/layouts/sidebar/search.tsx b/src/layouts/sidebar/search.tsx index 686f1225d..113ed546e 100644 --- a/src/layouts/sidebar/search.tsx +++ b/src/layouts/sidebar/search.tsx @@ -65,11 +65,11 @@ export function SearchButton({ lang }: SearchButtonProps) { export const searchIndexKo = lazy(() => fetchSearchIndex("ko")); export const searchIndexEn = lazy(() => fetchSearchIndex("en")); async function fetchSearchIndex(lang: string): Promise { - const res = await fetch(`/content-index/opi-${lang}.json`); + const res = await fetch(`/content-index/${lang}.json`); return JSON.parse((await res.text()).normalize("NFKD")) as SearchIndex; } -export type SearchIndex = SearchIndexItem[]; +export type SearchIndex = Record; export interface SearchIndexItem { slug: string; title?: string; @@ -98,7 +98,8 @@ export function SearchScreen(props: SearchScreenProps) { const fuse = createMemo(() => { const index = searchIndex.latest; if (!index) return; - const filteredIndex = index + const filteredIndex = Object.values(index) + .flat() .filter((item) => { const navMenuSystemVersion = props.navMenuSystemVersions[item.slug.replace(/^opi/, "")]; diff --git a/src/misc/contentIndex.ts b/src/misc/contentIndex.ts index 481820ff8..af6446014 100644 --- a/src/misc/contentIndex.ts +++ b/src/misc/contentIndex.ts @@ -1,6 +1,17 @@ +import type { Lang } from "~/type"; + export const indexFilesMapping = { - blog: "blog/", - "opi-ko": "opi/ko/", - "release-notes": "release-notes/(note)/", -} as const satisfies Record; -export type IndexFileName = keyof typeof indexFilesMapping; + ko: { + docs: { + "원 페이먼트 인프라": "opi/ko/", + "파트너 정산 자동화": "platform/", + "릴리즈 노트": "release-notes/(note)/", + }, + blog: { + "기술 블로그": "blog/", + }, + }, + en: { + docs: { "docs-en": "docs/en/" }, + }, +} as const satisfies Record>>; diff --git a/src/routes/(root).tsx b/src/routes/(root).tsx index 926233078..e38ec32e7 100644 --- a/src/routes/(root).tsx +++ b/src/routes/(root).tsx @@ -1,17 +1,25 @@ import "~/styles/article.css"; import { Link } from "@solidjs/meta"; -import { createAsync, useLocation } from "@solidjs/router"; -import { createMemo, type JSXElement } from "solid-js"; +import { + cache, + createAsync, + type RouteDefinition, + useLocation, +} from "@solidjs/router"; +import { createMemo, createResource, type JSXElement, Show } from "solid-js"; import { MDXProvider } from "solid-mdx"; import * as prose from "~/components/prose"; import type { DocsEntry } from "~/content/config"; import Gnb from "~/layouts/gnb/Gnb"; import SidebarProvider from "~/layouts/sidebar/context"; +import { SearchProvider, SearchScreen } from "~/layouts/sidebar/search"; import SidebarBackground from "~/layouts/sidebar/SidebarBackground"; import { getOpiFullSlug, loadDoc } from "~/misc/opi"; +import { calcNavMenuSystemVersions } from "~/state/nav"; import { SystemVersionProvider } from "~/state/system-version"; +import type { Lang } from "~/type"; interface Props { children: JSXElement; @@ -19,6 +27,25 @@ interface Props { const navAsMenuPaths = ["/platform", "/blog", "/release-notes"]; +export const route = { + preload: ({ location }) => { + const lang = location.pathname.includes("/en/") ? "en" : "ko"; + void loadNavMenuSystemVersions(lang); + + const fullSlug = getOpiFullSlug(location.pathname); + if (!fullSlug) return; + + void loadDoc(fullSlug); + }, +} satisfies RouteDefinition; + +const loadNavMenuSystemVersions = cache(async (lang: Lang) => { + "use server"; + + const { navMenu } = await import("~/state/server-only/nav"); + return calcNavMenuSystemVersions(navMenu[lang] || []); +}, "nav-menu-system-versions"); + export default function Layout(props: Props) { const location = useLocation(); const lang = createMemo(() => @@ -37,6 +64,9 @@ export default function Layout(props: Props) { throw e; } }); + const [navMenuSystemVersions] = createResource(() => + loadNavMenuSystemVersions(lang()), + ); return ( @@ -46,19 +76,29 @@ export default function Layout(props: Props) { /> -
- - location.pathname.startsWith(path), + +
+ + location.pathname.startsWith(path), + )} + docData={docData()?.frontmatter as DocsEntry} + /> + +
+ {props.children} +
+
+ + {(versions) => ( + )} - docData={docData()?.frontmatter as DocsEntry} - /> - -
- {props.children} -
-
+ +
diff --git a/src/routes/(root)/opi.tsx b/src/routes/(root)/opi.tsx index d26dedc65..12c9b6c0e 100644 --- a/src/routes/(root)/opi.tsx +++ b/src/routes/(root)/opi.tsx @@ -1,10 +1,5 @@ -import { - cache, - createAsync, - type RouteDefinition, - useLocation, -} from "@solidjs/router"; -import { createMemo, createResource, type JSXElement, Show } from "solid-js"; +import { createAsync, useLocation } from "@solidjs/router"; +import { createMemo, type JSXElement, Show } from "solid-js"; import { NotFoundError } from "~/components/404"; import Metadata from "~/components/Metadata"; @@ -12,29 +7,9 @@ import * as prose from "~/components/prose"; import type { DocsEntry } from "~/content/config"; import DocsNavMenu from "~/layouts/sidebar/DocsNavMenu"; import RightSidebar from "~/layouts/sidebar/RightSidebar"; -import { SearchProvider, SearchScreen } from "~/layouts/sidebar/search"; import { getOpiFullSlug, loadDoc } from "~/misc/opi"; -import { calcNavMenuSystemVersions } from "~/state/nav"; import { Lang } from "~/type"; -const loadNavMenuSystemVersions = cache(async (lang: Lang) => { - "use server"; - - const { navMenu } = await import("~/state/server-only/nav"); - return calcNavMenuSystemVersions(navMenu[lang] || []); -}, "opi/nav-menu-system-versions"); - -export const route = { - preload: ({ location }) => { - const fullSlug = getOpiFullSlug(location.pathname); - if (!fullSlug) return; - const lang = fullSlug.split("/")[0]; - - void loadDoc(fullSlug); - void loadNavMenuSystemVersions(lang as Lang); - }, -} satisfies RouteDefinition; - export default function Docs(props: { children: JSXElement }) { const location = useLocation(); const fullSlug = createMemo(() => { @@ -52,54 +27,41 @@ export default function Docs(props: { children: JSXElement }) { deferStream: true, }); const frontmatter = createMemo(() => doc()?.frontmatter as DocsEntry); - const [navMenuSystemVersions] = createResource(params, ({ lang }) => - loadNavMenuSystemVersions(lang), - ); return ( - -
- -
- - {(frontmatter) => ( - <> - -
-
- {frontmatter().title} - -

- {frontmatter().description} -

-
-
- {props.children} -
- - )} -
- -
+
+ +
+ + {(frontmatter) => ( + <> + +
+
+ {frontmatter().title} + +

+ {frontmatter().description} +

+
+
+ {props.children} +
+ + )} +
+
- - {(versions) => ( - - )} - - +
); } diff --git a/src/routes/content-index/[fileName].ts b/src/routes/content-index/[fileName].ts deleted file mode 100644 index 90483cc9c..000000000 --- a/src/routes/content-index/[fileName].ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server"; -import * as yaml from "js-yaml"; - -import { type IndexFileName, indexFilesMapping } from "~/misc/contentIndex"; -import { toPlainText } from "~/misc/mdx"; - -export async function GET({ params }: APIEvent) { - const fileName = params.fileName?.replace(".json", ""); - const slug = - fileName && - fileName in indexFilesMapping && - indexFilesMapping[fileName as IndexFileName]; - if (!slug) return new Response(null, { status: 404 }); - const entryMap = import.meta.glob("~/routes/**/*.mdx", { - query: "?raw", - }); - const mdxTable = Object.fromEntries( - ( - await Promise.all( - Object.entries(entryMap).map(async ([path, importEntry]) => { - const match = path.match(/\/routes\/\(root\)\/(.+)\.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: entrySlug, text: toPlainText(md) }, - ] as const; - }), - ) - ).filter(Boolean), - ); - return new Response(JSON.stringify(Object.values(mdxTable)), { - 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 }; - } -} diff --git a/src/routes/content-index/[lang]/[fileName].ts b/src/routes/content-index/[lang]/[fileName].ts new file mode 100644 index 000000000..3d3592587 --- /dev/null +++ b/src/routes/content-index/[lang]/[fileName].ts @@ -0,0 +1,86 @@ +import type { APIEvent } from "@solidjs/start/server"; +import * as yaml from "js-yaml"; +import { z } from "zod"; + +import { indexFilesMapping as _indexFilesMapping } from "~/misc/contentIndex"; +import { toPlainText } from "~/misc/mdx"; + +export async function GET({ params }: APIEvent) { + const lang = z.enum(["ko", "en"]).safeParse(params.lang).data; + if (!lang) return new Response(null, { status: 404 }); + const fileName = params.fileName?.replace(".json", ""); + const indexFilesMapping = _indexFilesMapping[lang]; + const mappedSlug = + fileName && + fileName in indexFilesMapping && + indexFilesMapping[fileName as keyof typeof indexFilesMapping]; + if (!mappedSlug) return new Response(null, { status: 404 }); + const entryMap = import.meta.glob("~/routes/**/*.mdx", { + query: "?raw", + }); + const mdxTable = Object.fromEntries( + await Promise.all( + Object.entries(mappedSlug).map( + async ([title, slug]) => + [title, await generateMdxTable(entryMap, slug)] as const, + ), + ), + ); + return new Response(JSON.stringify(Object.values(mdxTable)), { + headers: { + "Content-Type": "application/json", + }, + }); +} + +interface CutFrontmatterResult { + frontmatter: unknown; + md: string; +} +async function generateMdxTable( + entryMap: Record Promise>, + slug: string, +) { + return ( + await Promise.all( + Object.entries(entryMap).map(async ([path, importEntry]) => { + const match = path.match(/\/routes\/\(root\)\/(.+)\.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 { + ...frontmatter, + slug: entrySlug, + text: toPlainText(md), + } as const; + }), + ) + ).filter(Boolean); +} + +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 }; + } +}