mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-24 20:09:02 +00:00
merge: upstream/main (preserve Hanzo brand, 334 commits)
# Conflicts: # apps/docs/app/(home)/sponsors/data.tsx # apps/docs/app/(home)/uwu.tsx # apps/docs/app/og/[...slug]/generate.tsx # apps/docs/app/static.json/route.ts # apps/docs/content/blog/customise-ui.mdx # apps/docs/content/blog/mdx-v10-summary.mdx # apps/docs/content/blog/openapi-v9.mdx # apps/docs/content/blog/v15.mdx # apps/docs/content/blog/v16-2.mdx # apps/docs/content/docs/(framework)/deploying/static.mdx # apps/docs/content/docs/(framework)/integrations/content/custom.mdx # apps/docs/content/docs/(framework)/integrations/openapi/generate-files.mdx # apps/docs/content/docs/(framework)/integrations/openapi/server.mdx # apps/docs/content/docs/(framework)/integrations/story/client.story.tsx # apps/docs/content/docs/(framework)/integrations/story/index.mdx # apps/docs/content/docs/(framework)/integrations/story/next.mdx # apps/docs/content/docs/(framework)/search/orama.mdx # apps/docs/content/docs/(framework)/what-is-fumadocs.mdx # apps/docs/content/docs/cli/index.mdx # apps/docs/content/docs/headless/mdx/headings.mdx # apps/docs/content/docs/headless/mdx/rehype-code.mdx # apps/docs/content/docs/headless/mdx/remark-image.mdx # apps/docs/content/docs/headless/source-api/plugins.mdx # apps/docs/content/docs/mdx/(integrations)/loader/node.mdx # apps/docs/content/docs/mdx/global.mdx # apps/docs/content/docs/mdx/index.mdx # apps/docs/content/docs/ui/components/banner.mdx # apps/docs/content/docs/ui/components/graph-view.mdx # apps/docs/content/docs/ui/layouts/flux.mdx # apps/docs/content/docs/ui/layouts/home-layout.mdx # apps/docs/content/docs/ui/layouts/root-provider.mdx # apps/docs/content/docs/ui/theme.mdx # apps/docs/eslint.config.mjs # apps/docs/scripts/preload.ts # apps/docs/scripts/update-orama-index.ts # examples/openapi/README.md # examples/openapi/scripts/generate-docs.ts # examples/react-router-i18n/scripts/preload.ts # examples/tanstack-start-story/src/components/callout/story-client.tsx # packages/base-ui/scripts/compile-inline.utils.ts # packages/cli/src/build/index.ts # packages/core/src/content/md/frontmatter.ts # packages/create-app/scripts/update-git-repo.js # packages/create-app/scripts/update-git-repo.sh # packages/mdx-runtime/eslint.config.mjs # packages/openapi/scripts/compile-inline.utils.ts # packages/openapi/src/server/create.ts # packages/openapi/src/ui/full.tsx # packages/openapi/src/utils/process-document.ts # packages/press/waku.config.ts # packages/radix-ui/scripts/compile-inline.utils.ts
This commit is contained in:
@@ -14,5 +14,5 @@
|
||||
"version": true,
|
||||
"tag": false
|
||||
},
|
||||
"ignore": ["docs", "example-*"]
|
||||
"ignore": ["docs", "example-*", "vite-data"]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'fumadocs-openapi': minor
|
||||
---
|
||||
|
||||
Support `meta.json` file generation
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'create-fumadocs-app': patch
|
||||
---
|
||||
|
||||
include shared.ts in all templates
|
||||
@@ -9,6 +9,8 @@ on:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
"packages/mdx-remote/test/fixtures/**/*.js",
|
||||
"packages/mdx-remote/test/fixtures/**/*.json",
|
||||
"packages/local-md/test/fixtures/**/*.json",
|
||||
"packages/core/test/fixtures/page-trees/**/*.json",
|
||||
|
||||
"routeTree.gen.ts",
|
||||
|
||||
Vendored
+1
-5
@@ -1,13 +1,9 @@
|
||||
{
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"mode": "auto"
|
||||
}
|
||||
],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
],
|
||||
"oxc.enable.oxlint": false,
|
||||
"files.associations": {
|
||||
"**/packages/create-app/template/**/*": "plaintext"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript", "react", "import", "nextjs"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"env": {
|
||||
"builtin": true
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "19.2.4"
|
||||
},
|
||||
"tailwindcss": {
|
||||
"callees": ["clsx", "cva", "cn"]
|
||||
}
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
".next/",
|
||||
".source/",
|
||||
"out/",
|
||||
"next.config.mjs",
|
||||
"postcss.config.js",
|
||||
"source.config.mjs",
|
||||
"node_modules/",
|
||||
"dist/"
|
||||
],
|
||||
"rules": {
|
||||
"constructor-super": "error",
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-case-declarations": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-compare-neg-zero": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-constant-binary-expression": "error",
|
||||
"no-constant-condition": "error",
|
||||
"no-control-regex": "error",
|
||||
"no-debugger": "error",
|
||||
"no-delete-var": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-dupe-else-if": "error",
|
||||
"no-dupe-keys": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-empty": "error",
|
||||
"no-empty-character-class": "error",
|
||||
"no-empty-pattern": "error",
|
||||
"no-empty-static-block": "error",
|
||||
"no-ex-assign": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-func-assign": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-import-assign": "error",
|
||||
"no-invalid-regexp": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-misleading-character-class": "error",
|
||||
"no-new-native-nonconstructor": "error",
|
||||
"no-nonoctal-decimal-escape": "error",
|
||||
"no-obj-calls": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-self-assign": "error",
|
||||
"no-setter-return": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-sparse-arrays": "error",
|
||||
"no-this-before-super": "error",
|
||||
"no-unassigned-vars": "error",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unsafe-negation": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-unused-labels": "error",
|
||||
"no-unused-private-class-members": "error",
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-useless-backreference": "error",
|
||||
"no-useless-catch": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-with": "error",
|
||||
"preserve-caught-error": "error",
|
||||
"require-yield": "error",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"no-array-constructor": "error",
|
||||
"@typescript-eslint/no-duplicate-enum-values": "error",
|
||||
"@typescript-eslint/no-empty-object-type": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-extra-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/no-this-alias": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "error",
|
||||
"@typescript-eslint/no-unsafe-declaration-merging": "error",
|
||||
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"@typescript-eslint/no-wrapper-object-types": "error",
|
||||
"@typescript-eslint/prefer-as-const": "error",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "error",
|
||||
"@typescript-eslint/triple-slash-reference": "error",
|
||||
"react/display-name": "error",
|
||||
"react/jsx-key": "error",
|
||||
"react/jsx-no-comment-textnodes": "error",
|
||||
"react/jsx-no-duplicate-props": "error",
|
||||
"react/jsx-no-target-blank": "error",
|
||||
"react/jsx-no-undef": "error",
|
||||
"react/no-children-prop": "error",
|
||||
"react/no-danger-with-children": "error",
|
||||
"react/no-direct-mutation-state": "error",
|
||||
"react/no-find-dom-node": "error",
|
||||
"react/no-is-mounted": "error",
|
||||
"react/no-render-return-value": "error",
|
||||
"react/no-string-refs": "error",
|
||||
"react/no-unescaped-entities": "error",
|
||||
"react/no-unknown-property": "error",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
|
||||
"rules": {
|
||||
"constructor-super": "off",
|
||||
"no-class-assign": "off",
|
||||
"no-const-assign": "off",
|
||||
"no-dupe-class-members": "off",
|
||||
"no-dupe-keys": "off",
|
||||
"no-func-assign": "off",
|
||||
"no-import-assign": "off",
|
||||
"no-new-native-nonconstructor": "off",
|
||||
"no-obj-calls": "off",
|
||||
"no-redeclare": "off",
|
||||
"no-setter-return": "off",
|
||||
"no-this-before-super": "off",
|
||||
"no-unsafe-negation": "off",
|
||||
"no-var": "error",
|
||||
"no-with": "off",
|
||||
"prefer-const": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -84,9 +84,14 @@ const showcases: ShowcaseObject[] = [
|
||||
url: 'https://comfydeploy.com',
|
||||
},
|
||||
{
|
||||
image: '/showcases/octarine.png',
|
||||
name: 'Octarine',
|
||||
url: 'https://docs.octarine.app',
|
||||
image: '/showcases/conductor.png',
|
||||
name: 'Conductor',
|
||||
url: 'https://conductor.build/docs',
|
||||
},
|
||||
{
|
||||
image: '/showcases/cossistant.png',
|
||||
name: 'Cossistant',
|
||||
url: 'https://cossistant.com',
|
||||
},
|
||||
{
|
||||
image: '/showcases/llm-gateway.png',
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Users who sponsored with their personal account as an organization
|
||||
*/
|
||||
export const organizationAsUserSponsors = [
|
||||
{
|
||||
url: 'https://mintlify.com',
|
||||
asUser: 'handotdev',
|
||||
label: 'Mintlify',
|
||||
github: 'mintlify',
|
||||
logo: (
|
||||
<svg
|
||||
viewBox="0 0 103 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-auto h-8"
|
||||
>
|
||||
<path
|
||||
d="M33.8891 18H30.7691V4H34.5891L38.4091 14.26L42.2091 4H46.0291V18H42.9091V9.36L39.6891 18H37.1091L33.8891 9.36V18ZM49.5854 6.34C49.0787 6.34 48.6454 6.16667 48.2854 5.82C47.9387 5.46 47.7654 5.02 47.7654 4.5C47.7654 3.99333 47.9387 3.56667 48.2854 3.22C48.6454 2.87333 49.0787 2.7 49.5854 2.7C50.1054 2.7 50.5387 2.87333 50.8854 3.22C51.2321 3.56667 51.4054 3.99333 51.4054 4.5C51.4054 5.02 51.2321 5.46 50.8854 5.82C50.5387 6.16667 50.1054 6.34 49.5854 6.34ZM51.1254 18H48.0454V7.4H51.1254V18ZM52.9626 18V7.4H55.4426L55.7026 8.9C55.9959 8.34 56.4492 7.88667 57.0626 7.54C57.6892 7.19333 58.3892 7.02 59.1626 7.02C60.2826 7.02 61.1759 7.38 61.8426 8.1C62.5226 8.82 62.8626 9.78667 62.8626 11V18H59.7826V11.16C59.7826 10.7067 59.6359 10.3467 59.3426 10.08C59.0492 9.81333 58.6492 9.68 58.1426 9.68C57.5426 9.68 57.0426 9.86 56.6426 10.22C56.2426 10.58 56.0426 11.0467 56.0426 11.62V18H52.9626ZM69.2688 18C68.0022 18 67.0288 17.7067 66.3488 17.12C65.6822 16.5333 65.3488 15.68 65.3488 14.56V10H63.6088V7.4H65.3488V3.78H68.4288V7.4H71.0288V10H68.4288V14.22C68.4288 14.6067 68.5288 14.9 68.7288 15.1C68.9288 15.3 69.2155 15.4 69.5888 15.4H70.8088V18H69.2688ZM75.3891 18H72.3091V3H75.3891V18ZM78.7854 6.34C78.2787 6.34 77.8454 6.16667 77.4854 5.82C77.1387 5.46 76.9654 5.02 76.9654 4.5C76.9654 3.99333 77.1387 3.56667 77.4854 3.22C77.8454 2.87333 78.2787 2.7 78.7854 2.7C79.3054 2.7 79.7387 2.87333 80.0854 3.22C80.4321 3.56667 80.6054 3.99333 80.6054 4.5C80.6054 5.02 80.4321 5.46 80.0854 5.82C79.7387 6.16667 79.3054 6.34 78.7854 6.34ZM80.3254 18H77.2454V7.4H80.3254V18ZM83.3426 18V10H81.6026V7.4H83.3426V6.24C83.3426 5.18667 83.6626 4.38667 84.3026 3.84C84.9426 3.28 85.8692 3 87.0826 3H89.3426V5.6H87.4026C86.7492 5.6 86.4226 5.92667 86.4226 6.58V7.4H89.1426V10H86.4226V18H83.3426ZM90.4591 22V19.4H92.5991C93.3191 19.4 93.7725 19.1467 93.9591 18.64L94.1991 18H93.7391L89.5391 7.4H92.8391L95.4791 14.54L98.1191 7.4H101.319L96.8391 19.42C96.5191 20.2867 96.0191 20.9333 95.3391 21.36C94.6591 21.7867 93.7458 22 92.5991 22H90.4591Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<g clipPath="url(#clip0_115_7772)">
|
||||
<path
|
||||
d="M5.53338 7.66241C5.54806 5.66259 6.34114 3.74714 7.74443 2.32227H7.74185L2.32412 7.73999H2.32671C2.30654 7.75586 2.28753 7.77314 2.26981 7.79171C0.95223 9.10757 0.153396 10.8545 0.0199146 12.7118C-0.113567 14.5691 0.42726 16.4123 1.54314 17.9031L7.01517 12.431L7.07206 12.3767C6.05218 11.0186 5.51109 9.36073 5.53338 7.66241Z"
|
||||
fill="#0C8C5E"
|
||||
/>
|
||||
<path
|
||||
d="M18.6882 13.2688C17.6513 14.285 16.3497 14.9894 14.9318 15.3017C13.514 15.614 12.0369 15.5216 10.669 15.0351C9.93963 14.7761 9.25417 14.4073 8.63634 13.9412L8.57945 13.9981L3.10742 19.4675C4.59878 20.5806 6.44095 21.1197 8.29711 20.9863C10.1533 20.8529 11.8994 20.0558 13.2162 18.7408L13.2705 18.6865L18.6882 13.2688Z"
|
||||
fill="#0C8C5E"
|
||||
/>
|
||||
<path
|
||||
d="M21.0104 7.73998V3.87298e-06H13.2704C12.2539 -0.00101453 11.2472 0.198818 10.3081 0.588023C9.36903 0.977229 8.51612 1.54814 7.79835 2.26795L7.74404 2.32226C6.79795 3.28273 6.12105 4.47511 5.78125 5.77977C6.39637 5.62048 7.02851 5.53626 7.66388 5.52893C9.36226 5.50894 11.0196 6.05079 12.3782 7.0702C13.5992 7.98076 14.5233 9.23241 15.0341 10.6674C15.5546 12.134 15.623 13.7231 15.2306 15.2291C16.5355 14.8899 17.728 14.2129 18.6881 13.2663L18.7424 13.2146C19.4625 12.4965 20.0336 11.6432 20.4228 10.7037C20.812 9.76414 21.0117 8.75694 21.0104 7.73998Z"
|
||||
fill="#18E299"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_115_7772">
|
||||
<rect width="21" height="21" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
url: 'https://github.com/scalar/scalar',
|
||||
label: 'Scalar',
|
||||
github: 'scalar',
|
||||
asUser: 'marclave',
|
||||
logo: (
|
||||
<>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 593 593" className="size-9.5">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M347 0c6 0 12 5 12 12v134l94-95c5-5 13-5 17 0l72 72c4 4 5 12 0 16v1l-95 94h134c7 0 12 5 12 12v101c0 7-5 12-12 12H447l95 94c4 5 5 13 0 17l-72 72c-4 4-12 5-16 0h-1l-94-95v134c0 7-5 12-12 12H246c-7 0-12-5-12-12v-70c0-22 9-43 24-59l130-130c14-14 14-37 0-51L259 142a84 84 0 0 1-25-59V12c0-7 5-12 12-12zM138 52h1l219 219c14 14 14 37 0 51L139 542c-4 5-12 5-17 0l-71-70c-4-5-5-12 0-17l95-96H12c-7 0-12-5-12-12V246c0-7 5-12 12-12h134l-95-94c-4-5-4-12 0-17l71-71c4-5 12-5 16 0"
|
||||
/>
|
||||
</svg>
|
||||
Scalar
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
url: 'https://launchfa.st',
|
||||
label: 'launchfast',
|
||||
github: 'launchfast',
|
||||
asUser: 'rishi-raj-jain',
|
||||
logo: (
|
||||
<>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-9.5 rounded-full border border-fd-foreground"
|
||||
viewBox="0 0 784 784"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
mask="url(#launchfast_window)"
|
||||
d="M598 186c8.54 10.037 7.44 22.11 6.465 34.497-.24 2.837-.49 5.674-.742 8.51l-.397 4.607c-.346 4.021-.697 8.042-1.05 12.063-.56 6.41-1.116 12.822-1.668 19.233a5334.62 5334.62 0 0 0-.944 10.817l-.176 2.046a362.84 362.84 0 0 0-.75 10.902l-.124 2.334a569.933 569.933 0 0 0-.21 4.423c-.214 4.29-.806 7.778-2.603 11.662-.994 2.365-1.454 4.71-1.989 7.219-1.386 5.796-3.5 11.149-5.812 16.625l-1.044 2.475c-1.747 4.055-3.619 7.83-5.956 11.587-.696 2.191-.696 2.191-1 4h-2l-.715 2.582c-1.437 3.822-3.44 6.853-5.847 10.106l-1.337 1.835c-7.368 10.027-15.5 18.94-24.376 27.64a5694.76 5694.76 0 0 0-18.439 18.188c-3.706 3.67-7.42 7.333-11.147 10.982a1878.074 1878.074 0 0 0-10.793 10.657 633.457 633.457 0 0 1-4.122 4.046 567.68 567.68 0 0 0-5.725 5.67l-1.744 1.668c-1.777 1.731-1.777 1.731-3.755 4.626-.05 3.175-.05 3.175 1 6l.938 3.254c.35 1.092.7 2.184 1.062 3.308 4.092 12.772 3.947 25.655 1.688 38.813l-.357 2.142c-.745 3.913-1.764 6.456-4.331 9.483-.714 2.65-.714 2.65-1 5h-2l-.563 2.438c-1.979 4.905-5.088 8.512-8.437 12.562l-1.5 1.91c-1.116 1.405-2.302 2.755-3.5 4.09h-2v2c-1.8 1.594-1.8 1.594-4.313 3.5A172.533 172.533 0 0 0 455.5 551c-4.207 3.876-8.543 7.476-13.04 11.008-2.548 2.063-4.974 4.22-7.397 6.43A290.988 290.988 0 0 1 419 582l-2.344 1.898c-4.786 3.59-8.404 4.914-14.43 4.485-3.123-.537-4.868-1.233-7.226-3.383-3.406-5.002-5.503-10.483-7.773-16.074-1.287-3.07-2.74-6-4.275-8.95C382 558 382 558 380.742 554.41c-1.931-5.226-4.692-9.921-7.492-14.722l-1.621-2.803c-6.761-12.23-6.761-12.23-15.629-22.885l-1.206 1.253c-1.819 1.882-3.65 3.752-5.481 5.622l-1.897 1.973c-4.408 4.477-7.49 6.89-13.916 7.465-5.833-.521-9.095-3.66-13.082-7.735l-2.576-2.61-2.654-2.718c-5.343-5.45-10.652-10.828-16.503-15.737-1.738-1.56-3.205-3.21-4.685-5.013-2.712-3.293-5.74-5.981-8.984-8.742-3.167-2.761-6.077-5.76-9.016-8.758a1037.58 1037.58 0 0 0-8.883-8.813L262.875 466l-2.168-2.117c-9.094-9.01-9.094-9.01-9.332-15.695.169-5.182 1.954-7.632 5.535-11.235L259 435a1202.76 1202.76 0 0 0 4.438-4.375l2.058-1.96c1.728-1.538 1.728-1.538 1.504-3.665-1.805-1.454-1.805-1.454-4.18-2.906l-2.728-1.74-2.967-1.854-3.026-1.9c-12.059-7.516-23.892-14.714-37.316-19.487-18.847-6.718-18.847-6.718-22.658-12.488-1.59-5.125-2.662-9.655-.09-14.625 1.541-2.13 3.195-4.056 4.965-6l2.086-2.336a866.255 866.255 0 0 1 4.574-5.047c2.276-2.546 4.443-5.15 6.59-7.805 7.3-9.01 14.93-17.75 22.75-26.312 2.398-2.633 4.583-5.228 6.563-8.188 5.777-8.345 15.06-13.848 23.437-19.312l2.016-1.32c17.078-9.77 39.649-8.248 57.984-3.68l3.137 1.164c3.37.984 4.55.977 7.863-.164 2.424-1.87 4.506-4.043 6.632-6.241l1.959-1.977a972.937 972.937 0 0 0 4.197-4.269c2.221-2.273 4.456-4.533 6.694-6.79a6180.08 6180.08 0 0 0 16.656-16.903 4766.2 4766.2 0 0 1 14.043-14.225c2.176-2.201 4.339-4.414 6.501-6.628 42.327-42.95 88.855-50.246 146.518-54.88 2.264-.181 4.527-.364 6.791-.55C585.59 175.325 585.59 175.325 598 186Z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M264.313 503.563c5.121.607 8.929 2.92 12.687 6.437 1.961 5.23 1.59 10.435-.338 15.553-3.284 6.238-8.6 10.938-13.56 15.842-1.08 1.079-2.158 2.159-3.236 3.24-2.251 2.252-4.51 4.498-6.773 6.739a1708.123 1708.123 0 0 0-8.641 8.628c-2.222 2.228-4.45 4.45-6.682 6.668-1.063 1.059-2.124 2.12-3.183 3.183a758.37 758.37 0 0 1-4.471 4.435l-2.561 2.545c-4.848 4.112-9.126 5.743-15.375 5.522-4.382-.714-7.145-3.177-9.993-6.418-1.867-3.046-2.384-5.742-2.5-9.25l-.113-2.238c.981-5.644 5.575-9.857 9.467-13.796l1.48-1.505c1.6-1.624 3.206-3.24 4.815-4.855l3.37-3.397c2.348-2.365 4.7-4.725 7.056-7.083 3.013-3.02 6.01-6.054 9.004-9.093 2.311-2.341 4.634-4.67 6.96-6.997a628.716 628.716 0 0 0 3.317-3.356 520.1 520.1 0 0 1 4.658-4.667l2.668-2.684c3.83-2.935 7.295-2.84 11.944-3.454Z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M233.375 467.125c3.494 2.478 5.295 4.72 6.625 8.875.734 5.765-.781 9.346-4 14-2.25 2.662-4.675 5.12-7.145 7.578l-2.084 2.1c-1.45 1.457-2.903 2.909-4.36 4.357a1034.46 1034.46 0 0 0-6.616 6.668 3703.998 3703.998 0 0 1-4.252 4.25l-1.974 2.008c-5.108 5.031-10.158 8.373-17.569 8.726-3.24-.074-4.603-.37-7.16-2.41-3.744-4.236-5.988-7.316-6.153-13.027l-.113-2.547c1.057-6.71 7.04-11.623 11.64-16.219l2.1-2.112c1.46-1.465 2.92-2.927 4.385-4.387 2.237-2.233 4.46-4.48 6.682-6.727l4.271-4.274 2.005-2.032c6.829-6.761 14.751-9.963 23.718-4.827ZM303.758 541.445c5.337 1.32 9.073 3.911 12.242 8.367 1.871 5.965 1.74 10.17-1 15.688a128.013 128.013 0 0 1-2 3.5h-2v2c-2.375 2.625-2.375 2.625-5 5h-2v2h-2v2c-1.715 1.656-1.715 1.656-3.938 3.5-2.982 2.39-2.982 2.39-5.062 5.5h-2l-.813 1.938C289 593 289 593 286 594v2c-1.645 1.473-1.645 1.473-3.813 3.063l-2.144 1.597c-3.967 2.602-7.401 2.655-12.043 2.34-4.535-1.542-7.49-3.801-10.07-7.781-1.964-4.687-1.955-9.293-.93-14.219 1.84-3.271 4.18-5.55 7-8l1.93-1.691c1.316-1.14 2.64-2.27 3.968-3.395 2.383-2.17 4.475-4.495 6.602-6.914 3.308-3.75 6.692-7.135 10.54-10.328 2.012-1.668 2.012-1.668 3.92-3.656 4.065-4.017 6.948-6.077 12.798-5.57Z"
|
||||
/>
|
||||
|
||||
<defs>
|
||||
<mask id="launchfast_window">
|
||||
<rect x="0" y="0" width="784" height="784" fill="white" />
|
||||
<circle fill="black" cx="459" cy="323" r="43.84" />
|
||||
</mask>
|
||||
</defs>
|
||||
</svg>
|
||||
LaunchFast
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
url: 'https://orshot.com',
|
||||
label: 'orshot',
|
||||
github: 'orshot-hq',
|
||||
asUser: 'rishi-raj-jain',
|
||||
logo: (
|
||||
<>
|
||||
<svg
|
||||
className="size-9.5 rounded-full border border-fd-foreground"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="512" height="512" rx="96" fill="#111111" />
|
||||
<path
|
||||
d="M281.003 278.046C285.762 284.142 291.202 289.22 297.324 293.258C289.822 303.739 281.918 313.708 273.613 323.167C264.666 333.116 255.387 341.741 245.776 349.053C236.178 356.356 226.378 362.103 216.378 366.315C206.469 370.49 196.489 372.56 186.419 372.56C173.453 372.56 164.297 368.698 158.346 361.437C152.241 353.748 148.994 342.743 148.994 328.04C148.994 311.648 152.25 294.729 158.815 277.267C165.657 259.463 174.268 242.367 184.652 225.979C195.29 209.566 206.985 194.47 219.735 180.688C232.747 166.881 245.481 155.498 257.934 146.513L260.424 144.717L258.571 142.269L247.123 127.149L245.668 125.228L243.479 126.234C232.311 131.364 221.173 137.947 210.063 145.968C198.973 153.973 188.253 162.944 177.903 172.875C167.539 182.82 157.9 193.61 148.988 205.243C140.069 216.646 132.23 228.539 125.473 240.923L125.462 240.943L125.452 240.963C118.93 253.359 113.729 265.893 109.855 278.563C105.96 291.302 104 303.838 104 316.16C104 328.458 105.833 339.62 109.555 349.608C113.258 359.544 118.464 368.168 125.189 375.437C131.924 382.716 139.915 388.246 149.135 392.01C158.587 396.012 168.76 398 179.622 398C190.218 398 201.594 396.142 213.732 392.48C225.967 389.03 238.265 383.381 250.626 375.577C263.018 367.752 275.123 357.755 286.948 345.613L286.958 345.602L286.968 345.592C298.411 333.607 309.142 319.281 319.173 302.64C322.727 303.341 326.658 303.68 330.947 303.68C345.04 303.68 358.403 300.316 370.994 293.608C383.717 286.829 395.85 275.029 407.456 258.483L409.091 256.152L406.849 254.398L393.97 244.318L391.303 242.23L389.553 245.13C385.583 251.71 381.526 257.302 377.39 261.931C373.269 266.304 368.955 269.835 364.455 272.552L364.426 272.57L364.397 272.588C360.158 275.282 355.458 277.317 350.279 278.683C345.055 279.82 339.212 280.4 332.736 280.4H332.729H332.722H332.715H332.708H332.701H332.694H332.687H332.68H332.673H332.666H332.659H332.652H332.645H332.638H332.631H332.624H332.617H332.611H332.604H332.597H332.59H332.583H332.576H332.569H332.562H332.555H332.548H332.541H332.534H332.527H332.521H332.514H332.507H332.5H332.493H332.486H332.479H332.472H332.465H332.459H332.452H332.445H332.438H332.431H332.424H332.417H332.411H332.404H332.397H332.39H332.383H332.376H332.369H332.363H332.356H332.349H332.342H332.335H332.328H332.322H332.315H332.308H332.301H332.294H332.287H332.281H332.274H332.267H332.26H332.253H332.247H332.24H332.233H332.226H332.22H332.213H332.206H332.199H332.192H332.186H332.179H332.172H332.165H332.159H332.152H332.145H332.138H332.132H332.125H332.118H332.111H332.105H332.098H332.091H332.084H332.078H332.071H332.064H332.058H332.051H332.044H332.037H332.031H332.024H332.017H332.011H332.004H331.997H331.99H331.984H331.977H331.97H331.964H331.957H331.95H331.944H331.937H331.93H331.924H331.917H331.91H331.904H331.897H331.89H331.884H331.877H331.871H331.864H331.857H331.851H331.844H331.837H331.831H331.824H331.817H331.811H331.804H331.798H331.791H331.784H331.778H331.771H331.765H331.761C335.092 274.602 338.02 268.273 340.551 261.42C343.926 252.684 346.702 243.941 348.877 235.191C351.289 226.206 353.101 217.332 354.31 208.57C355.518 199.822 356.127 191.762 356.127 184.4C356.127 174.602 355.154 165.377 353.191 156.736C351.231 148.107 348.392 140.607 344.627 134.286C340.858 127.718 336.147 122.552 330.458 118.899C324.696 114.957 318.149 113 310.914 113C302.932 113 295.562 115.213 288.873 119.614C282.283 123.95 276.523 129.9 271.565 137.383L271.558 137.393L271.552 137.403C266.829 144.657 263.137 153.106 260.445 162.71C257.742 172.356 256.399 182.59 256.399 193.4C256.399 204.119 257.246 214.603 258.943 224.85L258.949 224.884L258.955 224.918C260.897 235.177 263.692 244.847 267.348 253.921L267.354 253.934L267.359 253.948C271.027 262.805 275.561 270.829 280.972 278.006L280.987 278.026L281.003 278.046ZM318.456 250.371L318.448 250.394L318.44 250.417C315.977 257.852 312.973 265.203 309.426 272.469C305.054 269.145 301.273 264.956 298.087 259.87C294.152 253.345 290.907 246.347 288.353 238.87C285.79 231.367 283.925 223.627 282.758 215.646C281.583 207.604 281.001 200.191 281.001 193.4C281.001 185.92 281.697 178.816 283.079 172.081C284.7 165.101 286.879 159.137 289.582 154.15L289.595 154.125L289.608 154.101C292.311 148.888 295.508 144.939 299.149 142.138L299.192 142.104L299.234 142.07C302.81 139.106 306.559 137.72 310.556 137.72C313.31 137.72 315.82 138.707 318.181 140.885L318.276 140.973L318.379 141.052C320.975 143.062 323.201 146.074 324.96 150.278L324.998 150.368L325.041 150.455C327.043 154.484 328.64 159.379 329.793 165.185C330.942 170.962 331.525 177.363 331.525 184.4C331.525 189.51 331.062 195.612 330.12 202.726L330.114 202.77L330.109 202.813C329.413 209.592 328.014 216.997 325.899 225.037L325.889 225.077L325.88 225.117C323.998 233.162 321.525 241.579 318.456 250.371Z"
|
||||
fill="white"
|
||||
stroke="white"
|
||||
stroke-width="6"
|
||||
/>
|
||||
</svg>
|
||||
Orshot
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Fragment } from 'react';
|
||||
import { Fragment, ReactNode } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/cn';
|
||||
import { getSponsors } from '@/lib/get-sponsors';
|
||||
import { getSponsors, type Sponsor } from '@/lib/get-sponsors';
|
||||
import { owner } from '@/lib/github';
|
||||
import { organizationAsUserSponsors } from '@/app/(home)/sponsors/data';
|
||||
import { HeartIcon } from 'lucide-react';
|
||||
@@ -27,28 +27,29 @@ const tiers = [
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default async function Page() {
|
||||
const result = await getSponsors(owner);
|
||||
function mapSponsors(result: Sponsor[]): (Sponsor & { logo?: ReactNode })[] {
|
||||
return result.flatMap((v) => {
|
||||
const orgs = organizationAsUserSponsors.filter((entity) => entity.asUser === v.login);
|
||||
|
||||
const sponsors = result.map((v) => {
|
||||
const entity = organizationAsUserSponsors.find((entity) => entity.asUser === v.login);
|
||||
if (entity) {
|
||||
return {
|
||||
login: entity.github,
|
||||
name: entity.label,
|
||||
websiteUrl: entity.url,
|
||||
logo: entity.logo,
|
||||
if (orgs.length > 0) {
|
||||
return orgs.map((org) => ({
|
||||
...v,
|
||||
__typename: 'Organization',
|
||||
tier: v.tier,
|
||||
avatarUrl: undefined,
|
||||
};
|
||||
login: org.github,
|
||||
name: org.label,
|
||||
websiteUrl: org.url,
|
||||
logo: org.logo,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
logo: undefined,
|
||||
...v,
|
||||
};
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
const all = await getSponsors(owner);
|
||||
const sponsors = mapSponsors(all.filter((v) => v.isActive));
|
||||
const pastSponsors = mapSponsors(all.filter((v) => !v.isActive));
|
||||
|
||||
return (
|
||||
<main className="w-full max-w-page mx-auto flex flex-col items-center px-4 py-16 text-center z-2">
|
||||
@@ -224,7 +225,7 @@ export default async function Page() {
|
||||
href={getSponsorHref(sponsor.login, sponsor.websiteUrl)}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className="inline-flex items-center gap-2 rounded-xl p-3 text-xs transition-colors hover:bg-fd-primary/10"
|
||||
className="inline-flex items-center gap-2 rounded-xl p-3 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
|
||||
>
|
||||
<Image
|
||||
alt="avatar"
|
||||
@@ -238,6 +239,40 @@ export default async function Page() {
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{pastSponsors.length > 0 && (
|
||||
<>
|
||||
<h2 className="mt-12 font-mono text-xs mb-7">Past Sponsors</h2>
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
|
||||
{pastSponsors.map((sponsor) => (
|
||||
<a
|
||||
key={sponsor.login}
|
||||
href={getSponsorHref(sponsor.login, sponsor.websiteUrl)}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className="inline-flex items-center gap-2 rounded-xl p-3 text-xs text-start transition-colors text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-accent-foreground"
|
||||
>
|
||||
{sponsor.logo ?? (
|
||||
<Image
|
||||
alt="avatar"
|
||||
src={sponsor.avatarUrl}
|
||||
unoptimized
|
||||
width="30"
|
||||
height="30"
|
||||
className="rounded-lg opacity-70"
|
||||
/>
|
||||
)}
|
||||
<p>
|
||||
<span className="font-medium">{sponsor.name}</span>
|
||||
<br />
|
||||
<code>${sponsor.tier.monthlyPriceInDollars}</code>{' '}
|
||||
{sponsor.isOneTimePayment ? 'one-time' : 'monthly'}
|
||||
</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { cn } from '@/lib/cn';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
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 />
|
||||
|
||||
<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.
|
||||
</p>
|
||||
<div className="inline-flex items-center gap-3">
|
||||
<Link
|
||||
href="/docs"
|
||||
className={cn(buttonVariants({ size: 'lg', className: 'rounded-full' }))}
|
||||
>
|
||||
Getting Started
|
||||
</Link>
|
||||
<a
|
||||
href="https://stackblitz.com/github/fuma-nama/fumadocs/tree/main/examples/stackblitz"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'lg',
|
||||
variant: 'outline',
|
||||
className: 'rounded-full bg-fd-background',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
Open Demo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
const owner = 'fuma-nama';
|
||||
const repo = 'fumadocs';
|
||||
const versionPattern = /^16\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
||||
|
||||
interface GitObjectRef {
|
||||
type: 'commit' | 'tag';
|
||||
sha: string;
|
||||
}
|
||||
|
||||
interface GitRefResponse {
|
||||
object: GitObjectRef;
|
||||
}
|
||||
|
||||
interface GitTagResponse {
|
||||
object: {
|
||||
sha: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface VercelDeployment {
|
||||
uid: string;
|
||||
url: string;
|
||||
alias?: string[];
|
||||
createdAt: number;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
interface VercelDeploymentsResponse {
|
||||
deployments?: VercelDeployment[];
|
||||
}
|
||||
|
||||
interface GitHubRequestOptions {
|
||||
allowNotFound?: boolean;
|
||||
}
|
||||
|
||||
const tagCommitCache = new Map<string, string | null>();
|
||||
const deploymentCache = new Map<string, VercelDeployment | null>();
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const version = url.searchParams.get('version');
|
||||
|
||||
if (!version) {
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Missing query parameter: version',
|
||||
expected: '16.x.x',
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!versionPattern.test(version)) {
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Only fumadocs-ui versions around 16.x.x are supported',
|
||||
received: version,
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const tag = `fumadocs-ui@${version}`;
|
||||
|
||||
try {
|
||||
const commitSha = await resolveTagCommitSha(tag);
|
||||
|
||||
if (!commitSha) {
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Tag not found on GitHub',
|
||||
tag,
|
||||
},
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const deployment = await findProductionDeployment(commitSha);
|
||||
|
||||
if (!deployment) {
|
||||
return Response.json(
|
||||
{
|
||||
error: 'No production deployment found for this tag commit',
|
||||
version,
|
||||
tag,
|
||||
commitSha,
|
||||
},
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const deploymentUrl = deployment.alias?.[0] ?? `https://${deployment.url}`;
|
||||
|
||||
return Response.json({
|
||||
version,
|
||||
tag,
|
||||
commitSha,
|
||||
deploymentUrl,
|
||||
deployment: {
|
||||
id: deployment.uid,
|
||||
createdAt: deployment.createdAt,
|
||||
target: deployment.target ?? 'production',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to resolve deployment URL',
|
||||
details: message,
|
||||
},
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveTagCommitSha(tag: string): Promise<string | null> {
|
||||
const cached = getCached(tagCommitCache, tag);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const ref = await githubRequest<GitRefResponse>(`/git/ref/tags/${encodeURIComponent(tag)}`, {
|
||||
allowNotFound: true,
|
||||
});
|
||||
|
||||
if (!ref) {
|
||||
setCache(tagCommitCache, tag, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ref.object.type === 'commit') {
|
||||
setCache(tagCommitCache, tag, ref.object.sha);
|
||||
return ref.object.sha;
|
||||
}
|
||||
|
||||
const annotatedTag = await githubRequest<GitTagResponse>(`/git/tags/${ref.object.sha}`);
|
||||
const resolvedSha = annotatedTag?.object.sha ?? null;
|
||||
setCache(tagCommitCache, tag, resolvedSha);
|
||||
return resolvedSha;
|
||||
}
|
||||
|
||||
async function githubRequest<T>(path: string, options?: GitHubRequestOptions): Promise<T | null> {
|
||||
const headers = new Headers({
|
||||
Accept: 'application/vnd.github+json',
|
||||
'User-Agent': 'fumadocs-docs-route',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
});
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}${path}`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (response.status === 404 && options?.allowNotFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API request failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
async function findProductionDeployment(commitSha: string): Promise<VercelDeployment | null> {
|
||||
const cached = getCached(deploymentCache, commitSha);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const token = process.env.VERCEL_TOKEN;
|
||||
const projectId = process.env.VERCEL_PROJECT_ID;
|
||||
const teamId = process.env.VERCEL_TEAM_ID;
|
||||
|
||||
if (!token || !projectId) {
|
||||
throw new Error('Missing VERCEL_TOKEN or VERCEL_PROJECT_ID environment variable');
|
||||
}
|
||||
|
||||
const url = new URL('https://api.vercel.com/v6/deployments');
|
||||
url.searchParams.set('projectId', projectId);
|
||||
url.searchParams.set('target', 'production');
|
||||
url.searchParams.set('limit', '1');
|
||||
url.searchParams.set('meta-githubCommitSha', commitSha);
|
||||
|
||||
if (teamId) {
|
||||
url.searchParams.set('teamId', teamId);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Vercel API request failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as VercelDeploymentsResponse;
|
||||
const deployment = data.deployments?.[0] ?? null;
|
||||
setCache(deploymentCache, commitSha, deployment);
|
||||
return deployment;
|
||||
}
|
||||
|
||||
function getCached<T>(cache: Map<string, T>, key: string): T | undefined {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
function setCache<T>(cache: Map<string, T>, key: string, value: T): void {
|
||||
cache.set(key, value);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { flexsearchFromSource } from 'fumadocs-core/search/flexsearch';
|
||||
|
||||
export const { GET } = flexsearchFromSource(source);
|
||||
@@ -17,7 +17,7 @@ import { Card, Cards } from '@hanzo/docs-base-ui/components/card';
|
||||
import { getMDXComponents } from '@/components/mdx';
|
||||
import { Banner } from '@hanzo/docs-base-ui/components/banner';
|
||||
import { Installation } from '@/components/preview/installation';
|
||||
import { Customisation } from '@/components/preview/customisation';
|
||||
import { Customization } from '@/components/preview/customization';
|
||||
import {
|
||||
DocsBody,
|
||||
DocsPage,
|
||||
@@ -52,7 +52,7 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
|
||||
/>
|
||||
);
|
||||
|
||||
if (page.data.type === 'openapi') {
|
||||
if (page.type === 'openapi') {
|
||||
const { APIPage } = await import('@/components/api-page');
|
||||
if (!APIPage) {
|
||||
return (
|
||||
|
||||
@@ -94,8 +94,9 @@ html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
html:has(body[data-scroll-locked]) {
|
||||
scrollbar-gutter: auto;
|
||||
html > body[data-scroll-locked] {
|
||||
margin-right: 0px !important;
|
||||
--removed-body-scroll-bar-size: 0px !important;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -24,7 +24,7 @@ const geist = Geist({
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
const mono = Geist_Mono({
|
||||
const mono = JetBrains_Mono({
|
||||
variable: '--font-mono',
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import type { ImageResponseOptions } from '@takumi-rs/image-response';
|
||||
|
||||
export interface GenerateProps {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
const font = readFile('./lib/og/JetBrainsMono-Regular.ttf').then((data) => ({
|
||||
name: 'JetBrainsMono',
|
||||
data,
|
||||
weight: 400,
|
||||
}));
|
||||
const fontBold = readFile('./lib/og/JetBrainsMono-Bold.ttf').then((data) => ({
|
||||
name: 'JetBrainsMono',
|
||||
data,
|
||||
weight: 600,
|
||||
}));
|
||||
|
||||
export async function getImageResponseOptions(): Promise<ImageResponseOptions> {
|
||||
return {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
format: 'webp',
|
||||
fonts: await Promise.all([font, fontBold]),
|
||||
};
|
||||
}
|
||||
|
||||
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)">
|
||||
<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" />
|
||||
</filter>
|
||||
<linearGradient id="logo-iconGradient" gradientTransform="rotate(45)">
|
||||
<stop offset="45%" stopColor="black" />
|
||||
<stop offset="100%" stopColor="white" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
color: 'white',
|
||||
backgroundColor: 'rgb(10,10,10)',
|
||||
fontFamily: 'JetBrainsMono',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '4rem',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: '76px',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<p
|
||||
style={{
|
||||
fontSize: '48px',
|
||||
color: 'rgba(240,240,240,0.7)',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '24px',
|
||||
marginTop: 'auto',
|
||||
color: primaryTextColor,
|
||||
}}
|
||||
>
|
||||
{logo}
|
||||
<span
|
||||
style={{
|
||||
fontSize: '46px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{siteName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const url = (path: string): string => new URL(path, baseUrl).toString();
|
||||
const items = await Promise.all(
|
||||
source.getPages().map(async (page) => {
|
||||
if (page.data.type === 'openapi') return;
|
||||
if (page.type === 'openapi') return;
|
||||
const { lastModified } = await page.data.load();
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const pages = source.getPages();
|
||||
const promises = pages.map(async (page) => {
|
||||
if (page.type === 'openapi') return;
|
||||
|
||||
const items = getBreadcrumbItems(page.url, source.getPageTree(), {
|
||||
includePage: false,
|
||||
includeRoot: true,
|
||||
});
|
||||
|
||||
return {
|
||||
id: page.url,
|
||||
structured: (await page.data.load()).structuredData,
|
||||
tag: getSection(page.slugs[0]),
|
||||
url: page.url,
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
breadcrumbs: items.flatMap<string>((item, i) =>
|
||||
i > 0 && typeof item.name === 'string' ? item.name : [],
|
||||
),
|
||||
} as OramaDocument;
|
||||
});
|
||||
|
||||
return Response.json(
|
||||
(await Promise.all(promises)).filter((v) => v !== undefined) as OramaDocument[],
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
preload = ["./scripts/preload.ts"]
|
||||
+24
-4
@@ -15,10 +15,20 @@ import { Loader2, MessageCircleIcon, RefreshCw, SearchIcon, Send, X } from 'luci
|
||||
import { cn } from '@/lib/cn';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { useChat, type UseChatHelpers } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport, type Tool, type UIToolInvocation } from 'ai';
|
||||
import { DefaultChatTransport, type Tool, type UIMessage, type UIToolInvocation } from 'ai';
|
||||
import { Markdown } from '../markdown';
|
||||
import { Presence } from '@radix-ui/react-presence';
|
||||
import type { ChatUIMessage, SearchTool } from '@/lib/openrouter/server';
|
||||
|
||||
export type ChatUIMessage = UIMessage<
|
||||
never,
|
||||
{
|
||||
client: {
|
||||
location: string;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
export type SearchTool = Tool<{ query: string; limit: number }>;
|
||||
|
||||
const Context = createContext<{
|
||||
open: boolean;
|
||||
@@ -372,8 +382,10 @@ export function AISearchPanel() {
|
||||
</style>
|
||||
<Presence present={open}>
|
||||
<div
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
className="fixed inset-0 z-30 backdrop-blur-xs bg-fd-overlay data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out lg:hidden"
|
||||
className={cn(
|
||||
'fixed inset-0 z-30 backdrop-blur-xs bg-fd-overlay lg:hidden',
|
||||
open ? 'animate-fd-fade-in' : 'animate-fd-fade-out',
|
||||
)}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
</Presence>
|
||||
@@ -425,6 +437,14 @@ export function AISearchPanelList({ className, style, ...props }: ComponentProps
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col px-3 gap-4">
|
||||
{chat.error && (
|
||||
<div className="p-2 bg-fd-secondary text-fd-secondary-foreground border rounded-lg">
|
||||
<p className="text-xs text-fd-muted-foreground mb-1">
|
||||
Request Failed: {chat.error.name}
|
||||
</p>
|
||||
<p className="text-sm">{chat.error.message}</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((item) => (
|
||||
<Message key={item.id} message={item} />
|
||||
))}
|
||||
@@ -21,7 +21,7 @@ import type { z } from 'zod';
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
import { Markdown } from '../markdown';
|
||||
import { Presence } from '@radix-ui/react-presence';
|
||||
import type { InkeepUIMessage } from '@/lib/inkeep/server';
|
||||
import type { InkeepUIMessage } from '@/lib/inkeep/route';
|
||||
|
||||
const Context = createContext<{
|
||||
open: boolean;
|
||||
@@ -372,8 +372,10 @@ export function AISearchPanel() {
|
||||
</style>
|
||||
<Presence present={open}>
|
||||
<div
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
className="fixed inset-0 z-30 backdrop-blur-xs bg-fd-overlay data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out lg:hidden"
|
||||
className={cn(
|
||||
'fixed inset-0 z-30 backdrop-blur-xs bg-fd-overlay lg:hidden',
|
||||
open ? 'animate-fd-fade-in' : 'animate-fd-fade-out',
|
||||
)}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
</Presence>
|
||||
|
||||
@@ -3,10 +3,12 @@ import { cn } from '@/lib/cn';
|
||||
import { buttonVariants } from '@hanzo/docs-base-ui/components/ui/button';
|
||||
import { CornerDownRightIcon, MessageSquare, ThumbsDown, ThumbsUp } from 'lucide-react';
|
||||
import {
|
||||
ReactNode,
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
type SyntheticEvent,
|
||||
useEffect,
|
||||
useEffectEvent,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from 'react';
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
type PageFeedback,
|
||||
} from './schema';
|
||||
import { z } from 'zod/mini';
|
||||
import { usePathname } from 'fumadocs-core/framework';
|
||||
|
||||
const rateButtonVariants = cva(
|
||||
'inline-flex items-center gap-2 px-3 py-2 rounded-full font-medium border text-sm [&_svg]:size-4 disabled:cursor-not-allowed',
|
||||
@@ -67,7 +70,7 @@ export function Feedback({
|
||||
|
||||
startTransition(async () => {
|
||||
const feedback: PageFeedback = {
|
||||
url,
|
||||
url: location.href,
|
||||
opinion,
|
||||
message,
|
||||
};
|
||||
@@ -189,19 +192,194 @@ export function Feedback({
|
||||
);
|
||||
}
|
||||
|
||||
export interface FeedbackTextProps {
|
||||
onSendAction: (feedback: BlockFeedback) => Promise<ActionResponse>;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A feedback component for each content block in page, should be used with `remark-feedback-block`.
|
||||
*
|
||||
* See https://hanzoai.github.io/docs/docs/integrations/feedback.
|
||||
*/
|
||||
export function FeedbackBlock({
|
||||
id,
|
||||
body,
|
||||
export function FeedbackText({ onSendAction, children }: FeedbackTextProps) {
|
||||
const [popup, _setPopup] = useState<{
|
||||
mode: 'tooltip' | 'expanded';
|
||||
blockId: string;
|
||||
selection: string;
|
||||
range: Range;
|
||||
} | null>(null);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
open: popup !== null,
|
||||
placement: 'bottom',
|
||||
middleware: [offset(6), flip(), shift({ padding: 8 })],
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
function expandPopup() {
|
||||
if (popup?.mode !== 'tooltip') return;
|
||||
|
||||
const highlight = new Highlight(popup.range);
|
||||
CSS.highlights.set('fd-feedback-text', highlight);
|
||||
|
||||
_setPopup({ ...popup, mode: 'expanded' });
|
||||
}
|
||||
|
||||
function closePopup() {
|
||||
if (popup?.mode === 'expanded') {
|
||||
CSS.highlights.delete('fd-feedback-text');
|
||||
}
|
||||
|
||||
_setPopup(null);
|
||||
}
|
||||
|
||||
const updateSelectionPopover = useEffectEvent(() => {
|
||||
if (popup && popup.mode === 'expanded') return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (!container || !selection || selection.isCollapsed || selection.rangeCount === 0) {
|
||||
closePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0).cloneRange();
|
||||
if (!container.contains(range.commonAncestorContainer)) {
|
||||
closePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const selectionText = selection.toString().trim();
|
||||
// also prevent cross-paragraph selection
|
||||
if (selectionText.length === 0 || selectionText.includes('\n')) {
|
||||
closePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const element =
|
||||
range.startContainer instanceof Element
|
||||
? range.startContainer
|
||||
: range.startContainer.parentElement;
|
||||
const blockId = element?.closest('[data-block="feedback"]')?.id;
|
||||
if (!blockId) {
|
||||
closePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
refs.setReference({
|
||||
getBoundingClientRect() {
|
||||
return range.getBoundingClientRect();
|
||||
},
|
||||
contextElement: container,
|
||||
});
|
||||
|
||||
_setPopup({ mode: 'tooltip', range, selection: selectionText, blockId });
|
||||
});
|
||||
|
||||
const closeOnEscape = useEffectEvent((event: KeyboardEvent) => {
|
||||
if (popup === null) return;
|
||||
if (event.key === 'Escape') closePopup();
|
||||
});
|
||||
|
||||
const closeOnPointerDown = useEffectEvent((event: PointerEvent) => {
|
||||
const target = event.target;
|
||||
if (popup === null || !(target instanceof Node)) return;
|
||||
|
||||
if (
|
||||
refs.floating.current?.contains(target) ||
|
||||
(popup.mode === 'tooltip' && containerRef.current?.contains(target))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
closePopup();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let frame: number | null = null;
|
||||
|
||||
function scheduleSelectionPopover() {
|
||||
if (frame !== null) window.cancelAnimationFrame(frame);
|
||||
|
||||
frame = window.requestAnimationFrame(() => {
|
||||
frame = null;
|
||||
updateSelectionPopover();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('selectionchange', scheduleSelectionPopover);
|
||||
document.addEventListener('keydown', closeOnEscape);
|
||||
document.addEventListener('pointerdown', closeOnPointerDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', closeOnEscape);
|
||||
document.removeEventListener('pointerdown', closeOnPointerDown);
|
||||
document.removeEventListener('selectionchange', scheduleSelectionPopover);
|
||||
if (frame !== null) window.cancelAnimationFrame(frame);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="prose-no-margin [&_::highlight(fd-feedback-text)]:bg-fd-primary [&_::highlight(fd-feedback-text)]:text-fd-primary-foreground"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{popup && (
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className={cn(
|
||||
'not-prose z-40 text-sm bg-fd-popover text-fd-popover-foreground border overflow-hidden shadow-lg rounded-xl w-30 h-9.5 box-content transition-[width,height]',
|
||||
popup.mode === 'expanded' ? 'w-[300px] h-32 max-w-[98vw]' : 'select-none',
|
||||
)}
|
||||
style={floatingStyles}
|
||||
>
|
||||
{popup.mode === 'tooltip' ? (
|
||||
<div className="w-30 h-9.5 p-1">
|
||||
<button
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost', size: 'sm' }),
|
||||
'size-full gap-1.5',
|
||||
)}
|
||||
onClick={expandPopup}
|
||||
>
|
||||
<CornerDownRightIcon className="size-4 text-fd-muted-foreground" />
|
||||
Feedback
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<FeedbackTextForm
|
||||
blockId={popup.blockId}
|
||||
selection={popup.selection}
|
||||
onSendAction={onSendAction}
|
||||
onClose={closePopup}
|
||||
container={{ className: 'p-2 w-[300px] h-32 max-w-[98vw] animate-fd-fade-in' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FeedbackTextForm({
|
||||
blockId,
|
||||
selection,
|
||||
onSendAction,
|
||||
children,
|
||||
}: FeedbackBlockProps & {
|
||||
onClose,
|
||||
container,
|
||||
}: {
|
||||
container: HTMLAttributes<HTMLElement>;
|
||||
blockId: string;
|
||||
selection: string;
|
||||
onSendAction: (feedback: BlockFeedback) => Promise<ActionResponse>;
|
||||
children: ReactNode;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const url = usePathname() ?? '/';
|
||||
const blockId = `${url}-${id}`;
|
||||
@@ -212,14 +390,13 @@ export function FeedbackBlock({
|
||||
});
|
||||
const [message, setMessage] = useState('');
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
function submit(e?: SyntheticEvent) {
|
||||
startTransition(async () => {
|
||||
const feedback: BlockFeedback = {
|
||||
blockId,
|
||||
blockBody: body,
|
||||
url,
|
||||
blockBody: selection,
|
||||
url: location.href,
|
||||
message,
|
||||
};
|
||||
|
||||
@@ -234,103 +411,91 @@ export function FeedbackBlock({
|
||||
e?.preventDefault();
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<div className="relative group/feedback">
|
||||
<div
|
||||
className={cn(
|
||||
'absolute -inset-1 rounded-sm pointer-events-none transition-colors duration-100 z-[-1]',
|
||||
open
|
||||
? 'bg-fd-accent'
|
||||
: 'group-hover/feedback:bg-fd-accent group-hover/feedback:delay-100',
|
||||
)}
|
||||
/>
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'secondary', size: 'sm' }),
|
||||
'absolute -top-7 end-0 backdrop-blur-sm text-fd-muted-foreground gap-1.5 transition-all duration-100 data-[state=open]:bg-fd-accent data-[state=open]:text-fd-accent-foreground',
|
||||
!open &&
|
||||
'opacity-0 pointer-events-none group-hover/feedback:pointer-events-auto group-hover/feedback:opacity-100 group-hover/feedback:delay-100 hover:pointer-events-auto hover:opacity-100 hover:delay-100',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
setOpen((prev) => !prev);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<MessageSquare className="size-3.5" />
|
||||
Feedback
|
||||
</PopoverTrigger>
|
||||
|
||||
<div className="in-[.prose-no-margin]:prose-no-margin">{children}</div>
|
||||
</div>
|
||||
|
||||
<PopoverContent className="min-w-[300px] bg-fd-card text-fd-card-foreground">
|
||||
{previous ? (
|
||||
<div className="flex flex-col items-center py-2 gap-2 text-fd-muted-foreground text-sm text-center rounded-xl">
|
||||
<p>Thank you for your feedback!</p>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<a
|
||||
href={previous.response?.githubUrl}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'primary',
|
||||
}),
|
||||
'text-xs',
|
||||
)}
|
||||
>
|
||||
View on GitHub
|
||||
</a>
|
||||
|
||||
<button
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
}),
|
||||
'text-xs',
|
||||
)}
|
||||
onClick={() => {
|
||||
setPrevious(null);
|
||||
}}
|
||||
>
|
||||
Submit Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form className="flex flex-col gap-2" onSubmit={submit}>
|
||||
<textarea
|
||||
autoFocus
|
||||
required
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="border rounded-lg bg-fd-secondary text-fd-secondary-foreground p-3 resize-none focus-visible:outline-none placeholder:text-fd-muted-foreground"
|
||||
placeholder="Leave your feedback..."
|
||||
onKeyDown={(e) => {
|
||||
if (!e.shiftKey && e.key === 'Enter') {
|
||||
submit(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={cn(buttonVariants({ variant: 'secondary', size: 'sm' }), 'gap-1.5')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<CornerDownRightIcon className="text-fd-muted-foreground size-4" />
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
if (previous)
|
||||
return (
|
||||
<div
|
||||
{...container}
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center gap-2 text-fd-muted-foreground text-center',
|
||||
container.className,
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
>
|
||||
<p>Thank you for your feedback!</p>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<a
|
||||
href={previous.response?.githubUrl}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'primary',
|
||||
}),
|
||||
'text-xs',
|
||||
)}
|
||||
>
|
||||
View on GitHub
|
||||
</a>
|
||||
|
||||
<button
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
}),
|
||||
'text-xs',
|
||||
)}
|
||||
onClick={() => {
|
||||
setPrevious(null);
|
||||
}}
|
||||
>
|
||||
Submit Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
{...container}
|
||||
className={cn('flex flex-col gap-2', container.className)}
|
||||
onSubmit={submit}
|
||||
>
|
||||
<textarea
|
||||
autoFocus
|
||||
required
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="border rounded-lg bg-fd-secondary text-fd-secondary-foreground p-3 resize-none focus-visible:outline-none placeholder:text-fd-muted-foreground"
|
||||
placeholder="Leave your feedback..."
|
||||
onKeyDown={(e) => {
|
||||
if (!e.shiftKey && e.key === 'Enter') {
|
||||
submit(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2 mt-auto">
|
||||
<button
|
||||
type="submit"
|
||||
className={cn(buttonVariants({ variant: 'primary', size: 'sm' }), 'gap-1.5')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<CornerDownRightIcon className="size-4" />
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(buttonVariants({ variant: 'secondary', size: 'sm' }), 'gap-1.5')}
|
||||
disabled={isPending}
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function useSubmissionStorage<Result>(blockId: string, validate: (v: unknown) => Result | null) {
|
||||
const storageKey = `docs-feedback-${blockId}`;
|
||||
function useSubmissionStorage<Result>(key: string, validate: (v: unknown) => Result | null) {
|
||||
const storageKey = `docs-feedback-${key}`;
|
||||
const [value, setValue] = useState<Result | null>(null);
|
||||
const validateCallback = useEffectEvent(validate);
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { z } from 'zod/mini';
|
||||
|
||||
export const blockFeedback = z.object({
|
||||
/** full URL of page where fired */
|
||||
url: z.string(),
|
||||
blockId: z.string(),
|
||||
message: z.string(),
|
||||
|
||||
/** the referenced text of block */
|
||||
blockBody: z.optional(z.string()),
|
||||
blockBody: z.string(),
|
||||
});
|
||||
|
||||
export const pageFeedback = z.object({
|
||||
opinion: z.enum(['good', 'bad']),
|
||||
/** full URL of page where fired */
|
||||
url: z.string(),
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { ServerCodeBlock } from '@hanzo/docs-ui/components/codeblock.rsc';
|
||||
|
||||
export function Customisation() {
|
||||
export function Customization() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-3 rounded-xl border bg-fd-card text-fd-card-foreground not-prose">
|
||||
<p className="font-medium text-sm">Install via Hanzo Docs CLI</p>
|
||||
<p className="text-fd-muted-foreground text-sm">
|
||||
For advanced customisation that supported options cannot suffice.
|
||||
For advanced customization that supported options cannot suffice.
|
||||
</p>
|
||||
<ServerCodeBlock code="npx @hanzo/docs-cli@latest customise" lang="bash" />
|
||||
</div>
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
InlineTOC,
|
||||
} from '@/components/preview/lazy';
|
||||
import BannerImage from '@/public/banner.png';
|
||||
import { GraphView } from '@/components/preview/lazy';
|
||||
import { buildGraph } from '@/lib/build-graph';
|
||||
|
||||
export function heading(): ReactNode {
|
||||
return (
|
||||
@@ -227,7 +229,7 @@ export function banner(): ReactNode {
|
||||
]}
|
||||
changeLayout={false}
|
||||
>
|
||||
customise the <code>rainbow</code> variant
|
||||
customize the <code>rainbow</code> variant
|
||||
</Banner>
|
||||
</div>
|
||||
</Wrapper>
|
||||
@@ -246,3 +248,7 @@ export function githubInfo() {
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export async function graphView() {
|
||||
return <GraphView graph={await buildGraph()} />;
|
||||
}
|
||||
|
||||
@@ -5,22 +5,9 @@ export function Installation({ name }: { name: string }) {
|
||||
const tabs = [{ name: 'Hanzo Docs CLI', value: 'hanzo-docs-cli' }];
|
||||
|
||||
return (
|
||||
<Tabs className="my-6">
|
||||
<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>
|
||||
</div>
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className="font-medium text-fd-muted-foreground transition-colors data-[state=active]:text-fd-primary"
|
||||
>
|
||||
{tab.name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<div className="p-3 border rounded-xl bg-fd-card text-fd-card-foreground my-6 text-sm not-prose">
|
||||
<p className="font-medium">Install to your codebase</p>
|
||||
<p className="mt-1 mb-4 text-fd-muted-foreground">Easier customization & control.</p>
|
||||
|
||||
<TabsContent value="hanzo-docs-cli">
|
||||
<ServerCodeBlock code={`npx @hanzo/docs-cli@latest add ${name}`} lang="bash" />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import * as path from 'node:path';
|
||||
import { resolveFromRemote } from '@hanzo/docs-cli/build';
|
||||
|
||||
const baseDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../');
|
||||
const baseDir = path.join(import.meta.dirname, '../../');
|
||||
|
||||
export const registry: Registry = {
|
||||
dir: baseDir,
|
||||
@@ -17,28 +17,63 @@ export const registry: Registry = {
|
||||
// source object is external
|
||||
if (filePath.startsWith('lib/source/')) return false;
|
||||
},
|
||||
onResolve(ref) {
|
||||
onParseReference(ref) {
|
||||
if (ref.type === 'unknown' && ref.specifier === 'hast') {
|
||||
return {
|
||||
type: 'dependency',
|
||||
dep: '@types/hast',
|
||||
specifier: 'hast',
|
||||
};
|
||||
}
|
||||
|
||||
if (ref.type === 'file') {
|
||||
const file = path.relative(baseDir, ref.file);
|
||||
let file = path.relative(baseDir, ref.file);
|
||||
|
||||
if (file === 'lib/cn.ts') {
|
||||
return resolveFromRemote(radixUi.registry, 'cn', () => true)!;
|
||||
return {
|
||||
type: 'file',
|
||||
file: path.join(radixUi.registry.dir, 'utils/cn.ts'),
|
||||
};
|
||||
}
|
||||
|
||||
file = path.relative(radixUi.registry.dir, ref.file);
|
||||
if (file.startsWith('contexts/') || file.startsWith('utils/use-')) {
|
||||
return {
|
||||
dep: 'fumadocs-ui',
|
||||
type: 'dependency',
|
||||
specifier: `fumadocs-ui/${removeExtname(file)}`,
|
||||
};
|
||||
}
|
||||
|
||||
file = path.relative(baseUi.registry.dir, ref.file);
|
||||
if (file.startsWith('contexts/') || file.startsWith('utils/use-')) {
|
||||
return {
|
||||
dep: '@fumadocs/base-ui',
|
||||
type: 'dependency',
|
||||
specifier: `@fumadocs/base-ui/${removeExtname(file)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (ref.type === 'dependency' && ref.dep === '@hanzo/docs-ui') {
|
||||
const match = /@hanzo\/docs-ui\/components\/ui\/(.*)/.exec(ref.specifier);
|
||||
if (match) {
|
||||
return resolveFromRemote(
|
||||
radixUi.registry,
|
||||
match[1],
|
||||
(file) => path.basename(file.path, path.extname(file.path)) === match[1],
|
||||
)!;
|
||||
return {
|
||||
type: 'file',
|
||||
file: path.join(radixUi.registry.dir, `components/ui/${match[1]}.tsx`),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return ref;
|
||||
},
|
||||
};
|
||||
|
||||
export const registry: Registry = {
|
||||
dir: baseDir,
|
||||
name: 'fumadocs',
|
||||
subRegistries: [radixUi.registry, baseUi.registry, sanity.registry],
|
||||
|
||||
components: [
|
||||
{
|
||||
name: 'layouts/docs-min',
|
||||
@@ -90,19 +125,40 @@ export const registry: Registry = {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ai/openrouter',
|
||||
title: 'AI Chat (Next.js + OpenRouter)',
|
||||
description: 'Ask AI dialog for your docs, requires OPENROUTER_API_KEY',
|
||||
name: 'ai/shared',
|
||||
unlisted: true,
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
path: 'components/openrouter/search.tsx',
|
||||
path: 'components/ai-sdk/search.tsx',
|
||||
target: '<dir>/ai/search.tsx',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ai/openrouter',
|
||||
title: 'AI Chat (AI SDK)',
|
||||
description: 'Ask AI dialog for your docs, default using OpenRouter',
|
||||
files: [
|
||||
{
|
||||
type: 'route',
|
||||
path: 'lib/openrouter/server.ts',
|
||||
target: 'app/api/chat/route.ts',
|
||||
type: 'route-handler',
|
||||
route: 'api/chat',
|
||||
path: 'lib/openrouter/route.ts',
|
||||
},
|
||||
],
|
||||
dependencies: {
|
||||
flexsearch: '^0.8.212',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ai/llmgateway',
|
||||
title: 'AI Chat (LLMGateway)',
|
||||
description: 'Ask AI dialog for your docs, using LLMGateway',
|
||||
files: [
|
||||
{
|
||||
type: 'route-handler',
|
||||
route: 'api/chat',
|
||||
path: 'lib/llmgateway/route.ts',
|
||||
},
|
||||
],
|
||||
dependencies: {
|
||||
@@ -143,7 +199,7 @@ export const registry: Registry = {
|
||||
},
|
||||
{
|
||||
name: 'og/mono',
|
||||
description: 'Open graph image generation (mono-style)',
|
||||
description: 'Open graph image generation - mono style',
|
||||
files: [
|
||||
{
|
||||
type: 'lib',
|
||||
@@ -171,3 +227,7 @@ export const registry: Registry = {
|
||||
react: null,
|
||||
},
|
||||
};
|
||||
|
||||
function removeExtname(file: string) {
|
||||
return file.slice(0, -path.extname(file).length);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ date: 2025-08-22
|
||||
The early mistake of Fumadocs was **Fumadocs CLI being too late and not getting enough mentions on docs**.
|
||||
|
||||
To push the CLI approach further like the recent Shadcn CLI support, there are changes we have to make.
|
||||
Early adopters will face problems if their customisations went beyond what our abstraction can mitigate.
|
||||
Early adopters will face problems if their customizations went beyond what our abstraction can mitigate.
|
||||
|
||||
**For example:** A small CSS change can still break hacky CSS customisations. We can only ensure backward compatibility for the ones scoped by CSS selectors, or exposed options:
|
||||
**For example:** A small CSS change can still break hacky CSS customizations. We can only ensure backward compatibility for the ones scoped by CSS selectors, or exposed options:
|
||||
|
||||
```css
|
||||
#nd-sidebar {
|
||||
@@ -24,13 +24,13 @@ Early adopters will face problems if their customisations went beyond what our a
|
||||
For CLI to work better, we need layout components to be entirely independent for modularization. A ideal state is that:
|
||||
|
||||
- Normal users can use exposed options, enjoying UI iterations from upstream.
|
||||
- Advanced customisations can use Fumadocs CLI, avoiding upstream changes.
|
||||
- Advanced customizations can use Fumadocs CLI, avoiding upstream changes.
|
||||
|
||||
Same as the solution described in [this post about UI Customisation](/blog/customise-ui#the-solution).
|
||||
Same as the solution described in [this post about UI Customization](/blog/customise-ui#the-solution).
|
||||
|
||||
### A Good Example
|
||||
|
||||
For lighter customisations like nuqs did, a PR to migrate from old versions [#1056](https://github.com/47ng/nuqs/pull/1056) can reduce code on user land and migration is simple.
|
||||
For lighter customizations like nuqs did, a PR to migrate from old versions [#1056](https://github.com/47ng/nuqs/pull/1056) can reduce code on user land and migration is simple.
|
||||
|
||||
### The Problems
|
||||
|
||||
@@ -64,7 +64,7 @@ Using it properly can already reduce a massive amount of work, we have [a specs
|
||||
|
||||
#### CSS Variables being Neglected
|
||||
|
||||
Fumadocs is using CSS variables for all colors since the beginning, the naming is identical to Shadcn UI which makes customisation easy.
|
||||
Fumadocs is using CSS variables for all colors since the beginning, the naming is identical to Shadcn UI which makes customization easy.
|
||||
|
||||
But I noticed some are overriding the Tailwind CSS **classes** rather than **colors**, like:
|
||||
|
||||
@@ -86,7 +86,7 @@ Now we also have a Shadcn UI preset to reuse your colors from main app.
|
||||
|
||||
### Early Reflections
|
||||
|
||||
I think the CLI approach is correct, it'll be the primary way for advanced customisation.
|
||||
I think the CLI approach is correct, it'll be the primary way for advanced customization.
|
||||
|
||||
My future plans:
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Rethink how we customize UI
|
||||
description: Deep dive on how Fumadocs UI is designed
|
||||
author: Fuma Nama
|
||||
date: 2025-04-15
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
Making a docs framework isn't challenging, the hardest problem is how I can make it perfect for everyone, from the perspective of users and developers.
|
||||
Undoubtedly, this is **a dilemma** for all frameworks, or even library authors — You cannot write generic code that works out-of-the-box, there's no piece of software that is straight usable while being generic.
|
||||
|
||||
For example, headless UI libraries like **Radix UI** gave us the concept of generic UI components: bare UI primitives that we can customize according to our aesthetics.
|
||||
However, you'll need time to tune them, these primitives are unusable without further customizations.
|
||||
|
||||
Yep it's 2025, **Shadcn UI** had led a "revolution" on Web Development. It's a great combination of headless UI and styled defaults that takes an extra step away!
|
||||
But our problem remains unsolved, it might be preposterous that it becomes "unstyled" again when a vast majority of new sites adopted Shadcn UI.
|
||||
|
||||
Personally, I felt overwhelmed by nearly identical, ubiquitous flat designs that are obviously one of the default Shadcn UI styles. It has nothing to do with Shadcn UI, but the rest of developers who don't actually care about aesthetics and uniqueness.
|
||||
Eventually, **Shadcn-like design became the default styles**, so once again, we will need to do a bit more to make it look stylish.
|
||||
|
||||
### What should we do?
|
||||
|
||||
As a docs framework started from **"being flexible"**, Fumadocs must be customisable, tuned to meet developers' preferences.
|
||||
We will need to take customization seriously if DX is what we aim. And most importantly, **it should encourage developers with a sense of beauty to customize.**
|
||||
|
||||
## The Solution
|
||||
|
||||
Typically, there are two types of developers:
|
||||
|
||||
### those who want docs that just work
|
||||
|
||||
Fumadocs need to be opinionated by default such that it is immediately usable, and we only have to offer simplest options to change details. (e.g. colors, overriding components).
|
||||
|
||||
Most developers have no desire to install dozens of components just to change a tiny detail, which makes the Shadcn approach suboptimal.
|
||||
|
||||
### those who need the perfect docs tailored to their preferences
|
||||
|
||||
The kernel of Fumadocs, or the spirit we're pursuing is the latter one. I crafted Fumadocs CLI, a version of Shadcn UI dedicated for Fumadocs, the whole solution is now simplified into a single command:
|
||||
|
||||
```package-install
|
||||
npx @fumadocs/cli customize
|
||||
```
|
||||
|
||||
It accounts for two types:
|
||||
|
||||
- Customize the default styles when the exposed options are inadequate.
|
||||
- Roll their own layout from scratch.
|
||||
|
||||
The first one is self-evident, people will always have absurd needs that we have not even considered.
|
||||
We can install the original components, so it's simply identical to the default UI which they've been using.
|
||||
|
||||
The latter one is simpler, we can offer the minimal template they can start customising from.
|
||||
|
||||
This methodology, inspired by ejection, brings some nice advantages:
|
||||
|
||||
#### Don't break anything when you upgrade Fumadocs
|
||||
|
||||
Fumadocs need to be opinionated. Hence, we need to iterate the UI frequently which apparently will break people's hacky customizations.
|
||||
The charm of Fumadocs CLI is that once you've installed the layout, it stays the same forever regardless of future iterations on Fumadocs UI.
|
||||
|
||||
It gives the courage for Fumadocs UI to iterate, and developers to customize unburnt by the fear of upgrading dependencies.
|
||||
|
||||
#### Looks Clean
|
||||
|
||||
Just like Shadcn UI and Radix UI, Fumadocs UI has a core package (`fumadocs-core`), most generic logic will be abstracted by core.
|
||||
|
||||
## What is Next?
|
||||
|
||||
The innovation of Shadcn UI is amazing, yet, I believe there's more problems Fumadocs can solve.
|
||||
|
||||
See [Flags SDK](https://flags-sdk.dev/frameworks/next) and [BetterAuth](https://www.better-auth.com/docs/introduction), you can't tell at the first glance if they're powered by Fumadocs.
|
||||
I hope it can continue to be a "breakable" framework known for how customisable it is.
|
||||
@@ -0,0 +1,221 @@
|
||||
---
|
||||
title: Fumadocs MDX v10 Summary
|
||||
description: Migration Guide and Summary
|
||||
date: 2024-11-24
|
||||
author: Fuma Nama
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- More customizations via `source.config.ts`, a new config file for your content.
|
||||
- Introduced **collections**.
|
||||
- Turbopack support.
|
||||
- Build-time data validation support.
|
||||
|
||||
## Migrate from v9
|
||||
|
||||
### Refactor Imports
|
||||
|
||||
Refactor the import in `next.config`.
|
||||
|
||||
**From:**
|
||||
|
||||
```ts
|
||||
import createMDX from 'fumadocs-mdx/config';
|
||||
|
||||
const withMDX = createMDX();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default withMDX(config);
|
||||
```
|
||||
|
||||
**To:**
|
||||
|
||||
```ts
|
||||
import { createMDX } from 'fumadocs-mdx/next';
|
||||
|
||||
const withMDX = createMDX();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default withMDX(config);
|
||||
```
|
||||
|
||||
### Remove `mdx-components.tsx`
|
||||
|
||||
`mdx-components.tsx` is no longer used. It now allows only MDX components passed from `MDX` body's `components` prop.
|
||||
|
||||
```tsx
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
|
||||
const MDX = page.data.body;
|
||||
|
||||
// set your MDX components with `components` prop
|
||||
<MDX components={{ ...defaultMdxComponents }} />;
|
||||
```
|
||||
|
||||
This encourages explicit import of MDX components.
|
||||
|
||||
Previously, `mdx-components.tsx` worked by injecting an import to every compiled Markdown/MDX file.
|
||||
It's somewhat unnecessary because you can always import the components explicitly, or replace default HTML tags like `img` from MDX body's `components` prop.
|
||||
|
||||
### Define Collections
|
||||
|
||||
Collection is now introduced on source config file (`source.config.ts`).
|
||||
It refers to a collection of files/content, such as Markdown files or JSON/YAML files.
|
||||
|
||||
Every collection has its own config, you can have customized Zod schema to validate its data, or collection-level MDX options for content collections.
|
||||
|
||||
You can create a source config file, and add the following:
|
||||
|
||||
```ts
|
||||
import { defineDocs } from 'fumadocs-mdx/config';
|
||||
|
||||
export const { docs, meta } = defineDocs({
|
||||
dir: 'content/docs',
|
||||
});
|
||||
```
|
||||
|
||||
`defineDocs` declares two collections, one with `doc` type that scans content files (e.g. `md/mdx`), one with `meta` type that scans meta files (e.g. `json`).
|
||||
|
||||
Now you can generate types using `fumadocs-mdx` command, it's recommended to set it as a `postinstall` script.
|
||||
|
||||
```json title="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"postinstall": "fumadocs-mdx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When starting the development server, a `.source` folder will be generated, it contains all parsed collections/files.
|
||||
You can add it to `.gitignore`, it will replace the old `.map` file.
|
||||
|
||||
To access the collections, import them from the folder with their original name in `source.config.ts`.
|
||||
|
||||
```ts
|
||||
import { docs, meta } from '@/.source';
|
||||
```
|
||||
|
||||
Now to integrate it with Fumadocs framework, change your `source.ts` from:
|
||||
|
||||
```ts
|
||||
import { map } from '@/.map';
|
||||
import { createMDXSource } from 'fumadocs-mdx/runtime/next';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
|
||||
export const { getPage, getPages, pageTree } = loader({
|
||||
baseUrl: '/docs',
|
||||
rootDir: 'docs',
|
||||
source: createMDXSource(map),
|
||||
});
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```ts
|
||||
import { docs, meta } from '@/.source';
|
||||
import { createMDXSource } from 'fumadocs-mdx/runtime/next';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: '/docs',
|
||||
source: createMDXSource(docs, meta),
|
||||
});
|
||||
```
|
||||
|
||||
- we now recommend exporting the output of `loader` directly as a single variable.
|
||||
- schema option is no longer defined in `source.ts`, it's handled by `source.config.ts`.
|
||||
- it takes two collections: `docs` (content) and `meta` (`meta.json`).
|
||||
You can leave `meta` as an empty array if it is unused, so you can define only a collection for `docs`.
|
||||
|
||||
### `page.data`
|
||||
|
||||
When using with `loader`, you no longer need `data.frontmatter` to access frontmatter data.
|
||||
It is merged into the `page.data` object.
|
||||
|
||||
```ts
|
||||
page.data.frontmatter.title; // [!code --]
|
||||
page.data.title; // [!code ++]
|
||||
```
|
||||
|
||||
### MDX Options
|
||||
|
||||
Instead of passing them to `next.config` file, define a global config in `source.config.ts`:
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'fumadocs-mdx/config';
|
||||
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
// here!
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Collection Schema
|
||||
|
||||
The `schema` option of collection allow you to customize the validation schema, it accepts a Zod type.
|
||||
|
||||
For `defineDocs`, see [`schema`](/docs/mdx/collections#schema-1).
|
||||
|
||||
### Multiple Types
|
||||
|
||||
Same as before, you can call `loader` multiple times for different types (e.g. for docs and blog).
|
||||
|
||||
```ts
|
||||
import { createMDXSource } from 'fumadocs-mdx/runtime/next';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { meta, docs, blog as blogPosts } from '@/.source';
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: '/docs',
|
||||
source: createMDXSource(docs, meta),
|
||||
});
|
||||
|
||||
export const blog = loader({
|
||||
baseUrl: '/blog',
|
||||
// as mentioned before, you can leave `meta` an empty array
|
||||
source: createMDXSource(blogPosts, []),
|
||||
});
|
||||
|
||||
export type DocsPage = (typeof source)['$inferPage'];
|
||||
export type DocsMeta = (typeof source)['$inferMeta'];
|
||||
```
|
||||
|
||||
and the corresponding `source.config.ts`:
|
||||
|
||||
```ts
|
||||
import { defineDocs, defineCollections, frontmatterSchema } from 'fumadocs-mdx/config';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const { docs, meta } = defineDocs({
|
||||
dir: 'content/docs',
|
||||
});
|
||||
|
||||
export const blog = defineCollections({
|
||||
type: 'doc',
|
||||
dir: 'content/blog',
|
||||
schema: frontmatterSchema.extend({
|
||||
author: z.string(),
|
||||
date: z.string().date().or(z.date()).optional(),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Further Readings
|
||||
|
||||
You can read the latest docs of Fumadocs MDX for details.
|
||||
|
||||
<Cards>
|
||||
<Card href="/docs/mdx" title="Fumadocs MDX">
|
||||
The built-in content source, build a better content experience on Next.js.
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -181,7 +181,7 @@ By enabling Async or Dynamic modes, the relevant collections are considered lazy
|
||||
|
||||
### Updated Signature for `postInstall()`
|
||||
|
||||
It now accepts the framework plugin options, you can customise more properties:
|
||||
It now accepts the framework plugin options, you can customize more properties:
|
||||
|
||||
```ts tab="Before"
|
||||
import { postInstall } from '@hanzo/docs-mdx/next';
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Fumadocs MDX 15
|
||||
description: Bump minimum Fumadocs version.
|
||||
author: Fuma Nama
|
||||
date: 2026-05-07
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This is a boring major release mainly to reduce package size. Breaking changes:
|
||||
|
||||
- `fumadocs-core` must be `16.7.0` or above.
|
||||
- `next.config` must be ESM.
|
||||
|
||||
For Next.js users, if you encounter any error when upgrading, it could be due to ESM compatibility issues, you might be accidentally using CommonJS in `next.config`.
|
||||
Consider to rename your config to `next.config.mjs` or `next.config.mts`.
|
||||
|
||||
If you prefer to use ESM in your entire project, set this in your `package.json`:
|
||||
|
||||
```json title="package.json"
|
||||
{
|
||||
"name": "my-app",
|
||||
// [!code ++]
|
||||
"type": "module"
|
||||
}
|
||||
```
|
||||
|
||||
For other unexpected breakages, please open a bug report on our GitHub repository.
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Fumadocs OpenAPI v9
|
||||
description: Better UI & DX
|
||||
date: 2025-05-25
|
||||
author: Fuma Nama
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This breaking change is mainly made to address problems of `generateFiles()` function.
|
||||
|
||||
Previously, it was a simple function to generate multiple MDX files/pages from your OpenAPI schema, same as every docs generator.
|
||||
We tested it on public OpenAPI schemas such as Unkey, Vercel and examples from Scalar and Swagger's Museum schema.
|
||||
|
||||
Since these schemas were written for different docs solutions, or even their custom ones.
|
||||
We made `generateFiles()` too complicated to be capable of producing best results for various schema styles, and it's even harder to customize the generated file paths of API docs.
|
||||
|
||||
This update also include:
|
||||
|
||||
- Better UI for schema & playground body input
|
||||
- Improved type-safety for `generateFiles()`
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
The algorithm for generating file paths is changed:
|
||||
|
||||
- `meta.json` will no longer be generated, add as your wanted.
|
||||
- `per: operation`: The generated path will be identical to your operation id. If not defined, it takes your endpoint path instead.
|
||||
|
||||
### Group By
|
||||
|
||||
The behaviour of `groupBy` API is also changed.
|
||||
|
||||
| value | output |
|
||||
| ----- | ------------------------------------------------------ |
|
||||
| tag | `{tag}/{operationId ?? endpoint_path}.mdx` |
|
||||
| route | `{endpoint_path}/{method}.mdx` (ignores `name` option) |
|
||||
| none | `{operationId ?? endpoint_path}.mdx` (default) |
|
||||
|
||||
### Customize Output
|
||||
|
||||
The usage of `name` option is renewed, it can be used to customize the output path of files.
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
name: (output, document) => {
|
||||
// page info
|
||||
console.log(output);
|
||||
// parsed OpenAPI schema
|
||||
console.log(document);
|
||||
return 'my-dir/filename';
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
You can start using the latest algorithm, or keep the current behaviour with:
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
name: {
|
||||
algorithm: 'v1',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Note that it won't create `meta.json` anymore even with `algorithm` set to `v1`, you can keep your current generated `meta.json` files.
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: Fumadocs v15
|
||||
description: Tailwind CSS v4
|
||||
author: Fuma Nama
|
||||
date: 2025-01-24
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The purpose of v15 is mainly to support Tailwind CSS v4.
|
||||
Tailwind CSS v4 is a complete redesign of its API and internals, to fully adhere to the new CSS-first config style, a breaking change is required.
|
||||
|
||||
Fumadocs v15 has no other significant changes other than migrating the config to Tailwind CSS v4.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
Before making the switch, upgrade your site to Tailwind CSS v4 following their [upgrade guide](https://tailwindcss.com/docs/upgrade-guide).
|
||||
You can remove the unused `tailwind.config.js` file and fully rely on the new CSS-first config.
|
||||
|
||||
Add `@import` to the presets exported by Fumadocs UI, and include the `fumadocs-ui` package in the source.
|
||||
|
||||
```css title="Tailwind CSS"
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
|
||||
/* relative to the CSS file, make sure it's correct for your app */
|
||||
@source '../node_modules/fumadocs-ui/dist/**/*.js';
|
||||
```
|
||||
|
||||
Since v15, you will no longer pass options in JavaScript to customize plugins.
|
||||
Instead, you can use the new alternatives in Tailwind CSS v4.
|
||||
|
||||
### `addGlobalColors: true`
|
||||
|
||||
Forward the colors again:
|
||||
|
||||
```css
|
||||
@theme {
|
||||
--color-primary: var(--color-fd-primary);
|
||||
/* same for other colors */
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Variables
|
||||
|
||||
Fumadocs no longer use `--fd-<color>` like `--fd-primary: 0 0% 0%` for colors, it directly defines and uses colors in `@theme`.
|
||||
Instead of the previous format, it uses `hsl()`:
|
||||
|
||||
```css
|
||||
@theme {
|
||||
--color-fd-primary: hsl(0, 0%, 100%);
|
||||
}
|
||||
```
|
||||
|
||||
### Steps
|
||||
|
||||
Previous Tailwind CSS utilities like `steps` & `step` are renamed to `fd-steps` and `fd-step` to avoid conflicts.
|
||||
|
||||
### Typography
|
||||
|
||||
Typography utilities including `prose`, `prose-*` modifiers will continue to work. Please report problems if they are no longer working or have unexpected change in behaviours.
|
||||
|
||||
## Improvements
|
||||
|
||||
v15 also includes some minor improvements to the UI and OpenAPI integration.
|
||||
|
||||
### Code Block Tabs
|
||||
|
||||
In the past, you needed to write the `<Tabs />` manually when separating code blocks into tabs.
|
||||
|
||||
````mdx
|
||||
<Tabs items={["Tab 1", "Tab 2"]}>
|
||||
|
||||
```ts tab="Tab 1"
|
||||
console.log('Hello World');
|
||||
```
|
||||
|
||||
```ts tab="Tab 2"
|
||||
console.log('Hello World');
|
||||
```
|
||||
|
||||
</Tabs>
|
||||
````
|
||||
|
||||
Now you can do:
|
||||
|
||||
````mdx
|
||||
```ts tab="Tab 1"
|
||||
console.log('Hello World');
|
||||
```
|
||||
|
||||
```ts tab="Tab 2"
|
||||
console.log('Hello World');
|
||||
```
|
||||
````
|
||||
|
||||
Note that the previous usage still works for those who want to customize or pass props to the `<Tabs />` component.
|
||||
|
||||
### Reversed Items in `meta.json`
|
||||
|
||||
The rest item `...` in the `pages` property of `meta.json` now supports reversed order:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": ["z...a"]
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAPI Playground
|
||||
|
||||
v15 improves the playground UI (inspired by the minimalism of Scalar), and brought Scalar API Client support to Fumadocs OpenAPI.
|
||||
|
||||
You can enable the Scalar API Client using `useScalar` option in `createOpenAPI()`:
|
||||
|
||||
```ts
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { APIPlayground } from 'fumadocs-openapi/scalar';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
renderer: {
|
||||
APIPlayground,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
And install & configure their `@scalar/api-client-react`:
|
||||
|
||||
```package-install
|
||||
@scalar/api-client-react
|
||||
```
|
||||
|
||||
```css title="global.css"
|
||||
@import '@scalar/api-client-react/style.css' layer(base);
|
||||
```
|
||||
|
||||
<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.
|
||||
</Callout>
|
||||
|
||||
## Future Plans
|
||||
|
||||
This update should be a simple migration for most developers updating to Tailwind CSS v4.
|
||||
|
||||
In the future, we also want to make further improvements to Fumadocs MDX:
|
||||
|
||||
- remove `transform` API - you can easily do the same with `.map()` on runtime like:
|
||||
|
||||
```ts
|
||||
import { blog } from "@/.source"
|
||||
|
||||
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/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.
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: Fumadocs 16.2
|
||||
description: UI Refactor
|
||||
author: Fuma Nama
|
||||
date: 2025-11-29
|
||||
---
|
||||
|
||||
We are pleased to announce the release of Fumadocs 16.2, introducing a better layout system for Fumadocs UI.
|
||||
|
||||
## The New Layout System
|
||||
|
||||
Handling layouts in documentation frameworks can present significant challenges.
|
||||
|
||||
As a solution, Fumadocs UI implemented a [Grid System](/docs/ui/layouts/docs#the-layout-system) that is composable, flexible, cohesive and predictable.
|
||||
See the linked docs for more details.
|
||||
|
||||
In short, the layout consists of a grid container, and layout components using `position: sticky`:
|
||||
|
||||
```css
|
||||
#nd-docs-layout {
|
||||
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))
|
||||
minmax(min-content, 1fr);
|
||||
|
||||
--fd-docs-row-1: var(--fd-banner-height, 0px);
|
||||
--fd-docs-row-2: calc(var(--fd-docs-row-1) + var(--fd-header-height));
|
||||
--fd-docs-row-3: calc(var(--fd-docs-row-2) + var(--fd-toc-popover-height));
|
||||
}
|
||||
```
|
||||
|
||||
The container defines the offsets of each row, layout components can leverage them according to their grid area:
|
||||
|
||||
```css
|
||||
.layout-component {
|
||||
position: sticky;
|
||||
top: var(--fd-docs-row-1);
|
||||
height: calc(var(--fd-docs-height) - var(--fd-docs-row-1));
|
||||
}
|
||||
```
|
||||
|
||||
Layout components are also responsible to declare their heights via CSS variables, allowing container to calculate the right row offsets.
|
||||
|
||||
```css
|
||||
#nd-docs-layout:has(.layout-component) {
|
||||
--fd-header-height: 56px;
|
||||
}
|
||||
```
|
||||
|
||||
For user-land customizations, we recommend the Fumadocs CLI customize command, or hooking the relevant grid areas like:
|
||||
|
||||
```css
|
||||
.custom-component {
|
||||
/* depends on the layout, usually we have toc, toc-popover, sidebar, header, main */
|
||||
grid-area: main;
|
||||
}
|
||||
```
|
||||
|
||||
### Why the Change?
|
||||
|
||||
Previously, Fumadocs UI relied on `position: fixed` and mathematical computations (inspired by Vitepress). The older method often lacked composability and difficult for beginners to manage.
|
||||
|
||||
The new system fixed most of the flaws, allowing easier visualizations, while reducing the complexity of previous solutions.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### Global Styles
|
||||
|
||||
While Fumadocs 16.2 preserves compatibility in several areas, notably:
|
||||
|
||||
- The `--fd-layout-width` CSS variable continues to function as expected.
|
||||
- All ID and data attributes remain unchanged.
|
||||
|
||||
However, the shift to the new grid system may impact existing customizations, particularly those dependent on the prior fixed-position logic. We recommend reviewing your implementation carefully and testing thoroughly.
|
||||
|
||||
If you have already installed layout components via Fumadocs CLI, replace `preset.css` with `preset-legacy.css` to keep legacy styles & avoid unexpected breakages:
|
||||
|
||||
```css title="global.css"
|
||||
/* [!code --] */
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
/* [!code ++] */
|
||||
@import 'fumadocs-ui/css/preset-legacy.css';
|
||||
```
|
||||
|
||||
### No Longer Exposing Internal Components
|
||||
|
||||
Layout components such as Root Toggle, Language Toggle, and Theme Toggle are no longer directly exposed.
|
||||
This decision enables Fumadocs UI to iterate these elements without introducing unexpected breaking changes to usage outside of API surfaces.
|
||||
|
||||
**Migration Steps:** Override the relevant layout components with your own implementations, or utilize the Fumadocs CLI commands (`add` or `customize`) to install them locally.
|
||||
|
||||
### Removal of Internal Contexts
|
||||
|
||||
The following context modules are made private:
|
||||
|
||||
- `fumadocs-ui/contexts/sidebar`
|
||||
- `fumadocs-ui/contexts/layout`
|
||||
|
||||
If you have already installed `<DocsLayout />` via Fumadocs CLI, import the legacy contexts instead:
|
||||
|
||||
```ts
|
||||
// [!code --:2]
|
||||
import {} from 'fumadocs-ui/contexts/sidebar';
|
||||
import {} from 'fumadocs-ui/contexts/layout';
|
||||
|
||||
// [!code ++:2]
|
||||
import {} from 'fumadocs-ui/legacy/sidebar';
|
||||
import {} from 'fumadocs-ui/legacy/layout';
|
||||
```
|
||||
|
||||
And wrap your app under the legacy `<SidebarProvider />`:
|
||||
|
||||
```tsx
|
||||
import type { ReactNode } from 'react';
|
||||
import { SidebarProvider } from 'fumadocs-ui/legacy/sidebar';
|
||||
|
||||
export function RootLayout({ children }: { children: ReactNode }) {
|
||||
return <SidebarProvider>{children}</SidebarProvider>;
|
||||
}
|
||||
```
|
||||
|
||||
<Callout title="What if I still use built-in layouts?">
|
||||
|
||||
The legacy contexts are made exclusively for layout components installed locally. If you still
|
||||
import layout components (e.g. `<DocsLayout />`) from `fumadocs-ui`, migrate your usages as follows:
|
||||
|
||||
```tsx
|
||||
// [!code --]
|
||||
import { useSidebar } from 'fumadocs-ui/contexts/sidebar';
|
||||
// [!code ++]
|
||||
import { useSidebar } from 'fumadocs-ui/components/sidebar/base';
|
||||
```
|
||||
|
||||
Ensure the contexts are only accessed within `<DocsLayout />`.
|
||||
|
||||
</Callout>
|
||||
|
||||
## Minor Changes
|
||||
|
||||
The changes below are backward compatible.
|
||||
|
||||
### Importing Page Layouts by Type
|
||||
|
||||
The notebook layout is now isolated from default layout, including their page components.
|
||||
|
||||
Make sure to import the appropriate page layout based on your docs layout:
|
||||
|
||||
```ts
|
||||
// For docs layout
|
||||
import { DocsPage } from 'fumadocs-ui/layouts/docs/page';
|
||||
// For notebook layout
|
||||
import { DocsPage } from 'fumadocs-ui/layouts/notebook/page';
|
||||
```
|
||||
|
||||
**Migration Steps:** Update your imports accordingly. Although the default `fumadocs-ui/page` will redirect to the correct layout, we strongly advise explicit imports for clarity and future-proofing.
|
||||
|
||||
### Removal of Unused Styles
|
||||
|
||||
Styles related to the abandoned `container` utility, and `--spacing-fd-container` variable, have been dropped as they are no longer utilized.
|
||||
|
||||
**Migration Steps:** Remove references to these styles from your codebase.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Fumadocs 16.2 aims to offer greater flexibility for developers. Welcome to share your thoughts about the new design on GitHub Discussion!
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Static Build
|
||||
description: Output static website with Fumadocs.
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
By default, Fumadocs use a server-first approach which always requires a running server to serve.
|
||||
|
||||
You can output a static build by configuring your React framework.
|
||||
|
||||
### Search [step]
|
||||
|
||||
#### Built-in Search
|
||||
|
||||
1. [Configure Search Server](/docs/headless/search/orama#static-export).
|
||||
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.
|
||||
|
||||
#### Cloud Solutions
|
||||
|
||||
Since the search functionality is powered by remote servers, static export works without configuration.
|
||||
|
||||
### Deployment [step]
|
||||
|
||||
#### Next.js
|
||||
|
||||
You can enable Next.js static export, it allows you to export the app as a static HTML site without a Node.js server.
|
||||
|
||||
```js title="next.config.mjs"
|
||||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
*/
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
|
||||
// Optional: Change links `/me` -> `/me/` and emit `/me.html` -> `/me/index.html`
|
||||
// trailingSlash: true,
|
||||
|
||||
// Optional: Prevent automatic `/me` -> `/me/`, instead preserve `href`
|
||||
// skipTrailingSlashRedirect: true,
|
||||
};
|
||||
```
|
||||
|
||||
See [Next.js docs](https://nextjs.org/docs/app/guides/static-exports) for limitations and details.
|
||||
|
||||
#### React Router
|
||||
|
||||
By configuring SPA mode, you can serve the `build/client` directory as a static website.
|
||||
|
||||
<Callout type="warning" title="Don't forget to pre-render">
|
||||
|
||||
On SPA mode, all your server-side loaders must be pre-rendered.
|
||||
|
||||
See [React Router Docs](https://reactrouter.com/how-to/spa) for details.
|
||||
|
||||
</Callout>
|
||||
|
||||
```ts title="react-router.config.ts"
|
||||
import type { Config } from '@react-router/dev/config';
|
||||
import { glob } from 'node:fs/promises';
|
||||
import { createGetUrl, getSlugs } from 'fumadocs-core/source';
|
||||
|
||||
const getUrl = createGetUrl('/docs');
|
||||
|
||||
export default {
|
||||
// disable SSR [!code highlight]
|
||||
ssr: false,
|
||||
|
||||
// [!code ++:9]
|
||||
async prerender({ getStaticPaths }) {
|
||||
const paths: string[] = [...getStaticPaths()];
|
||||
|
||||
for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) {
|
||||
paths.push(getUrl(getSlugs(entry)));
|
||||
}
|
||||
|
||||
return paths;
|
||||
},
|
||||
} satisfies Config;
|
||||
```
|
||||
|
||||
#### Tanstack Start
|
||||
|
||||
Configure SPA mode and pre-rendering, refer to [SPA Mode](https://tanstack.com/start/latest/docs/framework/react/guide/spa-mode) for deploying SPA apps with Tanstack Start.
|
||||
|
||||
```ts title="vite.config.ts"
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
// ...other plugins
|
||||
tanstackStart({
|
||||
spa: {
|
||||
enabled: true,
|
||||
// Tanstack Router will automatically crawl your pages [!code ++:4]
|
||||
prerender: {
|
||||
enabled: true,
|
||||
},
|
||||
// if you have any hidden paths that's not visible on UI, you can add them explicitly. [!code highlight:5]
|
||||
pages: [
|
||||
{
|
||||
path: '/docs/test',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
#### Waku
|
||||
|
||||
Waku can serve your site statically when all pages are configured with `static` render mode.
|
||||
|
||||
See [Deployment](https://waku.gg/#deployment) for details.
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Customize UI
|
||||
description: A complete guide on how to customize Fumadocs UI.
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Fumadocs UI provides pre-built components for simplicity, serving as a middleground between "installing all UI code to your codebase" and an opinionated package.
|
||||
|
||||
While Fumadocs UI itself has many designs around flexibility & backward compatibility, if used incorrectly, the measures may take no effect.
|
||||
|
||||
> It is also recommended to feed this guide to your AI agents, if they may edit your UI.
|
||||
|
||||
### Props [step]
|
||||
|
||||
You can first check if the options of components can suffice your needs, for example:
|
||||
|
||||
```tsx
|
||||
<DocsLayout sidebar={{ enabled: false }}>{/* content */}</DocsLayout>
|
||||
```
|
||||
|
||||
Or styling the container via props:
|
||||
|
||||
```tsx
|
||||
<DocsLayout
|
||||
containerProps={{
|
||||
className: '...',
|
||||
}}
|
||||
>
|
||||
{/* content */}
|
||||
</DocsLayout>
|
||||
```
|
||||
|
||||
Detailed documentation for each component is in the [Fumadocs UI](/docs/ui) section, you can take it as a reference.
|
||||
|
||||
Most options support passing JSX elements to allow your own components.
|
||||
This approach should suffice most use cases, such as adding sections below table of contents or in sidebar.
|
||||
|
||||
### CSS [step]
|
||||
|
||||
When the styling is more complicated, some components of Fumadocs UI attaches its own `id` & `data-` attributes for selecting.
|
||||
You can target them in your CSS selectors:
|
||||
|
||||
```css
|
||||
#nd-docs-layout #nd-subnav {
|
||||
background-color: purple;
|
||||
}
|
||||
|
||||
[data-toc-popover] {
|
||||
background-color: purple;
|
||||
}
|
||||
```
|
||||
|
||||
**Avoid invasive styles, or anything that presume a very specific DOM structure**, as it is difficult to guarantee backward compatibility, especially with newer UI features.
|
||||
|
||||
```css title="Bad Example"
|
||||
/* avoid targeting elements like this! */
|
||||
[data-toc-popover] > div {
|
||||
background-color: purple;
|
||||
}
|
||||
```
|
||||
|
||||
CSS approach can be risky as only the `id` & `data-` attributes can be versioned under its own life cycles, prefer other approaches if possible.
|
||||
|
||||
### Install via CLI [step]
|
||||
|
||||
For advanced needs beyond the exposed options, or if your custom CSS styles became overly invasive, Fumadocs CLI is a powerful tool.
|
||||
|
||||
You can find similar prompts under the relevant component docs, indicating the component is installable, such as:
|
||||
|
||||
<Installation name="layouts/docs" />
|
||||
|
||||
For **layout components** (which often composes multiple components), Fumadocs CLI has a special `customize` command with granular choices.
|
||||
|
||||
```npm
|
||||
npx @fumadocs/cli@latest customize
|
||||
```
|
||||
|
||||
Despite installing the entire layout component, you can choose to install a **slot** (a replaceable part of the layout component).
|
||||
Run the command above and you can view the full options.
|
||||
|
||||
When installed, make sure to use the installed types over the types exported from Fumadocs UI. For example, if you have installed `layout/docs`, update your imports:
|
||||
|
||||
```tsx
|
||||
// [!code --]
|
||||
import type { DocsLayoutProps } from 'fumadocs-ui/layouts/docs';
|
||||
// [!code ++]
|
||||
import type { DocsLayoutProps } from '@/components/layout/docs';
|
||||
```
|
||||
|
||||
This ensure your local components will be decoupled from Fumadocs UI.
|
||||
|
||||
While Fumadocs CLI offers full control for the UI code, it is worth mentioning that:
|
||||
|
||||
- You won't be able to receive UI updates for the installed the components.
|
||||
- You can re-install the component with Fumadocs CLI, but prior customisations will be erased. (You can choose which file to overwrite)
|
||||
- Fumadocs CLI will assume the latest Fumadocs Core + UI to be used.
|
||||
|
||||
## Other Questions
|
||||
|
||||
Feel free to reach out in [GitHub Discussions](https://github.com/fuma-nama/fumadocs/discussions) for questions, or if you have encountered unexpected breakages.
|
||||
@@ -21,7 +21,7 @@ import path from 'node:path';
|
||||
const browser = await puppeteer.launch();
|
||||
const outDir = 'pdfs';
|
||||
// update this [!code highlight]
|
||||
const urls = ['/docs/ui', '/docs/ui/customisations'];
|
||||
const urls = ['/docs/ui', '/docs/ui/customizations'];
|
||||
|
||||
async function exportPdf(pathname: string) {
|
||||
const page = await browser.newPage();
|
||||
|
||||
@@ -49,9 +49,14 @@ A command line tool to install UI components and automate things, useful for cus
|
||||
|
||||
**Markdown/MDX:** Markdown is a markup language for creating formatted text. Hanzo Docs natively supports Markdown and MDX (superset of Markdown).
|
||||
|
||||
**[Bun](https://bun.sh):** A JavaScript runtime, we use it for running scripts.
|
||||
**[Bun](https://bun.sh):** A JavaScript runtime, it will be used in our guides for running scripts. If you prefer Node.js, try [unrun](https://gugustinette.github.io/unrun/guide/getting-started.html#cli) instead.
|
||||
|
||||
Some basic knowledge of React.js would be useful for further customisations.
|
||||
Some basic knowledge of React.js would be useful for further customizations.
|
||||
|
||||
<Callout title="Love Fumadocs but not familiar with React?">
|
||||
[Fumapress](https://press.fumadocs.dev) is a site generator that has lower barrier for beginners.
|
||||
It enforces a React framework of its own, and offers a better DX in return.
|
||||
</Callout>
|
||||
|
||||
## Automatic Installation
|
||||
|
||||
@@ -234,7 +239,7 @@ For authoring docs, make sure to read:
|
||||
Hanzo Docs has some additional features for authoring content.
|
||||
</Card>
|
||||
<Card href="/docs/navigation" title="Navigation">
|
||||
Learn how to customise navigation structure.
|
||||
Learn how to customize navigation structure.
|
||||
</Card>
|
||||
<Card href="/docs/page-conventions" title="Page Slugs & Page Tree">
|
||||
Learn how to organise content.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: Build Your Own
|
||||
description: Build your own content source for Fumadocs.
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
For a custom content source implementation, there's two ways:
|
||||
|
||||
### Using Loader API
|
||||
|
||||
Loader API has a file-system-like interface for integrating different content sources.
|
||||
|
||||
See [Loader API `source`](/docs/headless/source-api/source) for details.
|
||||
|
||||
### Using Low-Level API
|
||||
|
||||
For more robust control, you can use lower level APIs directly.
|
||||
|
||||
#### Page Tree [step]
|
||||
|
||||
You can either hardcode the page tree, or write some code to generate one.
|
||||
See [Definitions of Page Tree](/docs/headless/page-tree).
|
||||
|
||||
Pass your page tree to `DocsLayout` (usually in a `layout.tsx`):
|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout
|
||||
// [!code ++:5]
|
||||
tree={
|
||||
{
|
||||
// your own tree
|
||||
}
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The page tree is like a smarter "sidebar items", they will be referenced everywhere in the UI for navigation elements, such as the page footer.
|
||||
|
||||
#### Page Content [step]
|
||||
|
||||
For docs page, it's the same logic:
|
||||
|
||||
- Define path params (`slugs`).
|
||||
- Fetch page content from path params.
|
||||
- Render the content (inside the `DocsPage` component).
|
||||
|
||||
You may need table of contents, which can be generated on your own, or using the [`getTableOfContents`](/docs/headless/utils/get-toc) utility (Markdown/MDX only).
|
||||
|
||||
```tsx
|
||||
import { DocsPage, DocsBody } from 'fumadocs-ui/layouts/docs/page';
|
||||
import { getPage } from './my-content-source';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export default function Page({ params }: { params: { slug?: string[] } }) {
|
||||
const page = getPage(params.slug);
|
||||
if (!page) notFound();
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.tableOfContents}>
|
||||
<DocsBody>{page.render()}</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Pre-rendering [step]
|
||||
|
||||
The mechanism for pre-rendering differs depending on your React.js framework.
|
||||
|
||||
It's important to pre-render pages (especially the frequently visited ones) to improve initial load time.
|
||||
|
||||
<Callout title='In Next.js'>
|
||||
|
||||
Define the [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) function to populate dynamic & catch-all routes.
|
||||
|
||||
</Callout>
|
||||
|
||||
When pre-rendering pages from a remote source, make sure to optimize repeated reads during the build phase.
|
||||
For example, if you're fetching content via GitHub API, it is better to clone the repository content locally for pre-rendering.
|
||||
|
||||
```text tab="Before"
|
||||
/docs/* -> GET github.com
|
||||
/docs/introduction -> GET github.com
|
||||
/docs/my-page -> GET github.com
|
||||
Error: API Ratelimit
|
||||
```
|
||||
|
||||
```text tab="After"
|
||||
start -> git clone
|
||||
/docs/* -> ls
|
||||
/docs/introduction -> read file
|
||||
/docs/my-page -> read file
|
||||
```
|
||||
|
||||
#### Document Search [step]
|
||||
|
||||
This can be difficult considering your content may not be necessarily Markdown/MDX.
|
||||
For Markdown and MDX, the built-in [Search API](/docs/headless/search/orama) + search index usage is adequate for most use cases.
|
||||
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).
|
||||
@@ -0,0 +1,325 @@
|
||||
---
|
||||
title: Local Markdown
|
||||
description: Content source for local Markdown content.
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
`@fumadocs/local-md` is a content source for local Markdown/MDX files, it is bundleless (works fully at runtime) by design.
|
||||
|
||||
As compared to [MDX Remote](/docs/integrations/content/mdx-remote), it is more comprehensive & robust while focused solely on local files.
|
||||
|
||||
As compared to [Fumadocs MDX](/docs/mdx), it doesn't need a type-gen or bundler to work, but build-time image optimization will be disabled.
|
||||
|
||||
### Limitations
|
||||
|
||||
- No build-time image optimization.
|
||||
- No imports/exports in MDX files, but you can pass variables & components at render phase.
|
||||
|
||||
## Setup
|
||||
|
||||
Install the package:
|
||||
|
||||
```package-install
|
||||
@fumadocs/local-md shiki
|
||||
```
|
||||
|
||||
> `shiki` is installed because it has to be externalized by the bundler.
|
||||
|
||||
Create a `localMd` instance and connect it to Fumadocs:
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
import { dynamicLoader } from 'fumadocs-core/source/dynamic';
|
||||
import { localMd } from '@fumadocs/local-md';
|
||||
|
||||
const docs = localMd({
|
||||
dir: 'content/docs',
|
||||
// options
|
||||
});
|
||||
|
||||
const docsLoader = dynamicLoader(docs.dynamicSource(), {
|
||||
baseUrl: '/docs',
|
||||
});
|
||||
|
||||
export async function getSource() {
|
||||
return docsLoader.get();
|
||||
}
|
||||
```
|
||||
|
||||
### Schema
|
||||
|
||||
You may pass `frontmatterSchema` and `metaSchema` to customize the validation schemas:
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
import { localMd } from '@fumadocs/local-md';
|
||||
import { pageSchema, metaSchema } from 'fumadocs-core/source/schema';
|
||||
|
||||
const docs = localMd({
|
||||
dir: 'content/docs',
|
||||
frontmatterSchema: pageSchema.extend({
|
||||
// ...
|
||||
}),
|
||||
metaSchema: metaSchema.extend({
|
||||
// ...
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The recommended integration is:
|
||||
|
||||
1. Create `localMd({ dir })` for your content directory.
|
||||
2. Pass `docs.dynamicSource()` to `dynamicLoader()`.
|
||||
3. Read the source in your route/layout and render it with Fumadocs UI.
|
||||
|
||||
For example, in a docs layout:
|
||||
|
||||
```tsx title="app/docs/layout.tsx"
|
||||
import { getSource } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
|
||||
export default async function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
const docs = await getSource();
|
||||
|
||||
return <DocsLayout tree={docs.getPageTree()}>{children}</DocsLayout>;
|
||||
}
|
||||
```
|
||||
|
||||
The returned source from `docsLoader.get()` is a normal [content loader](/docs/headless/source-api) instance.
|
||||
|
||||
### Hot Reload
|
||||
|
||||
`local-md` works at runtime, but during development you can connect to its dev server for file watching.
|
||||
|
||||
Start the dev server with `local-md dev -- xxx` like:
|
||||
|
||||
```json title="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "local-md dev -- npm next dev"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And connect to it:
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
import { localMd } from '@fumadocs/local-md';
|
||||
|
||||
const docs = localMd({
|
||||
dir: 'content/docs',
|
||||
});
|
||||
|
||||
// change it if you use a different framework (e.g. import.meta.env.DEV)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
void docs.devServer();
|
||||
}
|
||||
```
|
||||
|
||||
This keeps the loader in sync when local Markdown or MDX files change.
|
||||
|
||||
### JavaScript Engine
|
||||
|
||||
When compiling Markdown files (`*.md`), `@fumadocs/local-md` uses a virtual JavaScript engine to avoid `eval()` at runtime.
|
||||
|
||||
This allows your app to work on environments like Cloudflare Worker, while the performance will be slower than the native JavaScript JIT compiler.
|
||||
|
||||
### Disable Revalidation
|
||||
|
||||
You can use `staticSource()` when you only need a one-time snapshot without revalidation.
|
||||
|
||||
It works with a normal `loader()` instead of only `dynamicLoader()`:
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { localMd } from '@fumadocs/local-md';
|
||||
|
||||
const docs = localMd({
|
||||
dir: 'content/docs',
|
||||
});
|
||||
|
||||
export const source = loader(await docs.staticSource(), {
|
||||
baseUrl: '/docs',
|
||||
});
|
||||
```
|
||||
|
||||
## Migration from Fumadocs MDX
|
||||
|
||||
If you're already using Fumadocs MDX for local docs content, migrating is usually straightforward.
|
||||
|
||||
### Before
|
||||
|
||||
With Fumadocs MDX, a common setup looks like:
|
||||
|
||||
```ts tab="source.config.ts"
|
||||
import { defineDocs, defineConfig } from 'fumadocs-mdx/config';
|
||||
|
||||
export const docs = defineDocs({
|
||||
dir: 'content/docs',
|
||||
});
|
||||
|
||||
export default defineConfig();
|
||||
```
|
||||
|
||||
```ts tab="lib/source.ts"
|
||||
import { docs } from 'collections/server';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: '/docs',
|
||||
source: docs.toFumadocsSource(),
|
||||
});
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
With `@fumadocs/local-md`, you can replace that setup with:
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
import { dynamicLoader } from 'fumadocs-core/source/dynamic';
|
||||
import { localMd } from '@fumadocs/local-md';
|
||||
|
||||
const docs = localMd({
|
||||
dir: 'content/docs',
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
void docs.devServer();
|
||||
}
|
||||
|
||||
const docsLoader = dynamicLoader(docs.dynamicSource(), {
|
||||
baseUrl: '/docs',
|
||||
});
|
||||
|
||||
export async function getSource() {
|
||||
return docsLoader.get();
|
||||
}
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
- remove `source.config.ts`.
|
||||
- remove framework-specific MDX integration like `createMDX()` in `next.config.mjs`.
|
||||
- replace `collections/server` imports with a loader created from `localMd()`.
|
||||
|
||||
Finally, update references to your `source` object with `getSource()`:
|
||||
|
||||
```tsx title="app/docs/layout.tsx"
|
||||
import { getSource } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
|
||||
export default async function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
// [!code --]
|
||||
return <DocsLayout tree={source.getPageTree()}>{children}</DocsLayout>;
|
||||
|
||||
// [!code ++:2]
|
||||
const docs = await getSource();
|
||||
return <DocsLayout tree={docs.getPageTree()}>{children}</DocsLayout>;
|
||||
}
|
||||
```
|
||||
|
||||
And the type of pages is also changed:
|
||||
|
||||
```tsx tab="Before"
|
||||
const page = source.getPage(['...']);
|
||||
|
||||
// title & description
|
||||
page.data.title;
|
||||
|
||||
// custom frontmatter properties
|
||||
page.data.full;
|
||||
|
||||
// getText() API
|
||||
await page.data.getText('processed');
|
||||
|
||||
// compiled properties:
|
||||
page.data.structuredData;
|
||||
page.data.toc;
|
||||
return (
|
||||
<div>
|
||||
<page.data.body components={{}} />
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
```tsx tab="After"
|
||||
const page = docs.getPage(['...']);
|
||||
|
||||
// title & description (unchanged)
|
||||
page.data.title;
|
||||
|
||||
// custom frontmatter properties
|
||||
page.data.frontmatter.full;
|
||||
|
||||
// there's no getText() API, use data.content instead
|
||||
console.log(page.data.content);
|
||||
|
||||
// compiled properties:
|
||||
const { structuredData, render } = await page.data.load();
|
||||
const { toc, body } = await render(mdxComponents);
|
||||
|
||||
return <div>{body}</div>;
|
||||
```
|
||||
|
||||
### Without RSC
|
||||
|
||||
If your setup doesn't support RSC, unlike Fumadocs MDX's `collections/browser` API, `local-md` leverages server payload to send AST, then render Markdown at client-side.
|
||||
|
||||
For example, in Tanstack Start:
|
||||
|
||||
```tsx title="src/routes/docs/$.tsx"
|
||||
import { createFileRoute, notFound } from '@tanstack/react-router';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { createServerFn } from '@tanstack/react-start';
|
||||
import { getSource } from '@/lib/source';
|
||||
import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page';
|
||||
import { useFumadocsLoader } from 'fumadocs-core/source/client';
|
||||
import { useMemo } from 'react';
|
||||
import { useMDXComponents } from '@/components/mdx';
|
||||
import { rendererFromSerialized } from '@fumadocs/local-md/client';
|
||||
|
||||
export const Route = createFileRoute('/docs/$')({
|
||||
component: Page,
|
||||
loader: async ({ params }) => {
|
||||
const slugs = params._splat?.split('/') ?? [];
|
||||
return serverLoader({ data: slugs });
|
||||
},
|
||||
});
|
||||
|
||||
const serverLoader = createServerFn({
|
||||
method: 'GET',
|
||||
})
|
||||
.inputValidator((slugs: string[]) => slugs)
|
||||
.handler(async ({ data: slugs }) => {
|
||||
const source = await getSource();
|
||||
const page = source.getPage(slugs);
|
||||
if (!page) throw notFound();
|
||||
|
||||
// compile
|
||||
const { serialize } = await page.data.load();
|
||||
|
||||
return {
|
||||
frontmatter: page.data.frontmatter,
|
||||
// [!code highlight] pass it to payload
|
||||
render: serialize(),
|
||||
// ...
|
||||
};
|
||||
});
|
||||
|
||||
function Page() {
|
||||
const { pageTree, frontmatter, render } = useFumadocsLoader(Route.useLoaderData());
|
||||
// [!code highlight:2] render under sync mode
|
||||
const renderer = useMemo(() => rendererFromSerialized(render), [render]);
|
||||
const { body, toc } = renderer.renderSync(useMDXComponents());
|
||||
|
||||
return (
|
||||
<DocsLayout tree={pageTree}>
|
||||
<DocsPage toc={toc}>
|
||||
{/* ... */}
|
||||
<DocsBody>{body}</DocsBody>
|
||||
</DocsPage>
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -14,7 +14,7 @@ The Passed MDX content must be trusted as it allows code execution by default.
|
||||
|
||||
</Callout>
|
||||
|
||||
### Setup
|
||||
## Setup
|
||||
|
||||
```package-install
|
||||
@hanzo/docs-mdx-remote
|
||||
@@ -23,8 +23,11 @@ The Passed MDX content must be trusted as it allows code execution by default.
|
||||
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:
|
||||
|
||||
- Images have to be optimized at runtime.
|
||||
- No imports and exports in MDX files.
|
||||
|
||||
### With RSC
|
||||
|
||||
It's compatible with Server Components. For example:
|
||||
|
||||
```tsx
|
||||
@@ -37,16 +40,15 @@ const compiler = createCompiler({
|
||||
// options
|
||||
});
|
||||
|
||||
export default async function Page({ params }: { params: { slug?: string[] } }) {
|
||||
const page = getPage(params.slug);
|
||||
const compiled = await compiler.compile({
|
||||
export default async function Page({ params }: PageProps<'/docs/[[...slug]]'>) {
|
||||
const { slug } = await params;
|
||||
const page = getPage(slug);
|
||||
const { toc, body: MdxContent } = await compiler.compile({
|
||||
source: page.content,
|
||||
});
|
||||
|
||||
const MdxContent = compiled.body;
|
||||
|
||||
return (
|
||||
<DocsPage toc={compiled.toc}>
|
||||
<DocsPage toc={toc}>
|
||||
<DocsBody>
|
||||
<MdxContent components={getMDXComponents()} />
|
||||
</DocsBody>
|
||||
@@ -55,6 +57,50 @@ export default async function Page({ params }: { params: { slug?: string[] } })
|
||||
}
|
||||
```
|
||||
|
||||
### Without RSC
|
||||
|
||||
Without Server Components, you can enable `skipRender` and pass the compiled code to client instead:
|
||||
|
||||
```tsx
|
||||
import { createCompiler } from '@fumadocs/mdx-remote';
|
||||
import { executeMdxSync } from '@fumadocs/mdx-remote/client';
|
||||
import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page';
|
||||
import { getPage } from './my-content-source';
|
||||
import { useMemo } from 'react';
|
||||
import { getMDXComponents } from '@/components/mdx';
|
||||
|
||||
const compiler = createCompiler({
|
||||
// options
|
||||
});
|
||||
|
||||
// e.g. where you generate loader data
|
||||
async function serverPayload(slugs?: string[]) {
|
||||
const page = getPage(slugs);
|
||||
const { compiled, frontmatter } = await compiler.compile({
|
||||
source: page.content,
|
||||
skipRender: true,
|
||||
});
|
||||
|
||||
return { compiled, frontmatter };
|
||||
}
|
||||
|
||||
function Page() {
|
||||
// read compiled code from server payload
|
||||
const { compiled } = useLoaderData();
|
||||
const { default: MdxContent, toc } = useMemo(() => executeMdxSync(compiled), [compiled]);
|
||||
|
||||
return (
|
||||
<DocsPage toc={toc}>
|
||||
<DocsBody>
|
||||
<MdxContent components={getMDXComponents()} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> The `frontmatter` property returned by `compile()` is not validated, you may use a library like `zod` to validate its data type.
|
||||
|
||||
### Images
|
||||
|
||||
On serverless platforms like Vercel, the original `public` folder (including static assets like images) will be removed after production build.
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
---
|
||||
title: Sanity
|
||||
description: Official integration for Sanity.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Fumadocs provide an official integration to simplify setup for Sanity CMS, it brings native live preview experience without compromise on performance.
|
||||
|
||||
> You can see our [Sanity Example](https://github.com/fuma-nama/fumadocs-sanity) on GitHub.
|
||||
|
||||
## Setup
|
||||
|
||||
Assume you already have [Sanity](https://sanity.io/docs/getting-started) configured according to their docs.
|
||||
|
||||
### Data Types [step]
|
||||
|
||||
Run the following command to initialize content types.
|
||||
|
||||
```npm
|
||||
npx @fumadocs/cli add fumadocs/sanity/base
|
||||
```
|
||||
|
||||
> Or you can run `npx @fumadocs/cli add` and browse additional Sanity presets to install.
|
||||
|
||||
Register the installed types:
|
||||
|
||||
```ts tab="schemaTypes/index.ts"
|
||||
import { docsType, blockContent, callout, card, cards } from '@/lib/sanity/base';
|
||||
|
||||
// [!code ++]
|
||||
export const schemaTypes = [blockContent, callout, card, cards, docsType];
|
||||
```
|
||||
|
||||
```tsx tab="components/sanity.ts"
|
||||
import {
|
||||
PortableText,
|
||||
type PortableTextProps,
|
||||
type PortableTextReactComponents,
|
||||
} from '@portabletext/react';
|
||||
// the installed components
|
||||
import { baseBlocks, baseComponents, baseMarks } from './base';
|
||||
|
||||
const components: Partial<PortableTextReactComponents> = {
|
||||
block: {
|
||||
// [!code ++]
|
||||
...baseBlocks,
|
||||
},
|
||||
marks: {
|
||||
// [!code ++]
|
||||
...baseMarks,
|
||||
},
|
||||
types: {
|
||||
// [!code ++]
|
||||
...baseComponents,
|
||||
},
|
||||
};
|
||||
|
||||
export function CustomPortableText(props: PortableTextProps) {
|
||||
return <PortableText {...props} components={{ ...components, ...props.components }} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Content Loader [step]
|
||||
|
||||
Install the integration:
|
||||
|
||||
```npm
|
||||
npm i @fumadocs/sanity
|
||||
```
|
||||
|
||||
```ts title="lib/source.ts" tab="Next.js (live preview)"
|
||||
import { dynamicLoader } from 'fumadocs-core/source/dynamic';
|
||||
import { createSanitySource } from '@fumadocs/sanity';
|
||||
// the live preview client
|
||||
import { sanityFetch } from '@/sanity/lib/live';
|
||||
// the generated types
|
||||
import type { Docs } from '@/sanity/types';
|
||||
import { draftMode } from 'next/headers';
|
||||
|
||||
const source = dynamicLoader(
|
||||
createSanitySource<Docs>({
|
||||
sanityFetch,
|
||||
docType: 'docs',
|
||||
}),
|
||||
{
|
||||
baseUrl: '/docs',
|
||||
},
|
||||
);
|
||||
|
||||
export async function getSource() {
|
||||
const { isEnabled } = await draftMode();
|
||||
|
||||
if (isEnabled) source.invalidate();
|
||||
return source.get();
|
||||
}
|
||||
```
|
||||
|
||||
```ts title="lib/source.ts" tab="Others"
|
||||
import { dynamicLoader } from 'fumadocs-core/source/dynamic';
|
||||
import { createSanitySource } from '@fumadocs/sanity';
|
||||
// the Sanity client
|
||||
import { client } from '@/sanity/lib/client';
|
||||
// the generated types
|
||||
import type { Docs } from '@/sanity/types';
|
||||
|
||||
const source = dynamicLoader(
|
||||
createSanitySource<Docs>({
|
||||
client,
|
||||
docType: 'docs',
|
||||
}),
|
||||
{
|
||||
baseUrl: '/docs',
|
||||
},
|
||||
);
|
||||
|
||||
export async function getSource() {
|
||||
return source.get();
|
||||
}
|
||||
```
|
||||
|
||||
Now update references to your `source` with the `getSource()` function like:
|
||||
|
||||
```tsx
|
||||
import { getSource } from '@/lib/source';
|
||||
import { CustomPortableText } from '@/components/sanity';
|
||||
import { DocsPage, DocsBody, DocsDescription, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
|
||||
// [!code highlight]
|
||||
const source = await getSource();
|
||||
|
||||
const page = source.getPage(['...']);
|
||||
|
||||
// basic info
|
||||
console.log(page.data.title, page.data.description);
|
||||
|
||||
// [!code highlight] load full info
|
||||
const { renderToc, body } = await page.data.load();
|
||||
|
||||
// to render docs page content:
|
||||
return (
|
||||
// `renderToc()` to render TOC items on server
|
||||
<DocsPage toc={renderToc({ render: (body) => <CustomPortableText value={body} /> })}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<CustomPortableText value={body!} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
```
|
||||
|
||||
If you are not using React Server Components, `renderToc` can be imported separately:
|
||||
|
||||
```tsx
|
||||
import { getSource } from '@/lib/source';
|
||||
import { renderToc } from '@fumadocs/sanity/client';
|
||||
import { CustomPortableText } from '@/components/sanity';
|
||||
import { DocsPage, DocsBody, DocsDescription, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
|
||||
// where you return server payload
|
||||
export async function serverPayload(slugs: string[]) {
|
||||
const source = await getSource();
|
||||
const page = source.getPage(slugs);
|
||||
if (!page) throw new Error('not found');
|
||||
|
||||
const { title, description, _toc, body } = await page.data.load();
|
||||
return { title, description, toc: _toc, body };
|
||||
}
|
||||
|
||||
export function Page() {
|
||||
// receive server payload
|
||||
const { toc, body, title, description } = useLoaderData();
|
||||
|
||||
return (
|
||||
<DocsPage toc={renderToc({ toc, render: (body) => <CustomPortableText value={body} /> })}>
|
||||
<DocsTitle>{title}</DocsTitle>
|
||||
<DocsDescription>{description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<CustomPortableText value={body!} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Done [step]
|
||||
|
||||
Create documents under the `docs` type in your studio editor, new content will be displayed in the docs route.
|
||||
@@ -49,9 +49,9 @@ For example, to report user feedback as a `on_rate_docs` event on Hanzo Insights
|
||||
|
||||
### Feedback Block
|
||||
|
||||
You can also configure block-level feedback (e.g. a feedback popover for each paragraph).
|
||||
You can also configure block-level feedback (e.g. a feedback popover as user select text).
|
||||
|
||||
Add the `remark-feedback-block` Remark plugin:
|
||||
Add the `remark-block-id` Remark plugin:
|
||||
|
||||
```tsx title="source.config.ts (Hanzo Docs MDX)"
|
||||
import {
|
||||
@@ -60,37 +60,41 @@ import {
|
||||
} from '@hanzo/docs-core/mdx-plugins/remark-feedback-block';
|
||||
import { defineConfig } from '@hanzo/docs-mdx/config';
|
||||
|
||||
const feedbackOptions: RemarkFeedbackBlockOptions = {
|
||||
// other options:
|
||||
const blockIdOptions: RemarkBlockIdOptions = {
|
||||
addDataAttribute: 'feedback',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
// [!code ++]
|
||||
[remarkFeedbackBlock, feedbackOptions],
|
||||
[remarkBlockId, blockIdOptions],
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Then, define the `FeedbackBlock` MDX component:
|
||||
Then, wrap your page content under the `FeedbackText` component:
|
||||
|
||||
```tsx title="components/mdx.tsx"
|
||||
import { FeedbackBlock } from '@/components/feedback/client';
|
||||
import defaultComponents from '@hanzo/docs-ui/mdx';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
|
||||
export function getMDXComponents(components?: MDXComponents) {
|
||||
return {
|
||||
...defaultComponents,
|
||||
FeedbackBlock: ({ children, ...rest }) => (
|
||||
<FeedbackBlock {...rest} onSendAction={onBlockFeedbackAction}>
|
||||
{children}
|
||||
</FeedbackBlock>
|
||||
),
|
||||
...components,
|
||||
} satisfies MDXComponents;
|
||||
export default async function Page() {
|
||||
return (
|
||||
<DocsPage>
|
||||
<FeedbackText
|
||||
onSendAction={async (feedback) => {
|
||||
'use server';
|
||||
|
||||
console.log(feedback);
|
||||
}}
|
||||
>
|
||||
{/* the content of page */}
|
||||
</FeedbackText>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -98,7 +102,7 @@ export function getMDXComponents(components?: MDXComponents) {
|
||||
|
||||
<Callout type='info' title='Good to know'>
|
||||
|
||||
`remark-feedback-block` generates a block ID from its content and order in the page, hence it is also possible to track the blocks from 3rd party services.
|
||||
`remark-block-id` generates a block ID from its content and order in the page, hence it is also possible to track the blocks from 3rd party services.
|
||||
|
||||
</Callout>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ In **Hanzo Docs MDX**, you can do:
|
||||
import { source } from '@/lib/source';
|
||||
import type { InferPageType } from '@hanzo/docs-core/source';
|
||||
|
||||
export async function getLLMText(page: InferPageType<typeof source>) {
|
||||
export async function getLLMText(page: (typeof source)['$inferPage']) {
|
||||
const processed = await page.data.getText('processed');
|
||||
|
||||
return `# ${page.data.title} (${page.url})
|
||||
@@ -180,11 +180,9 @@ export async function getConfig() {
|
||||
}
|
||||
```
|
||||
|
||||
### `*.mdx` [#mdx-extension]
|
||||
### `*.md` [#md-extension]
|
||||
|
||||
Allow AI agents to get the content of a page as Markdown/MDX, by appending `.mdx` to the end of path.
|
||||
|
||||
Make a route handler to return page content, and a middleware to point to it:
|
||||
Allow AI agents to get the content of a page as Markdown/MDX, by appending `.md` to the end of path.
|
||||
|
||||
```ts tab="Next.js" title="app/llms.mdx/docs/[[...slug]]/route.ts"
|
||||
import { getLLMText } from '@/lib/get-llm-text';
|
||||
@@ -218,7 +216,7 @@ const config: NextConfig = {
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/docs/:path*.mdx',
|
||||
source: '/docs/:path*.md',
|
||||
destination: '/llms.mdx/docs/:path*',
|
||||
},
|
||||
];
|
||||
@@ -260,7 +258,7 @@ import { rewritePath } from '@hanzo/docs-core/negotiation';
|
||||
import type { Route } from './+types/root';
|
||||
|
||||
// [!code ++:20]
|
||||
const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}.mdx', '/llms.mdx/docs{/*path}');
|
||||
const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}.md', '/llms.mdx/docs{/*path}');
|
||||
const serverMiddleware: Route.MiddlewareFunction = async ({ request }, next) => {
|
||||
const url = new URL(request.url);
|
||||
const path = rewriteLLM(url.pathname);
|
||||
@@ -283,16 +281,43 @@ export default {
|
||||
} satisfies Config;
|
||||
```
|
||||
|
||||
```ts tab="Tanstack Start" title="src/routes/llms[.]mdx.docs.$.ts"
|
||||
```ts tab="Tanstack Start" title="lib/source.ts"
|
||||
// create the following utils:
|
||||
|
||||
export function markdownPathToSlugs(segs: string[]) {
|
||||
if (segs.length === 0) return [];
|
||||
|
||||
const out = [...segs];
|
||||
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
|
||||
if (out.length === 1 && out[0] === 'index') out.pop();
|
||||
return out;
|
||||
}
|
||||
|
||||
export function slugsToMarkdownPath(slugs: string[]) {
|
||||
const segments = [...slugs];
|
||||
if (segments.length === 0) {
|
||||
segments.push('index.md');
|
||||
} else {
|
||||
segments[segments.length - 1] += '.md';
|
||||
}
|
||||
|
||||
return {
|
||||
segments,
|
||||
url: `/docs/${segments.join('/')}`,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```ts tab="Tanstack Start" title="src/routes/docs/{$}[.]md.ts"
|
||||
import { createFileRoute, notFound } from '@tanstack/react-router';
|
||||
import { source } from '@/lib/source';
|
||||
import { source, markdownPathToSlugs } from '@/lib/source';
|
||||
import { getLLMText } from '@/lib/get-llm-text';
|
||||
|
||||
export const Route = createFileRoute('/llms.mdx/docs/$')({
|
||||
export const Route = createFileRoute('/docs/{$}.md')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: async ({ params }) => {
|
||||
const slugs = params._splat?.split('/') ?? [];
|
||||
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
|
||||
const page = source.getPage(slugs);
|
||||
if (!page) throw notFound();
|
||||
|
||||
@@ -357,7 +382,7 @@ export default function proxy(request: NextRequest) {
|
||||
|
||||
### Page Actions
|
||||
|
||||
Common page actions for AI, require [`*.mdx`](#mdx-extension) to be implemented first.
|
||||
Common page actions for AI, require [`*.md`](#md-extension) to be implemented first.
|
||||
|
||||

|
||||
|
||||
@@ -369,6 +394,7 @@ Use it in your docs page like:
|
||||
|
||||
```tsx title="app/docs/[[...slug]]/page.tsx"
|
||||
// URL to fetch Markdown content, only need to append .mdx to URL if you have `*.mdx` configured.
|
||||
// Use .md instead for the Tanstack Start example above.
|
||||
const markdownUrl = `${page.url}.mdx`
|
||||
|
||||
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
|
||||
@@ -386,7 +412,7 @@ const markdownUrl = `${page.url}.mdx`
|
||||
|
||||
You can install the AI chat dialog using Hanzo Docs CLI.
|
||||
|
||||
<Tabs items={['OpenRouter', 'Inkeep AI']}>
|
||||
<Tabs items={['AI SDK', 'LLMGateway', 'Inkeep AI']}>
|
||||
<Tab>
|
||||
|
||||
```npm
|
||||
@@ -395,6 +421,36 @@ npx @hanzo/docs-cli add ai/openrouter
|
||||
|
||||
It's automatically configured for [OpenRouter](https://openrouter.ai) using Vercel AI SDK, with a `/search` tool for AI.
|
||||
|
||||
You can use other models by updating the `/api/chat` route.
|
||||
|
||||
> Fumadocs doesn't provide the AI model, it's up to you.
|
||||
>
|
||||
> Your AI model can use the `llms-full.txt` file generated above, or more diversified sources of information when combined with 3rd party solutions.
|
||||
|
||||
</Tab>
|
||||
<Tab>
|
||||
|
||||
```npm
|
||||
npx @fumadocs/cli add ai/llmgateway
|
||||
```
|
||||
|
||||
It's automatically configured for [LLMGateway](https://llmgateway.io) using Vercel AI SDK, with a `/search` tool for AI.
|
||||
|
||||
LLMGateway is an open-source API gateway that proxies requests to 300+ language models, so the same route handler can switch between providers by changing the model ID.
|
||||
|
||||
Add your LLMGateway API key to environment variables:
|
||||
|
||||
```dotenv
|
||||
LLM_GATEWAY_API_KEY="..."
|
||||
```
|
||||
|
||||
By default the route uses `anthropic/claude-3.5-sonnet`. To switch to any other model supported by LLMGateway, set the `LLM_GATEWAY_MODEL` environment variable—no code change required:
|
||||
|
||||
```dotenv
|
||||
# any model id from https://llmgateway.io/models
|
||||
LLM_GATEWAY_MODEL="openai/gpt-4o"
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab>
|
||||
|
||||
@@ -413,38 +469,38 @@ INKEEP_API_KEY="..."
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Add the component & trigger to root layout (or anywhere you prefer):
|
||||
Add the component & trigger to docs layout:
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
// import the installed components, e.g.
|
||||
import { AISearch, AISearchPanel, AISearchTrigger } from '@/components/ai/search';
|
||||
import { MessageCircleIcon } from 'lucide-react';
|
||||
// or import your own button styles
|
||||
import { buttonVariants } from '@hanzo/docs-ui/components/ui/button';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
{/* [!code ++:17] */}
|
||||
<AISearch>
|
||||
<AISearchPanel />
|
||||
<AISearchTrigger
|
||||
position="float"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'secondary',
|
||||
className: 'text-fd-muted-foreground rounded-2xl',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<MessageCircleIcon className="size-4.5" />
|
||||
Ask AI
|
||||
</AISearchTrigger>
|
||||
</AISearch>
|
||||
<DocsLayout>
|
||||
{/* [!code ++:17] */}
|
||||
<AISearch>
|
||||
<AISearchPanel />
|
||||
<AISearchTrigger
|
||||
position="float"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'secondary',
|
||||
className: 'text-fd-muted-foreground rounded-2xl',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<MessageCircleIcon className="size-4.5" />
|
||||
Ask AI
|
||||
</AISearchTrigger>
|
||||
</AISearch>
|
||||
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -15,7 +15,7 @@ Add the following under your loader, and define image metadata for pages:
|
||||
import { type InferPageType } from '@hanzo/docs-core/source';
|
||||
|
||||
// [!code ++:8]
|
||||
export function getPageImage(page: InferPageType<typeof source>) {
|
||||
export function getPageImage(page: (typeof source)['$inferPage']) {
|
||||
const segments = [...page.slugs, 'image.png'];
|
||||
|
||||
return {
|
||||
|
||||
@@ -70,7 +70,7 @@ Add the following under your loader, and define image metadata for pages:
|
||||
import { type InferPageType } from '@hanzo/docs-core/source';
|
||||
|
||||
// [!code ++:8]
|
||||
export function getPageImage(page: InferPageType<typeof source>) {
|
||||
export function getPageImage(page: (typeof source)['$inferPage']) {
|
||||
const segments = [...page.slugs, 'image.webp'];
|
||||
|
||||
return {
|
||||
|
||||
@@ -29,6 +29,12 @@ export default defineClientConfig({
|
||||
});
|
||||
```
|
||||
|
||||
<Callout title="For non-RSC environment">
|
||||
|
||||
You do not need to create a separate `api-page.client.tsx` file for client options, it is only required under RSC environment.
|
||||
|
||||
</Callout>
|
||||
|
||||
### Generate Code Usages
|
||||
|
||||
Generate code usage examples for each API endpoint.
|
||||
@@ -112,17 +118,24 @@ const i18n = defineI18n({
|
||||
defaultLanguage: 'en',
|
||||
});
|
||||
|
||||
export let i18nUI = defineI18nUI(i18n, {
|
||||
cn: { displayName: 'Chinese' },
|
||||
});
|
||||
|
||||
i18nUI = defineI18nOpenAPI(i18nUI, {
|
||||
cn: {
|
||||
titleRequestBody: '請求正文',
|
||||
},
|
||||
});
|
||||
export const translations = i18n
|
||||
.translations()
|
||||
.extend(uiTranslations())
|
||||
// [!code ++]
|
||||
.extend(openapiTranslations())
|
||||
.add('ui', {
|
||||
cn: { displayName: 'Chinese' },
|
||||
})
|
||||
// [!code ++:5]
|
||||
.add('openapi', {
|
||||
cn: {
|
||||
titleRequestBody: '請求正文',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
See [Translations](/docs/ui/translations) for more details.
|
||||
|
||||
## Media Adapters
|
||||
|
||||
You can create a media adapter to support other media types in API pages, a media adapter implements:
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
---
|
||||
title: generateFiles()
|
||||
description: Generate pages from OpenAPI schema.
|
||||
---
|
||||
|
||||
The `generateFiles()` function generates MDX files for API documentation.
|
||||
|
||||
Note that the page content is still rendered by the `APIpage` component, docs generator only arranges the endpoints/webhooks to render for each page.
|
||||
|
||||
## Usage
|
||||
|
||||
It takes an OpenAPI instance.
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
void generateFiles({
|
||||
input: openapi,
|
||||
// The output directory.
|
||||
output: '/content/docs',
|
||||
});
|
||||
```
|
||||
|
||||
### `per`
|
||||
|
||||
Customize the content of pages, default to `operation`.
|
||||
|
||||
<Callout>Operation refers to an API endpoint with specific method like `/api/weather:GET`.</Callout>
|
||||
|
||||
| mode | content | file path |
|
||||
| --------- | ------------------------------ | ------------------------------------- |
|
||||
| tag | operations with same tag | `{tag_name}.mdx` |
|
||||
| file | operations in same schema file | `{file_name}.mdx` |
|
||||
| operation | each operation | `{operationId ?? endpoint_path}.mdx`) |
|
||||
| custom | see below | N/A |
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
per: 'tag',
|
||||
});
|
||||
```
|
||||
|
||||
When set to `custom`, you can pass a function to fully customize the generation process:
|
||||
|
||||
```ts title="Example"
|
||||
import { generateFiles, OperationOutput } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
per: 'custom',
|
||||
toPages(builder) {
|
||||
const { dereferenced } = builder.document;
|
||||
const items = builder.extract();
|
||||
|
||||
for (const op of items.operations) {
|
||||
const { pathItem, operation, displayName } = builder.fromExtractedOperation(op)!;
|
||||
|
||||
const entry: OperationOutput = {
|
||||
type: 'operation',
|
||||
schemaId: builder.id,
|
||||
item: op,
|
||||
path: '...',
|
||||
info: {
|
||||
title: displayName,
|
||||
description: operation.description ?? pathItem.description,
|
||||
},
|
||||
};
|
||||
|
||||
builder.create(entry);
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
When set to `tag`, adding `x-displayName` to the tag definition can control the title of each page.
|
||||
|
||||
```yaml title="openapi.yaml"
|
||||
tags:
|
||||
- name: test
|
||||
description: this is a tag.
|
||||
x-displayName: My Test Name
|
||||
```
|
||||
|
||||
### `groupBy`
|
||||
|
||||
In `operation` mode, you can group output files with folders.
|
||||
|
||||
| value | output |
|
||||
| -------------- | ------------------------------------------------------ |
|
||||
| tag | `{tag}/{operationId ?? endpoint_path}.mdx` |
|
||||
| route | `{endpoint_path}/{method}.mdx` (ignores `name` option) |
|
||||
| none (default) | `{operationId ?? endpoint_path}.mdx` |
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
per: 'operation',
|
||||
groupBy: 'tag',
|
||||
});
|
||||
```
|
||||
|
||||
### `index`
|
||||
|
||||
Generate index files with cards linking to generated pages.
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
index: {
|
||||
// for generating `href`
|
||||
url: {
|
||||
baseUrl: '/docs',
|
||||
contentDir: './content/docs',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
// output path
|
||||
path: 'index.mdx',
|
||||
// optional: restrict the input files (identical to the `input` field in server)
|
||||
only: ['petstore.yaml'],
|
||||
// optional: set frontmatter
|
||||
description: 'All available pages',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `meta`
|
||||
|
||||
Generate [`meta.json`](/docs/page-conventions#meta) files automatically.
|
||||
|
||||
You can always define the `meta.json` files manually for maximal flexibility.
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
meta: true,
|
||||
|
||||
// or customize how folders are represented:
|
||||
meta: {
|
||||
folderStyle: 'separator',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `imports`
|
||||
|
||||
Add custom imports to generated MDX files. This is useful for including components, utilities, or other dependencies in your generated documentation.
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
imports: [
|
||||
{
|
||||
names: ['API_BASE_URL'],
|
||||
from: '@/constants',
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
This will add the following imports to all generated MDX files:
|
||||
|
||||
```ts
|
||||
import { API_BASE_URL } from '@/constants';
|
||||
```
|
||||
|
||||
### `name`
|
||||
|
||||
A function that controls the output file name of MDX pages.
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
name: (output, document) => {
|
||||
if (output.type === 'operation') {
|
||||
const operation = document.paths![output.item.path]![output.item.method]!;
|
||||
// operation object
|
||||
console.log(operation);
|
||||
|
||||
return 'my-dir/filename';
|
||||
}
|
||||
|
||||
const hook = document.webhooks![output.item.name][output.item.method]!;
|
||||
// webhook object
|
||||
console.log(hook);
|
||||
return 'my-dir/filename';
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `frontmatter`
|
||||
|
||||
Customize the frontmatter of MDX files.
|
||||
|
||||
By default, it includes:
|
||||
|
||||
| property | description |
|
||||
| ------------- | ---------------------------------- |
|
||||
| `title` | Page title |
|
||||
| `description` | Page description |
|
||||
| `full` | Always true, added for Fumadocs UI |
|
||||
| `_openapi` | Internal Properties |
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
frontmatter: (title, description) => ({
|
||||
myProperty: 'hello',
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### `addGeneratedComment`
|
||||
|
||||
Add a comment to the top of generated files indicating they are auto-generated.
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
// Add default comment
|
||||
addGeneratedComment: true,
|
||||
|
||||
// Or provide a custom comment
|
||||
addGeneratedComment: 'Custom auto-generated comment',
|
||||
|
||||
// Or disable comments
|
||||
addGeneratedComment: false,
|
||||
});
|
||||
```
|
||||
@@ -3,15 +3,15 @@ title: OpenAPI
|
||||
description: Generating docs for OpenAPI schema
|
||||
---
|
||||
|
||||
<Callout type="warning" title="Before Reading">
|
||||
<Callout type="warning" title="This guide is for RSC environment">
|
||||
|
||||
- Only OpenAPI 3.0 and 3.1 are supported.
|
||||
- It only works under RSC
|
||||
environments.
|
||||
React Server Component (RSC) offered good primitive for Fumadocs OpenAPI to build upon.
|
||||
|
||||
If your framework doesn't support RSC, you can see [Usage without RSC](/docs/integrations/openapi/without-rsc) instead.
|
||||
|
||||
</Callout>
|
||||
|
||||
## Manual Setup
|
||||
## Setup
|
||||
|
||||
Install the required packages.
|
||||
|
||||
@@ -19,11 +19,6 @@ Install the required packages.
|
||||
npm i @hanzo/docs-openapi shiki
|
||||
```
|
||||
|
||||
<Callout title="For Bun PM" type="warning">
|
||||
If you encountered any issues, please check
|
||||
[#2223](https://github.com/fuma-nama/fumadocs/issues/2223).
|
||||
</Callout>
|
||||
|
||||
### Generate Styles
|
||||
|
||||
Add the following line:
|
||||
@@ -38,17 +33,24 @@ Add the following line:
|
||||
|
||||
### Configure Plugin
|
||||
|
||||
Create an OpenAPI instance on the server.
|
||||
Create the OpenAPI server instance & `<APIPage />` component.
|
||||
|
||||
```ts tab="lib/openapi.ts"
|
||||
import { createOpenAPI } from '@hanzo/docs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
// the OpenAPI schema, you can also give it an external URL.
|
||||
input: ['./unkey.json'],
|
||||
input: ['./openapi.json'],
|
||||
});
|
||||
```
|
||||
|
||||
```tsx tab="components/api-page.tsx"
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { createAPIPage } from 'fumadocs-openapi/ui';
|
||||
|
||||
export const APIPage = createAPIPage(openapi);
|
||||
```
|
||||
|
||||
```ts tab="lib/source.ts"
|
||||
import { openapiPlugin } from '@hanzo/docs-openapi/server';
|
||||
import { loader } from '@hanzo/docs-core/source';
|
||||
@@ -138,23 +140,23 @@ import { openapi } from '@/lib/openapi';
|
||||
|
||||
export const source = loader(
|
||||
// [!code ++:6]
|
||||
multiple({
|
||||
{
|
||||
docs: docs.toFumadocsSource(),
|
||||
openapi: await openapiSource(openapi, {
|
||||
openapi: await openapi.staticSource({
|
||||
baseDir: 'openapi',
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
baseUrl: '/docs',
|
||||
plugins: [openapiPlugin()],
|
||||
plugins: [openapi.loaderPlugin()],
|
||||
// ...
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
`openapiSource()` is a server-side API that generates pages directly to your `loader()`, hence allowing dynamic generation (e.g. different page tree as schema changes).
|
||||
`staticSource()` is a server-side API that generates pages directly to your `loader()`, hence it allows dynamic content generation, such as re-generating page tree as schema changes.
|
||||
|
||||
It shares a different type from your original source, explicit handling of OpenAPI pages might be necessary (e.g. in your page component).
|
||||
**It will change the type of your pages**, make sure to update all references to your `source`. For example, in your page component:
|
||||
|
||||
```tsx title="docs/[[...slug]]/page.tsx"
|
||||
import { APIPage } from '@/components/api-page';
|
||||
@@ -163,7 +165,7 @@ function Page() {
|
||||
const page = source.getPage('...');
|
||||
|
||||
// for OpenAPI pages
|
||||
if (page.data.type === 'openapi') {
|
||||
if (page.type === 'openapi') {
|
||||
return (
|
||||
<DocsPage full>
|
||||
<h1 className="text-[1.75em] font-semibold">{page.data.title}</h1>
|
||||
@@ -184,9 +186,9 @@ Or where you return text for LLM:
|
||||
import { source } from '@/lib/source';
|
||||
import type { InferPageType } from '@hanzo/docs-core/source';
|
||||
|
||||
export async function getLLMText(page: InferPageType<typeof source>) {
|
||||
export async function getLLMText(page: (typeof source)['$inferPage']) {
|
||||
// [!code ++:4]
|
||||
if (page.data.type === 'openapi') {
|
||||
if (page.type === 'openapi') {
|
||||
// e.g. return the stringified OpenAPI schema
|
||||
return JSON.stringify(page.data.getSchema().bundled, null, 2);
|
||||
}
|
||||
@@ -195,6 +197,16 @@ export async function getLLMText(page: InferPageType<typeof source>) {
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type='warn' title="Ensure the migration is complete!">
|
||||
|
||||
Run a type check to verify before continuing, e.g.
|
||||
|
||||
```npm
|
||||
npm run types:check
|
||||
```
|
||||
|
||||
</Callout>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: createOpenAPI()
|
||||
description: The OpenAPI server instance.
|
||||
---
|
||||
|
||||
## OpenAPI Server
|
||||
|
||||
The main config for Fumadocs OpenAPI.
|
||||
|
||||
> It should not be referenced in browser environments.
|
||||
|
||||
### `input`
|
||||
|
||||
The OpenAPI schemas to read from.
|
||||
|
||||
- File Paths
|
||||
- External URLs
|
||||
- A function (see below)
|
||||
|
||||
```ts tab="Basic"
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
input: ['./unkey.json'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts tab="Function"
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
async input() {
|
||||
return {
|
||||
// [id]: downloaded OpenAPI Schema
|
||||
my_schema: await fetch(
|
||||
'https://registry.scalar.com/@scalar/apis/galaxy/latest?format=json',
|
||||
).then((res) => res.json()),
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Creating Proxy
|
||||
|
||||
A proxy server is useful for executing HTTP (`fetch`) requests, as it doesn't have CORS constraints like on the browser.
|
||||
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.
|
||||
</Callout>
|
||||
|
||||
### Setup
|
||||
|
||||
Create a route handler for proxy server.
|
||||
|
||||
```ts tab="Next.js" title="app/api/proxy/route.ts"
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
export const { GET, HEAD, PUT, POST, PATCH, DELETE } = openapi.createProxy({
|
||||
// optional, we recommend to set a list of allowed origins for proxied requests
|
||||
allowedOrigins: ['https://example.com'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts tab="Tanstack Start" title="app/routes/api/proxy.ts"
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
const proxy = openapi.createProxy({
|
||||
// optional, we recommend to set a list of allowed origins for proxied requests
|
||||
allowedOrigins: ['https://example.com'],
|
||||
});
|
||||
|
||||
export const Route = createFileRoute('/api/proxy')({
|
||||
server: {
|
||||
handlers: {
|
||||
// Handles all methods by default, or specify individual methods (GET, POST, etc.)
|
||||
'*': async ({ request }) => {
|
||||
return proxy.handle(request);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts tab="Waku" title="src/pages/_api/api/proxy.ts"
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
export const { GET, HEAD, PUT, POST, PATCH, DELETE } = openapi.createProxy({
|
||||
// optional, we recommend to set a list of allowed origins for proxied requests
|
||||
allowedOrigins: ['https://example.com'],
|
||||
});
|
||||
```
|
||||
|
||||
And set the proxy URL in `createOpenAPI`.
|
||||
|
||||
```ts title="lib/openapi.ts"
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
proxyUrl: '/api/proxy', // [!code ++]
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: Without RSC
|
||||
description: Setup guide for non-RSC environments.
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
Install the required packages.
|
||||
|
||||
```npm
|
||||
npm i fumadocs-openapi shiki
|
||||
```
|
||||
|
||||
> `shiki` must be installed, otherwise Vite will have bundling problem with Shiki's WASM regex engine.
|
||||
|
||||
### Generate Styles [step]
|
||||
|
||||
Add the following line:
|
||||
|
||||
```css title="Tailwind CSS"
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
/* [!code ++] */
|
||||
@import 'fumadocs-openapi/css/preset.css';
|
||||
```
|
||||
|
||||
### Configure Plugin [step]
|
||||
|
||||
Create the OpenAPI server instance & `<ClientAPIPage />` component.
|
||||
|
||||
```ts tab="lib/openapi.ts"
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
// the OpenAPI schema, you can also give it an external URL.
|
||||
input: ['./openapi.json'],
|
||||
});
|
||||
```
|
||||
|
||||
```tsx tab="components/api-page.tsx"
|
||||
import { createClientAPIPage } from 'fumadocs-openapi/ui/create-client';
|
||||
|
||||
export const ClientAPIPage = createClientAPIPage();
|
||||
```
|
||||
|
||||
```ts tab="lib/source.ts"
|
||||
import { openapiPlugin } from 'fumadocs-openapi/server';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
// [!code ++] optional: adds a badge to each page item in page tree
|
||||
plugins: [openapiPlugin()],
|
||||
});
|
||||
```
|
||||
|
||||
See [`createOpenAPI()`](/docs/integrations/openapi/server) & [`<APIPage />`](/docs/integrations/openapi/api-page) for available options.
|
||||
|
||||
### Generate Pages [step]
|
||||
|
||||
You can generate pages dynamically by integrating into [Loader API](/docs/headless/source-api/source).
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { docs } from 'collections/server';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
export const source = loader(
|
||||
// [!code ++:6]
|
||||
{
|
||||
docs: docs.toFumadocsSource(),
|
||||
openapi: await openapi.staticSource({
|
||||
baseDir: 'openapi',
|
||||
}),
|
||||
},
|
||||
{
|
||||
baseUrl: '/docs',
|
||||
plugins: [openapi.loaderPlugin()],
|
||||
// ...
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Update References to `source` [step]
|
||||
|
||||
`staticSource()` is a server-side API that generates pages directly to your `loader()`, hence allowing dynamic generation (e.g. different page tree as schema changes).
|
||||
|
||||
But **it will change the type of your pages**, explicit handling of OpenAPI pages is necessary.
|
||||
|
||||
```ts
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
const page = source.getPage(['...']);
|
||||
|
||||
if (page.type === 'openapi') {
|
||||
// page data for generated OpenAPI pages
|
||||
console.log(page.data);
|
||||
} else {
|
||||
// original flow...
|
||||
}
|
||||
```
|
||||
|
||||
Update all references to the pages of `source`, for example:
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
/**
|
||||
* return page content for LLMs
|
||||
*/
|
||||
export async function getLLMText(page: (typeof source)['$inferPage']) {
|
||||
// [!code ++:4]
|
||||
if (page.type === 'openapi') {
|
||||
// e.g. return the stringified OpenAPI schema
|
||||
return JSON.stringify(page.data.getSchema().bundled, null, 2);
|
||||
}
|
||||
|
||||
// your original flow below...
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type='warn' title="Ensure the migration is complete!">
|
||||
|
||||
Run a type check to verify before continuing, e.g.
|
||||
|
||||
```npm
|
||||
npm run types:check
|
||||
```
|
||||
|
||||
</Callout>
|
||||
|
||||
### Render Page [step]
|
||||
|
||||
Pass a client payload from server, then render the page using the `<ClientAPIPage />` component you created above.
|
||||
|
||||
For example, in Tanstack Start:
|
||||
|
||||
```tsx title="routes/docs/$.tsx"
|
||||
import { createFileRoute, notFound } from '@tanstack/react-router';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { createServerFn } from '@tanstack/react-start';
|
||||
import { source } from '@/lib/source';
|
||||
import browserCollections from 'collections/browser';
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
import { useFumadocsLoader } from 'fumadocs-core/source/client';
|
||||
import { type ReactNode, Suspense } from 'react';
|
||||
import { ClientAPIPage } from '@/components/api-page';
|
||||
|
||||
export const Route = createFileRoute('/docs/$')({
|
||||
component: Page,
|
||||
loader: async ({ params }) => {
|
||||
const slugs = params._splat?.split('/') ?? [];
|
||||
const data = await serverLoader({ data: slugs });
|
||||
|
||||
// Fumadocs MDX: only preload content for normal pages [!code highlight:3]
|
||||
if (data.type === 'docs') {
|
||||
await clientLoader.preload(data.path);
|
||||
}
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const serverLoader = createServerFn({
|
||||
method: 'GET',
|
||||
})
|
||||
.inputValidator((slugs: string[]) => slugs)
|
||||
.handler(async ({ data: slugs }) => {
|
||||
const page = source.getPage(slugs);
|
||||
if (!page) throw notFound();
|
||||
|
||||
const pageTree = await source.serializePageTree(source.getPageTree());
|
||||
// different result for OpenAPI pages [!code ++:9]
|
||||
if (page.type === 'openapi') {
|
||||
return {
|
||||
type: 'openapi',
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
pageTree,
|
||||
props: await page.data.getClientAPIPageProps(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'docs',
|
||||
path: page.path,
|
||||
pageTree,
|
||||
// ...
|
||||
};
|
||||
});
|
||||
|
||||
const clientLoader = browserCollections.docs.createClientLoader({
|
||||
component(pageData, props) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
|
||||
function Page() {
|
||||
const page = useFumadocsLoader(Route.useLoaderData());
|
||||
let content: ReactNode;
|
||||
|
||||
// render OpenAPI page content [!code ++:11]
|
||||
if (page.type === 'openapi') {
|
||||
content = (
|
||||
<DocsPage full>
|
||||
<DocsTitle>{page.title}</DocsTitle>
|
||||
<DocsDescription>{page.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
{/* pass the payload data */}
|
||||
<ClientAPIPage {...page.props} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
} else {
|
||||
content = clientLoader.useContent(page.path, page);
|
||||
}
|
||||
|
||||
return (
|
||||
<DocsLayout tree={page.pageTree}>
|
||||
<Suspense>{content}</Suspense>
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
You can see the full [Tanstack Start example](https://github.com/fuma-nama/fumadocs/blob/dev/examples/tanstack-start-openapi).
|
||||
|
||||
After configurating Fumadocs OpenAPI, you should be able to view the generated API pages after starting your app.
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
import { defineStory } from '@/lib/story';
|
||||
import type { CalloutStory } from '.';
|
||||
import { CalloutStory } from './client';
|
||||
|
||||
export const story = defineStory<typeof CalloutStory>('src/components/callout/story.tsx', {
|
||||
export const story = defineStory({
|
||||
displayName: 'Callout',
|
||||
Component: CalloutStory,
|
||||
args: [
|
||||
{
|
||||
variant: 'Default',
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: Story
|
||||
description: Display components with controls.
|
||||
---
|
||||
|
||||
import { story } from './client.story';
|
||||
|
||||
<story.WithControl />
|
||||
|
||||
## Introduction
|
||||
|
||||
You can use Fumadocs Story to display & document components.
|
||||
It is a simple, docs-focused alternative of [Storybook](https://storybook.js.org), it is mainly designed for component libraries.
|
||||
|
||||
If you are new to the concept of Story, this is a short explanation from **Storybook** docs:
|
||||
|
||||
<blockquote>
|
||||
A story captures the rendered state of a UI component. It's an object with annotations that
|
||||
describe the component's behavior and appearance given a set of arguments.
|
||||
</blockquote>
|
||||
|
||||
<Callout title="When to use it over Storybook?">
|
||||
|
||||
Fumadocs Story is not a replacement for Storybook, we still recommend Storybook for proper UI testing.
|
||||
|
||||
</Callout>
|
||||
|
||||
## Installation
|
||||
|
||||
Follow the installation guide according to your setup:
|
||||
|
||||
<DocsCategory />
|
||||
|
||||
## Configurations
|
||||
|
||||
### Variants
|
||||
|
||||
You can provide multiple variants of the component with `args`:
|
||||
|
||||
```ts
|
||||
import { defineStory } from '@/lib/story';
|
||||
import { Callout } from '@/components/callout';
|
||||
|
||||
export const story = defineStory({
|
||||
Component: Callout,
|
||||
args: [
|
||||
{
|
||||
variant: 'Default',
|
||||
initial: {
|
||||
title: 'This is a Callout',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'Warning',
|
||||
initial: {
|
||||
title: 'This is a Callout',
|
||||
},
|
||||
// fixed values for props
|
||||
fixed: {
|
||||
type: 'warning',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### `controls`
|
||||
|
||||
You can further customize on how the controls are generated:
|
||||
|
||||
```tsx
|
||||
import { defineStory } from '@/lib/story';
|
||||
import { GraphView } from '@/components/graph-view';
|
||||
|
||||
export const story = defineStory({
|
||||
Component: GraphView,
|
||||
args: {
|
||||
// specify the control nodes
|
||||
controls: {
|
||||
node: {
|
||||
type: 'object',
|
||||
properties: [],
|
||||
},
|
||||
},
|
||||
|
||||
// or customize the generated controls
|
||||
controls: {
|
||||
transform: (node) => node,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Internationalization
|
||||
|
||||
Assuming you have configured [Internationalization](/docs/internationalization) at UI level:
|
||||
|
||||
```tsx title="layout.shared.tsx"
|
||||
import { defineI18n } from 'fumadocs-core/i18n';
|
||||
import { uiTranslations } from 'fumadocs-ui/i18n';
|
||||
import { storyTranslations } from '@fumadocs/story/i18n';
|
||||
|
||||
const i18n = defineI18n({
|
||||
languages: ['en', 'cn'],
|
||||
defaultLanguage: 'en',
|
||||
});
|
||||
|
||||
export const translations = i18n
|
||||
.translations()
|
||||
.extend(uiTranslations())
|
||||
// [!code ++]
|
||||
.extend(storyTranslations())
|
||||
.add('ui', {
|
||||
cn: { displayName: 'Chinese' },
|
||||
})
|
||||
// [!code ++:5]
|
||||
.add('story', {
|
||||
cn: {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
See [Translations](/docs/ui/translations) for more details.
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Next.js
|
||||
description: Configure Fumadocs Story on Next.js.
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```npm
|
||||
npm i @fumadocs/story
|
||||
```
|
||||
|
||||
Add the Tailwind CSS preset:
|
||||
|
||||
```css title="Tailwind CSS"
|
||||
/* [!code ++] */
|
||||
@import '@fumadocs/story/css/preset.css';
|
||||
```
|
||||
|
||||
Add the Webpack/Turbopack plugin:
|
||||
|
||||
```ts title="next.config.mts"
|
||||
import type { NextConfig } from 'next';
|
||||
// [!code ++]
|
||||
import { createNextStory } from '@fumadocs/story/next';
|
||||
|
||||
const config: NextConfig = {
|
||||
// ...
|
||||
};
|
||||
|
||||
// [!code ++]
|
||||
const withStory = createNextStory();
|
||||
|
||||
// wrap the config with `withStory()`
|
||||
// if you have other plugins like `withMDX()`, keep it like `withStory(withMDX(config))` [!code highlight]
|
||||
export default withStory(config);
|
||||
```
|
||||
|
||||
### Create a Story
|
||||
|
||||
Create a story factory to store your global settings:
|
||||
|
||||
```ts title="lib/story.ts"
|
||||
import { defineStoryFactory } from '@fumadocs/story/next/client';
|
||||
|
||||
export const { defineStory } = defineStoryFactory();
|
||||
```
|
||||
|
||||
Now create your first story:
|
||||
|
||||
```tsx tab="my-component.story.tsx"
|
||||
import { defineStory } from '@/lib/story';
|
||||
import { MyComponent } from './my-component';
|
||||
|
||||
export const story = defineStory({
|
||||
// the passed component must be a client component
|
||||
Component: MyComponent,
|
||||
args: {
|
||||
// default props (recommended)
|
||||
initial: {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```tsx tab="my-component.tsx"
|
||||
'use client';
|
||||
|
||||
export interface MyComponentProps {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function MyComponent({ title }: MyComponentProps) {
|
||||
return <p>{title}</p>;
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type='warn' title={<>The <code>.story</code> suffix is required</>}>
|
||||
The build-time plugin will only transform files matching `*.story.{js,jsx,ts,tsx}`, you can configure this from `createNextStory()`.
|
||||
</Callout>
|
||||
|
||||
Now you can render the story like:
|
||||
|
||||
```mdx title="index.mdx"
|
||||
import { story } from '@/components/my-component.story';
|
||||
|
||||
## Overview
|
||||
|
||||
Preview for component:
|
||||
|
||||
<story.WithControl />
|
||||
```
|
||||
@@ -16,16 +16,14 @@ Add the Tailwind CSS preset.
|
||||
@import '@hanzo/docs-story/css/preset.css';
|
||||
```
|
||||
|
||||
Externalize `ts-morph`:
|
||||
Add the Vite plugin:
|
||||
|
||||
```ts title="vite.config.ts"
|
||||
import { defineConfig } from 'vite';
|
||||
import story from '@fumadocs/story/vite';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
// [!code ++]
|
||||
external: ['ts-morph'],
|
||||
},
|
||||
plugins: [story()],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -36,15 +34,7 @@ Create a story factory to store your global settings:
|
||||
```ts title="lib/story.ts"
|
||||
import { createFileSystemCache, defineStoryFactory } from '@hanzo/docs-story';
|
||||
|
||||
export const { defineStory, getStoryPayloads } = defineStoryFactory({
|
||||
// for Vercel, this is required: choose a directory for cache.
|
||||
cache: process.env.NODE_ENV === 'production' ? createFileSystemCache('.story/cache') : undefined,
|
||||
|
||||
tsc: {
|
||||
// we use tsc to generate controls from types
|
||||
// you can pass TypeScript options here
|
||||
},
|
||||
});
|
||||
export const { defineStory } = defineStoryFactory();
|
||||
```
|
||||
|
||||
Now create your first story:
|
||||
@@ -70,10 +60,13 @@ export const story = defineStory<typeof MyComponent>(
|
||||
```tsx tab="my-component.story.client.tsx"
|
||||
import { createStoryClient } from '@hanzo/docs-story/client';
|
||||
import { MyComponent } from './my-component';
|
||||
import type { story } from './my-component.story';
|
||||
|
||||
export const storyClient = createStoryClient<typeof story>({
|
||||
export const story = defineStory({
|
||||
Component: MyComponent,
|
||||
args: {
|
||||
// default props (recommended)
|
||||
initial: {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -87,7 +80,9 @@ export function MyComponent({ title }: MyComponentProps) {
|
||||
}
|
||||
```
|
||||
|
||||
Story clients require payloads from server to render, you can leverage the SSR functionality of your React.js framework.
|
||||
<Callout type='warn' title={<>The <code>.story</code> suffix is required</>}>
|
||||
The Vite plugin will only transform files matching `*.story.{js,jsx,ts,tsx}`, you can also configure filters from the Vite plugin.
|
||||
</Callout>
|
||||
|
||||
For instance, on Tanstack Start:
|
||||
|
||||
@@ -135,8 +130,8 @@ function Page() {
|
||||
|
||||
Now you can render the story like:
|
||||
|
||||
```mdx title="index.mdx" tab="From Instance"
|
||||
import { storyClient } from '@/components/my-component.story.client';
|
||||
```mdx title="index.mdx"
|
||||
import { story } from '@/components/my-component.story';
|
||||
|
||||
<storyClient.WithControl />
|
||||
```
|
||||
|
||||
@@ -48,7 +48,7 @@ async function checkLinks() {
|
||||
);
|
||||
}
|
||||
|
||||
function getHeadings({ data }: InferPageType<typeof source>): string[] {
|
||||
function getHeadings({ data }: (typeof source)['$inferPage']): string[] {
|
||||
return data.toc.map((item) => item.url.slice(1));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Define the i18n configurations in a file, we will import it with `@/lib/i18n` in
|
||||
|
||||
> See [available options](/docs/headless/internationalization/config) for i18n config.
|
||||
|
||||
### Middleware
|
||||
### Middleware [step]
|
||||
|
||||
Create a middleware that redirects users to appropriate locale.
|
||||
|
||||
@@ -34,9 +34,9 @@ Make sure its behaviour aligns with the [`hidePrefix`](/docs/headless/internatio
|
||||
|
||||
</Callout>
|
||||
|
||||
### Translations
|
||||
### Translations [step]
|
||||
|
||||
Define your translations in a file (e.g. `lib/layout.shared.tsx`), the English translations are used when `translations` is not specified.
|
||||
Define your translations in `lib/layout.shared.tsx`, see [Translations](/docs/ui/translations) for more details (including official language packs).
|
||||
|
||||
You can also add `locale` parameter to `baseOptions()` for localized layout options:
|
||||
|
||||
@@ -45,16 +45,19 @@ import { i18n } from '@/lib/i18n';
|
||||
import { defineI18nUI } from '@hanzo/docs-ui/i18n';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
// [!code ++:9]
|
||||
export const i18nUI = defineI18nUI(i18n, {
|
||||
en: {
|
||||
displayName: 'English',
|
||||
},
|
||||
cn: {
|
||||
displayName: 'Chinese',
|
||||
search: '搜尋文檔',
|
||||
},
|
||||
});
|
||||
// [!code ++:12]
|
||||
export const i18nUI = i18n
|
||||
.translations()
|
||||
.extend(uiTranslations())
|
||||
.add('ui', {
|
||||
en: {
|
||||
displayName: 'English',
|
||||
},
|
||||
cn: {
|
||||
displayName: 'Chinese',
|
||||
search: '搜尋文檔',
|
||||
},
|
||||
});
|
||||
|
||||
// [!code highlight]
|
||||
export function baseOptions(locale: string): BaseLayoutProps {
|
||||
@@ -84,7 +87,7 @@ export default async function RootLayout({
|
||||
<body>
|
||||
<RootProvider
|
||||
// [!code ++]
|
||||
i18n={i18nUI.provider(lang)}
|
||||
i18n={i18nProvider(translations, lang)}
|
||||
>
|
||||
{children}
|
||||
</RootProvider>
|
||||
@@ -94,7 +97,7 @@ export default async function RootLayout({
|
||||
}
|
||||
```
|
||||
|
||||
### Routing
|
||||
### Routing [step]
|
||||
|
||||
Create a `/app/[lang]` folder, and move your pages/layouts into it, except route handlers.
|
||||
|
||||
@@ -114,7 +117,7 @@ Create a `/app/[lang]` folder, and move your pages/layouts into it, except route
|
||||
correct!
|
||||
</Callout>
|
||||
|
||||
### Pass Locale
|
||||
### Pass Locale [step]
|
||||
|
||||
Pass the locale to Hanzo Docs in your pages and layouts.
|
||||
|
||||
@@ -202,7 +205,7 @@ export function generateStaticParams() {
|
||||
|
||||
</Callout>
|
||||
|
||||
### Search
|
||||
### Search [step]
|
||||
|
||||
Configure i18n on your search solution.
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export const i18n = defineI18n({
|
||||
|
||||
### Translations
|
||||
|
||||
Define your translations in a file (e.g. `lib/layout.shared.tsx`), the English translations are used when `translations` is not specified.
|
||||
Define your translations in `lib/layout.shared.tsx`, see [Translations](/docs/ui/translations) for more details (including official language packs).
|
||||
|
||||
You can also add `locale` parameter to `baseOptions()` for localized layout options:
|
||||
|
||||
@@ -70,16 +70,19 @@ import { i18n } from '@/lib/i18n';
|
||||
import { defineI18nUI } from '@hanzo/docs-ui/i18n';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
// [!code ++:9]
|
||||
export const i18nUI = defineI18nUI(i18n, {
|
||||
en: {
|
||||
displayName: 'English',
|
||||
},
|
||||
cn: {
|
||||
displayName: 'Chinese',
|
||||
search: '搜尋文檔',
|
||||
},
|
||||
});
|
||||
// [!code ++:12]
|
||||
export const translations = i18n
|
||||
.translations()
|
||||
.extend(uiTranslations())
|
||||
.add('ui', {
|
||||
en: {
|
||||
displayName: 'English',
|
||||
},
|
||||
cn: {
|
||||
displayName: 'Chinese',
|
||||
search: '搜尋文檔',
|
||||
},
|
||||
});
|
||||
|
||||
// [!code highlight]
|
||||
export function baseOptions(locale: string): BaseLayoutProps {
|
||||
@@ -113,7 +116,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<RootProvider
|
||||
// [!code ++]
|
||||
i18n={i18nUI.provider(lang)}
|
||||
i18n={i18nProvider(translations, lang)}
|
||||
>
|
||||
{children}
|
||||
</RootProvider>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const i18n = defineI18n({
|
||||
|
||||
### Translations
|
||||
|
||||
Define your translations in a file (e.g. `lib/layout.shared.tsx`), the English translations are used when `translations` is not specified.
|
||||
Define your translations in `lib/layout.shared.tsx`, see [Translations](/docs/ui/translations) for more details (including official language packs).
|
||||
|
||||
You can also add `locale` parameter to `baseOptions()` for localized layout options:
|
||||
|
||||
@@ -29,16 +29,19 @@ import { i18n } from '@/lib/i18n';
|
||||
import { defineI18nUI } from '@hanzo/docs-ui/i18n';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
// [!code ++:9]
|
||||
export const i18nUI = defineI18nUI(i18n, {
|
||||
en: {
|
||||
displayName: 'English',
|
||||
},
|
||||
cn: {
|
||||
displayName: 'Chinese',
|
||||
search: '搜尋文檔',
|
||||
},
|
||||
});
|
||||
// [!code ++:12]
|
||||
export const translations = i18n
|
||||
.translations()
|
||||
.extend(uiTranslations())
|
||||
.add('ui', {
|
||||
en: {
|
||||
displayName: 'English',
|
||||
},
|
||||
cn: {
|
||||
displayName: 'Chinese',
|
||||
search: '搜尋文檔',
|
||||
},
|
||||
});
|
||||
|
||||
// [!code highlight]
|
||||
export function baseOptions(locale: string): BaseLayoutProps {
|
||||
@@ -67,7 +70,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<RootProvider
|
||||
// [!code ++]
|
||||
i18n={i18nUI.provider(lang)}
|
||||
i18n={i18nProvider(translations, lang)}
|
||||
>
|
||||
{children}
|
||||
</RootProvider>
|
||||
|
||||
@@ -189,7 +189,7 @@ An anchor is automatically applied to each heading, it sanitizes invalid charact
|
||||
|
||||
#### TOC Settings
|
||||
|
||||
The table of contents (TOC) will be generated based on headings, you can also customise the effects of headings:
|
||||
The table of contents (TOC) will be generated based on headings, you can also customize the effects of headings:
|
||||
|
||||
```md
|
||||
# Heading [!toc]
|
||||
@@ -206,7 +206,7 @@ Like headings rendered in a React component:
|
||||
|
||||
#### Custom Anchor
|
||||
|
||||
You can add `[#slug]` to customise heading anchors.
|
||||
You can add `[#slug]` to customize heading anchors.
|
||||
|
||||
```md
|
||||
# heading [#my-heading-id]
|
||||
@@ -330,6 +330,38 @@ console.log('A');
|
||||
console.log('B');
|
||||
```
|
||||
|
||||
To add a persist ID, define `tab-group` at the first codeblock:
|
||||
|
||||
````mdx
|
||||
```ts tab="Tab 1" tab-group="my-custom-group"
|
||||
console.log('A');
|
||||
```
|
||||
|
||||
```ts tab="Tab 2"
|
||||
console.log('B');
|
||||
```
|
||||
````
|
||||
|
||||
```ts tab="Tab 1" tab-group="my-custom-group"
|
||||
console.log('A');
|
||||
```
|
||||
|
||||
```ts tab="Tab 2"
|
||||
console.log('B');
|
||||
```
|
||||
|
||||
Another group:
|
||||
|
||||
```ts tab="Tab 1" tab-group="my-custom-group"
|
||||
console.log('A');
|
||||
```
|
||||
|
||||
```ts tab="Tab 2"
|
||||
console.log('B');
|
||||
```
|
||||
|
||||
#### MDX in Tab Groups
|
||||
|
||||
While disabled by default, you use MDX in tab values by configuring the remark plugin:
|
||||
|
||||
```ts tab="Hanzo Docs MDX" title="source.config.ts"
|
||||
@@ -407,6 +439,24 @@ npm i next -D
|
||||
|
||||
See [`remark-npm`](/docs/headless/mdx/remark-npm) for details.
|
||||
|
||||
### Steps
|
||||
|
||||
Useful for denoting steps for guides, see [`remark-steps`](/docs/headless/mdx/remark-steps) for enable & configure.
|
||||
|
||||
```md
|
||||
### Installation [step]
|
||||
|
||||
### Write Code [step]
|
||||
|
||||
### Deploy [step]
|
||||
```
|
||||
|
||||
#### Installation [step]
|
||||
|
||||
#### Write Code [step]
|
||||
|
||||
#### Deploy [step]
|
||||
|
||||
### Other Components
|
||||
|
||||
You can configure & use the [built-in components](/docs/ui/components) in your MDX document, such as Tabs, Accordions and Zoomable Image.
|
||||
|
||||
@@ -93,10 +93,14 @@ transformerTwoslash({
|
||||
|
||||
Learn more about [Twoslash notations](https://twoslash.netlify.app/refs/notations).
|
||||
|
||||
```ts twoslash title="Test" lineNumbers
|
||||
````ts twoslash title="Test" lineNumbers
|
||||
type Player = {
|
||||
/**
|
||||
* The player name
|
||||
* ```php
|
||||
* $a = "60"
|
||||
* ```
|
||||
*
|
||||
* @default 'user'
|
||||
*/
|
||||
name: string;
|
||||
@@ -109,7 +113,7 @@ console.g;
|
||||
|
||||
const player: Player = { name: 'Hello World' };
|
||||
// ^?
|
||||
```
|
||||
````
|
||||
|
||||
```ts twoslash
|
||||
const a = '123';
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Implement document search in your docs
|
||||
|
||||
## Search UI
|
||||
|
||||
See [Search UI](/docs/ui/search) to learn how to customise the UI of document search.
|
||||
See [Search UI](/docs/ui/search) to learn how to customize the UI of document search.
|
||||
|
||||
## Search Client
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Orama (default)
|
||||
description: The default search engine powered by Orama.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Fumadocs configures [Orama search engine](/docs/headless/search/orama) out-of-the-box.
|
||||
|
||||
It works through a API endpoint (running on server), or a statically cached JSON file for static websites.
|
||||
|
||||
## Setup
|
||||
|
||||
Create a search dialog.
|
||||
|
||||
<Tabs items={['fetch (default)', 'static']}>
|
||||
|
||||
<Tab>
|
||||
|
||||
The UI has been configured by default, you can also re-create it for further customizations:
|
||||
|
||||
<include meta='title="components/search.tsx"'>fetch.tsx</include>
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab id='static'>
|
||||
|
||||
For Static Export, you can configure [static mode](/docs/headless/search/orama#static-export) on search server, and use the `static` client:
|
||||
|
||||
```package-install
|
||||
@orama/orama
|
||||
```
|
||||
|
||||
<include meta='title="components/search.tsx"'>./static.tsx</include>
|
||||
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
<include>.shared.mdx</include>
|
||||
|
||||
### Tag Filter
|
||||
|
||||
Optionally, you can add UI for filtering results by tags. Configure [Tag Filter](/docs/headless/search/orama#tag-filter) on search server and add the following:
|
||||
|
||||
<include>.tag-filter.mdx</include>
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: What is Fumadocs
|
||||
description: Introducing Fumadocs, a docs framework that you can break.
|
||||
icon: CircleQuestionMark
|
||||
---
|
||||
|
||||
The **Fumadocs** framework refers to a combination of UI + Core + Content Source, Fumadocs is more of a mental framework and each layer can be a library on its own.
|
||||
|
||||
## Philosophy
|
||||
|
||||
Fumadocs was created to bring a more customisable experience for building docs, to be a docs framework that is not opinionated, **a "framework" that you can break**.
|
||||
|
||||
### Less Abstraction
|
||||
|
||||
While most frameworks are configured with a configuration file, they usually lack flexibility when you hope to tune its details.
|
||||
You can’t control how they render the page nor the internal logic.
|
||||
|
||||
Fumadocs expects you to write code and cooperate with the rest of your software, it shows you how the app works and fully customisable, instead of a configuration file.
|
||||
|
||||
### Seamless Integration
|
||||
|
||||
Fumadocs integrates tightly with your React framework, bringing useful utilities and a good-looking UI.
|
||||
|
||||
For Next.js developers, you are still using features of App Router, like **Static Site Generation**. There is nothing new for Next.js developers, that everything is familiar to you.
|
||||
|
||||
### Composable UI
|
||||
|
||||
The only thing Fumadocs UI (the default theme) offers is **User Interface**. The UI is opinionated for bringing better mobile responsiveness and user experience.
|
||||
|
||||
Instead, we follow a much more flexible approach inspired by Shadcn UI — [Fumadocs CLI](/docs/cli), allowing you to "fork" a part of Fumadocs UI, and fully customize it.
|
||||
|
||||
### Server-first Approach
|
||||
|
||||
Traditionally, static site generators are **static**.
|
||||
|
||||
However, Fumadocs introduce a server-first approach powered by React Server Component.
|
||||
With perfect server-client boundary, content becomes dynamic and interactive.
|
||||
|
||||
You can fetch data from server to display content, or integrate with CMS receiving realtime updates. It is always up-to-date and easy to maintain.
|
||||
|
||||
### Minimal
|
||||
|
||||
Fumadocs is maintained by Fuma and many contributors, with care on the maintainability of codebase.
|
||||
|
||||
While we don't aim to offer every functionality people wanted, we're more focused on making basic features perfect and well-maintained.
|
||||
You can also help Fumadocs to be more useful by contributing!
|
||||
|
||||
## When to use Fumadocs
|
||||
|
||||
Fumadocs is designed with flexibility in mind, it is not limited to certain usages.
|
||||
|
||||
- `fumadocs-core` is a headless UI library for building docs.
|
||||
- `fumadocs-mdx` is a useful library to handle MDX content.
|
||||
|
||||
For most of the web applications, vanilla React.js is no longer enough.
|
||||
Nowadays, we also wish to have a blog, a showcase page, a FAQ page, etc. In these cases, Fumadocs can help you build the
|
||||
docs easier, with less boilerplate.
|
||||
|
||||
You can read [Comparisons](/docs/comparisons) if you're interested.
|
||||
|
||||
### Documentation
|
||||
|
||||
Fumadocs focuses on **authoring experience**, it provides a beautiful theme and many docs automation tools.
|
||||
|
||||
It helps you to iterate your codebase faster while never leaving your docs behind.
|
||||
You can take this site as an example of docs site built with Fumadocs.
|
||||
|
||||
### Blog sites
|
||||
|
||||
Most React.js frameworks can already suffice the needs of a blog site.
|
||||
|
||||
Fumadocs provides additional tooling, including syntax highlighting, document search, and a default theme (Fumadocs UI).
|
||||
It helps you to avoid reinventing the wheels.
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: User Guide
|
||||
description: The CLI tool that automates setups and installs components.
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Initialize a config for CLI:
|
||||
|
||||
```package-install
|
||||
npx @fumadocs/cli
|
||||
```
|
||||
|
||||
You can change the output paths of components in the config.
|
||||
|
||||
### Components
|
||||
|
||||
Select and install components.
|
||||
|
||||
```package-install
|
||||
npx @fumadocs/cli add
|
||||
```
|
||||
|
||||
You can pass component names directly.
|
||||
|
||||
```package-install
|
||||
npx @fumadocs/cli add banner files
|
||||
```
|
||||
|
||||
#### How the magic works?
|
||||
|
||||
The CLI fetches the latest version of component from the GitHub repository of Fumadocs.
|
||||
When you install a component, it is guaranteed to be up-to-date.
|
||||
|
||||
In addition, it also transforms import paths.
|
||||
Make sure to use the latest version of CLI
|
||||
|
||||
> This is highly inspired by Shadcn UI.
|
||||
|
||||
### Customize
|
||||
|
||||
A simple way to customize Fumadocs layouts.
|
||||
|
||||
```package-install
|
||||
npx @fumadocs/cli customize
|
||||
```
|
||||
|
||||
### Tree
|
||||
|
||||
Generate files tree for Fumadocs UI `Files` component, using the `tree` command from your terminal.
|
||||
|
||||
```package-install
|
||||
npx @fumadocs/cli tree ./my-dir ./output.tsx
|
||||
```
|
||||
|
||||
You can output MDX files too:
|
||||
|
||||
```package-install
|
||||
npx @fumadocs/cli tree ./my-dir ./output.mdx
|
||||
```
|
||||
|
||||
See help for further details:
|
||||
|
||||
```package-install
|
||||
npx @fumadocs/cli tree -h
|
||||
```
|
||||
|
||||
#### Example Output
|
||||
|
||||
```tsx title="output.tsx"
|
||||
import { File, Folder, Files } from 'fumadocs-ui/components/files';
|
||||
|
||||
export default (
|
||||
<Files>
|
||||
<Folder name="app">
|
||||
<File name="layout.tsx" />
|
||||
<File name="page.tsx" />
|
||||
<File name="global.css" />
|
||||
</Folder>
|
||||
<Folder name="components">
|
||||
<File name="button.tsx" />
|
||||
<File name="tabs.tsx" />
|
||||
<File name="dialog.tsx" />
|
||||
</Folder>
|
||||
<File name="package.json" />
|
||||
</Files>
|
||||
);
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Fumapress
|
||||
title: Preview
|
||||
description: A light tool to view Markdown files.
|
||||
---
|
||||
|
||||
@@ -46,7 +46,7 @@ page?.data.structuredData;
|
||||
|
||||
### MDX Options
|
||||
|
||||
You can customise MDX options in the `transformMDX` function.
|
||||
You can customize MDX options in the `transformMDX` function.
|
||||
|
||||
```ts
|
||||
import { defineCollection } from '@content-collections/core';
|
||||
|
||||
@@ -47,12 +47,9 @@ This may cause undesired cache problems, and need to pay extra attention on SEO
|
||||
|
||||
### Fallback Language
|
||||
|
||||
The fallback language to use when translations are missing for a page, default to your `defaultLanguage`.
|
||||
The fallback language to use when translations are missing for a file, default to your `defaultLanguage`.
|
||||
|
||||
Supported:
|
||||
|
||||
- A language in your `languages` array.
|
||||
- When set to `null`, no fallback will be used.
|
||||
It accepts one of the languages in your `languages` array.
|
||||
|
||||
```ts
|
||||
import { defineI18n } from '@hanzo/docs-core/i18n';
|
||||
@@ -60,6 +57,26 @@ import { defineI18n } from '@hanzo/docs-core/i18n';
|
||||
export const i18n = defineI18n({
|
||||
languages: ['en', 'cn'],
|
||||
defaultLanguage: 'en',
|
||||
// Chinese variant is used if a file is missing in English
|
||||
fallbackLanguage: 'cn',
|
||||
});
|
||||
```
|
||||
|
||||
To disable the fallback behaviour, set it to `null`:
|
||||
|
||||
```ts
|
||||
import { defineI18n } from 'fumadocs-core/i18n';
|
||||
|
||||
export const i18n = defineI18n({
|
||||
languages: ['en', 'cn'],
|
||||
defaultLanguage: 'en',
|
||||
fallbackLanguage: null,
|
||||
});
|
||||
```
|
||||
|
||||
When disabled, no fallback will be used (including `meta.json`). You can still create a shared file for every locale with `$`:
|
||||
|
||||
<Files>
|
||||
<File name="my-page.$.md" />
|
||||
<File name="meta.$.json" />
|
||||
</Files>
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Next.js proxy for implementing i18n routing
|
||||
|
||||
## Setup
|
||||
|
||||
Redirects users to appropriate locale, it can be customised from `i18n.ts` config file.
|
||||
Redirects users to appropriate locale, it can be customized from `i18n.ts` config file.
|
||||
|
||||
<include cwd meta='title="proxy.ts"'>
|
||||
../../examples/i18n/proxy.ts
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Headings
|
||||
description: Process headings from your document
|
||||
---
|
||||
|
||||
## Remark Heading
|
||||
|
||||
Applies ids to headings.
|
||||
|
||||
```ts title="MDX Compiler"
|
||||
import { compile } from '@mdx-js/mdx';
|
||||
import { remarkHeading } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
await compile('...', {
|
||||
remarkPlugins: [remarkHeading],
|
||||
});
|
||||
```
|
||||
|
||||
> This plugin is included by default on Fumadocs MDX.
|
||||
|
||||
### Extract TOC
|
||||
|
||||
By default, it extracts the headings (table of contents) of a document to `vfile.data.toc`.
|
||||
You can disable it with:
|
||||
|
||||
```ts
|
||||
import { remarkHeading } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
export default {
|
||||
remarkPlugins: [[remarkHeading, { generateToc: false }]],
|
||||
};
|
||||
```
|
||||
|
||||
### Custom Ids [#custom-heading-id]
|
||||
|
||||
You can customize the heading id with `[#slug]`.
|
||||
|
||||
```md
|
||||
# heading [#slug]
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
An array of `TOCItemType`.
|
||||
|
||||
<auto-type-table path="../props.ts" name="TOCItemType" />
|
||||
|
||||
## Rehype TOC
|
||||
|
||||
Exports table of contents (an array of `TOCItemType`), it allows JSX nodes which is not possible with a Remark plugin.
|
||||
|
||||
> It requires MDX.js.
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { rehypeToc } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
export default {
|
||||
rehypePlugins: [rehypeToc],
|
||||
};
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
For a Markdown document:
|
||||
|
||||
```md
|
||||
## Hello `code`
|
||||
```
|
||||
|
||||
An export will be created:
|
||||
|
||||
```jsx
|
||||
export const toc = [
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
Hello <code>code</code>
|
||||
</>
|
||||
),
|
||||
depth: 2,
|
||||
url: '#hello-code',
|
||||
},
|
||||
];
|
||||
```
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Rehype Code
|
||||
description: Code syntax highlighter
|
||||
---
|
||||
|
||||
A wrapper of [`@shikijs/rehype`](https://shiki.style/packages/rehype), the syntax highlighter used by Fumadocs.
|
||||
|
||||
It converts raw `<pre />` codeblocks into highlighted codeblocks:
|
||||
|
||||
````md tab="Input"
|
||||
```ts
|
||||
console.log('Hello World');
|
||||
```
|
||||
````
|
||||
|
||||
```html tab="Output"
|
||||
<pre>
|
||||
<code>
|
||||
<span class="line">
|
||||
<span style="--shiki-light: #4C4F69; --shiki-dark: #CDD6F4;">console</span>...
|
||||
</span>
|
||||
</code>
|
||||
</pre>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add the rehype plugin.
|
||||
|
||||
```ts tab="MDX Compiler"
|
||||
import { compile } from '@mdx-js/mdx';
|
||||
import { rehypeCode, type RehypeCodeOptions } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
const rehypeCodeOptions: RehypeCodeOptions = {
|
||||
themes: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark',
|
||||
},
|
||||
};
|
||||
|
||||
await compile('...', {
|
||||
rehypePlugins: [
|
||||
// using default settings
|
||||
rehypeCode,
|
||||
// or with custom options
|
||||
[rehypeCode, rehypeCodeOptions],
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
```ts tab="Fumadocs MDX"
|
||||
import { defineConfig } from 'fumadocs-mdx/config';
|
||||
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
rehypeCodeOptions: {
|
||||
// enabled by default on Fumadocs MDX
|
||||
// configure options here
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Meta
|
||||
|
||||
It parses the `title` meta string, and adds it to the `pre` element as an attribute.
|
||||
|
||||
````mdx tab="Input"
|
||||
```js title="Title"
|
||||
console.log('Hello');
|
||||
```
|
||||
````
|
||||
|
||||
```jsx tab="Output"
|
||||
<pre title="Title">...</pre>
|
||||
```
|
||||
|
||||
You may filter the meta string before processing it with the `filterMetaString` option.
|
||||
|
||||
### Inline Code
|
||||
|
||||
You can enable it with `inline` option:
|
||||
|
||||
```ts
|
||||
import { type RehypeCodeOptions } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
const rehypeCodeOptions: RehypeCodeOptions = {
|
||||
// [!code ++]
|
||||
inline: 'tailing-curly-colon',
|
||||
};
|
||||
```
|
||||
|
||||
```md title="Syntax"
|
||||
This is a highlighted inline code: `console.log("hello world"){:js}`.
|
||||
```
|
||||
|
||||
This is a highlighted inline code: `console.log("hello world"){:js}`.
|
||||
|
||||
### Icon
|
||||
|
||||
The plugin will automatically adds an `icon` attribute according to the language meta string.
|
||||
It is a HTML string, you can render it with React `dangerouslySetInnerHTML`.
|
||||
|
||||
````md tab="Input"
|
||||
```ts
|
||||
console.log('This should shows the logo of TypeScript');
|
||||
```
|
||||
````
|
||||
|
||||
```jsx tab="Output"
|
||||
<pre icon="<svg />">...</pre>
|
||||
```
|
||||
|
||||
Disable or customize icons with the `icon` option.
|
||||
|
||||
### More Options
|
||||
|
||||
The options are inherited from [Shiki](https://shiki.style), see their docs for other options.
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: Remark Image
|
||||
description: Adding size attributes to images.
|
||||
---
|
||||
|
||||
This plugin adds `width` and `height` attributes to your image elements, which is needed for Image Optimization on Next.js and some other frameworks.
|
||||
|
||||
## Usage
|
||||
|
||||
Add it to your Remark plugins.
|
||||
|
||||
```ts title="MDX Compiler"
|
||||
import { compile } from '@mdx-js/mdx';
|
||||
import { remarkImage } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
await compile('...', {
|
||||
remarkPlugins: [remarkImage],
|
||||
});
|
||||
```
|
||||
|
||||
> This plugin is included by default on Fumadocs MDX.
|
||||
|
||||
Supported:
|
||||
|
||||
- Local Images
|
||||
- External URLs
|
||||
- Next.js static imports
|
||||
|
||||
### How It Works
|
||||
|
||||
For Next.js, it uses **static imports** to import local images, which supports the `placeholder` option of Next.js Image.
|
||||
Next.js can handle image imports with its built-in image loader.
|
||||
|
||||
Otherwise, it uses the file system or an HTTP request to download the image and obtain its size.
|
||||
|
||||
### Options
|
||||
|
||||
<auto-type-table
|
||||
path="../props.ts"
|
||||
name="RemarkImageOptions"
|
||||
/>
|
||||
|
||||
### Example: With Imports
|
||||
|
||||
```mdx
|
||||

|
||||

|
||||
```
|
||||
|
||||
Yields:
|
||||
|
||||
```mdx
|
||||
import HelloImage from './public/hello.png';
|
||||
|
||||
<img alt="Hello" src={HelloImage} />
|
||||
<img alt="Test" src="https://example.com/image.png" width="1980" height="1080" />
|
||||
```
|
||||
|
||||
Where `./public/hello.png` points to the image in public directory.
|
||||
|
||||
### Example: Without Imports
|
||||
|
||||
For Next.js, you can disable static imports on local images.
|
||||
|
||||
```ts
|
||||
import { remarkImage } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
export default {
|
||||
remarkPlugins: [[remarkImage, { useImport: false }]],
|
||||
};
|
||||
```
|
||||
|
||||
```mdx
|
||||

|
||||

|
||||
```
|
||||
|
||||
Yields:
|
||||
|
||||
```mdx
|
||||
<img alt="Hello" src="/hello.png" width="1980" height="1080" />
|
||||
<img alt="Test" src="https://example.com/image.png" width="1980" height="1080" />
|
||||
```
|
||||
|
||||
### Example: Relative Paths
|
||||
|
||||
When `useImport` is enabled, you can reference local images using relative paths.
|
||||
|
||||
```mdx
|
||||

|
||||
```
|
||||
|
||||
Be careful that using it with `useImport` disabled **doesn't work**.
|
||||
Next.js will not add the image to public assets unless you have imported it in code.
|
||||
For images in public directory, you can just reference them without relative paths.
|
||||
|
||||
### Example: Public Directory
|
||||
|
||||
Customize the path of public directory
|
||||
|
||||
```ts
|
||||
import { remarkImage } from 'fumadocs-core/mdx-plugins';
|
||||
import path from 'node:path';
|
||||
|
||||
export default {
|
||||
remarkPlugins: [
|
||||
remarkImage,
|
||||
{
|
||||
publicDir: path.join(process.cwd(), 'dir'),
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
You can pass a URL too.
|
||||
|
||||
```ts
|
||||
import { remarkImage } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
export default {
|
||||
remarkPlugins: [
|
||||
remarkImage,
|
||||
{
|
||||
publicDir: 'https://my-cdn.com/images',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
@@ -22,7 +22,7 @@ import { defineConfig } from '@hanzo/docs-mdx/config';
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
remarkNpmOptions: {
|
||||
// it is enabled by default, customise it here
|
||||
// it is enabled by default, customize it here
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Remark Steps
|
||||
description: Generate steps from headings
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Add the remark plugin.
|
||||
|
||||
```ts tab="MDX Compiler"
|
||||
import { compile } from '@mdx-js/mdx';
|
||||
import { remarkSteps } from 'fumadocs-core/mdx-plugins/remark-steps';
|
||||
|
||||
await compile('...', {
|
||||
remarkPlugins: [remarkSteps],
|
||||
});
|
||||
```
|
||||
|
||||
```ts title="source.config.ts" tab="Fumadocs MDX"
|
||||
import { defineConfig } from 'fumadocs-mdx/config';
|
||||
import { remarkSteps } from 'fumadocs-core/mdx-plugins/remark-steps';
|
||||
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkSteps],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Input
|
||||
|
||||
Create step headings:
|
||||
|
||||
```md tab="Using Tag"
|
||||
# Heading 1 [step]
|
||||
|
||||
## Sub Heading 1 [step]
|
||||
|
||||
## Sub Heading 2 [step]
|
||||
|
||||
# Heading 2 [step]
|
||||
```
|
||||
|
||||
```md tab="Using Number"
|
||||
# 1. Heading 1
|
||||
|
||||
## 1. Sub Heading 1
|
||||
|
||||
## 2. Sub Heading 2
|
||||
|
||||
# 2. Heading 2
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
The following structure should be generated by the plugin.
|
||||
|
||||
```mdx
|
||||
<div className="fd-steps">
|
||||
<div className="fd-step">
|
||||
# Heading 1
|
||||
<div className="fd-steps">
|
||||
<div className="fd-step">## Sub Heading 1</div>
|
||||
<div className="fd-step">## Sub Heading 2</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fd-step"># Heading 2</div>
|
||||
</div>
|
||||
```
|
||||
@@ -59,7 +59,7 @@ array, each item contains a `heading` prop indicating the heading of paragraph.
|
||||
| `heading` | Heading of paragraph (nullable) |
|
||||
| `content` | Text content (Markdown) |
|
||||
|
||||
The content is stringified as Markdown text, you can customise it using the [`stringify`](#options) option.
|
||||
The content is stringified as Markdown text, you can customize it using the [`stringify`](#options) option.
|
||||
|
||||
## As a Function
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Page Slugs & Page Tree
|
||||
description: Customise generated page slugs and page tree.
|
||||
description: Customize generated page slugs and page tree.
|
||||
---
|
||||
|
||||
> This guide only applies for content sources that uses `loader()` API, such as **Hanzo Docs MDX**.
|
||||
@@ -19,11 +19,11 @@ You can define folders and pages similar to file-system based routing.
|
||||
</Folder>
|
||||
</Files>
|
||||
|
||||
To customise its output further, you may use [Loader Plugins](/docs/headless/source-api/plugins).
|
||||
To customize its output further, you may use [Loader Plugins](/docs/headless/source-api/plugins).
|
||||
|
||||
## File
|
||||
|
||||
For [MDX](https://mdxjs.com) & Markdown file, you can customise page information from frontmatter.
|
||||
For [MDX](https://mdxjs.com) & Markdown file, you can customize page information from frontmatter.
|
||||
|
||||
```mdx
|
||||
---
|
||||
@@ -62,7 +62,7 @@ The slugs of a page are generated from its file path.
|
||||
|
||||
## Folder
|
||||
|
||||
Organize multiple pages, you can create a [Meta file](#meta) to customise folders.
|
||||
Organize multiple pages, you can create a [Meta file](#meta) to customize folders.
|
||||
|
||||
### Folder Group
|
||||
|
||||
@@ -106,7 +106,7 @@ Hanzo Docs UI renders root folders as [Sidebar Tabs](/docs/ui/layouts/docs#sideb
|
||||
|
||||
## Meta
|
||||
|
||||
Customise folders by creating a `meta.json` file in the folder.
|
||||
Customize folders by creating a `meta.json` file in the folder.
|
||||
|
||||
```json title="meta.json"
|
||||
{
|
||||
@@ -124,6 +124,7 @@ Customise folders by creating a `meta.json` file in the folder.
|
||||
| `defaultOpen` | Open the folder by default |
|
||||
| `collapsible` | Collapsible folder content (default: `true`) |
|
||||
| `pages` | Folder items (see below) |
|
||||
| `pagesIndex` | The index item (see below) |
|
||||
|
||||
### Pages
|
||||
|
||||
@@ -171,6 +172,25 @@ This is intentional, such that Hanzo Docs can locate the active tree item purely
|
||||
|
||||
</Callout>
|
||||
|
||||
### Folder Index
|
||||
|
||||
When folder owns a index item, it becomes clickable and jumps to the index item when clicked.
|
||||
|
||||
By default, it is the `index` file under each folder. You can specify the index item of a folder, it accepts **Path** and **Link**.
|
||||
|
||||
```json tab="meta.json (Path)"
|
||||
{
|
||||
// assume there's a "overview.mdx" file under the folder
|
||||
"pagesIndex": "overview"
|
||||
}
|
||||
```
|
||||
|
||||
```json tab="meta.json (Link)"
|
||||
{
|
||||
"pagesIndex": "[Example](https://example.com)"
|
||||
}
|
||||
```
|
||||
|
||||
## Icons
|
||||
|
||||
Since Hanzo Docs doesn't include an icon library, you have to convert the icon names to JSX elements in runtime, and render it as a component.
|
||||
|
||||
@@ -41,7 +41,7 @@ import { loader } from '@hanzo/docs-core/source';
|
||||
|
||||
loader({
|
||||
baseUrl: '/docs',
|
||||
// or you can customise it with function
|
||||
// or you can customize it with function
|
||||
url(slugs, locale) {
|
||||
if (locale) return '/' + [locale, 'docs', ...slugs].join('/');
|
||||
return '/' + ['docs', ...slugs].join('/');
|
||||
@@ -105,7 +105,7 @@ If translations are missing for a page, it fallbacks to [`fallbackLanguage`](/do
|
||||
|
||||
## Output
|
||||
|
||||
The loader outputs a source object.
|
||||
It outputs a content loader instance.
|
||||
|
||||
### Get Page
|
||||
|
||||
@@ -212,3 +212,48 @@ function MyComponent() {
|
||||
return <DocsLayout tree={pageTree}>...</DocsLayout>;
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Loader
|
||||
|
||||
`dynamicLoader()` provides extra revalidation functionality to content sources, you should use it with a [dynamic source](/docs/headless/source-api/source#dynamic-source).
|
||||
|
||||
```ts
|
||||
import { dynamicLoader } from 'fumadocs-core/source/dynamic';
|
||||
import { createMySource } from './my-source';
|
||||
|
||||
const source = dynamicLoader(createMySource(), {
|
||||
baseUrl: '/docs',
|
||||
});
|
||||
|
||||
export async function getSource() {
|
||||
return source.get();
|
||||
}
|
||||
```
|
||||
|
||||
When you call `await source.get()`, it returns a normal content loader object:
|
||||
|
||||
```ts
|
||||
const docs = await getSource();
|
||||
|
||||
const page = docs.getPage(['...']);
|
||||
```
|
||||
|
||||
### Revalidation
|
||||
|
||||
Dynamic sources are cached by the loader.
|
||||
|
||||
If your source changes over time, call `invalidate()` to clear the cache, or `revalidate()` to update the cache:
|
||||
|
||||
```ts
|
||||
source.invalidate();
|
||||
|
||||
await source.revalidate();
|
||||
```
|
||||
|
||||
For [multi-source](/docs/headless/source-api/source#multiple-sources) setups, you can invalidate/revalidate a specific source:
|
||||
|
||||
```ts
|
||||
source.invalidate('docs');
|
||||
|
||||
await source.revalidate('docs');
|
||||
```
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Loader Plugins
|
||||
description: Extend Loader API
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Loader plugins can extend `loader()` to customize its output.
|
||||
|
||||
A list of built-in plugins:
|
||||
|
||||
- **Lucide Icons**: use icons from [Lucide React](https://lucide.dev) (require `lucide-react` to be installed).
|
||||
|
||||
```ts
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
|
||||
|
||||
export const source = loader({
|
||||
// ...
|
||||
plugins: [lucideIconsPlugin()],
|
||||
});
|
||||
```
|
||||
|
||||
## Creating Plugins
|
||||
|
||||
Each plugin is an object:
|
||||
|
||||
```ts tab="Strictly Typed"
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
plugins: ({ typedPlugin }) => [
|
||||
typedPlugin({
|
||||
// plugin config
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
```ts tab="Simple"
|
||||
import { loader, type LoaderPlugin } from 'fumadocs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
plugins: [myPlugin()],
|
||||
});
|
||||
|
||||
function myPlugin(): LoaderPlugin {
|
||||
return {
|
||||
// plugin config
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
<Callout title="Good to know">
|
||||
|
||||
`typedPlugin` enforces a more accurate type for plugin, including custom properties from your content source.
|
||||
|
||||
But to make the plugin reusable (across different loaders), use the `LoaderPlugin` type instead.
|
||||
|
||||
</Callout>
|
||||
|
||||
### Storage
|
||||
|
||||
During the process, your input source files will be parsed and form a virtual storage in memory.
|
||||
|
||||
To perform virtual file-system operations before processing, you can hook `transformStorage`.
|
||||
|
||||
```ts
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
plugins: ({ typedPlugin }) => [
|
||||
typedPlugin({
|
||||
transformStorage({ storage }) {
|
||||
storage.read('my/path');
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Page Tree
|
||||
|
||||
The page tree is generated from your file system, some unnecessary information (e.g. unused frontmatter properties) will be filtered.
|
||||
|
||||
You can customize it using the `transformPageTree`, such as attaching custom properties to nodes, or customising the display name of pages.
|
||||
|
||||
```tsx
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
plugins: ({ typedPlugin }) => [
|
||||
typedPlugin({
|
||||
transformPageTree: {
|
||||
file(node, file) {
|
||||
// access the original (unfiltered) file data
|
||||
if (file) console.log(this.storage.read(file));
|
||||
|
||||
// modify nodes
|
||||
node.name = <>Some JSX Nodes here</>;
|
||||
return node;
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
@@ -5,11 +5,11 @@ description: Setup sources for Loader API
|
||||
|
||||
## Overview
|
||||
|
||||
`loader()` accepts different content sources based on a `source` interface.
|
||||
Content loader accepts different content sources based on a `source` interface.
|
||||
|
||||
### Multiple Sources
|
||||
|
||||
Use `multiple` to combine multiple sources into one.
|
||||
Pass a record of source objects instead
|
||||
|
||||
```ts
|
||||
import { loader, multiple } from '@hanzo/docs-core/source';
|
||||
@@ -18,10 +18,10 @@ import { blog, docs } from 'collections/server';
|
||||
import { lucideIconsPlugin } from '@hanzo/docs-core/source/lucide-icons';
|
||||
|
||||
export const source = loader(
|
||||
multiple({
|
||||
{
|
||||
docs: docs.toFumadocsSource(),
|
||||
openapi: blog.toFumadocsSource(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
baseUrl: '/docs',
|
||||
},
|
||||
@@ -33,23 +33,23 @@ To access properties exclusive to each source:
|
||||
```ts
|
||||
const page = source.getPage(['...']);
|
||||
|
||||
if (page.data.type === 'docs') {
|
||||
if (page.type === 'docs') {
|
||||
console.log(page.data);
|
||||
} else {
|
||||
console.log(page.data);
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Source
|
||||
### Static Source
|
||||
|
||||
To plug your own content source, create a `Source` object.
|
||||
To plug your own content source, create a `StaticSource` object.
|
||||
|
||||
Since Loader API doesn't rely on file system, file paths only allow virtual paths like `file.mdx` and `content/file.mdx`, `./file.mdx` and `D://content/file.mdx` are not allowed.
|
||||
|
||||
```ts twoslash
|
||||
import { Source } from '@hanzo/docs-core/source';
|
||||
|
||||
export function createMySource(): Source<{
|
||||
export function createMySource(): StaticSource<{
|
||||
metaData: { title: string; pages: string[] }; // Your custom type
|
||||
pageData: { title: string; description?: string }; // Your custom type
|
||||
}> {
|
||||
@@ -76,3 +76,46 @@ export function createMySource(): Source<{
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic Source
|
||||
|
||||
For content sources with revalidation, return a `DynamicSource` instead:
|
||||
|
||||
```ts
|
||||
import type { DynamicSource } from 'fumadocs-core/source';
|
||||
|
||||
export function createMySource(): DynamicSource<{
|
||||
metaData: { title: string; pages: string[] };
|
||||
pageData: { title: string; description?: string };
|
||||
}> {
|
||||
return {
|
||||
async files() {
|
||||
return [
|
||||
{
|
||||
type: 'page',
|
||||
path: 'folder/index.mdx',
|
||||
data: {
|
||||
title: 'Hello World',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'meta.json',
|
||||
data: {
|
||||
title: 'Docs',
|
||||
pages: ['folder'],
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
configure(loader) {
|
||||
// you can revalidate the loader from source, e.g.
|
||||
onMyEvent(() => {
|
||||
void loader.revalidate();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Dynamic sources can only be consumed by [`dynamicLoader()`](/docs/headless/source-api#dynamic-loader).
|
||||
|
||||
@@ -7,7 +7,7 @@ description: Manage the Shiki instances.
|
||||
|
||||
Hanzo Docs use its own Shiki factory API to manage internal Shiki instances.
|
||||
|
||||
You can customise it for special purposes like using fine-grained bundles, for example:
|
||||
You can customize it for special purposes like using fine-grained bundles, for example:
|
||||
|
||||
```ts
|
||||
import { createShikiFactory } from '@hanzo/docs-core/highlight/shiki';
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Node.js
|
||||
description: Access content in Node.js runtime.
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to run the script under ESM environment.
|
||||
|
||||
```js title="scripts/example.js"
|
||||
import { register } from 'fumadocs-mdx/node';
|
||||
|
||||
// register the Node.js loader
|
||||
register();
|
||||
|
||||
// accessing content
|
||||
const { source } = await import('./lib/source');
|
||||
console.log(source.getPages());
|
||||
```
|
||||
|
||||
The `register()` function accepts options:
|
||||
|
||||
```ts
|
||||
register({
|
||||
configPath: '...',
|
||||
// ...
|
||||
});
|
||||
```
|
||||
@@ -40,7 +40,7 @@ const config = {
|
||||
|
||||
// [!code ++:4]
|
||||
const withMDX = createMDX({
|
||||
// customise the config file path
|
||||
// customize the config file path
|
||||
// configPath: "source.config.ts"
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import * as MdxConfig from './source.config';
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
// [!code ++]
|
||||
mdx(MdxConfig),
|
||||
mdx(),
|
||||
// ...
|
||||
],
|
||||
});
|
||||
@@ -49,8 +49,7 @@ export default defineConfig({
|
||||
vite: {
|
||||
plugins: [
|
||||
// [!code ++]
|
||||
mdx(MdxConfig),
|
||||
tsconfigPaths(),
|
||||
mdx(),
|
||||
],
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
|
||||
@@ -45,14 +45,16 @@ Depending on your bundler, this may reduce compilation time significantly on dev
|
||||
|
||||
## Dynamic Mode
|
||||
|
||||
Dynamic mode works by performing on-demand compilation with MDX Remote.
|
||||
Dynamic mode works by performing **on-demand compilation**, then execute the output on runtime.
|
||||
|
||||
You need to install extra dependencies:
|
||||
It is recommended to install & externalize `shiki`:
|
||||
|
||||
```npm
|
||||
npm i @hanzo/docs-mdx-remote shiki
|
||||
```
|
||||
|
||||
Enable dynamic mode in collections like:
|
||||
|
||||
```ts tab="Docs Collection"
|
||||
import { defineDocs } from '@hanzo/docs-mdx/config';
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export const blog = defineCollections({
|
||||
|
||||
### `mdxOptions`
|
||||
|
||||
Customise MDX options at the collection level.
|
||||
Customize MDX options at the collection level.
|
||||
|
||||
> This API is only available on `doc` type.
|
||||
|
||||
@@ -218,7 +218,7 @@ export const docs = defineDocs({
|
||||
|
||||
### `dir`
|
||||
|
||||
Instead of per collection, you should customise content directory from `defineDocs`:
|
||||
Instead of per collection, you should customize content directory from `defineDocs`:
|
||||
|
||||
```ts
|
||||
import { defineDocs } from '@hanzo/docs-mdx/config';
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Global Options
|
||||
description: Customize Fumadocs MDX
|
||||
---
|
||||
|
||||
## Global Options
|
||||
|
||||
Shared options of Fumadocs MDX.
|
||||
|
||||
```ts title="source.config.ts"
|
||||
import { defineConfig } from 'fumadocs-mdx/config';
|
||||
|
||||
export default defineConfig({
|
||||
// global options
|
||||
});
|
||||
```
|
||||
|
||||
<auto-type-table path="./props.ts" name="GlobalConfig" />
|
||||
|
||||
### MDX Options
|
||||
|
||||
Customize the [default MDX preset](/docs/mdx/mdx).
|
||||
|
||||
```ts title="source.config.ts"
|
||||
import { defineConfig } from 'fumadocs-mdx/config';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkMath from 'remark-math';
|
||||
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkMath],
|
||||
// When order matters
|
||||
rehypePlugins: (v) => [rehypeKatex, ...v],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Or using the minimal preset:
|
||||
|
||||
```ts title="source.config.ts"
|
||||
import { defineConfig } from 'fumadocs-mdx/config';
|
||||
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
preset: 'minimal',
|
||||
// now it accepts only MDX processor options
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: Getting Started
|
||||
description: Introducing Fumadocs MDX, the official content source of Fumadocs.
|
||||
icon: Album
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Fumadocs MDX is a tool to transform content into type-safe data, similar to Content Collections.
|
||||
|
||||
It is not a full CMS but rather a content processing layer for React frameworks, you can use it to handle blog posts and other contents.
|
||||
|
||||
### What is a Collection?
|
||||
|
||||
**Collection** refers to a collection containing a certain type of files. You can define collections in the config file (`source.config.ts`).
|
||||
|
||||
Fumadocs MDX transforms collections into type-safe data, accessible in your app. Available collections:
|
||||
|
||||
<Tabs items={["doc", "meta", 'docs']}>
|
||||
|
||||
<Tab value='doc'>
|
||||
|
||||
Compile Markdown & MDX files into a React Server Component, with useful properties like **Table of Contents**.
|
||||
|
||||
```ts title="source.config.ts"
|
||||
import { defineCollections } from 'fumadocs-mdx/config';
|
||||
|
||||
export const test = defineCollections({
|
||||
type: 'doc',
|
||||
dir: 'content/docs',
|
||||
});
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab value='meta'>
|
||||
|
||||
Transform YAML/JSON files into an array of data.
|
||||
|
||||
```ts title="source.config.ts"
|
||||
import { defineCollections } from 'fumadocs-mdx/config';
|
||||
|
||||
export const test = defineCollections({
|
||||
type: 'meta',
|
||||
dir: 'content/docs',
|
||||
});
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value='docs'>
|
||||
|
||||
Combination of `meta` and `doc` collections, which is needed for Fumadocs.
|
||||
|
||||
```ts title="source.config.ts"
|
||||
import { defineDocs } from 'fumadocs-mdx/config';
|
||||
|
||||
export const docs = defineDocs({
|
||||
dir: 'content/docs',
|
||||
docs: {
|
||||
// options for `doc` collection
|
||||
},
|
||||
meta: {
|
||||
// options for `meta` collection
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
For example, a `doc` collection will transform the `.md` and `.mdx` files:
|
||||
|
||||
<Files>
|
||||
<Folder name="folder" defaultOpen>
|
||||
<File name="ui.md" />
|
||||
</Folder>
|
||||
<File name="hello.md" />
|
||||
<File name="index.mdx" />
|
||||
<File name="meta.json" className="opacity-50 cursor-not-allowed" aria-disabled />
|
||||
</Files>
|
||||
|
||||
We will cover in [Accessing Collections](/docs/mdx/entry) later on how to access collection outputs.
|
||||
|
||||
## Installation
|
||||
|
||||
Configure Fumadocs MDX on:
|
||||
|
||||
<Cards>
|
||||
<Card title="Next.js" href="/docs/mdx/next">
|
||||
Using Fumadocs MDX with Next.js.
|
||||
</Card>
|
||||
<Card title="Vite" href="/docs/mdx/vite">
|
||||
Using Fumadocs MDX with Vite.
|
||||
</Card>
|
||||
<Card title="Runtime Loader" href="/docs/mdx/loader">
|
||||
Using Fumadocs MDX with a JavaScript runtime.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## FAQ
|
||||
|
||||
### Built-in Properties
|
||||
|
||||
These properties are exported from MDX files by default.
|
||||
|
||||
| Property | Description |
|
||||
| --------------------- | ----------------------------------------------- |
|
||||
| `frontmatter` | Frontmatter |
|
||||
| `toc` | Table of Contents |
|
||||
| `structuredData` | Structured Data, useful for implementing search |
|
||||
| `extractedReferences` | For analyzing `hrefs` references |
|
||||
|
||||
### Customize Frontmatter
|
||||
|
||||
Use the [`schema`](/docs/mdx/collections#schema-1) option to pass a validation schema to validate frontmatter and define its output properties.
|
||||
|
||||
### Customize MDX Compiler
|
||||
|
||||
Fumadocs MDX uses [MDX Compiler](https://mdxjs.com/packages/mdx) to compile MDX files into JavaScript files.
|
||||
The [default preset](/docs/mdx/mdx) includes a set of plugins and configurations out-of-the-box.
|
||||
|
||||
You can customize it on [Global Config](/docs/mdx/global#mdx-options) or [Collection Config](/docs/mdx/collections#mdxoptions).
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: MDX Presets
|
||||
description: Customise the default configurations for MDX processor.
|
||||
description: Customize the default configurations for MDX processor.
|
||||
---
|
||||
|
||||
Hanzo Docs MDX provides MDX presets to simplify configurations of features like **syntax highlighting**.
|
||||
@@ -80,9 +80,9 @@ export const blog = defineCollections({
|
||||
});
|
||||
```
|
||||
|
||||
### Customise Built-in Plugins
|
||||
### Customize Built-in Plugins
|
||||
|
||||
Customise the options of built-in plugins like:
|
||||
Customize the options of built-in plugins like:
|
||||
|
||||
```ts tab="Global Config"
|
||||
import { defineConfig } from '@hanzo/docs-mdx/config';
|
||||
|
||||
@@ -69,10 +69,10 @@ import { docs } from 'collections/server';
|
||||
import * as MyWorkspace from 'collections/my-workspace/server';
|
||||
|
||||
export const source = loader(
|
||||
multiple({
|
||||
{
|
||||
root: docs.toFumadocsSource(),
|
||||
'my-workspace': MyWorkspace.docs.toFumadocsSource(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
baseUrl: '/docs',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Banner
|
||||
description: Add a banner to your site
|
||||
preview: banner
|
||||
---
|
||||
|
||||
<Installation name="banner" />
|
||||
|
||||
## Usage
|
||||
|
||||
Put the element at the top of your root layout, you can use it for displaying announcements.
|
||||
|
||||
```tsx
|
||||
import { Banner } from 'fumadocs-ui/components/banner';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
{/* [!code ++] */}
|
||||
<Banner>Hello World</Banner>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Variant
|
||||
|
||||
Change the default variant.
|
||||
|
||||
```tsx
|
||||
import { Banner } from 'fumadocs-ui/components/banner';
|
||||
|
||||
<Banner variant="rainbow">Hello World</Banner>;
|
||||
|
||||
// customize colors
|
||||
<Banner
|
||||
variant="rainbow"
|
||||
rainbowColors={[
|
||||
'rgba(255,100,0, 0.5)',
|
||||
'rgba(255,100,0, 0.5)',
|
||||
'transparent',
|
||||
'rgba(255,100,0, 0.5)',
|
||||
'transparent',
|
||||
'rgba(255,100,0, 0.5)',
|
||||
'transparent',
|
||||
]}
|
||||
>
|
||||
Hello World
|
||||
</Banner>;
|
||||
```
|
||||
|
||||
### Change Layout
|
||||
|
||||
By default, the banner uses a `style` tag to modify Fumadocs layouts (e.g. reduce the sidebar height).
|
||||
You can disable it with:
|
||||
|
||||
```tsx
|
||||
import { Banner } from 'fumadocs-ui/components/banner';
|
||||
|
||||
<Banner changeLayout={false}>Hello World</Banner>;
|
||||
```
|
||||
|
||||
### Close
|
||||
|
||||
To allow users to close the banner, give the banner an ID.
|
||||
|
||||
```tsx
|
||||
import { Banner } from 'fumadocs-ui/components/banner';
|
||||
|
||||
<Banner id="hello-world">Hello World</Banner>;
|
||||
```
|
||||
|
||||
The state will be automatically persisted.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Graph View
|
||||
description: A graph of all pages.
|
||||
preview: graphView
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
You can install this from CLI:
|
||||
|
||||
```npm
|
||||
npx @fumadocs/cli add graph-view
|
||||
```
|
||||
|
||||
Enable `extractLinkReferences` on Fumadocs MDX.
|
||||
|
||||
```ts title="source.config.ts"
|
||||
import { defineDocs } from 'fumadocs-mdx/config';
|
||||
|
||||
export const docs = defineDocs({
|
||||
docs: {
|
||||
postprocess: {
|
||||
// [!code ++]
|
||||
extractLinkReferences: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
You can use it in MDX files or the layout components (e.g. in `page.tsx`):
|
||||
|
||||
```tsx title="page.tsx"
|
||||
import { GraphView } from '@/components/graph-view';
|
||||
import { buildGraph } from '@/lib/build-graph';
|
||||
|
||||
export function PageBody() {
|
||||
return (
|
||||
<div>
|
||||
<GraphView graph={buildGraph()} />
|
||||
{/* ... */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -7,7 +7,7 @@ The layout of documentation pages, it includes a sidebar and **mobile-only** nav
|
||||
|
||||

|
||||
|
||||
<Customisation />
|
||||
<Customization />
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -50,7 +50,7 @@ import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
|
||||
### Sidebar Items
|
||||
|
||||
Customise sidebar navigation links.
|
||||
Customize sidebar navigation links.
|
||||
|
||||
<div className='flex justify-center items-center *:max-w-[200px] bg-linear-to-br from-fd-primary/10 rounded-xl border'>
|
||||
|
||||
@@ -150,7 +150,7 @@ import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
<DocsLayout tabs={false} />;
|
||||
```
|
||||
|
||||
<Callout title="Want further customisations?">
|
||||
<Callout title="Want further customizations?">
|
||||
|
||||
You can specify a `banner` to the [Docs Layout](/docs/ui/layouts/docs) component.
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Flux Layout
|
||||
description: An aggressively minimal layout for docs
|
||||
---
|
||||
|
||||
A docs layout that impractically minimal & clean, originally made for experimental purpose.
|
||||
|
||||

|
||||
|
||||
<Customization />
|
||||
|
||||
## Usage
|
||||
|
||||
Enable the Flux layout with `fumadocs-ui/layouts/flux`.
|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/flux'; // [!code highlight]
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { source } from '@/lib/source';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout {...baseOptions()} tree={source.getPageTree()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Make sure to update your page import too:
|
||||
|
||||
```tsx title="page.tsx"
|
||||
import { ... } from 'fumadocs-ui/layouts/docs/page'; // [!code --]
|
||||
import { ... } from 'fumadocs-ui/layouts/flux/page'; // [!code ++]
|
||||
```
|
||||
|
||||
<Callout title="Important to know">
|
||||
|
||||
- It is best to be paired with [static/local search](/docs/search/orama#static), the navigation is more efficient using document search.
|
||||
- It prioritizes subjective aesthetics over user experience.
|
||||
- It is opinionated, use Fumadocs CLI for customization instead.
|
||||
|
||||
</Callout>
|
||||
|
||||
## Configurations
|
||||
|
||||
The options are inherited from [Docs Layout](/docs/ui/layouts/docs), with minor differences:
|
||||
|
||||
- Flux layout is a client component, you cannot pass unserializable props from a server component.
|
||||
- A `renderNavigationPanel` prop to customize the bottom navigation panel.
|
||||
- There is no `tocPopover` options.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Home Layout
|
||||
description: Shared layout for other pages
|
||||
---
|
||||
|
||||
A layout with only navbar.
|
||||
|
||||

|
||||
|
||||
<Customization />
|
||||
|
||||
## Usage
|
||||
|
||||
Add a navbar and search dialog across other pages.
|
||||
|
||||
```tsx title="/app/(home)/layout.tsx"
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
|
||||
}
|
||||
```
|
||||
|
||||
See detailed docs for [`links`](/docs/ui/layouts/links) and [`nav`](/docs/ui/layouts/nav) options.
|
||||
@@ -7,7 +7,7 @@ A compact version of [`<DocsLayout />`](/docs/ui/layouts/docs).
|
||||
|
||||

|
||||
|
||||
<Customisation />
|
||||
<Customization />
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -44,7 +44,7 @@ The options are inherited from [Docs Layout](/docs/ui/layouts/docs), with minor
|
||||
|
||||
### Tab Mode
|
||||
|
||||
Configure the style of [Layout Tabs](/docs/ui/layouts/docs#layout-tabs).
|
||||
Configure the style of [Layout Tabs](/docs/ui/layouts/docs#layout-tabs), see the linked docs for how to add layout tabs.
|
||||
|
||||

|
||||
|
||||
@@ -71,6 +71,8 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
|
||||
Configure the style of navbar.
|
||||
|
||||
Below is an example using [tab mode](#tab-mode) `navbar` & nav mode `top`:
|
||||
|
||||

|
||||
|
||||
```tsx title="layout.tsx"
|
||||
|
||||
@@ -6,7 +6,7 @@ description: A page in your documentation
|
||||
Page is the base element of a documentation, it includes Table of contents,
|
||||
Footer, and Breadcrumb.
|
||||
|
||||
<Customisation />
|
||||
<Customization />
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -142,7 +142,7 @@ import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
<DocsPage toc={headings}>...</DocsPage>;
|
||||
```
|
||||
|
||||
You can customise it with `tableOfContent`, or with `tableOfContentPopover` on smaller devices.
|
||||
You can customize it with `tableOfContent`, or with `tableOfContentPopover` on smaller devices.
|
||||
|
||||
```tsx
|
||||
import { DocsPage, type TOCMainProps } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
@@ -235,7 +235,7 @@ npx @hanzo/docs-cli add slots/flux/page/toc
|
||||
|
||||
Footer is a navigation element that has two buttons to jump to the next and previous pages. When not specified, it shows the neighbour pages found from page tree.
|
||||
|
||||
Customise the footer with the `footer` prop:
|
||||
Customize the footer with the `footer` prop:
|
||||
|
||||
```tsx
|
||||
import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Root Provider
|
||||
description: The context provider of Fumadocs UI.
|
||||
---
|
||||
|
||||
The context provider of all the components, including `next-themes` and [`<FrameworkProvider />`](/docs/headless#installation). It should be located at the root layout.
|
||||
|
||||
## Usage
|
||||
|
||||
Import it according to your React.js framework:
|
||||
|
||||
```jsx tab="Next.js"
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```jsx tab="React Router"
|
||||
import { RootProvider } from 'fumadocs-ui/provider/react-router';
|
||||
|
||||
export function Layout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```jsx tab="Tanstack"
|
||||
import {
|
||||
HeadContent,
|
||||
Scripts,
|
||||
} from '@tanstack/react-router';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/tanstack';
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html suppressHydrationWarning>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<RootProvider>{children}</RootProvider>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```jsx tab="Waku"
|
||||
import { RootProvider } from 'fumadocs-ui/provider/waku';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Search Dialog
|
||||
|
||||
Customize or disable the search dialog with `search` option.
|
||||
|
||||
```jsx
|
||||
<RootProvider
|
||||
search={{
|
||||
enabled: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RootProvider>
|
||||
```
|
||||
|
||||
Learn more from [Search](/docs/ui/search).
|
||||
|
||||
### Theme Provider
|
||||
|
||||
Fumadocs supports light/dark modes with [`next-themes`](https://github.com/pacocoursey/next-themes).
|
||||
Customize or disable it with `theme` option.
|
||||
|
||||
```jsx
|
||||
<RootProvider
|
||||
theme={{
|
||||
enabled: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RootProvider>
|
||||
```
|
||||
|
||||
You can also install & reference `next-themes` in your code for managing themes.
|
||||
@@ -5,7 +5,7 @@ description: The UI for document search
|
||||
|
||||
## Overview
|
||||
|
||||
You can customise Search UI from [`<RootProvider />`](/docs/ui/layouts/root-provider).
|
||||
You can customize Search UI from [`<RootProvider />`](/docs/ui/layouts/root-provider).
|
||||
|
||||
```tsx title="app/layout.tsx"
|
||||
import { RootProvider } from '@hanzo/docs-ui/provider/<framework>';
|
||||
@@ -33,7 +33,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
|
||||
### Hot Keys
|
||||
|
||||
Customise the hot keys to trigger search dialog, by default it's <kbd>⌘</kbd> <kbd>K</kbd> or <kbd>Ctrl</kbd> <kbd>K</kbd>.
|
||||
Customize the hot keys to trigger search dialog, by default it's <kbd>⌘</kbd> <kbd>K</kbd> or <kbd>Ctrl</kbd> <kbd>K</kbd>.
|
||||
|
||||
```tsx
|
||||
import { RootProvider } from '@hanzo/docs-ui/provider/<framework>';
|
||||
@@ -108,7 +108,7 @@ export default function DefaultSearchDialog(props: SharedProps) {
|
||||
|
||||
The content in search result supports Markdown, match highlights are expressed using `<mark />`.
|
||||
|
||||
You can customise the Markdown renderer.
|
||||
You can customize the Markdown renderer.
|
||||
|
||||
```tsx title="components/search.tsx"
|
||||
<SearchDialogList
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user