From c2283744f117c6170e20e139e9a51e1aa895bbbf Mon Sep 17 00:00:00 2001 From: jhislop-design Date: Tue, 1 Sep 2026 16:27:47 -0600 Subject: [PATCH] feat(partners): add the Vercel Gold partner page Vercel joins the Gold tier, so /partners/vercel gets a full partner page instead of falling through to the generic /partners/$partner route. The page mirrors the Railway partner page section for section: hero, stats, features, create-and-deploy walkthrough, existing-app setup, pricing, testimonials, FAQ, and CTA. It reuses the shared Card, Panel, Button and CodeBlock primitives, so the FAQ accordion and the code copy buttons come from the design system rather than local state. Page content is sourced from Vercel's own pages rather than written from memory. Pricing is the published Hobby/Pro/Enterprise tiers plus Pro overage rates, the deploy steps match Vercel's TanStack Start guide and our own Start hosting guide, and every testimonial is a real attributed quote from a Vercel case study. Also route both partner pages' vercel.com/railway.com and pricing links through the partner-page UTM campaign. They previously inherited the site-wide gold-launch campaign from the central partner record, which made outbound clicks from these pages indistinguishable from every other placement. Co-Authored-By: Claude Opus 5 --- src/routeTree.gen.ts | 21 + src/routes/partners.railway.tsx | 2 +- src/routes/partners.vercel.tsx | 841 +++++++++++++++++++++ src/utils/vercel-partner.ts | 39 + tests/application-starter-partners.test.ts | 36 + 5 files changed, 938 insertions(+), 1 deletion(-) create mode 100644 src/routes/partners.vercel.tsx create mode 100644 src/utils/vercel-partner.ts diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 6825cf783..d40b894f4 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -59,6 +59,7 @@ import { Route as ShowcaseIdRouteImport } from './routes/showcase/$id' import { Route as ShopSearchRouteImport } from './routes/shop.search' import { Route as ShopCartRouteImport } from './routes/shop.cart' import { Route as ShopHandleRouteImport } from './routes/shop.$handle' +import { Route as PartnersVercelRouteImport } from './routes/partners.vercel' import { Route as PartnersRailwayRouteImport } from './routes/partners.railway' import { Route as PartnersNetlifyRouteImport } from './routes/partners.netlify' import { Route as PartnersPartnerRouteImport } from './routes/partners.$partner' @@ -468,6 +469,11 @@ const ShopHandleRoute = ShopHandleRouteImport.update({ path: '/$handle', getParentRoute: () => ShopRoute, } as any) +const PartnersVercelRoute = PartnersVercelRouteImport.update({ + id: '/vercel', + path: '/vercel', + getParentRoute: () => PartnersRoute, +} as any) const PartnersRailwayRoute = PartnersRailwayRouteImport.update({ id: '/railway', path: '/railway', @@ -1424,6 +1430,7 @@ export interface FileRoutesByFullPath { '/partners/$partner': typeof PartnersPartnerRoute '/partners/netlify': typeof PartnersNetlifyRoute '/partners/railway': typeof PartnersRailwayRoute + '/partners/vercel': typeof PartnersVercelRoute '/shop/$handle': typeof ShopHandleRoute '/shop/cart': typeof ShopCartRoute '/shop/search': typeof ShopSearchRoute @@ -1626,6 +1633,7 @@ export interface FileRoutesByTo { '/partners/$partner': typeof PartnersPartnerRoute '/partners/netlify': typeof PartnersNetlifyRoute '/partners/railway': typeof PartnersRailwayRoute + '/partners/vercel': typeof PartnersVercelRoute '/shop/$handle': typeof ShopHandleRoute '/shop/cart': typeof ShopCartRoute '/shop/search': typeof ShopSearchRoute @@ -1834,6 +1842,7 @@ export interface FileRoutesById { '/partners/$partner': typeof PartnersPartnerRoute '/partners/netlify': typeof PartnersNetlifyRoute '/partners/railway': typeof PartnersRailwayRoute + '/partners/vercel': typeof PartnersVercelRoute '/shop/$handle': typeof ShopHandleRoute '/shop/cart': typeof ShopCartRoute '/shop/search': typeof ShopSearchRoute @@ -2046,6 +2055,7 @@ export interface FileRouteTypes { | '/partners/$partner' | '/partners/netlify' | '/partners/railway' + | '/partners/vercel' | '/shop/$handle' | '/shop/cart' | '/shop/search' @@ -2248,6 +2258,7 @@ export interface FileRouteTypes { | '/partners/$partner' | '/partners/netlify' | '/partners/railway' + | '/partners/vercel' | '/shop/$handle' | '/shop/cart' | '/shop/search' @@ -2455,6 +2466,7 @@ export interface FileRouteTypes { | '/partners/$partner' | '/partners/netlify' | '/partners/railway' + | '/partners/vercel' | '/shop/$handle' | '/shop/cart' | '/shop/search' @@ -3019,6 +3031,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ShopHandleRouteImport parentRoute: typeof ShopRoute } + '/partners/vercel': { + id: '/partners/vercel' + path: '/vercel' + fullPath: '/partners/vercel' + preLoaderRoute: typeof PartnersVercelRouteImport + parentRoute: typeof PartnersRoute + } '/partners/railway': { id: '/partners/railway' path: '/railway' @@ -4457,6 +4476,7 @@ interface PartnersRouteChildren { PartnersPartnerRoute: typeof PartnersPartnerRoute PartnersNetlifyRoute: typeof PartnersNetlifyRoute PartnersRailwayRoute: typeof PartnersRailwayRoute + PartnersVercelRoute: typeof PartnersVercelRoute PartnersIndexRoute: typeof PartnersIndexRoute } @@ -4464,6 +4484,7 @@ const PartnersRouteChildren: PartnersRouteChildren = { PartnersPartnerRoute: PartnersPartnerRoute, PartnersNetlifyRoute: PartnersNetlifyRoute, PartnersRailwayRoute: PartnersRailwayRoute, + PartnersVercelRoute: PartnersVercelRoute, PartnersIndexRoute: PartnersIndexRoute, } diff --git a/src/routes/partners.railway.tsx b/src/routes/partners.railway.tsx index 9028dc796..61ef1670c 100644 --- a/src/routes/partners.railway.tsx +++ b/src/routes/partners.railway.tsx @@ -43,7 +43,7 @@ const TANSTACK_START_RAILWAY_DOCS_PATH = RAILWAY_DOCS_RESOURCE.href const RAILWAY_CANONICAL_HREF = RAILWAY_PARTNER.canonicalHref ?? RAILWAY_PARTNER.href const railwayHomeUrl = new URL(RAILWAY_CANONICAL_HREF) -railwayHomeUrl.search = new URL(RAILWAY_PARTNER.href).search +railwayHomeUrl.search = new URL(RAILWAY_HREF).search const RAILWAY_HOME_HREF = railwayHomeUrl.toString() const railwayPricingUrl = new URL('/pricing', RAILWAY_CANONICAL_HREF) railwayPricingUrl.search = railwayHomeUrl.search diff --git a/src/routes/partners.vercel.tsx b/src/routes/partners.vercel.tsx new file mode 100644 index 000000000..dbe6c7957 --- /dev/null +++ b/src/routes/partners.vercel.tsx @@ -0,0 +1,841 @@ +import * as React from 'react' +import { Link, createFileRoute } from '@tanstack/react-router' +import { + ArrowUUpLeftIcon, + ArrowUpRightIcon, + ChartLineIcon, + CheckIcon, + CpuIcon, + GitPullRequestIcon, + GlobeIcon, + LightningIcon, + PlusIcon, + RocketIcon, + ShieldCheckIcon, + SparkleIcon, +} from '@phosphor-icons/react' +import { twMerge } from 'tailwind-merge' +import { Card } from '~/components/Card' +import { Button } from '~/ui' +import { Panel, PanelContent, PanelTrigger } from '~/components/Panel' +import { CodeBlock } from '~/components/markdown/CodeBlock' +import { seo } from '~/utils/seo' +import { + PartnerImage, + partnerCategoryLabels, + partnerTierLabels, +} from '~/utils/partners' +import { getPartnerJsonLd } from '~/utils/partner-pages' +import { trackEvent } from '~/utils/analytics' +import { getVercelPartnerPageModel } from '~/utils/vercel-partner' +import { SITE_URL } from '~/utils/site' +import defaultOgImage from '~/images/og.png' + +const VERCEL_PAGE_MODEL = getVercelPartnerPageModel() +const { + docsHash: TANSTACK_START_VERCEL_DOCS_HASH, + docsResource: VERCEL_DOCS_RESOURCE, + partner: VERCEL_PARTNER, +} = VERCEL_PAGE_MODEL +const VERCEL_HREF = + 'https://vercel.com/new?utm_medium=sponsor&utm_source=tanstack&utm_campaign=partner-page' +const VERCEL_START_GUIDE_HREF = + 'https://vercel.com/kb/guide/deploy-a-tanstack-start-app-to-vercel?utm_medium=sponsor&utm_source=tanstack&utm_campaign=partner-page' +const TANSTACK_START_VERCEL_DOCS_PATH = VERCEL_DOCS_RESOURCE.href +const VERCEL_CANONICAL_HREF = + VERCEL_PARTNER.canonicalHref ?? VERCEL_PARTNER.href +const vercelHomeUrl = new URL(VERCEL_CANONICAL_HREF) +vercelHomeUrl.search = new URL(VERCEL_HREF).search +const VERCEL_HOME_HREF = vercelHomeUrl.toString() +const vercelPricingUrl = new URL('/pricing', VERCEL_CANONICAL_HREF) +vercelPricingUrl.search = vercelHomeUrl.search +const VERCEL_PRICING_HREF = vercelPricingUrl.toString() +const VERCEL_OG_IMAGE = new URL(defaultOgImage, SITE_URL).toString() +const VERCEL_TIER_LABEL = VERCEL_PARTNER.tier + ? partnerTierLabels[VERCEL_PARTNER.tier] + : undefined +const VERCEL_PARTNERSHIP_LABEL = + VERCEL_PARTNER.status === 'active' + ? 'Current TanStack partner' + : 'Previous TanStack partner' +const VERCEL_PARTNER_TITLE_LABEL = + VERCEL_PARTNER.status === 'active' + ? `${partnerTierLabels[VERCEL_PARTNER.tier]} TanStack Partner` + : 'Previous TanStack Partner' +const VERCEL_PARTNER_BADGE_LABEL = + VERCEL_PARTNER.status === 'active' + ? `${partnerTierLabels[VERCEL_PARTNER.tier]} Partner` + : VERCEL_TIER_LABEL + ? `Previous ${VERCEL_TIER_LABEL} Partner` + : 'Previous Partner' + +type FeatureIcon = React.ComponentType<{ className?: string }> + +const features: Array<{ Icon: FeatureIcon; title: string; desc: string }> = [ + { + Icon: RocketIcon, + title: 'Zero-config framework detection', + desc: 'Once Nitro is configured, Vercel detects TanStack Start and supplies the build command and output settings for you.', + }, + { + Icon: GitPullRequestIcon, + title: 'Preview deployments', + desc: 'Every push gets its own deployment URL, so a branch or pull request can be reviewed running in production-like conditions.', + }, + { + Icon: CpuIcon, + title: 'Fluid compute', + desc: 'The Nitro Vite plugin compiles your server code into Vercel Functions. They run on Fluid compute by default and scale automatically.', + }, + { + Icon: GlobeIcon, + title: 'Global delivery', + desc: 'Responses are served from Vercel’s global points of presence, with regions you can configure per project.', + }, + { + Icon: ArrowUUpLeftIcon, + title: 'Instant rollbacks', + desc: 'Every deployment keeps its own immutable URL. Promote an earlier deployment to production when you need to roll back.', + }, + { + Icon: ShieldCheckIcon, + title: 'Web Application Firewall', + desc: 'Managed WAF with configurable firewall rules, plus TLS/SSL encryption and HTTPS certificates on every plan.', + }, + { + Icon: LightningIcon, + title: 'Skew Protection', + desc: 'Clients keep talking to the deployment version they loaded, so in-flight sessions do not break mid-deploy. Included on Pro.', + }, + { + Icon: SparkleIcon, + title: 'AI Gateway and Sandbox', + desc: 'Route model traffic through AI Gateway and run untrusted or agent-generated code in isolated Sandbox environments.', + }, +] + +const steps: Array<{ num: string; title: string; code: string }> = [ + { + num: '01', + title: 'Enter your app', + code: 'cd my-tanstack-app', + }, + { + num: '02', + title: 'Install the Vercel CLI', + code: 'npm install -g vercel', + }, + { num: '03', title: 'Authenticate', code: 'vercel login' }, + { num: '04', title: 'Deploy to production', code: 'vercel --prod' }, +] + +const NITRO_VITE_CONFIG = `// vite.config.ts +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { defineConfig } from 'vite' +import { nitro } from 'nitro/vite' +import viteReact from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [tanstackStart(), nitro(), viteReact()], +})` + +const VERCEL_JSON = `{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "tanstack-start" +}` + +const pricing: Array<{ + plan: string + price: string + note: string + features: Array + highlight?: boolean +}> = [ + { + plan: 'Hobby', + price: '$0', + note: 'for personal, non-commercial projects', + features: [ + '1M edge requests per month', + '100 GB data transfer per month', + 'Up to 3 firewall rules', + 'HTTPS certificates included', + 'No credit card required', + ], + }, + { + plan: 'Pro', + price: '$20', + note: 'per user/month, includes $20 of usage', + features: [ + '10M edge requests per month', + '1 TB data transfer per month', + 'Up to 40 firewall rules', + '1 custom environment', + 'Skew Protection', + ], + highlight: true, + }, + { + plan: 'Enterprise', + price: 'Custom', + note: 'for teams at scale', + features: [ + 'Role-based access control', + 'Audit logs', + 'SAML SSO and Directory Sync', + 'Custom limits on metered services', + 'Platform SLAs and support', + ], + }, +] + +const meteredPricing: Array<[string, string]> = [ + ['Edge requests', '$2.00 / 1M'], + ['Data transfer', '$0.15 / GB'], + ['Function invocations', '$0.60 / 1M'], +] + +const testimonials: Array<{ quote: string; author: string; role: string }> = [ + { + quote: + 'We needed a quality product for a quality product. We evaluated other options, but really Vercel was a no-brainer.', + author: 'Jonathan Lemon', + role: 'Software Engineering Manager at Sonos', + }, + { + quote: + 'We try not to reinvent infrastructure we don’t have to. We’d rather spend that engineering energy on the product.', + author: 'Sherwin Yu', + role: 'Head of AI and Product Engineering at Gamma', + }, + { + quote: + 'With Vercel, I was able to quickly set up wildcard subdomains for my users and upsell them with custom domains.', + author: 'Noah Bragg', + role: 'Founder at Potion.so', + }, +] + +const faqs: Array<{ q: string; a: string }> = [ + { + q: 'Does Vercel support TanStack Start SSR and streaming?', + a: 'Yes. TanStack Start runs on Vercel through Nitro. Create a Vercel-ready app with the TanStack CLI, or add the Nitro setup from the Start hosting guide to an existing app.', + }, + { + q: 'Do I need to configure a build command?', + a: 'Usually not. Vercel detects TanStack Start and supplies the build command and output settings. If detection fails, add a vercel.json with "framework": "tanstack-start" to make it explicit. The TanStack CLI writes that file for you.', + }, + { + q: 'How does Vercel pricing actually work?', + a: 'Hobby is $0 and covers personal, non-commercial projects with 1M edge requests and 100 GB of data transfer per month. Pro is $20 per user/month and includes $20 of monthly usage credit, with metered rates beyond it. Enterprise is custom.', + }, + { + q: 'What makes Vercel different from Railway or Render?', + a: 'Vercel is strongest when preview workflows, Git-based deployment, global delivery, and serverless compute are central to how the team works. Server code deploys as Vercel Functions on Fluid compute rather than a long-running container you size yourself.', + }, + { + q: 'Does every pull request get a preview deployment?', + a: 'Yes. Connect the project to Git and each push produces its own deployment with a unique URL, so a change can be reviewed running before it is promoted to production.', + }, + { + q: 'How do I add environment variables?', + a: 'Add them under Settings > Environment Variables, or with vercel env add MY_KEY. Do not prefix server secrets with VITE_, since VITE_ values are inlined into browser code.', + }, + { + q: 'Can I migrate an existing TanStack app to Vercel?', + a: 'Yes. Install Nitro, add the nitro() plugin to your Vite config, then import the Git repository in Vercel or run npx vercel. If routes 404 after deploying, confirm nitro() is present in the Vite config and redeploy.', + }, +] + +const PAGE_TITLE = `Deploy TanStack to ${VERCEL_PARTNER.name} | ${VERCEL_PARTNER_TITLE_LABEL}` +const PAGE_DESCRIPTION = + 'Vercel runs TanStack Start through Nitro, with zero-config framework detection, a preview deployment for every push, Vercel Functions on Fluid compute, and global delivery. Hobby is free and Pro is $20 per user/month.' + +function getFaqJsonLd() { + return { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: faqs.map((faq) => ({ + '@type': 'Question', + name: faq.q, + acceptedAnswer: { + '@type': 'Answer', + text: faq.a, + }, + })), + } +} + +export const Route = createFileRoute('/partners/vercel')({ + head: () => ({ + meta: seo({ + title: PAGE_TITLE, + description: PAGE_DESCRIPTION, + keywords: `deploy tanstack to vercel, tanstack start vercel, tanstack router vercel, vercel hosting, tanstack deployment, vercel tanstack partner`, + image: VERCEL_OG_IMAGE, + }), + scripts: [ + { + type: 'application/ld+json', + children: JSON.stringify(getPartnerJsonLd(VERCEL_PARTNER)), + }, + { + type: 'application/ld+json', + children: JSON.stringify(getFaqJsonLd()), + }, + ], + }), + component: VercelPartnerPage, +}) + +function CheckBadge() { + return ( + + + + ) +} + +function trackVercelClick() { + trackEvent('partner_clicked', { + partner_id: VERCEL_PARTNER.id, + placement: 'detail', + destination: 'external', + destination_host: new URL(VERCEL_CANONICAL_HREF).host, + partner_tier: VERCEL_PARTNER.tier, + }) +} + +function trackTanStackDocsClick() { + trackEvent('partner_clicked', { + partner_id: VERCEL_PARTNER.id, + placement: 'detail', + destination: 'internal_resource', + partner_tier: VERCEL_PARTNER.tier, + }) +} + +function VercelCodeExample({ + code, + lang, + title, +}: { + code: string + lang: string + title: string +}) { + return ( + + {code} + + ) +} + +function VercelPartnerPage() { + const [openFaq, setOpenFaq] = React.useState(null) + + React.useEffect(() => { + trackEvent('partner_viewed', { + partner_id: VERCEL_PARTNER.id, + placement: 'detail', + partner_tier: VERCEL_PARTNER.tier, + }) + }, []) + + return ( +
+
+ + + {/* Hero */} +
+
+
+ +
+
+ + {VERCEL_PARTNER_BADGE_LABEL} ·{' '} + {partnerCategoryLabels[VERCEL_PARTNER.category]} + +
+ + + {VERCEL_PARTNERSHIP_LABEL} + +
+
+
+ +

