& {
+ 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 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 , 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(rawMdx: string) {
- return await compileMDX({
- 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();
+
+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 {
+ if (gitRootCache !== undefined) return gitRootCache;
+
try {
- const { content, filePath } = await getRawMdx(slug);
- const mdx = await parseMdx(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 {
+ 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 {
+ 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({
+ 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 {
+ 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 {
+ 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 tags
- const combinedRegex = /(```[\s\S]*?```)|^(#{2,4})\s(.+)$|]*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(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(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;
- }
- }
- });
-};
diff --git a/lib/mdx-components.ts b/lib/mdx-components.ts
new file mode 100644
index 0000000..357f69c
--- /dev/null
+++ b/lib/mdx-components.ts
@@ -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,
+});
diff --git a/lib/mdx/Outlet.tsx b/lib/mdx/Outlet.tsx
new file mode 100644
index 0000000..7fc10d5
--- /dev/null
+++ b/lib/mdx/Outlet.tsx
@@ -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 (
+
+ {output.map((child) => (
+
+ ))}
+
+ );
+}
+
+function ChildCard({ description, href, title }: ChildCardProps) {
+ return (
+
+ {title}
+ {description}
+
+ );
+}
diff --git a/lib/mdx/index.ts b/lib/mdx/index.ts
new file mode 100644
index 0000000..06d45f8
--- /dev/null
+++ b/lib/mdx/index.ts
@@ -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.
diff --git a/lib/routes.ts b/lib/routes.ts
index a5bcff2..9bea03f 100644
--- a/lib/routes.ts
+++ b/lib/routes.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 };
diff --git a/lib/search/algolia.ts b/lib/search/algolia.ts
index 451bb2a..fdd61dd 100644
--- a/lib/search/algolia.ts
+++ b/lib/search/algolia.ts
@@ -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,
}
diff --git a/lib/toc.ts b/lib/toc.ts
index ae2eb9f..99ddbf5 100644
--- a/lib/toc.ts
+++ b/lib/toc.ts
@@ -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[];
diff --git a/lib/utils.ts b/lib/utils.ts
index 2191bf3..e94dcf0 100644
--- a/lib/utils.ts
+++ b/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)
}
diff --git a/next.config.mjs b/next.config.mjs
index 69082f3..baa953f 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -9,6 +9,24 @@ const nextConfig = {
},
],
},
+ experimental: {
+ optimizePackageImports: ["lucide-react", "react-icons"],
+ },
+ async headers() {
+ return [
+ {
+ source: "/(.*)",
+ headers: [
+ { key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" },
+ { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
+ { key: "X-Frame-Options", value: "DENY" },
+ { key: "X-Content-Type-Options", value: "nosniff" },
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
+ { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
+ ],
+ },
+ ]
+ },
}
export default nextConfig
diff --git a/package.json b/package.json
index 3335c72..3626317 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,9 @@
{
"name": "docubook",
- "version": "2.5.1",
+ "template": "nextjs-docker",
+ "description": "Next.js standalone with docker image documentation template for DocuBook",
+ "version": "1.0.0",
+ "type": "module",
"private": true,
"scripts": {
"dev": "next dev",
@@ -9,10 +12,10 @@
"lint": "eslint ."
},
"dependencies": {
- "@docsearch/css": "^3.9.0",
- "@docsearch/react": "^3.9.0",
- "@radix-ui/react-accordion": "^1.2.12",
- "@radix-ui/react-avatar": "^1.1.11",
+ "@docsearch/css": "^4.6.2",
+ "@docsearch/react": "^4.6.2",
+ "@docubook/core": "^1.7.0",
+ "@docubook/mdx-content": "^3.2.1",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -20,48 +23,41 @@
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
- "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
- "algoliasearch": "^5.46.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "cmdk": "^1.1.1",
- "framer-motion": "^12.26.2",
- "geist": "^1.5.1",
- "gray-matter": "^4.0.3",
- "lucide-react": "^0.511.0",
- "next": "^16.1.6",
- "next-mdx-remote": "^6.0.0",
- "next-themes": "^0.4.4",
+ "framer-motion": "^12.38.0",
+ "geist": "^1.7.0",
+ "lucide-react": "^1.7.0",
+ "next": "^16.2.6",
+ "next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3",
- "react-icons": "^5.5.0",
- "rehype-autolink-headings": "^7.1.0",
- "rehype-code-titles": "^1.2.1",
- "rehype-prism-plus": "^2.0.1",
- "rehype-slug": "^6.0.0",
- "remark-gfm": "^4.0.1",
"sonner": "^1.7.4",
- "tailwind-merge": "^2.6.0",
- "tailwindcss-animate": "^1.0.7",
- "unist-util-visit": "^5.0.0"
+ "tailwind-merge": "^2.6.1",
+ "tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
- "@tailwindcss/postcss": "^4.1.18",
+ "@eslint/js": "^9.39.4",
+ "@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/typography": "^0.5.19",
- "@types/node": "^20.19.30",
+ "@types/node": "^20.19.37",
"@types/react": "19.2.8",
"@types/react-dom": "19.2.3",
- "autoprefixer": "^10.4.23",
- "eslint": "^9.39.2",
+ "@types/unist": "^3.0.3",
+ "autoprefixer": "^10.4.27",
+ "eslint": "^9.39.4",
"eslint-config-next": "16.1.3",
- "postcss": "^8.5.6",
- "tailwindcss": "^4.1.18",
- "typescript": "^5.9.3"
+ "postcss": "^8.5.8",
+ "prettier": "^3.8.1",
+ "prettier-plugin-tailwindcss": "^0.6.14",
+ "tailwindcss": "^4.2.2",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.57.2"
},
"overrides": {
"@types/react": "19.2.8",
"@types/react-dom": "19.2.3"
}
-}
+}
\ No newline at end of file
diff --git a/postcss.config.cjs b/postcss.config.cjs
new file mode 100644
index 0000000..483f378
--- /dev/null
+++ b/postcss.config.cjs
@@ -0,0 +1,5 @@
+module.exports = {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
diff --git a/postcss.config.js b/postcss.config.js
deleted file mode 100644
index b4bee66..0000000
--- a/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- plugins: {
- '@tailwindcss/postcss': {},
- autoprefixer: {},
- },
-};
diff --git a/styles/algolia.css b/styles/algolia.css
index 4ec175b..55dd493 100644
--- a/styles/algolia.css
+++ b/styles/algolia.css
@@ -9,13 +9,14 @@
--docsearch-logo-color: hsl(var(--primary-foreground));
/* Modal */
- --docsearch-modal-width: 560px;
+ --docsearch-modal-width: 800px;
--docsearch-modal-height: 600px;
--docsearch-modal-background: hsl(var(--background));
--docsearch-modal-shadow: 0 0 0 1px hsl(var(--border)), 0 8px 20px rgba(0, 0, 0, 0.2);
/* SearchBox */
- --docsearch-searchbox-height: 56px;
+ --docsearch-searchbox-width: 230px;
+ --docsearch-searchbox-height: 35px;
--docsearch-searchbox-background: hsl(var(--input));
--docsearch-searchbox-focus-background: hsl(var(--card));
--docsearch-searchbox-shadow: none;
@@ -42,8 +43,8 @@
background-color: hsl(var(--secondary));
border: 1px solid hsl(var(--border));
border-radius: 9999px;
- width: 250px;
- height: 35px;
+ width: var(--docsearch-searchbox-width);
+ height: var(--docsearch-searchbox-height);
color: hsl(var(--muted-foreground));
transition: width 0.3s ease;
margin: 0;
@@ -98,6 +99,27 @@
border-top: 1px solid hsl(var(--border));
}
+.dark .DocSearch-Footer .DocSearch-Label {
+ color: var(--docsearch-muted-color);
+}
+
+.dark button.DocSearch-Action.DocSearch-Close {
+ color: var(--docsearch-muted-color);
+}
+
+.dark .DocSearch-Hit-Container .DocSearch-Hit-icon {
+ color: var(--docsearch-muted-color);
+}
+
+.dark .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-Container .DocSearch-Hit-icon,
+.dark .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-action {
+ color: var(--docsearch-highlight-color);
+}
+
+.dark .DocSearch-Hit-Container .DocSearch-Hit-path {
+ color: var(--docsearch-muted-color);
+}
+
.DocSearch-Footer--commands kbd {
background-color: hsl(var(--secondary));
border: 1px solid hsl(var(--border));
@@ -115,7 +137,7 @@
color: hsl(var(--muted-foreground));
border: 1px solid hsl(var(--border));
box-shadow: none;
- padding: 2px 4px;
+ padding: 4px 8px;
margin-right: 0.4em;
height: 20px;
width: 32px;
diff --git a/styles/globals.css b/styles/globals.css
index 807bd7e..e60226e 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -1,7 +1,6 @@
-@import 'tailwindcss';
+@import "tailwindcss";
@plugin '@tailwindcss/typography';
-
@custom-variant dark (&:is(.dark *));
@utility container {
@@ -85,8 +84,27 @@
}
}
- @keyframes shiny-text {
+ @keyframes collapsible-down {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(--radix-collapsible-content-height);
+ }
+ }
+
+ @keyframes collapsible-up {
+ from {
+ height: var(--radix-collapsible-content-height);
+ }
+
+ to {
+ height: 0;
+ }
+ }
+
+ @keyframes shiny-text {
0%,
90%,
100% {
@@ -109,7 +127,6 @@
color utility to any element that depends on these defaults.
*/
@layer base {
-
*,
::after,
::before,
@@ -119,6 +136,14 @@
}
}
+@utility animate-collapsible-down {
+ animation: collapsible-down 0.2s ease-out;
+}
+
+@utility animate-collapsible-up {
+ animation: collapsible-up 0.2s ease-out;
+}
+
@utility animate-shine {
--animate-shine: shine var(--duration) infinite linear;
animation: var(--animate-shine);
@@ -206,7 +231,7 @@
overflow-x: auto;
}
- pre>code {
+ pre > code {
display: grid;
max-width: inherit !important;
padding: 14px 0 !important;
@@ -230,20 +255,19 @@
}
.highlight-line {
- @apply bg-primary/5 border-l-2 border-primary/30;
+ @apply bg-primary/5 border-primary/30 border-l-2;
}
.rehype-code-title {
- @apply px-2 -mb-8 w-full text-sm pb-5 font-medium mt-5 font-code;
+ @apply font-code mt-5 -mb-8 w-full px-2 pb-5 text-sm font-medium;
}
- .highlight-comp>code {
+ .highlight-comp > code {
background-color: transparent !important;
}
}
@layer utilities {
-
@keyframes shine {
0% {
background-position: 0% 0%;
@@ -257,4 +281,4 @@
background-position: 0% 0%;
}
}
-}
\ No newline at end of file
+}
diff --git a/styles/override.css b/styles/override.css
index 24bb3a0..f94cbe3 100644
--- a/styles/override.css
+++ b/styles/override.css
@@ -87,121 +87,8 @@
}
.dark .punctuation {
- color: hsl(85 20% 70%);
- /* Light gray-green */
-}
-
-/* Custom styling for code blocks */
-
-.code-block-container {
- position: relative;
- margin: 1.5rem 0;
- border: 1px solid hsl(var(--border));
- overflow: hidden;
- font-size: 0.875rem;
- border-radius: 0.75rem;
-}
-
-.code-block-header {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- background-color: hsl(var(--muted));
- padding: 0.5rem 1rem;
- border-bottom: 1px solid hsl(var(--border));
- color: hsl(var(--muted-foreground));
- font-family: monospace;
- font-size: 0.8rem;
-}
-
-.code-block-actions {
- position: absolute;
- top: 0.5rem;
- right: 0.75rem;
- z-index: 10;
-}
-
-.code-block-actions button {
- color: hsl(var(--muted-foreground));
- transition: color 0.2s ease-in-out;
-}
-
-.code-block-actions button:hover {
- color: hsl(var(--foreground));
-}
-
-
-.code-block-body pre[class*="language-"] {
- margin: 0 !important;
- padding: 0 !important;
- background: transparent !important;
-}
-
-.line-numbers-wrapper {
- position: absolute;
- top: 0;
- left: 0;
- width: 3rem;
- padding-top: 1rem;
- text-align: right;
- color: var(--line-number-color);
- user-select: none;
-}
-
-.line-highlight {
- position: absolute;
- left: 0;
- right: 0;
- background: hsl(var(--primary) / 0.1);
- border-left: 2px solid hsl(var(--primary));
- pointer-events: none;
-}
-
-.code-block-body pre[data-line-numbers="true"] .line-highlight {
- padding-left: 3.5rem;
-}
-
-.code-block-body::-webkit-scrollbar {
- height: 8px;
-}
-
-.code-block-body::-webkit-scrollbar-track {
- background: transparent;
-}
-
-.code-block-body::-webkit-scrollbar-thumb {
- background: hsl(var(--border));
- border-radius: 4px;
-}
-
-.code-block-body::-webkit-scrollbar-thumb:hover {
- background: hsl(var(--muted));
-}
-
-/* Custom styling for youtube blocks */
-.youtube {
- position: relative;
- padding-bottom: 56.25%;
- /* Aspect Ratio 16:9 */
- height: 0;
- overflow: hidden;
- background: #000;
- /* Black background to blend the player */
- border-radius: 8px;
- /* Rounded corners */
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
- /* Soft shadow */
-}
-
-.youtube iframe {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- border: none;
- border-radius: 8px;
- /* Rounded corners on iframe */
+ color: #9ca3af;
+ /* Lighter gray for dark mode */
}
/* Hide main navbar and footer when docs layout is active */
@@ -216,4 +103,4 @@ body:has(.docs-layout) #main-content {
width: 100%;
padding: 0;
margin: 0;
-}
\ No newline at end of file
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
deleted file mode 100644
index 305592b..0000000
--- a/tailwind.config.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import typography from "@tailwindcss/typography"
-
-const config = {
- darkMode: ["class"],
- content: [
- "./app/**/*.{ts,tsx}",
- "./components/**/*.{ts,tsx}",
- "./contents/**/*.{md,mdx}",
- "../../packages/ui/src/**/*.{ts,tsx}",
- ],
- plugins: [typography],
-}
-
-export default config
diff --git a/template.config.json b/template.config.json
new file mode 100644
index 0000000..4991423
--- /dev/null
+++ b/template.config.json
@@ -0,0 +1,22 @@
+{
+ "name": "nextjs-docker",
+ "id": "nextjs-docker",
+ "description": "Next.js standalone with Docker image (optimized for coolify, etc.)",
+ "framework": "nextjs",
+ "base": "nextjs",
+ "packageManagers": ["npm", "yarn", "pnpm", "bun"],
+ "features": [
+ "Next.js 16",
+ "React 19",
+ "TypeScript",
+ "Tailwind CSS",
+ "MDX Support",
+ "Dark Mode",
+ "Search (Algolia)",
+ "Responsive Design",
+ "Docker Deployment"
+ ],
+ "author": "wildan.nrs",
+ "repository": "https://github.com/DocuBook/packages/template/nextjs-docker",
+ "homepage": "https://docubook.pro"
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
new file mode 100644
index 0000000..f54758f
--- /dev/null
+++ b/tsconfig.base.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "Base",
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "exclude": ["node_modules"]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 9e9bbf7..2977737 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,41 +1,22 @@
{
- "compilerOptions": {
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "react-jsx",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": [
- "./*"
- ]
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.base.json",
+ "compilerOptions": {
+ "plugins": [{ "name": "next" }],
+ "paths": {
+ "@/*": [
+ "./*"
+ ]
+ }
},
- "target": "ES2017"
- },
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ".next/types/**/*.ts",
- ".next/dev/types/**/*.ts"
- ],
- "exclude": [
- "node_modules"
- ]
-}
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file