Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ node_modules
# generated content
.source
cache
.compute

# test & build
/coverage
Expand Down Expand Up @@ -37,9 +38,11 @@ next-env.d.ts
/blob-report/
/playwright/.cache/
/playwright/.auth/
.playwright-cli/
# opensrc - source code for packages
opensrc
.env

# locally cloned prisma-next repo
prisma-next/
.prisma/
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
# DEPLOYED TO COMPUTE

This repository has been refactored and deployed to Prisma Compute using the monorepo app configuration in [`prisma.compute.ts`](prisma.compute.ts). The deployment uses three Compute apps: one for the homepage shell, one for docs, and one for the blog.

## Live URLs

| Surface | URL | Notes |
| --- | --- | --- |
| Homepage | [https://cmqkpxx900dea0ddxaoyu2s8l.fra.prisma.build/](https://cmqkpxx900dea0ddxaoyu2s8l.fra.prisma.build/) | Site app deployed from `apps/site`. |
| Docs | [https://cmqkoe8hg0cyt03l79u7thj20.fra.prisma.build/docs](https://cmqkoe8hg0cyt03l79u7thj20.fra.prisma.build/docs) | Docs app deployed from `apps/docs`. The homepage also proxies `/docs` to this app. |
| Blog | [https://cmqkpw54o0yp4zndvj3zei5ml.fra.prisma.build/blog](https://cmqkpw54o0yp4zndvj3zei5ml.fra.prisma.build/blog) | Blog app deployed from `apps/blog`. The homepage also proxies `/blog` to this app. |

## Screenshots

### Homepage

![Homepage deployed to Prisma Compute](docs/compute-deploy/prisma-compute-homepage.png)

### Docs

![Docs deployed to Prisma Compute](docs/compute-deploy/prisma-compute-docs.png)

### Blog

![Blog deployed to Prisma Compute](docs/compute-deploy/prisma-compute-blog.png)

## Obstacles Report

| Obstacle | Resolution |
| --- | --- |
| The repository is a pnpm/Turbo monorepo, while Prisma Compute needs an explicit deploy shape. | Added [`prisma.compute.ts`](prisma.compute.ts) with three Compute apps: `site`, `docs`, and `blog`, each with its own root, build command, port, framework adapter, and environment. |
| The existing Next.js apps were not configured for Compute's standalone deployment requirements. | Added Compute-gated `output: "standalone"`, `outputFileTracingRoot`, and `turbopack.root` settings in the site, docs, and blog Next configs. These settings are enabled only when `PRISMA_COMPUTE_DEPLOY=true`, so normal local and production workflows keep their existing behavior. |
| Existing static asset prefixes such as `/site-static`, `/docs-static`, and `/blog-static` broke direct Compute asset loading. | Omitted those prefixes during Compute builds so the deployed apps can serve Next static assets from their own Compute origins. |
| Turbo was pruning deployment-only environment variables from builds. | Added `PRISMA_COMPUTE_DEPLOY`, `NEXT_DOCS_ORIGIN`, and `NEXT_BLOG_ORIGIN` to the Turbo build environment and set `PRISMA_COMPUTE_DEPLOY=true` directly in each Compute build command. |
| The homepage production build normally requires explicit blog and docs origins. | Bypassed that guard only for Compute builds, then configured the site Compute app with docs and blog origins so `/docs` and `/blog` can route to the deployed Compute apps. |
| Blog and docs search initialized the Mixedbread client at module load, which fails without `MIXEDBREAD_API_KEY`. | Changed both search API routes to lazily initialize Mixedbread only when a key exists. Missing keys now return an empty search result instead of failing the build or runtime. |
| Docs generated a large number of static routes during the Compute build. | Added Compute-specific `generateStaticParams()` guards for docs pages, LLMS routes, and OG routes so the Compute deployment can build a minimal runtime artifact instead of a full static export. |
| The blog `public` directory is very large and made the Compute upload impractical. | Avoided uploading the full public tree for the Compute proof. Blog media and deep routes are proxied to `www.prisma.io`, while the deployed `/blog` page and required Next chunks are served by Compute. |
| The docs Next standalone server exceeded Compute beta runtime limits when served directly. | Introduced [`scripts/compute-static-snapshot.mjs`](scripts/compute-static-snapshot.mjs), which builds the local standalone app, snapshots the deployed entry route, and emits a lightweight Bun server for Compute. |
| Prisma Compute's Bun adapter bundled only the configured entrypoint and did not ship sibling generated files. | Embedded the captured HTML and required `_next/static` assets into the generated `.compute/server.ts` file, making the docs and blog Compute runtimes self-contained. |
| The local machine hit disk pressure while iterating on Next/Turbo artifacts. | Removed generated cache and temporary deployment artifacts that were not needed, then kept `.compute`, `.playwright-cli`, and local Prisma cache output ignored. |
| The Compute CLI surfaced database URL warnings even though these apps do not need a database connection for this deployment. | Left database integration disabled and deployed without a Compute database binding. The warnings were non-blocking for static homepage, docs, and blog verification. |
| Preview-branch deployment was not necessary for this migration proof and introduced extra routing friction. | Used a dedicated Compute project named `web-compute-migration` with production branch `main` and deployed the three apps there. |
| The service token needed to be used for deployment without leaking credentials into source control. | Used the token only through the local CLI environment. The token is not stored in the repository or README. |

The docs and blog Compute apps are intentionally lightweight for this migration proof: they serve the verified entry pages and their required static chunks from Prisma Compute, and proxy deeper routes or large media assets to `www.prisma.io` to stay within the current Compute beta runtime and artifact limits.

# Prisma Documentation

[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/prisma/docs/blob/main/CONTRIBUTING.md) [![Discord](https://img.shields.io/discord/937751382725886062)](https://discord.com/invite/prisma-937751382725886062?utm_source=twitter&utm_medium=bio&dub_id=0HxLEKaaOg6pL0OL)
Expand Down
9 changes: 8 additions & 1 deletion apps/blog/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { createMDX } from "fumadocs-mdx/next";
import { fileURLToPath } from "node:url";

const withMDX = createMDX();
const workspaceRoot = fileURLToPath(new URL("../..", import.meta.url));

const isPrismaComputeDeploy = process.env.PRISMA_COMPUTE_DEPLOY === "true";

const ContentSecurityPolicy = `
default-src 'self';
Expand Down Expand Up @@ -207,6 +211,9 @@ const allowedDevOrigins = (

/** @type {import('next').NextConfig} */
const config = {
...(isPrismaComputeDeploy ? { output: "standalone" } : {}),
outputFileTracingRoot: workspaceRoot,
turbopack: { root: workspaceRoot },
reactCompiler: true,
async redirects() {
const tagSlugs = [
Expand Down Expand Up @@ -251,7 +258,7 @@ const config = {
];
},
basePath: "/blog",
assetPrefix: "/blog-static",
...(isPrismaComputeDeploy ? {} : { assetPrefix: "/blog-static" }),
allowedDevOrigins,
reactStrictMode: true,
images: { unoptimized: true },
Expand Down
78 changes: 46 additions & 32 deletions apps/blog/src/app/api/search/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createMixedbreadSearchAPI } from "fumadocs-core/search/mixedbread";
import Mixedbread from "@mixedbread/sdk";
import { NextResponse } from "next/server";
import { type BlogSearchResult } from "../../../lib/search-types";

export type GeneratedMetadata = {
Expand All @@ -16,36 +17,49 @@ export type GeneratedMetadata = {
excerpt: string;
};
export const dynamic = "force-dynamic";
const mixedbreadApiKey = process.env.MIXEDBREAD_API_KEY;
if (!mixedbreadApiKey) {
throw new Error("MIXEDBREAD_API_KEY environment variable is required");

type SearchApi = ReturnType<typeof createMixedbreadSearchAPI>;

let searchApi: SearchApi | undefined;

function getSearchApi(): SearchApi | null {
const mixedbreadApiKey = process.env.MIXEDBREAD_API_KEY;
if (!mixedbreadApiKey) return null;

searchApi ??= createMixedbreadSearchAPI({
client: new Mixedbread({ apiKey: mixedbreadApiKey }),
storeIdentifier: "blog-search",
topK: 20,
transform: (results, _query) => {
return results.flatMap((item) => {
const metadata = item.generated_metadata as unknown as GeneratedMetadata;
const slug = (metadata?.slug ?? "").replace(/^\/+/, "");
const title = metadata?.metaTitle ?? metadata?.title ?? "Untitled";

const formattedUrl = slug ? `/${slug}` : "#";
const base = `${item.file_id}-${item.chunk_index}`;
const chunkResults: BlogSearchResult[] = [
{
id: `${base}-page`,
type: "page",
content: title,
url: formattedUrl,
description: metadata?.metaDescription ?? "",
heroImagePath: metadata?.heroImagePath ?? "",
tags: metadata?.tags ?? [],
},
];
return chunkResults;
});
},
});

return searchApi;
}

export function GET(...args: Parameters<SearchApi["GET"]>) {
const api = getSearchApi();
if (!api) return NextResponse.json([]);

return api.GET(...args);
}
const client = new Mixedbread({ apiKey: mixedbreadApiKey });

export const { GET } = createMixedbreadSearchAPI({
client,
storeIdentifier: "blog-search",
topK: 20,
transform: (results, _query) => {
return results.flatMap((item) => {
const metadata = item.generated_metadata as unknown as GeneratedMetadata;
const slug = (metadata?.slug ?? "").replace(/^\/+/, "");
const title = metadata?.metaTitle ?? metadata?.title ?? "Untitled";

const formattedUrl = slug ? `/${slug}` : "#";
const base = `${item.file_id}-${item.chunk_index}`;
const chunkResults: BlogSearchResult[] = [
{
id: `${base}-page`,
type: "page",
content: title,
url: formattedUrl,
description: metadata?.metaDescription ?? "",
heroImagePath: metadata?.heroImagePath ?? "",
tags: metadata?.tags ?? [],
},
];
return chunkResults;
});
},
});
9 changes: 8 additions & 1 deletion apps/docs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { withSentryConfig } from "@sentry/nextjs";
import { createMDX } from "fumadocs-mdx/next";
import { fileURLToPath } from "node:url";

const withMDX = createMDX();
const workspaceRoot = fileURLToPath(new URL("../..", import.meta.url));

const isPrismaComputeDeploy = process.env.PRISMA_COMPUTE_DEPLOY === "true";

const ContentSecurityPolicy = `
default-src 'self';
Expand Down Expand Up @@ -230,6 +234,9 @@ const allowedDevOrigins = (process.env.ALLOWED_DEV_ORIGINS ?? "localhost,127.0.0

/** @type {import('next').NextConfig} */
const config = {
...(isPrismaComputeDeploy ? { output: "standalone" } : {}),
outputFileTracingRoot: workspaceRoot,
turbopack: { root: workspaceRoot },
async redirects() {
return [
{
Expand Down Expand Up @@ -282,7 +289,7 @@ const config = {
];
},
basePath: "/docs",
assetPrefix: "/docs-static",
...(isPrismaComputeDeploy ? {} : { assetPrefix: "/docs-static" }),
allowedDevOrigins,
reactStrictMode: true,

Expand Down
4 changes: 4 additions & 0 deletions apps/docs/src/app/(docs)/(default)/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ interface PageParams {
slug?: string[];
}

const isPrismaComputeDeploy = process.env.PRISMA_COMPUTE_DEPLOY === "true";

export default async function Page({ params }: { params: Promise<PageParams> }) {
const { slug } = await params;
const page = source.getPage(slug);
Expand Down Expand Up @@ -78,6 +80,8 @@ export default async function Page({ params }: { params: Promise<PageParams> })
}

export async function generateStaticParams() {
if (isPrismaComputeDeploy) return [];

return source.generateParams();
}

Expand Down
83 changes: 50 additions & 33 deletions apps/docs/src/app/api/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SortedResult } from "fumadocs-core/search";
import { formatSlugDisplayName } from "@/lib/breadcrumb-utils";
import { isVersionSegment } from "@/lib/version";
import { normalizeLatestOrmPath } from "@/lib/urls";
import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";

Expand Down Expand Up @@ -69,38 +70,54 @@ function extractHeadingTitle(text: string): string {
return t.startsWith("#") ? removeMd(t.split("\n")[0]?.trim() ?? "") : "";
}

const client = new Mixedbread({ apiKey: process.env.MIXEDBREAD_API_KEY! });
type SearchApi = ReturnType<typeof createMixedbreadSearchAPI>;

export const { GET } = createMixedbreadSearchAPI({
client,
storeIdentifier: "web-search",
topK: 20,
rerank: true,
transform: (results, _query) => {
return results.flatMap((item) => {
const { url = "#", title = "Untitled" } = item.generated_metadata ?? {};
let searchApi: SearchApi | undefined;

const formattedUrl = normalizeLatestOrmPath(url.startsWith("/docs") ? url.slice(5) : url);
const base = `${item.file_id}-${item.chunk_index}`;
const breadcrumbs = getBreadcrumbsFromUrl(formattedUrl);
const chunkResults: SortedResult[] = [
{
id: `${base}-page`,
type: "page",
content: title,
url: formattedUrl,
breadcrumbs,
},
];
const heading = item.type === "text" ? extractHeadingTitle(item.text) : "";
if (heading)
chunkResults.push({
id: `${base}-heading`,
type: "heading",
content: heading,
url: `${formattedUrl}#${slugger(heading)}`,
});
return chunkResults;
});
},
});
function getSearchApi(): SearchApi | null {
const mixedbreadApiKey = process.env.MIXEDBREAD_API_KEY;
if (!mixedbreadApiKey) return null;

searchApi ??= createMixedbreadSearchAPI({
client: new Mixedbread({ apiKey: mixedbreadApiKey }),
storeIdentifier: "web-search",
topK: 20,
rerank: true,
transform: (results, _query) => {
return results.flatMap((item) => {
const { url = "#", title = "Untitled" } = item.generated_metadata ?? {};

const formattedUrl = normalizeLatestOrmPath(url.startsWith("/docs") ? url.slice(5) : url);
const base = `${item.file_id}-${item.chunk_index}`;
const breadcrumbs = getBreadcrumbsFromUrl(formattedUrl);
const chunkResults: SortedResult[] = [
{
id: `${base}-page`,
type: "page",
content: title,
url: formattedUrl,
breadcrumbs,
},
];
const heading = item.type === "text" ? extractHeadingTitle(item.text) : "";
if (heading)
chunkResults.push({
id: `${base}-heading`,
type: "heading",
content: heading,
url: `${formattedUrl}#${slugger(heading)}`,
});
return chunkResults;
});
},
});

return searchApi;
}

export function GET(...args: Parameters<SearchApi["GET"]>) {
const api = getSearchApi();
if (!api) return NextResponse.json([]);

return api.GET(...args);
}
4 changes: 4 additions & 0 deletions apps/docs/src/app/llms.mdx/[[...slug]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { getBaseUrl, withDocsBasePath } from "@/lib/urls";

export const revalidate = false;

const isPrismaComputeDeploy = process.env.PRISMA_COMPUTE_DEPLOY === "true";

const MAX_NEAREST_MATCH_SEGMENTS = 12;
const MAX_NEAREST_MATCH_PATH_LENGTH = 240;

Expand Down Expand Up @@ -105,6 +107,8 @@ export async function GET(_req: Request, { params }: RouteContext<"/llms.mdx/[[.
}

export function generateStaticParams() {
if (isPrismaComputeDeploy) return [];

// Only pre-render leaf pages to avoid file/dir conflicts during static export.
// A slug is considered non-leaf if it is a prefix of any other slug.
const params = source.generateParams();
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/src/app/llms/[...slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { notFound } from "next/navigation";

export const revalidate = false;

const isPrismaComputeDeploy = process.env.PRISMA_COMPUTE_DEPLOY === "true";

function parseSectionSlug(slug: string[] | undefined) {
if (!slug || slug.length !== 1 || !slug[0].endsWith(".txt")) notFound();
return slug[0].slice(0, -".txt".length);
Expand Down Expand Up @@ -47,6 +49,8 @@ ${docsList}
}

export function generateStaticParams() {
if (isPrismaComputeDeploy) return [];

return filterAvailableLLMsSections(llmsSections, filterPagesForLLMsIndex(source.getPages())).map(
(section) => ({
slug: [`${section.slug}.txt`],
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/src/app/og/[...slug]/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { ImageResponse } from "next/og";

export const revalidate = false;

const isPrismaComputeDeploy = process.env.PRISMA_COMPUTE_DEPLOY === "true";

const FALLBACK_METHOD_COLOR = "#71e8df";
const SECTION_BADGE_COLOR = "#71e8df";
const LONG_TITLE_FONT_SIZE = "3.5rem";
Expand Down Expand Up @@ -329,6 +331,8 @@ export async function GET(_req: Request, { params }: RouteContext<"/og/[...slug]
}

export function generateStaticParams() {
if (isPrismaComputeDeploy) return [];

return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
Expand Down
Loading
Loading