+ Ship TanStack apps +
+ on agentic infrastructure +

+ +

+ Vercel builds, deploys, and runs TanStack Start apps through Nitro. + Push to Git and every change gets its own preview deployment, while + server code runs as Vercel Functions on Fluid compute. +

+ +

+ "Vercel has just completely scaled with that usage. We've never had + it fall over due to capacity or had to provision anything extra." - + Andy Yoon, Lead Frontend Engineer at OpenEvidence +

+ +
+ + +
+

+ No credit card required. Hobby is free for personal, non-commercial + projects. +

+
+ + {/* Stats */} +
+ {[ + ['Zero-config', 'TanStack Start detection'], + ['Every push', 'Preview deployment'], + ['Fluid compute', 'Default for functions'], + ['$0', 'To get started'], + ].map(([value, label]) => ( +
+
{value}
+
+ {label} +
+
+ ))} +
+ + {/* Features */} +
+

+ Why TanStack teams choose Vercel +

+

+ Vercel is strongest when preview workflows, Git-based deployment, + global delivery, and serverless compute are central to how your team + ships. +

+
+ {features.map(({ Icon, title, desc }) => ( + + +
{title}
+

+ {desc} +

+
+ ))} +
+
+ + {/* How it works */} +
+

+ Create a Vercel-ready app +

