mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 10:55:57 +00:00
Docs & CLI: configure llms-full.txt and og image generation by default
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'create-fumadocs-app': patch
|
||||
---
|
||||
|
||||
[Next.js] configure `llms-full.txt` and og image generation by default
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
ImageResponse,
|
||||
type ImageResponseOptions,
|
||||
} from '@takumi-rs/image-response';
|
||||
import { type ImageResponseOptions } from '@takumi-rs/image-response';
|
||||
import type { ReactNode } from 'react';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
@@ -10,41 +7,30 @@ export interface GenerateProps {
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
const font = await fs.readFile('./lib/og/JetBrainsMono-Regular.ttf');
|
||||
const fontBold = await fs.readFile('./lib/og/JetBrainsMono-Bold.ttf');
|
||||
const font = fs.readFile('./lib/og/JetBrainsMono-Regular.ttf');
|
||||
const fontBold = fs.readFile('./lib/og/JetBrainsMono-Bold.ttf');
|
||||
|
||||
export async function generateOGImage(
|
||||
options: GenerateProps & Partial<ImageResponseOptions>,
|
||||
): Promise<ImageResponse> {
|
||||
const { title, description, ...rest } = options;
|
||||
|
||||
return new ImageResponse(
|
||||
generate({
|
||||
title,
|
||||
description,
|
||||
}),
|
||||
{
|
||||
format: 'webp',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Mono',
|
||||
data: font,
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: 'Mono',
|
||||
data: fontBold,
|
||||
weight: 600,
|
||||
},
|
||||
],
|
||||
...rest,
|
||||
},
|
||||
);
|
||||
export async function getImageResponseOptions(): Promise<ImageResponseOptions> {
|
||||
return {
|
||||
format: 'webp',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Mono',
|
||||
data: await font,
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: 'Mono',
|
||||
data: await fontBold,
|
||||
weight: 600,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function generate({ title, description }: GenerateProps) {
|
||||
export function generate({ title, description }: GenerateProps) {
|
||||
const siteName = 'Fumadocs';
|
||||
const primaryTextColor = 'rgb(240,240,240)';
|
||||
const logo = (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { generateOGImage } from './generate';
|
||||
import { generate as MetadataImage, getImageResponseOptions } from './generate';
|
||||
import { ImageResponse } from '@takumi-rs/image-response';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
@@ -12,10 +13,15 @@ export async function GET(
|
||||
const page = source.getPage(slug.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
|
||||
return generateOGImage({
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
});
|
||||
return new ImageResponse(
|
||||
(
|
||||
<MetadataImage
|
||||
title={page.data.title}
|
||||
description={page.data.description}
|
||||
/>
|
||||
),
|
||||
await getImageResponseOptions(),
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams(): {
|
||||
|
||||
@@ -7,46 +7,57 @@ description: Usage with Next.js Metadata API.
|
||||
|
||||
## Metadata Image
|
||||
|
||||
Fumadocs has a built-in metadata image generator.
|
||||
You can generate metadata images dynamically using `next/og`.
|
||||
|
||||
You will need a route handler to get started.
|
||||
Add the following under your loader, and define image metadata for pages:
|
||||
|
||||
<include cwd meta='title="app/docs-og/[...slug]/route.tsx"'>
|
||||
../../examples/next-mdx/app/docs-og/[...slug]/route.tsx
|
||||
</include>
|
||||
```ts tab="lib/source.ts"
|
||||
import { type InferPageType } from 'fumadocs-core/source';
|
||||
|
||||
> We need to append `image.png` to the end of slugs so that we can access it via `/docs-og/my-page/image.png`.
|
||||
// [!code ++:8]
|
||||
export function getPageImage(page: InferPageType<typeof source>) {
|
||||
const segments = [...page.slugs, 'image.png'];
|
||||
|
||||
In your docs page, add the image to metadata.
|
||||
return {
|
||||
segments,
|
||||
url: `/og/docs/${segments.join('/')}`,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="app/docs/[[...slug]]/page.tsx"
|
||||
```tsx tab="app/docs/[[...slug]]/page.tsx"
|
||||
import { notFound } from 'next/navigation';
|
||||
import { source } from '@/lib/source';
|
||||
import { source, getPageImage } from '@/lib/source';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug?: string[] }>;
|
||||
}) {
|
||||
const { slug = [] } = await params;
|
||||
const page = source.getPage(slug);
|
||||
export async function generateMetadata(
|
||||
props: PageProps<'/docs/[[...slug]]'>,
|
||||
): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
if (!page) notFound();
|
||||
|
||||
const image = ['/docs-og', ...slug, 'image.png'].join('/');
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
openGraph: {
|
||||
images: image,
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
images: image,
|
||||
// [!code ++]
|
||||
images: getPageImage(page).url,
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
> We append `image.png` to the end of slugs so that we can access it via `/og/docs/my-page/image.png`.
|
||||
|
||||
Finally, create a route handler to generate images at build time:
|
||||
|
||||
<include cwd meta='title="app/og/docs/[...slug]/route.tsx"'>
|
||||
../../examples/next-mdx/app/og/docs/[...slug]/route.tsx
|
||||
</include>
|
||||
|
||||
You can specify options for Satori (used by `next/og`), see https://github.com/vercel/satori for reference.
|
||||
|
||||
### Other Presets
|
||||
|
||||
There's other available styles on Fumadocs CLI, such as `mono`:
|
||||
@@ -55,24 +66,4 @@ There's other available styles on Fumadocs CLI, such as `mono`:
|
||||
npx @fumadocs/cli@latest add og/mono
|
||||
```
|
||||
|
||||
Replaced your old `generateOGImage` with the installed one.
|
||||
|
||||
### Customisations
|
||||
|
||||
You can specify options for Satori, like adding fonts:
|
||||
|
||||
```ts
|
||||
import { generateOGImage } from 'fumadocs-ui/og';
|
||||
|
||||
generateOGImage({
|
||||
fonts: [
|
||||
{
|
||||
name: 'Roboto',
|
||||
// Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here.
|
||||
data: robotoArrayBuffer,
|
||||
weight: 400,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
Replaced your old `generate` with the installed one.
|
||||
|
||||
+28
-59
@@ -1,4 +1,3 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import type { ReactNode } from 'react';
|
||||
import fs from 'node:fs/promises';
|
||||
import type { ImageResponseOptions } from 'next/server';
|
||||
@@ -6,69 +5,39 @@ import type { ImageResponseOptions } from 'next/server';
|
||||
export interface GenerateProps {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
site?: ReactNode;
|
||||
logo?: ReactNode;
|
||||
}
|
||||
|
||||
const font = await fs.readFile('./lib/og/JetBrainsMono-Regular.ttf');
|
||||
const fontBold = await fs.readFile('./lib/og/JetBrainsMono-Bold.ttf');
|
||||
const font = fs.readFile('./lib/og/JetBrainsMono-Regular.ttf');
|
||||
const fontBold = fs.readFile('./lib/og/JetBrainsMono-Bold.ttf');
|
||||
|
||||
export async function generateOGImage(
|
||||
options: GenerateProps & Partial<ImageResponseOptions>,
|
||||
): Promise<ImageResponse> {
|
||||
const { title, description, ...rest } = options;
|
||||
|
||||
return new ImageResponse(
|
||||
generate({
|
||||
title,
|
||||
description,
|
||||
}),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Mono',
|
||||
data: font,
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: 'Mono',
|
||||
data: fontBold,
|
||||
weight: 600,
|
||||
},
|
||||
],
|
||||
...rest,
|
||||
},
|
||||
);
|
||||
export async function getImageResponseOptions(): Promise<ImageResponseOptions> {
|
||||
return {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Mono',
|
||||
data: await font,
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: 'Mono',
|
||||
data: await fontBold,
|
||||
weight: 600,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function generate({ title, description }: GenerateProps) {
|
||||
const siteName = 'Fumadocs';
|
||||
export function generate({
|
||||
title,
|
||||
description,
|
||||
logo,
|
||||
site = 'My App',
|
||||
}: GenerateProps) {
|
||||
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
|
||||
@@ -123,7 +92,7 @@ function generate({ title, description }: GenerateProps) {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{siteName}
|
||||
{site}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { generateOGImage } from 'fumadocs-ui/og';
|
||||
import { source } from '@/lib/source';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: RouteContext<'/docs-og/[...slug]'>,
|
||||
) {
|
||||
const { slug } = await params;
|
||||
const page = source.getPage(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((page) => ({
|
||||
...page,
|
||||
slug: [...page.slug, 'image.png'],
|
||||
}));
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import {
|
||||
DocsBody,
|
||||
DocsDescription,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { notFound } from 'next/navigation';
|
||||
import { getMDXComponents } from '@/mdx-components';
|
||||
import type { Metadata } from 'next';
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
|
||||
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
|
||||
const params = await props.params;
|
||||
@@ -21,7 +22,12 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<MDX components={getMDXComponents()} />
|
||||
<MDX
|
||||
components={getMDXComponents({
|
||||
// this allows you to link to other pages with relative file paths
|
||||
a: createRelativeLink(source, page),
|
||||
})}
|
||||
/>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
@@ -41,5 +47,8 @@ export async function generateMetadata(
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
openGraph: {
|
||||
images: getPageImage(page).url,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { getLLMText, source } from '@/lib/source';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET() {
|
||||
const scan = source.getPages().map(getLLMText);
|
||||
const scanned = await Promise.all(scan);
|
||||
|
||||
return new Response(scanned.join('\n\n'));
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { generate as DefaultImage } from 'fumadocs-ui/og';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: RouteContext<'/og/docs/[...slug]'>,
|
||||
) {
|
||||
const { slug } = await params;
|
||||
const page = source.getPage(slug.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<DefaultImage
|
||||
title={page.data.title}
|
||||
description={page.data.description}
|
||||
site="My App"
|
||||
/>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.getPages().map((page) => ({
|
||||
lang: page.locale,
|
||||
slug: getPageImage(page).segments,
|
||||
}));
|
||||
}
|
||||
@@ -1,7 +1,24 @@
|
||||
import { docs } from '@/.source';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { type InferPageType, loader } from 'fumadocs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: '/docs',
|
||||
source: docs.toFumadocsSource(),
|
||||
});
|
||||
|
||||
export function getPageImage(page: InferPageType<typeof source>) {
|
||||
const segments = [...page.slugs, 'image.png'];
|
||||
|
||||
return {
|
||||
segments,
|
||||
url: `/og/docs/${segments.join('/')}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLLMText(page: InferPageType<typeof source>) {
|
||||
const processed = await page.data.getText('processed');
|
||||
|
||||
return `# ${page.data.title}
|
||||
|
||||
${processed}`;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
|
||||
|
||||
export const docs = defineDocs({
|
||||
dir: 'content/docs',
|
||||
docs: {
|
||||
postprocess: {
|
||||
includeProcessedMarkdown: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default defineConfig();
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import {
|
||||
DocsBody,
|
||||
DocsDescription,
|
||||
DocsPage,
|
||||
DocsTitle,
|
||||
} from 'fumadocs-ui/page';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
import { getMDXComponents } from '@/mdx-components';
|
||||
import type { Metadata } from 'next';
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
|
||||
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
if (!page) notFound();
|
||||
|
||||
const MDXContent = page.data.body;
|
||||
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>
|
||||
<MDXContent
|
||||
<MDX
|
||||
components={getMDXComponents({
|
||||
// this allows you to link to other pages with relative file paths
|
||||
a: createRelativeLink(source, page),
|
||||
@@ -47,5 +47,8 @@ export async function generateMetadata(
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
openGraph: {
|
||||
images: getPageImage(page).url,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { getLLMText, source } from '@/lib/source';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET() {
|
||||
const scan = source.getPages().map(getLLMText);
|
||||
const scanned = await Promise.all(scan);
|
||||
|
||||
return new Response(scanned.join('\n\n'));
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { generate as DefaultImage } from 'fumadocs-ui/og';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: RouteContext<'/og/docs/[...slug]'>,
|
||||
) {
|
||||
const { slug } = await params;
|
||||
const page = source.getPage(slug.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<DefaultImage
|
||||
title={page.data.title}
|
||||
description={page.data.description}
|
||||
site="My App"
|
||||
/>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.getPages().map((page) => ({
|
||||
lang: page.locale,
|
||||
slug: getPageImage(page).segments,
|
||||
}));
|
||||
}
|
||||
@@ -1,9 +1,25 @@
|
||||
import { docs } from '@/.source';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { type InferPageType, loader } from 'fumadocs-core/source';
|
||||
|
||||
// See https://fumadocs.vercel.app/docs/headless/source-api for more info
|
||||
export const source = loader({
|
||||
// it assigns a URL to your pages
|
||||
baseUrl: '/docs',
|
||||
source: docs.toFumadocsSource(),
|
||||
});
|
||||
|
||||
export function getPageImage(page: InferPageType<typeof source>) {
|
||||
const segments = [...page.slugs, 'image.png'];
|
||||
|
||||
return {
|
||||
segments,
|
||||
url: `/og/docs/${segments.join('/')}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLLMText(page: InferPageType<typeof source>) {
|
||||
const processed = await page.data.getText('processed');
|
||||
|
||||
return `# ${page.data.title} (${page.url})
|
||||
|
||||
${processed}`;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,13 @@ import {
|
||||
} from 'fumadocs-mdx/config';
|
||||
|
||||
// You can customise Zod schemas for frontmatter and `meta.json` here
|
||||
// see https://fumadocs.dev/docs/mdx/collections#define-docs
|
||||
// see https://fumadocs.dev/docs/mdx/collections
|
||||
export const docs = defineDocs({
|
||||
docs: {
|
||||
schema: frontmatterSchema,
|
||||
postprocess: {
|
||||
includeProcessedMarkdown: true,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
schema: metaSchema,
|
||||
|
||||
Reference in New Issue
Block a user