refactor: improve routing utils in templates

This commit is contained in:
Fuma Nama
2026-07-23 19:00:01 +08:00
parent 67aaef3b4c
commit 746ded311d
57 changed files with 260 additions and 264 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ import * as Twoslash from 'fumadocs-twoslash/ui';
import { Callout } from 'fumadocs-ui/components/callout';
import { TypeTable } from 'fumadocs-ui/components/type-table';
import * as Preview from '@/components/preview';
import { createMetadata, getPageImage } from '@/lib/metadata';
import { createMetadata, getPageImageUrl } from '@/lib/metadata';
import { source } from '@/lib/source';
import { Wrapper } from '@/components/preview/wrapper';
import { Mermaid } from '@/components/mdx/mermaid';
@@ -176,7 +176,7 @@ export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): P
const description = page.data.description ?? 'The library for building documentation sites';
const image = {
url: getPageImage(page).url,
url: getPageImageUrl(page).url,
width: 1200,
height: 630,
};
+2 -2
View File
@@ -2,7 +2,7 @@ import { source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { generate as MetadataImage, getImageResponseOptions } from './generate';
import { ImageResponse } from 'takumi-js/response';
import { getPageImage } from '@/lib/metadata';
import { getPageImageUrl } from '@/lib/metadata';
export const revalidate = false;
@@ -21,6 +21,6 @@ export function generateStaticParams(): {
slug: string[];
}[] {
return source.getPages().map((page) => ({
slug: getPageImage(page).segments,
slug: getPageImageUrl(page).segments,
}));
}
@@ -280,19 +280,12 @@ export default {
} satisfies Config;
```
```ts tab="Tanstack Start" title="lib/source.ts"
```ts tab="Tanstack Start" title="lib/shared.ts"
// create the following utils:
export function markdownPathToSlugs(segs: string[]) {
if (segs.length === 0) return [];
export const docsRoute = '/docs';
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[]) {
export function encodeMarkdownUrl(slugs: string[], locale?: string) {
const segments = [...slugs];
if (segments.length === 0) {
segments.push('index.md');
@@ -300,23 +293,31 @@ export function slugsToMarkdownPath(slugs: string[]) {
segments[segments.length - 1] += '.md';
}
return {
segments,
url: `/docs/${segments.join('/')}`,
};
return '/' + [locale, ...docsRoute.split('/'), ...segments].filter(Boolean).join('/');
}
/** @returns page slugs */
export function decodeMarkdownUrl(segments: string[]) {
if (segments.length === 0) return [];
const out = [...segments];
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
if (out.length === 1 && out[0] === 'index') out.pop();
return out;
}
```
```ts tab="Tanstack Start" title="src/routes/docs/{$}[.]md.ts"
import { createFileRoute, notFound } from '@tanstack/react-router';
import { source, markdownPathToSlugs } from '@/lib/source';
import { source } from '@/lib/source';
import { decodeMarkdownUrl } from '@/lib/shared';
import { getLLMText } from '@/lib/get-llm-text';
export const Route = createFileRoute('/docs/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
const slugs = decodeMarkdownUrl(params._splat?.split('/') ?? []);
const page = source.getPage(slugs);
if (!page) throw notFound();
@@ -364,12 +365,12 @@ export async function getConfig() {
```ts tab="Waku" title="lib/source.ts"
// create a util for Markdown URLs (append /content.md):
export function getPageMarkdownUrl(page: { slugs: string[] }) {
export function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) {
const segments = [...page.slugs, 'content.md'];
return {
segments,
url: `/llms.mdx/docs/${segments.join('/')}`,
url: '/' + [page.locale, 'llms.mdx', 'docs', ...segments].filter(Boolean).join('/'),
};
}
```
@@ -13,19 +13,19 @@ Add the following under your loader, and define image metadata for pages:
```ts tab="lib/source.ts"
// [!code ++:8]
export function getPageImage(page: (typeof source)['$inferPage']) {
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
url: '/' + [page.locale, 'og', 'docs', ...segments].filter(Boolean).join('/'),
};
}
```
```tsx tab="app/docs/[[...slug]]/page.tsx"
import { notFound } from 'next/navigation';
import { source, getPageImage } from '@/lib/source';
import { source, getPageImageUrl } from '@/lib/source';
import type { Metadata } from 'next';
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
@@ -38,7 +38,7 @@ export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): P
description: page.data.description,
openGraph: {
// [!code ++]
images: getPageImage(page).url,
images: getPageImageUrl(page).url,
},
};
}
@@ -30,19 +30,19 @@ Add the following under your loader, and define image metadata for pages:
```ts tab="Next.js" title="lib/source.ts"
// [!code ++:8]
export function getPageImage(page: (typeof source)['$inferPage']) {
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.webp'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
url: '/' + [page.locale, 'og', 'docs', ...segments].filter(Boolean).join('/'),
};
}
```
```tsx tab="Next.js" title="app/docs/[[...slug]]/page.tsx"
import { notFound } from 'next/navigation';
import { source, getPageImage } from '@/lib/source';
import { source, getPageImageUrl } from '@/lib/source';
import type { Metadata } from 'next';
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
@@ -55,57 +55,60 @@ export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): P
description: page.data.description,
openGraph: {
// [!code ++]
images: getPageImage(page).url,
images: getPageImageUrl(page).url,
},
};
}
```
```tsx tab="React Router" title="app/lib/og.ts"
export function getPageImage(slugs: string[]) {
const segments = [...slugs, 'image.webp'];
```tsx tab="React Router" title="app/lib/shared.ts"
export const docsImageRoute = '/og/docs';
return {
segments,
url: `/og/docs/${segments.join('/')}`,
};
// [!code ++:5]
export function getPageImagePath(slugs: string[], locale?: string) {
return (
'/' + [locale, ...docsImageRoute.split('/'), ...slugs, 'image.webp'].filter(Boolean).join('/')
);
}
```
```tsx tab="React Router" title="app/routes/docs.tsx"
// [!code ++]
import { getPageImage } from '@/lib/og';
import { getPageImagePath } from '@/lib/shared';
// Somewhere in your loader
return {
// [!code ++]
imagePath: getPageImagePath(page.slugs, page.locale),
};
// Somewhere in your component or client loader
return (
<>
{/* [!code ++] */}
<meta property="og:image" content={getPageImage(page.slugs).url} />
<meta property="og:image" content={imagePath} />
</>
);
```
```tsx tab="Vite Based Framework" title="app/lib/source.ts"
// [!code ++:8]
export function getPageImage(slugs: string[]) {
const segments = [...slugs, 'image.webp'];
// [!code ++:5]
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.webp'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
};
return '/' + [page.locale, 'og', 'docs', ...segments].filter(Boolean).join('/');
}
```
```tsx tab="Vite Based Framework" title="app/routes/docs.tsx"
// [!code ++]
import { getPageImage } from '@/lib/source';
import { getPageImageUrl } from '@/lib/source';
// Somewhere in your component or client loader
return (
<>
{/* [!code ++] */}
<meta property="og:image" content={getPageImage(page.slugs).url} />
<meta property="og:image" content={getPageImageUrl(page)} />
</>
);
```
@@ -115,7 +118,7 @@ return (
Finally, create a route handler to generate images at build time:
```tsx tab="Next.js" title="app/og/docs/[...slug]/route.tsx"
import { getPageImage, source } from '@/lib/source';
import { getPageImageUrl, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'takumi-js/response';
import { generate as DefaultImage } from 'fumadocs-ui/og/takumi';
@@ -140,7 +143,7 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
slug: getPageImageUrl(page).segments,
}));
}
```
@@ -217,6 +220,7 @@ export default [
import type { Config } from '@react-router/dev/config';
import { glob } from 'node:fs/promises';
import { createGetUrl, getSlugs } from 'fumadocs-core/source';
import { getPageImagePath } from '@/lib/shared';
const getUrl = createGetUrl('/docs');
@@ -233,7 +237,7 @@ export default {
const slugs = getSlugs(entry);
paths.push(getUrl(slugs));
paths.push(`/og/docs/${[...slugs, 'image.webp'].join('/')}`);
paths.push(getPageImagePath(slugs));
}
return paths;
+2 -2
View File
@@ -34,12 +34,12 @@ export function createMetadata(override: Metadata): Metadata {
};
}
export function getPageImage(page: Page) {
export function getPageImageUrl(page: Page) {
const segments = [...page.slugs, 'image.webp'];
return {
segments,
url: `/og/${segments.join('/')}`,
url: '/' + [page.locale, 'og', ...segments].filter(Boolean).join('/'),
};
}
+3 -6
View File
@@ -13,13 +13,10 @@ export function getStructuredData(entry: CollectionEntry<'docs'>): StructuredDat
return structure(entry.body);
}
export function getPageImage(slugs: string[]) {
const segments = [...slugs, 'image.webp'];
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.webp'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
};
return '/' + [page.locale, 'og', 'docs', ...segments].filter(Boolean).join('/');
}
async function createMySource() {
+3 -2
View File
@@ -3,7 +3,7 @@ import { render, type CollectionEntry } from "astro:content";
import { Docs } from "@/components/docs";
import defaultMdxComponents from "fumadocs-ui/mdx";
import "@/styles/global.css";
import { getPageImage, source } from "@/lib/source";
import { getPageImageUrl, source } from "@/lib/source";
import Layout from "@/components/layout.astro";
import type { TOCItemType } from "fumadocs-core/toc";
@@ -21,7 +21,8 @@ export async function getStaticPaths() {
const { page } = Astro.props;
const { Content, headings } = await render(page);
const slugs = typeof Astro.params.slug === "string" ? Astro.params.slug.split("/") : [];
const image = getPageImage(slugs).url;
const docsPage = source.getPage(slugs)!;
const image = getPageImageUrl(docsPage);
const toc: TOCItemType[] = headings.map((heading) => ({
depth: heading.depth,
@@ -1,4 +1,4 @@
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
import { getPageImageUrl, getPageMarkdownUrl, source } from '@/lib/source';
import {
DocsBody,
DocsDescription,
@@ -57,7 +57,7 @@ export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): P
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
images: getPageImageUrl(page).url,
},
};
}
@@ -1,4 +1,4 @@
import { getPageImage, source } from '@/lib/source';
import { getPageImageUrl, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { generate as DefaultImage } from 'fumadocs-ui/og';
@@ -23,6 +23,6 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
slug: getPageImageUrl(page).segments,
}));
}
+3 -3
View File
@@ -10,12 +10,12 @@ export const source = loader({
plugins: [lucideIconsPlugin()],
});
export function getPageImage(page: (typeof source)['$inferPage']) {
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: `${docsImageRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
@@ -24,7 +24,7 @@ export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
return {
segments,
url: `${docsContentRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
@@ -1,4 +1,4 @@
import { getPageImage, getPageMarkdownUrl, getSource } from '@/lib/source';
import { getPageImageUrl, getPageMarkdownUrl, getSource } from '@/lib/source';
import {
DocsBody,
DocsDescription,
@@ -80,7 +80,7 @@ export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): P
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
images: getPageImageUrl(page).url,
},
};
}
@@ -1,4 +1,4 @@
import { getPageImage, getSource } from '@/lib/source';
import { getPageImageUrl, getSource } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { generate as DefaultImage } from 'fumadocs-ui/og';
@@ -25,6 +25,6 @@ export async function generateStaticParams() {
const docs = await getSource();
return docs.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
slug: getPageImageUrl(page).segments,
}));
}
+3 -3
View File
@@ -21,12 +21,12 @@ export async function getSource() {
return docsLoader.get();
}
export function getPageImage(page: (typeof docsLoader)['$inferPage']) {
export function getPageImageUrl(page: (typeof docsLoader)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: `${docsImageRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
@@ -35,7 +35,7 @@ export function getPageMarkdownUrl(page: (typeof docsLoader)['$inferPage']) {
return {
segments,
url: `${docsContentRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
@@ -1,4 +1,4 @@
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
import { getPageImageUrl, getPageMarkdownUrl, source } from '@/lib/source';
import {
DocsBody,
DocsDescription,
@@ -57,7 +57,7 @@ export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): P
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
images: getPageImageUrl(page).url,
},
};
}
@@ -1,4 +1,4 @@
import { getPageImage, source } from '@/lib/source';
import { getPageImageUrl, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { generate as DefaultImage } from 'fumadocs-ui/og';
@@ -23,6 +23,6 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
slug: getPageImageUrl(page).segments,
}));
}
+3 -3
View File
@@ -9,12 +9,12 @@ export const source = loader({
plugins: [],
});
export function getPageImage(page: (typeof source)['$inferPage']) {
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: `${docsImageRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
@@ -23,7 +23,7 @@ export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
return {
segments,
url: `${docsContentRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
import { getPageImageUrl, getPageMarkdownUrl, source } from '@/lib/source';
import {
DocsBody,
DocsDescription,
@@ -57,7 +57,7 @@ export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): P
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
images: getPageImageUrl(page).url,
},
};
}
@@ -1,4 +1,4 @@
import { getPageImage, source } from '@/lib/source';
import { getPageImageUrl, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { generate as DefaultImage } from 'fumadocs-ui/og';
@@ -23,6 +23,6 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
slug: getPageImageUrl(page).segments,
}));
}
+3 -3
View File
@@ -10,12 +10,12 @@ export const source = loader({
plugins: [lucideIconsPlugin()],
});
export function getPageImage(page: (typeof source)['$inferPage']) {
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: `${docsImageRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
@@ -24,7 +24,7 @@ export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
return {
segments,
url: `${docsContentRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
+1 -1
View File
@@ -12,7 +12,7 @@ export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
return {
segments,
url: `${docsContentRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
-5
View File
@@ -1,5 +0,0 @@
export function getPageImagePath(slugs: string[]) {
const segments = [...slugs, 'image.webp'];
return `/og/docs/${segments.join('/')}`;
}
+6
View File
@@ -9,3 +9,9 @@ export const gitConfig = {
repo: 'fumadocs',
branch: 'main',
};
export function getPageImagePath(slugs: string[], locale?: string) {
return (
'/' + [locale, ...docsImageRoute.split('/'), ...slugs, 'image.webp'].filter(Boolean).join('/')
);
}
+1 -1
View File
@@ -12,7 +12,7 @@ export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
return {
segments,
url: `${docsContentRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
+2 -3
View File
@@ -11,9 +11,8 @@ import {
import { getPageMarkdownUrl, source } from '@/lib/source';
import browserCollections from 'collections/browser';
import { baseOptions } from '@/lib/layout.shared';
import { gitConfig } from '@/lib/shared';
import { gitConfig, getPageImagePath } from '@/lib/shared';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import { getPageImagePath } from '@/lib/og';
import { useMDXComponents } from '@/components/mdx';
export async function loader({ params }: Route.LoaderArgs) {
@@ -25,7 +24,7 @@ export async function loader({ params }: Route.LoaderArgs) {
path: page.path,
markdownUrl: getPageMarkdownUrl(page).url,
pageTree: await source.serializePageTree(source.getPageTree()),
imagePath: getPageImagePath(slugs),
imagePath: getPageImagePath(page.slugs, page.locale),
};
}
+1 -1
View File
@@ -1,7 +1,7 @@
import type { Config } from '@react-router/dev/config';
import { glob } from 'node:fs/promises';
import { createGetUrl, getSlugs } from 'fumadocs-core/source';
import { getPageImagePath } from './app/lib/og';
import { getPageImagePath } from '@/lib/shared';
const getUrl = createGetUrl('/docs');
+4 -7
View File
@@ -9,13 +9,10 @@ export const source = loader({
plugins: [lucideIconsPlugin()],
});
export function getPageImage(slugs: string[]) {
const segments = [...slugs, 'image.webp'];
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.webp'];
return {
segments,
url: `${docsImageRoute}/${segments.join('/')}`,
};
return '/' + [page.locale, ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/');
}
export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
@@ -23,7 +20,7 @@ export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
return {
segments,
url: `${docsContentRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
@@ -1,4 +1,4 @@
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
import { getPageImageUrl, getPageMarkdownUrl, source } from '@/lib/source';
import { PageProps } from 'waku/router';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import {
@@ -21,7 +21,7 @@ export default function Page({ slugs }: PageProps<'/docs/[...slugs]'>) {
const markdownUrl = getPageMarkdownUrl(page).url;
return (
<DocsPage toc={page.data.toc}>
<meta property="og:image" content={getPageImage(slugs).url} />
<meta property="og:image" content={getPageImageUrl(page)} />
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
@@ -8,3 +8,24 @@ export const gitConfig = {
repo: 'fumadocs',
branch: 'main',
};
export function encodeMarkdownUrl(slugs: string[], locale?: string) {
const segments = [...slugs];
if (segments.length === 0) {
segments.push('index.md');
} else {
segments[segments.length - 1] += '.md';
}
return '/' + [locale, ...docsRoute.split('/'), ...segments].filter(Boolean).join('/');
}
/** @returns page slugs */
export function decodeMarkdownUrl(segments: string[]) {
if (segments.length === 0) return [];
const out = [...segments];
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
if (out.length === 1 && out[0] === 'index') out.pop();
return out;
}
@@ -20,29 +20,6 @@ export async function getSource() {
return source.get();
}
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: `${docsRoute}/${segments.join('/')}`,
};
}
export async function getLLMText(page: (typeof source)['$inferPage']) {
return `# ${page.data.title} (${page.url})
@@ -1,7 +1,7 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { createServerFn } from '@tanstack/react-start';
import { getSource, slugsToMarkdownPath } from '@/lib/source';
import { getSource } from '@/lib/source';
import {
DocsBody,
DocsDescription,
@@ -11,7 +11,7 @@ import {
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/docs/page';
import { baseOptions } from '@/lib/layout.shared';
import { gitConfig } from '@/lib/shared';
import { encodeMarkdownUrl, gitConfig } from '@/lib/shared';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import { useMemo } from 'react';
import { useMDXComponents } from '@/components/mdx';
@@ -40,7 +40,7 @@ const serverLoader = createServerFn({
path: page.path,
frontmatter: page.data.frontmatter,
render: serialize(),
markdownUrl: slugsToMarkdownPath(page.slugs).url,
markdownUrl: encodeMarkdownUrl(page.slugs, page.locale),
pageTree: await source.serializePageTree(source.getPageTree()),
};
});
@@ -1,11 +1,12 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getLLMText, getSource, markdownPathToSlugs } from '@/lib/source';
import { getLLMText, getSource } from '@/lib/source';
import { decodeMarkdownUrl } from '@/lib/shared';
export const Route = createFileRoute('/docs/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
const slugs = decodeMarkdownUrl(params._splat?.split('/') ?? []);
const source = await getSource();
const page = source.getPage(slugs);
if (!page) throw notFound();
@@ -1,8 +1,7 @@
import { createMiddleware, createStart } from '@tanstack/react-start';
import { isMarkdownPreferred } from 'fumadocs-core/negotiation';
import { redirect } from '@tanstack/react-router';
import { docsRoute } from '@/lib/shared';
import { slugsToMarkdownPath } from './lib/source';
import { docsRoute, encodeMarkdownUrl } from '@/lib/shared';
const llmMiddleware = createMiddleware().server(({ next, request }) => {
const url = new URL(request.url);
@@ -16,7 +15,7 @@ const llmMiddleware = createMiddleware().server(({ next, request }) => {
.slice(docsRoute.length)
.split('/')
.filter((v) => v.length > 0);
url.pathname = slugsToMarkdownPath(slugs).url;
url.pathname = encodeMarkdownUrl(slugs);
throw redirect(url);
}
@@ -8,3 +8,24 @@ export const gitConfig = {
repo: 'fumadocs',
branch: 'main',
};
export function encodeMarkdownUrl(slugs: string[], locale?: string) {
const segments = [...slugs];
if (segments.length === 0) {
segments.push('index.md');
} else {
segments[segments.length - 1] += '.md';
}
return '/' + [locale, ...docsRoute.split('/'), ...segments].filter(Boolean).join('/');
}
/** @returns page slugs */
export function decodeMarkdownUrl(segments: string[]) {
if (segments.length === 0) return [];
const out = [...segments];
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
if (out.length === 1 && out[0] === 'index') out.pop();
return out;
}
@@ -17,29 +17,6 @@ export const source = loader(
},
);
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: `${docsRoute}/${segments.join('/')}`,
};
}
export async function getLLMText(page: (typeof source)['$inferPage']) {
if (page.type === 'openapi') return JSON.stringify(page.data.getSchema(), null, 2);
@@ -1,7 +1,7 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/notebook';
import { createServerFn } from '@tanstack/react-start';
import { slugsToMarkdownPath, source } from '@/lib/source';
import { source } from '@/lib/source';
import browserCollections from 'collections/browser';
import {
DocsBody,
@@ -12,7 +12,7 @@ import {
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/notebook/page';
import { baseOptions } from '@/lib/layout.shared';
import { gitConfig } from '@/lib/shared';
import { encodeMarkdownUrl, gitConfig } from '@/lib/shared';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import { Suspense, type ReactNode } from 'react';
import { useMDXComponents } from '@/components/mdx';
@@ -53,7 +53,7 @@ const serverLoader = createServerFn({
return {
type: 'docs',
path: page.path,
markdownUrl: slugsToMarkdownPath(page.slugs).url,
markdownUrl: encodeMarkdownUrl(page.slugs, page.locale),
pageTree,
};
});
@@ -1,11 +1,12 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getLLMText, markdownPathToSlugs, source } from '@/lib/source';
import { getLLMText, source } from '@/lib/source';
import { decodeMarkdownUrl } from '@/lib/shared';
export const Route = createFileRoute('/docs/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
const slugs = decodeMarkdownUrl(params._splat?.split('/') ?? []);
const page = source.getPage(slugs);
if (!page) throw notFound();
+2 -3
View File
@@ -1,8 +1,7 @@
import { createMiddleware, createStart } from '@tanstack/react-start';
import { isMarkdownPreferred } from 'fumadocs-core/negotiation';
import { redirect } from '@tanstack/react-router';
import { docsRoute } from '@/lib/shared';
import { slugsToMarkdownPath } from './lib/source';
import { docsRoute, encodeMarkdownUrl } from '@/lib/shared';
const llmMiddleware = createMiddleware().server(({ next, request }) => {
const url = new URL(request.url);
@@ -16,7 +15,7 @@ const llmMiddleware = createMiddleware().server(({ next, request }) => {
.slice(docsRoute.length)
.split('/')
.filter((v) => v.length > 0);
url.pathname = slugsToMarkdownPath(slugs).url;
url.pathname = encodeMarkdownUrl(slugs);
throw redirect(url);
}
@@ -8,3 +8,24 @@ export const gitConfig = {
repo: 'fumadocs',
branch: 'main',
};
export function encodeMarkdownUrl(slugs: string[], locale?: string) {
const segments = [...slugs];
if (segments.length === 0) {
segments.push('index.md');
} else {
segments[segments.length - 1] += '.md';
}
return '/' + [locale, ...docsRoute.split('/'), ...segments].filter(Boolean).join('/');
}
/** @returns page slugs */
export function decodeMarkdownUrl(segments: string[]) {
if (segments.length === 0) return [];
const out = [...segments];
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
if (out.length === 1 && out[0] === 'index') out.pop();
return out;
}
@@ -9,29 +9,6 @@ export const source = loader({
plugins: [lucideIconsPlugin()],
});
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: `${docsRoute}/${segments.join('/')}`,
};
}
export async function getLLMText(page: (typeof source)['$inferPage']) {
const processed = await page.data.getText('processed');
@@ -1,7 +1,7 @@
import { createFileRoute, Link, notFound } from '@tanstack/react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { createServerFn } from '@tanstack/react-start';
import { slugsToMarkdownPath, source } from '@/lib/source';
import { source } from '@/lib/source';
import browserCollections from 'collections/browser';
import {
DocsBody,
@@ -12,7 +12,7 @@ import {
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/docs/page';
import { baseOptions } from '@/lib/layout.shared';
import { gitConfig } from '@/lib/shared';
import { encodeMarkdownUrl, gitConfig } from '@/lib/shared';
import { staticFunctionMiddleware } from '@tanstack/start-static-server-functions';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import { Suspense } from 'react';
@@ -39,7 +39,7 @@ const loader = createServerFn({
return {
path: page.path,
markdownUrl: slugsToMarkdownPath(page.slugs).url,
markdownUrl: encodeMarkdownUrl(page.slugs, page.locale),
pageTree: await source.serializePageTree(source.getPageTree()),
};
});
@@ -1,11 +1,12 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getLLMText, markdownPathToSlugs, source } from '@/lib/source';
import { getLLMText, source } from '@/lib/source';
import { decodeMarkdownUrl } from '@/lib/shared';
export const Route = createFileRoute('/docs/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
const slugs = decodeMarkdownUrl(params._splat?.split('/') ?? []);
const page = source.getPage(slugs);
if (!page) throw notFound();
+21
View File
@@ -8,3 +8,24 @@ export const gitConfig = {
repo: 'fumadocs',
branch: 'main',
};
export function encodeMarkdownUrl(slugs: string[], locale?: string) {
const segments = [...slugs];
if (segments.length === 0) {
segments.push('index.md');
} else {
segments[segments.length - 1] += '.md';
}
return '/' + [locale, ...docsRoute.split('/'), ...segments].filter(Boolean).join('/');
}
/** @returns page slugs */
export function decodeMarkdownUrl(segments: string[]) {
if (segments.length === 0) return [];
const out = [...segments];
out[out.length - 1] = out[out.length - 1].replace(/\.md$/, '');
if (out.length === 1 && out[0] === 'index') out.pop();
return out;
}
-23
View File
@@ -9,29 +9,6 @@ export const source = loader({
plugins: [lucideIconsPlugin()],
});
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: `${docsRoute}/${segments.join('/')}`,
};
}
export async function getLLMText(page: (typeof source)['$inferPage']) {
const processed = await page.data.getText('processed');
@@ -1,7 +1,7 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { createServerFn } from '@tanstack/react-start';
import { slugsToMarkdownPath, source } from '@/lib/source';
import { source } from '@/lib/source';
import browserCollections from 'collections/browser';
import {
DocsBody,
@@ -12,7 +12,7 @@ import {
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/docs/page';
import { baseOptions } from '@/lib/layout.shared';
import { gitConfig } from '@/lib/shared';
import { encodeMarkdownUrl, gitConfig } from '@/lib/shared';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import { Suspense } from 'react';
import { useMDXComponents } from '@/components/mdx';
@@ -37,7 +37,7 @@ const serverLoader = createServerFn({
return {
path: page.path,
markdownUrl: slugsToMarkdownPath(page.slugs).url,
markdownUrl: encodeMarkdownUrl(page.slugs, page.locale),
pageTree: await source.serializePageTree(source.getPageTree()),
};
});
@@ -1,11 +1,12 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getLLMText, markdownPathToSlugs, source } from '@/lib/source';
import { getLLMText, source } from '@/lib/source';
import { decodeMarkdownUrl } from '@/lib/shared';
export const Route = createFileRoute('/docs/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = markdownPathToSlugs(params._splat?.split('/') ?? []);
const slugs = decodeMarkdownUrl(params._splat?.split('/') ?? []);
const page = source.getPage(slugs);
if (!page) throw notFound();
+2 -3
View File
@@ -1,8 +1,7 @@
import { createMiddleware, createCsrfMiddleware, createStart } from '@tanstack/react-start';
import { isMarkdownPreferred } from 'fumadocs-core/negotiation';
import { redirect } from '@tanstack/react-router';
import { docsRoute } from '@/lib/shared';
import { slugsToMarkdownPath } from './lib/source';
import { docsRoute, encodeMarkdownUrl } from '@/lib/shared';
const csrfMiddleware = createCsrfMiddleware({
filter: (ctx) => ctx.handlerType === 'serverFn',
@@ -20,7 +19,7 @@ const llmMiddleware = createMiddleware().server(({ next, request }) => {
.slice(docsRoute.length)
.split('/')
.filter((v) => v.length > 0);
url.pathname = slugsToMarkdownPath(slugs).url;
url.pathname = encodeMarkdownUrl(slugs);
throw redirect(url);
}
+4 -7
View File
@@ -9,13 +9,10 @@ export const source = loader({
plugins: [lucideIconsPlugin()],
});
export function getPageImage(slugs: string[]) {
const segments = [...slugs, 'image.webp'];
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.webp'];
return {
segments,
url: `${docsImageRoute}/${segments.join('/')}`,
};
return '/' + [page.locale, ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/');
}
export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
@@ -23,7 +20,7 @@ export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
return {
segments,
url: `${docsContentRoute}/${segments.join('/')}`,
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
import { getPageImageUrl, getPageMarkdownUrl, source } from '@/lib/source';
import { PageProps } from 'waku/router';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import {
@@ -21,7 +21,7 @@ export default function Page({ slugs }: PageProps<'/docs/[...slugs]'>) {
const markdownUrl = getPageMarkdownUrl(page).url;
return (
<DocsPage toc={page.data.toc}>
<meta property="og:image" content={getPageImage(slugs).url} />
<meta property="og:image" content={getPageImageUrl(page)} />
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
+2 -2
View File
@@ -1,6 +1,6 @@
import type { SortedResult } from '@/search';
import type { SearchClient } from '../client';
import { BASE_PATH } from '@/utils/url';
import { BASE_PATH, join } from '@/utils/url';
export interface FetchOptions {
/**
@@ -26,7 +26,7 @@ export interface FetchOptions {
const globalCache = new Map();
export function fetchClient({
api = `${BASE_PATH}api/search`,
api = join(BASE_PATH, '/api/search'),
locale,
tag,
cache = globalCache,
@@ -2,7 +2,7 @@ import type { SearchClient } from '../client';
import type { ExportedData } from '../flexsearch';
import type { Document } from 'flexsearch';
import { createDocument, search, type Doc } from '../flexsearch/utils';
import { BASE_PATH } from '@/utils/url';
import { BASE_PATH, join } from '@/utils/url';
export interface FlexsearchStaticOptions {
/**
@@ -22,7 +22,7 @@ function initDocument(data: Record<string, string>) {
const cacheMap = new Map<string, Promise<Map<string, Document<Doc>>>>();
export function flexsearchStaticClient(options: FlexsearchStaticOptions = {}): SearchClient {
const { from = `${BASE_PATH}api/search`, locale = '', tag } = options;
const { from = join(BASE_PATH, '/api/search'), locale = '', tag } = options;
let dbs = cacheMap.get(from);
if (!dbs && typeof window !== 'undefined') {
@@ -4,7 +4,7 @@ import { searchAdvanced } from '@/search/orama/search/advanced';
import type { advancedSchema, simpleSchema } from '@/search/orama/create-db';
import type { ExportedData } from '@/search/server';
import type { SearchClient } from '../client';
import { BASE_PATH } from '@/utils/url';
import { BASE_PATH, join } from '@/utils/url';
export interface StaticOptions {
/**
@@ -82,7 +82,7 @@ async function loadDB(
return dbs;
}
function getDBCached({ from = `${BASE_PATH}api/search`, initOrama }: StaticOptions) {
function getDBCached({ from = join(BASE_PATH, '/api/search'), initOrama }: StaticOptions) {
const cacheKey = from;
const cached = cache.get(cacheKey);
if (cached) return cached;
+14 -3
View File
@@ -1,11 +1,22 @@
/**
* The base path (Vite), always ends with `/`.
* The base path (Vite)
*/
export let BASE_PATH: string =
export const BASE_PATH: string =
typeof import.meta.env !== 'undefined' && typeof import.meta.env.BASE_URL === 'string'
? import.meta.env.BASE_URL
: '/';
if (!BASE_PATH.endsWith('/')) BASE_PATH += '/';
export function join(...paths: string[]) {
let out = '';
for (let p of paths) {
if (out.length > 0) {
if (p.startsWith('/')) p = p.slice(1);
if (!out.endsWith('/')) out += '/';
}
out += p;
}
return out;
}
/**
* normalize URL into the Fumadocs standard form (`/slug-1/slug-2`).
@@ -13,13 +13,10 @@ export function getStructuredData(entry: CollectionEntry<'docs'>): StructuredDat
return structure(entry.body);
}
export function getPageImage(slugs: string[]) {
const segments = [...slugs, 'image.webp'];
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.webp'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
};
return '/' + [page.locale, 'og', 'docs', ...segments].filter(Boolean).join('/');
}
async function createMySource() {
@@ -3,7 +3,7 @@ import { render, type CollectionEntry } from "astro:content";
import { Docs } from "@/components/docs";
import defaultMdxComponents from "fumadocs-ui/mdx";
import "@/styles/global.css";
import { getPageImage, source } from "@/lib/source";
import { getPageImageUrl, source } from "@/lib/source";
import Layout from "@/components/layout.astro";
import type { TOCItemType } from "fumadocs-core/toc";
@@ -21,7 +21,8 @@ export async function getStaticPaths() {
const { page } = Astro.props;
const { Content, headings } = await render(page);
const slugs = typeof Astro.params.slug === "string" ? Astro.params.slug.split("/") : [];
const image = getPageImage(slugs).url;
const docsPage = source.getPage(slugs)!;
const image = getPageImageUrl(docsPage);
const toc: TOCItemType[] = headings.map((heading) => ({
depth: heading.depth,
+3 -6
View File
@@ -17,11 +17,8 @@ export const getSource = revalidable({
export type Source = Awaited<ReturnType<typeof getSource>>;
export type SourcePage = Source['$inferPage'];
export function getPageImage(slugs: string[]) {
const segments = [...slugs, 'image.webp'];
export function getPageImageUrl(page: SourcePage) {
const segments = [...page.slugs, 'image.webp'];
return {
segments,
url: `/og/${segments.join('/')}`,
};
return '/' + [page.locale, 'og', ...segments].filter(Boolean).join('/');
}
+2 -2
View File
@@ -1,6 +1,6 @@
import type { PageProps } from 'waku/router';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { getPageImage, getSource, type SourcePage } from '@/lib/source';
import { getPageImageUrl, getSource, type SourcePage } from '@/lib/source';
import { layoutConfig } from '@/layouts/config';
import { getConfigRuntime } from '@/config/load-runtime';
import { Card, Cards } from 'fumadocs-ui/components/card';
@@ -211,7 +211,7 @@ async function MdContent({
return renderContainer(
<DocsPage toc={toc}>
<title>{page.data.title}</title>
<meta property="og:image" content={getPageImage(slugs).url} />
<meta property="og:image" content={getPageImageUrl(page)} />
<meta property="og:description" content={page.data.description} />
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>