diff --git a/.github/workflows/lychee.yml b/.github/workflows/lychee.yml index ca2971fe40..a830bd80af 100644 --- a/.github/workflows/lychee.yml +++ b/.github/workflows/lychee.yml @@ -37,6 +37,7 @@ jobs: with: # prisma.io/extensions ships with PR #8232; drop this exclude once it is live. # /compare/neon, /compare/planetscale, and /compare/supabase ship with PR #8516; drop that exclude once they are live. + # /compare/vercel-neon-drizzle is not live yet; drop that exclude once it is. # dashboard.clerk.com/~/ resolves to the signed-in user's application and returns 404 to anonymous requests. args: >- --cache @@ -58,7 +59,7 @@ jobs: --exclude 'https://pris.ly/vercel-app-demo-live' --exclude 'https://app-deploy-demo.prisma.io' --exclude '^https://www\.prisma\.io/extensions' - --exclude '^https://www\.prisma\.io/compare/(neon|planetscale|supabase)$' + --exclude '^https://www\.prisma\.io/compare/(neon|planetscale|supabase|vercel-neon-drizzle)$' --exclude '^https://dashboard\.clerk\.com/~/' --accept 200,201,204,301,302,303,307,308,403,429 ${{ steps.changed-docs.outputs.all_changed_files }} @@ -94,7 +95,7 @@ jobs: --exclude 'https://pris.ly/vercel-app-demo-live' --exclude 'https://app-deploy-demo.prisma.io' --exclude '^https://www\.prisma\.io/extensions' - --exclude '^https://www\.prisma\.io/compare/(neon|planetscale|supabase)$' + --exclude '^https://www\.prisma\.io/compare/(neon|planetscale|supabase|vercel-neon-drizzle)$' --exclude '^https://dashboard\.clerk\.com/~/' --accept 200,201,204,301,302,303,307,308,403,429 ${{ steps.changed-docs.outputs.all_changed_files }} diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index 342371e2ea..3a0117fd67 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -276,6 +276,7 @@ const agentMarkdownPaths = [ "/compare/neon", "/compare/planetscale", "/compare/supabase", + "/compare/vercel-neon-drizzle", ]; /** diff --git a/apps/site/src/app/compare/article.tsx b/apps/site/src/app/compare/article.tsx index 90b9359cd8..8f76feb06e 100644 --- a/apps/site/src/app/compare/article.tsx +++ b/apps/site/src/app/compare/article.tsx @@ -33,6 +33,7 @@ const AI_MARKDOWN: Partial> = { neon: "/compare/neon.md", planetscale: "/compare/planetscale.md", supabase: "/compare/supabase.md", + "vercel-neon-drizzle": "/compare/vercel-neon-drizzle.md", }; const TOC: Record = { @@ -112,6 +113,18 @@ const TOC: Record = { { id: "faq", label: "FAQ", also: ["next"] }, { id: "sources", label: "Sources" }, ], + "vercel-neon-drizzle": [ + { id: "choose", label: "When to choose" }, + { id: "feature-table", label: "Features" }, + { id: "where-each", label: "Where each fits" }, + { id: "apart", label: "Why Prisma" }, + { id: "pricing", label: "Pricing" }, + { id: "wrong-choice", label: "When it is the wrong choice" }, + { id: "migrate", label: "Switching", also: ["migrate-back"] }, + { id: "proof", label: "What teams say" }, + { id: "faq", label: "FAQ", also: ["next"] }, + { id: "sources", label: "Sources" }, + ], }; /** @@ -168,28 +181,47 @@ export function comparisonChooseTable({ } /** Buttons under the introduction. ORM pages lead with the ORM quickstart. */ -export function comparisonHeroCtas(kind: "orm" | "postgres" = "postgres") { +export function comparisonHeroCtas( + kind: "orm" | "postgres" = "postgres", + options?: { secondary?: boolean; primaryLabel?: string }, +) { const primary = kind === "orm" ? ORM_HERO_CTAS.primary : POSTGRES_HERO_CTAS.primary; const secondary = kind === "orm" ? ORM_HERO_CTAS.secondary : POSTGRES_HERO_CTAS.secondary; + const showSecondary = options?.secondary !== false; return (

- {primary.label} - {" "} - - {secondary.label} - + {options?.primaryLabel ?? primary.label} + + {showSecondary ? ( + <> + {" "} + + {secondary.label} + + + ) : null}

); } /** Closing action. ORM pages keep the 500K+ Prisma ORM figure and ORM-specific buttons. */ -export function comparisonClosingCta(kind: "orm" | "postgres" = "postgres") { +export function comparisonClosingCta( + kind: "orm" | "postgres" = "postgres", + options?: { instantDatabase?: boolean; secondary?: boolean; firstCheck?: string }, +) { const cta = kind === "orm" ? ORM_COMPARISON_CTA : COMPARISON_CTA; + const checks = options?.firstCheck + ? cta.checks.map((item, index) => + index === 0 ? { ...item, label: options.firstCheck as string } : item, + ) + : cta.checks; + const showInstantDatabase = options?.instantDatabase !== false; + const showSecondary = options?.secondary !== false; return (
) : null}
    - {cta.checks.map((item) => ( + {checks.map((item) => (
  • {item.label}
  • ))}
-

- Need a database?{" "} - - Create an instant database - - . -

+ {showInstantDatabase ? ( +

+ Need a database?{" "} + + Create an instant database + + . +

+ ) : null}

{cta.primaryCta.label} - {" "} - - {cta.secondaryCta.label} - + + {showSecondary ? ( + <> + {" "} + + {cta.secondaryCta.label} + + + ) : null}

); diff --git a/apps/site/src/app/compare/comparisons.ts b/apps/site/src/app/compare/comparisons.ts index 331b8d1bfb..7599f5d51c 100644 --- a/apps/site/src/app/compare/comparisons.ts +++ b/apps/site/src/app/compare/comparisons.ts @@ -4,6 +4,10 @@ import { MONGOOSE_DESCRIPTION, MONGOOSE_TITLE } from "./mongoose/_content"; import { NEON_DESCRIPTION, NEON_TITLE } from "./neon/_content"; import { PLANETSCALE_DESCRIPTION, PLANETSCALE_TITLE } from "./planetscale/_content"; import { SUPABASE_DESCRIPTION, SUPABASE_TITLE } from "./supabase/_content"; +import { + VERCEL_NEON_DRIZZLE_DESCRIPTION, + VERCEL_NEON_DRIZZLE_TITLE, +} from "./vercel-neon-drizzle/_content"; export const COMPARISON_INDEX = { path: "/compare", @@ -84,6 +88,13 @@ export const COMPARISONS = [ title: SUPABASE_TITLE, description: SUPABASE_DESCRIPTION, }, + { + slug: "vercel-neon-drizzle", + category: "platform", + competitor: "Vercel + Neon + Drizzle", + title: VERCEL_NEON_DRIZZLE_TITLE, + description: VERCEL_NEON_DRIZZLE_DESCRIPTION, + }, ] as const satisfies readonly ComparisonEntry[]; export type ComparisonSlug = (typeof COMPARISONS)[number]["slug"]; diff --git a/apps/site/src/app/compare/page-ai-actions.tsx b/apps/site/src/app/compare/page-ai-actions.tsx index 4246d05b2c..3fffcb7e19 100644 --- a/apps/site/src/app/compare/page-ai-actions.tsx +++ b/apps/site/src/app/compare/page-ai-actions.tsx @@ -53,6 +53,14 @@ const SUPABASE_PROMPT = `I am choosing between Prisma Postgres and Supabase for Ask me up to three short questions if I need Auth/Storage/Realtime out of the box, whether I want the TypeScript app hosted next to Postgres, or whether HIPAA/SOC 2/ISO 27001 matter.`; +const VERCEL_NEON_DRIZZLE_PROMPT = `I am choosing between the Prisma platform and Vercel + Neon + Drizzle for a TypeScript backend. Using only what the article states, plus my answers: + +1. Tell me which fits if I want Prisma ORM, Prisma Postgres, and Prisma Compute in one platform vs if I want to wire Drizzle, Neon, and Vercel myself. +2. Call out the real tradeoffs (one platform vs three providers, Prisma schema vs Drizzle SQL-first, preview branches vs Neon branches plus Vercel previews, operations vs CU-hours plus Vercel Pro) without selling. +3. Give me the first steps to try the one you recommend. + +Ask me up to three short questions if I already use Vercel, whether I prefer Drizzle's query-builder style, or whether I want the TypeScript app hosted next to Postgres.`; + const NEON_PROMPT = `I am choosing between Prisma Postgres and Neon for a TypeScript app. Using only what the article states, plus my answers: 1. Tell me which fits if I want Prisma ORM optional plus Prisma Compute in one project vs if I want Neon Functions, Auth, Object Storage, and AI Gateway with the UI elsewhere. @@ -63,17 +71,19 @@ Ask me up to three short questions if I need a copy of production data on previe /** Prompt every Open-with-AI logo prefills. The page's `.md` alternate is the source of truth. */ export function choosePrompt(pageMdUrl: string) { - const body = pageMdUrl.includes("/compare/planetscale.md") - ? PLANETSCALE_PROMPT - : pageMdUrl.includes("/compare/supabase.md") - ? SUPABASE_PROMPT - : pageMdUrl.includes("/compare/neon.md") - ? NEON_PROMPT - : pageMdUrl.includes("/compare/convex.md") - ? CONVEX_PROMPT - : pageMdUrl.includes("/compare/mongoose.md") - ? MONGOOSE_PROMPT - : KYSELY_PROMPT; + const body = pageMdUrl.includes("/compare/vercel-neon-drizzle.md") + ? VERCEL_NEON_DRIZZLE_PROMPT + : pageMdUrl.includes("/compare/planetscale.md") + ? PLANETSCALE_PROMPT + : pageMdUrl.includes("/compare/supabase.md") + ? SUPABASE_PROMPT + : pageMdUrl.includes("/compare/neon.md") + ? NEON_PROMPT + : pageMdUrl.includes("/compare/convex.md") + ? CONVEX_PROMPT + : pageMdUrl.includes("/compare/mongoose.md") + ? MONGOOSE_PROMPT + : KYSELY_PROMPT; return `Read this article and treat it as the source of truth: ${pageMdUrl} diff --git a/apps/site/src/app/compare/vercel-neon-drizzle.json/route.ts b/apps/site/src/app/compare/vercel-neon-drizzle.json/route.ts new file mode 100644 index 0000000000..2cbfb3eed5 --- /dev/null +++ b/apps/site/src/app/compare/vercel-neon-drizzle.json/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from "next/server"; +import { getVercelNeonDrizzleFacts } from "../vercel-neon-drizzle/facts"; + +/** Machine-readable comparison facts, each value with a source URL and dateChecked. */ +export function GET() { + return NextResponse.json(getVercelNeonDrizzleFacts(), { + headers: { + "Cache-Control": "public, max-age=300, s-maxage=3600", + }, + }); +} diff --git a/apps/site/src/app/compare/vercel-neon-drizzle/_content.ts b/apps/site/src/app/compare/vercel-neon-drizzle/_content.ts new file mode 100644 index 0000000000..4553082b5d --- /dev/null +++ b/apps/site/src/app/compare/vercel-neon-drizzle/_content.ts @@ -0,0 +1,252 @@ +export const VERCEL_NEON_DRIZZLE_PATH = "/compare/vercel-neon-drizzle"; + +export const VERCEL_NEON_DRIZZLE_TITLE = + "Prisma vs Vercel + Neon + Drizzle: two ways to build a TypeScript backend"; + +export const VERCEL_NEON_DRIZZLE_DESCRIPTION = + "Compare Prisma ORM, Prisma Postgres, and Prisma Compute with the Vercel, Neon, and Drizzle setup: one TypeScript platform, or a host, a database, and an ORM from three providers. Checked October 2026."; + +export const CHECKED = "October 2026"; + +/** The existing Prisma ORM vs Drizzle page. Do not invent /compare/drizzle. */ +export const DRIZZLE_ORM_COMPARE_PATH = "/docs/orm/v7/more/comparisons/prisma-and-drizzle"; + +/** The blog's author page and photo; `/blog` is rewritten to the blog app. */ +export const VERCEL_NEON_DRIZZLE_AUTHOR = { + name: "Martin Janse van Rensburg", + path: "/blog/author/martin-janse-van-rensburg", + image: "/blog/authors/martin-janse-van-rensburg.png", +} as const; + +export const VERCEL_NEON_DRIZZLE_PUBLISHED = { iso: "2026-10-19", label: "October 2026" } as const; +export const VERCEL_NEON_DRIZZLE_UPDATED = { iso: "2026-10-19", label: "October 2026" } as const; + +export const CHOOSE_PRISMA = [ + "You want fewer integrations to manage. The ORM, the database, and the hosting come from one platform.", + "You want each preview and staging environment to own its app services and its database.", + "You use coding agents throughout development. They work from one Prisma schema, one CLI, and one API.", + "You want one stack from early development onward. Start with [Prisma ORM](/docs/orm) and add [Prisma Postgres](/docs/postgres) and [Prisma Compute](/docs/compute) as your needs grow.", +]; + +export const CHOOSE_COMBO = [ + "You want to pick the tool you prefer for each layer and connect them yourself.", + "You already deploy on Vercel and want Neon Postgres installed from the Vercel Marketplace.", + "You prefer Drizzle's SQL-first, query-builder style.", + "You want preview databases that start as copy-on-write clones of production data.", +]; + +export type CompareRow = { + feature: string; + combo: string; + prisma: string; + sources: { combo: string; prisma: string }; +}; + +/** Columns run Feature, Vercel + Neon + Drizzle, Prisma. */ +export const COMPARE_ROWS: readonly CompareRow[] = [ + { + feature: "Core setup", + combo: "Choose and wire Drizzle, Neon, and Vercel across three providers.", + prisma: + "[Prisma ORM](/docs/orm), [Prisma Postgres](/docs/postgres), and [Prisma Compute](/docs/compute) in one platform.", + sources: { + combo: "https://orm.drizzle.team/docs/get-started/neon-new", + prisma: "https://www.prisma.io/docs/postgres", + }, + }, + { + feature: "Data model", + combo: "Drizzle schema in your app code, connected to a Neon database.", + prisma: "One Prisma schema that the ORM, migrations, and the CLI all read.", + sources: { + combo: "https://orm.drizzle.team/docs/sql-schema-declaration", + prisma: "https://www.prisma.io/docs/orm", + }, + }, + { + feature: "Environments", + combo: + "A Neon branch per Vercel preview deployment through the Neon integration for Vercel, cloned from production data.", + prisma: + "A Prisma [preview branch](/docs/compute/branching) owns its app services and its own database, which starts empty.", + sources: { + combo: "https://neon.com/docs/guides/vercel-native-integration", + prisma: "https://www.prisma.io/docs/compute/branching", + }, + }, + { + feature: "Agent workflow", + combo: "The agent moves across three vendors' tools and docs.", + prisma: "The agent works through the Prisma schema, CLI, and [REST API](/docs/rest-api).", + sources: { + combo: "https://orm.drizzle.team/docs/get-started/neon-new", + prisma: "https://www.prisma.io/docs/rest-api", + }, + }, + { + feature: "Billing", + combo: + "Neon bills compute in CU-hours plus storage. Vercel bills a Pro seat plus usage. Drizzle is free. Neon installed from the Vercel Marketplace is billed through Vercel.", + prisma: + "Prisma Postgres and Prisma Compute share one set of plans, billed by operations and storage, with spend limits on paid plans.", + sources: { + combo: "https://neon.com/pricing", + prisma: "https://www.prisma.io/pricing", + }, + }, +]; + +export const COMBO_FITS = [ + "You want to pick the tool you prefer for each layer and connect them yourself.", + "You already deploy on Vercel and want Neon Postgres installed from the Vercel Marketplace.", + "You prefer Drizzle's SQL-first, query-builder style.", + "You want preview databases that start as copy-on-write clones of production data.", +]; + +export const PRISMA_FITS = [ + "You want fewer integrations to manage. The ORM, the database, and the hosting come from one platform.", + "You use coding agents throughout development. They work from one Prisma schema, one CLI, and one API.", + "You want each preview and staging environment to own its app services and its database.", + "You want one stack from early development onward. Start with Prisma ORM and add Prisma Postgres and Prisma Compute as your needs grow.", +]; + +export const APART = [ + { + id: "integration", + title: "Integration across the stack", + body: "Prisma ORM, Prisma Postgres, and Prisma Compute are parts of one platform. Vercel, Neon, and Drizzle are three products from three vendors that you connect and keep in step yourself.", + }, + { + id: "agent-context", + title: "Developer and agent context", + body: "One Prisma schema, one CLI, and one [REST API](/docs/rest-api) cover the data model, the database, and deploys. With Vercel + Neon + Drizzle, that context is spread across three tools and three sets of docs.", + }, + { + id: "preview-environments", + title: "Preview environments", + body: "A Prisma [preview branch](/docs/compute/branching) owns both its app services and its database. With Vercel and Neon, a preview deployment and its database branch are linked through the Neon integration for Vercel, which you install and configure separately.", + }, + { + id: "spend", + title: "Spend managed in one place", + body: "Prisma Postgres and Prisma Compute share one set of plans, with spend limits on paid plans. Neon and Vercel each have their own pricing model and their own spend controls.", + }, +]; + +export const PRICING: readonly { + label: string; + combo: string; + prisma: string; + sources: { combo: string; prisma: string }; +}[] = [ + { + label: "ORM", + combo: "Drizzle is free and open source.", + prisma: "Prisma ORM is free and open source, and always will be.", + sources: { + combo: "https://orm.drizzle.team", + prisma: "https://www.prisma.io/docs/orm", + }, + }, + { + label: "Database", + combo: + "Neon Free is $0, with 100 CU-hours and 1 GB of storage per project. Launch compute is $0.106 per CU-hour. Scale is $0.222 per CU-hour. Storage on paid plans is $0.35 per GB-month.", + prisma: + "Prisma Postgres Free is $0. No credit card required. 200,000 operations a month, 2 GB of storage, and 50 databases. Paid plans bill operations and storage, with spend limits.", + sources: { + combo: "https://neon.com/pricing", + prisma: "https://www.prisma.io/pricing", + }, + }, + { + label: "Hosting", + combo: + "Vercel Hobby is free for personal, non-commercial use. Pro is $20 a month for one deploying seat plus usage, and includes $20 in monthly credit. Extra deploying seats are $20 a month each.", + prisma: + "Prisma Compute sits on the same Prisma plans as Prisma Postgres: Free $0, Starter $10, Pro $49, Business $129 a month.", + sources: { + combo: "https://vercel.com/docs/plans/pro-plan", + prisma: "https://www.prisma.io/pricing", + }, + }, + { + label: "How you pay", + combo: + "Neon and Vercel each bill on their own pricing model, or Neon bills through Vercel when installed from the Vercel Marketplace. Drizzle adds no bill.", + prisma: + "One set of plans for Prisma Postgres and Prisma Compute, with spend limits on paid plans.", + sources: { + combo: "https://neon.com/docs/guides/vercel-native-integration", + prisma: "https://www.prisma.io/pricing", + }, + }, +]; + +export const WRONG_CHOICE = [ + "You want to pick the tool you prefer for each layer and connect Drizzle, Neon, and Vercel yourself. Prisma puts the ORM, database, and hosting on one platform.", + "You want hosting and the database managed from the Vercel dashboard. Prisma Postgres works with an app on Vercel, but it is managed from the Prisma Console.", + "You want preview databases seeded with production data. A Neon branch is a copy-on-write clone of production; a Prisma preview database starts empty.", + "You prefer Drizzle's SQL-first, query-builder style. Prisma ORM uses a declarative Prisma schema. See [how Prisma ORM compares to Drizzle](/docs/orm/v7/more/comparisons/prisma-and-drizzle).", +]; + +export const FAQ: readonly { question: string; answer: string }[] = [ + { + question: "How does Prisma ORM compare to Drizzle?", + answer: + "Drizzle is a SQL-first TypeScript query builder. Prisma ORM uses a declarative Prisma schema and a generated client. This page compares the whole Vercel + Neon + Drizzle setup with the Prisma platform. The ORM-only comparison is [Prisma ORM vs Drizzle](/docs/orm/v7/more/comparisons/prisma-and-drizzle). Source: Prisma docs, October 2026.", + }, + { + question: "How do Vercel, Neon, and Prisma bill this stack?", + answer: + "Drizzle is free and open source. Neon Free is $0; Launch compute is $0.106 per CU-hour and Scale is $0.222 per CU-hour. Vercel Hobby is free for personal use; Pro is $20 a month for one deploying seat plus usage. Neon installed from the Vercel Marketplace is billed through Vercel. Prisma ORM is free and open source, and always will be. Prisma Postgres Free is $0 with no credit card, 200,000 operations, 2 GB of storage, and 50 databases. Paid Prisma plans are Starter $10, Pro $49, and Business $129 a month, with spend limits. Source: [Neon pricing](https://neon.com/pricing), [Vercel pricing](https://vercel.com/pricing), and [Prisma pricing](/pricing), October 2026.", + }, + { + question: "Do I need Prisma ORM to use Prisma Postgres?", + answer: + "No. Prisma Postgres is PostgreSQL. Any Postgres client works with it, so Prisma ORM is optional. This page is about using Prisma ORM, Prisma Postgres, and Prisma Compute together. Source: [Prisma Postgres](/docs/postgres), October 2026.", + }, + { + question: "Can I keep Vercel and use Prisma Postgres?", + answer: + "Yes. Prisma Postgres works with any Postgres client, including an app you host on Vercel. Prisma Compute is optional app hosting in the same Prisma project. Source: [Prisma Postgres](/docs/postgres) and [Prisma Compute](/docs/compute), October 2026.", + }, + { + question: "Do Prisma preview environments include the app and the database?", + answer: + "A Prisma preview branch can own both its Compute services and its Prisma Postgres database. Production data is not copied into that database. With Vercel and Neon, the Neon integration for Vercel can create a copy-on-write Neon branch for each preview deployment. Source: [Compute branching](/docs/compute/branching) and [Neon + Vercel](https://neon.com/docs/guides/vercel-native-integration), October 2026.", + }, +]; + +/** Vercel is a hosted service, so it carries no license or code repository. */ +export const TOOLS = { + drizzle: { + name: "Drizzle", + softwareVersion: "current", + license: "https://github.com/drizzle-team/drizzle-orm/blob/main/LICENSE", + codeRepository: "https://github.com/drizzle-team/drizzle-orm", + programmingLanguage: "TypeScript", + url: "https://orm.drizzle.team", + }, + neon: { + name: "Neon", + softwareVersion: "current", + license: "https://www.apache.org/licenses/LICENSE-2.0", + codeRepository: "https://github.com/neondatabase/neon", + programmingLanguage: "Rust", + url: "https://neon.com", + }, + vercel: { + name: "Vercel", + softwareVersion: "current", + url: "https://vercel.com", + }, + prisma: { + name: "Prisma", + softwareVersion: "current", + license: "https://www.apache.org/licenses/LICENSE-2.0", + codeRepository: "https://github.com/prisma/prisma", + programmingLanguage: "TypeScript", + url: "https://www.prisma.io", + }, +} as const; diff --git a/apps/site/src/app/compare/vercel-neon-drizzle/_sections.tsx b/apps/site/src/app/compare/vercel-neon-drizzle/_sections.tsx new file mode 100644 index 0000000000..084c072d5d --- /dev/null +++ b/apps/site/src/app/compare/vercel-neon-drizzle/_sections.tsx @@ -0,0 +1,473 @@ +import Link from "next/link"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { TESTIMONIALS } from "@/components/sections/testimonials-data"; +import { AnchorHeading } from "../anchor-heading"; +import { + CompareArticle, + comparisonChooseTable, + comparisonClosingCta, + comparisonDisclosure, + comparisonHeroCtas, +} from "../article"; +import { comparisonByline } from "../byline"; +import { comparisonProofCards } from "../proof-cards"; +import { articleReadingStats } from "../reading-time"; +import { CompareTable } from "../compare-table"; +import { ComparisonPricingTable } from "../pricing-table"; +import { QUIET_LINK } from "../table-styles"; +import { renderFaqAnswer } from "@/lib/faq-answer"; +import { + APART, + CHECKED, + CHOOSE_COMBO, + CHOOSE_PRISMA, + COMBO_FITS, + COMPARE_ROWS, + DRIZZLE_ORM_COMPARE_PATH, + FAQ, + PRICING, + PRISMA_FITS, + VERCEL_NEON_DRIZZLE_AUTHOR, + VERCEL_NEON_DRIZZLE_TITLE, + VERCEL_NEON_DRIZZLE_UPDATED, + WRONG_CHOICE, +} from "./_content"; + +const INLINE = + "font-medium text-foreground underline underline-offset-4 decoration-black/35 transition-colors hover:decoration-black/60"; + +/** + * One rhythm for the whole body: 56px between sections and 20px under every h2. + * Prose margins are zeroed so these gaps are the only spacing, and every + * paragraph and list item shares one size, line height and colour. Code spans from prose and from + * `renderFaqAnswer` share one treatment, and a span that wraps keeps its padding on both lines. + */ +const BODY = + "flex flex-col gap-14 [&_:is(h2,h3)]:my-0 [&_:is(h2,h3)]:text-primary [&_:is(p,ul,ol,li,table,figure,img)]:my-0 [&_:is(p,li)]:text-base [&_:is(p,li)]:leading-7 [&_:is(p,li)]:text-foreground [&_code]:box-decoration-clone [&_code]:rounded [&_code]:border-0 [&_code]:bg-black/[0.045] [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-[0.875em] [&_code]:font-[family-name:var(--font-code)] [&_code]:font-medium [&_code]:text-primary [&_code]:before:content-none [&_code]:after:content-none [&_table_code]:text-[13px] [&_blockquote]:my-0 [&_blockquote]:border-0 [&_blockquote]:pl-0 [&_blockquote]:font-normal [&_blockquote]:not-italic [&_blockquote_p]:before:content-none [&_blockquote_p]:after:content-none"; +const SECTION = "flex flex-col gap-5"; +const LIST = "list-disc space-y-2 pl-5 marker:text-black/25"; +const STEPS = "list-decimal space-y-2 pl-5 marker:text-muted-foreground"; +const CHOOSE_LABEL = "font-heading text-lg leading-snug font-medium"; + +const PROOF = TESTIMONIALS.filter( + (item) => item.name === "Ron Cohen" || item.name === "Omar López", +); + +const LOGOS: Record = { + Bucket: { src: "/logos/customers/bucket.png", width: 108 }, + "Cal.com": { src: "/logos/companies/cal.svg", width: 96 }, +}; + +const SOURCES = [ + { + product: "Vercel, Neon, and Drizzle", + links: [ + { label: "Vercel pricing", href: "https://vercel.com/pricing" }, + { label: "Vercel Pro plan", href: "https://vercel.com/docs/plans/pro-plan" }, + { label: "Neon pricing", href: "https://neon.com/pricing" }, + { label: "Neon + Vercel", href: "https://neon.com/docs/guides/vercel-native-integration" }, + { label: "Drizzle", href: "https://orm.drizzle.team" }, + { label: "Drizzle on Neon", href: "https://orm.drizzle.team/docs/get-started/neon-new" }, + { + label: "Drizzle schema", + href: "https://orm.drizzle.team/docs/sql-schema-declaration", + }, + ], + }, + { + product: "Prisma ORM, Prisma Postgres, and Prisma Compute", + links: [ + { label: "Prisma ORM docs", href: "/docs/orm" }, + { label: "Prisma ORM vs Drizzle", href: DRIZZLE_ORM_COMPARE_PATH }, + { label: "Prisma Postgres", href: "/docs/postgres" }, + { label: "Prisma Compute", href: "/docs/compute" }, + { label: "Prisma REST API", href: "/docs/rest-api" }, + { label: "Compute limitations", href: "/docs/compute/limitations" }, + { label: "Compute branching", href: "/docs/compute/branching" }, + { label: "Switch from Drizzle", href: "/docs/guides/switch-to-prisma-orm/from-drizzle" }, + { + label: "Import from an existing database", + href: "/docs/postgres/import-from-existing-database", + }, + { label: "Apache License 2.0", href: "https://github.com/prisma/prisma/blob/main/LICENSE" }, + { label: "Prisma pricing", href: "/pricing" }, + ], + }, +] as const; + +function SourceLink({ href, children }: { href: string; children: string }) { + return href.startsWith("/") ? ( + + {children} + + ) : ( + + {children} + + ); +} + +/** Build the shared content tree for the HTML page and its Markdown rendition. */ +function buildVercelNeonDrizzlePage(minutes: number) { + return [ +
+

+ {VERCEL_NEON_DRIZZLE_TITLE} +

+

+ Compare{" "} + + Prisma ORM + + ,{" "} + + Prisma Postgres + + , and{" "} + + Prisma Compute + {" "} + with the Vercel, Neon, and Drizzle setup, where you combine a host, a database, and an ORM + from three separate providers. Pick Prisma when you want the ORM, managed Postgres, and app + hosting from one platform, with one schema and one CLI across deploys and previews. Pick + Vercel + Neon + Drizzle when you would rather choose and connect those three products + yourself. +

+ {comparisonHeroCtas("postgres", { + secondary: false, + primaryLabel: "Get started with Prisma", + })} + {comparisonByline({ + author: VERCEL_NEON_DRIZZLE_AUTHOR, + updated: VERCEL_NEON_DRIZZLE_UPDATED, + minutes, + })} + {comparisonDisclosure({ + competitor: "Vercel, Neon, and Drizzle", + checked: CHECKED, + prismaProduct: "Prisma ORM, Prisma Postgres, and Prisma Compute", + })} +
, +
+
+ + When should I choose Vercel + Neon + Drizzle or Prisma? + + {comparisonChooseTable({ + competitor: "Vercel + Neon + Drizzle", + prisma: "Prisma", + competitorItems: CHOOSE_COMBO, + prismaItems: CHOOSE_PRISMA, + })} +
+ +
+ + How do Vercel + Neon + Drizzle and Prisma compare? + +

+ Both cover a TypeScript backend: an ORM, managed Postgres, and a place to run the app. + Vercel, Neon, and Drizzle are three products you wire together. Prisma ORM, Prisma + Postgres, and Prisma Compute sit in one platform. +

+ ({ + label: row.feature, + cells: [renderFaqAnswer(row.combo), renderFaqAnswer(row.prisma)], + }))} + /> +
+ +
+ + Where does each one fit? + +
+
+

Vercel + Neon + Drizzle

+

+ This setup combines three products to cover the app, the database, and the data layer. +

+

+ + Drizzle + {" "} + is the data layer: a free, open-source, type-safe TypeScript ORM and query builder. It + connects to a database you host somewhere else. See{" "} + + how Prisma ORM compares to Drizzle + + . +

+

+ + Neon + {" "} + is the database: serverless Postgres with branching and scale-to-zero, billed by + compute. +

+

+ + Vercel + {" "} + is the host. It runs your frontend and full-stack app with serverless functions. +

+

+ Each product handles its own layer. You connect them yourself, keep environments in + step across providers, and watch two pricing models. +

+
    + {COMBO_FITS.map((item) => ( +
  • {item}
  • + ))} +
+
+
+

Prisma

+

Prisma brings the same workflow into one platform.

+

+ + Prisma ORM + {" "} + is a type-safe data layer built around a declarative Prisma schema. +

+

+ + Prisma Postgres + {" "} + is managed Postgres in the same project as your services. +

+

+ + Prisma Compute + {" "} + hosts TypeScript apps next to Prisma Postgres, with a preview environment per branch. +

+

+ One CLI and one{" "} + + REST API + {" "} + manage all three, for developers and coding agents alike. +

+
    + {PRISMA_FITS.map((item) => ( +
  • {item}
  • + ))} +
+
+
+
+ +
+ + Where Prisma differs from Vercel + Neon + Drizzle + +
+ {APART.map((item) => ( +
+ + {item.title} + +

{renderFaqAnswer(item.body)}

+
+ ))} +
+
+ +
+ + How much do Vercel + Neon + Drizzle and Prisma cost? + + ({ + label: row.label, + cells: [renderFaqAnswer(row.combo), renderFaqAnswer(row.prisma)], + }))} + note={ + <> + Prisma Postgres and Prisma Compute share one set of plans. Neon and Vercel price + separately. Prisma ORM is free and open source, and always will be. Prisma Postgres + Free is $0 a month for 200,000 operations, 2 GB of storage, and 50 databases. No + credit card required. The{" "} + + pricing page + {" "} + lists the paid plans. Neon Launch compute is $0.106 per CU-hour ( + + Neon pricing + + ). Vercel Pro is $20 a month plus usage ( + + Vercel pricing + + , {CHECKED}). + + } + /> +
+ +
+ + When is Prisma the wrong choice? + +
    + {WRONG_CHOICE.map((item) => ( +
  • {renderFaqAnswer(item)}
  • + ))} +
+
+ +
+ + How do I switch from Vercel + Neon + Drizzle to Prisma? + +
    +
  1. + Create a Prisma Postgres database. The{" "} + + import from an existing database + {" "} + guide uses pg_dump and pg_restore over the direct connection. +
  2. +
  3. + Move the data layer with the{" "} + + switch from Drizzle + {" "} + guide if you want Prisma ORM. Prisma ORM is optional; Prisma Postgres works with any + Postgres client. +
  4. +
  5. + Point the app at the Prisma Postgres connection string. Host the HTTP app on Prisma + Compute, or keep Vercel if that host still fits. +
  6. +
+
+ +
+ + How do I switch from Prisma to Vercel + Neon + Drizzle? + +
    +
  1. + Create a Neon database and dump Prisma Postgres with pg_dump over the + direct connection, then restore into Neon. Both sides are PostgreSQL. +
  2. +
  3. + Point the app at the Neon connection string. Keep using SQL or an ORM; Drizzle is one + option, documented on{" "} + + Drizzle + Neon + + . +
  4. +
  5. Deploy the app on Vercel. Prisma Postgres stays in place until you stop using it.
  6. +
+
+ +
+ + What do teams say about Prisma? + +

Two teams on what Prisma does for them.

+ {comparisonProofCards( + PROOF.map((item) => ({ + name: item.name, + role: item.role, + company: item.company, + quote: item.quote, + logo: LOGOS[item.company], + })), + )} +
+ +
+ + Frequently asked questions + + + {FAQ.map((faq, index) => ( + + + {faq.question} + + + {renderFaqAnswer(faq.answer)} + + + ))} + +
+
, + comparisonClosingCta("postgres", { + instantDatabase: false, + secondary: false, + firstCheck: "Get started with Prisma and start building in minutes", + }), +
+

+ Sources +

+

+ Every competitor figure on this page comes from these pages, checked {CHECKED}. +

+
+ {SOURCES.map((group) => ( +
+

{group.product}

+
    + {group.links.map((link) => ( +
  • + {link.label} +
  • + ))} +
+
+ ))} +
+
, + ]; +} + +const VERCEL_NEON_DRIZZLE_READING = articleReadingStats(buildVercelNeonDrizzlePage(1)); + +export function getVercelNeonDrizzleReadingStats() { + return articleReadingStats(buildVercelNeonDrizzlePage(VERCEL_NEON_DRIZZLE_READING.minutes)); +} + +/** Build the shared content tree for the HTML page and its Markdown rendition. */ +export function getVercelNeonDrizzlePageContent() { + return buildVercelNeonDrizzlePage(VERCEL_NEON_DRIZZLE_READING.minutes); +} + +/** Render the comparison in the guide article frame. */ +export function VercelNeonDrizzlePage() { + return ( + {getVercelNeonDrizzlePageContent()} + ); +} diff --git a/apps/site/src/app/compare/vercel-neon-drizzle/facts.ts b/apps/site/src/app/compare/vercel-neon-drizzle/facts.ts new file mode 100644 index 0000000000..fcbcb3c16d --- /dev/null +++ b/apps/site/src/app/compare/vercel-neon-drizzle/facts.ts @@ -0,0 +1,101 @@ +import { + COMPARE_ROWS, + PRICING, + TOOLS, + VERCEL_NEON_DRIZZLE_PATH, + VERCEL_NEON_DRIZZLE_PUBLISHED, + VERCEL_NEON_DRIZZLE_UPDATED, +} from "./_content"; +import { getVercelNeonDrizzleReadingStats } from "./_sections"; + +export type FactValue = { + value: string | null; + source: string; + dateChecked: string; +}; + +/** Machine-readable feature and pricing cells with a source on every value. */ +export function getVercelNeonDrizzleFacts() { + const dateChecked = VERCEL_NEON_DRIZZLE_UPDATED.iso; + return { + path: VERCEL_NEON_DRIZZLE_PATH, + dateChecked, + tools: TOOLS, + features: COMPARE_ROWS.map((row) => ({ + feature: row.feature, + combo: { + value: row.combo, + source: row.sources.combo, + dateChecked, + } satisfies FactValue, + prisma: { + value: row.prisma, + source: row.sources.prisma, + dateChecked, + } satisfies FactValue, + })), + pricing: PRICING.map((row) => ({ + label: row.label, + combo: { + value: row.combo, + source: row.sources.combo, + dateChecked, + } satisfies FactValue, + prisma: { + value: row.prisma, + source: row.sources.prisma, + dateChecked, + } satisfies FactValue, + })), + }; +} + +export function getVercelNeonDrizzleAboutEntities() { + return [ + { + "@type": "SoftwareApplication", + name: TOOLS.prisma.name, + softwareVersion: TOOLS.prisma.softwareVersion, + license: TOOLS.prisma.license, + codeRepository: TOOLS.prisma.codeRepository, + programmingLanguage: TOOLS.prisma.programmingLanguage, + url: TOOLS.prisma.url, + }, + { + "@type": "SoftwareApplication", + name: TOOLS.drizzle.name, + softwareVersion: TOOLS.drizzle.softwareVersion, + license: TOOLS.drizzle.license, + codeRepository: TOOLS.drizzle.codeRepository, + programmingLanguage: TOOLS.drizzle.programmingLanguage, + url: TOOLS.drizzle.url, + }, + { + "@type": "SoftwareApplication", + name: TOOLS.neon.name, + softwareVersion: TOOLS.neon.softwareVersion, + license: TOOLS.neon.license, + codeRepository: TOOLS.neon.codeRepository, + programmingLanguage: TOOLS.neon.programmingLanguage, + url: TOOLS.neon.url, + }, + { + "@type": "SoftwareApplication", + name: TOOLS.vercel.name, + softwareVersion: TOOLS.vercel.softwareVersion, + url: TOOLS.vercel.url, + }, + ]; +} + +/** Article JSON-LD fields that sit on top of the shared comparison graph. */ +export function getVercelNeonDrizzleArticleFields() { + const { wordCount, timeRequired } = getVercelNeonDrizzleReadingStats(); + return { + datePublished: VERCEL_NEON_DRIZZLE_PUBLISHED.iso, + dateModified: VERCEL_NEON_DRIZZLE_UPDATED.iso, + wordCount, + timeRequired, + about: getVercelNeonDrizzleAboutEntities(), + }; +} diff --git a/apps/site/src/app/compare/vercel-neon-drizzle/page.tsx b/apps/site/src/app/compare/vercel-neon-drizzle/page.tsx new file mode 100644 index 0000000000..9bc8203196 --- /dev/null +++ b/apps/site/src/app/compare/vercel-neon-drizzle/page.tsx @@ -0,0 +1,68 @@ +import localFont from "next/font/local"; +import { JsonLd } from "@prisma-docs/ui/components/json-ld"; +import { + getComparisonMetadata, + getComparisonOgImageUrl, + getComparisonStructuredData, +} from "@/lib/comparison-metadata"; +import { createFaqStructuredData } from "@/lib/structured-data"; +import { getBaseUrl } from "@/lib/url"; +import { comparisonAuthorPerson } from "../byline"; +import { + FAQ, + VERCEL_NEON_DRIZZLE_AUTHOR, + VERCEL_NEON_DRIZZLE_PATH, + VERCEL_NEON_DRIZZLE_TITLE, +} from "./_content"; +import { getVercelNeonDrizzleArticleFields } from "./facts"; +import { VercelNeonDrizzlePage } from "./_sections"; + +/** + * The docs' code face. Shared with the Kysely comparison so both pages load + * one copy of the unbracketed Mona Sans Mono file. + */ +const monaSansMono = localFont({ + src: "../kysely/mona-sans-mono.woff2", + variable: "--font-code", + display: "swap", + weight: "200 900", +}); + +export const metadata = getComparisonMetadata("vercel-neon-drizzle"); + +const faqStructuredData = createFaqStructuredData( + VERCEL_NEON_DRIZZLE_PATH, + [...FAQ], + VERCEL_NEON_DRIZZLE_TITLE, +); + +/** The shared comparison graph, with this page's author, dates, image, and tool entities. */ +function articleStructuredData() { + const base = getComparisonStructuredData("vercel-neon-drizzle"); + const [article, ...rest] = base["@graph"]; + return { + ...base, + "@graph": [ + { + ...article, + ...getVercelNeonDrizzleArticleFields(), + image: getComparisonOgImageUrl("vercel-neon-drizzle"), + author: comparisonAuthorPerson(VERCEL_NEON_DRIZZLE_AUTHOR, getBaseUrl()), + }, + ...rest, + ], + }; +} + +/** Render the comparison and its article, breadcrumb and FAQ structured data. */ +export default function Page() { + return ( + <> + + +
+ +
+ + ); +} diff --git a/apps/site/src/components/sections/compare-links-data.ts b/apps/site/src/components/sections/compare-links-data.ts index de6d99d3ab..0cc8e7965c 100644 --- a/apps/site/src/components/sections/compare-links-data.ts +++ b/apps/site/src/components/sections/compare-links-data.ts @@ -59,6 +59,12 @@ export const COMPARE_PRISMA_LINKS: CompareLink[] = [ description: "Managed PostgreSQL compared: optional Prisma ORM and Prisma Compute in one project, or Auth, Storage, Realtime, and Edge Functions.", }, + { + label: "Prisma vs Vercel + Neon + Drizzle", + href: "/compare/vercel-neon-drizzle", + description: + "One TypeScript platform compared with a host, a database, and an ORM from three providers.", + }, { label: "Neon vs Prisma Postgres for hosting and the database in one place", href: "/blog/neon-vs-prisma-postgres-app-hosting-in-one-place", diff --git a/apps/site/src/lib/comparison-metadata.ts b/apps/site/src/lib/comparison-metadata.ts index 0dfb158d80..c05722bddc 100644 --- a/apps/site/src/lib/comparison-metadata.ts +++ b/apps/site/src/lib/comparison-metadata.ts @@ -23,7 +23,8 @@ export function getComparisonMetadata(slug: ComparisonSlug) { slug === "convex" || slug === "neon" || slug === "planetscale" || - slug === "supabase" + slug === "supabase" || + slug === "vercel-neon-drizzle" ) { types["application/json"] = new URL(`${page.path}.json`, baseUrl).toString(); } diff --git a/apps/site/src/lib/markdown/comparisons.test.tsx b/apps/site/src/lib/markdown/comparisons.test.tsx index f8ed7b2a59..3d2e80a49b 100644 --- a/apps/site/src/lib/markdown/comparisons.test.tsx +++ b/apps/site/src/lib/markdown/comparisons.test.tsx @@ -22,6 +22,13 @@ import * as supabase from "@/app/compare/supabase/_content"; import { getSupabaseArticleFields, getSupabaseFacts } from "@/app/compare/supabase/facts"; import { GET as getSupabaseFactsResponse } from "@/app/compare/supabase.json/route"; import { getSupabaseReadingStats } from "@/app/compare/supabase/_sections"; +import * as vercelNeonDrizzle from "@/app/compare/vercel-neon-drizzle/_content"; +import { + getVercelNeonDrizzleArticleFields, + getVercelNeonDrizzleFacts, +} from "@/app/compare/vercel-neon-drizzle/facts"; +import { GET as getVercelNeonDrizzleFactsResponse } from "@/app/compare/vercel-neon-drizzle.json/route"; +import { getVercelNeonDrizzleReadingStats } from "@/app/compare/vercel-neon-drizzle/_sections"; import { WORDS_PER_MINUTE, readingMinutes } from "@/app/compare/reading-time"; import { GET as getMongooseFactsResponse } from "@/app/compare/mongoose.json/route"; import { @@ -61,7 +68,15 @@ function plainMarkdown(text: string): string { for (const entry of COMPARISONS) { test(`${entry.slug} Markdown preserves comparison cells and every FAQ answer`, () => { - const content = { convex, kysely, mongoose, neon, planetscale, supabase }[entry.slug]; + const content = { + convex, + kysely, + mongoose, + neon, + planetscale, + supabase, + "vercel-neon-drizzle": vercelNeonDrizzle, + }[entry.slug]; const page = getMarkdownPage(`/compare/${entry.slug}`); assert.ok(page); const markdown = renderMarkdownDocument(page); @@ -405,6 +420,27 @@ test("kysely Markdown keeps the 500K Prisma ORM figure in the closing copy", () planetscaleMarkdown, /Need a database\? \[Create an instant database\]\(https:\/\/create-db\.prisma\.io\/web\)/, ); + const vercelNeonDrizzlePage = getMarkdownPage("/compare/vercel-neon-drizzle"); + assert.ok(vercelNeonDrizzlePage); + const vercelNeonDrizzleMarkdown = renderMarkdownDocument(vercelNeonDrizzlePage); + assert.ok(vercelNeonDrizzleMarkdown.includes("Free to start, no credit card required.")); + assert.ok(vercelNeonDrizzleMarkdown.includes("Trusted by 500K+ developers globally")); + assert.ok( + vercelNeonDrizzleMarkdown.includes("Prisma ORM is free and open source, and always will be."), + ); + assert.ok( + vercelNeonDrizzleMarkdown.includes("[Get started with Prisma](https://console.prisma.io)"), + ); + assert.doesNotMatch(vercelNeonDrizzleMarkdown, /Get started with Prisma Postgres/); + assert.ok( + vercelNeonDrizzleMarkdown.includes("Get started with Prisma and start building in minutes"), + ); + assert.ok(vercelNeonDrizzleMarkdown.includes("Read the docs for guides and API reference")); + assert.ok(vercelNeonDrizzleMarkdown.includes("[Get started free](https://console.prisma.io)")); + assert.doesNotMatch(vercelNeonDrizzleMarkdown, /Create a database and start building in minutes/); + assert.doesNotMatch(vercelNeonDrizzleMarkdown, /create-db\.prisma\.io/); + assert.doesNotMatch(vercelNeonDrizzleMarkdown, /Create an instant database/); + assert.doesNotMatch(vercelNeonDrizzleMarkdown, /\[Talk to us\]/); assert.equal(POSTGRES_HERO_CTAS.secondary.href, CREATE_DB_WEB); assert.equal(POSTGRES_HERO_CTAS.secondary.label, "Create an instant database"); assert.equal(POSTGRES_HERO_CTAS.primary.href, "https://console.prisma.io"); @@ -1097,7 +1133,8 @@ test("Neon Markdown carries the byline, disclosure, and Sources on the shared ch assert.ok(markdown.includes("$0.222")); assert.ok(markdown.includes("$0.35")); assert.doesNotMatch(markdown, /database branching/); - assert.doesNotMatch(markdown, /one plan|one bill/); + assert.doesNotMatch(markdown, /one plan|one bill|three bills|three accounts/); + assert.doesNotMatch(markdown, /\]\(here\)|\[here\]/); assert.doesNotMatch(markdown, /zero cold starts|no cold starts/); assert.doesNotMatch(markdown, /requires Prisma ORM/); assert.doesNotMatch(markdown, /the only|the first|the best/); @@ -1213,7 +1250,8 @@ test("Supabase Markdown carries the byline, disclosure, and Sources on the share /HIPAA starts with Pro|HIPAA from Pro|HIPAA support starts with Pro/, ); assert.doesNotMatch(markdown, /database branching/); - assert.doesNotMatch(markdown, /one plan|one bill/); + assert.doesNotMatch(markdown, /one plan|one bill|three bills|three accounts/); + assert.doesNotMatch(markdown, /\]\(here\)|\[here\]/); assert.doesNotMatch(markdown, /zero cold starts|no cold starts/); assert.doesNotMatch(markdown, /requires Prisma ORM/); assert.doesNotMatch(markdown, /listed in one place/); @@ -1310,7 +1348,8 @@ test("PlanetScale Markdown carries the byline, disclosure, and Sources on the sh assert.ok(markdown.includes("platform preview")); assert.doesNotMatch(markdown, /500 MB storage on Free/); assert.doesNotMatch(markdown, /database branching/); - assert.doesNotMatch(markdown, /one plan|one bill/); + assert.doesNotMatch(markdown, /one plan|one bill|three bills|three accounts/); + assert.doesNotMatch(markdown, /\]\(here\)|\[here\]/); assert.doesNotMatch(markdown, /zero cold starts|no cold starts/); assert.doesNotMatch(markdown, /requires Prisma ORM/); assert.doesNotMatch(markdown, /the only|the first|the best/); @@ -1368,3 +1407,119 @@ test("planetscale metadata advertises Markdown and JSON alternates", () => { assert.match(types["text/markdown"] ?? "", /\/compare\/planetscale\.md$/); assert.match(types["application/json"] ?? "", /\/compare\/planetscale\.json$/); }); + +test("Vercel + Neon + Drizzle Markdown carries the byline, disclosure, and Sources on the shared chrome", () => { + const page = getMarkdownPage("/compare/vercel-neon-drizzle"); + assert.ok(page); + const markdown = renderMarkdownDocument(page); + const minutes = getVercelNeonDrizzleReadingStats().minutes; + assert.ok( + markdown.includes( + `By [${vercelNeonDrizzle.VERCEL_NEON_DRIZZLE_AUTHOR.name}](${vercelNeonDrizzle.VERCEL_NEON_DRIZZLE_AUTHOR.path}) · Updated ${vercelNeonDrizzle.VERCEL_NEON_DRIZZLE_UPDATED.label} · ${minutes} min read`, + ), + ); + for (const social of MARTIN_SOCIALS) { + assert.ok( + markdown.includes(`[${social.label}](${social.href})`), + `missing ${social.label} profile`, + ); + } + assert.ok(markdown.includes("Checked October 2026.")); + assert.ok( + markdown.includes(`dateModified: ${vercelNeonDrizzle.VERCEL_NEON_DRIZZLE_UPDATED.iso}`), + ); + assert.ok(markdown.includes("Prisma makes Prisma ORM, Prisma Postgres, and Prisma Compute")); + assert.ok(markdown.includes("\n## Sources\n")); + assert.ok(markdown.includes("\n## When should I choose Vercel + Neon + Drizzle or Prisma?\n")); + const choose = markdown.slice( + markdown.indexOf("## When should I choose Vercel + Neon + Drizzle or Prisma?"), + markdown.indexOf("## How do Vercel + Neon + Drizzle and Prisma compare?"), + ); + assert.match(choose, /^\| Vercel \+ Neon \+ Drizzle \| Prisma \|$/m); + assert.doesNotMatch(choose, /### Vercel/); + assert.ok(markdown.includes("\n## How do Vercel + Neon + Drizzle and Prisma compare?\n")); + assert.ok(markdown.includes("\n## Where does each one fit?\n")); + assert.ok(markdown.includes("\n## How much do Vercel + Neon + Drizzle and Prisma cost?\n")); + assert.ok(markdown.includes("| Backend need | Vercel + Neon + Drizzle | Prisma |")); + assert.ok(markdown.includes("| Cost | Vercel + Neon + Drizzle | Prisma |")); + assert.ok(markdown.includes("$0.106")); + assert.ok(markdown.includes("$20")); + assert.ok(markdown.includes("200,000")); + assert.ok(markdown.includes("2 GB")); + assert.ok(markdown.includes(vercelNeonDrizzle.DRIZZLE_ORM_COMPARE_PATH)); + assert.doesNotMatch(markdown, /\/compare\/drizzle/); + assert.doesNotMatch(markdown, /500 MB storage on Free/); + assert.doesNotMatch(markdown, /database branching/); + assert.doesNotMatch(markdown, /one plan|one bill|three bills|three accounts/); + assert.doesNotMatch(markdown, /\]\(here\)|\[here\]/); + assert.doesNotMatch(markdown, /zero cold starts|no cold starts/); + assert.doesNotMatch(markdown, /requires Prisma ORM/); + assert.doesNotMatch(markdown, /the only|the first|the best/); + assert.doesNotMatch(markdown, /listed in one place/); + assert.doesNotMatch(markdown, /When should I choose Kysely/); + assert.doesNotMatch(markdown, /Copy link|Copied/); + const article = readFileSync(join(compareDir, "article.tsx"), "utf8"); + assert.match(article, /"vercel-neon-drizzle": "\/compare\/vercel-neon-drizzle\.md"/); + const sections = readFileSync(join(compareDir, "vercel-neon-drizzle/_sections.tsx"), "utf8"); + assert.match(sections, /comparisonChooseTable/); + assert.match(sections, /max-w-4xl text-balance font-heading/); + assert.match(sections, /mt-6 max-w-3xl text-pretty/); + assert.match(sections, /comparisonProofCards/); + assert.match(sections, /comparisonDisclosure/); + assert.match(sections, /secondary: false/); + assert.match(sections, /instantDatabase: false/); + assert.match(sections, /primaryLabel: "Get started with Prisma"/); + assert.match(sections, /firstCheck: "Get started with Prisma and start building in minutes"/); + assert.doesNotMatch(sections, /create-db\.prisma\.io/); + assert.doesNotMatch(sections, /defaultValue=/); + assert.doesNotMatch(sections, /PageAiActions/); + assert.doesNotMatch(sections, /dark:/); + const ai = readFileSync(join(compareDir, "page-ai-actions.tsx"), "utf8"); + assert.match(ai, /pageMdUrl.includes\("\/compare\/vercel-neon-drizzle\.md"\)/); + assert.match( + ai, + /I am choosing between the Prisma platform and Vercel \+ Neon \+ Drizzle for a TypeScript backend/, + ); + assert.match(ai, /wire Drizzle, Neon, and Vercel myself/); +}); + +test("vercel-neon-drizzle JSON facts carry a source and dateChecked on every value", async () => { + const facts = getVercelNeonDrizzleFacts(); + assert.equal(facts.path, "/compare/vercel-neon-drizzle"); + assert.equal(facts.dateChecked, vercelNeonDrizzle.VERCEL_NEON_DRIZZLE_UPDATED.iso); + assert.equal(facts.tools.drizzle.softwareVersion, "current"); + assert.ok(!("combo" in facts.tools)); + assert.ok(!("license" in facts.tools.vercel)); + assert.equal(facts.features.length, vercelNeonDrizzle.COMPARE_ROWS.length); + for (const row of [...facts.features, ...facts.pricing]) { + assert.match(row.combo.source, /^https?:\/\//); + assert.match(row.prisma.source, /^https?:\/\//); + assert.equal(row.combo.dateChecked, facts.dateChecked); + assert.equal(row.prisma.dateChecked, facts.dateChecked); + } + const response = getVercelNeonDrizzleFactsResponse(); + assert.match(response.headers.get("content-type") ?? "", /^application\/json/); + assert.deepEqual(await response.json(), facts); + + const fields = getVercelNeonDrizzleArticleFields(); + const stats = getVercelNeonDrizzleReadingStats(); + assert.equal(fields.datePublished, vercelNeonDrizzle.VERCEL_NEON_DRIZZLE_PUBLISHED.iso); + assert.equal(fields.dateModified, vercelNeonDrizzle.VERCEL_NEON_DRIZZLE_UPDATED.iso); + assert.equal(WORDS_PER_MINUTE, 225); + assert.equal(fields.wordCount, stats.wordCount); + assert.equal(fields.timeRequired, `PT${stats.minutes}M`); + assert.equal(stats.minutes, readingMinutes(stats.wordCount)); + assert.ok(stats.wordCount > 800); + assert.deepEqual( + fields.about.map((entity) => entity.name), + ["Prisma", "Drizzle", "Neon", "Vercel"], + ); + assert.ok(!("license" in (fields.about[3] ?? {}))); +}); + +test("vercel-neon-drizzle metadata advertises Markdown and JSON alternates", () => { + const types = getComparisonMetadata("vercel-neon-drizzle").alternates?.types; + assert.ok(types); + assert.match(types["text/markdown"] ?? "", /\/compare\/vercel-neon-drizzle\.md$/); + assert.match(types["application/json"] ?? "", /\/compare\/vercel-neon-drizzle\.json$/); +}); diff --git a/apps/site/src/lib/markdown/comparisons.tsx b/apps/site/src/lib/markdown/comparisons.tsx index 8956494db7..1d727cc5f2 100644 --- a/apps/site/src/lib/markdown/comparisons.tsx +++ b/apps/site/src/lib/markdown/comparisons.tsx @@ -22,6 +22,11 @@ import { import { getPlanetScalePageContent } from "@/app/compare/planetscale/_sections"; import { CHECKED as SUPABASE_CHECKED, SUPABASE_UPDATED } from "@/app/compare/supabase/_content"; import { getSupabasePageContent } from "@/app/compare/supabase/_sections"; +import { + CHECKED as VERCEL_NEON_DRIZZLE_CHECKED, + VERCEL_NEON_DRIZZLE_UPDATED, +} from "@/app/compare/vercel-neon-drizzle/_content"; +import { getVercelNeonDrizzlePageContent } from "@/app/compare/vercel-neon-drizzle/_sections"; import type { MarkdownPage } from "@/lib/markdown-pages"; import { heading, joinBlocks } from "./blocks"; import { renderComparisonContent } from "./comparison-content"; @@ -33,6 +38,7 @@ export const comparisonContent = { neon: getNeonPageContent, planetscale: getPlanetScalePageContent, supabase: getSupabasePageContent, + "vercel-neon-drizzle": getVercelNeonDrizzlePageContent, } satisfies Record ReactNode>; /** Last-checked date and modification time per comparison; every slug needs an entry. */ @@ -43,6 +49,10 @@ const comparisonFreshness = { neon: { checked: NEON_CHECKED, dateModified: NEON_UPDATED.iso }, planetscale: { checked: PLANETSCALE_CHECKED, dateModified: PLANETSCALE_UPDATED.iso }, supabase: { checked: SUPABASE_CHECKED, dateModified: SUPABASE_UPDATED.iso }, + "vercel-neon-drizzle": { + checked: VERCEL_NEON_DRIZZLE_CHECKED, + dateModified: VERCEL_NEON_DRIZZLE_UPDATED.iso, + }, } satisfies Record; /** Register complete comparison renditions using the same trees as the HTML pages. */ diff --git a/apps/site/src/lib/markdown/markdown-parity.test.ts b/apps/site/src/lib/markdown/markdown-parity.test.ts index 3229e43684..71ab928363 100644 --- a/apps/site/src/lib/markdown/markdown-parity.test.ts +++ b/apps/site/src/lib/markdown/markdown-parity.test.ts @@ -86,6 +86,7 @@ const PAGE_SOURCES: Record = { "/compare/neon": ["src/app/compare/neon/_sections.tsx"], "/compare/planetscale": ["src/app/compare/planetscale/_sections.tsx"], "/compare/supabase": ["src/app/compare/supabase/_sections.tsx"], + "/compare/vercel-neon-drizzle": ["src/app/compare/vercel-neon-drizzle/_sections.tsx"], "/mcp": [ "src/app/mcp/page.tsx", "src/app/mcp/_components/mcp-hero-section.tsx", @@ -123,6 +124,7 @@ const MIN_LITERAL_HEADINGS: Record = { "/compare/neon": 10, "/compare/planetscale": 10, "/compare/supabase": 10, + "/compare/vercel-neon-drizzle": 10, }; const HEADING_PATTERN =