mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Fumadocs Module (#831)
* init changes * Core: Create search api from source * Core: Support creating metadata API from sources * UI: Refactor imports for layouts * Fix a few mistakes * Rename APIs * CLI: Support plugins * UI: Change default URL of title on i18n mode * CLI: Support auto-updating configs for i18n * CLI: Support i18n + og image plugins * UI: Bundle icons into dist * Docs: Fix build issues * CLI: Add OpenAPI Plugin * CLI: Improve instructions * CLI: Support `add` command to add components * UI: Avoid multiple referenced files * CLI: Support custom import paths & config * Docs: Update docs * Add dev deps * CLI: Add select for components
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-ui': minor
|
||||
---
|
||||
|
||||
Bundle icons into dist
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'fumadocs-core': major
|
||||
---
|
||||
|
||||
**Remove deprecated `languages` and `defaultLanguage` option from loader**
|
||||
|
||||
**migrate:** Use I18n config API
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': minor
|
||||
---
|
||||
|
||||
Create search api from source (Support i18n without modifying search route handler)
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
'fumadocs-ui': major
|
||||
---
|
||||
|
||||
**Refactor import paths for layouts**
|
||||
|
||||
**migrate:** Use
|
||||
|
||||
```ts
|
||||
import { DocsLayout } from "fumadocs-ui/layouts/docs"
|
||||
|
||||
import { HomeLayout } from "fumadocs-ui/layouts/home"
|
||||
|
||||
import { BaseLayoutProps } from "fumadocs-ui/layouts/shared"
|
||||
```
|
||||
|
||||
Instead of
|
||||
|
||||
```ts
|
||||
import { DocsLayout } from "fumadocs-ui/layout"
|
||||
|
||||
import { HomeLayout } from "fumadocs-ui/home-layout"
|
||||
|
||||
import { HomeLayoutProps } from "fumadocs-ui/home-layout"
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Change default URL of title on i18n mode
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Add center to root toggle
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': major
|
||||
---
|
||||
|
||||
Improved usage for `createI18nSearchAPI` (replaced `createI18nSearchAPIExperimental`)
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'fumadocs-ui': major
|
||||
---
|
||||
|
||||
**Remove `getImageMeta` from `fumadocs-ui/og`**
|
||||
|
||||
**migrate:** Use Metadata API from `fumadocs-core/server`
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'fumadocs-core': major
|
||||
'fumadocs-ui': major
|
||||
---
|
||||
|
||||
Replace `fumadocs-core/search/shared` with `fumadocs-core/server`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': minor
|
||||
---
|
||||
|
||||
Support creating metadata API from sources
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { HomeLayout } from 'fumadocs-ui/home-layout';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
export default function Layout({
|
||||
|
||||
@@ -14,9 +14,8 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { TypeTable } from 'fumadocs-ui/components/type-table';
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { getImageMeta } from 'fumadocs-ui/og';
|
||||
import Preview from '@/components/preview';
|
||||
import { createMetadata } from '@/utils/metadata';
|
||||
import { createMetadata, metadataImage } from '@/utils/metadata';
|
||||
import { openapi, utils } from '@/app/source';
|
||||
import { Wrapper } from '@/components/preview/wrapper';
|
||||
|
||||
@@ -91,19 +90,15 @@ export function generateMetadata({ params }: { params: Param }): Metadata {
|
||||
const description =
|
||||
page.data.description ?? 'The library for building documentation sites';
|
||||
|
||||
const image = getImageMeta('og', page.slugs);
|
||||
|
||||
return createMetadata({
|
||||
title: page.data.title,
|
||||
description,
|
||||
openGraph: {
|
||||
url: `/docs/${page.slugs.join('/')}`,
|
||||
images: image,
|
||||
},
|
||||
twitter: {
|
||||
images: image,
|
||||
},
|
||||
});
|
||||
return createMetadata(
|
||||
metadataImage.withImage(page.slugs, {
|
||||
title: page.data.title,
|
||||
description,
|
||||
openGraph: {
|
||||
url: `/docs/${page.slugs.join('/')}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams(): Param[] {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
import { docsOptions } from '@/app/layout.config';
|
||||
import 'fumadocs-ui/twoslash.css';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { type DocsLayoutProps } from 'fumadocs-ui/layout';
|
||||
import { type DocsLayoutProps } from 'fumadocs-ui/layouts/docs';
|
||||
import { BookIcon, Heart, LayoutTemplateIcon } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { RootToggle } from 'fumadocs-ui/components/layout/root-toggle';
|
||||
import type { HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
import { FumadocsIcon, NavChildren } from '@/app/layout.client';
|
||||
import Logo from '@/public/logo.png';
|
||||
import { utils } from '@/app/source';
|
||||
import { modes } from '@/utils/modes';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
githubUrl: 'https://github.com/fuma-nama/fumadocs',
|
||||
nav: {
|
||||
title: (
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { generateOGImage } from 'fumadocs-ui/og';
|
||||
import { type ImageResponse } from 'next/og';
|
||||
import { type NextRequest } from 'next/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { utils } from '@/app/source';
|
||||
import { metadataImage } from '@/utils/metadata';
|
||||
|
||||
const font = readFileSync('./app/og/[...slug]/Geist-Regular.ttf');
|
||||
const fontBold = readFileSync('./app/og/[...slug]/Geist-Bold.ttf');
|
||||
|
||||
export function GET(
|
||||
_: NextRequest,
|
||||
{
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
slug: string[];
|
||||
};
|
||||
},
|
||||
): ImageResponse {
|
||||
const page = utils.getPage(params.slug.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
|
||||
export const GET = metadataImage.createAPI((page): ImageResponse => {
|
||||
return generateOGImage({
|
||||
primaryTextColor: 'rgb(240,240,240)',
|
||||
primaryColor: 'rgba(255,255,255,0.3)',
|
||||
primaryColor: 'rgba(255,255,255,0.2)',
|
||||
title: page.data.title,
|
||||
icon: (
|
||||
<svg
|
||||
@@ -63,13 +49,10 @@ export function GET(
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export function generateStaticParams(): {
|
||||
slug: string[];
|
||||
}[] {
|
||||
return utils.generateParams().map((param) => ({
|
||||
...param,
|
||||
slug: [...param.slug, 'og.png'],
|
||||
}));
|
||||
return metadataImage.generateParams();
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export const utils = loader({
|
||||
The language switch is now added to Docs Layout.
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <DocsLayout i18n>{children}</DocsLayout>;
|
||||
|
||||
@@ -56,9 +56,9 @@ And pass it to Source API.
|
||||
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "../../examples/content-collections/app/source.ts",
|
||||
"file": "../../examples/content-collections/lib/source.ts",
|
||||
"codeblock": {
|
||||
"meta": "title=\"app/source.ts\""
|
||||
"meta": "title=\"lib/source.ts\""
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -66,7 +66,7 @@ And pass it to Source API.
|
||||
Done! You can access the pages and generated page tree from Source API.
|
||||
|
||||
```ts
|
||||
import { getPage } from '@/app/source';
|
||||
import { getPage } from '@/lib/source';
|
||||
|
||||
const page = getPage(slugs);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Define the i18n configurations in a file, we will import it with `@/i18n` in thi
|
||||
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "../../examples/i18n/i18n.ts",
|
||||
"file": "../../examples/i18n/lib/i18n.ts",
|
||||
"codeblock": {
|
||||
"lang": "ts",
|
||||
"meta": "title=\"i18n.ts\""
|
||||
@@ -35,7 +35,7 @@ export const source = loader({
|
||||
Update the usages to your source:
|
||||
|
||||
```ts
|
||||
import { source } from '@/app/source';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
// get page tree
|
||||
source.pageTree[params.lang];
|
||||
@@ -121,7 +121,7 @@ Make sure to update references to `source` and configure Static Site Generation
|
||||
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "../../examples/i18n/app/[lang]/[[...slug]]/page.tsx",
|
||||
"file": "../../examples/i18n/app/[lang]/docs/[[...slug]]/page.tsx",
|
||||
"codeblock": {
|
||||
"lang": "tsx",
|
||||
"meta": "title=\"page.tsx\""
|
||||
@@ -183,9 +183,9 @@ import { I18nProvider } from 'fumadocs-ui/i18n';
|
||||
To allow users changing their language, enable `i18n` on your layouts.
|
||||
|
||||
```tsx title="app/layout.config.tsx" twoslash
|
||||
import type { HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
i18n: true,
|
||||
};
|
||||
```
|
||||
|
||||
@@ -79,7 +79,7 @@ It's useful for implementing versioned docs, or multi-docs similar to this
|
||||
documentation.
|
||||
|
||||
```ts
|
||||
import { source } from '@/app/source';
|
||||
import { source } from '@/lib/source';
|
||||
import { createSearchAPI } from 'fumadocs-core/search/server';
|
||||
|
||||
export const { GET } = createSearchAPI('advanced', {
|
||||
|
||||
@@ -12,7 +12,7 @@ It requires a page tree and the url of page.
|
||||
|
||||
```ts
|
||||
import { findNeighbour } from 'fumadocs-core/server';
|
||||
import { pageTree } from '@/app/source';
|
||||
import { pageTree } from '@/lib/source';
|
||||
|
||||
const neighbours = findNeighbour(pageTree, '/url/to/page');
|
||||
```
|
||||
|
||||
@@ -69,7 +69,7 @@ You can add a post install script to generate types before executing CLI tools (
|
||||
|
||||
To integrate with Fumadocs, create:
|
||||
|
||||
```ts title="app/source.ts"
|
||||
```ts title="lib/source.ts"
|
||||
import { docs, meta } from '@/.source';
|
||||
import { createMDXSource } from 'fumadocs-mdx';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
|
||||
@@ -21,7 +21,7 @@ export default defineConfig({
|
||||
After doing this, a `lastModified` number will be exported for each document, you can convert it to a JavaScript Date object.
|
||||
|
||||
```js
|
||||
import { source } from '@/app/source';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
const page = source.getPage('...');
|
||||
|
||||
|
||||
@@ -12,26 +12,35 @@ Make sure to read their [Metadata section](https://nextjs.org/docs/app/building-
|
||||
|
||||
### Open Graph Image
|
||||
|
||||
For docs pages, Fumadocs UI has a built-in dynamic image generator.
|
||||
For docs pages, Fumadocs has a built-in dynamic image generator.
|
||||
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "../../examples/next-mdx/lib/metadata.ts",
|
||||
"codeblock": {
|
||||
"meta": "title=\"lib/metadata.ts\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You will need a route handler to get started.
|
||||
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "../../examples/next-mdx-og/app/og/[...slug]/route.tsx",
|
||||
"file": "../../examples/next-mdx/app/docs-og/[...slug]/route.tsx",
|
||||
"codeblock": {
|
||||
"meta": "title=\"app/og/[...slug]/route.tsx\""
|
||||
"meta": "title=\"app/docs-og/[...slug]/route.tsx\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> We need to append `og.png` to the end of slugs so that we can access it via `/og/my-page/og.png`.
|
||||
> We need to append `og.png` to the end of slugs so that we can access it via `/docs-og/my-page/og.png`.
|
||||
|
||||
In your docs page, add the image to metadata.
|
||||
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "../../examples/next-mdx-og/app/docs/[[...slug]]/page.tsx",
|
||||
"file": "../../examples/next-mdx/app/docs/[[...slug]]/page.tsx",
|
||||
"codeblock": {
|
||||
"meta": "title=\"app/docs/[[...slug]]/page.tsx\""
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ Add `APIPage` to your MDX Components, so that you can use it in MDX files.
|
||||
|
||||
```tsx title="page.tsx"
|
||||
import defaultComponents from 'fumadocs-ui/mdx';
|
||||
import { openapi } from '@/app/source';
|
||||
import { openapi } from '@/lib/source';
|
||||
|
||||
<MDX
|
||||
components={{
|
||||
|
||||
@@ -12,7 +12,7 @@ Add a [navbar](/docs/ui/blocks/navbar) and search dialog across other pages.
|
||||
It doesn't contain a sidebar, a page tree is not required.
|
||||
|
||||
```jsx
|
||||
import { HomeLayout } from 'fumadocs-ui/home-layout';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return <HomeLayout>{children}</HomeLayout>;
|
||||
|
||||
@@ -12,7 +12,7 @@ The layout of documentation pages, it includes a sidebar and [navbar](/docs/ui/b
|
||||
Pass your page tree to the component.
|
||||
|
||||
```jsx
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return <DocsLayout tree={tree}>{children}</DocsLayout>;
|
||||
@@ -24,7 +24,7 @@ export default function Layout({ children }) {
|
||||
Provide elements to navigate between pages.
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
|
||||
<DocsLayout sidebar={{ footer: <ShareButton /> }} />;
|
||||
```
|
||||
@@ -41,7 +41,7 @@ It can also hit the limits of some other hosting platforms.
|
||||
You can disable prefetching to reduce the amount of RSC requests.
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
|
||||
<DocsLayout sidebar={{ prefetch: false }} />;
|
||||
```
|
||||
|
||||
@@ -14,9 +14,9 @@ A link to navigate to a URL/href, can be external.
|
||||
|
||||
```tsx title="app/layout.config.tsx"
|
||||
import { BookIcon } from 'lucide-react';
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
links: [
|
||||
{
|
||||
icon: <BookIcon />,
|
||||
@@ -38,9 +38,9 @@ The conditions to be marked as active.
|
||||
| `none` | Never be active |
|
||||
|
||||
```tsx title="app/layout.config.tsx"
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
links: [
|
||||
{
|
||||
text: 'Blog',
|
||||
@@ -56,9 +56,9 @@ export const baseOptions: HomeLayoutProps = {
|
||||
Set the item as secondary, secondary items will be displayed differently on navbar.
|
||||
|
||||
```tsx title="app/layout.config.tsx"
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
links: [
|
||||
{
|
||||
text: 'Blog',
|
||||
@@ -74,9 +74,9 @@ export const baseOptions: HomeLayoutProps = {
|
||||
You can restrict where the item is displayed.
|
||||
|
||||
```tsx title="app/layout.config.tsx"
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
links: [
|
||||
{
|
||||
on: 'menu',
|
||||
@@ -94,9 +94,9 @@ Icon items are secondary by default.
|
||||
|
||||
```tsx title="app/layout.config.tsx"
|
||||
import { BookIcon } from 'lucide-react';
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
links: [
|
||||
{
|
||||
type: 'icon',
|
||||
@@ -114,9 +114,9 @@ export const baseOptions: HomeLayoutProps = {
|
||||
Same as link item, but is shown as a button.
|
||||
|
||||
```tsx title="app/layout.config.tsx"
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
links: [
|
||||
{
|
||||
type: 'button',
|
||||
@@ -132,9 +132,9 @@ export const baseOptions: HomeLayoutProps = {
|
||||
A dropdown menu containing multiple items.
|
||||
|
||||
```tsx title="app/layout.config.tsx"
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
links: [
|
||||
{
|
||||
type: 'menu',
|
||||
@@ -152,9 +152,9 @@ export const baseOptions: HomeLayoutProps = {
|
||||
Display a custom component.
|
||||
|
||||
```tsx title="app/layout.config.tsx"
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
links: [
|
||||
{
|
||||
type: 'custom',
|
||||
|
||||
@@ -8,9 +8,9 @@ description: Top navigation bar
|
||||
The navigation header provides common interactive elements.
|
||||
|
||||
```tsx
|
||||
import type { HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
githubUrl: 'https://github.com/fuma-nama/fumadocs',
|
||||
nav: {
|
||||
title: 'My App',
|
||||
@@ -28,9 +28,9 @@ To make the navbar background transparent, you can configure transparent mode.
|
||||
It uses the `none` mode by default.
|
||||
|
||||
```tsx
|
||||
import type { HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
nav: {
|
||||
transparentMode: 'top',
|
||||
},
|
||||
|
||||
@@ -57,7 +57,7 @@ Optional, display the other pages in its folder.
|
||||
|
||||
```tsx
|
||||
import { DocsPage, DocsCategory } from 'fumadocs-ui/page';
|
||||
import { getPages } from '@/app/source';
|
||||
import { getPages } from '@/lib/source';
|
||||
|
||||
<DocsPage>
|
||||
<DocsCategory page={page} pages={getPages()} />
|
||||
|
||||
@@ -9,7 +9,7 @@ preview: root-toggle
|
||||
Add this component to your sidebar or other places you want.
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { RootToggle } from 'fumadocs-ui/components/layout/root-toggle';
|
||||
|
||||
<DocsLayout
|
||||
|
||||
@@ -120,7 +120,7 @@ npm run dev
|
||||
|
||||
In the project, you can see:
|
||||
|
||||
- `app/source.ts`: Where you organize code for the source adapter, it is used here to read from your content source.
|
||||
- `lib/source.ts`: Where you organize code for the source adapter, it is used here to read from your content source.
|
||||
- `app/layout.config.tsx`: Shared options for layouts, this is optional but preferred to keep.
|
||||
- `app/(home)`: The route group for your landing page and other pages.
|
||||
- `app/docs`: The entire docs.
|
||||
|
||||
@@ -22,7 +22,7 @@ You can see [Organizing Pages](/docs/ui/page-conventions) to learn how to custom
|
||||
It is passed to the [Docs Layout](/docs/ui/blocks/layout) component and shared to navigation elements:
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
|
||||
<DocsLayout
|
||||
tree={
|
||||
@@ -39,9 +39,9 @@ They are displayed on all Fumadocs UI layouts (e.g. Home Layout and Docs Layout)
|
||||
Links items are passed to the layout.
|
||||
|
||||
```tsx twoslash title="app/layout.config.tsx"
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
links: [
|
||||
{
|
||||
text: 'Documentation',
|
||||
@@ -57,9 +57,9 @@ export const baseOptions: HomeLayoutProps = {
|
||||
A shortcut for adding GitHub repository link item.
|
||||
|
||||
```tsx twoslash title="app/layout.config.tsx"
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
githubUrl: 'https://github.com',
|
||||
};
|
||||
```
|
||||
@@ -71,9 +71,9 @@ A part of all Fumadocs UI layouts, it displays the link items and your logo.
|
||||
```tsx twoslash title="app/layout.config.tsx"
|
||||
import React from 'react';
|
||||
// ---cut---
|
||||
import { type HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
nav: {
|
||||
// Give it a logo
|
||||
title: <div>Logo</div>,
|
||||
@@ -95,7 +95,7 @@ You can use them to add additional components.
|
||||
For example, adding an announcement to the top of sidebar.
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
|
||||
<DocsLayout
|
||||
sidebar={{
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { File, Folder } from 'fumadocs-ui/components/files';
|
||||
import type { InlineTOC } from 'fumadocs-ui/components/inline-toc';
|
||||
import type { TypeTable } from 'fumadocs-ui/components/type-table';
|
||||
import type { Card } from 'fumadocs-ui/components/card';
|
||||
import type { DocsLayoutProps } from 'fumadocs-ui/layout';
|
||||
import type { DocsLayoutProps } from 'fumadocs-ui/layouts/docs';
|
||||
import type { AnchorHTMLAttributes, ComponentPropsWithoutRef } from 'react';
|
||||
import type { DocsPageProps } from 'fumadocs-ui/page';
|
||||
import type { AutoTypeTable } from 'fumadocs-typescript/ui';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { Metadata } from 'next/types';
|
||||
import { createMetadataImage } from 'fumadocs-core/server';
|
||||
import { utils } from '@/app/source';
|
||||
|
||||
export function createMetadata(override: Metadata): Metadata {
|
||||
return {
|
||||
@@ -26,3 +28,8 @@ export const baseUrl =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? new URL('http://localhost:3000')
|
||||
: new URL(`https://${process.env.VERCEL_URL!}`);
|
||||
|
||||
export const metadataImage = createMetadataImage({
|
||||
source: utils,
|
||||
imageRoute: 'og',
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { HomeLayout } from 'fumadocs-ui/home-layout';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { source } from '@/app/source';
|
||||
import { createSearchAPI } from 'fumadocs-core/search/server';
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
export const { GET } = createSearchAPI('advanced', {
|
||||
indexes: source.getPages().map((page) => ({
|
||||
title: page.data.title,
|
||||
structuredData: page.data.structuredData,
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
})),
|
||||
});
|
||||
export const { GET } = createFromSource(source);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { source } from '@/app/source';
|
||||
import { source } from '@/lib/source';
|
||||
import type { Metadata } from 'next';
|
||||
import {
|
||||
DocsPage,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { source } from '../source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
nav: {
|
||||
title: 'My App',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return <HomeLayout {...baseOptions}>{children}</HomeLayout>;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main
|
||||
style={{
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
textAlign: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
Hello World
|
||||
</h1>
|
||||
<p>
|
||||
You can open{' '}
|
||||
<Link
|
||||
href="/docs"
|
||||
style={{
|
||||
fontWeight: '600',
|
||||
textDecoration: 'underline',
|
||||
}}
|
||||
>
|
||||
/docs
|
||||
</Link>{' '}
|
||||
and see the documentation.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createAPI } from '@/lib/metadata';
|
||||
import { generateOGImage } from 'fumadocs-ui/src/og';
|
||||
|
||||
export const GET = createAPI((page) => {
|
||||
return generateOGImage({
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
});
|
||||
});
|
||||
+4
-4
@@ -1,5 +1,4 @@
|
||||
import { source } from '@/app/source';
|
||||
import type { Metadata } from 'next';
|
||||
import { source } from '@/lib/source';
|
||||
import {
|
||||
DocsPage,
|
||||
DocsBody,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
} from 'fumadocs-ui/page';
|
||||
import { notFound } from 'next/navigation';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import { withImage } from '@/lib/metadata';
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
@@ -42,8 +42,8 @@ export function generateMetadata({
|
||||
const page = source.getPage(params.slug, params.lang);
|
||||
if (!page) notFound();
|
||||
|
||||
return {
|
||||
return withImage(page.slugs, {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
} satisfies Metadata;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
export default function Layout({
|
||||
params,
|
||||
children,
|
||||
}: {
|
||||
params: { lang: string };
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DocsLayout {...baseOptions} tree={source.pageTree[params.lang]}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { RootProvider } from 'fumadocs-ui/provider';
|
||||
import 'fumadocs-ui/style.css';
|
||||
import { RootProvider } from 'fumadocs-ui/provider';
|
||||
import { Inter } from 'next/font/google';
|
||||
import type { ReactNode } from 'react';
|
||||
import { I18nProvider } from 'fumadocs-ui/i18n';
|
||||
import { source } from '@/app/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
@@ -46,18 +44,7 @@ export default function Layout({
|
||||
}[lang]
|
||||
}
|
||||
>
|
||||
<RootProvider>
|
||||
<DocsLayout
|
||||
tree={source.pageTree[lang]}
|
||||
nav={{
|
||||
title: lang === 'cn' ? '文檔' : 'My App',
|
||||
url: `/${lang}`,
|
||||
}}
|
||||
i18n
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
</RootProvider>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
import { source } from '@/app/source';
|
||||
import { i18n } from '@/i18n';
|
||||
import { createI18nSearchAPIExperimental } from 'fumadocs-core/search/server';
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
export const { GET } = createI18nSearchAPIExperimental('advanced', {
|
||||
i18n,
|
||||
indexes: source.getLanguages().flatMap((entry) => {
|
||||
return entry.pages.map((page) => ({
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
title: page.data.title,
|
||||
structuredData: page.data.structuredData,
|
||||
locale: entry.language,
|
||||
}));
|
||||
}),
|
||||
});
|
||||
export const { GET } = createFromSource(source);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
i18n: true,
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { createMDXSource } from 'fumadocs-mdx';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { i18n } from '@/i18n';
|
||||
import { docs, meta } from '@/.source';
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: '/',
|
||||
source: createMDXSource(docs, meta),
|
||||
i18n,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createMetadataImage } from 'fumadocs-core/server';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
export const { createAPI, withImage, generateParams } = createMetadataImage({
|
||||
source,
|
||||
});
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
import { docs, meta } from '@/.source';
|
||||
import { createMDXSource } from 'fumadocs-mdx';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { docs, meta } from '@/.source';
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: '/docs',
|
||||
source: createMDXSource(docs, meta),
|
||||
i18n,
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createI18nMiddleware } from 'fumadocs-core/i18n';
|
||||
import { i18n } from '@/i18n';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
|
||||
export default createI18nMiddleware(i18n);
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { source } from '@/app/source';
|
||||
import { createSearchAPI } from 'fumadocs-core/search/server';
|
||||
|
||||
export const { GET } = createSearchAPI('advanced', {
|
||||
indexes: source.getPages().map((page) => ({
|
||||
title: page.data.title,
|
||||
structuredData: page.data.structuredData,
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
})),
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import { source } from '@/app/source';
|
||||
import type { Metadata } from 'next';
|
||||
import {
|
||||
DocsPage,
|
||||
DocsBody,
|
||||
DocsTitle,
|
||||
DocsDescription,
|
||||
} from 'fumadocs-ui/page';
|
||||
import { notFound } from 'next/navigation';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import { getImageMeta } from 'fumadocs-ui/og';
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: { slug?: string[] };
|
||||
}) {
|
||||
const page = source.getPage(params.slug);
|
||||
if (!page) notFound();
|
||||
|
||||
const MDX = page.data.body;
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<MDX components={{ ...defaultMdxComponents }} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return source.generateParams();
|
||||
}
|
||||
|
||||
export function generateMetadata({ params }: { params: { slug?: string[] } }) {
|
||||
const page = source.getPage(params.slug);
|
||||
if (!page) notFound();
|
||||
|
||||
const image = getImageMeta('og', page.slugs);
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
openGraph: {
|
||||
images: image,
|
||||
},
|
||||
twitter: {
|
||||
images: image,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
} satisfies Metadata;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { source } from '../source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import type { ReactNode } from 'react';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout tree={source.pageTree} {...baseOptions}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
nav: {
|
||||
title: 'My App',
|
||||
},
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { RootProvider } from 'fumadocs-ui/provider';
|
||||
import 'fumadocs-ui/style.css';
|
||||
import { Inter } from 'next/font/google';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className={inter.className}>
|
||||
<body>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { generateOGImage } from 'fumadocs-ui/og';
|
||||
import { type NextRequest } from 'next/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { source } from '@/app/source';
|
||||
|
||||
export function GET(
|
||||
_: NextRequest,
|
||||
{ params }: { params: { slug: string[] } },
|
||||
) {
|
||||
const page = source.getPage(params.slug.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
|
||||
return generateOGImage({
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
site: 'My App',
|
||||
});
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.generateParams().map((params) => ({
|
||||
...params,
|
||||
slug: [...params.slug, 'og.png'],
|
||||
}));
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
title: Hello World
|
||||
description: |
|
||||
Your first `document`
|
||||
```ts
|
||||
console.log("Hello World")
|
||||
```
|
||||
---
|
||||
|
||||
Hey there! fsd asfd sdfsfda
|
||||
sfd
|
||||
|
||||
fdsa ff dsaf sdf sda fasd fsd
|
||||
|
||||
## Heading
|
||||
|
||||
Hello World dsasfdafsd
|
||||
|
||||
<Cards>
|
||||
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
|
||||
<Card title="Learn more about Fumadocs" href="https://fumadocs.vercel.app" />
|
||||
</Cards>
|
||||
|
||||
### Heading
|
||||
|
||||
#### Heading
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
title: Hello World
|
||||
description: Your first document
|
||||
---
|
||||
|
||||
Hey there!
|
||||
|
||||
## Heading
|
||||
|
||||
<Cards>
|
||||
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
|
||||
<Card title="Learn more about Fumadocs" href="https://fumadocs.vercel.app" />
|
||||
</Cards>
|
||||
|
||||
### Heading
|
||||
|
||||
```js
|
||||
console.log('Hello World');
|
||||
```
|
||||
|
||||
#### Heading
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
title: Fumadocs
|
||||
description: It's night, I love it
|
||||
---
|
||||
|
||||
Hey there! It's 12:13 AM now.
|
||||
@@ -1,10 +0,0 @@
|
||||
import { createMDX } from 'fumadocs-mdx/next';
|
||||
|
||||
const withMDX = createMDX();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default withMDX(config);
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "example-next-mdx-og",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev --turbo",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"fumadocs-core": "workspace:*",
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"next": "^14.2.8",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { defineDocs, defineConfig } from 'fumadocs-mdx/config';
|
||||
|
||||
export const { docs, meta } = defineDocs();
|
||||
|
||||
export default defineConfig();
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { HomeLayout } from 'fumadocs-ui/home-layout';
|
||||
import { baseOptions } from '../layout.config';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import { source } from '@/app/source';
|
||||
import { createSearchAPI } from 'fumadocs-core/search/server';
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
export const { GET } = createSearchAPI('advanced', {
|
||||
indexes: source.getPages().map((page) => ({
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
structuredData: page.data.structuredData,
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
})),
|
||||
});
|
||||
export const { GET } = createFromSource(source);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { generateOGImage } from 'fumadocs-ui/og';
|
||||
import { metadataImage } from '@/lib/metadata';
|
||||
|
||||
export const GET = metadataImage.createAPI((page) => {
|
||||
return generateOGImage({
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
site: 'My App',
|
||||
});
|
||||
});
|
||||
|
||||
export function generateStaticParams() {
|
||||
return metadataImage.generateParams();
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { source } from '@/app/source';
|
||||
import type { Metadata } from 'next';
|
||||
import { source } from '@/lib/source';
|
||||
import {
|
||||
DocsPage,
|
||||
DocsBody,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
} from 'fumadocs-ui/page';
|
||||
import { notFound } from 'next/navigation';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import { metadataImage } from '@/lib/metadata';
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
@@ -36,10 +36,10 @@ export async function generateStaticParams() {
|
||||
|
||||
export function generateMetadata({ params }: { params: { slug?: string[] } }) {
|
||||
const page = source.getPage(params.slug);
|
||||
|
||||
if (!page) notFound();
|
||||
|
||||
return {
|
||||
return metadataImage.withImage(page.slugs, {
|
||||
title: page.data.title,
|
||||
} satisfies Metadata;
|
||||
description: page.data.description,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { source } from '../source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
nav: {
|
||||
title: 'My App',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createMetadataImage } from 'fumadocs-core/server';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
export const metadataImage = createMetadataImage({
|
||||
source,
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { HomeLayout } from 'fumadocs-ui/src/home-layout';
|
||||
import { HomeLayout } from 'fumadocs-ui/home-layout';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { source } from '@/app/source';
|
||||
import { createSearchAPI } from 'fumadocs-core/search/server';
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
export const { GET } = createSearchAPI('advanced', {
|
||||
indexes: source.getPages().map((page) => ({
|
||||
title: page.data.title,
|
||||
structuredData: page.data.structuredData,
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
})),
|
||||
});
|
||||
export const { GET } = createFromSource(source);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { metadataImage } from '@/lib/metadata';
|
||||
import { generateOGImage } from 'fumadocs-ui/og';
|
||||
|
||||
export const GET = metadataImage.createAPI((page) => {
|
||||
return generateOGImage({
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
});
|
||||
});
|
||||
|
||||
export function generateStaticParams() {
|
||||
return metadataImage.generateParams();
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { source, openapi } from '@/app/source';
|
||||
import type { Metadata } from 'next';
|
||||
import { source, openapi } from '@/lib/source';
|
||||
import {
|
||||
DocsPage,
|
||||
DocsBody,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
} from 'fumadocs-ui/page';
|
||||
import { notFound } from 'next/navigation';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import { metadataImage } from '@/lib/metadata';
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
@@ -47,8 +47,8 @@ export function generateMetadata({ params }: { params: { slug?: string[] } }) {
|
||||
|
||||
if (!page) notFound();
|
||||
|
||||
return {
|
||||
return metadataImage.withImage(page.slugs, {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
} satisfies Metadata;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { source } from '../source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { source } from '../../lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HomeLayoutProps } from 'fumadocs-ui/home-layout';
|
||||
import { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
nav: {
|
||||
title: <span className="text-lg font-bold py-2">Unkey Docs</span>,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createMetadataImage } from 'fumadocs-core/server';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
export const metadataImage = createMetadataImage({
|
||||
source,
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createMDXSource } from 'fumadocs-mdx';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { attachFile, createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { loader } from 'fumadocs-core/dist/source';
|
||||
import { attachFile, createOpenAPI } from 'fumadocs-openapi/dist/server';
|
||||
import { docs, meta } from '@/.source';
|
||||
|
||||
export const source = loader({
|
||||
@@ -1,15 +1,18 @@
|
||||
import * as OpenAPI from 'fumadocs-openapi';
|
||||
import { rimrafSync } from 'rimraf';
|
||||
|
||||
rimrafSync('./content/docs/(api)', {
|
||||
const out = './content/docs/(api)';
|
||||
|
||||
// clean generated files
|
||||
rimrafSync(out, {
|
||||
filter(v) {
|
||||
return !v.endsWith('index.mdx') && !v.endsWith('meta.json');
|
||||
},
|
||||
});
|
||||
|
||||
void OpenAPI.generateFiles({
|
||||
input: ['./unkey.json'],
|
||||
output: './content/docs/(api)',
|
||||
per: 'operation',
|
||||
// input files
|
||||
input: ['./openapi.json'],
|
||||
output: out,
|
||||
groupBy: 'tag',
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getDocs } from '../source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layout';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
import { initHotReload } from '@fumadocs/mdx-remote/github/next';
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ['custom/library'],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
src/generated.js
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "fumadocs-cli",
|
||||
"version": "0.0.0",
|
||||
"description": "The CLI tool for Fumadocs",
|
||||
"keywords": [
|
||||
"NextJs",
|
||||
"Docs",
|
||||
"Fumadocs"
|
||||
],
|
||||
"homepage": "https://fumadocs.vercel.app",
|
||||
"repository": "github:fuma-nama/fumadocs",
|
||||
"license": "MIT",
|
||||
"author": "Fuma Nama",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"fumadocs": "./dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rimraf dist",
|
||||
"dev": "tsup --watch",
|
||||
"lint": "eslint .",
|
||||
"sync": "tsx ./scripts/sync.ts",
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"commander": "^12.1.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"package-manager-detector": "^0.2.0",
|
||||
"picocolors": "^1.1.0",
|
||||
"ts-morph": "^23.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/node": "22.5.4",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"fast-glob": "^3.3.1",
|
||||
"tsconfig": "workspace:*",
|
||||
"tsx": "^4.19.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import fg from 'fast-glob';
|
||||
|
||||
export const templates = {
|
||||
'lib/metadata': './examples/next-mdx/lib/metadata.ts',
|
||||
'app/docs-og/[...slug]/route':
|
||||
'./examples/next-mdx/app/docs-og/[...slug]/route.tsx',
|
||||
|
||||
'lib/i18n': './examples/i18n/lib/i18n.ts',
|
||||
middleware: './examples/i18n/middleware.ts',
|
||||
|
||||
'scripts/generate-docs': './examples/openapi/scripts/generate-docs.mjs',
|
||||
};
|
||||
|
||||
export async function sync(): Promise<void> {
|
||||
const generated: Record<string, string> = {};
|
||||
|
||||
const resolve = Object.entries(templates).map(async ([name, file]) => {
|
||||
generated[name] = await fs
|
||||
.readFile(
|
||||
// from root dir
|
||||
path.resolve('../../', file),
|
||||
)
|
||||
.then((v) => v.toString());
|
||||
});
|
||||
|
||||
await Promise.all(resolve);
|
||||
|
||||
const files = await fg(['*.tsx', '!api.tsx', '!*.client.tsx'], {
|
||||
cwd: path.resolve('../../packages/ui/src/components'),
|
||||
});
|
||||
const components = files.map((file) =>
|
||||
path.basename(file, path.extname(file)),
|
||||
);
|
||||
|
||||
const out = `
|
||||
export const generated = ${JSON.stringify(generated)}
|
||||
export const components = ${JSON.stringify(components)}
|
||||
`;
|
||||
|
||||
await fs.writeFile('./src/generated.js', out);
|
||||
}
|
||||
|
||||
void sync();
|
||||
@@ -0,0 +1,178 @@
|
||||
import path from 'node:path';
|
||||
import { sync as spawnSync } from 'cross-spawn';
|
||||
import { log, confirm, isCancel, outro, spinner, intro } from '@clack/prompts';
|
||||
import { type Project } from 'ts-morph';
|
||||
import picocolors from 'picocolors';
|
||||
import { createEmptyProject } from '@/utils/typescript';
|
||||
import { getPackageManager } from '@/utils/get-package-manager';
|
||||
import { exists } from '@/utils/fs';
|
||||
import { type Config } from '@/config';
|
||||
import {
|
||||
getOutputPath,
|
||||
toReferencePath,
|
||||
transformReferences,
|
||||
} from '@/utils/transform-references';
|
||||
import { isSrc } from '@/utils/is-src';
|
||||
|
||||
interface Context {
|
||||
config: Config;
|
||||
project: Project;
|
||||
branch: string;
|
||||
src: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of downloaded paths (without ext)
|
||||
*/
|
||||
const downloadedFiles = new Map<string, FindResult>();
|
||||
const excludedDeps = ['react', 'next', 'fumadocs-ui', 'fumadocs-core'];
|
||||
|
||||
export async function add(
|
||||
name: string,
|
||||
branch = 'main',
|
||||
config: Config = {},
|
||||
): Promise<void> {
|
||||
intro(picocolors.bold(picocolors.bgCyan(picocolors.black('Add Component'))));
|
||||
const project = createEmptyProject();
|
||||
|
||||
const result = await downloadFile(
|
||||
`src/components/${name}`,
|
||||
getOutputPath(`components/${name}`, config),
|
||||
{
|
||||
project,
|
||||
branch,
|
||||
config,
|
||||
src: await isSrc(),
|
||||
},
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
log.error(`Component: ${name} not found`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const deps = result.deps.filter((v) => !excludedDeps.includes(v));
|
||||
const value = await confirm({
|
||||
message: `This component requires extra dependencies (${deps.join(' ')}), do you want to install?`,
|
||||
});
|
||||
|
||||
if (isCancel(value)) {
|
||||
outro(picocolors.bold(picocolors.greenBright('Component downloaded')));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
const spin = spinner();
|
||||
spin.start('Installing dependencies...');
|
||||
spawnSync(`${await getPackageManager()} install ${deps.join(' ')}`, {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
spin.stop('Dependencies installed.');
|
||||
}
|
||||
|
||||
outro(picocolors.bold(picocolors.greenBright('Component installed')));
|
||||
}
|
||||
|
||||
interface DownloadFileResult {
|
||||
deps: string[];
|
||||
found: FindResult;
|
||||
}
|
||||
|
||||
async function downloadFile(
|
||||
pathWithoutExt: string,
|
||||
outPathWithoutExt: string,
|
||||
ctx: Context,
|
||||
): Promise<DownloadFileResult | undefined> {
|
||||
const deps: string[] = [];
|
||||
const rootUrl = `https://raw.githubusercontent.com/fuma-nama/fumadocs/${ctx.branch}/packages/ui`;
|
||||
|
||||
const found = await find(rootUrl, pathWithoutExt);
|
||||
if (!found) return;
|
||||
|
||||
const outPath = `${outPathWithoutExt}.${found.ext}`;
|
||||
const file = ctx.project.createSourceFile(outPath, found.code, {
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
await transformReferences(
|
||||
file,
|
||||
{
|
||||
alias: 'src',
|
||||
relativeTo: path.dirname(pathWithoutExt),
|
||||
},
|
||||
async (resolved) => {
|
||||
if (resolved.type === 'dep') {
|
||||
deps.push(resolved.name);
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadPath = getOutputPath(
|
||||
path.relative('src', resolved.path),
|
||||
ctx.config,
|
||||
);
|
||||
|
||||
const downloaded = await downloadFile(resolved.path, downloadPath, ctx);
|
||||
if (!downloaded)
|
||||
throw new Error(`Cannot download file: ${resolved.path}`);
|
||||
|
||||
deps.push(...downloaded.deps);
|
||||
return toReferencePath(outPath, downloadPath);
|
||||
},
|
||||
);
|
||||
|
||||
const isOverride = await exists(outPath);
|
||||
|
||||
if (isOverride) {
|
||||
const value = await confirm({
|
||||
message: `Do you want to override ${outPath}?`,
|
||||
});
|
||||
|
||||
if (isCancel(value)) {
|
||||
outro('Ended');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
await file.save();
|
||||
}
|
||||
} else {
|
||||
await file.save();
|
||||
}
|
||||
|
||||
return {
|
||||
found,
|
||||
deps,
|
||||
};
|
||||
}
|
||||
|
||||
interface FindResult {
|
||||
ext: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
async function find(
|
||||
rootUrl: string,
|
||||
pathWithoutExt: string,
|
||||
): Promise<FindResult | undefined> {
|
||||
const cached = downloadedFiles.get(pathWithoutExt);
|
||||
if (cached) return cached;
|
||||
|
||||
for (const ext of resolveExtensions) {
|
||||
const res = await fetch(`${rootUrl}/${pathWithoutExt}.${ext}`);
|
||||
if (!res.ok) continue;
|
||||
log.step(`downloaded ${pathWithoutExt}.${ext}`);
|
||||
|
||||
const result: FindResult = {
|
||||
ext,
|
||||
code: await res.text(),
|
||||
};
|
||||
|
||||
downloadedFiles.set(pathWithoutExt, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only look for TypeScript extensions
|
||||
*/
|
||||
const resolveExtensions = ['tsx', 'ts'];
|
||||
@@ -0,0 +1,165 @@
|
||||
import * as process from 'node:process';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
intro,
|
||||
confirm,
|
||||
isCancel,
|
||||
cancel,
|
||||
spinner,
|
||||
log,
|
||||
note,
|
||||
} from '@clack/prompts';
|
||||
import picocolors from 'picocolors';
|
||||
import { sync } from 'cross-spawn';
|
||||
import { getPackageManager } from '@/utils/get-package-manager';
|
||||
import { exists } from '@/utils/fs';
|
||||
import { isSrc } from '@/utils/is-src';
|
||||
import { type Config } from '@/config';
|
||||
import {
|
||||
getOutputPath,
|
||||
toReferencePath,
|
||||
transformReferences,
|
||||
} from '@/utils/transform-references';
|
||||
import { createEmptyProject } from '@/utils/typescript';
|
||||
|
||||
export type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
export interface PluginContext extends Config {
|
||||
src: boolean;
|
||||
|
||||
/**
|
||||
* Original path in `files` - Transformed output path
|
||||
*/
|
||||
outFileMap: Map<string, string>;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
dependencies: string[];
|
||||
files: (ctx: PluginContext) => Awaitable<Record<string, string>>;
|
||||
instructions: (ctx: PluginContext) => Awaitable<
|
||||
(
|
||||
| {
|
||||
type: 'code';
|
||||
title?: string;
|
||||
code: string;
|
||||
}
|
||||
| {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
)[]
|
||||
>;
|
||||
|
||||
transform?: (ctx: PluginContext) => Awaitable<void>;
|
||||
}
|
||||
|
||||
export async function init(plugin: Plugin, config: Config = {}): Promise<void> {
|
||||
intro(
|
||||
picocolors.bgCyan(picocolors.black(picocolors.bold('Installing Plugins'))),
|
||||
);
|
||||
const ctx: PluginContext = {
|
||||
src: await isSrc(),
|
||||
outFileMap: new Map(),
|
||||
...config,
|
||||
};
|
||||
|
||||
const files = await plugin.files(ctx);
|
||||
const project = createEmptyProject();
|
||||
|
||||
for (const [name, content] of Object.entries(files)) {
|
||||
const file = getOutputPath(name, ctx);
|
||||
ctx.outFileMap.set(name, file);
|
||||
|
||||
log.step(picocolors.green(`Writing ${file} ★`));
|
||||
|
||||
if (await exists(file)) {
|
||||
const value = await confirm({
|
||||
message: `${file} already exists`,
|
||||
active: 'Override',
|
||||
inactive: 'Skip',
|
||||
});
|
||||
|
||||
if (isCancel(value)) {
|
||||
cancel('Operation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!value) continue;
|
||||
}
|
||||
|
||||
const sourceFile = project.createSourceFile(file, content, {
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
await transformReferences(
|
||||
sourceFile,
|
||||
{
|
||||
alias: ctx.src ? 'src' : 'root',
|
||||
relativeTo: path.dirname(file),
|
||||
},
|
||||
(resolved) => {
|
||||
if (resolved.type !== 'file') return;
|
||||
|
||||
return toReferencePath(file, getOutputPath(resolved.path, ctx));
|
||||
},
|
||||
);
|
||||
|
||||
await sourceFile.save();
|
||||
}
|
||||
|
||||
if (plugin.dependencies.length > 0) {
|
||||
const manager = await getPackageManager();
|
||||
|
||||
const value = await confirm({
|
||||
message: `This plugin contains additional dependencies, do you want to install them? (detected: ${manager})`,
|
||||
});
|
||||
|
||||
if (isCancel(value)) {
|
||||
cancel('Operation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
const spin = spinner();
|
||||
spin.start('Installing dependencies');
|
||||
sync(`${manager} install ${plugin.dependencies.join(' ')}`, {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
spin.stop('Successfully installed.');
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.transform) {
|
||||
const value = await confirm({
|
||||
message:
|
||||
'This plugin contains changes to your files, do you want to apply them?',
|
||||
});
|
||||
|
||||
if (isCancel(value)) {
|
||||
cancel('Operation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
await plugin.transform(ctx);
|
||||
note(
|
||||
`You can format the output with Prettier or other code formating tools
|
||||
prettier . --write`,
|
||||
picocolors.bold(picocolors.green('Changes Applied')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const instructions = await plugin.instructions(ctx);
|
||||
for (const text of instructions) {
|
||||
if (text.type === 'text') {
|
||||
log.message(text.text, {
|
||||
symbol: '○',
|
||||
});
|
||||
}
|
||||
|
||||
if (text.type === 'code') {
|
||||
note(text.code, text.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import { isSrc } from '@/utils/is-src';
|
||||
|
||||
export interface Config {
|
||||
aliases?: {
|
||||
/**
|
||||
* Path for importing `cn` utility.
|
||||
*
|
||||
* Can be from Shadcn UI or your own `cn` function (Tailwind CSS supported)
|
||||
*
|
||||
* Be careful to **not** include file extensions.
|
||||
*/
|
||||
cn?: string;
|
||||
uiDir?: string;
|
||||
componentsDir?: string;
|
||||
libDir?: string;
|
||||
};
|
||||
|
||||
commands?: {
|
||||
/**
|
||||
* command to format output code automatically
|
||||
*/
|
||||
format?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const src = await isSrc();
|
||||
|
||||
export const defaultConfig = {
|
||||
aliases: {
|
||||
cn: src ? './src/utils/cn' : './utils/cn',
|
||||
componentsDir: src ? './src/components' : './components',
|
||||
uiDir: src ? './src/components/ui' : './components/ui',
|
||||
libDir: src ? './src/lib' : './lib',
|
||||
},
|
||||
} satisfies Config;
|
||||
|
||||
export async function loadConfig(file = './cli.json'): Promise<Config> {
|
||||
try {
|
||||
const content = await fs.readFile(file);
|
||||
|
||||
return JSON.parse(content.toString()) as Config;
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function initConfig(file = './cli.json'): Promise<void> {
|
||||
await fs.writeFile(file, JSON.stringify(defaultConfig, null, 2));
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { type templates } from '../scripts/sync';
|
||||
|
||||
export const generated: typeof templates;
|
||||
|
||||
export const components: string[];
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Command } from 'commander';
|
||||
import picocolors from 'picocolors';
|
||||
import { isCancel, multiselect, outro } from '@clack/prompts';
|
||||
import { init } from '@/commands/init';
|
||||
import { add } from '@/commands/add';
|
||||
import { initConfig, loadConfig } from '@/config';
|
||||
import { plugins } from '@/plugins';
|
||||
import { components } from '@/generated';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('fumadocs')
|
||||
.description('CLI to setup Fumadocs')
|
||||
.version(packageJson.version);
|
||||
|
||||
program
|
||||
.command('i')
|
||||
.description('init a config for Fumadocs CLI')
|
||||
.action(async () => {
|
||||
await initConfig();
|
||||
console.log(picocolors.green('Successful: ./cli.json'));
|
||||
});
|
||||
|
||||
program
|
||||
.command('init')
|
||||
.description('init a new plugin to your docs')
|
||||
.argument('<string>', 'plugin name')
|
||||
.option('--config <string>')
|
||||
.action(async (str: string, { config }) => {
|
||||
const loadedConfig = await loadConfig(config as string | undefined);
|
||||
const plugin = str in plugins ? plugins[str] : undefined;
|
||||
|
||||
if (!plugin) throw new Error(`Plugin not found: ${str}`);
|
||||
|
||||
await init(plugin, loadedConfig);
|
||||
});
|
||||
|
||||
program
|
||||
.command('add')
|
||||
.description('add a new component to your docs')
|
||||
.argument('[components...]', 'components to download')
|
||||
.option(
|
||||
'--branch <string>',
|
||||
'the Git branch name of Fumadocs repo to download from (e.g. main)',
|
||||
)
|
||||
.option('--config <string>')
|
||||
.action(
|
||||
async (
|
||||
str: string[],
|
||||
{ config, branch }: { config?: string; branch?: string },
|
||||
) => {
|
||||
let target = str;
|
||||
|
||||
if (str.length === 0) {
|
||||
const value = await multiselect({
|
||||
message: 'Select components to install',
|
||||
options: components.map((c) => ({
|
||||
label: c,
|
||||
value: c,
|
||||
})),
|
||||
});
|
||||
|
||||
if (isCancel(value)) {
|
||||
outro('Ended');
|
||||
return;
|
||||
}
|
||||
|
||||
target = value as string[];
|
||||
}
|
||||
|
||||
const loadedConfig = await loadConfig(config);
|
||||
for (const name of target) {
|
||||
await add(name, branch ?? 'main', loadedConfig);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
program.parse();
|
||||
@@ -0,0 +1,83 @@
|
||||
import path from 'node:path';
|
||||
import picocolors from 'picocolors';
|
||||
import { type Plugin } from '@/commands/init';
|
||||
import { generated } from '@/generated';
|
||||
import { transformLayoutConfig } from '@/utils/transform-layout-config';
|
||||
import { resolveAppPath } from '@/utils/is-src';
|
||||
import { moveFiles } from '@/utils/move-files';
|
||||
import { isRelative } from '@/utils/fs';
|
||||
import { transformSourceI18n } from '@/utils/transform-source-i18n';
|
||||
import { defaultConfig } from '@/config';
|
||||
import { createEmptyProject } from '@/utils/typescript';
|
||||
|
||||
export const i18nPlugin: Plugin = {
|
||||
files: () => ({
|
||||
'lib/i18n.ts': generated['lib/i18n'],
|
||||
'middleware.ts': generated.middleware,
|
||||
}),
|
||||
dependencies: [],
|
||||
instructions: () => [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Moved the ./app files to a [lang] route group.
|
||||
Make sure to update the params of page.tsx and route.ts (if necessary):`,
|
||||
},
|
||||
{
|
||||
type: 'code',
|
||||
title: 'page.tsx',
|
||||
code: `
|
||||
export default function Page({
|
||||
params,
|
||||
}: {
|
||||
${picocolors.underline(picocolors.bold('params: { lang: string; slug?: string[] };'))}
|
||||
})
|
||||
`.trim(),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Update the usages to `source` with:',
|
||||
},
|
||||
{
|
||||
type: 'code',
|
||||
title: 'page.tsx',
|
||||
code: `const page = source.getPage(params.slug, params.lang);
|
||||
const pages = source.getPage(params.lang);`,
|
||||
},
|
||||
{
|
||||
type: 'code',
|
||||
title: 'layout.tsx',
|
||||
code: `const tree = source.pageTree[params.lang];`,
|
||||
},
|
||||
],
|
||||
async transform(ctx) {
|
||||
const project = createEmptyProject();
|
||||
|
||||
await Promise.all([
|
||||
transformLayoutConfig(
|
||||
project,
|
||||
resolveAppPath('./app/layout.config.tsx', ctx.src),
|
||||
),
|
||||
transformSourceI18n(
|
||||
project,
|
||||
path.join(
|
||||
ctx.aliases?.libDir ?? defaultConfig.aliases.libDir,
|
||||
'source.ts',
|
||||
),
|
||||
ctx,
|
||||
),
|
||||
]);
|
||||
|
||||
await moveFiles(
|
||||
resolveAppPath('./app', ctx.src),
|
||||
resolveAppPath('./app/[lang]', ctx.src),
|
||||
(v) => {
|
||||
return (
|
||||
path.basename(v, path.extname(v)) !== 'layout.config' &&
|
||||
!isRelative('./app/api', v)
|
||||
);
|
||||
},
|
||||
project,
|
||||
ctx.src,
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ogImagePlugin } from '@/plugins/og-image';
|
||||
import { i18nPlugin } from '@/plugins/i18n';
|
||||
import { openapiPlugin } from '@/plugins/openapi';
|
||||
import { type Plugin } from '@/commands/init';
|
||||
|
||||
export const plugins: Record<string, Plugin> = {
|
||||
'og-image': ogImagePlugin,
|
||||
i18n: i18nPlugin,
|
||||
openapi: openapiPlugin,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import picocolors from 'picocolors';
|
||||
import { generated } from '@/generated';
|
||||
import { type Plugin, type PluginContext } from '@/commands/init';
|
||||
import { exists } from '@/utils/fs';
|
||||
import { resolveAppPath } from '@/utils/is-src';
|
||||
import { getOutputPath } from '@/utils/transform-references';
|
||||
|
||||
function isI18nEnabled(ctx: PluginContext): Promise<boolean> {
|
||||
return exists(getOutputPath('lib/i18n.ts', ctx));
|
||||
}
|
||||
|
||||
export const ogImagePlugin: Plugin = {
|
||||
files: async (ctx) => {
|
||||
const route = (await isI18nEnabled(ctx))
|
||||
? 'app/[lang]/docs-og/[...slug]/route.tsx'
|
||||
: 'app/docs-og/[...slug]/route.tsx';
|
||||
|
||||
return {
|
||||
'lib/metadata.ts': generated['lib/metadata'],
|
||||
[route]: generated['app/docs-og/[...slug]/route'],
|
||||
};
|
||||
},
|
||||
dependencies: [],
|
||||
instructions: (ctx) => [
|
||||
{
|
||||
type: 'text',
|
||||
text: picocolors.cyanBright(picocolors.bold('Import the utils like:')),
|
||||
},
|
||||
{
|
||||
type: 'code',
|
||||
title: 'ts',
|
||||
code: 'import { metadataImage } from "@/lib/metadata";',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: picocolors.cyanBright(
|
||||
picocolors.bold('Add the images to your metadata:'),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'code',
|
||||
title: resolveAppPath('app/docs/[[...slug]]/page.tsx', ctx.src),
|
||||
code: `
|
||||
export function generateMetadata({ params }: { params: { slug?: string[] } }) {
|
||||
const page = source.getPage(params.slug);
|
||||
|
||||
if (!page) notFound();
|
||||
|
||||
${picocolors.bold(picocolors.underline('return metadataImage.withImage(page.slugs, {'))}
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
});
|
||||
}
|
||||
`.trim(),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { StructureKind } from 'ts-morph';
|
||||
import { type Plugin } from '@/commands/init';
|
||||
import { createEmptyProject } from '@/utils/typescript';
|
||||
import { generated } from '@/generated';
|
||||
import { getPackageManager } from '@/utils/get-package-manager';
|
||||
import { transformTailwind } from '@/utils/transform-tailwind';
|
||||
import { type Config, defaultConfig } from '@/config';
|
||||
|
||||
export const openapiPlugin: Plugin = {
|
||||
files: () => ({
|
||||
'scripts/generate-docs.mjs': generated['scripts/generate-docs'],
|
||||
}),
|
||||
dependencies: ['fumadocs-openapi', 'rimraf'],
|
||||
async transform(ctx) {
|
||||
await Promise.all([
|
||||
transformSource(ctx),
|
||||
transformTailwind({
|
||||
addContents: [`./node_modules/fumadocs-openapi/dist/**/*.js`],
|
||||
}),
|
||||
addScript(),
|
||||
]);
|
||||
},
|
||||
instructions: async () => [
|
||||
{
|
||||
type: 'text',
|
||||
text: `I've made some changes to your Tailwind CSS config.
|
||||
You can add the APIPage component to your page.tsx:`,
|
||||
},
|
||||
{
|
||||
type: 'code',
|
||||
title: 'page.tsx',
|
||||
code: `import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import { openapi } from '@/lib/source';
|
||||
|
||||
<MDX
|
||||
components={{
|
||||
...defaultMdxComponents,
|
||||
APIPage: openapi.APIPage,
|
||||
}}
|
||||
/>;`,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: `Paste your OpenAPI schema to ./openapi.json, and use this script to generate docs:`,
|
||||
},
|
||||
{
|
||||
type: 'code',
|
||||
title: 'Terminal',
|
||||
code: `${await getPackageManager()} run build:docs`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async function addScript(): Promise<void> {
|
||||
const content = await fs.readFile('package.json');
|
||||
const parsed = JSON.parse(content.toString()) as Record<string, unknown>;
|
||||
|
||||
if (typeof parsed.scripts !== 'object') return;
|
||||
|
||||
parsed.scripts ??= {};
|
||||
Object.assign(parsed.scripts ?? {}, {
|
||||
'build:docs': 'node ./scripts/generate-docs.mjs',
|
||||
});
|
||||
|
||||
await fs.writeFile('package.json', JSON.stringify(parsed, null, 2));
|
||||
}
|
||||
|
||||
async function transformSource(config: Config): Promise<void> {
|
||||
const source = path.join(
|
||||
config.aliases?.libDir ?? defaultConfig.aliases.libDir,
|
||||
'source.ts',
|
||||
);
|
||||
const project = createEmptyProject();
|
||||
|
||||
const file = project.createSourceFile(source, undefined, {
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
file.addImportDeclaration({
|
||||
kind: StructureKind.ImportDeclaration,
|
||||
namedImports: ['createOpenAPI'],
|
||||
moduleSpecifier: 'fumadocs-openapi/server',
|
||||
});
|
||||
|
||||
file.addStatements(`export const openapi = createOpenAPI();`);
|
||||
await file.save();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export async function exists(pathLike: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(pathLike);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isRelative(from: string, to: string): boolean {
|
||||
return !path.relative(from, to).startsWith(`..${path.sep}`);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { detect } from 'package-manager-detector';
|
||||
|
||||
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
||||
|
||||
export async function getPackageManager(): Promise<PackageManager> {
|
||||
const result = await detect();
|
||||
|
||||
return result?.name ?? 'npm';
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user