* MDX: support Vite 8 `moduleType` option

* UI: fix TOC ease function

* docs: add Takumi integration (#2820)

* docs: add Takumi integration

* docs: mention pre-bundled Geist font for Takumi

* docs: fix grammar

* add React Router example

* Docs: fix

---------

Co-authored-by: Fuma Nama <[email protected]>

* UI: forward sidebar components

* UI: fix callout icons

* Version Packages (#2819)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Chore: Switch to tsdown for bundling

* UI: improve TOC animation

* fix build

* TypeScript: Improve error message

* Chore: migrate to oxfmt

* Core: Support `findSiblings()` page tree utility

* Docs: separate UI as a new tab

* Docs: better layout guide

* fix llms.mdx

* fix tsdown incorrect CSS bundling

* Core: Support context-aware type-safe `slugs` function in `loader()`

* UI: improve details

* Docs: refactor navigation guides

* Docs: introduce type-safe slugs option

* Obsidian: fix slugification

* OpenAPI: Fix Parameter Serialization

* Core: Support `<auto-files />` syntax in `remark-mdx-files` plugin

* Python: fix bundle size

* Version Packages (#2826)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: Kane Wang <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Fuma Nama
2025-12-31 19:33:09 +08:00
committed by GitHub
co-authored by Kane Wang github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
697 changed files with 5900 additions and 10061 deletions
+4 -4
View File
@@ -6,12 +6,12 @@ Before submitting a pull request, there are some guidelines you should notice.
### Guidelines
This project is a monorepo using Turborepo, pnpm and
[Changesets](https://github.com/changesets/changesets).
[Changesets](https://github.com/changesets/changesets).
#### Before Submitting
- Check if there's other similar PRs.
- Format your code with `pnpm run prettier`.
- Format your code with `pnpm run format`.
- Add changesets with `pnpm changeset`, which documents the changes you've made.
- Run unit tests with `pnpm test` and update snapshots if necessary.
@@ -27,7 +27,7 @@ OR open a bug report and link it in your PR.
#### Docs
Contributing to the docs is relatively easier, make sure to check the typos and grammatical mistakes before submitting.
Contributing to the docs is relatively easier, make sure to check the typos and grammatical mistakes before submitting.
### New to contributing?
@@ -37,4 +37,4 @@ it is located in `/apps/docs/content/docs`.
To run the docs site in dev mode,
build the dependencies with `pnpm run build --filter=./packages/*` and run `pnpm run dev --filter=docs` to start the dev server.
You don't need any extra environment variables to run this project.
You don't need any extra environment variables to run this project.
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
run: pnpm i --frozen-lockfile
- name: Lint Format
run: pnpm lint:prettier
run: pnpm lint:format
- name: Check Types
run: pnpm build --filter=./packages/* && pnpm i --frozen-lockfile && pnpm types:check
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"embeddedLanguageFormatting": "auto",
"ignorePatterns": [
"CHANGELOG.md",
"pnpm-lock.yaml",
".github/**/*.md",
"packages/create-app/versions.json",
"**/test/out/**/*",
"**/test/fixtures/**/*.output.*",
"**/test/fixtures/**/*.out.*",
"out",
".content-collections",
"examples/obsidian/content/docs/vault/**/*",
"examples/obsidian/obsidian/Obsidian Vault",
".content-collections/**/*",
"packages/mdx-remote/test/fixtures/**/*.js",
"packages/mdx-remote/test/fixtures/**/*.json",
"packages/core/test/fixtures/page-trees/**/*.json",
"routeTree.gen.ts",
],
}
-29
View File
@@ -1,29 +0,0 @@
CHANGELOG.md
pnpm-lock.yaml
.github/**/*.md
packages/create-app/versions.json
**/test/out/**/*
**/test/fixtures/**/*.output.*
**/test/fixtures/**/*.out.*
out
.content-collections
apps/docs/content/docs/ui/museum/*.mdx
!apps/docs/content/docs/ui/museum/index.mdx
examples/openapi/content/docs/(api)/**/*.mdx
!examples/openapi/content/docs/(api)/index.mdx
examples/obsidian/content/docs/vault/**/*
examples/obsidian/obsidian/Obsidian Vault
.content-collections/**/*
packages/mdx-remote/test/fixtures/**/*.js
packages/mdx-remote/test/fixtures/**/*.json
packages/core/test/fixtures/page-trees/**/*.json
routeTree.gen.ts
+4 -1
View File
@@ -11,5 +11,8 @@
"files.associations": {
"**/packages/create-app/template/**/*": "plaintext"
},
"editor.defaultFormatter": "esbenp.prettier-vscode"
"prettier.enable": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "oxc.oxc-vscode",
"oxc.linter.enabled": false
}
@@ -10,11 +10,7 @@ export function ShareButton({ url }: { url: string }) {
});
return (
<button
type="button"
className={cn(buttonVariants({ className: 'gap-2' }))}
onClick={onCopy}
>
<button type="button" className={cn(buttonVariants({ className: 'gap-2' }))} onClick={onCopy}>
{isChecked ? <Check className="size-4" /> : <Share className="size-4" />}
{isChecked ? 'Copied URL' : 'Share Post'}
</button>
+3 -7
View File
@@ -28,8 +28,7 @@ export default async function Page(props: PageProps<'/blog/[slug]'>) {
<p className="mb-1 text-sm text-fd-muted-foreground">At</p>
<p className="font-medium">
{new Date(
page.data.date ??
path.basename(page.path, path.extname(page.path)),
page.data.date ?? path.basename(page.path, path.extname(page.path)),
).toDateString()}
</p>
</div>
@@ -61,9 +60,7 @@ export default async function Page(props: PageProps<'/blog/[slug]'>) {
);
}
export async function generateMetadata(
props: PageProps<'/blog/[slug]'>,
): Promise<Metadata> {
export async function generateMetadata(props: PageProps<'/blog/[slug]'>): Promise<Metadata> {
const params = await props.params;
const page = blog.getPage([params.slug]);
@@ -71,8 +68,7 @@ export async function generateMetadata(
return createMetadata({
title: page.data.title,
description:
page.data.description ?? 'The library for building documentation sites',
description: page.data.description ?? 'The library for building documentation sites',
});
}
+1 -3
View File
@@ -39,9 +39,7 @@ export default function Page() {
className="flex flex-col bg-fd-card rounded-2xl border shadow-sm p-4 transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
>
<p className="font-medium">{post.data.title}</p>
<p className="text-sm text-fd-muted-foreground">
{post.data.description}
</p>
<p className="text-sm text-fd-muted-foreground">{post.data.description}</p>
<p className="mt-auto pt-4 text-xs text-brand">
{new Date(post.data.date ?? getName(post.path)).toDateString()}
+1 -7
View File
@@ -15,13 +15,7 @@ Apply typography styles with:
```jsx
export default function withArticle(props) {
return (
<main
{...props}
className={cn(
'w-full max-w-[1400px] mx-auto px-4 py-12',
props.className,
)}
>
<main {...props} className={cn('w-full max-w-[1400px] mx-auto px-4 py-12', props.className)}>
<article className="prose">{props.children}</article>
</main>
);
-69
View File
@@ -1,69 +0,0 @@
import { Building2, Library, Pencil } from 'lucide-react';
import Link, { type LinkProps } from 'next/link';
export default function DocsPage() {
return (
<main className="w-full max-w-page mx-auto flex flex-col flex-1 justify-center items-center px-4 py-16 text-center z-2">
<h1 className="mb-4 text-3xl font-semibold md:text-4xl">
Getting Started
</h1>
<p className="text-fd-muted-foreground">
Portal to different sections of docs.
</p>
<div className="mt-8 grid grid-cols-1 gap-4 text-start md:grid-cols-2">
{[
{
name: 'Fumadocs',
description:
'The full-powered documentation framework with an excellent UI.',
icon: <Building2 className="size-full" />,
href: '/docs/ui',
},
{
name: 'Fumadocs Core',
description: 'The core library of Fumadocs.',
icon: <Library className="size-full" />,
href: '/docs/headless',
},
{
name: 'Fumadocs MDX',
description:
'The library for handling MDX in your React.js framework.',
icon: <Pencil className="size-full" />,
href: '/docs/mdx',
},
{
name: 'Fumadocs CLI',
description: 'The CLI tool for Fumadocs.',
icon: <Pencil className="size-full" />,
href: '/docs/cli',
},
].map((item) => (
<Item key={item.name} href={item.href}>
<Icon>{item.icon}</Icon>
<h2 className="mb-2 font-medium">{item.name}</h2>
<p className="text-sm text-fd-muted-foreground">
{item.description}
</p>
</Item>
))}
</div>
</main>
);
}
function Icon({ children }: { children: React.ReactNode }) {
return (
<div className="mb-2 size-8 rounded-lg border p-1 text-fd-muted-foreground bg-fd-muted shadow-md">
{children}
</div>
);
}
function Item(props: LinkProps & { children: React.ReactNode }) {
return (
<Link {...props} className="bg-fd-card rounded-2xl border p-4 shadow-lg">
{props.children}
</Link>
);
}
+4 -20
View File
@@ -36,13 +36,7 @@ export function NextSVG(props: SVGProps<SVGSVGElement>): React.ReactElement {
d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z"
fill="url(#:R0:paint0_linear_408_134)"
/>
<rect
fill="url(#:R0:paint1_linear_408_134)"
height="72"
width="12"
x="115"
y="54"
/>
<rect fill="url(#:R0:paint1_linear_408_134)" height="72" width="12" x="115" y="54" />
</g>
<defs>
<linearGradient
@@ -90,17 +84,9 @@ export function VercelLogo(props: SVGProps<SVGSVGElement>): React.ReactElement {
);
}
export function NetlifyLogo(
props: SVGProps<SVGSVGElement>,
): React.ReactElement {
export function NetlifyLogo(props: SVGProps<SVGSVGElement>): React.ReactElement {
return (
<svg
aria-label="Netlify"
width="512"
height="209"
viewBox="0 0 512 209"
{...props}
>
<svg aria-label="Netlify" width="512" height="209" viewBox="0 0 512 209" {...props}>
<g>
<path
d="M117.436 207.036V154.604L118.529 153.51H129.452L130.545 154.604V207.036L129.452 208.13H118.529L117.436 207.036Z"
@@ -159,9 +145,7 @@ export function NetlifyLogo(
);
}
export function OpenAPIIcon(
props: SVGProps<SVGSVGElement>,
): React.ReactElement {
export function OpenAPIIcon(props: SVGProps<SVGSVGElement>): React.ReactElement {
return (
<svg fill="currentColor" viewBox="0 0 512 512" {...props}>
<path d="m.2041784 294.3757629 135.0729218-.0019836c1.0510101 10.3673706 3.8841248 20.5690308 8.4522095 30.170166l-115.790062 69.7536621c-15.1902405-26.1945495-24.7731477-59.514801-27.7350693-99.9218445zm131.8023066 196.8029786 51.4067841-124.8407288c-5.2182617-2.7826233-10.2264862-6.0957642-14.9382172-9.9553528l-95.4706802 95.4720459c19.4898223 17.5570679 39.5628662 30.7094116 59.0021133 39.3240357zm-65.6047211-45.6471253 95.2843628-95.2860413c-4.9330902-4.9628906-9.4824219-10.8501282-13.6674042-17.6151123l-115.5066642 69.5827027c10.4910965 17.068634 22.1482162 31.8469848 33.8897056 43.3184509zm313.9973145 6.3275757-95.5295105-95.5253601c-1.0485229.8474121-2.1081848 1.671936-3.184845 2.4666138l69.7098999 115.7191772c9.7306824-6.0542297 19.3898926-13.8226013 29.0044556-22.6604309zm-36.7403565 27.5314331-69.5648804-115.4782104c-25.9638519 14.4993591-53.3182526 17.5528564-82.4063263 6.3365173l-51.3009338 124.5834045c70.200531 28.4663086 142.9616852 20.3123169 203.2721405-15.4417114zm-171.9461059-267.7199554-69.7079392-115.7203217c-10.1640396 6.5237503-19.8186493 14.1064529-29.0061646 22.6601715l95.5275803 95.5278625c1.0482636-.8363342 2.1095887-1.6597595 3.1865235-2.4677123zm-171.712616 73.5650482 134.7621918-.0019531c.291153-23.8374939 8.7095032-45.6818237 26.9275513-65.005722l-95.2863083-95.2865982c-43.9871293 46.6117019-65.8919295 100.1147994-66.4034348 160.2942733zm226.6937714-91.9374695.0058594-134.7585754c-44.7272644.2738724-84.0045013 12.32621-116.959053 32.5391159l69.5626297 115.4798889c13.0764617-7.6767197 28.6641692-12.980278 47.3905639-13.2604294zm186.4042816-37.1177979-99.4490967 99.4510498c2.2626953 6.6661987 3.7337646 13.5315552 4.4528503 20.4605103h135.0907898c-.8839111-40.9807434-14.4047241-80.9386597-40.0945434-119.9115601zm40.2803039 129.0516358h-134.747406c-.4680481 25.240448-9.8990479 48.2441101-26.923645 65.0134888l95.2901917 95.2860413c43.8191833-43.1222535 65.1218261-96.9472352 66.3808593-160.2995301zm-217.5386657-226.4921341-.0058594 135.0865784c7.082489.8026276 13.8835602 2.3424988 20.4658203 4.4556732l99.4153442-99.4150772c-35.131958-24.5753861-76.5397338-38.3618508-119.8753051-40.1271744zm195.1100769-56.1426528c-39.1646423 11.456749-55.5329285 55.1829491-38.7815857 88.6712189l-137.3599701 137.3618682c-32.4068756-16.1573944-74.9024811-1.5699463-87.7949829 36.0340729-15.7162628 45.8401489 24.2427673 91.8019104 71.7535858 82.5325317 42.0110779-8.1963196 62.3093567-54.1882019 44.4657593-90.0109253l137.4894714-137.4859314c34.6590576 17.2256775 79.5329285-1.0651627 89.3959961-41.653656 11.4301758-47.0377617-32.6579895-89.0548125-79.1682739-75.449179z" />
+9 -20
View File
@@ -23,7 +23,7 @@ export default function Layout({ children }: LayoutProps<'/'>) {
items: [
{
text: 'Getting Started',
url: '/docs/ui',
url: '/docs',
icon: <Book />,
},
{
@@ -39,18 +39,17 @@ export default function Layout({ children }: LayoutProps<'/'>) {
children: (
<NavbarMenu>
<NavbarMenuTrigger>
<Link href="/docs/ui">Documentation</Link>
<Link href="/docs">Documentation</Link>
</NavbarMenuTrigger>
<NavbarMenuContent>
<NavbarMenuLink href="/docs/ui" className="md:row-span-2">
<NavbarMenuLink href="/docs" className="md:row-span-2">
<div className="-mx-3 -mt-3">
<Image
src={Preview}
alt="Perview"
className="rounded-t-lg object-cover"
style={{
maskImage:
'linear-gradient(to bottom,white 60%,transparent)',
maskImage: 'linear-gradient(to bottom,white 60%,transparent)',
}}
/>
</div>
@@ -60,10 +59,7 @@ export default function Layout({ children }: LayoutProps<'/'>) {
</p>
</NavbarMenuLink>
<NavbarMenuLink
href="/docs/ui/components"
className="lg:col-start-2"
>
<NavbarMenuLink href="/docs/ui/components" className="lg:col-start-2">
<ComponentIcon className="bg-fd-primary text-fd-primary-foreground p-1 mb-2 rounded-md" />
<p className="font-medium">Components</p>
<p className="text-fd-muted-foreground text-sm">
@@ -71,22 +67,15 @@ export default function Layout({ children }: LayoutProps<'/'>) {
</p>
</NavbarMenuLink>
<NavbarMenuLink
href="/docs/ui/openapi"
className="lg:col-start-2"
>
<NavbarMenuLink href="/docs/openapi" className="lg:col-start-2">
<Server className="bg-fd-primary text-fd-primary-foreground p-1 mb-2 rounded-md" />
<p className="font-medium">OpenAPI</p>
<p className="text-fd-muted-foreground text-sm">
Generate interactive playgrounds and docs for your OpenAPI
schema.
Generate interactive playgrounds and docs for your OpenAPI schema.
</p>
</NavbarMenuLink>
<NavbarMenuLink
href="/docs/ui/markdown"
className="lg:col-start-3 lg:row-start-1"
>
<NavbarMenuLink href="/docs/markdown" className="lg:col-start-3 lg:row-start-1">
<Pencil className="bg-fd-primary text-fd-primary-foreground p-1 mb-2 rounded-md" />
<p className="font-medium">Markdown</p>
<p className="text-fd-muted-foreground text-sm">
@@ -95,7 +84,7 @@ export default function Layout({ children }: LayoutProps<'/'>) {
</NavbarMenuLink>
<NavbarMenuLink
href="/docs/ui/manual-installation"
href="/docs/manual-installation"
className="lg:col-start-3 lg:row-start-2"
>
<PlusIcon className="bg-fd-primary text-fd-primary-foreground p-1 mb-2 rounded-md" />
+1 -3
View File
@@ -57,9 +57,7 @@ export function Marquee({
key={i}
className={cn(
'flex shrink-0 justify-around [gap:var(--gap)]',
vertical
? 'animate-marquee-vertical flex-col'
: 'animate-marquee flex-row',
vertical ? 'animate-marquee-vertical flex-col' : 'animate-marquee flex-row',
pauseOnHover && 'group-hover:[animation-play-state:paused]',
reverse && '[animation-direction:reverse]',
)}
+14 -28
View File
@@ -127,9 +127,7 @@ export function CreateAppAnimation() {
lines.push(
<span key="command_type">
{installCmd.substring(0, tick)}
{tick < timeCommandEnter && (
<div className="inline-block h-3 w-1 animate-pulse bg-white" />
)}
{tick < timeCommandEnter && <div className="inline-block h-3 w-1 animate-pulse bg-white" />}
</span>,
);
@@ -176,8 +174,7 @@ export function CreateAppAnimation() {
)}
<pre className="overflow-hidden rounded-xl border text-sm shadow-lg bg-fd-card">
<div className="flex flex-row items-center gap-2 border-b px-4 py-2">
<TerminalIcon className="size-4" />{' '}
<span className="font-bold">Terminal</span>
<TerminalIcon className="size-4" /> <span className="font-bold">Terminal</span>
<div className="grow" />
<div className="size-2 rounded-full bg-red-400" />
</div>
@@ -206,17 +203,14 @@ function LaunchAppWindow(props: HTMLAttributes<HTMLDivElement>) {
);
}
const previewButtonVariants = cva(
'w-20 h-8 text-sm font-medium transition-colors rounded-full',
{
variants: {
active: {
true: 'text-fd-primary-foreground',
false: 'text-fd-muted-foreground',
},
const previewButtonVariants = cva('w-20 h-8 text-sm font-medium transition-colors rounded-full', {
variants: {
active: {
true: 'text-fd-primary-foreground',
false: 'text-fd-muted-foreground',
},
},
);
});
export function PreviewImages(props: ComponentProps<'div'>) {
const [active, setActive] = useState(0);
const previews = [
@@ -261,9 +255,7 @@ export function PreviewImages(props: ComponentProps<'div'>) {
alt="preview"
className={cn(
'col-start-1 row-start-1 select-none',
active === i
? 'animate-in fade-in slide-in-from-bottom-12 duration-800'
: 'invisible',
active === i ? 'animate-in fade-in slide-in-from-bottom-12 duration-800' : 'invisible',
)}
/>
))}
@@ -291,8 +283,7 @@ export function Writing({
}: {
tabs: Record<(typeof WritingTabs)[number]['value'], ReactNode>;
}) {
const [tab, setTab] =
useState<(typeof WritingTabs)[number]['value']>('writer');
const [tab, setTab] = useState<(typeof WritingTabs)[number]['value']>('writer');
return (
<div className="col-span-full my-20">
@@ -300,8 +291,8 @@ export function Writing({
Anybody can write.
</h2>
<p className="text-center mb-8 mx-auto w-full max-w-[800px]">
Native support for Markdown & MDX, offering intuitive, convenient and
extensive syntax for non-dev writers, developers, and AI agents.
Native support for Markdown & MDX, offering intuitive, convenient and extensive syntax for
non-dev writers, developers, and AI agents.
</p>
<div className="flex justify-center items-center gap-4 text-fd-muted-foreground mb-6">
{WritingTabs.map((item) => (
@@ -355,9 +346,7 @@ export function AgnosticBackground() {
);
}
export function ContentAdoptionBackground(
props: ComponentProps<typeof GrainGradient>,
) {
export function ContentAdoptionBackground(props: ComponentProps<typeof GrainGradient>) {
const { resolvedTheme } = useTheme();
return (
@@ -376,10 +365,7 @@ export function ContentAdoptionBackground(
}
let observer: IntersectionObserver;
const observerTargets = new WeakMap<
Element,
(entry: IntersectionObserverEntry) => void
>();
const observerTargets = new WeakMap<Element, (entry: IntersectionObserverEntry) => void>();
function useIsVisible(ref: RefObject<HTMLElement | null>) {
const [visible, setVisible] = useState(false);
File diff suppressed because one or more lines are too long
+5 -15
View File
@@ -302,12 +302,9 @@ export default function Showcase() {
return (
<main className="px-4 py-12 z-2 w-full max-w-[1400px] mx-auto **:border-neutral-400 dark:**:border-neutral-700">
<div className="relative overflow-hidden border border-dashed p-6">
<h1 className="mb-4 text-xl font-medium">
The docs framework designed with care.
</h1>
<h1 className="mb-4 text-xl font-medium">The docs framework designed with care.</h1>
<p className="text-fd-muted-foreground">
A list of beautiful open-source projects with their docs powered by
Fumadocs.
A list of beautiful open-source projects with their docs powered by Fumadocs.
</p>
<div className="mt-6">
<a
@@ -343,10 +340,7 @@ export default function Showcase() {
viewBox="0 0 74 64"
className="size-6 mt-1"
>
<path
d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z"
fill="currentColor"
/>
<path d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z" fill="currentColor" />
</svg>
<div>
<h2 className="text-sm font-medium mb-2">
@@ -389,9 +383,7 @@ export default function Showcase() {
</Link>
</div>
</div>
<h2 className="text-xl font-medium mt-12 px-4">
Fumadocs can power your blog, too.
</h2>
<h2 className="text-xl font-medium mt-12 px-4">Fumadocs can power your blog, too.</h2>
<div className="mt-6 grid gap-2.5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{blogs.map((showcase) => (
<ShowcaseItem key={showcase.url} {...showcase} />
@@ -431,9 +423,7 @@ function ShowcaseItem({ name, url, image }: ShowcaseObject) {
rel="noreferrer noopener"
className="flex aspect-[1.91/1] flex-col border border-dashed p-4 transition-all hover:bg-fd-accent"
>
<p className="font-mono text-xs mb-2 text-fd-muted-foreground">
{new URL(url).hostname}
</p>
<p className="font-mono text-xs mb-2 text-fd-muted-foreground">{new URL(url).hostname}</p>
<p className="text-xl font-medium">{name}</p>
</a>
);
+1 -5
View File
@@ -47,11 +47,7 @@ export const organizationAsUserSponsors = [
asUser: 'marclave',
logo: (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 593 593"
className="size-9.5"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 593 593" className="size-9.5">
<path
fill="currentColor"
fillRule="evenodd"
+9 -43
View File
@@ -30,9 +30,7 @@ export default async function Page() {
const result = await getSponsors(owner);
const sponsors = result.map((v) => {
const entity = organizationAsUserSponsors.find(
(entity) => entity.asUser === v.login,
);
const entity = organizationAsUserSponsors.find((entity) => entity.asUser === v.login);
if (entity) {
return {
login: entity.github,
@@ -62,8 +60,7 @@ export default async function Page() {
/>
<h1 className="text-4xl font-semibold">Support Fumadocs</h1>
<p className="mt-4 text-sm">
Your sponsorship means a lot to open-source projects, including
Fumadocs.
Your sponsorship means a lot to open-source projects, including Fumadocs.
</p>
<a
href="https://github.com/sponsors/fuma-nama"
@@ -77,19 +74,9 @@ export default async function Page() {
>
Sponsor
</a>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 600 200"
className="max-w-[600px]"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 200" className="max-w-[600px]">
<defs>
<linearGradient
id="circuit-1-fill"
x1="0"
y1="0"
x2="0.939104"
y2="0.786487"
>
<linearGradient id="circuit-1-fill" x1="0" y1="0" x2="0.939104" y2="0.786487">
<stop offset="0%" stopColor="rgb(50,50,60)" />
<stop offset="100%" stopColor="rgb(30,30,38)" />
</linearGradient>
@@ -158,13 +145,7 @@ export default async function Page() {
animationDelay: `${(Math.pow(2, i) * 360).toString()}ms`,
}}
/>
<rect
x="360"
y={120 + i * 20}
width="8"
height="5"
fill="rgb(100,100,120)"
/>
<rect x="360" y={120 + i * 20} width="8" height="5" fill="rgb(100,100,120)" />
</Fragment>
))}
@@ -177,13 +158,7 @@ export default async function Page() {
filter: 'drop-shadow(2px 0px 8px rgb(255, 100, 255))',
}}
/>
<text
dx="256"
dy="147.793041"
fontSize="18"
fontWeight="400"
fill="rgba(70,70,86,0.9)"
>
<text dx="256" dy="147.793041" fontSize="18" fontWeight="400" fill="rgba(70,70,86,0.9)">
Fumadocs
</text>
</svg>
@@ -192,9 +167,7 @@ export default async function Page() {
{sponsors
.filter((sponsor) => sponsor.__typename === 'Organization')
.map((sponsor) => {
const tier = tiers.find(
(tier) => sponsor.tier.monthlyPriceInDollars >= tier.min,
);
const tier = tiers.find((tier) => sponsor.tier.monthlyPriceInDollars >= tier.min);
return (
<a
@@ -219,21 +192,14 @@ export default async function Page() {
</>
)}
</div>
{tier && (
<p className={cn('text-xs', tier.color)}>{tier.name}</p>
)}
{tier && <p className={cn('text-xs', tier.color)}>{tier.name}</p>}
</a>
);
})}
</div>
<h2 className="mt-12 font-mono text-xs mb-7">Hosting Sponsor</h2>
<a href="https://vercel.com" rel="noreferrer noopener">
<svg
aria-label="Vercel logotype"
role="img"
viewBox="0 0 283 64"
className="w-32"
>
<svg aria-label="Vercel logotype" role="img" viewBox="0 0 283 64" className="w-32">
<path
d="M141.68 16.25c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zm117.14-14.5c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zm-39.03 3.5c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9v-46h9zM37.59.25l36.95 64H.64l36.95-64zm92.38 5l-27.71 48-27.71-48h10.39l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10v14.8h-9v-34h9v9.2c0-5.08 5.91-9.2 13.2-9.2z"
fill="currentColor"
+4 -13
View File
@@ -7,26 +7,17 @@ import Logo from '@/public/logo.png';
export function UwuHero() {
return (
<div className="z-2 hidden flex-col items-center pb-8 text-center bg-fd-background border-x border-t [.uwu_&]:flex">
<Image
alt="logo"
src={Logo}
className="mb-6 w-full max-w-[400px] px-4"
priority
/>
<Image alt="logo" src={Logo} className="mb-6 w-full max-w-[400px] px-4" priority />
<p className="mb-6 h-fit p-2 text-lg text-fd-muted-foreground md:max-w-[80%] md:text-xl">
Fumadocs is the framework for building documentation with{' '}
<b className="font-medium text-fd-foreground">
anime and fuwa fuwa power
</b>
. Using the power of weebs and waifus.
<b className="font-medium text-fd-foreground">anime and fuwa fuwa power</b>. Using the power
of weebs and waifus.
</p>
<div className="inline-flex items-center gap-3">
<Link
href="/docs"
className={cn(
buttonVariants({ size: 'lg', className: 'rounded-full' }),
)}
className={cn(buttonVariants({ size: 'lg', className: 'rounded-full' }))}
>
Getting Started
</Link>
@@ -10,26 +10,18 @@ import { Wrapper } from '@/components/preview/wrapper';
import { Mermaid } from '@/components/mdx/mermaid';
import { Feedback } from '@/components/feedback';
import { onRateAction, owner, repo } from '@/lib/github';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import Link from 'fumadocs-core/link';
import { getPageTreePeers } from 'fumadocs-core/page-tree';
import { findSiblings } from 'fumadocs-core/page-tree';
import { Card, Cards } from 'fumadocs-ui/components/card';
import { getMDXComponents } from '@/mdx-components';
import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions';
import { Banner } from 'fumadocs-ui/components/banner';
import { Installation } from '@/components/preview/installation';
import { Customisation } from '@/components/preview/customisation';
import {
DocsBody,
DocsPage,
PageLastUpdate,
} from 'fumadocs-ui/layouts/docs/page';
import { DocsBody, DocsPage, PageLastUpdate } from 'fumadocs-ui/layouts/docs/page';
import { NotFound } from '@/components/not-found';
import { getSuggestions } from '@/app/docs/[...slug]/suggestions';
import { getSuggestions } from './suggestions';
import { PathUtils } from 'fumadocs-core/source';
function PreviewRenderer({ preview }: { preview: string }): ReactNode {
@@ -43,13 +35,15 @@ function PreviewRenderer({ preview }: { preview: string }): ReactNode {
export const revalidate = false;
export default async function Page(props: PageProps<'/docs/[...slug]'>) {
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page)
return (
<NotFound getSuggestions={() => getSuggestions(params.slug.join(' '))} />
<NotFound
getSuggestions={async () => (params.slug ? getSuggestions(params.slug.join(' ')) : [])}
/>
);
if (page.data.type === 'openapi') {
@@ -75,9 +69,7 @@ export default async function Page(props: PageProps<'/docs/[...slug]'>) {
}}
>
<h1 className="text-[1.75em] font-semibold">{page.data.title}</h1>
<p className="text-lg text-fd-muted-foreground mb-2">
{page.data.description}
</p>
<p className="text-lg text-fd-muted-foreground mb-2">{page.data.description}</p>
<div className="flex flex-row flex-wrap gap-2 items-center border-b pb-6">
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
<ViewOptions
@@ -100,20 +92,14 @@ export default async function Page(props: PageProps<'/docs/[...slug]'>) {
return (
<HoverCard>
<HoverCardTrigger
href={
found.hash
? `${found.page.url}#${found.hash}`
: found.page.url
}
href={found.hash ? `${found.page.url}#${found.hash}` : found.page.url}
{...props}
>
{props.children}
</HoverCardTrigger>
<HoverCardContent className="text-sm">
<p className="font-medium">{found.page.data.title}</p>
<p className="text-fd-muted-foreground">
{found.page.data.description}
</p>
<p className="text-fd-muted-foreground">{found.page.data.description}</p>
</HoverCardContent>
</HoverCard>
);
@@ -141,18 +127,24 @@ export default async function Page(props: PageProps<'/docs/[...slug]'>) {
function DocsCategory({ url }: { url: string }) {
return (
<Cards>
{getPageTreePeers(source.pageTree, url).map((peer) => (
<Card key={peer.url} title={peer.name} href={peer.url}>
{peer.description}
</Card>
))}
{findSiblings(source.getPageTree(), url).map((item) => {
if (item.type === 'separator') return;
if (item.type === 'folder') {
if (!item.index) return;
item = item.index;
}
return (
<Card key={item.url} title={item.name} href={item.url}>
{item.description}
</Card>
);
})}
</Cards>
);
}
export async function generateMetadata(
props: PageProps<'/docs/[...slug]'>,
): Promise<Metadata> {
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
const { slug = [] } = await props.params;
const page = source.getPage(slug);
if (!page)
@@ -160,8 +152,7 @@ export async function generateMetadata(
title: 'Not Found',
});
const description =
page.data.description ?? 'The library for building documentation sites';
const description = page.data.description ?? 'The library for building documentation sites';
const image = {
url: getPageImage(page).url,
+5 -11
View File
@@ -1,12 +1,9 @@
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions, linkItems, logo } from '@/lib/layout.shared';
import { source } from '@/lib/source';
import {
AISearch,
AISearchPanel,
AISearchTrigger,
} from '@/components/ai/search';
import { AISearch, AISearchPanel, AISearchTrigger } from '@/components/ai/search';
import 'katex/dist/katex.min.css';
import { getSection } from '@/lib/source/navigation';
export default function Layout({ children }: LayoutProps<'/docs'>) {
const base = baseOptions();
@@ -14,7 +11,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
return (
<DocsLayout
{...base}
tree={source.pageTree}
tree={source.getPageTree()}
// just icon items
links={linkItems.filter((item) => item.type === 'icon')}
nav={{
@@ -22,9 +19,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
title: (
<>
{logo}
<span className="font-medium in-[.uwu]:hidden max-md:hidden">
Fumadocs
</span>
<span className="font-medium in-[.uwu]:hidden max-md:hidden">Fumadocs</span>
</>
),
}}
@@ -33,8 +28,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
transform(option, node) {
const meta = source.getNodeMeta(node);
if (!meta || !node.icon) return option;
const color = `var(--${meta.path.split('/')[0]}-color, var(--color-fd-foreground))`;
const color = `var(--${getSection(meta.path)}-color, var(--color-fd-foreground))`;
return {
...option,
+9 -6
View File
@@ -64,10 +64,7 @@
--animate-stroke: stroke 5s linear infinite;
--color-gradient-radial: radial-gradient(circle, var(--tw-gradient-stops));
--color-repeat-gradient-to-r: repeating-linear-gradient(
to right,
var(--tw-gradient-stops)
);
--color-repeat-gradient-to-r: repeating-linear-gradient(to right, var(--tw-gradient-stops));
--color-repeat-gradient-to-br: repeating-linear-gradient(
to bottom right,
var(--tw-gradient-stops)
@@ -82,9 +79,14 @@
--color-fd-primary: var(--headless-color);
}
.framework {
--color-fd-primary: var(--framework-color);
}
:root {
--headless-color: hsl(250, 80%, 54%);
--ui-color: hsl(41, 100%, 40%);
--framework-color: hsl(41, 100%, 40%);
--ui-color: hsl(217, 100%, 58%);
}
html {
@@ -101,7 +103,8 @@ body {
.dark {
--headless-color: hsl(250 100% 80%);
--ui-color: #fff383;
--framework-color: #fff383;
--ui-color: #a9ceff;
}
@keyframes circuit_1 {
+6 -16
View File
@@ -3,24 +3,17 @@
import { useParams } from 'next/navigation';
import { type ReactNode, useId } from 'react';
import { cn } from '@/lib/cn';
import { getSection } from '@/lib/source/navigation';
export function Body({
children,
}: {
children: ReactNode;
}): React.ReactElement {
export function Body({ children }: { children: ReactNode }): React.ReactElement {
const mode = useMode();
return (
<body className={cn(mode, 'relative flex min-h-screen flex-col')}>
{children}
</body>
);
return <body className={cn(mode, 'relative flex min-h-screen flex-col')}>{children}</body>;
}
export function useMode(): string | undefined {
const { slug } = useParams();
return Array.isArray(slug) && slug.length > 0 ? slug[0] : undefined;
const { slug = [] } = useParams();
if (Array.isArray(slug)) return getSection(slug[0]);
}
export function FumadocsIcon(props: React.SVGProps<SVGSVGElement>) {
@@ -36,10 +29,7 @@ export function FumadocsIcon(props: React.SVGProps<SVGSVGElement>) {
strokeWidth="1"
/>
<defs>
<linearGradient
id={`${id}-iconGradient`}
gradientTransform="rotate(45)"
>
<linearGradient id={`${id}-iconGradient`} gradientTransform="rotate(45)">
<stop offset="45%" stopColor="var(--color-fd-background)" />
<stop offset="100%" stopColor="var(--color-fd-primary)" />
</linearGradient>
+2 -6
View File
@@ -37,14 +37,10 @@ export const viewport: Viewport = {
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html
lang="en"
className={`${geist.variable} ${mono.variable}`}
suppressHydrationWarning
>
<html lang="en" className={`${geist.variable} ${mono.variable}`} suppressHydrationWarning>
<Body>
<NextProvider>
<TreeContextProvider tree={source.pageTree}>
<TreeContextProvider tree={source.getPageTree()}>
<Provider>{children}</Provider>
</TreeContextProvider>
</NextProvider>
@@ -5,10 +5,7 @@ import { notFound } from 'next/navigation';
export const revalidate = false;
export async function GET(
_req: NextRequest,
{ params }: RouteContext<'/llms.mdx/[...slug]'>,
) {
export async function GET(_req: NextRequest, { params }: RouteContext<'/llms.mdx/[[...slug]]'>) {
const slug = (await params).slug;
const page = source.getPage(slug);
if (!page) notFound();
+4 -3
View File
@@ -1,4 +1,5 @@
import { source } from '@/lib/source';
import { getSection } from '@/lib/source/navigation';
export const revalidate = false;
@@ -8,10 +9,10 @@ export async function GET() {
const map = new Map<string, string[]>();
for (const page of source.getPages()) {
const dir = page.slugs[0];
const list = map.get(dir) ?? [];
const section = getSection(page.slugs[0]);
const list = map.get(section) ?? [];
list.push(`- [${page.data.title}](${page.url}): ${page.data.description}`);
map.set(dir, list);
map.set(section, list);
}
for (const [key, value] of map) {
+2 -13
View File
@@ -31,22 +31,11 @@ export function generate({ title, description }: GenerateProps) {
const siteName = 'Fumadocs';
const primaryTextColor = 'rgb(240,240,240)';
const logo = (
<svg
width="60"
height="60"
viewBox="0 0 180 180"
filter="url(#logo-shadow)"
>
<svg width="60" height="60" viewBox="0 0 180 180" filter="url(#logo-shadow)">
<circle cx="90" cy="90" r="86" fill="url(#logo-iconGradient)" />
<defs>
<filter id="logo-shadow" colorInterpolationFilters="sRGB">
<feDropShadow
dx="0"
dy="0"
stdDeviation="4"
floodColor="white"
floodOpacity="1"
/>
<feDropShadow dx="0" dy="0" stdDeviation="4" floodColor="white" floodOpacity="1" />
</filter>
<linearGradient id="logo-iconGradient" gradientTransform="rotate(45)">
<stop offset="45%" stopColor="black" />
+2 -8
View File
@@ -6,19 +6,13 @@ import { getPageImage } from '@/lib/metadata';
export const revalidate = false;
export async function GET(
_req: Request,
{ params }: RouteContext<'/og/[...slug]'>,
) {
export async function GET(_req: Request, { params }: RouteContext<'/og/[...slug]'>) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
return new ImageResponse(
<MetadataImage
title={page.data.title}
description={page.data.description}
/>,
<MetadataImage title={page.data.title} description={page.data.description} />,
await getImageResponseOptions(),
);
}
+1 -4
View File
@@ -32,10 +32,7 @@ export function Provider({ children }: { children: ReactNode }) {
}}
>
<TooltipProvider>
<script
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: inject }}
/>
<script suppressHydrationWarning dangerouslySetInnerHTML={{ __html: inject }} />
{children}
</TooltipProvider>
</RootProvider>
+4 -5
View File
@@ -1,6 +1,7 @@
import { source } from '@/lib/source';
import type { OramaDocument } from 'fumadocs-core/search/orama-cloud';
import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb';
import { getSection } from '@/lib/source/navigation';
export const revalidate = false;
@@ -9,7 +10,7 @@ export async function GET(): Promise<Response> {
const promises = pages.map(async (page) => {
if (page.data.type === 'openapi') return;
const items = getBreadcrumbItems(page.url, source.pageTree, {
const items = getBreadcrumbItems(page.url, source.getPageTree(), {
includePage: false,
includeRoot: true,
});
@@ -17,7 +18,7 @@ export async function GET(): Promise<Response> {
return {
id: page.url,
structured: (await page.data.load()).structuredData,
tag: page.slugs[0],
tag: getSection(page.slugs[0]),
url: page.url,
title: page.data.title,
description: page.data.description,
@@ -28,8 +29,6 @@ export async function GET(): Promise<Response> {
});
return Response.json(
(await Promise.all(promises)).filter(
(v) => v !== undefined,
) as OramaDocument[],
(await Promise.all(promises)).filter((v) => v !== undefined) as OramaDocument[],
);
}
+1 -4
View File
@@ -55,10 +55,7 @@ export function rehypeWrapWords() {
}
function createProcessor(): Processor {
const processor = remark()
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeWrapWords);
const processor = remark().use(remarkGfm).use(remarkRehype).use(rehypeWrapWords);
return {
async process(content) {
+3 -15
View File
@@ -1,20 +1,10 @@
'use client';
import { useMemo, useState } from 'react';
import {
Check,
ChevronDown,
Copy,
ExternalLinkIcon,
MessageCircleIcon,
} from 'lucide-react';
import { Check, ChevronDown, Copy, ExternalLinkIcon, MessageCircleIcon } from 'lucide-react';
import { cn } from '@/lib/cn';
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from 'fumadocs-ui/components/ui/popover';
import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover';
import { cva } from 'class-variance-authority';
const cache = new Map<string, string>();
@@ -88,9 +78,7 @@ export function ViewOptions({
}) {
const items = useMemo(() => {
const fullMarkdownUrl =
typeof window !== 'undefined'
? new URL(markdownUrl, window.location.origin)
: 'loading';
typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`;
return [
+6 -24
View File
@@ -41,11 +41,7 @@ function Header() {
<p className="text-sm font-medium mb-2">Ask AI</p>
<p className="text-xs text-fd-muted-foreground">
Powered by{' '}
<a
href="https://inkeep.com"
target="_blank"
rel="noreferrer noopener"
>
<a href="https://inkeep.com" target="_blank" rel="noreferrer noopener">
Inkeep AI
</a>
</p>
@@ -112,9 +108,7 @@ function SearchAIActions() {
const StorageKeyInput = '__ai_search_input';
function SearchAIInput(props: ComponentProps<'form'>) {
const { status, sendMessage, stop } = useChatContext();
const [input, setInput] = useState(
() => localStorage.getItem(StorageKeyInput) ?? '',
);
const [input, setInput] = useState(() => localStorage.getItem(StorageKeyInput) ?? '');
const isLoading = status === 'streaming' || status === 'submitted';
const onStart = (e?: SyntheticEvent) => {
e?.preventDefault();
@@ -129,11 +123,7 @@ function SearchAIInput(props: ComponentProps<'form'>) {
}, [isLoading]);
return (
<form
{...props}
className={cn('flex items-start pe-2', props.className)}
onSubmit={onStart}
>
<form {...props} className={cn('flex items-start pe-2', props.className)} onSubmit={onStart}>
<Input
value={input}
placeholder={isLoading ? 'AI is answering...' : 'Ask a question'}
@@ -216,10 +206,7 @@ function List(props: Omit<ComponentProps<'div'>, 'dir'>) {
<div
ref={containerRef}
{...props}
className={cn(
'fd-scroll-container overflow-y-auto min-w-0 flex flex-col',
props.className,
)}
className={cn('fd-scroll-container overflow-y-auto min-w-0 flex flex-col', props.className)}
>
{props.children}
</div>
@@ -252,10 +239,7 @@ const roleName: Record<string, string> = {
assistant: 'fumadocs',
};
function Message({
message,
...props
}: { message: UIMessage } & ComponentProps<'div'>) {
function Message({ message, ...props }: { message: UIMessage } & ComponentProps<'div'>) {
let markdown = '';
let links: z.infer<typeof ProvideLinksToolSchema>['links'] = [];
@@ -311,9 +295,7 @@ export function AISearch({ children }: { children: ReactNode }) {
});
return (
<Context value={useMemo(() => ({ chat, open, setOpen }), [chat, open])}>
{children}
</Context>
<Context value={useMemo(() => ({ chat, open, setOpen }), [chat, open])}>{children}</Context>
);
}
+1 -4
View File
@@ -21,10 +21,7 @@ export default async function ContributorCounter({
.slice(0, displayCount);
return (
<div
{...props}
className={cn('flex flex-col items-center gap-4', props.className)}
>
<div {...props} className={cn('flex flex-col items-center gap-4', props.className)}>
<div className="flex flex-row flex-wrap items-center justify-center md:pe-4">
{topContributors.map((contributor, i) => (
<a
+1 -4
View File
@@ -3,10 +3,7 @@ import { cn } from '@/lib/cn';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { ThumbsDown, ThumbsUp } from 'lucide-react';
import { type SyntheticEvent, useEffect, useState, useTransition } from 'react';
import {
Collapsible,
CollapsibleContent,
} from 'fumadocs-ui/components/ui/collapsible';
import { Collapsible, CollapsibleContent } from 'fumadocs-ui/components/ui/collapsible';
import { cva } from 'class-variance-authority';
import { usePathname } from 'next/navigation';
+2 -11
View File
@@ -1,12 +1,5 @@
'use client';
import {
lazy,
type RefObject,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { lazy, type RefObject, useEffect, useMemo, useRef, useState } from 'react';
import type {
ForceGraphMethods,
ForceGraphProps,
@@ -101,9 +94,7 @@ function ClientOnly({
ctx.arc(node.x!, node.y!, radius, 0, 2 * Math.PI, false);
const hoverNode = hoveredRef.current;
const isActive =
hoverNode?.id === node.id ||
hoverNode?.neighbors?.includes(node.id as string);
const isActive = hoverNode?.id === node.id || hoverNode?.neighbors?.includes(node.id as string);
ctx.fillStyle = isActive
? style.getPropertyValue('--color-fd-primary')
+2 -7
View File
@@ -16,10 +16,7 @@ export function Mermaid({ chart }: { chart: string }) {
const cache = new Map<string, Promise<unknown>>();
function cachePromise<T>(
key: string,
setPromise: () => Promise<T>,
): Promise<T> {
function cachePromise<T>(key: string, setPromise: () => Promise<T>): Promise<T> {
const cached = cache.get(key);
if (cached) return cached as Promise<T>;
@@ -31,9 +28,7 @@ function cachePromise<T>(
function MermaidContent({ chart }: { chart: string }) {
const id = useId();
const { resolvedTheme } = useTheme();
const { default: mermaid } = use(
cachePromise('mermaid', () => import('mermaid')),
);
const { default: mermaid } = use(cachePromise('mermaid', () => import('mermaid')));
mermaid.initialize({
startOnLoad: false,
+1 -5
View File
@@ -19,11 +19,7 @@ export function NotFound(props: NotFoundProps) {
<h1 className="text-4xl font-bold font-mono">Not Found</h1>
<div className="w-full border border-fd-foreground/50 border-dashed p-4 max-w-[400px]">
<Suspense
fallback={
<p className="text-sm text-fd-muted-foreground">
Finding Alternatives...
</p>
}
fallback={<p className="text-sm text-fd-muted-foreground">Finding Alternatives...</p>}
>
<Alternative {...props} />
</Suspense>
@@ -16,11 +16,7 @@ export default function Example() {
className="w-fit bg-transparent px-4 py-2 text-sm focus-visible:outline-none"
>
{Object.keys(bundledLanguages).map((lang) => (
<option
value={lang}
key={lang}
className="bg-fd-card text-fd-card-foreground"
>
<option value={lang} key={lang} className="bg-fd-card text-fd-card-foreground">
{lang}
</option>
))}
+4 -16
View File
@@ -55,11 +55,7 @@ export function tabs(): ReactNode {
return (
<Wrapper>
<div className="prose-no-margin">
<Tabs
groupId="language"
persist
items={['Javascript', 'Rust', 'Typescript']}
>
<Tabs groupId="language" persist items={['Javascript', 'Rust', 'Typescript']}>
<Tab value="Javascript">Hello World in Javascript</Tab>
<Tab value="Rust">Hello World in Rust</Tab>
<Tab value="Typescript">Also works if items are not the same</Tab>
@@ -69,9 +65,7 @@ export function tabs(): ReactNode {
<Tab value="Javascript">
Value is shared! Try refresh and see if the value is persisted
</Tab>
<Tab value="Rust">
Value is shared! Try refresh and see if the value is persisted
</Tab>
<Tab value="Rust">Value is shared! Try refresh and see if the value is persisted</Tab>
</Tabs>
</div>
</Wrapper>
@@ -85,8 +79,7 @@ export function typeTable(): ReactNode {
<TypeTable
type={{
percentage: {
description:
'The percentage of scroll position to display the roll button',
description: 'The percentage of scroll position to display the roll button',
type: 'number',
default: '0.2',
},
@@ -215,12 +208,7 @@ export function banner(): ReactNode {
Be careful, Fumadocs v99 has released
</Banner>
<Banner
id="test-rainbow"
className="z-0"
variant="rainbow"
changeLayout={false}
>
<Banner id="test-rainbow" className="z-0" variant="rainbow" changeLayout={false}>
Using the <code>rainbow</code> variant
</Banner>
@@ -1,9 +1,4 @@
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from 'fumadocs-ui/components/ui/tabs';
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'fumadocs-ui/components/ui/tabs';
import { CodeBlock } from '@/components/code-block';
export function Installation({ name }: { name: string }) {
@@ -14,9 +9,7 @@ export function Installation({ name }: { name: string }) {
<TabsList className="flex flex-col gap-3 text-sm items-start p-3 mb-2 bg-fd-card text-fd-card-foreground rounded-xl border not-prose sm:flex-row">
<div className="me-auto">
<p className="font-medium">Install to your codebase</p>
<p className="mt-1 text-fd-muted-foreground">
Easier customisation & control.
</p>
<p className="mt-1 text-fd-muted-foreground">Easier customisation & control.</p>
</div>
{tabs.map((tab) => (
<TabsTrigger
+2 -6
View File
@@ -9,12 +9,8 @@ export const InlineTOC = dynamic(() =>
import('fumadocs-ui/components/inline-toc').then((res) => res.InlineTOC),
);
export const File = dynamic(() =>
import('fumadocs-ui/components/files').then((res) => res.File),
);
export const Files = dynamic(() =>
import('fumadocs-ui/components/files').then((res) => res.Files),
);
export const File = dynamic(() => import('fumadocs-ui/components/files').then((res) => res.File));
export const Files = dynamic(() => import('fumadocs-ui/components/files').then((res) => res.Files));
export const Folder = dynamic(() =>
import('fumadocs-ui/components/files').then((res) => res.Folder),
);
+3 -11
View File
@@ -23,18 +23,11 @@ export const registry: Registry = {
const filePath = path.relative(baseDir, ref.file);
if (filePath === 'lib/cn.ts') {
return resolveFromRemote(
ui.registry,
'cn',
(file) => file.path === 'cn.ts',
)!;
return resolveFromRemote(ui.registry, 'cn', (file) => file.path === 'cn.ts')!;
}
}
if (
ref.type === 'dependency' &&
ref.specifier === 'fumadocs-ui/components/ui/button'
) {
if (ref.type === 'dependency' && ref.specifier === 'fumadocs-ui/components/ui/button') {
return resolveFromRemote(
radixUi.registry,
'button',
@@ -74,8 +67,7 @@ export const registry: Registry = {
{
name: 'ai/search',
title: 'AI Search (Next.js Only)',
description:
'Ask AI dialog for your docs, you need to configure Inkeep first',
description: 'Ask AI dialog for your docs, you need to configure Inkeep first',
files: [
{
type: 'components',
+10 -17
View File
@@ -15,11 +15,7 @@ import {
} from 'fumadocs-ui/components/dialog/search';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { useMemo, useState } from 'react';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from 'fumadocs-ui/components/ui/popover';
import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover';
import { ArrowRight, ChevronDown } from 'lucide-react';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { cn } from '@/lib/cn';
@@ -35,12 +31,17 @@ const items = [
},
{
name: 'Framework',
description: 'Only results about Fumadocs UI & guides',
description: 'Only results about framework guides',
value: 'framework',
},
{
name: 'UI',
description: 'Only results about Fumadocs UI',
value: 'ui',
},
{
name: 'Core',
description: 'Only results about headless features',
description: 'Only results about Fumadocs Core',
value: 'headless',
},
{
@@ -94,10 +95,7 @@ export default function CustomSearchDialog(props: SharedProps) {
<div className="inline-flex items-center gap-2 text-fd-muted-foreground">
<ArrowRight className="size-4" />
<p>
Jump to{' '}
<span className="font-medium text-fd-foreground">
{page.name}
</span>
Jump to <span className="font-medium text-fd-foreground">{page.name}</span>
</p>
</div>
),
@@ -107,12 +105,7 @@ export default function CustomSearchDialog(props: SharedProps) {
}, [router, search, searchMap]);
return (
<SearchDialog
search={search}
onSearchChange={setSearch}
isLoading={query.isLoading}
{...props}
>
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
+1 -3
View File
@@ -8,9 +8,7 @@ import Link from 'fumadocs-core/link';
const HoverCard = HoverCardPrimitive.Root;
function HoverCardTrigger(
props: ComponentProps<typeof HoverCardPrimitive.Trigger>,
) {
function HoverCardTrigger(props: ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger asChild>
<Link {...props} />
@@ -38,8 +38,7 @@ Fumadocs MDX is not intended to evolve into a CMS; it remains a foundational con
Fumadocs MDX is closely associated with Fumadocs, yet the content processing layer should ideally operate independently, without reliance on any specific documentation framework.
<Callout title="Note">
At present, Fumadocs MDX depends solely on the remark and rehype plugins from
`fumadocs-core`.
At present, Fumadocs MDX depends solely on the remark and rehype plugins from `fumadocs-core`.
</Callout>
In the near future, certain components will transition to `fuma-content`, improving extensibility and introducing more APIs. Such as:
+3 -9
View File
@@ -98,9 +98,7 @@ import { InlineTOC } from 'fumadocs-ui/components/inline-toc';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { blog } from '@/lib/source';
export default async function Page(props: {
params: Promise<{ slug: string }>;
}) {
export default async function Page(props: { params: Promise<{ slug: string }> }) {
const params = await props.params;
const page = blog.getPage([params.slug]);
@@ -126,9 +124,7 @@ export default async function Page(props: {
</div>
<div>
<p className="mb-1 text-sm text-fd-muted-foreground">At</p>
<p className="font-medium">
{new Date(page.data.date).toDateString()}
</p>
<p className="font-medium">{new Date(page.data.date).toDateString()}</p>
</div>
</div>
</article>
@@ -149,9 +145,7 @@ And configure metadata:
import { notFound } from 'next/navigation';
import { blog } from '@/lib/source';
export async function generateMetadata(props: {
params: Promise<{ slug: string }>;
}) {
export async function generateMetadata(props: { params: Promise<{ slug: string }> }) {
const params = await props.params;
const page = blog.getPage([params.slug]);
+1 -5
View File
@@ -194,11 +194,7 @@ export type DocsMeta = InferMetaType<typeof source>;
and the corresponding `source.config.ts`:
```ts
import {
defineDocs,
defineCollections,
frontmatterSchema,
} from 'fumadocs-mdx/config';
import { defineDocs, defineCollections, frontmatterSchema } from 'fumadocs-mdx/config';
import { z } from 'zod';
export const { docs, meta } = defineDocs({
+1 -1
View File
@@ -73,4 +73,4 @@ This allows Root Provider to handle the hierarchy correctly without revealing to
Changing the hierarchy of different providers may result in problems since they actually rely on each other, now it is all integrated into `<RootProvider />`, including I18n configuration.
The new [Internationalization](/docs/ui/internationalization) guide requires Fumadocs 15.2.3 or above.
The new [Internationalization](/docs/internationalization) guide requires Fumadocs 15.2.3 or above.
+4 -11
View File
@@ -160,18 +160,11 @@ export const APIPage = createAPIPage(openapi, {
<div>{/* custom layout */}</div>
),
renderAPIExampleUsageTabs: (generators) => <div>{/* custom tabs */}</div>,
renderPageLayout: ({ operations, webhooks }) => (
<div>{/* custom page */}</div>
),
renderPageLayout: ({ operations, webhooks }) => <div>{/* custom page */}</div>,
renderOperationLayout: (slots) => <div>{/* custom operation */}</div>,
renderWebhookLayout: ({
header,
authSchemes,
parameters,
body,
responses,
callbacks,
}) => <div>{/* custom webhook */}</div>,
renderWebhookLayout: ({ header, authSchemes, parameters, body, responses, callbacks }) => (
<div>{/* custom webhook */}</div>
),
},
});
```
+2 -3
View File
@@ -227,8 +227,7 @@ See [Auto Type Table](/docs/ui/components/auto-type-table).
Fumadocs v14 is compatible with Next.js 15, supporting sync and async usages of dynamic APIs.
<Callout type="info" title="Backward compatible">
Next.js 14 is also supported, notice that guides/tutorials in the docs are
mainly for Next.js 15.
Next.js 14 is also supported, notice that guides/tutorials in the docs are mainly for Next.js 15.
</Callout>
## UI Improvements
@@ -237,7 +236,7 @@ Fumadocs v14 is compatible with Next.js 15, supporting sync and async usages of
The navigation menu on Home layout is redesigned, with better animation and flexibility.
See the [new API](/docs/ui/navigation).
See the [new API](/docs/navigation).
![New Navbar](/blog/v14-navbar.png)
+3 -4
View File
@@ -135,9 +135,8 @@ And install & configure their `@scalar/api-client-react`:
```
<Callout>
Be careful that you must configure Tailwind CSS first, using the pre-built
stylesheet of Fumadocs UI will conflict with their `style.css` because both
libraries use Tailwind CSS for styling.
Be careful that you must configure Tailwind CSS first, using the pre-built stylesheet of Fumadocs
UI will conflict with their `style.css` because both libraries use Tailwind CSS for styling.
</Callout>
## Future Plans
@@ -156,4 +155,4 @@ export const updatedBlog = blog.map(...)
- remove Manifest API - it was designed to export search indexes, but now it's recommended to implement using route handlers.
- Mention more about our [MDX Remote](/docs/headless/custom-source#mdx-remote) package, it will be the primary solution to handle large docs sites with performance needs that bundlers couldn't do, including lazy compilation of MDX files.
- Mention more about our [MDX Remote](/docs/integrations/content/mdx-remote) package, it will be the primary solution to handle large docs sites with performance needs that bundlers couldn't do, including lazy compilation of MDX files.
+1 -4
View File
@@ -21,10 +21,7 @@ In short, the layout consists of a grid container, and layout componenents using
grid-template:
'sidebar header toc'
'sidebar toc-popover toc'
'sidebar main toc' 1fr / minmax(var(--fd-sidebar-col), 1fr) minmax(
0,
var(--fd-page-col)
)
'sidebar main toc' 1fr / minmax(var(--fd-sidebar-col), 1fr) minmax(0, var(--fd-page-col))
minmax(min-content, 1fr);
--fd-docs-row-1: var(--fd-banner-height, 0px);
@@ -1,7 +1,7 @@
---
title: Comparisons
description: How is Fumadocs different from other existing frameworks?
icon: GitCompareArrows
icon: Scale
---
## Nextra
@@ -1,7 +1,6 @@
---
title: Deploying
description: Deploy your Fumadocs app
icon: Rocket
---
## Overview
@@ -13,7 +12,7 @@ Your Fumadocs app is powered by the underlying React framework (e.g. Next.js), y
- [Tanstack Start](https://tanstack.com/start/latest/docs/framework/react/guide/hosting)
- [Waku](https://waku.gg/#deployment)
If you are looking to host the app as a SPA app (fully static, hostable on CDN), see [Static Build](/docs/ui/static-export).
If you are looking to host the app as a SPA app (fully static, hostable on CDN), see [Static Build](/docs/deploying/static).
There's some extra notes on specific combinations:
@@ -14,7 +14,7 @@ You can output a static build by configuring your React framework.
#### Built-in Search
1. [Configure Search Server](/docs/headless/search/orama#static-export).
2. [Configure Search UI/Client](/docs/ui/search/orama#static).
2. [Configure Search UI/Client](/docs/search/orama#static).
After the configurations, your app will statically store the search indexes, and search will be computed on browser instead.
@@ -0,0 +1,7 @@
---
title: Guides
description: Useful guides for extending Fumadocs.
icon: Book
---
<DocsCategory />
@@ -0,0 +1,302 @@
---
title: Quick Start
description: Getting Started with Fumadocs
icon: Rocket
---
## Introduction
Fumadocs <span className='text-fd-muted-foreground text-sm'>(Foo-ma docs)</span> is a **documentation framework**, designed to be fast, flexible,
and composes seamlessly into your React framework.
Fumadocs has different parts:
<Cards>
<Card icon={<CpuIcon className="text-purple-300" />} title='Fumadocs Core' href='/docs/headless'>
Handles most of the logic, including document search, content source adapters, and Markdown extensions.
</Card>
<Card icon={<PanelsTopLeft className="text-blue-300" />} title='Fumadocs UI' href='/docs/ui'>
The default theme of Fumadocs offers a beautiful look for documentation sites and interactive components.
</Card>
<Card icon={<Database />} title='Content Source'>
The source of your content, can be a CMS or local data layers like [Fumadocs MDX](/docs/mdx) (the official content source).
</Card>
<Card icon={<Terminal />} title='Fumadocs CLI' href='/docs/cli'>
A command line tool to install UI components and automate things, useful for customizing layouts.
</Card>
</Cards>
<Callout title="Want to learn more?">
Read our in-depth [What is Fumadocs](/docs/what-is-fumadocs) introduction.
</Callout>
### Terminology
**Markdown/MDX:** Markdown is a markup language for creating formatted text. Fumadocs natively supports Markdown and MDX (superset of Markdown).
**[Bun](https://bun.sh):** A JavaScript runtime, we use it for running scripts.
Some basic knowledge of React.js would be useful for further customisations.
## Automatic Installation
A minimum version of Node.js 22 required.
```bash tab="npm"
npm create fumadocs-app
```
```bash tab="pnpm"
pnpm create fumadocs-app
```
```bash tab="yarn"
yarn create fumadocs-app
```
```bash tab="bun"
bun create fumadocs-app
```
It will ask you the built-in template to use:
- **React.js framework**: Next.js, Waku, React Router, Tanstack Start.
- **Content source**: Fumadocs MDX.
A new fumadocs app should be initialized. Now you can start hacking!
<Callout title='From Existing Codebase?'>
You can follow the [Manual Installation](/docs/manual-installation) guide to get started.
</Callout>
### Enjoy!
Create your first MDX file in the docs folder.
```mdx title="content/docs/index.mdx"
---
title: Hello World
---
## Yo what's up
```
Run the app in development mode and see http://localhost:3000/docs.
```npm
npm run dev
```
## FAQ
Some common questions you may encounter.
<Accordions>
<Accordion id='upgrade-fumadocs' title="Getting error with missing APIs or bugs?">
Make sure to upgrade Fumadocs when you've encountered any problems or trying out new features:
```bash title="pnpm"
pnpm update -i -r --latest
```
</Accordion>
<Accordion id='change-base-url' title="How to change the base route of docs?">
Routing is handled by your React framework, you need to change the routing structure first.
For example, in Next.js, rename the route (`/docs/*` -> `/info/*`):
<Files>
<Folder name="app/docs" defaultOpen className="opacity-50" disabled>
<File name="layout.tsx" />
</Folder>
<Folder name="app/info" defaultOpen>
<File name="layout.tsx" />
</Folder>
</Files>
Or rename from `/docs/*` to `/*` using a route group:
<Files>
<Folder name="app/(docs)" defaultOpen>
<File name="layout.tsx" />
</Folder>
</Files>
Finally, update the base URL of pages in `source.ts`:
```ts title="lib/source.ts"
import { loader } from 'fumadocs-core/source';
export const source = loader({
baseUrl: '/info', // to the new value [!code highlight]
});
```
</Accordion>
<Accordion id='multi-docs' title="How to implement multi-docs?">
We recommend to use [Sidebar Tabs](/docs/ui/layouts/docs#sidebar-tabs).
</Accordion>
</Accordions>
### For Vite
<Accordions>
<Accordion id='vite-context-error' title="Getting error with React contexts?">
There's some weird pre-bundling problems with Vite: [#3910](https://github.com/vitejs/vite/issues/3910).
Make sure to exclude Fumadocs from pre-bundling and add it to `noExternal`:
```ts title="vite.config.ts"
import { defineConfig } from 'vite';
export default defineConfig({
resolve: {
// add other Fumadocs deps as needed
noExternal: [
'fumadocs-core',
'fumadocs-ui',
'fumadocs-openapi',
'@fumadocs/base-ui',
'@fumadocs/ui',
],
},
});
```
</Accordion>
</Accordions>
### For Next.js
<Accordions>
<Accordion id='node-23' title="Getting error on production build?">
Node.js 23.1 might have problems with Next.js, see [#1021](https://github.com/fuma-nama/fumadocs/issues/1021). Make sure to change your Node.js version.
</Accordion>
<Accordion id='dynamic-route' title="It uses Dynamic Route, will it be poor in performance?">
Next.js turns dynamic route into static routes when `generateStaticParams` is configured.
Hence, it is as fast as static pages.
You can [enable Static Exports](/docs/deploying/static) on Next.js to get a static build output.
</Accordion>
<Accordion id='webpack' title='Reading from "fumadocs-mdx:collections/server" is not handled by plugins'>
Webpack resolves import namespace **before** your import aliases in `tsconfig.json`. When using Fumadocs MDX, you have to rename your import alias to `collections/*` instead.
```json title="tsconfig.json"
{
"compilerOptions": {
"paths": {
// [!code ++]
"collections/*": [".source/*"]
}
}
}
```
Make sure to update the references as well.
```ts title="lib/source.ts"
// [!code --]
import { docs } from 'fumadocs-mdx:collections/server';
// [!code ++]
import { docs } from 'collections/server';
```
</Accordion>
<Accordion id='custom-layout-docs-page' title='How to create a page in /docs without docs layout?'>
Same as managing layouts in Next.js App Router, remove the original MDX file from content directory (`/content/docs`).
This ensures duplicated pages will not cause errors.
Now, You can add the page to another route group, which isn't a descendant of docs layout.
For example, to replace `/docs/test`:
<Files>
<File name="(home)/docs/test/page.tsx" />
<Folder name="docs" defaultOpen>
<File name="layout.tsx" />
<File name="[[...slug]]/page.tsx" />
</Folder>
</Files>
For `/docs`, you need to change the catch-all route to be non-optional:
<Files>
<File name="(home)/docs/page.tsx" />
<Folder name="docs" defaultOpen>
<File name="layout.tsx" />
<File name="[...slug]/page.tsx" />
</Folder>
</Files>
</Accordion>
</Accordions>
## Learn More
New to here? Don't worry, we are welcome for your questions.
If you find anything confusing, please give your feedback on [Github Discussion](https://github.com/fuma-nama/fumadocs/discussions)!
### Writing Content
For authoring docs, make sure to read:
<Cards>
<Card href="/docs/markdown" title="Markdown">
Fumadocs has some additional features for authoring content.
</Card>
<Card href="/docs/navigation" title="Navigation">
Learn how to customise navigation structure.
</Card>
<Card href="/docs/page-conventions" title="Page Slugs & Page Tree">
Learn how to organise content.
</Card>
<Card
href="/docs/ui/components"
title="Components"
description="See all available components to enhance your docs"
/>
</Cards>
### Special Needs
<Cards>
<Card
href="/docs/deploying/static"
title="Configure Static Export"
description="Learn how to enable static export on your docs"
/>
<Card
href="/docs/internationalization"
title="Internationalization"
description="Learn how to enable i18n"
/>
<Card href="/docs/ui/theme" title="Color Themes" description="Add themes to Fumadocs UI" />
<Card href="/docs/ui/layouts" title="Layouts" description="Customise your Fumadocs UI layouts" />
</Cards>
@@ -0,0 +1,4 @@
{
"$schema": "../../../../../.source/json-schema/docs.meta.json",
"title": "Content Generators"
}
@@ -62,5 +62,5 @@ export function getMDXComponents(components?: MDXComponents) {
Some syntax features need to be enabled separately:
- [Mermaid](/docs/ui/markdown/mermaid).
- [Math](/docs/ui/markdown/math).
- [Mermaid](/docs/markdown/mermaid).
- [Math](/docs/markdown/math).
@@ -58,8 +58,8 @@ void generate();
```
<Callout type="warn" title="Be careful">
While most docgens use Markdown or reStructuredText, Fumadocs uses **MDX**.
Make sure your doc is valid in MDX syntax before running.
While most docgens use Markdown or reStructuredText, Fumadocs uses **MDX**. Make sure your doc is
valid in MDX syntax before running.
</Callout>
### MDX Components
@@ -0,0 +1,13 @@
---
title: OG Image Generation
description: Generate dynamic preview image for docs pages
index: true
---
You can configure [Open Graph](https://ogp.me) image metadata, which defines the preview image of docs pages in website/social media, such as:
<div className='mx-auto w-full max-w-[400px]'>
![preview](./preview.png)
</div>
@@ -30,9 +30,7 @@ import { notFound } from 'next/navigation';
import { source, getPageImage } from '@/lib/source';
import type { Metadata } from 'next';
export async function generateMetadata(
props: PageProps<'/docs/[[...slug]]'>,
): Promise<Metadata> {
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
@@ -66,4 +64,4 @@ There's other available styles on Fumadocs CLI, such as `mono`:
npx @fumadocs/cli@latest add og/mono
```
Replaced your old `generate` with the installed one.
Replace your old `generate` with the installed one.
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

@@ -0,0 +1,216 @@
---
title: Takumi
description: Integrate Takumi for framework agnostic and fast metadata image generation.
---
## Installation
```npm
npm install @takumi-rs/image-response
```
Make sure to add `@takumi-rs/image-response` to `serverExternalPackages` (Next.js) or `external` depends on what bundler you are using.
```ts title="next.config.ts" tab="Next.js"
import type { NextConfig } from 'next';
const config: NextConfig = {
// [!code ++]
serverExternalPackages: ['@takumi-rs/image-response'],
};
```
```ts title="vite.config.ts" tab="Nitro / TanStack Start"
import { defineConfig } from 'vite';
// [!code ++]
import takumiPackageJson from '@takumi-rs/core/package.json' with { type: 'json' };
export default defineConfig({
nitro: {
externals: {
// [!code ++:2]
external: ['@takumi-rs/core'],
traceInclude: Object.keys(takumiPackageJson.optionalDependencies),
},
},
});
```
```ts title="vite.config.ts" tab="Vite"
import { defineConfig } from 'vite';
export default defineConfig({
ssr: {
// [!code ++]
external: ['@takumi-rs/image-response'],
},
});
```
## Metadata Image
You can generate metadata images dynamically using `@takumi-rs/image-response`.
Add the following under your loader, and define image metadata for pages:
```ts title="lib/source.ts"
import { type InferPageType } from 'fumadocs-core/source';
// [!code ++:8]
export function getPageImage(page: InferPageType<typeof source>) {
const segments = [...page.slugs, 'image.webp'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
};
}
```
```tsx tab="Next.js" title="app/docs/[[...slug]]/page.tsx"
import { notFound } from 'next/navigation';
import { source, getPageImage } from '@/lib/source';
import type { Metadata } from 'next';
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
openGraph: {
// [!code ++]
images: getPageImage(page).url,
},
};
}
```
```tsx tab="React Router" title="app/docs/page.tsx"
// [!code ++]
import { getPageImage } from '@/lib/source';
// Somewhere in your component or client loader
return (
<>
{/* [!code ++] */}
<meta name="og:image" content={getPageImage(page).url} />
</>
);
```
> We append `image.webp` to the end of slugs so that we can access it via `/og/docs/my-page/image.webp`, which results in smaller image sizes. You could use `image.png` if you prefer.
Finally, create a route handler to generate images at build time:
```tsx tab="Next.js" title="app/docs/og.tsx"
import { getPageImage, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from '@takumi-rs/image-response';
import { generate as DefaultImage } from 'fumadocs-ui/og';
export const revalidate = false;
export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
return new ImageResponse(
<DefaultImage title={page.data.title} description={page.data.description} site="My App" />,
{
width: 1200,
height: 630,
format: 'webp',
},
);
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
}));
}
```
```tsx tab="React Router" title="app/docs/og.tsx"
import { ImageResponse } from '@takumi-rs/image-response';
import { source } from '@/lib/source';
import { generate as DefaultImage } from 'fumadocs-ui/og';
import type { Route } from './+types/og';
export function loader({ params }: Route.LoaderArgs) {
const slugs = params['*']
.split('/')
.filter((v) => v.length > 0)
.slice(0, -1);
const page = source.getPage(slugs);
if (!page) throw new Response(undefined, { status: 404 });
return new ImageResponse(
<DefaultImage title={page.data.title} description={page.data.description} site="My App" />,
{
width: 1200,
height: 630,
format: 'webp',
},
);
}
```
```ts tab="React Router" title="app/routes.ts"
import { type RouteConfig, route } from '@react-router/dev/routes';
export default [
// [!code ++]
route('/og/docs/*', 'docs/og.tsx'),
] satisfies RouteConfig;
```
```ts tab="React Router" title="react-router.config.ts"
import type { Config } from '@react-router/dev/config';
// [!code ++:17]
function getDocsPaths() {
const paths = Object.keys(
import.meta.glob('./**/*.{mdx,md}', {
base: './content',
}),
);
return paths.map(relativeFileNameToPath);
}
function relativeFileNameToPath(name: string) {
const path = name.slice(1).replace(/(index)?\.(md|mdx)$/, '');
if (!path.endsWith('/')) return `${path}/`;
return path;
}
export default {
// [!code ++:6]
prerender({ getStaticPaths }) {
const docsPaths = getDocsPaths();
const docsOgPaths = docsPaths.map((path) => `/og${path}image.webp`);
return [...getStaticPaths(), ...docsPaths, ...docsOgPaths];
},
routeDiscovery: {
mode: 'initial',
},
} satisfies Config;
```
> Takumi comes with pre-bundled full-axis (100-900) [`Geist`](https://vercel.com/font) and `Geist Mono` fonts, so you don't need to worry about it.
See [Takumi's Documentation](https://takumi.kane.tw/docs) for more details or advanced usage.
### Other Templates
There's other available templates, see [Takumi Templates](https://takumi.kane.tw/docs/templates) for a full list.
@@ -1,23 +1,10 @@
---
title: Custom Source
description: Build your own content source
title: Build Your Own
description: Build your own content source for Fumadocs.
---
## Introduction
**Fumadocs is very flexible.** You can integrate with any content source, even without an official adapter.
### Examples
You can see examples to use Fumadocs with a CMS, which allows a nice experience on publishing content, and real-time update without re-building the app.
- [BaseHub](https://github.com/fuma-nama/fumadocs-basehub)
- [Sanity](https://github.com/fuma-nama/fumadocs-sanity)
- [Community: Payload CMS by `MFarabi619`](https://github.com/MFarabi619/fumadocs-payloadcms)
- [Community: Payload CMS by `bapspatil`](https://github.com/bapspatil/fumadocs-payload-template)
## Building Content Source
For a custom content source implementation, there's two ways:
### Using Loader API
@@ -123,60 +110,3 @@ Otherwise, you will have to bring your own implementation.
We recommend 3rd party solutions like Orama or Algolia Search. They are more flexible than the built-in Search API, and is easier to integrate with remote sources.
Fumadocs offers a variant of [Search Integrations](/docs/headless/search).
## MDX Remote
Fumadocs offers the **MDX Remote** package, it is a helper to integrate Markdown-based content sources with Fumadocs.
You can think it as a `next-mdx-remote` with built-in plugins for Fumadocs.
### Setup
```package-install
@fumadocs/mdx-remote
```
The main feature it offers is the MDX Compiler, it can compile MDX content to JSX nodes.
Since it doesn't use a bundler, there's some limitations:
- No imports and exports in MDX files.
It's compatible with Server Components. For example:
```tsx
import { createCompiler } from '@fumadocs/mdx-remote';
import { getPage } from './my-content-source';
import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page';
import { getMDXComponents } from '@/mdx-components';
const compiler = createCompiler({
// options
});
export default async function Page({
params,
}: {
params: { slug?: string[] };
}) {
const page = getPage(params.slug);
const compiled = await compiler.compile({
source: page.content,
});
const MdxContent = compiled.body;
return (
<DocsPage toc={compiled.toc}>
<DocsBody>
<MdxContent components={getMDXComponents()} />
</DocsBody>
</DocsPage>
);
}
```
### Images
On serverless platforms like Vercel, the original `public` folder (including static assets like images) will be removed after production build.
The MDX compiler might no longer be able to access local images in `public`.
When referencing images, make sure to use a URL.
@@ -0,0 +1,19 @@
---
title: Content Source
description: Integrate different content sources with Fumadocs.
---
## Introduction
**Fumadocs is very flexible.** You can integrate with any content source, even without an official adapter.
### Examples
You can see examples to use Fumadocs with a CMS, which allows a nice experience on publishing content, and real-time update without re-building the app.
- [BaseHub](https://github.com/fuma-nama/fumadocs-basehub)
- [Sanity](https://github.com/fuma-nama/fumadocs-sanity)
- [Community: Payload CMS by `MFarabi619`](https://github.com/MFarabi619/fumadocs-payloadcms)
- [Community: Payload CMS by `bapspatil`](https://github.com/bapspatil/fumadocs-payload-template)
Not enough? [Build your own content source](/docs/integrations/content/custom).
@@ -0,0 +1,63 @@
---
title: MDX Remote
description: Compiled Markdown/MDX content on demand.
---
## Introduction
Fumadocs offers the **MDX Remote** package, it is a helper to integrate Markdown/MDX content sources with Fumadocs.
You can think it as a `next-mdx-remote` with built-in plugins for Fumadocs.
<Callout type='warn' title="Be Careful">
The Passed MDX content must be trusted as it allows code execution by default.
</Callout>
### Setup
```package-install
@fumadocs/mdx-remote
```
The main feature it offers is the MDX Compiler, it can compile MDX content to JSX nodes.
Since it doesn't use a bundler, there's some limitations:
- No imports and exports in MDX files.
It's compatible with Server Components. For example:
```tsx
import { createCompiler } from '@fumadocs/mdx-remote';
import { getPage } from './my-content-source';
import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page';
import { getMDXComponents } from '@/mdx-components';
const compiler = createCompiler({
// options
});
export default async function Page({ params }: { params: { slug?: string[] } }) {
const page = getPage(params.slug);
const compiled = await compiler.compile({
source: page.content,
});
const MdxContent = compiled.body;
return (
<DocsPage toc={compiled.toc}>
<DocsBody>
<MdxContent components={getMDXComponents()} />
</DocsBody>
</DocsPage>
);
}
```
### Images
On serverless platforms like Vercel, the original `public` folder (including static assets like images) will be removed after production build.
The MDX compiler might no longer be able to access local images in `public`.
When referencing images, make sure to use a URL.
@@ -7,6 +7,8 @@ description: Receive feedback from your users
Feedback is crucial for knowing what your reader thinks, and help you to further improve documentation content.
You can integrate a simple feedback system with Fumadocs.
## Installation
Install it using **Fumadocs CLI**.
@@ -110,10 +110,7 @@ import { notFound } from 'next/navigation';
export const revalidate = false;
export async function GET(
_req: Request,
{ params }: RouteContext<'/llms.mdx/[[...slug]]'>,
) {
export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/[[...slug]]'>) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
@@ -180,14 +177,8 @@ import { rewritePath } from 'fumadocs-core/negotiation';
import type { Route } from './+types/root';
// [!code ++:20]
const { rewrite: rewriteLLM } = rewritePath(
'/docs{/*path}.mdx',
'/llms.mdx{/*path}',
);
const serverMiddleware: Route.MiddlewareFunction = async (
{ request },
next,
) => {
const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}.mdx', '/llms.mdx{/*path}');
const serverMiddleware: Route.MiddlewareFunction = async ({ request }, next) => {
const url = new URL(request.url);
const path = rewriteLLM(url.pathname);
if (path) return Response.redirect(new URL(path, url));
@@ -226,10 +217,7 @@ import { createMiddleware, createStart } from '@tanstack/react-start';
import { rewritePath } from 'fumadocs-core/negotiation';
import { redirect } from '@tanstack/react-router';
const { rewrite: rewriteLLM } = rewritePath(
'/docs{/*path}.mdx',
'llms.mdx{/*path}',
);
const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}.mdx', 'llms.mdx{/*path}');
const llmMiddleware = createMiddleware().server(({ next, request }) => {
const url = new URL(request.url);
@@ -323,11 +311,7 @@ To setup for Inkeep AI:
```tsx
import { AISearchTrigger } from '@/components/search';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
@@ -5,10 +5,7 @@ import { notFound } from 'next/navigation';
export const revalidate = false;
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> },
) {
export async function GET(_req: NextRequest, { params }: { params: Promise<{ slug?: string[] }> }) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
@@ -26,10 +26,7 @@ void generateFiles({
Customise the content of pages, default to `operation`.
<Callout>
Operation refers to an API endpoint with specific method like
`/api/weather:GET`.
</Callout>
<Callout>Operation refers to an API endpoint with specific method like `/api/weather:GET`.</Callout>
| mode | content | file path |
| --------- | ------------------------------ | ------------------------------------- |
@@ -58,8 +55,7 @@ void generateFiles({
const items = builder.extract();
for (const op of items.operations) {
const { pathItem, operation, displayName } =
builder.fromExtractedOperation(op)!;
const { pathItem, operation, displayName } = builder.fromExtractedOperation(op)!;
const entry: OperationOutput = {
type: 'operation',
@@ -46,9 +46,8 @@ A proxy server is useful for executing HTTP (`fetch`) requests, as it doesn't ha
We can use it for executing HTTP requests on the OpenAPI playground, when the target API endpoints do not have CORS configured correctly.
<Callout type="warn" title="Warning">
Do not use this on unreliable sites and API endpoints, the proxy server will
forward all received headers & body, including HTTP-only `Cookies` and
`Authorization` header.
Do not use this on unreliable sites and API endpoints, the proxy server will forward all received
headers & body, including HTTP-only `Cookies` and `Authorization` header.
</Callout>
### Setup
@@ -64,7 +63,7 @@ export const { GET, HEAD, PUT, POST, PATCH, DELETE } = openapi.createProxy({
});
```
> Follow the [Getting Started](/docs/ui/openapi) guide if `openapi` instance is not yet configured.
> Follow the [Getting Started](/docs/openapi) guide if `openapi` instance is not yet configured.
And set the proxy URL in `createOpenAPI`.
@@ -12,12 +12,7 @@ You can use [`next-validate-link`](https://next-validate-link.vercel.app) to val
Create a script:
```ts title="scripts/lint.ts"
import {
type FileObject,
printErrors,
scanURLs,
validateFiles,
} from 'next-validate-link';
import { type FileObject, printErrors, scanURLs, validateFiles } from 'next-validate-link';
import type { InferPageType } from 'fumadocs-core/source';
import { source } from '@/lib/source';
@@ -50,8 +50,8 @@ Create a `/app/[lang]` folder, and move your pages/layouts into it, except route
</Files>
<Callout title="Common Mistake" type="error">
Did you accidentally find your styles lost? Make sure the import path to
`global.css` is still correct!
Did you accidentally find your styles lost? Make sure the import path to `global.css` is still
correct!
</Callout>
Provide UI translations and other config to `<RootProvider />`, the English translations are used when `translations` is not specified.
@@ -210,7 +210,7 @@ Configure i18n on your search solution.
## Writing Documents
See [i18n routing](/docs/ui/page-conventions#i18n-routing) to learn how to create pages for specific locales.
See [i18n routing](/docs/page-conventions#i18n-routing) to learn how to create pages for specific locales.
### Navigation
@@ -64,13 +64,7 @@ export default [
Provide UI translations and other config to `<RootProvider />`, the English translations are used as fallbacks.
```tsx title="app/root.tsx"
import {
Links,
Meta,
Scripts,
ScrollRestoration,
useParams,
} from 'react-router';
import { Links, Meta, Scripts, ScrollRestoration, useParams } from 'react-router';
import { RootProvider } from 'fumadocs-ui/provider/base';
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
import { defineI18nUI } from 'fumadocs-ui/i18n';
@@ -177,7 +171,7 @@ export async function loader({ params }: Route.LoaderArgs) {
return {
path: page.path,
// [!code --]
tree: source.pageTree,
tree: source.getPageTree(),
// [!code ++]
tree: source.getPageTree(params.lang),
};
@@ -205,7 +199,7 @@ Configure i18n on your search solution.
## Writing Documents
See [i18n routing](/docs/ui/page-conventions#i18n-routing) to learn how to create pages for specific locales.
See [i18n routing](/docs/page-conventions#i18n-routing) to learn how to create pages for specific locales.
### Navigation
@@ -186,7 +186,7 @@ Configure i18n on your search solution.
## Writing Documents
See [i18n routing](/docs/ui/page-conventions#i18n-routing) to learn how to create pages for specific locales.
See [i18n routing](/docs/page-conventions#i18n-routing) to learn how to create pages for specific locales.
### Navigation
@@ -1,6 +1,6 @@
---
title: Manual Installation
description: Add Fumadocs to existing projects.
index: true
icon: Wrench
index: true
---
@@ -44,12 +44,7 @@ Create the following routes:
Wrap your entire app under Fumadocs providers:
```tsx title="__root.tsx"
import {
createRootRoute,
HeadContent,
Outlet,
Scripts,
} from '@tanstack/react-router';
import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router';
import * as React from 'react';
import { RootProvider } from 'fumadocs-ui/provider/tanstack';
@@ -121,7 +121,7 @@ import { getPageTreePeers } from 'fumadocs-core/page-tree';
import { source } from '@/lib/source';
<Cards>
{getPageTreePeers(source.pageTree, '/docs/my-page').map((peer) => (
{getPageTreePeers(source.getPageTree(), '/docs/my-page').map((peer) => (
<Card key={peer.url} title={peer.name} href={peer.url}>
{peer.description}
</Card>
@@ -35,10 +35,7 @@ export default defineConfig({
light: 'github-light',
dark: 'github-dark',
},
transformers: [
...(rehypeCodeDefaultOptions.transformers ?? []),
transformerTwoslash(),
],
transformers: [...(rehypeCodeDefaultOptions.transformers ?? []), transformerTwoslash()],
},
},
});
@@ -0,0 +1,25 @@
{
"title": "Framework",
"description": "The docs framework",
"icon": "Building",
"root": true,
"pages": [
"---Introduction---",
"index",
"what-is-fumadocs",
"comparisons",
"manual-installation",
"guides",
"---Writing---",
"page-conventions",
"markdown",
"---Configurations---",
"navigation",
"versioning",
"deploying",
"internationalization",
"search",
"---Integrations---",
"...integrations"
]
}
@@ -0,0 +1,28 @@
---
title: Navigation
description: Configure navigation in your Fumadocs app.
---
import { LinkIcon, PanelLeftDashed } from 'lucide-react';
**Fumadocs UI** provides different layouts to display content, you can specify navigation configurations via layouts.
<Cards>
<Card icon={<LinkIcon />} title="Layout Links" href="/docs/ui/layouts/links">
Display **navigation links** in your layouts.
It is useful for linking to frequently used resources like showcase and pricing pages.
</Card>
<Card icon={<PanelLeftDashed />} title="Sidebar Items" href="/docs/ui/layouts/docs#sidebar-items">
Sidebar renders links to all documentation pages.
Under the hood, **Page Tree** is used to represent the navigation structure.
</Card>
</Cards>
@@ -29,12 +29,7 @@ export default function CustomSearchDialog(props: SharedProps) {
});
return (
<SearchDialog
search={search}
onSearchChange={setSearch}
isLoading={query.isLoading}
{...props}
>
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>

Some files were not shown because too many files have changed in this diff Show More