diff --git a/apps/registry/app/docs/[slug]/page.tsx b/apps/registry/app/docs/[slug]/page.tsx new file mode 100644 index 00000000..51cfb66a --- /dev/null +++ b/apps/registry/app/docs/[slug]/page.tsx @@ -0,0 +1,154 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +import { Breadcrumb, MDXContent, Sidebar } from "@vllnt/ui"; +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import Script from "next/script"; + +import { getPageContent } from "@/lib/content"; +import { DOCS_PAGES, getDocsPage, getDocsPath } from "@/lib/docs-pages"; +import { + breadcrumbLd, + jsonLdScriptAttributes, + techArticleLd, +} from "@/lib/jsonld"; +import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og"; +import { canonical } from "@/lib/seo"; +import { getSidebarSections } from "@/lib/sidebar-sections"; + +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.ai"; + +type Props = { + params: Promise<{ slug: string }>; +}; + +export function generateStaticParams(): { slug: string }[] { + return DOCS_PAGES.map((page) => ({ slug: page.slug })); +} + +const ROOT_CHANGELOG_PATH = path.join(process.cwd(), "..", "..", "CHANGELOG.md"); +const PACKAGE_CHANGELOG_PATH = path.join( + process.cwd(), + "..", + "..", + "packages", + "ui", + "CHANGELOG.md", +); + +async function readChangelogFile(filePath: string): Promise { + try { + return (await readFile(filePath, "utf8")).trim(); + } catch { + return ""; + } +} + +async function readChangelog(): Promise { + const [rootChangelog, packageChangelog] = await Promise.all([ + readChangelogFile(ROOT_CHANGELOG_PATH), + readChangelogFile(PACKAGE_CHANGELOG_PATH), + ]); + + return [ + rootChangelog ? `## Repository changelog\n\n${rootChangelog}` : "", + packageChangelog ? `## Package changelog\n\n${packageChangelog}` : "", + ] + .filter(Boolean) + .join("\n\n"); +} + +export async function generateMetadata(props: Props): Promise { + const { slug } = await props.params; + const docsPage = getDocsPage(slug); + + if (!docsPage) { + return { + title: "Documentation", + }; + } + + const { frontmatter } = await getPageContent(`docs/${docsPage.slug}`); + const og = frontmatter.og; + const href = getDocsPath(docsPage); + + return { + alternates: { canonical: canonical(href) }, + description: frontmatter.description, + openGraph: generateOGMetadata({ + description: og?.description ?? frontmatter.description, + title: og?.title ?? frontmatter.title, + type: og?.type ?? frontmatter.type, + }), + title: frontmatter.title, + twitter: generateTwitterMetadata({ + description: og?.description ?? frontmatter.description, + title: og?.title ?? frontmatter.title, + type: og?.type ?? frontmatter.type, + }), + }; +} + +export default async function DocsSlugPage(props: Props) { + const { slug } = await props.params; + const docsPage = getDocsPage(slug); + + if (!docsPage) { + notFound(); + } + + const { content, frontmatter } = await getPageContent(`docs/${docsPage.slug}`); + const pageContent = + docsPage.slug === "changelog" + ? `${content}\n\n${await readChangelog()}` + : content; + const pageUrl = `${SITE_URL}${getDocsPath(docsPage)}`; + + return ( + <> +