+

+ The TanStack CLI adds the Vercel deployment setup while it creates + your app, including the vercel.json that makes + framework detection explicit. +

+ +
+ +
+ +

Deploy the app

+

+ Import the Git repository in Vercel to deploy on every push, or ship + it from the CLI. +

+
+ {steps.map(({ num, title, code }) => ( + +
+ {num} +
+
+
{title}
+ + {code} + +
+
+ ))} +
+ +
+ + +
+
+ + {/* Existing TanStack Start apps */} +
+

+ Add Vercel to an existing app +

+

+ Install Nitro and register its Vite plugin. Nitro compiles your + server code into output that Vercel deploys as Vercel Functions. +

+ +
+ +
+ +

+ If framework detection does not pick up TanStack Start, set it + explicitly: +

+ +
+ +
+ +
+ + +
+
+ + {/* Pricing */} +
+

+ Pricing that scales with you +

+

+ Hobby is free for personal, non-commercial projects. Pro is $20 per + user/month and includes $20 of monthly usage credit, with metered + rates beyond it. +

+ +
+ {pricing.map(({ plan, price, note, features: pf, highlight }) => ( + + {highlight ? ( + + Most popular + + ) : null} +
{plan}
+
+ {price} +
+
+ {note} +
+
    + {pf.map((feat) => ( +
  • + + {feat} +
  • + ))} +
+
+ ))} +
+ +
+ {meteredPricing.map(([key, value]) => ( +
+
+ {key} +
+
+ {value} +
+
+ ))} +
+

