Skip to content

Commit e8bdf11

Browse files
committed
chore: add architecture overview
1 parent 6ddbbee commit e8bdf11

2 files changed

Lines changed: 334 additions & 0 deletions

File tree

ARCHITECTURE.md

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
# Multi-Zone Architecture Overview
2+
3+
This repository is a pnpm monorepo with three closely related Next.js apps:
4+
5+
- `apps/site`: the main Prisma website and the public multi-zone entrypoint
6+
- `apps/blog`: the blog zone
7+
- `apps/docs`: the docs zone
8+
9+
At a high level, users experience docs and blog content as `www.prisma.io/docs/*` and `www.prisma.io/blog/*`, but those paths are served by separate Next.js apps behind the scenes.
10+
11+
## Two Layers
12+
13+
There are two architectural layers to keep in mind:
14+
15+
1. The monorepo layer decides how code is organized, shared, built, and run.
16+
2. The runtime layer decides which app answers a given request.
17+
18+
The monorepo layer is defined by `pnpm-workspace.yaml`, the root `package.json`, and `turbo.json`.
19+
20+
- `pnpm-workspace.yaml` includes `apps/*` and `packages/*`, so all apps and shared packages live in one workspace.
21+
- The root `package.json` delegates common workflows to Turbo with `pnpm build`, `pnpm dev`, `pnpm check`, and `pnpm types:check`.
22+
- `turbo.json` defines the task graph, shared cache inputs, and build outputs for `.next`, `dist`, and cache folders.
23+
24+
This matches the useful Turborepo framing for this repo: package boundaries are managed at the workspace level, while runtime composition happens inside the app configs rather than through root scripts.
25+
26+
## Shared Packages
27+
28+
All three apps rely on shared workspace packages:
29+
30+
- `packages/ui` (`@prisma-docs/ui`): shared navigation, footer, theming, and UI helpers
31+
- `packages/eclipse` (`@prisma/eclipse`): shared design system components and styles
32+
33+
That means the apps are separate deployable zones, but they still present a mostly unified UI by consuming the same package-level building blocks.
34+
35+
## App Roles
36+
37+
| App | Local port | Runtime role | Key routing config |
38+
|------|------|------|------|
39+
| `apps/site` | `3000` | Main website host and multi-zone entrypoint | `assetPrefix: "/site-static"` and cross-zone `rewrites()` |
40+
| `apps/docs` | `3001` | Standalone docs zone | `basePath: "/docs"` and `assetPrefix: "/docs-static"` |
41+
| `apps/blog` | `3002` | Standalone blog zone | `basePath: "/blog"` and `assetPrefix: "/blog-static"` |
42+
43+
## `apps/site`
44+
45+
`apps/site` is the root host zone. Its own `README.md` describes it as the "Primary host app for the multi-zone Next.js setup."
46+
47+
Important characteristics:
48+
49+
- It runs on port `3000`.
50+
- It does not use a `basePath`.
51+
- It serves its own static assets from `/site-static`.
52+
- It owns the cross-zone rewrites that make docs and blog look like subpaths of the main site.
53+
- It also contains a large `redirects()` table for legacy Prisma routes and hostnames.
54+
55+
The most important part of the multi-zone setup lives in `apps/site/next.config.mjs`:
56+
57+
```758:790:apps/site/next.config.mjs
58+
// Proxy canonical docs path to docs infrastructure
59+
{
60+
source: "/docs",
61+
destination: `${DOCS_ORIGIN}/docs`,
62+
missing: [{ type: "host", value: DOCS_ORIGIN_HOST }],
63+
},
64+
{
65+
source: "/docs/:any*",
66+
destination: `${DOCS_ORIGIN}/docs/:any*`,
67+
missing: [{ type: "host", value: DOCS_ORIGIN_HOST }],
68+
},
69+
{
70+
source: "/docs-static/:path*",
71+
destination: `${DOCS_ORIGIN}/docs-static/:path*`,
72+
missing: [{ type: "host", value: DOCS_ORIGIN_HOST }],
73+
},
74+
75+
// Proxy canonical blog path to blog infrastructure
76+
{
77+
source: "/blog",
78+
destination: `${BLOG_ORIGIN}/blog`,
79+
missing: [{ type: "host", value: BLOG_ORIGIN_HOST }],
80+
},
81+
```
82+
83+
Two details matter here:
84+
85+
- `NEXT_DOCS_ORIGIN` and `NEXT_BLOG_ORIGIN` tell the site app where the docs and blog deployments live.
86+
- The `missing` host checks prevent the site from rewriting a request if it is already running on the docs or blog host, which avoids rewrite loops.
87+
88+
In production, `apps/site` requires both origin variables. In development, it falls back to `https://docs.prisma.io` and `https://blog.prisma.io` if they are unset.
89+
90+
## `apps/docs`
91+
92+
`apps/docs` is a standalone Next.js app that still expects to live under `/docs`.
93+
94+
Important characteristics:
95+
96+
- It runs on port `3001`.
97+
- It sets `basePath: "/docs"`.
98+
- It sets `assetPrefix: "/docs-static"`.
99+
- Its root path redirects to `/docs`.
100+
- It wraps the Next config in `withSentryConfig(...)`.
101+
- Its build runs `fetch-openapi` before `next build`.
102+
103+
Content and structure are Fumadocs-driven:
104+
105+
- `apps/docs/source.config.ts` defines the main docs collection in `content/docs`.
106+
- It also defines a versioned `content/docs.v6` collection.
107+
- Docs frontmatter includes metadata fields like `url`, `metaTitle`, and `metaDescription`, which feed the docs app's routing and SEO layer.
108+
109+
The docs deployment also has its own `apps/docs/vercel.json`, but that file is primarily a large redirect map for old docs URLs. It is not what makes the multi-zone architecture work. The multi-zone behavior comes from `apps/site` rewriting to the docs origin.
110+
111+
## `apps/blog`
112+
113+
`apps/blog` is also a standalone Next.js app, but mounted under `/blog`.
114+
115+
Important characteristics:
116+
117+
- It runs on port `3002`.
118+
- It sets `basePath: "/blog"`.
119+
- It sets `assetPrefix: "/blog-static"`.
120+
- Its root path redirects to `/blog`.
121+
- It has tag redirects that rewrite older tag URLs into query-param based routes.
122+
123+
Blog content is also Fumadocs-based:
124+
125+
- `apps/blog/source.config.ts` defines a collection at `content/blog`.
126+
- Blog frontmatter includes authors, date, hero image paths, tags, excerpts, and metadata fields.
127+
128+
Like docs, blog is a separate app and separate deployment target. The difference is that end users usually reach it through the main site's `/blog/*` path model.
129+
130+
## Request Flow
131+
132+
This is the request path users usually experience in production:
133+
134+
```mermaid
135+
flowchart LR
136+
user[UserBrowser]
137+
site[appsSite]
138+
blog[appsBlog]
139+
docs[appsDocs]
140+
ui[packagesUi]
141+
eclipse[packagesEclipse]
142+
143+
user -->|"www.prisma.io/*"| site
144+
site -->|"rewrite /blog/*"| blog
145+
site -->|"rewrite /docs/*"| docs
146+
site --> ui
147+
site --> eclipse
148+
blog --> ui
149+
blog --> eclipse
150+
docs --> ui
151+
docs --> eclipse
152+
```
153+
154+
In other words:
155+
156+
1. A browser hits `www.prisma.io`.
157+
2. `apps/site` handles the request first.
158+
3. If the path starts with `/docs` or `/blog`, `apps/site` proxies that request to the corresponding origin.
159+
4. The target app still serves content with its own `basePath`, asset prefix, metadata rules, and local navigation.
160+
161+
This follows a Next.js multi-zone pattern: each zone owns its own app boundary, while the top-level host composes them into one public URL space.
162+
163+
## Why `basePath` and `assetPrefix` Matter
164+
165+
The Next.js-side separation is deliberate:
166+
167+
- `apps/docs` owns the `/docs` path space and `/docs-static` assets.
168+
- `apps/blog` owns the `/blog` path space and `/blog-static` assets.
169+
- `apps/site` stays at the root path space and uses `/site-static`.
170+
171+
That separation prevents static asset collisions and keeps each app self-contained enough to run on its own origin or locally on its own port.
172+
173+
This is also the main Next.js best-practice insight that explains the repo: each zone has a clear app boundary, and shared behavior is coordinated through config rather than by blurring the app edges.
174+
175+
## Cross-Zone Linking Nuances
176+
177+
The linking model is intentionally mixed, and that is one of the easiest parts of the system to misunderstand.
178+
179+
### Main site navigation
180+
181+
In `apps/site/src/app/layout.tsx`, the top-level `Docs` and `Blog` nav items point to absolute `https://www.prisma.io/docs` and `https://www.prisma.io/blog` URLs, while many other site links are root-relative like `/pricing` or `/orm`.
182+
183+
That biases navigation toward the canonical public host, even though docs and blog are separate deployments behind the scenes.
184+
185+
### Blog navigation
186+
187+
In `apps/blog/src/app/(blog)/layout.tsx`, the nav mixes absolute and relative links:
188+
189+
- `Docs` is linked as `/docs`
190+
- `Blog` is linked as `https://www.prisma.io/blog`
191+
- Most product pages point to absolute `https://www.prisma.io/...` URLs
192+
193+
This works well when the user is on `www.prisma.io`, because `/docs` is handled by the site rewrite layer. It is less universal when the blog app is visited directly on its own origin, because `/docs` then depends on how that origin is configured at the edge.
194+
195+
### Docs navigation
196+
197+
In `apps/docs/src/lib/layout.shared.tsx`, docs mostly links internally within the docs zone, but the Prisma logo links back to `https://www.prisma.io`.
198+
199+
That makes docs feel like part of the main site, even though it is operationally a separate app.
200+
201+
### Shared footer behavior
202+
203+
The shared footer data in `packages/ui/src/data/footer.ts` uses relative `/docs` and `/blog` paths.
204+
205+
`packages/ui/src/lib/is-absolute-url.ts` exposes `getRedirectableLink()`, which can convert relative links to `https://www.prisma.io/...` when `absoluteLinks` is enabled. By default, the footer can stay relative, which is fine on the main host but can behave differently off the canonical `www` origin.
206+
207+
## URL Generation and Metadata Nuances
208+
209+
Each app has its own base-URL helper:
210+
211+
- `apps/site/src/lib/url.ts`
212+
- `apps/docs/src/lib/urls.ts`
213+
- `apps/blog/src/lib/url.ts`
214+
215+
Those helpers are used for things like `metadataBase`, canonical URLs, OpenGraph data, and local defaults.
216+
217+
The behavior differs slightly by app:
218+
219+
- `apps/site` normalizes `NEXT_PUBLIC_PRISMA_URL`, falls back to `https://www.prisma.io` in production, and otherwise uses `VERCEL_URL` or `http://localhost:3000`.
220+
- `apps/docs` and `apps/blog` fall back to their own local ports (`3001` and `3002`) if no environment variable is present.
221+
222+
That means each zone can reason about its own canonical base URL, but it also means environment consistency matters if you want metadata to align cleanly across deployments.
223+
224+
### When to use `withDocsBasePath()` and `withBlogBasePath()`
225+
226+
The `withDocsBasePath()` and `withBlogBasePath()` helpers are mainly for URL strings that will not be processed by Next.js routing for you.
227+
228+
Use them when you are building a path manually for:
229+
230+
- metadata fields such as `alternates.canonical`, `openGraph.url`, and `openGraph.images`
231+
- sitemap, robots, RSS, JSON-LD, and other SEO-oriented URL generation
232+
- API endpoint strings like `/api/search`
233+
- raw HTML elements such as `<a href="/...">` or `<img src="/...">`
234+
- any other plain string URL that must include `/docs` or `/blog` explicitly
235+
236+
Do not use them when you are already using framework-aware components that respect `basePath`, especially:
237+
238+
- `next/link`
239+
- `next/image`
240+
241+
In those cases, manually prefixing the path usually duplicates work and can create incorrect URLs.
242+
243+
### Fumadocs and MDX behavior
244+
245+
Fumadocs-backed pages generally already live inside a base-path-aware environment, so helpers are usually not needed for normal framework-managed navigation.
246+
247+
In this repo, that means:
248+
249+
- blog MDX exposes `Link` and `Image` from Next.js in `apps/blog/src/mdx-components.tsx`
250+
- docs MDX uses Fumadocs defaults from `fumadocs-ui/mdx` in `apps/docs/src/mdx-components.tsx`
251+
252+
The important exception is raw `img` handling. Both apps override MDX `img` rendering and explicitly prefix raw image sources before passing them to `ImageZoom`, because plain image source strings are not automatically rewritten the way `next/image` and `next/link` inputs are:
253+
254+
- `apps/docs/src/mdx-components.tsx` uses `withDocsBasePathForImageSrc(...)`
255+
- `apps/blog/src/mdx-components.tsx` uses `withBlogBasePathForImageSrc(...)`
256+
257+
So the practical rule is:
258+
259+
- if you are using Next or Fumadocs navigation/image components, you usually do not need the helper
260+
- if you are emitting a raw path string or raw HTML tag, you usually do need the helper
261+
262+
### OG image and metadata patterns
263+
264+
Open Graph and other metadata fields are a common place where the helper is still required, even in Next apps.
265+
266+
That is because metadata APIs are string-based configuration, not rendered `next/link` or `next/image` components.
267+
268+
The repo already uses the helpers this way:
269+
270+
- docs metadata prefixes canonical URLs and OG image paths with `withDocsBasePath(...)` in `apps/docs/src/app/(docs)/(default)/[[...slug]]/page.tsx`
271+
- blog metadata prefixes canonical URLs with `withBlogBasePath(...)` and image paths with `withBlogBasePathForImageSrc(...)` in `apps/blog/src/app/(blog)/[slug]/page.tsx`
272+
- blog home metadata also prefixes its OG image path in `apps/blog/src/app/(blog)/page.tsx`
273+
274+
One extra nuance for blog is that some metadata image paths are converted all the way to absolute URLs for JSON-LD and article metadata by combining:
275+
276+
- `withBlogBasePathForImageSrc(...)` to add `/blog`
277+
- `toAbsoluteUrl(...)` to anchor the result to the app's base URL
278+
279+
That pattern is important whenever a consumer expects a fully qualified URL instead of a root-relative path.
280+
281+
## Search Nuance
282+
283+
The site-level search endpoint in `apps/site/src/app/api/search/route.ts` normalizes blog and docs results back to `https://www.prisma.io`, not to the zone origins.
284+
285+
That is important because it confirms the public contract of the system:
286+
287+
- docs search results are surfaced as `https://www.prisma.io/docs/...`
288+
- blog search results are surfaced as `https://www.prisma.io/blog/...`
289+
290+
So even when content is physically served by different apps, search treats the main site host as the canonical public URL namespace.
291+
292+
## Local Development Nuance
293+
294+
Root `pnpm dev` starts all relevant apps together:
295+
296+
- site on `http://localhost:3000`
297+
- docs on `http://localhost:3001`
298+
- blog on `http://localhost:3002`
299+
300+
The key caveat is that `apps/site` defaults `NEXT_DOCS_ORIGIN` and `NEXT_BLOG_ORIGIN` to the production subdomains if they are not explicitly set.
301+
302+
So a developer visiting `http://localhost:3000/docs` or `http://localhost:3000/blog` may accidentally proxy to production content unless those origin variables are pointed at local services.
303+
304+
That is the most important operational caveat in the whole setup.
305+
306+
## Deployment Notes
307+
308+
Deployment behavior is split across a few layers:
309+
310+
- Root `vercel.json` defines the install command for the monorepo.
311+
- `apps/site/vercel.json` adds site-level redirects.
312+
- `apps/docs/vercel.json` adds docs-specific redirects for legacy content.
313+
314+
But the architectural composition of the zones is still defined primarily in `apps/site/next.config.mjs`, not in `vercel.json`.
315+
316+
That distinction is important:
317+
318+
- `vercel.json` mostly handles deployment- and edge-level redirects.
319+
- `next.config.mjs` defines how the apps compose into one public multi-zone site.
320+
321+
## Mental Model For Maintainers
322+
323+
When working in this repository, the safest mental model is:
324+
325+
- treat `apps/site`, `apps/blog`, and `apps/docs` as separate Next.js applications
326+
- treat `packages/ui` and `packages/eclipse` as the shared presentation layer
327+
- treat `apps/site` as the public traffic router for docs and blog
328+
- treat `/docs` and `/blog` as canonical public paths, even when their content is served by separate origins
329+
330+
If a change affects navigation, canonical URLs, asset paths, or local dev routing, verify it in both contexts:
331+
332+
- when the user is on `www.prisma.io` through `apps/site`
333+
- when the zone is accessed directly on its own app origin

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This repository is a **pnpm monorepo** containing the Prisma documentation, blog
1515
| `packages/ui` | Shared UI components and utilities (`@prisma-docs/ui`) |
1616

1717
See each app’s `README.md` for more detail.
18+
See [ARCHITECTURE.md](ARCHITECTURE.md) for the cross-app multi-zone overview.
1819

1920
## Contributing
2021

0 commit comments

Comments
 (0)