Migrated project from Quartz to Next.js with Fumadocs

This commit is contained in:
Jonas List 2025-04-14 22:08:00 +02:00
parent d0e8fca4a6
commit ff60b8afc1
383 changed files with 8990 additions and 152443 deletions

View file

@ -0,0 +1,66 @@
import { source } from "@/lib/source";
import {
DocsPage,
DocsBody,
DocsDescription,
DocsTitle,
} from "fumadocs-ui/page";
import { notFound } from "next/navigation";
import defaultMdxComponents, { createRelativeLink } from "fumadocs-ui/mdx";
import { ImageZoom } from "fumadocs-ui/components/image-zoom";
import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock";
import { metadataImage } from "@/lib/metadata";
export default async function Page(props: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
const MDXContent = page.data.body;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDXContent
components={{
...defaultMdxComponents,
a: createRelativeLink(source, page),
img: (props: React.ImgHTMLAttributes<HTMLImageElement>) => (
<ImageZoom {...props} />
),
pre: (props) => (
<CodeBlock {...props}>
<Pre>{props.children}</Pre>
</CodeBlock>
),
}}
/>
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return metadataImage.withImage(page.slugs, {
title: page.data.title,
description: page.data.description,
metadataBase:
process.env.NODE_ENV === "production"
? new URL("https://docs.zen-browser.app")
: undefined,
});
}

12
src/app/(docs)/layout.tsx Normal file
View file

@ -0,0 +1,12 @@
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import type { ReactNode } from "react";
import { baseOptions } from "@/app/layout.config";
import { source } from "@/lib/source";
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout tree={source.pageTree} {...baseOptions}>
{children}
</DocsLayout>
);
}

View file

@ -0,0 +1,27 @@
import { buttonVariants } from "fumadocs-ui/components/api";
import Link from "next/link";
export default function NotFound() {
return (
<div className="flex items-center w-full h-screen">
<div className="w-full space-y-6 text-center">
<div className="space-y-3">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl transition-transform">
404
</h1>
<p className="text-gray-500">
Looks like you&apos;ve ventured into the unknown digital realm.
</p>
</div>
<Link
className={buttonVariants({
color: "outline",
})}
href="/"
>
Return to Docs
</Link>
</div>
</div>
);
}

View file

@ -0,0 +1,7 @@
import { source } from "@/lib/source";
import { createFromSource } from "fumadocs-core/search/server";
// it should be cached forever
export const revalidate = false;
export const { staticGET: GET } = createFromSource(source);