+ Pro overage rates. See Vercel's pricing page for the full list of + metered services. +

+ +
+ + +
+
+ + {/* Testimonials */} +
+

+ What teams say about shipping on Vercel +

+

+ Quotes from engineers and founders running production workloads on + Vercel. +

+
+ {testimonials.map(({ quote, author, role }) => ( + +
+ "{quote}" +
+
+
{author}
+
+ {role} +
+
+
+ ))} +
+ +
+ + + + Deploy from Git, no credit card required + +
+
+ + {/* FAQ */} +
+

+ Frequently asked questions +

+

+ Common questions from TanStack developers evaluating Vercel. +

+ +
+ {faqs.map(({ q, a }, i) => { + const isOpen = openFaq === i + return ( + setOpenFaq(next ? i : null)} + className="border-b border-gray-200 dark:border-gray-800" + > + + + {q} + + + + +

+ {a} +

+
+
+ ) + })} +
+
+ + {/* CTA */} +
+
+ TanStack Start runs on Vercel through Nitro +
+

+ Ready to ship your TanStack app? +

+

+ Import a Git repository and Vercel handles the build, the preview + deployments, and the production rollout. +

+

+ Hobby is free for personal, non-commercial projects. +

+ +
+ + +
+
+ +

+ {VERCEL_PARTNER.status === 'active' + ? `${VERCEL_PARTNER.name} is a ${VERCEL_TIER_LABEL} TanStack partner. ` + : `${VERCEL_PARTNER.name} is a previous TanStack partner. `} + + Browse all TanStack partners + + .{' '} + + vercel.com + +

