refactor: docubook@latest template nextjs-docker
This commit is contained in:
121
lib/icon.ts
Normal file
121
lib/icon.ts
Normal 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;
|
||||
}
|
||||
476
lib/markdown.ts
476
lib/markdown.ts
@@ -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
67
lib/mdx-components.ts
Normal 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
33
lib/mdx/Outlet.tsx
Normal 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
11
lib/mdx/index.ts
Normal 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.
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
41
lib/utils.ts
41
lib/utils.ts
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user