diff --git a/apps/docs/app/[lang]/blog/[[...slug]]/page.tsx b/apps/docs/app/[lang]/blog/[[...slug]]/page.tsx index 224d5a0eb..95b343f51 100644 --- a/apps/docs/app/[lang]/blog/[[...slug]]/page.tsx +++ b/apps/docs/app/[lang]/blog/[[...slug]]/page.tsx @@ -1,8 +1,8 @@ import { notFound } from 'next/navigation'; -import { blog } from '@/app/source'; -import defaultMdxComponents from 'fumadocs-ui/mdx'; +import { blog } from '@/lib/source'; +import { getMDXComponents } from '@/mdx-components'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; -import { baseOptions } from '@/app/layout.config'; +import { baseOptions } from '@/lib/layout.shared'; import Link from 'next/link'; import { ArrowLeft } from 'lucide-react'; @@ -16,9 +16,7 @@ interface BlogPostData { body: React.ComponentType; } -const components = { - ...defaultMdxComponents, -} as any; +const components = getMDXComponents() as any; export default async function BlogPage({ params, @@ -32,7 +30,7 @@ export default async function BlogPage({ const posts = blog.getPages(); return ( - +

Blog

@@ -114,7 +112,7 @@ export default async function BlogPage({ const MDX = page.data.body; return ( - +
; @@ -26,19 +17,33 @@ export default async function Page(props: { const page = source.getPage(params.slug ?? [], params.lang); if (!page) notFound(); - const data = page.data as any; - const Content = data.body; + const MDX = page.data.body; return ( - + + {page.data.title} + {page.data.description} +
+ + +
-

{page.data.title}

- {page.data.description && ( -

- {page.data.description} -

- )} - +
); diff --git a/apps/docs/app/[lang]/docs/layout.tsx b/apps/docs/app/[lang]/docs/layout.tsx index 06ecb816d..38c912a62 100644 --- a/apps/docs/app/[lang]/docs/layout.tsx +++ b/apps/docs/app/[lang]/docs/layout.tsx @@ -1,8 +1,7 @@ -import { source } from '@/app/source'; -import type { Metadata } from 'next'; +import { source } from '@/lib/source'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import type { ReactNode } from 'react'; -import { baseOptions } from '@/app/layout.config'; +import { baseOptions } from '@/lib/layout.shared'; export default async function Layout({ params, @@ -16,7 +15,7 @@ export default async function Layout({ return ( {children} diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx index e2fcf3d95..f1c6ad457 100644 --- a/apps/docs/app/[lang]/layout.tsx +++ b/apps/docs/app/[lang]/layout.tsx @@ -18,21 +18,17 @@ export default async function LanguageLayout({ const { lang } = await params; return ( - - - ({ - name: LANGUAGE_NAMES[l] || l, - locale: l, - })), - }} - > - {children} - - - + ({ + name: LANGUAGE_NAMES[l] || l, + locale: l, + })), + }} + > + {children} + ); } diff --git a/apps/docs/app/[lang]/page.tsx b/apps/docs/app/[lang]/page.tsx index 649a0e9ec..46ac5d4fd 100644 --- a/apps/docs/app/[lang]/page.tsx +++ b/apps/docs/app/[lang]/page.tsx @@ -1,6 +1,6 @@ import { Database, Monitor, HardDrive, ShieldCheck, Puzzle, Code2, Rocket, Users, Blocks, LucideIcon } from 'lucide-react'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; -import { baseOptions } from '@/app/layout.config'; +import { baseOptions } from '@/lib/layout.shared'; import { getHomepageTranslations } from '@/lib/homepage-i18n'; import { HeroSection } from '@/components/hero-section'; import { CodePreview } from '@/components/code-preview'; @@ -91,7 +91,7 @@ export default async function HomePage({ ]; return ( - +
{/* Hero Section */} diff --git a/apps/docs/app/api/search/route.ts b/apps/docs/app/api/search/route.ts index afb3bf078..b02d7cd3a 100644 --- a/apps/docs/app/api/search/route.ts +++ b/apps/docs/app/api/search/route.ts @@ -1,10 +1,6 @@ -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('simple', { - indexes: source.getPages().map((page) => ({ - title: page.data.title ?? 'Untitled', - content: page.data.description ?? '', - url: page.url, - })), +export const { GET } = createFromSource(source, { + language: 'english', }); diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index c1d76161a..50b3bc296 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -1,14 +1,3 @@ -@import "tailwindcss"; -@import "fumadocs-ui/style.css"; - -@layer base { - :root { - --radius: 0.5rem; - } -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} +@import 'tailwindcss'; +@import 'fumadocs-ui/css/neutral.css'; +@import 'fumadocs-ui/css/preset.css'; diff --git a/apps/docs/app/layout.config.tsx b/apps/docs/app/layout.config.tsx deleted file mode 100644 index 57c25a799..000000000 --- a/apps/docs/app/layout.config.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; -import Image from 'next/image'; - -export const baseOptions: BaseLayoutProps = { - nav: { - title: ( -
- ObjectStack - ObjectStack -
- ), - transparentMode: 'top', - }, - links: [ - { - text: 'Documentation', - url: '/docs/', - active: 'nested-url', - }, - // { - // text: 'Blog', - // url: '/blog', - // active: 'nested-url', - // }, - // { - // text: 'Concepts', - // url: '/docs/concepts/manifesto', - // active: 'nested-url', - // }, - // { - // text: 'Specs', - // url: '/docs/specifications/data/architecture', - // active: 'nested-url', - // }, - // { - // text: 'Reference', - // url: '/docs/references/data/core/Object', - // active: 'nested-url', - // }, - ], - githubUrl: 'https://github.com/objectstack-ai/spec', -}; diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index f60456680..a1dd50f87 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -1,8 +1,6 @@ import './global.css'; -import { redirect } from 'next/navigation'; import type { ReactNode } from 'react'; import type { Metadata } from 'next'; -import { i18n } from '@/lib/i18n'; export const metadata: Metadata = { title: { @@ -16,11 +14,9 @@ export const metadata: Metadata = { }; export default function RootLayout({ children }: { children: ReactNode }) { - // Root layout is only used for redirects with middleware - // The actual layout is in [lang]/layout.tsx return ( - {children} + {children} ); } diff --git a/apps/docs/app/llms-full.txt/route.ts b/apps/docs/app/llms-full.txt/route.ts new file mode 100644 index 000000000..d494d2cbb --- /dev/null +++ b/apps/docs/app/llms-full.txt/route.ts @@ -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')); +} diff --git a/apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts b/apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts new file mode 100644 index 000000000..fe641f68e --- /dev/null +++ b/apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts @@ -0,0 +1,23 @@ +import { getLLMText, source } from '@/lib/source'; +import { notFound } from 'next/navigation'; + +export const revalidate = false; + +export async function GET( + _req: Request, + { params }: { params: Promise<{ slug?: string[] }> }, +) { + const { slug } = await params; + const page = source.getPage(slug); + if (!page) notFound(); + + return new Response(await getLLMText(page), { + headers: { + 'Content-Type': 'text/markdown', + }, + }); +} + +export function generateStaticParams() { + return source.generateParams(); +} diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts new file mode 100644 index 000000000..6639c252a --- /dev/null +++ b/apps/docs/app/llms.txt/route.ts @@ -0,0 +1,13 @@ +import { source } from '@/lib/source'; + +export const revalidate = false; + +export async function GET() { + const lines: string[] = []; + lines.push('# Documentation'); + lines.push(''); + for (const page of source.getPages()) { + lines.push(`- [${page.data.title}](${page.url}): ${page.data.description}`); + } + return new Response(lines.join('\n')); +} diff --git a/apps/docs/app/og/docs/[...slug]/route.tsx b/apps/docs/app/og/docs/[...slug]/route.tsx new file mode 100644 index 000000000..5efbc353e --- /dev/null +++ b/apps/docs/app/og/docs/[...slug]/route.tsx @@ -0,0 +1,33 @@ +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 }: { params: Promise<{ slug: string[] }> }, +) { + const { slug } = await params; + const page = source.getPage(slug.slice(0, -1)); + 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/apps/docs/app/source.ts b/apps/docs/app/source.ts deleted file mode 100644 index badf8aa26..000000000 --- a/apps/docs/app/source.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { docs, blog as blogCollection } from 'fumadocs-mdx:collections/server'; -import { loader } from 'fumadocs-core/source'; -import { i18n } from '@/lib/i18n'; - -export const source = loader({ - baseUrl: '/docs', - i18n, - source: docs.toFumadocsSource(), -}); - -export const blog = loader({ - baseUrl: '/blog', - source: blogCollection.toFumadocsSource(), -}); diff --git a/apps/docs/components/ai/page-actions.tsx b/apps/docs/components/ai/page-actions.tsx new file mode 100644 index 000000000..7bd762c0a --- /dev/null +++ b/apps/docs/components/ai/page-actions.tsx @@ -0,0 +1,159 @@ +'use client'; +import { useMemo, useState } from 'react'; +import { Check, ChevronDown, Copy, ExternalLinkIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'; +import { buttonVariants } from 'fumadocs-ui/components/ui/button'; +import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover'; + +const cache = new Map(); + +export function LLMCopyButton({ + /** + * A URL to fetch the raw Markdown/MDX content of page + */ + markdownUrl, +}: { + markdownUrl: string; +}) { + const [isLoading, setLoading] = useState(false); + const [checked, onClick] = useCopyButton(async () => { + const cached = cache.get(markdownUrl); + if (cached) return navigator.clipboard.writeText(cached); + + setLoading(true); + + try { + await navigator.clipboard.write([ + new ClipboardItem({ + 'text/plain': fetch(markdownUrl).then(async (res) => { + const content = await res.text(); + cache.set(markdownUrl, content); + + return content; + }), + }), + ]); + } finally { + setLoading(false); + } + }); + + return ( + + ); +} + +export function ViewOptions({ + markdownUrl, + githubUrl, +}: { + /** + * A URL to the raw Markdown/MDX content of page + */ + markdownUrl: string; + + /** + * Source file URL on GitHub + */ + githubUrl: string; +}) { + const items = useMemo(() => { + const fullMarkdownUrl = + typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading'; + const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`; + + return [ + { + title: 'Open in GitHub', + href: githubUrl, + icon: ( + + GitHub + + + ), + }, + { + title: 'Open in ChatGPT', + href: `https://chatgpt.com/?${new URLSearchParams({ + hints: 'search', + q, + })}`, + icon: ( + + OpenAI + + + ), + }, + { + title: 'Open in Claude', + href: `https://claude.ai/new?${new URLSearchParams({ + q, + })}`, + icon: ( + + Anthropic + + + ), + }, + ]; + }, [githubUrl, markdownUrl]); + + return ( + + + Open + + + + {items.map((item) => ( + + {item.icon} + {item.title} + + + ))} + + + ); +} diff --git a/apps/docs/components/ui/badge.tsx b/apps/docs/components/ui/badge.tsx deleted file mode 100644 index ef315a633..000000000 --- a/apps/docs/components/ui/badge.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - outline: "text-foreground border-border", - success: - "border-transparent bg-green-500 text-white hover:bg-green-500/80", - warning: - "border-transparent bg-yellow-500 text-white hover:bg-yellow-500/80", - destructive: - "border-transparent bg-red-500 text-white hover:bg-red-500/80", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} - -export { Badge, badgeVariants } diff --git a/apps/docs/components/ui/button.tsx b/apps/docs/components/ui/button.tsx deleted file mode 100644 index 9b9557e20..000000000 --- a/apps/docs/components/ui/button.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:bg-primary/90 hover:scale-105 hover:shadow-primary/30", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - outline: - "border border-border bg-card/50 backdrop-blur-sm hover:bg-accent hover:text-accent-foreground hover:scale-105", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-12 px-8 py-3", - sm: "h-9 px-4 py-2", - lg: "h-14 px-10 py-4", - icon: "h-10 w-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean -} - -const Button = React.forwardRef( - ({ className, variant, size, ...props }, ref) => { - return ( -