+
+
+ ) +} diff --git a/src/utils/vercel-partner.ts b/src/utils/vercel-partner.ts new file mode 100644 index 000000000..1f2e24db3 --- /dev/null +++ b/src/utils/vercel-partner.ts @@ -0,0 +1,39 @@ +import { getPartnerById, type Partner } from '~/utils/partners' + +const VERCEL_PARTNER_ID = 'vercel' +const VERCEL_DEPLOYMENT_ID = 'vercel' +const VERCEL_DOCS_HASH = 'vercel-official-partner' +const VERCEL_CREATE_COMMAND = `npx @tanstack/cli@latest create my-tanstack-app --deployment ${VERCEL_DEPLOYMENT_ID}` + +export function createVercelPartnerPageModel(partner: Partner | undefined) { + if (!partner || partner.id !== VERCEL_PARTNER_ID) { + throw new Error('The Vercel partner page requires the Vercel record') + } + + const docsResource = partner.resources?.find( + (resource) => + resource.kind === 'documentation' && + resource.href.startsWith('/start/') && + resource.href.includes('/guide/hosting'), + ) + + if (!docsResource) { + throw new Error( + 'The Vercel partner page requires a TanStack Start hosting resource', + ) + } + + return { + create: { + command: VERCEL_CREATE_COMMAND, + deploymentId: VERCEL_DEPLOYMENT_ID, + }, + docsHash: VERCEL_DOCS_HASH, + docsResource, + partner, + } +} + +export function getVercelPartnerPageModel() { + return createVercelPartnerPageModel(getPartnerById(VERCEL_PARTNER_ID)) +} diff --git a/tests/application-starter-partners.test.ts b/tests/application-starter-partners.test.ts index ee42395b5..4145e71bb 100644 --- a/tests/application-starter-partners.test.ts +++ b/tests/application-starter-partners.test.ts @@ -31,6 +31,10 @@ const { createRailwayPartnerPageModel, getRailwayPartnerPageModel, }: typeof import('../src/utils/railway-partner') = require('../src/utils/railway-partner') +const { + createVercelPartnerPageModel, + getVercelPartnerPageModel, +}: typeof import('../src/utils/vercel-partner') = require('../src/utils/vercel-partner') const { getStartHostingPartners, }: typeof import('../src/utils/start-hosting-partners') = require('../src/utils/start-hosting-partners') @@ -41,6 +45,9 @@ const { const { addOn: reactRailwayAddOn, }: typeof import('@tanstack/create/worker-manifest/frameworks/react/add-ons/railway') = require('@tanstack/create/worker-manifest/frameworks/react/add-ons/railway') +const { + addOn: reactVercelAddOn, +}: typeof import('@tanstack/create/worker-manifest/frameworks/react/add-ons/vercel') = require('@tanstack/create/worker-manifest/frameworks/react/add-ons/vercel') const formerPartnerFeatures = ['convex', 'neon', 'strapi'] @@ -170,6 +177,35 @@ test('custom Railway page derives its partner contract from central data', () => assert.equal(previousModel.docsResource, docsResource) }) +test('custom Vercel page derives its partner contract from central data', () => { + const model = getVercelPartnerPageModel() + const { create, docsResource, partner } = model + const centralPartner = partners.find((candidate) => candidate.id === 'vercel') + + assert.equal(partner, centralPartner) + assert.ok(partner) + assert.equal(centralPartner?.category, 'deployment') + assert.ok(centralPartner?.resources?.includes(docsResource)) + assert.equal(create.deploymentId, reactVercelAddOn.id) + assert.equal(reactVercelAddOn.partner.id, partner.id) + assert.equal( + create.command, + `npx @tanstack/cli@latest create my-tanstack-app --deployment ${reactVercelAddOn.id}`, + ) + + const previousModel = createVercelPartnerPageModel({ + ...partner, + status: 'inactive', + startDate: null, + endDate: null, + tier: undefined, + }) + + assert.equal(previousModel.partner.status, 'inactive') + assert.equal(previousModel.partner.tier, undefined) + assert.equal(previousModel.docsResource, docsResource) +}) + function assertDoesNotDefaultToFormerPartners(features: Array) { for (const feature of formerPartnerFeatures) { assert.equal(features.includes(feature), false)