refactor: docubook@latest template nextjs-docker

This commit is contained in:
gitfromwildan
2026-05-30 18:52:21 +07:00
parent bf2ef37f49
commit 80eb49d968
101 changed files with 1759 additions and 4165 deletions

121
lib/icon.ts Normal file
View File

@@ -0,0 +1,121 @@
import { createElement, type ComponentType, type SVGProps } from "react";
export type IconComponent = ComponentType<SVGProps<SVGSVGElement>>;
function createSocialIcon(title: string, pathData: string): IconComponent {
return function SocialIcon(props) {
return createElement(
"svg",
{
role: "img",
viewBox: "0 0 24 24",
xmlns: "http://www.w3.org/2000/svg",
fill: "currentColor",
...props,
},
createElement("title", null, title),
createElement("path", { d: pathData })
);
};
}
const FacebookIcon = createSocialIcon(
"Facebook",
"M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
);
const InstagramIcon = createSocialIcon(
"Instagram",
"M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"
);
const TwitterIcon = createSocialIcon(
"X",
"M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"
);
const YoutubeIcon = createSocialIcon(
"YouTube",
"M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
);
const ThreadsIcon = createSocialIcon(
"Threads",
"M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z"
);
const GithubIcon = createSocialIcon(
"GitHub",
"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
);
const GitlabIcon = createSocialIcon(
"GitLab",
"m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"
);
const DiscordIcon = createSocialIcon(
"Discord",
"M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"
);
const NpmIcon = createSocialIcon(
"npm",
"M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"
);
const TelegramIcon = createSocialIcon(
"Telegram",
"M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"
);
const GlobeIcon = createSocialIcon(
"Website",
"M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm7.93 9h-3.06a15.7 15.7 0 0 0-1.19-5.03A8.02 8.02 0 0 1 19.93 11ZM12 4.07c.85 1.18 1.63 3.13 1.86 5.93h-3.72c.23-2.8 1.01-4.75 1.86-5.93ZM8.32 5.97A15.7 15.7 0 0 0 7.13 11H4.07a8.02 8.02 0 0 1 4.25-5.03ZM4.07 13h3.06c.16 1.83.6 3.56 1.19 5.03A8.02 8.02 0 0 1 4.07 13Zm5.99 0h3.88c-.24 2.8-1.02 4.75-1.94 5.97-.92-1.22-1.7-3.17-1.94-5.97Zm5.62 5.03c.59-1.47 1.03-3.2 1.19-5.03h3.06a8.02 8.02 0 0 1-4.25 5.03Z"
);
export const SOCIAL_ICONS: Record<string, IconComponent> = {
facebook: FacebookIcon,
instagram: InstagramIcon,
twitter: TwitterIcon,
x: TwitterIcon,
youtube: YoutubeIcon,
threads: ThreadsIcon,
github: GithubIcon,
gitlab: GitlabIcon,
discord: DiscordIcon,
npm: NpmIcon,
telegram: TelegramIcon,
};
const SOCIAL_NAME_TO_KEY: Record<string, keyof typeof SOCIAL_ICONS> = {
facebook: "facebook",
instagram: "instagram",
twitter: "twitter",
x: "x",
youtube: "youtube",
threads: "threads",
github: "github",
gitlab: "gitlab",
discord: "discord",
npm: "npm",
telegram: "telegram",
};
function normalizeName(name: string): string {
return name.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
}
function getSocialKeyByName(name: string): keyof typeof SOCIAL_ICONS | null {
const normalized = normalizeName(name);
return SOCIAL_NAME_TO_KEY[normalized] ?? (SOCIAL_ICONS[normalized] ? (normalized as keyof typeof SOCIAL_ICONS) : null);
}
export function getSocialIconByName(name?: string): IconComponent {
if (!name) {
return GlobeIcon;
}
const key = getSocialKeyByName(name);
return key ? SOCIAL_ICONS[key] : GlobeIcon;
}

View File

@@ -1,217 +1,214 @@
import { compileMDX } from "next-mdx-remote/rsc";
import {
createMdxContentService,
type MdxCompileResult as CoreMdxCompileResult,
} from "@docubook/core";
import { cache } from "react";
import path from "path";
import { promises as fs } from "fs";
import remarkGfm from "remark-gfm";
import rehypePrism from "rehype-prism-plus";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeSlug from "rehype-slug";
import rehypeCodeTitles from "rehype-code-titles";
import { execFile } from "child_process";
import { promisify } from "util";
import { page_routes, ROUTES } from "./routes";
import { visit } from "unist-util-visit";
import type { Node, Parent } from "unist";
import matter from "gray-matter";
import type { TocItem } from "./toc";
import { mdxComponents as components } from "./mdx-components";
import { toIsoDateOnly } from "./utils";
// Type definitions for unist-util-visit
interface Element extends Node {
type: string;
tagName?: string;
properties?: Record<string, unknown> & {
className?: string[];
raw?: string;
};
children?: Node[];
value?: string;
raw?: string; // For internal use in processing
}
export type MdxCompileResult<Frontmatter> = CoreMdxCompileResult<Frontmatter>;
interface TextNode extends Node {
type: 'text';
value: string;
}
// custom components imports
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell } from "@/components/ui/table";
import Pre from "@/components/markdown/PreMdx";
import Note from "@/components/markdown/NoteMdx";
import { Stepper, StepperItem } from "@/components/markdown/StepperMdx";
import Image from "@/components/markdown/ImageMdx";
import Link from "@/components/markdown/LinkMdx";
import Outlet from "@/components/markdown/OutletMdx";
import Youtube from "@/components/markdown/YoutubeMdx";
import Tooltip from "@/components/markdown/TooltipsMdx";
import Card from "@/components/markdown/CardMdx";
import Button from "@/components/markdown/ButtonMdx";
import Accordion from "@/components/markdown/AccordionMdx";
import CardGroup from "@/components/markdown/CardGroupMdx";
import Kbd from "@/components/markdown/KeyboardMdx";
import { Release, Changes } from "@/components/markdown/ReleaseMdx";
import { File, Files, Folder } from "@/components/markdown/FileTreeMdx";
import AccordionGroup from "@/components/markdown/AccordionGroupMdx";
// add custom components
const components = {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Youtube,
Tooltip,
Card,
Button,
Accordion,
AccordionGroup,
CardGroup,
Kbd,
// Table Components
table: Table,
thead: TableHeader,
tbody: TableBody,
tfoot: TableFooter,
tr: TableRow,
th: TableHead,
td: TableCell,
// Release Note Components
Release,
Changes,
// File Tree Components
File,
Files,
Folder,
pre: Pre,
Note,
Stepper,
StepperItem,
img: Image,
a: Link,
Outlet,
export type DocsForSlugResult = MdxCompileResult<BaseMdxFrontmatter> & {
filePath: string;
tocs: TocItem[];
};
// helper function to handle rehype code titles, since by default we can't inject into the className of rehype-code-titles
const handleCodeTitles = () => (tree: Node) => {
visit(tree, "element", (node: Element, index: number | null, parent: Parent | null) => {
// Ensure the visited node is valid
if (!parent || index === null || node.tagName !== 'div') {
return;
}
// Check if this is the title div from rehype-code-titles
const isTitleDiv = node.properties?.className?.includes('rehype-code-title');
if (!isTitleDiv) {
return;
}
// Find the next <pre> element, skipping over other nodes like whitespace text
let nextElement = null;
for (let i = index + 1; i < parent.children.length; i++) {
const sibling = parent.children[i];
if (sibling.type === 'element') {
nextElement = sibling as Element;
break;
}
}
// If the next element is a <pre>, move the title to it
if (nextElement && nextElement.tagName === 'pre') {
const titleNode = node.children?.[0] as TextNode;
if (titleNode && titleNode.type === 'text') {
if (!nextElement.properties) {
nextElement.properties = {};
}
nextElement.properties['data-title'] = titleNode.value;
// Remove the original title div
parent.children.splice(index, 1);
// Return the same index to continue visiting from the correct position
return index;
}
}
});
};
// can be used for other pages like blogs, Guides etc
async function parseMdx<Frontmatter>(rawMdx: string) {
return await compileMDX<Frontmatter>({
source: rawMdx,
options: {
parseFrontmatter: true,
mdxOptions: {
rehypePlugins: [
preProcess,
rehypeCodeTitles,
handleCodeTitles,
rehypePrism,
rehypeSlug,
rehypeAutolinkHeadings,
postProcess,
],
remarkPlugins: [remarkGfm],
},
},
components,
});
}
// logic for docs
// Shared frontmatter shape used by docs pages.
// Keep this close to exported result types for easier discovery.
export type BaseMdxFrontmatter = {
title: string;
description: string;
image: string;
date: string;
date?: string | Date;
};
export async function getDocsForSlug(slug: string) {
// `React.cache` deduplicates calls within a single server-render pass.
// Keep request-level cache in app layer, while markdown pipeline lives in core.
const FILE_GIT_DATE_CACHE_MAX = 1000;
const fileGitDateCache = new Map<string, Date | undefined>();
function cacheSet(key: string, value: Date | undefined) {
if (fileGitDateCache.size >= FILE_GIT_DATE_CACHE_MAX) {
const firstKey = fileGitDateCache.keys().next().value!;
fileGitDateCache.delete(firstKey);
}
fileGitDateCache.set(key, value);
}
let repoLastCommitDateCache: Date | undefined | null = null;
let gitRootCache: string | undefined;
const execFileAsync = promisify(execFile);
async function getGitRootDir(): Promise<string | undefined> {
if (gitRootCache !== undefined) return gitRootCache;
try {
const { content, filePath } = await getRawMdx(slug);
const mdx = await parseMdx<BaseMdxFrontmatter>(content);
return {
...mdx,
filePath,
};
} catch (err) {
console.log(err);
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
gitRootCache = stdout.trim() || "";
return gitRootCache || undefined;
} catch {
gitRootCache = "";
return undefined;
}
}
async function getFileLastCommitDate(absoluteFilePath: string): Promise<Date | undefined> {
if (fileGitDateCache.has(absoluteFilePath)) {
return fileGitDateCache.get(absoluteFilePath);
}
try {
const gitRoot = await getGitRootDir();
if (!gitRoot) {
cacheSet(absoluteFilePath, undefined);
return undefined;
}
const relativePath = path.relative(gitRoot, absoluteFilePath);
if (
relativePath.startsWith("..") ||
!/^[a-zA-Z0-9\-_/.\s]+$/.test(relativePath) ||
/(^|\/)\.\.($|\/)/.test(relativePath)
) {
cacheSet(absoluteFilePath, undefined);
return undefined;
}
const { stdout } = await execFileAsync(
"git",
["log", "-1", "--format=%cI", "--", relativePath],
{ cwd: gitRoot }
);
const rawDate = stdout.trim();
if (!rawDate) {
cacheSet(absoluteFilePath, undefined);
return undefined;
}
const parsed = new Date(rawDate);
if (Number.isNaN(parsed.getTime())) {
cacheSet(absoluteFilePath, undefined);
return undefined;
}
cacheSet(absoluteFilePath, parsed);
return parsed;
} catch {
cacheSet(absoluteFilePath, undefined);
return undefined;
}
}
async function getRepoLastCommitDate(): Promise<Date | undefined> {
if (repoLastCommitDateCache !== null) {
return repoLastCommitDateCache ?? undefined;
}
try {
const gitRoot = await getGitRootDir();
if (!gitRoot) {
repoLastCommitDateCache = undefined;
return undefined;
}
const { stdout } = await execFileAsync("git", ["log", "-1", "--format=%cI"], {
cwd: gitRoot,
});
const rawDate = stdout.trim();
if (!rawDate) {
repoLastCommitDateCache = undefined;
return undefined;
}
const parsed = new Date(rawDate);
if (Number.isNaN(parsed.getTime())) {
repoLastCommitDateCache = undefined;
return undefined;
}
repoLastCommitDateCache = parsed;
return parsed;
} catch {
repoLastCommitDateCache = undefined;
return undefined;
}
}
const docsService = createMdxContentService<BaseMdxFrontmatter, TocItem>({
parseOptions: { components },
cacheFn: cache,
frontmatterEnricher: async (frontmatter, absoluteFilePath) => {
if (!frontmatter.date) {
const gitDate = await getFileLastCommitDate(absoluteFilePath);
if (gitDate) return { ...frontmatter, date: toIsoDateOnly(gitDate) };
const repoDate = await getRepoLastCommitDate();
if (repoDate) return { ...frontmatter, date: toIsoDateOnly(repoDate) };
// Keep `date` non-empty even outside git context.
return { ...frontmatter, date: "12-12-2025" };
}
return frontmatter;
},
});
// Return frontmatter only for a docs slug.
export async function getDocsFrontmatterForSlug(
slug: string
): Promise<BaseMdxFrontmatter | undefined> {
try {
return await docsService.getFrontmatterForSlug(slug);
} catch (error) {
console.error(error);
}
}
/**
* Get full compiled documentation (public API)
* Returns: JSX content, frontmatter, TOCs, file path
*/
export async function getDocsForSlug(slug: string): Promise<DocsForSlugResult | undefined> {
try {
return await docsService.getCompiledForSlug(slug);
} catch (error) {
console.error(error);
}
}
/**
* Get only table of contents for a slug
*/
export async function getDocsTocs(slug: string) {
const { content } = await getRawMdx(slug);
const rawMdx = content;
// Regex to match code blocks (```...```), standard markdown headings (##), and <Release> tags
const combinedRegex = /(```[\s\S]*?```)|^(#{2,4})\s(.+)$|<Release[^>]*version="([^"]+)"/gm;
let match;
const extractedHeadings = [];
while ((match = combinedRegex.exec(rawMdx)) !== null) {
// match[1] -> Code block content (ignore)
if (match[1]) continue;
// match[2] & match[3] -> Markdown headings
if (match[2]) {
const headingLevel = match[2].length;
const headingText = match[3].trim();
const slug = sluggify(headingText);
extractedHeadings.push({
level: headingLevel,
text: headingText,
href: `#${slug}`,
});
}
// match[4] -> Release component version
else if (match[4]) {
const version = match[4];
extractedHeadings.push({
level: 2,
text: `v${version}`,
href: `#${version}`,
});
}
try {
return await docsService.getTocsForSlug(slug);
} catch {
return [];
}
return extractedHeadings;
}
// Build static params for the docs route segment.
export function getDocsStaticParams() {
// Ensure the root docs page (docs/index.mdx) is statically generated at /docs.
// This page is not part of the generated docs tree routes by default.
// Filter out routes marked as noLink (parent categories that don't have their own page)
return [
{ slug: [] },
...page_routes
.map((page) => ({
slug: page.href.split("/").filter(Boolean),
}))
.filter(({ slug }) => slug.length > 0), // Exclude empty slugs
];
}
// Get previous and next page entries from the route list.
export function getPreviousNext(path: string) {
const index = page_routes.findIndex(({ href }) => href == `/${path}`);
return {
@@ -220,85 +217,36 @@ export function getPreviousNext(path: string) {
};
}
function sluggify(text: string) {
const slug = text.toLowerCase().replace(/\s+/g, "-");
return slug.replace(/[^a-z0-9-]/g, "");
}
async function getRawMdx(slug: string) {
const commonPath = path.join(process.cwd(), "/docs/");
const paths = [
path.join(commonPath, `${slug}.mdx`),
path.join(commonPath, slug, "index.mdx"),
];
for (const p of paths) {
try {
const content = await fs.readFile(p, "utf-8");
return {
content,
filePath: `docs/${path.relative(commonPath, p)}`,
};
} catch {
// ignore and try next
}
}
throw new Error(`Could not find mdx file for slug: ${slug}`);
}
function justGetFrontmatterFromMD<Frontmatter>(rawMd: string): Frontmatter {
return matter(rawMd).data as Frontmatter;
}
// Collect frontmatter of direct child pages under the given docs path.
export async function getAllChilds(pathString: string) {
const items = pathString.split("/").filter((it) => it !== "");
let page_routes_copy = ROUTES;
let nestedRoutes = ROUTES;
let prevHref = "";
// Resolve the nested route branch for the requested path.
let resolvedHref = "";
for (const it of items) {
const found = page_routes_copy.find((innerIt) => innerIt.href == `/${it}`);
const found = nestedRoutes.find((innerIt) => innerIt.href === `/${it}`);
if (!found) break;
prevHref += found.href;
page_routes_copy = found.items ?? [];
resolvedHref += found.href;
nestedRoutes = found.items ?? [];
}
if (!prevHref) return [];
if (!resolvedHref) return [];
const children = await Promise.all(
nestedRoutes.map(async (it) => {
const slug = `${resolvedHref}${it.href}`.replace(/^\/+/, "");
const frontmatter = await docsService.getFrontmatterForSlug(slug).catch(() => undefined);
if (!frontmatter) {
return null;
}
return await Promise.all(
page_routes_copy.map(async (it) => {
const slug = path.join(prevHref, it.href);
const { content } = await getRawMdx(slug);
return {
...justGetFrontmatterFromMD<BaseMdxFrontmatter>(content),
href: `/docs${prevHref}${it.href}`,
...frontmatter,
href: `/docs${resolvedHref}${it.href}`,
};
})
);
return children.filter((child): child is BaseMdxFrontmatter & { href: string } => child !== null);
}
// for copying the code in pre
const preProcess = () => (tree: Node) => {
visit(tree, (node: Node) => {
const element = node as Element;
if (element?.type === "element" && element?.tagName === "pre" && element.children) {
const [codeEl] = element.children as Element[];
if (codeEl.tagName !== "code" || !codeEl.children?.[0]) return;
const textNode = codeEl.children[0] as TextNode;
if (textNode.type === 'text' && textNode.value) {
element.raw = textNode.value;
}
}
});
};
const postProcess = () => (tree: Node) => {
visit(tree, "element", (node: Node) => {
const element = node as Element;
if (element?.type === "element" && element?.tagName === "pre") {
if (element.properties && element.raw) {
element.properties.raw = element.raw;
}
}
});
};

67
lib/mdx-components.ts Normal file
View File

@@ -0,0 +1,67 @@
import {
createMdxComponents,
type MdxComponentMap,
AccordionsMdx,
AccordionMdx,
CardsMdx,
ChangesMdx,
CodeBlock,
FileMdx,
FilesMdx,
FolderMdx,
KbdMdx,
NoteMdx,
ReleaseMdx,
StepsMdx,
StepMdx,
TabMdx,
TabsMdx,
TableBodyMdx,
TableCellMdx,
TableFooterMdx,
TableHeadMdx,
TableHeaderMdx,
TableMdx,
TableRowMdx,
TooltipMdx,
YoutubeMdx,
} from "@docubook/mdx-content";
import { ImageMdx, LinkMdx, ButtonMdx, CardMdx } from "@docubook/mdx-content/next";
import { customMdxComponents } from "@/lib/mdx";
const builtInOverrides: MdxComponentMap = {
Tabs: TabsMdx,
Tab: TabMdx,
table: TableMdx,
thead: TableHeaderMdx,
tbody: TableBodyMdx,
tfoot: TableFooterMdx,
tr: TableRowMdx,
th: TableHeadMdx,
td: TableCellMdx,
pre: CodeBlock,
Button: ButtonMdx,
Note: NoteMdx,
Step: StepMdx,
Steps: StepsMdx,
Accordion: AccordionMdx,
Accordions: AccordionsMdx,
Card: CardMdx,
Cards: CardsMdx,
Kbd: KbdMdx,
Release: ReleaseMdx,
Changes: ChangesMdx,
File: FileMdx,
Files: FilesMdx,
Folder: FolderMdx,
Youtube: YoutubeMdx,
Tooltip: TooltipMdx,
img: ImageMdx,
a: LinkMdx,
Link: LinkMdx,
};
export const mdxComponents = createMdxComponents({
...builtInOverrides,
...customMdxComponents,
});

33
lib/mdx/Outlet.tsx Normal file
View File

@@ -0,0 +1,33 @@
import Link from "next/link";
import { getAllChilds, type BaseMdxFrontmatter } from "@/lib/markdown";
type OutletProps = {
path: string;
};
type ChildCardProps = BaseMdxFrontmatter & { href: string };
export default async function Outlet({ path }: OutletProps) {
if (!path) {
throw new Error("path not provided");
}
const output = await getAllChilds(path);
return (
<div className="grid md:grid-cols-2 gap-5">
{output.map((child) => (
<ChildCard {...child} key={child.title} />
))}
</div>
);
}
function ChildCard({ description, href, title }: ChildCardProps) {
return (
<Link href={href} className="border rounded-md p-4 no-underline flex flex-col gap-0.5">
<h4 className="!my-0">{title}</h4>
<p className="text-sm text-muted-foreground !my-0">{description}</p>
</Link>
);
}

11
lib/mdx/index.ts Normal file
View File

@@ -0,0 +1,11 @@
import type { MdxComponentMap } from "@docubook/mdx-content";
import Outlet from "@/lib/mdx/Outlet";
// import your custom MDX components here and add them to the `customMdxComponents` object below to make them available in your MDX files. For example:
// import { MyCustomComponent } from "@/lib/mdx/MyCustomComponent";
export const customMdxComponents: MdxComponentMap = {
Outlet,
// MyCustomComponent, --- IGNORE ---
};
// you must also add MyCustomComponent.tsx to lib/mdx/MyCustomComponent.tsx and export it from there, and then export it from this file as well to make it available for import in mdx-components.ts.

View File

@@ -1,4 +1,4 @@
import docuConfig from "@/docu.json"; // Import JSON file
import docuConfig from "@/docu.json";
export type ContextInfo = {
icon: string;
@@ -14,7 +14,7 @@ export type EachRoute = {
items?: EachRoute[];
};
export const ROUTES: EachRoute[] = docuConfig.routes;
export const ROUTES: EachRoute[] = (docuConfig as { routes: EachRoute[] }).routes;
type Page = { title: string; href: string };

View File

@@ -1,5 +1,11 @@
export const algoliaConfig = {
export const algoliaConfig: {
appId: string | undefined
apiKey: string | undefined
indexName: string | undefined
askAiAssistantId: string | undefined
} = {
appId: process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_APP_ID,
apiKey: process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_API_KEY,
indexName: process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_INDEX_NAME,
askAiAssistantId: process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_ASKAI_ASSISTANT_ID,
}

View File

@@ -1,8 +1,5 @@
export interface TocItem {
level: number;
text: string;
href: string;
}
export type { TocItem } from "@docubook/core";
import type { TocItem } from "@docubook/core";
export interface MobTocProps {
tocs: TocItem[];

View File

@@ -5,35 +5,46 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Thursday, May 23, 2024
export function formatDate(dateStr: string): string {
const [day, month, year] = dateStr.split("-").map(Number)
const date = new Date(year, month - 1, day)
/** Parse both `dd-MM-yyyy` and ISO 8601 date strings into a Date object. */
function parseDate(dateStr: string): Date {
// ISO 8601: starts with 4-digit year (e.g. "2026-04-05" or "2026-04-05T00:00:00.000Z")
if (/^\d{4}-/.test(dateStr)) return new Date(dateStr);
// Legacy dd-MM-yyyy (e.g. "05-04-2026")
const [day, month, year] = dateStr.split("-").map(Number);
return new Date(year, month - 1, day);
}
// "Thursday, April 5, 2026"
export function formatDate(dateStrOrDate: string | Date): string {
const date = dateStrOrDate instanceof Date ? dateStrOrDate : parseDate(dateStrOrDate);
const options: Intl.DateTimeFormatOptions = {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}
};
return date.toLocaleDateString("en-US", options)
return date.toLocaleDateString("en-US", options);
}
// May 23, 2024
export function formatDate2(dateStr: string): string {
const [day, month, year] = dateStr.split("-").map(Number)
const date = new Date(year, month - 1, day)
// "Apr 5, 2026"
export function formatDate2(dateStrOrDate: string | Date): string {
const date = dateStrOrDate instanceof Date ? dateStrOrDate : parseDate(dateStrOrDate);
const options: Intl.DateTimeFormatOptions = {
month: "short",
day: "numeric",
year: "numeric",
}
return date.toLocaleDateString("en-US", options)
};
return date.toLocaleDateString("en-US", options);
}
export function stringToDate(date: string) {
const [day, month, year] = date.split("-").map(Number)
return new Date(year, month - 1, day)
export function stringToDate(date: string | Date) {
return date instanceof Date ? date : parseDate(date);
}
export function toIsoDateOnly(dateStrOrDate: string | Date): string {
const date = stringToDate(dateStrOrDate)
return date.toISOString().slice(0, 10)
}