BIN
src/app/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -0,0 +1,97 @@
import { metadataImage } from "@/lib/metadata";
import { ImageResponse } from "next/og";
export const GET = metadataImage.createAPI(async (page) => {
const size = {
width: 1200,
height: 630,
};
return new ImageResponse(
(
<div
style={{
fontSize: 48,
background: "#2E2E2E",
width: "100%",
height: "100%",
color: "#F2F0E3",
display: "flex",
flexDirection: "column",
paddingLeft: "80px",
paddingRight: "80px",
}}
>
<div style={{ position: "absolute", left: "80px", top: "60px" }}>
zen docs
</div>
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: "auto",
marginBottom: "auto",
}}
>
<div
style={{
fontWeight: "bolder",
fontSize: "52px",
marginBottom: "10px",
}}
>
{page.data.title}
</div>
<div
style={{
color: "#A9A79B",
fontSize: "36px",
}}
>
{page.data.description}
</div>
</div>
<svg
style={{
position: "absolute",
bottom: "-225px",
right: "-225px",
height: "450px",
width: "450px",
}}
width="32"
height="32"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M32 44.3077C38.7974 44.3077 44.3077 38.7974 44.3077 32C44.3077 25.2027 38.7974 19.6923 32 19.6923C25.2027 19.6923 19.6923 25.2027 19.6923 32C19.6923 38.7974 25.2027 44.3077 32 44.3077ZM41.8462 32C41.8462 37.4379 37.4379 41.8462 32 41.8462C26.5621 41.8462 22.1538 37.4379 22.1538 32C22.1538 26.5621 26.5621 22.1538 32 22.1538C37.4379 22.1538 41.8462 26.5621 41.8462 32Z"
fill="currentColor"
></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M53.3333 32C53.3333 43.7821 43.7821 53.3333 32 53.3333C20.2179 53.3333 10.6667 43.7821 10.6667 32C10.6667 20.2179 20.2179 10.6667 32 10.6667C43.7821 10.6667 53.3333 20.2179 53.3333 32ZM32 49.2308C41.5163 49.2308 49.2308 41.5163 49.2308 32C49.2308 22.4837 41.5163 14.7692 32 14.7692C22.4837 14.7692 14.7692 22.4837 14.7692 32C14.7692 41.5163 22.4837 49.2308 32 49.2308Z"
fill="currentColor"
></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 32C64 49.6731 49.6731 64 32 64C14.3269 64 0 49.6731 0 32C0 14.3269 14.3269 0 32 0C49.6731 0 64 14.3269 64 32ZM32 58.2564C46.501 58.2564 58.2564 46.501 58.2564 32C58.2564 17.499 46.501 5.74359 32 5.74359C17.499 5.74359 5.74359 17.499 5.74359 32C5.74359 46.501 17.499 58.2564 32 58.2564Z"
fill="currentColor"
></path>
</svg>
</div>
),
{
...size,
}
);
});
export function generateStaticParams() {
return metadataImage.generateParams();
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

60
src/app/global.css Normal file
View file

@ -0,0 +1,60 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@source '../../node_modules/fumadocs-ui/dist/**/*.js';
:root {
--color-fd-background: #F2F0E3;
--color-fd-foreground: #2E2E2E;
--color-fd-primary: #F76F53;
--color-fd-accent: #E6E4D7;
--color-fd-accent-foreground: #2E2E2E;
--color-fd-card: #F7F6EE;
--color-fd-card-foreground: #2E2E2E;
--color-fd-muted: #2e2e2e0b;
--color-fd-muted-foreground: #2e2e2ec5;
--color-fd-secondary: #E6E4D7;
--color-fd-secondary-foreground: #2E2E2E;
--color-fd-popover: #F2F0E3;
--color-fd-popover-foreground: #2E2E2E;
--color-fd-border: #EAE9E2;
}
.dark {
--color-fd-background: #1F1F1F;
--color-fd-foreground: #D1CFC0;
--color-fd-primary: #F76F53;
--color-fd-accent: #363636;
--color-fd-accent-foreground: #D1CFC0;
--color-fd-card: #161616;
--color-fd-card-foreground: #D1CFC0;
--color-fd-muted: #d1cfc00b;
--color-fd-muted-foreground: #D1CFC0c5;
--color-fd-secondary: #363636;
--color-fd-secondary-foreground: #2E2E2E;
--color-fd-popover: #1F1F1F;
--color-fd-popover-foreground: #D1CFC0;
--color-fd-border: #2E2E2E;
}
aside {
background-color: var(--color-fd-background) !important;
}
aside a[data-active="true"] {
color: var(--color-fd-foreground) !important;
background-color: var(--color-fd-accent) !important;
}
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
font-size: 14px;
}
g.main:has(> g.plot) > rect {
display: none;
}
video {
border-radius: 40px;
}

3
src/app/icon0.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="32" height="32"><svg color="#F76F53" width="32" height="32" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M32 44.3077C38.7974 44.3077 44.3077 38.7974 44.3077 32C44.3077 25.2027 38.7974 19.6923 32 19.6923C25.2027 19.6923 19.6923 25.2027 19.6923 32C19.6923 38.7974 25.2027 44.3077 32 44.3077ZM41.8462 32C41.8462 37.4379 37.4379 41.8462 32 41.8462C26.5621 41.8462 22.1538 37.4379 22.1538 32C22.1538 26.5621 26.5621 22.1538 32 22.1538C37.4379 22.1538 41.8462 26.5621 41.8462 32Z" fill="currentColor"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M53.3333 32C53.3333 43.7821 43.7821 53.3333 32 53.3333C20.2179 53.3333 10.6667 43.7821 10.6667 32C10.6667 20.2179 20.2179 10.6667 32 10.6667C43.7821 10.6667 53.3333 20.2179 53.3333 32ZM32 49.2308C41.5163 49.2308 49.2308 41.5163 49.2308 32C49.2308 22.4837 41.5163 14.7692 32 14.7692C22.4837 14.7692 14.7692 22.4837 14.7692 32C14.7692 41.5163 22.4837 49.2308 32 49.2308Z" fill="currentColor"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M64 32C64 49.6731 49.6731 64 32 64C14.3269 64 0 49.6731 0 32C0 14.3269 14.3269 0 32 0C49.6731 0 64 14.3269 64 32ZM32 58.2564C46.501 58.2564 58.2564 46.501 58.2564 32C58.2564 17.499 46.501 5.74359 32 5.74359C17.499 5.74359 5.74359 17.499 5.74359 32C5.74359 46.501 17.499 58.2564 32 58.2564Z" fill="currentColor"></path> </svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
@media (prefers-color-scheme: dark) { :root { filter: none; } }
</style></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
src/app/icon1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

27
src/app/layout.config.tsx Normal file
View file

@ -0,0 +1,27 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import Image from "next/image";
/**
* Shared layout configurations
*
* you can customise layouts individually from:
* Home Layout: app/(home)/layout.tsx
* Docs Layout: app/docs/layout.tsx
*/
export const baseOptions: BaseLayoutProps = {
nav: {
title: (
<>
<Image
width="24"
height="24"
src="/icon.svg"
aria-label="Logo"
alt="Logo"
/>
zen docs
</>
),
},
githubUrl: "https://github.com/zen-browser/docs",
};

26
src/app/layout.tsx Normal file
View file

@ -0,0 +1,26 @@
import "./global.css";
import { RootProvider } from "fumadocs-ui/provider";
import { Inter } from "next/font/google";
import type { ReactNode } from "react";
const inter = Inter({
subsets: ["latin"],
});
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
<RootProvider
search={{
options: {
type: "static",
},
}}
>
{children}
</RootProvider>
</body>
</html>
);
}

43
src/app/llms.txt/route.ts Normal file
View file

@ -0,0 +1,43 @@
import * as fs from "node:fs/promises";
import fg from "fast-glob";
import matter from "gray-matter";
import { remark } from "remark";
import remarkGfm from "remark-gfm";
import remarkStringify from "remark-stringify";
import remarkMdx from "remark-mdx";
import { remarkInclude } from "fumadocs-mdx/config";
export const revalidate = false;
const processor = remark()
.use(remarkMdx)
// https://fumadocs.vercel.app/docs/mdx/include
.use(remarkInclude)
// gfm styles
.use(remarkGfm)
// .use(your remark plugins)
.use(remarkStringify); // to string
export async function GET() {
// all scanned content
const files = await fg(["./content/docs/**/*.mdx"]);
const scan = files.map(async (file) => {
const fileContent = await fs.readFile(file);
const { content, data } = matter(fileContent.toString());
const processed = await processor.process({
path: file,
value: content,
});
return `file: ${file}
meta: ${JSON.stringify(data, null, 2)}
${processed}`;
});
const scanned = await Promise.all(scan);
return new Response(scanned.join("\n\n"));
}

21
src/app/manifest.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "Zen Docs",
"short_name": "Zen Docs",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#F2F0E3",
"background_color": "#F2F0E3",
"display": "standalone"
}

4
src/app/robots.txt Normal file
View file

@ -0,0 +1,4 @@
User-Agent: *
Allow: /
Sitemap: https://docs.zen-browser.app/sitemap.xml

60
src/app/sitemap.ts Normal file
View file

@ -0,0 +1,60 @@
import type { MetadataRoute } from "next";
import { source } from "@/lib/source";
export const dynamic = "force-static";
interface TreeNode {
$id: string;
type?: string;
name?: string;
url?: string;
index?: TreeNode;
children?: TreeNode[];
}
function generateSitemap(
root: TreeNode,
baseUrl: string = "https://docs.zen-browser.app"
): MetadataRoute.Sitemap {
const sitemap: MetadataRoute.Sitemap = [];
function traverse(node: TreeNode): void {
if (!node) return;
// If the node is a page and has a URL, add it to the sitemap.
if (node.type === "page" && node.url) {
// Concatenate baseUrl with the node URL.
const fullUrl = baseUrl + (node.url === "/" ? "" : node.url);
sitemap.push({
url: fullUrl,
// Use 'monthly' for the homepage and 'monthly' for other pages (customize as needed)
changeFrequency: node.url === "/" ? "monthly" : "monthly",
// Set a higher priority for the homepage
priority: node.url === "/" ? 1 : 0.8,
});
}
// If the node is a folder and has an index page, add that index page to the sitemap.
if (node.type === "folder" && node.index && node.index.url) {
const fullUrl = baseUrl + (node.index.url === "/" ? "" : node.index.url);
sitemap.push({
url: fullUrl,
lastModified: new Date(),
changeFrequency: node.index.url === "/" ? "monthly" : "monthly",
priority: node.index.url === "/" ? 1 : 0.8,
});
}
// Recursively process children if any.
if (node.children && node.children.length) {
node.children.forEach((child) => traverse(child));
}
}
traverse(root);
return sitemap;
}
export default function sitemap(): MetadataRoute.Sitemap {
return generateSitemap(source.pageTree as TreeNode);
}

7
src/lib/metadata.ts Normal file
View file

@ -0,0 +1,7 @@
import { createMetadataImage } from "fumadocs-core/server";
import { source } from "@/lib/source";
export const metadataImage = createMetadataImage({
imageRoute: "/docs-og",
source,
});

9
src/lib/source.ts Normal file
View file

@ -0,0 +1,9 @@
import { docs } from "@/.source";
import { loader } from "fumadocs-core/source";
// `loader()` also assign a URL to your pages
// See https://fumadocs.vercel.app/docs/headless/source-api for more info
export const source = loader({
baseUrl: "/",
source: docs.toFumadocsSource(),
});