diff --git a/apps/docs/app/(home)/page.tsx b/apps/docs/app/(home)/page.tsx index 61efbe3b5..bf64252ee 100644 --- a/apps/docs/app/(home)/page.tsx +++ b/apps/docs/app/(home)/page.tsx @@ -445,11 +445,12 @@ function Hero(): React.ReactElement { Build excellent documentation site with less effort

- Fumadocs is a beautiful{' '} - documentation framework with{' '} - a complete toolchain, - powered by Next.js App Router. - Designed to be flexible and fast. + Fumadocs is a{' '} + beautiful & robust{' '} + documentation framework with a complete toolchain. Designed for{' '} + Flexibility,{' '} + Performance and{' '} + Next.js.

- - - - - - - - - - - - - - - - - - - - - - - - - {children} Update the route handler (From Source) -You don't need to change anything, it handles i18n automatically. +Enable i18n from source object (in `loader` function).

Update the route handler (From Search Indexes)

diff --git a/apps/docs/content/docs/mdx/image.mdx b/apps/docs/content/docs/mdx/image.mdx deleted file mode 100644 index 058542ecb..000000000 --- a/apps/docs/content/docs/mdx/image.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Image Optimization -description: Adding images to your docs ---- - -Fumadocs MDX resolves images into static imports with [Remark Image](/docs/headless/mdx/remark-image). -Therefore, your images will be optimized automatically by the Next.js Image API. - -```mdx -![Hello](./hello.png) - -or in public folder - -![Hello](/hello.png) -``` - -Yields: - -```mdx -import HelloImage from './hello.png'; - -Hello -``` - -![Banner](/banner.png) diff --git a/apps/docs/content/docs/mdx/include.mdx b/apps/docs/content/docs/mdx/include.mdx new file mode 100644 index 000000000..4d51b308a --- /dev/null +++ b/apps/docs/content/docs/mdx/include.mdx @@ -0,0 +1,14 @@ +--- +title: Include +description: Reuse content from other Markdown files +--- + +## Usage + +Specify the target Markdown file path in `` tag (relative). + +```mdx title="page.mdx" +./another.mdx +``` + +This will display the content from target file (e.g. `another.mdx`). diff --git a/apps/docs/content/docs/mdx/meta.json b/apps/docs/content/docs/mdx/meta.json index 97583ffc3..18668684b 100644 --- a/apps/docs/content/docs/mdx/meta.json +++ b/apps/docs/content/docs/mdx/meta.json @@ -13,8 +13,8 @@ "mdx", "plugin", "---Features---", + "include", "last-modified", - "image", "manifest" ] } diff --git a/apps/docs/content/docs/mdx/performance.mdx b/apps/docs/content/docs/mdx/performance.mdx index e589d693f..255392afb 100644 --- a/apps/docs/content/docs/mdx/performance.mdx +++ b/apps/docs/content/docs/mdx/performance.mdx @@ -4,16 +4,37 @@ description: The performance of Fumadocs MDX icon: Rocket --- -## About +## Overview Fumadocs MDX is a bundler plugin, in other words, it has a higher performance bottleneck. With bundlers like Webpack and Turbopack, it is enough for large docs sites with nearly 500+ MDX files, which is sufficient for almost all docs sites. -### Advantages - Since Fumadocs MDX works with your bundler, you can import any files including client components in your MDX files. This allows a high flexibility and ensures everything is optimized by default. +### Image Optimization + +Fumadocs MDX resolves images into static imports with [Remark Image](/docs/headless/mdx/remark-image). +Therefore, your images will be optimized automatically by the Next.js Image API. + +```mdx +![Hello](./hello.png) + +or in public folder + +![Hello](/hello.png) +``` + +Yields: + +```mdx +import HelloImage from './hello.png'; + +Hello +``` + +![Banner](/banner.png) + ### Caveats Although Fumadocs MDX can handle nearly 500+ files, you are recommended to use other alternatives when the amount of MDX files exceeds 400. @@ -26,22 +47,9 @@ This is because of: ## Alternative -A popular alternative is `next-mdx-remote`, it allows rendering pages on-demand with SSG which can **highly increase your build speed.** -However, you cannot use import in MDX files anymore. - ### Remote Source -`next-mdx-remote` supports remote sources. - -For a huge amount of files, we would also recommend using a remote source like GitHub. -This allows you to fetch file content directly from your remote source and render it on-demand. - -This is very important because without a remote source to hold your files, the content of these files will be a part of your build. -Since host platforms like Vercel are not supposed to hold a huge build output, you might hit the limit of their plans. - -### Integration - -Fumadocs doesn't provide an integration for `next-mdx-remote`, the main reason is `next-mdx-remote` supports remote sources, which are not necessarily file-system based. -Fumadocs cannot generate a page tree for non file-system based sources. +Remote sources don't need to pre-compile MDX files, it can compile them on-demand with SSG which can **highly increase your build speed.** +However, you cannot use import in MDX files anymore. -To use `next-mdx-remote` with Fumadocs, see [MDX Remote](/docs/headless/remote) for configuring remote sources. +See [Remote Sources](/docs/headless/remote) for configuring remote sources. diff --git a/apps/docs/content/docs/ui/index.mdx b/apps/docs/content/docs/ui/index.mdx index e979c0915..cfa625b7b 100644 --- a/apps/docs/content/docs/ui/index.mdx +++ b/apps/docs/content/docs/ui/index.mdx @@ -24,7 +24,7 @@ Handles most of the logic, including document search, content source adapters, a } title='Fumadocs UI'> -The default theme of Fumadocs, it offers a beautiful look for documentation sites and many interactive components. +The default theme of Fumadocs, it offers a beautiful look for documentation sites and interactive components. @@ -55,8 +55,8 @@ You can take this site as an example of docs site built with Fumadocs. Since Next.js is already a powerful framework, most features can be implemented with **only Next.js**. -Fumadocs can process the content (e.g. syntax highlighting), and you can use the default theme (Fumadocs UI) or your custom theme. -It is particularly helpful if you want to build complicated things like search. +Fumadocs provides additional tooling for Next.js, including syntax highlighting, document search, and a default theme (Fumadocs UI). +It helps you to avoid reinventing the wheels. ### Terminology @@ -64,7 +64,7 @@ For a better understanding of the docs, there are some common terminologies you **Page Tree:** A tree of all the pages, separators and folders. It is essential for navigation elements, usually generated by the content source or hardcoded. See [Types definitions](/docs/headless/page-tree). -**Markdown (MDX) File:** A Markdown file could be located in a folder, or available remotely. +**Markdown/MDX:** Markdown is a markup language for creating formatted text. By default, Fumadocs supports Markdown and MDX (superset of Markdown). Some basic knowledge of Next.js App Router would be useful for further customisations. @@ -96,6 +96,7 @@ It will ask you the following questions: - Which content source to use? (Recommended: Fumadocs MDX) - Configure Tailwind CSS? +- Add ESLint config? - Install dependencies? After that, it will initialize a new fumadocs app. @@ -103,7 +104,7 @@ Now you can start hacking! ### Enjoy! -Create your first mdx file in the docs folder. +Create your first MDX file in the docs folder. ```mdx title="content/docs/index.mdx" --- @@ -123,7 +124,7 @@ npm run dev In the project, you can see: -- `lib/source.ts`: Where you organize code for content source adapter, we use [`loader`](/docs/headless/source-api) to read from content source. +- `lib/source.ts`: Where you organize code for content source adapter, we use [`loader`](/docs/headless/source-api) API to load from your content source. - `app/layout.config.tsx`: Shared options for layouts, this is optional but preferred to keep. - `app/(home)`: The route group for your landing page and other pages. - `app/docs`: The documentation layout and pages. @@ -137,8 +138,7 @@ For Fumadocs MDX, see [Introduction](/docs/mdx) for details: - `source.config.ts`: Config file for Fumadocs MDX. -For other formats and content sources, they can be supported using a custom adapter. -If you use a custom content source (e.g. Notion), you can [build a custom `loader` instead](/docs/headless/remote). +For other formats and content sources that is not supported by default (e.g. Sanity), you can [build a custom content source](/docs/headless/remote). ## FAQ diff --git a/apps/docs/content/docs/ui/internationalization.mdx b/apps/docs/content/docs/ui/internationalization.mdx index 1e893357b..195e1e328 100644 --- a/apps/docs/content/docs/ui/internationalization.mdx +++ b/apps/docs/content/docs/ui/internationalization.mdx @@ -4,6 +4,13 @@ description: Support multiple languages in your documentation icon: Globe --- + + + Fumadocs is not a full-powered i18n library, you can also use other libraries like [next-intl](https://github.com/amannn/next-intl). + I18n functionality of Fumadocs works without the built-in middleware. + + + You can setup i18n using Fumadocs CLI or update the configurations manually. > Read the [Next.js Docs](https://nextjs.org/docs/app/building-your-application/routing/internationalization) to learn more about implementing I18n in Next.js. @@ -42,7 +49,7 @@ export const source = loader({ }); ``` -Update the usages to your source: +Update the usages to your source to include a locale code: ```ts import { source } from '@/lib/source'; @@ -57,7 +64,8 @@ source.getPage(params.slug, params.lang); source.getPages(params.lang); ``` -See [Source API](/docs/headless/source-api) for other usages. +Note that without providing a locale code, it uses your default locale instead. +You can see [Source API](/docs/headless/source-api) for other usages. ### Middleware @@ -105,20 +113,6 @@ export default function RootLayout({ } ``` -### Docs Page - -Make sure to update references to `source` and configure Static Site Generation correctly. - -```json doc-gen:file -{ - "file": "../../examples/i18n/app/[lang]/docs/[[...slug]]/page.tsx", - "codeblock": { - "lang": "tsx", - "meta": "title=\"page.tsx\"" - } -} -``` - ### Writing Documents see [Page Conventions](/docs/ui/page-conventions#internationalization) to learn how to organize your documents. @@ -171,3 +165,17 @@ export const baseOptions: BaseLayoutProps = { i18n: true, }; ``` + +## Navigation + +Fumadocs will only handle i18n navigation for its own layouts (e.g. sidebar components), +you have to re-create components like `` from `next/link` and `useParams` hook to auto attend the locale to `href`. + +In addition, the [`fumadocs-core/dynamic-link`](/docs/headless/components/link#dynamic-hrefs) component supports linking to dynamic hrefs, including the locale prefix. +For Markdown/MDX content, you may use it instead of the default anchor (`a`) component: + +```mdx title="content.mdx" +import { DynamicLink } from 'fumadocs-core/dynamic-link'; + +This is a link +``` diff --git a/apps/docs/content/docs/ui/page-conventions.mdx b/apps/docs/content/docs/ui/page-conventions.mdx index 1ecb2faaa..e0a281879 100644 --- a/apps/docs/content/docs/ui/page-conventions.mdx +++ b/apps/docs/content/docs/ui/page-conventions.mdx @@ -1,6 +1,6 @@ --- title: Organizing Pages description: Organize documents with file-system based routing -_mdx: - mirror: ../headless/page-conventions.mdx --- + +../headless/page-conventions.mdx diff --git a/apps/docs/scripts/lint.mts b/apps/docs/scripts/lint.mts index 9bfc1c084..37a06a9c1 100644 --- a/apps/docs/scripts/lint.mts +++ b/apps/docs/scripts/lint.mts @@ -5,6 +5,7 @@ import { getTableOfContents } from 'fumadocs-core/server'; import fs from 'node:fs/promises'; import path from 'node:path'; import matter from 'gray-matter'; +import { remarkInclude } from 'fumadocs-mdx/config'; async function readFromPath(file: string) { const content = await fs @@ -28,23 +29,44 @@ async function checkLinks() { await fg('content/blog/**/*.mdx').then((files) => files.map(readFromPath)), ); + const docs = docsFiles.map(async (file) => { + const info = parseFilePath(path.relative('content/docs', file.path)); + + return { + value: getSlugs(info), + hashes: ( + await getTableOfContents( + { + path: file.path, + value: file.content, + }, + [remarkInclude], + ) + ).map((item) => item.url.slice(1)), + }; + }); + + const blogs = blogFiles.map(async (file) => { + const info = parseFilePath(path.relative('content/blog', file.path)); + + return { + value: getSlugs(info)[0], + hashes: ( + await getTableOfContents( + { + path: file.path, + value: file.content, + }, + [remarkInclude], + ) + ).map((item) => item.url.slice(1)), + }; + }); + const scanned = await scanURLs({ populate: { - '(home)/blog/[slug]': blogFiles.map((file) => { - const info = parseFilePath(path.relative('content/blog', file.path)); - - return { value: getSlugs(info)[0] }; - }), - 'docs/[...slug]': docsFiles.map((file) => { - const info = parseFilePath(path.relative('content/docs', file.path)); - - return { - value: getSlugs(info), - hashes: file.data?._mdx?.mirror - ? undefined - : getTableOfContents(file.content).map((item) => item.url.slice(1)), - }; - }), + '(home)/blog/[slug]': await Promise.all(blogs), + 'docs/[...slug]': await Promise.all(docs), }, }); diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 183a42f57..67b011767 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # next-docs-zeta +## 14.6.4 + +### Patch Changes + +- b71064a: Support remark plugins & vfile input on `getTableOfContents` + ## 14.6.3 ## 14.6.2 diff --git a/packages/core/package.json b/packages/core/package.json index ac22d52ee..a3ffdf7b7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "fumadocs-core", - "version": "14.6.3", + "version": "14.6.4", "description": "The library for building a documentation website in Next.js", "keywords": [ "NextJs", @@ -116,7 +116,8 @@ "shiki-transformers": "^1.0.1", "tsconfig": "workspace:*", "typescript": "^5.7.2", - "unified": "^11.0.5" + "unified": "^11.0.5", + "vfile": "^6.0.3" }, "peerDependencies": { "@oramacloud/client": "1.x.x || 2.x.x", diff --git a/packages/core/src/server/get-toc.ts b/packages/core/src/server/get-toc.ts index 2cca08e44..c93ed65b0 100644 --- a/packages/core/src/server/get-toc.ts +++ b/packages/core/src/server/get-toc.ts @@ -1,6 +1,8 @@ import { remark } from 'remark'; import type { ReactNode } from 'react'; import { remarkHeading } from '@/mdx-plugins/remark-heading'; +import type { PluggableList } from 'unified'; +import type { Compatible } from 'vfile'; export interface TOCItemType { title: ReactNode; @@ -13,9 +15,38 @@ export type TableOfContents = TOCItemType[]; /** * Get Table of Contents from markdown/mdx document (using remark) * - * @param content - Markdown content + * @param content - Markdown content or file */ -export function getTableOfContents(content: string): TableOfContents { +export function getTableOfContents(content: Compatible): TableOfContents; + +/** + * Get Table of Contents from markdown/mdx document (using remark) + * + * @param content - Markdown content or file + * @param remarkPlugins - remark plugins to be applied first + */ +export function getTableOfContents( + content: Compatible, + remarkPlugins: PluggableList, +): Promise; + +export function getTableOfContents( + content: Compatible, + remarkPlugins?: PluggableList, +): TableOfContents | Promise { + if (remarkPlugins) { + return remark() + .use(remarkPlugins) + .use(remarkHeading) + .process(content) + .then((result) => { + if ('toc' in result.data) return result.data.toc as TableOfContents; + + return []; + }); + } + + // compatible with previous versions const result = remark().use(remarkHeading).processSync(content); if ('toc' in result.data) return result.data.toc as TableOfContents; diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 161da3541..f7126de85 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,7 @@ # create-next-docs-app +## 14.6.4 + ## 14.6.3 ## 14.6.2 diff --git a/packages/create-app/package.json b/packages/create-app/package.json index abee5fb9c..8e8ef1f5a 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "create-fumadocs-app", - "version": "14.6.3", + "version": "14.6.4", "description": "Create a new documentation site with Fumadocs", "keywords": [ "NextJs", diff --git a/packages/mdx/CHANGELOG.md b/packages/mdx/CHANGELOG.md index 3dc04d0eb..64e716008 100644 --- a/packages/mdx/CHANGELOG.md +++ b/packages/mdx/CHANGELOG.md @@ -1,5 +1,13 @@ # next-docs-mdx +## 11.2.1 + +### Patch Changes + +- 3445182: Fix `include` hot-reload issues +- Updated dependencies [b71064a] + - fumadocs-core@14.6.4 + ## 11.2.0 ### Minor Changes diff --git a/packages/mdx/package.json b/packages/mdx/package.json index 137afdfa8..eaa6fc12c 100644 --- a/packages/mdx/package.json +++ b/packages/mdx/package.json @@ -1,6 +1,6 @@ { "name": "fumadocs-mdx", - "version": "11.2.0", + "version": "11.2.1", "description": "The built-in source for Fumadocs", "keywords": [ "NextJs", diff --git a/packages/mdx/src/config/cached.ts b/packages/mdx/src/config/cached.ts index f4e3175ec..352bab796 100644 --- a/packages/mdx/src/config/cached.ts +++ b/packages/mdx/src/config/cached.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto'; -import fs from 'node:fs'; +import * as fs from 'node:fs'; import { loadConfig, type LoadedConfig } from '@/config/load'; const cache = new Map< diff --git a/packages/mdx/src/config/index.ts b/packages/mdx/src/config/index.ts index a232086fe..c1aafc6cd 100644 --- a/packages/mdx/src/config/index.ts +++ b/packages/mdx/src/config/index.ts @@ -2,3 +2,4 @@ export * from './types'; export * from './define'; export * from '../utils/mdx-options'; export { frontmatterSchema, metaSchema } from '../utils/schema'; +export * from '../mdx-plugins/remark-include'; diff --git a/packages/mdx/src/loader-mdx.ts b/packages/mdx/src/loader-mdx.ts index e75456a7d..05662f378 100644 --- a/packages/mdx/src/loader-mdx.ts +++ b/packages/mdx/src/loader-mdx.ts @@ -19,15 +19,6 @@ export interface Options { }; } -interface InternalFrontmatter { - _mdx?: { - /** - * Mirror another MDX file - */ - mirror?: string; - }; -} - export interface MetaFile { path: string; data: { @@ -116,16 +107,6 @@ export default async function loader( frontmatter = result.data as Record; } - const props = (matter.data as InternalFrontmatter)._mdx ?? {}; - if (props.mirror) { - const mirrorPath = path.resolve(path.dirname(filePath), props.mirror); - this.addDependency(mirrorPath); - - matter.content = await fs - .readFile(mirrorPath) - .then((res) => grayMatter(res.toString()).content); - } - let timestamp: number | undefined; if (config.global?.lastModifiedTime === 'git') timestamp = (await getGitTimestamp(filePath))?.getTime(); diff --git a/packages/mdx/src/map/generate.ts b/packages/mdx/src/map/generate.ts index 95dad8fd1..c32514c25 100644 --- a/packages/mdx/src/map/generate.ts +++ b/packages/mdx/src/map/generate.ts @@ -91,14 +91,14 @@ async function getCollectionFiles( absolute: true, }); - result.forEach((item) => { - if (getTypeFromPath(item) !== collection.type) return; + for (const item of result) { + if (getTypeFromPath(item) !== collection.type) continue; files.set(item, { path: path.relative(dir, item), absolutePath: item, }); - }); + } }), ); diff --git a/packages/mdx/src/map/index.ts b/packages/mdx/src/map/index.ts index d90214b4c..b04fc78bb 100644 --- a/packages/mdx/src/map/index.ts +++ b/packages/mdx/src/map/index.ts @@ -1,5 +1,5 @@ -import path from 'node:path'; -import fs from 'node:fs'; +import * as path from 'node:path'; +import * as fs from 'node:fs'; import { readFile, writeFile } from 'node:fs/promises'; import grayMatter from 'gray-matter'; import { getConfigHash, loadConfigCached } from '@/config/cached'; diff --git a/packages/mdx/src/map/manifest.ts b/packages/mdx/src/map/manifest.ts index b3747bd53..7fcdb66a6 100644 --- a/packages/mdx/src/map/manifest.ts +++ b/packages/mdx/src/map/manifest.ts @@ -1,5 +1,5 @@ -import fs from 'node:fs'; -import path from 'node:path'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; import { type LoadedConfig } from '@/config/load'; import type { MetaFile } from '@/loader-mdx'; import { getTypeFromPath } from '@/utils/get-type-from-path'; diff --git a/packages/mdx/src/mdx-plugins/remark-include.ts b/packages/mdx/src/mdx-plugins/remark-include.ts new file mode 100644 index 000000000..c6993954c --- /dev/null +++ b/packages/mdx/src/mdx-plugins/remark-include.ts @@ -0,0 +1,56 @@ +import type { Processor, Transformer } from 'unified'; +import { visit } from 'unist-util-visit'; +import type { Literal, Root } from 'mdast'; +import * as path from 'node:path'; +import * as fs from 'node:fs/promises'; +import matter from 'gray-matter'; + +export function remarkInclude(this: Processor): Transformer { + return async (tree, file) => { + const queue: Promise[] = []; + + visit(tree, ['mdxJsxFlowElement', 'paragraph'] as const, (node) => { + let specifier: string | undefined; + + // without MDX, parse from paragraph nodes + if (node.type === 'paragraph' && node.children.length === 3) { + const [open, content, closure] = node.children; + + if ( + open.type === 'html' && + open.value === '' && + content.type === 'text' && + closure.type === 'html' && + closure.value === '' + ) { + specifier = content.value.trim(); + } + } + + if (node.type === 'mdxJsxFlowElement' && node.name === 'include') { + const child = node.children.at(0) as Literal | undefined; + + if (!child || child.type !== 'text') return; + specifier = child.value; + } + + if (!specifier) return 'skip'; + const targetPath = path.resolve(path.dirname(file.path), specifier); + + queue.push( + fs.readFile(targetPath).then((content) => { + const parsed = this.parse(matter(content).content); + + if (file.data._compiler) { + file.data._compiler.addDependency(targetPath); + } + Object.assign(node, parsed); + }), + ); + + return 'skip'; + }); + + await Promise.all(queue); + }; +} diff --git a/packages/mdx/src/postinstall.ts b/packages/mdx/src/postinstall.ts index 78ff4c216..ffc4e8940 100644 --- a/packages/mdx/src/postinstall.ts +++ b/packages/mdx/src/postinstall.ts @@ -1,5 +1,5 @@ -import path from 'node:path'; -import fs from 'node:fs'; +import * as path from 'node:path'; +import * as fs from 'node:fs'; import { findConfigFile, loadConfig } from '@/config/load'; import { generateTypes } from '@/map/generate'; diff --git a/packages/mdx/src/utils/build-mdx.ts b/packages/mdx/src/utils/build-mdx.ts index 64db440c6..e7368aa25 100644 --- a/packages/mdx/src/utils/build-mdx.ts +++ b/packages/mdx/src/utils/build-mdx.ts @@ -1,12 +1,6 @@ import { createProcessor, type ProcessorOptions } from '@mdx-js/mdx'; import type { VFile } from 'vfile'; -import type { Transformer } from 'unified'; -import { visit } from 'unist-util-visit'; -import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'; -import type { Literal } from 'mdast'; -import { readFileSync } from 'node:fs'; -import * as path from 'node:path'; -import matter from 'gray-matter'; +import { remarkInclude } from '@/mdx-plugins/remark-include'; type Processor = ReturnType; @@ -41,27 +35,13 @@ function cacheKey(group: string, format: string): string { return `${group}:${format}`; } -function remarkInclude(this: Processor, options: CompilerOptions): Transformer { - return (tree, file) => { - visit(tree, 'mdxJsxFlowElement', (node: MdxJsxFlowElement) => { - if (node.name === 'include') { - const child = node.children.at(0) as Literal | undefined; - - if (!child || child.type !== 'text') return; - const specifier = child.value; - - const targetPath = path.resolve(path.dirname(file.path), specifier); - - const content = readFileSync(targetPath); - const parsed = this.parse(matter(content).content); - - options.addDependency(targetPath); - Object.assign(node, parsed); - } - - return 'skip'; - }); - }; +declare module 'vfile' { + interface DataMap { + /** + * The compiler object from loader + */ + _compiler?: CompilerOptions; + } } /** @@ -93,12 +73,7 @@ export function buildMDX( outputFormat: 'program', development: process.env.NODE_ENV === 'development', ...rest, - remarkPlugins: [ - options._compiler - ? ([remarkInclude, options._compiler] as any) - : null, - ...(rest.remarkPlugins ?? []), - ], + remarkPlugins: [remarkInclude, ...(rest.remarkPlugins ?? [])], format, }), @@ -114,6 +89,7 @@ export function buildMDX( data: { ...data, frontmatter, + _compiler: options._compiler, }, }); } diff --git a/packages/openapi/CHANGELOG.md b/packages/openapi/CHANGELOG.md index c894a7575..f5698ed10 100644 --- a/packages/openapi/CHANGELOG.md +++ b/packages/openapi/CHANGELOG.md @@ -1,5 +1,15 @@ # @fuma-docs/openapi +## 5.10.2 + +### Patch Changes + +- Updated dependencies [b71064a] +- Updated dependencies [67124b1] +- Updated dependencies [1810868] + - fumadocs-core@14.6.4 + - fumadocs-ui@14.6.4 + ## 5.10.1 ### Patch Changes diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 2d81c0b61..c25350037 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "fumadocs-openapi", - "version": "5.10.1", + "version": "5.10.2", "description": "Generate MDX docs for your OpenAPI spec", "keywords": [ "NextJs", diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 43f3869b4..c2723ecc4 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,14 @@ # next-docs-ui +## 14.6.4 + +### Patch Changes + +- 67124b1: Improve theme toggle on Notebook layout +- 1810868: Enable `content-visibility` CSS features +- Updated dependencies [b71064a] + - fumadocs-core@14.6.4 + ## 14.6.3 ### Patch Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index 99311bed9..7a5cf8eeb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "fumadocs-ui", - "version": "14.6.3", + "version": "14.6.4", "description": "The framework for building a documentation website in Next.js", "keywords": [ "NextJs", diff --git a/packages/ui/src/layouts/docs.tsx b/packages/ui/src/layouts/docs.tsx index 135b7168f..81f5fc0ff 100644 --- a/packages/ui/src/layouts/docs.tsx +++ b/packages/ui/src/layouts/docs.tsx @@ -19,7 +19,6 @@ import { type IconItemType, BaseLinkItem, } from '@/layouts/links'; -import { getSidebarTabs, type TabOptions } from '@/utils/get-sidebar-tabs'; import { RootToggle } from '@/components/layout/root-toggle'; import { type BaseLayoutProps, getLinks } from './shared'; import { @@ -268,4 +267,5 @@ function SidebarFooterItems({ ); } -export { getSidebarTabs, type TabOptions, type LinkItemType }; +export { getSidebarTabsFromOptions, type TabOptions } from './docs/shared'; +export { type LinkItemType }; diff --git a/packages/ui/src/layouts/docs/shared.tsx b/packages/ui/src/layouts/docs/shared.tsx index d7e94093a..6bacbdba1 100644 --- a/packages/ui/src/layouts/docs/shared.tsx +++ b/packages/ui/src/layouts/docs/shared.tsx @@ -10,7 +10,6 @@ import { import { cn } from '@/utils/cn'; import { buttonVariants } from '@/components/ui/button'; import type { PageTree } from 'fumadocs-core/server'; -import { getSidebarTabs, type TabOptions } from '@/utils/get-sidebar-tabs'; import type { FC, ReactNode } from 'react'; import type { Option } from '@/components/layout/root-toggle'; @@ -18,6 +17,10 @@ export const layoutVariables = { '--fd-layout-offset': 'max(calc(50vw - var(--fd-layout-width) / 2), 0px)', }; +export interface TabOptions { + transform?: (option: Option, node: PageTree.Folder) => Option | null; +} + export interface SidebarOptions extends SidebarProps { enabled: boolean; component: ReactNode; @@ -108,3 +111,63 @@ export function getSidebarTabsFromOptions( return getSidebarTabs(tree); } } + +const defaultTransform: TabOptions['transform'] = (option, node) => { + if (!node.icon) return option; + + return { + ...option, + icon: ( +
+ {node.icon} +
+ ), + }; +}; + +function getSidebarTabs( + pageTree: PageTree.Root, + { transform = defaultTransform }: TabOptions = {}, +): Option[] { + function findOptions(node: PageTree.Folder): Option[] { + const results: Option[] = []; + + if (node.root) { + const index = node.index ?? node.children.at(0); + + if (index?.type === 'page') { + const option: Option = { + url: index.url, + title: node.name, + icon: node.icon, + description: node.description, + + urls: getFolderUrls(node), + }; + + const mapped = transform ? transform(option, node) : option; + if (mapped) results.push(mapped); + } + } + + for (const child of node.children) { + if (child.type === 'folder') results.push(...findOptions(child)); + } + + return results; + } + + return findOptions(pageTree as PageTree.Folder); +} + +function getFolderUrls(folder: PageTree.Folder): string[] { + const results: string[] = []; + if (folder.index) results.push(folder.index.url); + + for (const child of folder.children) { + if (child.type === 'page') results.push(child.url); + if (child.type === 'folder') results.push(...getFolderUrls(child)); + } + + return results; +} diff --git a/packages/ui/src/layouts/notebook.tsx b/packages/ui/src/layouts/notebook.tsx index ad2de4295..d4f7160f5 100644 --- a/packages/ui/src/layouts/notebook.tsx +++ b/packages/ui/src/layouts/notebook.tsx @@ -125,7 +125,12 @@ export function DocsLayout({
- {sidebarFooter} + + {!props.disableThemeSwitch ? ( + + ) : null} + {sidebarFooter} + ->(({ className, ...props }, ref) => ( -
+>((props, ref) => ( +
)); DocsBody.displayName = 'DocsBody'; diff --git a/packages/ui/src/theme/colors.ts b/packages/ui/src/theme/colors.ts index 2fa1cc219..68f1f4c0f 100644 --- a/packages/ui/src/theme/colors.ts +++ b/packages/ui/src/theme/colors.ts @@ -91,7 +91,7 @@ const neutral: Preset = { foreground: '0 0% 3.9%', muted: '0 0% 96.1%', 'muted-foreground': '0 0% 45.1%', - popover: '0 0% 100%', + popover: '0 0% 98%', 'popover-foreground': '0 0% 15.1%', card: '0 0% 94.7%', 'card-foreground': '0 0% 3.9%', @@ -123,11 +123,6 @@ const neutral: Preset = { ring: '0 0% 14.9%', }, css: { - '#nd-sidebar': { - '--muted': '0deg 0% 89%', - '--secondary': '0deg 0% 99%', - '--muted-foreground': '0 0% 30%', - }, '.dark #nd-sidebar': { '--muted': '0deg 0% 16%', '--secondary': '0deg 0% 18%', diff --git a/packages/ui/src/utils/get-sidebar-tabs.tsx b/packages/ui/src/utils/get-sidebar-tabs.tsx deleted file mode 100644 index 46004d409..000000000 --- a/packages/ui/src/utils/get-sidebar-tabs.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { PageTree } from 'fumadocs-core/server'; -import type { Option } from '@/components/layout/root-toggle'; - -export interface TabOptions { - transform?: (option: Option, node: PageTree.Folder) => Option | null; -} - -const defaultTransform: TabOptions['transform'] = (option, node) => { - if (!node.icon) return option; - - return { - ...option, - icon: ( -
- {node.icon} -
- ), - }; -}; - -export function getSidebarTabs( - pageTree: PageTree.Root, - { transform = defaultTransform }: TabOptions = {}, -): Option[] { - function findOptions(node: PageTree.Folder): Option[] { - const results: Option[] = []; - - if (node.root) { - const index = node.index ?? node.children.at(0); - - if (index?.type === 'page') { - const option: Option = { - url: index.url, - title: node.name, - icon: node.icon, - description: node.description, - - urls: getFolderUrls(node), - }; - - const mapped = transform ? transform(option, node) : option; - if (mapped) results.push(mapped); - } - } - - for (const child of node.children) { - if (child.type === 'folder') results.push(...findOptions(child)); - } - - return results; - } - - return findOptions(pageTree as PageTree.Folder); -} - -function getFolderUrls(folder: PageTree.Folder): string[] { - const results: string[] = []; - if (folder.index) results.push(folder.index.url); - - for (const child of folder.children) { - if (child.type === 'page') results.push(child.url); - if (child.type === 'folder') results.push(...getFolderUrls(child)); - } - - return results; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4ce1e3de..5519e256d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -622,6 +622,9 @@ importers: unified: specifier: ^11.0.5 version: 11.0.5 + vfile: + specifier: ^6.0.3 + version: 6.0.3 packages/create-app: dependencies: