diff --git a/apps/docs/app/docs/[[...slug]]/page.tsx b/apps/docs/app/docs/[[...slug]]/page.tsx
index 00469f7..f637faa 100644
--- a/apps/docs/app/docs/[[...slug]]/page.tsx
+++ b/apps/docs/app/docs/[[...slug]]/page.tsx
@@ -4,7 +4,7 @@ import {
DocsDescription,
DocsPage,
DocsTitle,
-} from "fumadocs-ui/layouts/docs/page";
+} from "fumadocs-ui/layouts/notebook/page";
import { notFound } from "next/navigation";
import { getMDXComponents } from "@/mdx-components";
import type { Metadata } from "next";
@@ -55,7 +55,7 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
),
}}
>
-
+
{page.data.title}
) {
diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx
index fa14649..5ba6c56 100644
--- a/apps/docs/app/layout.tsx
+++ b/apps/docs/app/layout.tsx
@@ -4,7 +4,6 @@ import { Inter, JetBrains_Mono } from "next/font/google";
import { Metadata } from "next";
import { TITLE_DESCRIPTION } from "./docs/[[...slug]]/page";
import { getPageImage, source } from "@/lib/source";
-import { notFound } from "next/navigation";
const inter = Inter({
subsets: ["latin"],
@@ -18,7 +17,9 @@ const jetbrainsMono = JetBrains_Mono({
const page = source.getPage(["/docs"]);
export const metadata: Metadata = {
- metadataBase: new URL(process.env.BASE_URL!),
+ metadataBase: new URL(
+ process.env.BASE_URL ?? "http://localhost:3000"
+ ),
title: TITLE_DESCRIPTION,
description:
"An opinionated HTTP router with typed path params, built-in body validation, and a clean auth model. Compatible with Cloudflare Workers, Node.js, Bun, Deno, and other modern JavaScript runtimes. Built by Liveblocks.",
diff --git a/apps/docs/components/ai/page-actions.tsx b/apps/docs/components/ai/page-actions.tsx
index c69e23e..0e38b55 100644
--- a/apps/docs/components/ai/page-actions.tsx
+++ b/apps/docs/components/ai/page-actions.tsx
@@ -137,7 +137,7 @@ export function PageActions({
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 cursor-pointer rounded-l-[5px] hover:bg-fd-accent hover:text-fd-accent-foreground"
>
{checked ? : }
- {checked ? "Copied!" : "Copy page"}
+ {checked ? "Copied!" : "Copy page"}
diff --git a/apps/docs/content/docs/get-started.mdx b/apps/docs/content/docs/get-started.mdx
new file mode 100644
index 0000000..aac5318
--- /dev/null
+++ b/apps/docs/content/docs/get-started.mdx
@@ -0,0 +1,204 @@
+---
+title: Get started
+description: Install Zen Router with npm, pnpm, bun, or yarn.
+---
+
+import { Step, Steps } from "fumadocs-ui/components/steps";
+
+Zen Router is an opinionated API router built by
+[Liveblocks](https://liveblocks.io), where it has been powering billions of
+requests per month. It’s designed for Cloudflare Workers, Bun, Node.js, and
+other modern JavaScript runtimes.
+
+
+
+
+### Install Zen Router
+
+Install the package with your package manager.
+
+```bash tab="npm"
+npm install @liveblocks/zenrouter
+```
+
+```bash tab="pnpm"
+pnpm add @liveblocks/zenrouter
+```
+
+```bash tab="bun"
+bun add @liveblocks/zenrouter
+```
+
+```bash tab="yarn"
+yarn add @liveblocks/zenrouter
+```
+
+
+
+
+### Create a router
+
+Create a router instance in your project.
+
+```ts lineNumbers tab="Cloudflare Workers"
+import { ZenRouter } from "@liveblocks/zenrouter";
+
+const zen = new ZenRouter({
+ authorize: async ({ req }) => {
+ // ...
+ return {};
+ },
+});
+
+export default zen;
+```
+
+```ts lineNumbers tab="Bun"
+import { ZenRouter } from "@liveblocks/zenrouter";
+
+const zen = new ZenRouter({
+ authorize: async ({ req }) => {
+ // ...
+ return {};
+ },
+});
+
+Bun.serve({ fetch: zen.fetch, port: 8000 });
+```
+
+```ts lineNumbers tab="Node.js"
+import { createServerAdapter } from "@whatwg-node/server";
+import { createServer } from "http";
+import { ZenRouter } from "@liveblocks/zenrouter";
+
+const zen = new ZenRouter({
+ authorize: async ({ req }) => {
+ // ...
+ return {};
+ },
+});
+
+const zenServer = createServerAdapter(zen.fetch);
+const httpServer = createServer(zenServer);
+
+httpServer.listen(3001);
+```
+
+
+
+
+
+### Authorize requests
+
+Set up an `authorize` function to [authenticate the request](/docs/authorization).
+
+```ts lineNumbers
+const zen = new ZenRouter({
+ authorize: async ({ req }) => {
+ const token = req.headers.get("Authorization");
+ const currentUser = await db.getUserByToken(token);
+
+ if (!currentUser) {
+ return false;
+ }
+
+ return { currentUser };
+ },
+});
+```
+
+
+
+
+### Define a GET route
+
+Use `zen.route` to [define a route](/docs/routes).
+
+```ts lineNumbers tab="Cloudflare Workers"
+import { abort } from "@liveblocks/zenrouter";
+
+// Use params in your route, e.g. `postId`
+zen.route(
+ "GET /api/posts/",
+
+ async ({ p, auth }) => {
+ const post = await db.getPostByUser(auth.currentUser.id, p.postId);
+
+ if (!post) {
+ abort(404);
+ }
+
+ return { id: post.id, title: post.title };
+ }
+);
+```
+
+
+
+
+
+### Define a POST route
+
+Now add a second endpoint. Pass a schema to [validate the request body](/docs/routes#body-validation).
+
+```ts lineNumbers tab="Cloudflare Workers" tab="zod"
+import { abort } from "@liveblocks/zenrouter";
+import { z } from "zod";
+
+// Validate the request body with zod
+zen.route(
+ "POST /api/posts",
+
+ z.object({ title: z.string() }),
+
+ async ({ auth, body }) => {
+ const post = await db.createPost({
+ title: body.title,
+ authorId: auth.currentUser.id,
+ });
+
+ if (!post.success) {
+ abort(500);
+ }
+
+ return { id: post.id, title: post.title };
+ }
+);
+```
+
+```ts lineNumbers tab="Cloudflare Workers" tab="decoders"
+import { abort } from "@liveblocks/zenrouter";
+import { object, string } from "decoders";
+
+// Validate the request body with decoders
+zen.route(
+ "POST /api/posts",
+
+ object({ title: string }),
+
+ async ({ auth, body }) => {
+ const post = await db.createPost({
+ title: body.title,
+ authorId: auth.currentUser.id,
+ });
+
+ if (!post.success) {
+ abort(500);
+ }
+
+ return { id: post.id, title: post.title };
+ }
+);
+```
+
+
+
+
+
+### Learn more
+
+Get started with [CORS](/docs/cors), [error handling](/docs/error-handling)...
+
+
+
+
diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx
index dce53a1..f8788d3 100644
--- a/apps/docs/content/docs/index.mdx
+++ b/apps/docs/content/docs/index.mdx
@@ -1,5 +1,5 @@
---
-title: Zen Router
+title: Overview
description: An opinionated HTTP router with typed path params, built-in body validation, and a clean auth model.
---
@@ -8,21 +8,17 @@ Zen Router is an opinionated API router built by
requests per month. It’s designed for Cloudflare Workers, Bun, Node.js, and
other modern JavaScript runtimes.
-```bash tab="npm"
-npm install @liveblocks/zenrouter
-```
-
-```bash tab="pnpm"
-pnpm add @liveblocks/zenrouter
-```
-
-```bash tab="bun"
-bun add @liveblocks/zenrouter
-```
+## Features
-```bash tab="yarn"
-yarn add @liveblocks/zenrouter
-```
+- [Type-safe everywhere](/docs/routes). Your handler receives `{ p, q, body }`, all fully typed.
+- [Body validation](/docs/routes#body-validation). With any [Standard Schema](https://standardschema.dev/) compatible library.
+- [Authorization](/docs/authorization). Mandatory by design, opt-out for public routes.
+- [Composing routers](/docs/composing-routers). Isolate routers by auth strategy with `ZenRelay`.
+- [CORS](/docs/cors). Built-in, not bolted on.
+- [Error handling](/docs/error-handling). `abort(404)` from any handler, customizable error shapes.
+- [Response helpers](/docs/response-helpers). `json()`, `html()`, `textStream()`, and more.
+- [OpenTelemetry](/docs/opentelemetry). Automatic span attributes for matched routes.
+- [Runs anywhere](/docs/supported-runtimes). Cloudflare Workers, Bun, Node.js, and any runtime with Web `Request`/`Response`.
## Quick example
@@ -283,15 +279,3 @@ zen.route(
export default zen;
```
-
-## Features
-
-- [Type-safe everywhere](/docs/routes). Your handler receives `{ p, q, body }`, all fully typed.
-- [Body validation](/docs/routes#body-validation). With any [Standard Schema](https://standardschema.dev/) compatible library.
-- [Authorization](/docs/authorization). Mandatory by design, opt-out for public routes.
-- [Composing routers](/docs/composing-routers). Isolate routers by auth strategy with `ZenRelay`.
-- [CORS](/docs/cors). Built-in, not bolted on.
-- [Error handling](/docs/error-handling). `abort(404)` from any handler, customizable error shapes.
-- [Response helpers](/docs/response-helpers). `json()`, `html()`, `textStream()`, and more.
-- [OpenTelemetry](/docs/opentelemetry). Automatic span attributes for matched routes.
-- [Runs anywhere](/docs/supported-runtimes). Cloudflare Workers, Bun, Node.js, and any runtime with Web `Request`/`Response`.
diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json
index 9bbe09b..83b2d29 100644
--- a/apps/docs/content/docs/meta.json
+++ b/apps/docs/content/docs/meta.json
@@ -1,6 +1,7 @@
{
"pages": [
"index",
+ "get-started",
"---Concepts---",
"routes",
"authorization",
diff --git a/apps/docs/lib/layout.shared.tsx b/apps/docs/lib/layout.shared.tsx
index 9bfcaba..468aa8a 100644
--- a/apps/docs/lib/layout.shared.tsx
+++ b/apps/docs/lib/layout.shared.tsx
@@ -21,7 +21,7 @@ export function baseOptions(): BaseLayoutProps {
>
- Zen Router
+ Zen Router
>
),
},
diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs
index 39f7525..77810b7 100644
--- a/apps/docs/next.config.mjs
+++ b/apps/docs/next.config.mjs
@@ -2,10 +2,6 @@ import { createMDX } from "fumadocs-mdx/next";
import { fileURLToPath } from "url";
import { dirname } from "path";
-if (!process.env.BASE_URL) {
- throw new Error("BASE_URL environment variable is not set");
-}
-
const withMDX = createMDX();
const __dirname = dirname(fileURLToPath(import.meta.url));