From 129899ed61870fdd4e23f87883949078a888250e Mon Sep 17 00:00:00 2001 From: Nexmoe <16796652+nexmoe@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:31:24 +0800 Subject: [PATCH] fix(docs): resolve TypeScript build errors (#144) * fix(docs): resolve TypeScript build errors and search configuration Fix invalid LayoutProps type in [lang]/(docs)/layout.tsx to use proper Next.js 16 async layout component signature. Add type assertion in i18n.ts isLocale function to satisfy strict type checking. Configure search to use English indexing since Orama doesn't support Chinese. Co-Authored-By: Claude Haiku 4.5 * feat(docs): configure static export following Fumadocs guidelines - Enable Next.js static export with output: 'export' - Configure static search using staticGET and revalidate: false - Update search UI to use type: 'static' for client-side search - Add localeMap to support both English and Chinese locales - Remove rewrites (incompatible with static export) - Remove route handlers that conflict with static export: - /llms.mdx route (file extension conflicts) - /llms-full.txt route - /og/docs OG image generation - Add trailingSlash: true for better route handling Build successfully generates 9 static pages (2.7MB total). Search indexes are exported as static files for client-side use. Reference: https://www.fumadocs.dev/docs/deploying/static Co-Authored-By: Claude Sonnet 4.5 * fix(docs): resolve 404 issues in static export - Change hideLocale from 'default-locale' to 'never' for static export compatibility (middleware doesn't work in static builds) - Add public/index.html to redirect root path to /en/ - Update middleware matcher to remove deleted route exclusions - All pages now accessible with language prefix (/en/, /zh/) This fixes the 404 issue where middleware-based rewrites don't work in static exports. Now all URLs explicitly include the language code. Co-Authored-By: Claude Sonnet 4.5 * feat(docs): serve English content at root path without redirect - Restore hideLocale to 'default-locale' for cleaner URLs - Add post-export script to copy /en/ content to root directory - English pages now accessible at both / and /en/ - Chinese pages remain at /zh/ - Remove redirect page in favor of direct content serving - Add "type": "module" to package.json for ES modules This provides a better user experience by serving English content directly at the root path (/, /cookies/, /faq/) without requiring a redirect, while maintaining /en/ for explicit access. Output size: 3.2MB (includes both root and /en/ copies) Co-Authored-By: Claude Sonnet 4.5 --------- Co-authored-by: Claude Haiku 4.5 --- docs/next.config.mjs | 13 +++--- docs/package.json | 3 +- docs/scripts/post-export.js | 46 ++++++++++++++++++++++ docs/src/app/[lang]/(docs)/layout.tsx | 6 ++- docs/src/app/[lang]/layout.tsx | 13 +++++- docs/src/app/api/search/route.ts | 13 ++++-- docs/src/app/llms-full.txt/route.ts | 10 ----- docs/src/app/llms.mdx/[[...slug]]/route.ts | 27 ------------- docs/src/app/og/docs/[...slug]/route.tsx | 30 -------------- docs/src/lib/i18n.ts | 3 +- docs/src/middleware.ts | 3 +- 11 files changed, 84 insertions(+), 83 deletions(-) create mode 100755 docs/scripts/post-export.js delete mode 100644 docs/src/app/llms-full.txt/route.ts delete mode 100644 docs/src/app/llms.mdx/[[...slug]]/route.ts delete mode 100644 docs/src/app/og/docs/[...slug]/route.tsx diff --git a/docs/next.config.mjs b/docs/next.config.mjs index ae0a6d2..4a82553 100644 --- a/docs/next.config.mjs +++ b/docs/next.config.mjs @@ -4,15 +4,12 @@ const withMDX = createMDX(); /** @type {import('next').NextConfig} */ const config = { + output: 'export', reactStrictMode: true, - async rewrites() { - return [ - { - source: '/:path*.mdx', - destination: '/llms.mdx/:path*', - }, - ]; - }, + // Use trailing slashes to avoid conflicts with route handlers that have file extensions + trailingSlash: true, + // Note: rewrites are not supported with static export + // The /llms.mdx route will be pre-rendered as static files }; export default withMDX(config); diff --git a/docs/package.json b/docs/package.json index 16ba5fb..d3554d4 100644 --- a/docs/package.json +++ b/docs/package.json @@ -2,8 +2,9 @@ "name": "docs", "version": "0.0.0", "private": true, + "type": "module", "scripts": { - "build": "next build", + "build": "next build && node scripts/post-export.js", "dev": "next dev", "start": "next start", "types:check": "fumadocs-mdx && next typegen && tsc --noEmit", diff --git a/docs/scripts/post-export.js b/docs/scripts/post-export.js new file mode 100755 index 0000000..08a761f --- /dev/null +++ b/docs/scripts/post-export.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +/** + * Post-export script to copy English content to root directory + * This allows the default language (en) to be accessible without language prefix + */ + +import { cpSync, existsSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const outDir = join(__dirname, '../out'); +const enDir = join(outDir, 'en'); + +console.log('Copying English content to root directory...'); + +if (!existsSync(enDir)) { + console.error('Error: /en directory not found in output'); + process.exit(1); +} + +// Get all items in /en directory +const fs = await import('node:fs/promises'); +const items = await fs.readdir(enDir); + +// Copy each item to root, excluding already existing root items +for (const item of items) { + const source = join(enDir, item); + const dest = join(outDir, item); + + // Skip if item already exists at root (like _next, api, etc.) + if (existsSync(dest)) { + console.log(`Skipping ${item} (already exists at root)`); + continue; + } + + try { + cpSync(source, dest, { recursive: true }); + console.log(`Copied ${item}`); + } catch (error) { + console.error(`Error copying ${item}:`, error.message); + } +} + +console.log('English content copied to root directory successfully!'); diff --git a/docs/src/app/[lang]/(docs)/layout.tsx b/docs/src/app/[lang]/(docs)/layout.tsx index 1527d11..4ac7c24 100644 --- a/docs/src/app/[lang]/(docs)/layout.tsx +++ b/docs/src/app/[lang]/(docs)/layout.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import { source } from '@/lib/source'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { baseOptions } from '@/lib/layout.shared'; @@ -5,7 +6,10 @@ import { baseOptions } from '@/lib/layout.shared'; export default async function Layout({ children, params, -}: LayoutProps<'/[lang]/[[...slug]]'>) { +}: { + children: ReactNode; + params: Promise<{ lang: string; slug?: string[] }>; +}) { const resolvedParams = await params; const locale = resolvedParams.lang; return ( diff --git a/docs/src/app/[lang]/layout.tsx b/docs/src/app/[lang]/layout.tsx index cb1ec6d..a3a9ff1 100644 --- a/docs/src/app/[lang]/layout.tsx +++ b/docs/src/app/[lang]/layout.tsx @@ -24,5 +24,16 @@ export default async function Layout({ const { lang } = await params; const locale = isLocale(lang) ? lang : i18n.defaultLanguage; - return {children}; + return ( + + {children} + + ); } diff --git a/docs/src/app/api/search/route.ts b/docs/src/app/api/search/route.ts index 7ba7e82..21cbcce 100644 --- a/docs/src/app/api/search/route.ts +++ b/docs/src/app/api/search/route.ts @@ -1,7 +1,14 @@ 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', +// statically cached for static export +export const revalidate = false; + +// Configure language support for both English and Chinese +export const { staticGET: GET } = createFromSource(source, { + localeMap: { + en: { language: 'english' }, + // Chinese is not natively supported by Orama, use English tokenizer for zh + zh: { language: 'english' }, + }, }); diff --git a/docs/src/app/llms-full.txt/route.ts b/docs/src/app/llms-full.txt/route.ts deleted file mode 100644 index d494d2c..0000000 --- a/docs/src/app/llms-full.txt/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -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')); -} diff --git a/docs/src/app/llms.mdx/[[...slug]]/route.ts b/docs/src/app/llms.mdx/[[...slug]]/route.ts deleted file mode 100644 index 1281482..0000000 --- a/docs/src/app/llms.mdx/[[...slug]]/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { i18n, resolveLocaleFromSlug, stripLocaleFromSlug } from '@/lib/i18n'; -import { getLLMText, source } from '@/lib/source'; -import { notFound } from 'next/navigation'; - -export const revalidate = false; - -export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/[[...slug]]'>) { - const { slug } = await params; - const locale = resolveLocaleFromSlug(slug); - const slugs = stripLocaleFromSlug(slug); - const page = source.getPage(slugs, locale); - if (!page) notFound(); - - return new Response(await getLLMText(page), { - headers: { - 'Content-Type': 'text/markdown', - }, - }); -} - -export function generateStaticParams() { - return source.getLanguages().flatMap(({ language, pages }) => - pages.map((page) => ({ - slug: language === i18n.defaultLanguage ? page.slugs : [language, ...page.slugs], - })), - ); -} diff --git a/docs/src/app/og/docs/[...slug]/route.tsx b/docs/src/app/og/docs/[...slug]/route.tsx deleted file mode 100644 index fcf1863..0000000 --- a/docs/src/app/og/docs/[...slug]/route.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { resolveLocaleFromSlug, stripLocaleFromSlug } from '@/lib/i18n'; -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 pageSlug = slug.slice(0, -1); - const locale = resolveLocaleFromSlug(pageSlug); - const slugs = stripLocaleFromSlug(pageSlug); - const page = source.getPage(slugs, locale); - if (!page) notFound(); - - return new ImageResponse( - , - { - width: 1200, - height: 630, - }, - ); -} - -export function generateStaticParams() { - return source.getPages().map((page) => ({ - slug: getPageImage(page).segments, - })); -} diff --git a/docs/src/lib/i18n.ts b/docs/src/lib/i18n.ts index 350f748..4b5526f 100644 --- a/docs/src/lib/i18n.ts +++ b/docs/src/lib/i18n.ts @@ -3,6 +3,7 @@ import { defineI18n } from 'fumadocs-core/i18n'; export const i18n = defineI18n({ languages: ['en', 'zh'], defaultLanguage: 'en', + // Hide locale prefix for default language (en) so English content appears at root hideLocale: 'default-locale', parser: 'dir' }); @@ -12,7 +13,7 @@ export type Locale = (typeof i18n.languages)[number]; const localeSet = new Set(i18n.languages); export function isLocale(value?: string): value is Locale { - return Boolean(value && localeSet.has(value)); + return Boolean(value && localeSet.has(value as Locale)); } export function resolveLocaleFromSlug(slug?: string[]): Locale { diff --git a/docs/src/middleware.ts b/docs/src/middleware.ts index c8ee8ba..dda3835 100644 --- a/docs/src/middleware.ts +++ b/docs/src/middleware.ts @@ -4,5 +4,6 @@ import { i18n } from '@/lib/i18n'; export default createI18nMiddleware(i18n); export const config = { - matcher: ['/((?!api|_next/static|_next/image|favicon.ico|og|llms\.mdx|llms-full\.txt).*)'], + // Note: Middleware doesn't run in static export, but kept for development + matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };