Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync #368

Merged
merged 17 commits into from
May 15, 2024
Merged

Sync #368

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/docs/app/(home)/blog/[slug]/page.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';
import { ShareIcon } from 'lucide-react';
import { cn } from '@/utils/cn';
import { buttonVariants } from '@/components/ui/button';

export function Control({ url }: { url: string }): React.ReactElement {
const onClick = (): void => {
void navigator.clipboard.writeText(`${window.location.origin}${url}`);
};

return (
<button
className={cn(
buttonVariants({ className: 'gap-2', variant: 'secondary' }),
)}
onClick={onClick}
>
<ShareIcon className="size-4" />
Share Post
</button>
);
}
74 changes: 74 additions & 0 deletions apps/docs/app/(home)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { blog } from '@/utils/source';
import { createMetadata } from '@/utils/metadata';
import { buttonVariants } from '@/components/ui/button';
import { Control } from '@/app/(home)/blog/[slug]/page.client';

interface Param {
slug: string;
}

export const dynamicParams = false;

export default function Page({
params,
}: {
params: Param;
}): React.ReactElement {
const page = blog.getPage([params.slug]);

if (!page) notFound();

return (
<>
<div className="container rounded-xl border bg-gradient-to-t from-primary/30 to-50% py-12 md:px-8">
<h1 className="mb-2 text-3xl font-bold">{page.data.title}</h1>
<p className="mb-4 text-muted-foreground">{page.data.description}</p>
<Link
href="/blog"
className={buttonVariants({ size: 'sm', variant: 'secondary' })}
>
Back
</Link>
</div>
<article className="container grid grid-cols-1 px-0 py-8 lg:grid-cols-[2fr_1fr] lg:px-4">
<div className="prose p-4">
<page.data.exports.default />
</div>
<div className="flex flex-col gap-4 border-l p-4">
<div>
<p className="mb-1 text-sm text-muted-foreground">Written by</p>
<p className="font-semibold">{page.data.author}</p>
</div>
<div>
<p className="mb-1 text-sm text-muted-foreground">At</p>
<p className="font-semibold">
{new Date(page.data.date ?? page.file.name).toDateString()}
</p>
</div>
<Control url={page.url} />
</div>
</article>
</>
);
}

export function generateMetadata({ params }: { params: Param }): Metadata {
const page = blog.getPage([params.slug]);

if (!page) notFound();

return createMetadata({
title: page.data.title,
description:
page.data.description ?? 'The library for building documentation sites',
});
}

export function generateStaticParams(): Param[] {
return blog.getPages().map<Param>((page) => ({
slug: page.slugs[0],
}));
}
57 changes: 57 additions & 0 deletions apps/docs/app/(home)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Link from 'next/link';
import { blog } from '@/utils/source';

export default function Page(): React.ReactElement {
const posts = blog.getPages();

const svg = `<svg viewBox='0 0 500 500' xmlns='http://www.w3.org/2000/svg'>
<filter id='noiseFilter'>
<feTurbulence
type='fractalNoise'
baseFrequency='0.65'
numOctaves='3'
stitchTiles='stitch'/>
</filter>

<rect width='100%' height='100%' filter='url(#noiseFilter)'/>
</svg>`;

return (
<main className="container max-sm:px-0 md:py-12">
<div
className="h-[300px] p-8 md:h-[400px] md:p-12"
style={{
backgroundImage: [
'radial-gradient(circle at 70% 10%, rgba(255,50,100,0.5), transparent)',
'radial-gradient(circle at 0% 80%, rgba(190,0,255,0.5), transparent)',
'radial-gradient(circle at 50% 50%, rgba(50,50,255,0.3), transparent)',
`url("data:image/svg+xml,${encodeURIComponent(svg)}")`,
].join(', '),
}}
>
<h1 className="mb-4 border-b-4 border-foreground pb-2 text-4xl font-bold md:text-5xl">
Fumadocs Blog
</h1>
<p>Light and gorgeous. like the moon</p>
</div>
<div className="grid grid-cols-1 gap-2 border md:grid-cols-3 lg:grid-cols-4">
{posts.map((post) => (
<Link
key={post.url}
href={post.url}
className="block bg-card p-4 hover:bg-accent"
>
<p className="font-medium">{post.data.title}</p>
<p className="text-sm text-muted-foreground">
{post.data.description}
</p>

<p className="mt-4 text-xs text-muted-foreground">
{new Date(post.data.date ?? post.file.name).toDateString()}
</p>
</Link>
))}
</div>
</main>
);
}
8 changes: 7 additions & 1 deletion apps/docs/app/layout.config.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DocsLayoutProps } from 'fumadocs-ui/layout';
import { LayoutTemplateIcon } from 'lucide-react';
import { BookIcon, LayoutTemplateIcon } from 'lucide-react';
import { utils } from '@/utils/source';
import { NavChildren, SidebarBanner, Title } from '@/app/layout.client';

