chore: use Waku for stackblitz example

This commit is contained in:
Fuma Nama
2026-05-21 16:58:06 +08:00
parent 9184d7f28d
commit 57295eff6e
44 changed files with 318 additions and 304 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ export default function Page() {
Getting Started
</Link>
<a
href="https://stackblitz.com/github/fuma-nama/fumadocs/tree/main/examples/next-stackblitz"
href="https://stackblitz.com/github/fuma-nama/fumadocs/tree/main/examples/stackblitz"
target="_blank"
rel="noreferrer noopener"
className={cn(buttonVariants({ variant: 'secondary' }), 'max-sm:text-sm')}
+1 -1
View File
@@ -22,7 +22,7 @@ export function UwuHero() {
Getting Started
</Link>
<a
href="https://stackblitz.com/github/fuma-nama/fumadocs/tree/main/examples/next-stackblitz"
href="https://stackblitz.com/github/fuma-nama/fumadocs/tree/main/examples/stackblitz"
className={cn(
buttonVariants({
size: 'lg',
-26
View File
@@ -1,26 +0,0 @@
# deps
/node_modules
# generated content
.source
# test & build
/coverage
/.next/
/out/
/build
*.tsbuildinfo
# misc
.DS_Store
*.pem
/.pnp
.pnp.js
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# others
.env*.local
.vercel
next-env.d.ts
@@ -1,16 +0,0 @@
import Link from 'next/link';
export default function HomePage() {
return (
<div className="flex flex-col justify-center text-center flex-1">
<h1 className="text-2xl font-bold mb-4">Hello World</h1>
<p>
You can open{' '}
<Link href="/docs" className="font-medium underline">
/docs
</Link>{' '}
and see the documentation.
</p>
</div>
);
}
@@ -1,7 +0,0 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
export const { GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
language: 'english',
});
-17
View File
@@ -1,17 +0,0 @@
import { RootProvider } from 'fumadocs-ui/provider/next';
import './global.css';
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
});
export default function Layout({ children }: LayoutProps<'/'>) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
<RootProvider>{children}</RootProvider>
</body>
</html>
);
}
@@ -1,23 +0,0 @@
import { getLLMText, getPageMarkdownUrl, source } from '@/lib/source';
import { notFound } from 'next/navigation';
export const revalidate = false;
export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
const { slug } = await params;
const page = source.getPage(slug?.slice(0, -1));
if (!page) notFound();
return new Response(await getLLMText(page), {
headers: {
'Content-Type': 'text/markdown',
},
});
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageMarkdownUrl(page).segments,
}));
}
@@ -1,28 +0,0 @@
import { getPageImage, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { generate as DefaultImage } from 'fumadocs-ui/og';
import { appName } from '@/lib/shared';
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={appName} />,
{
width: 1200,
height: 630,
},
);
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
}));
}
@@ -1,13 +0,0 @@
---
title: Hello World
description: Your first document
---
Welcome to the docs! You can start writing documents in `/content/docs`.
## What is Next?
<Cards>
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
<Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />
</Cards>
@@ -1,17 +0,0 @@
---
title: Components
description: Components
---
## Code Block
```js
console.log('Hello World');
```
## Cards
<Cards>
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
<Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />
</Cards>
-10
View File
@@ -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);
-31
View File
@@ -1,31 +0,0 @@
{
"name": "example-next-stackblitz",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start",
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit"
},
"dependencies": {
"fumadocs-core": "16.8.12",
"fumadocs-mdx": "15.0.7",
"fumadocs-ui": "16.8.12",
"lucide-react": "^1.16.0",
"next": "16.2.6",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.0",
"@types/mdx": "^2.0.13",
"@types/node": "^25.8.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.14",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3"
}
}
@@ -1,7 +0,0 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
-29
View File
@@ -1,29 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { isMarkdownPreferred, rewritePath } from 'fumadocs-core/negotiation';
import { docsContentRoute, docsRoute } from '@/lib/shared';
const { rewrite: rewriteDocs } = rewritePath(
`${docsRoute}{/*path}`,
`${docsContentRoute}{/*path}/content.md`,
);
const { rewrite: rewriteSuffix } = rewritePath(
`${docsRoute}{/*path}.md`,
`${docsContentRoute}{/*path}/content.md`,
);
export default function proxy(request: NextRequest) {
const result = rewriteSuffix(request.nextUrl.pathname);
if (result) {
return NextResponse.rewrite(new URL(result, request.nextUrl));
}
if (isMarkdownPreferred(request)) {
const result = rewriteDocs(request.nextUrl.pathname);
if (result) {
return NextResponse.rewrite(new URL(result, request.nextUrl));
}
}
return NextResponse.next();
}
-35
View File
@@ -1,35 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"],
"collections/*": ["./.source/*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
+6
View File
@@ -0,0 +1,6 @@
.DS_Store
/node_modules/
dist
.source
src/pages.gen.ts
+13
View File
@@ -0,0 +1,13 @@
# waku
This is a Waku application generated with [Create Fumadocs](https://github.com/fuma-nama/fumadocs).
Run development server:
```bash
npm run dev
# or
pnpm dev
# or
yarn dev
```
@@ -0,0 +1,31 @@
---
title: Hello World
description: Your favourite docs framework.
icon: Rocket
---
Hey there! Fumadocs is a docs framework that works on Waku!
## Heading
Hello World!
<Cards>
<Card title="Learn more about Waku" href="https://waku.gg" />
<Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />
</Cards>
### CodeBlock
```ts
console.log('Hello World');
```
#### Table
| Head | Description |
| ------------------------------- | ----------------------------------- |
| `hello` | Hello World |
| very **important** | Hey |
| _Surprisingly_ | Fumadocs |
| very long text that looks weird | hello world hello world hello world |
+12
View File
@@ -0,0 +1,12 @@
---
title: Test
description: This is another page
---
Hello World again!
## Installation
```npm
npm i fumadocs-core fumadocs-ui
```
+34
View File
@@ -0,0 +1,34 @@
{
"name": "example-waku",
"private": true,
"type": "module",
"sideEffects": false,
"scripts": {
"dev": "waku dev",
"build": "waku build",
"start": "waku start",
"types:check": "fumadocs-mdx && tsc --noEmit"
},
"dependencies": {
"@takumi-rs/image-response": "^1.2.1",
"fumadocs-core": "16.9.0",
"fumadocs-mdx": "15.0.7",
"fumadocs-ui": "16.9.0",
"lucide-react": "^1.16.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-server-dom-webpack": "^19.2.6",
"tailwind-merge": "^3.6.0",
"waku": "1.0.0-beta.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.0",
"@types/mdx": "^2.0.13",
"@types/node": "^25.8.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3",
"vite": "^8.0.13"
}
}
@@ -0,0 +1,7 @@
'use client';
import type { ReactNode } from 'react';
import { RootProvider } from 'fumadocs-ui/provider/waku';
export function Provider({ children }: { children: ReactNode }) {
return <RootProvider>{children}</RootProvider>;
}
@@ -1,4 +1,4 @@
export const appName = 'My App';
export const appName = 'Waku';
export const docsRoute = '/docs';
export const docsImageRoute = '/og/docs';
export const docsContentRoute = '/llms.mdx/docs';
@@ -1,17 +1,16 @@
import { docs } from 'collections/server';
import { loader } from 'fumadocs-core/source';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
import { docs } from 'collections/server';
import { docsContentRoute, docsImageRoute, docsRoute } from './shared';
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: docsRoute,
source: docs.toFumadocsSource(),
baseUrl: docsRoute,
plugins: [lucideIconsPlugin()],
});
export function getPageImage(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
export function getPageImage(slugs: string[]) {
const segments = [...slugs, 'image.webp'];
return {
segments,
@@ -0,0 +1 @@
export { DefaultNotFound as default } from 'fumadocs-ui/layouts/home/not-found';
@@ -1,6 +1,7 @@
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import type { ReactNode } from 'react';
import { baseOptions } from '@/lib/layout.shared';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
export default function Layout({ children }: LayoutProps<'/'>) {
export default function Layout({ children }: { children: ReactNode }) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
}
@@ -0,0 +1,21 @@
import { Link } from 'waku';
export default function Home() {
return (
<div className="flex-1 flex flex-col items-center justify-center text-center">
<h1 className="font-medium text-xl mb-4">Fumadocs on Waku.</h1>
<Link
to="/docs"
className="px-3 py-2 rounded-lg bg-fd-primary text-fd-primary-foreground font-medium text-sm mx-auto"
>
Open Docs
</Link>
</div>
);
}
export async function getConfig() {
return {
render: 'static',
};
}
@@ -0,0 +1,4 @@
import { createFromSource } from 'fumadocs-core/search/server';
import { source } from '@/lib/source';
export const { GET } = createFromSource(source);
@@ -1,10 +1,13 @@
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'));
}
export async function getConfig() {
return {
render: 'static' as const,
} as const;
}
@@ -0,0 +1,29 @@
import { getLLMText, source } from '@/lib/source';
import { ApiContext } from 'waku/router';
import { unstable_notFound } from 'waku/router/server';
export async function GET(
_: Request,
{ params }: ApiContext<'/llms.mdx/docs/[...slugs]/content.md'>,
) {
const slugs = params.slugs;
const page = source.getPage(slugs);
if (!page) unstable_notFound();
return new Response(await getLLMText(page), {
headers: {
'Content-Type': 'text/markdown',
},
});
}
export async function getConfig() {
const pages = source
.generateParams()
.map((item) => (item.lang ? [item.lang, ...item.slug] : item.slug));
return {
render: 'static' as const,
staticPaths: pages,
} as const;
}
@@ -1,8 +1,12 @@
import { source } from '@/lib/source';
import { llms } from 'fumadocs-core/source';
export const revalidate = false;
export function GET() {
return new Response(llms(source).index());
}
export async function getConfig() {
return {
render: 'static' as const,
} as const;
}
@@ -0,0 +1,31 @@
import { appName } from '@/lib/shared';
import { source } from '@/lib/source';
import { ImageResponse } from '@takumi-rs/image-response';
import { generate as DefaultImage } from 'fumadocs-ui/og/takumi';
import { ApiContext } from 'waku/router';
export async function GET(_: Request, { params }: ApiContext<'/og/docs/[...slugs]/image.webp'>) {
const page = source.getPage(params.slugs);
if (!page) return new Response(undefined, { status: 404 });
return new ImageResponse(
<DefaultImage title={page.data.title} description={page.data.description} site={appName} />,
{
width: 1200,
height: 630,
format: 'webp',
},
);
}
export async function getConfig() {
const pages = source
.generateParams()
.map((item) => (item.lang ? [item.lang, ...item.slug] : item.slug));
return {
render: 'static' as const,
staticPaths: pages,
} as const;
}
+20
View File
@@ -0,0 +1,20 @@
import type { ReactNode } from 'react';
import { Provider } from '@/components/provider';
import '@/styles/globals.css';
export default async function RootElement({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head></head>
<body data-version="1.0" className="flex flex-col min-h-screen">
<Provider>{children}</Provider>
</body>
</html>
);
}
export async function getConfig() {
return {
render: 'static',
} as const;
}
@@ -1,4 +1,6 @@
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
import { PageProps } from 'waku/router';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import {
DocsBody,
DocsDescription,
@@ -7,25 +9,22 @@ import {
MarkdownCopyButton,
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/docs/page';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/components/mdx';
import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { unstable_notFound } from 'waku/router/server';
import { gitConfig } from '@/lib/shared';
import { getMDXComponents } from '@/components/mdx';
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
export default function Page({ slugs }: PageProps<'/docs/[...slugs]'>) {
const page = source.getPage(slugs);
if (!page) unstable_notFound();
const MDX = page.data.body;
const markdownUrl = getPageMarkdownUrl(page).url;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsPage toc={page.data.toc}>
<meta property="og:image" content={getPageImage(slugs).url} />
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b pb-6">
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
<MarkdownCopyButton markdownUrl={markdownUrl} />
<ViewOptionsPopover
markdownUrl={markdownUrl}
@@ -44,20 +43,13 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
);
}
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
export async function getConfig() {
const pages = source
.generateParams()
.map((item) => (item.lang ? [item.lang, ...item.slug] : item.slug));
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
},
};
render: 'static' as const,
staticPaths: pages,
} as const;
}
@@ -1,10 +1,11 @@
import { source } from '@/lib/source';
import type { ReactNode } from 'react';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { source } from '@/lib/source';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/docs'>) {
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
<DocsLayout {...baseOptions()} tree={source.getPageTree()}>
{children}
</DocsLayout>
);
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "ES2022"],
"types": ["vite", "vite/client"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"noEmit": true,
"paths": {
"@/*": ["./src/*"],
"collections/*": ["./.source/*"]
}
},
"exclude": ["node_modules", "dist"]
}
+14
View File
@@ -0,0 +1,14 @@
import { defineConfig } from 'waku/config';
import mdx from 'fumadocs-mdx/vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
vite: {
resolve: {
tsconfigPaths: true,
external: ['@takumi-rs/image-response'],
},
plugins: [tailwindcss(), mdx()],
},
});
+1 -1
View File
@@ -14,7 +14,7 @@
"release": "turbo run build --filter=./packages/* && changeset publish",
"test": "vitest",
"types:check": "turbo run types:check --continue",
"version": "changeset version && pnpm install --lockfile-only"
"version": "changeset version && node scripts/update-stackblitz-versions.mjs && pnpm install --lockfile-only"
},
"devDependencies": {
"@changesets/cli": "^2.31.0",
+1 -1
View File
@@ -2,7 +2,7 @@ packages:
- packages/*
- apps/*
- examples/*
- '!examples/next-stackblitz'
- '!examples/stackblitz'
allowBuilds:
'@parcel/watcher': false
core-js: false
+26
View File
@@ -0,0 +1,26 @@
/**
* Update template for StackBlitz
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const packageDirs = {
'fumadocs-core': 'packages/core',
'fumadocs-mdx': 'packages/mdx',
'fumadocs-ui': 'packages/radix-ui',
};
const stackblitzPath = join(root, 'examples/stackblitz/package.json');
const stackblitz = JSON.parse(readFileSync(stackblitzPath, 'utf8'));
for (const [name, dir] of Object.entries(packageDirs)) {
if (!stackblitz.dependencies || !(name in stackblitz.dependencies)) continue;
const { version } = JSON.parse(readFileSync(join(root, dir, 'package.json'), 'utf8'));
stackblitz.dependencies[name] = version;
}
writeFileSync(stackblitzPath, `${JSON.stringify(stackblitz, null, 2)}\n`);