mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Sync (#2835)
* 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:
committed by
GitHub
co-authored by
Kane Wang
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
commit
c811618571
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
}
|
||||
@@ -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
|
||||
Vendored
+4
-1
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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]',
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+61
-152
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-4
@@ -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();
|
||||
@@ -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) {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,10 +32,7 @@ export function Provider({ children }: { children: ReactNode }) {
|
||||
}}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<script
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{ __html: inject }}
|
||||
/>
|
||||
<script suppressHydrationWarning dangerouslySetInnerHTML={{ __html: inject }} />
|
||||
{children}
|
||||
</TooltipProvider>
|
||||
</RootProvider>
|
||||
|
||||
@@ -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[],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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).
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Comparisons
|
||||
description: How is Fumadocs different from other existing frameworks?
|
||||
icon: GitCompareArrows
|
||||
icon: Scale
|
||||
---
|
||||
|
||||
## Nextra
|
||||
+1
-2
@@ -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:
|
||||
|
||||
+1
-1
@@ -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"
|
||||
}
|
||||
+2
-2
@@ -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).
|
||||
+2
-2
@@ -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]'>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
+2
-4
@@ -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.
|
||||
+2
-72
@@ -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.
|
||||
+2
@@ -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**.
|
||||
+5
-21
@@ -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>
|
||||
+1
-4
@@ -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();
|
||||
+2
-6
@@ -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',
|
||||
+3
-4
@@ -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`.
|
||||
|
||||
+1
-6
@@ -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';
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+3
-9
@@ -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
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Manual Installation
|
||||
description: Add Fumadocs to existing projects.
|
||||
index: true
|
||||
icon: Wrench
|
||||
index: true
|
||||
---
|
||||
+1
-6
@@ -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';
|
||||
|
||||
+1
-1
@@ -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>
|
||||
+1
-4
@@ -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>
|
||||
+1
-6
@@ -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
Reference in New Issue
Block a user