Expand All @@ -16,6 +16,12 @@ export const layoutOptions: Omit<DocsLayoutProps, 'children'> = {
banner: <SidebarBanner />,
},
links: [
{
icon: <BookIcon />,
text: 'Blog',
url: '/blog',
active: 'nested-url',
},
{
text: 'Showcase',
url: '/showcase',
Expand Down
106 changes: 106 additions & 0 deletions apps/docs/content/blog/2024-5-15.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
title: How Fumadocs works
description: The framework for building documentation
author: Fuma Nama
---

## About Fumadocs

1 year ago, I was having fun with Next.js App Rouer.
While experimenting it on my toy [No Deploy](https://nodeploy-neon.vercel.app), I planned to build a documentation.
However, Nextra does not support App Router.

To handle this, I implemented a small documentation site with solely Contentlayer and the new features from App Router.
It was working great, looks blazing-fast and minimal.
I cloned the logic from No Deploy and built this documentation framework.
With a few months of development, it soon became powerful and stable.

It was originally named `next-docs`, I renamed it to Fumadocs as it conflicts with Next.js Docs.

Thanks to the support from **Next.js** community, I've received a lot of suggestions along the way.
[Fumadocs](https://fumadocs.vercel.app) is now a framework used by my libraries and some other amazing projects.

### My Opinion

In Web development, most _"robust"_ frameworks/libraries are incredibly heavy and fabulous, but it indeed made our developer experience fancy.

On the top of Javascript, people bulit bundlers, transpilers, and even Typescript.
It feels very surprisingly that Javascript, a high-level scripting language, is more similar to assembly code in modern Web development.
We rarely use them without things like Webpack. This also applies to CSS, at least as my experience, I seldom use CSS without PostCSS.

While they might be necessary for compatibility and DX, the landing of React Server Component and Next.js App Router made the experience even more mindblowing.
It feels like magic. The cunning magical frameworks, and web development miracles.
This kind of design is insane, but it also makes us mindlessly forget the boundaries.

Beginners use Metadata API, while they have no idea how meta tag works.
They put server-side logic in a server component, while they have no idea how expensive the calculation is.
Even when we looked at the code, we can't predict the result without running it in production mode.
I saw too many of these misconceptions.

This happens on many frameworks, they are overly magical.
**I wanted to make it less-magic, and straightforward at least for most Next.js developers.**

### Fumadocs MDX

As the recommended content source, It is actually a webpack hack.
Since Next.js could only optimize static imports, it first transform your `.map.ts` to a file that roughy yields:

```ts
export default [import("./my/file.mdx"), ...];
```

And then transform MDX files with a custom loader. It makes all magic possible, but it doesn't have the ablility of lazy-loading MDX files.
Comparing to Nextra, it might be a suboptimal approach.

Nextra does it even easier, it directly transforms MDX files into pages. Because the Pages Router adapts Javascript files as a single page, it is possible.
In App Router, this is not possible anymore. Therefore, I didn't take this approach.

### Fumadocs Core

The core of Fumadocs is a bunch of utilties and MDX plugins.

- **Source API** construct page trees from content source, integrated with other content providers.
- **Headless components** accelerate Fumadocs UI and other custom UI implementations.
- **MDX plugins** bring a perfect developer experience to all integrations.
- **Search utilities** make it way easier to implement document search.

In addition, it also established the definitions of Page Tree and Page Conventions.
Over all, it is not yet a framework without Fumadocs UI.

In my opinion, the most valuable part in the codebase are MDX plugins.
I learnt a lot more about ASTs and the eco-system of remark/rehype while working on them.
Absolutely an amazing experience.

### Fumadocs UI

The UI implementation of Fumadocs using Tailwind CSS and Radix UI.
Its design system was inspired by Shadcn UI, using CSS variables for color utilities.

Although the structure of Fumadocs UI is even simpler than core, I've used some subtle hacks to solve the problem of `"use client"` directive.
The bundler I am using, [TSX](https://github.com/privatenumber/tsx), can't handle nested structures like client components imported from a server comopnent.
Therefore, I made a little hack to build server components and client components as an individual entry, then inject import statements after the process.

Also it took me some time to come up with the [preset approach](https://fumadocs.vercel.app/docs/ui/theme#docsui-plugin) for integrating Fumadocs UI with Tailwind CSS projects.

### Docs Generators

We have a few built-in integrations, like `fumadocs-openapi` which takes an OpenAPI schema and output MDX files.

For the OpenAPI one, it simply parse the schema and convert it to MDX file through string templates.

The Typescript integration does a bit more, it obtain type information with [Typscript Compiler API](https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API). Based on the information, it yields MDX files.
You can use it inside a server component, which is how `<AutoTypeTable />` works.

### CL/CI

As a project with very few contributors, I built the CL/CI process as convenient as possible for a better efficiency.
The entire release process is handled by [Changesets](https://github.com/changesets/changesets), and I wrote the scripts to update [the template repository](https://github.com/fuma-nama/fumadocs-ui-template) automatically.
It worked great so far.

### Thanks

[The Github repository of Fumadocs](https://github.com/fuma-nama/fumadocs) has reached 300 stars in 2024 March, it is a new achievement for me.
Welcome to give it a star to support my work!

> Original
> https://fuma-nama.vercel.app/blog/fumadocs
2 changes: 1 addition & 1 deletion apps/docs/utils/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ export function createMetadata(override: Metadata): Metadata {
export const baseUrl =
process.env.NODE_ENV === 'development'
? new URL('http://localhost:3000')
: new URL(`https://${process.env.VERCEL_URL}`);
: new URL(`https://${process.env.VERCEL_URL!}`);
29 changes: 22 additions & 7 deletions apps/docs/utils/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,35 @@ import { icons } from 'lucide-react';
import { map } from '@/.map';
import { create } from '@/components/ui/icon';

const frontmatterSchema = defaultSchemas.frontmatter.extend({
preview: z.string().optional(),
toc: z.boolean().default(true),
index: z.boolean().default(false),
});

export const utils = loader({
baseUrl: '/docs',
rootDir: 'docs',
icon(icon) {
if (icon in icons)
return create({ icon: icons[icon as keyof typeof icons] });
},
source: createMDXSource(map, { schema: { frontmatter: frontmatterSchema } }),
source: createMDXSource(map, {
schema: {
frontmatter: defaultSchemas.frontmatter.extend({
preview: z.string().optional(),
toc: z.boolean().default(true),
index: z.boolean().default(false),
}),
},
}),
});

export const blog = loader({
baseUrl: '/blog',
rootDir: 'blog',
source: createMDXSource(map, {
schema: {
frontmatter: defaultSchemas.frontmatter.extend({
author: z.string(),
date: z.string().date().optional(),
}),
},
}),
});

export type Page = InferPageType<typeof utils>;
Expand Down
6 changes: 3 additions & 3 deletions examples/contentlayer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
},
"dependencies": {
"contentlayer": "0.3.4",
"fumadocs-contentlayer": "1.1.18",
"fumadocs-core": "11.0.7",
"fumadocs-ui": "11.0.7",
"fumadocs-contentlayer": "1.1.20",
"fumadocs-core": "11.1.0",
"fumadocs-ui": "11.1.0",
"next": "^14.2.3",
"next-contentlayer": "^0.3.4",
"react": "18.3.1",
Expand Down
6 changes: 3 additions & 3 deletions examples/next-mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"start": "next start"
},
"dependencies": {
"fumadocs-core": "11.0.7",
"fumadocs-mdx": "8.2.16",
"fumadocs-ui": "11.0.7",
"fumadocs-core": "11.1.0",
"fumadocs-mdx": "8.2.18",
"fumadocs-ui": "11.1.0",
"next": "^14.2.3",
"react": "18.3.1",
"react-dom": "18.3.1"
Expand Down
13 changes: 13 additions & 0 deletions packages/contentlayer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# fumadocs-contentlayer

## 1.1.20

### Patch Changes

- [email protected]

## 1.1.19

### Patch Changes

- Updated dependencies [98258b5]
- [email protected]

## 1.1.18

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/contentlayer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fumadocs-contentlayer",
"version": "1.1.18",
"version": "1.1.20",
"description": "The Contentlayer adapter for Fumadocs",
"keywords": [
"NextJs",
Expand Down
8 changes: 8 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# next-docs-zeta

## 11.1.0

## 11.0.8

### Patch Changes

- 98258b5: Fix regex problems

## 11.0.7

### Patch Changes
Expand Down
Loading