POC: Platform-agnostic by design?
Reader, it was.
TL;DR
The monorepo's founding doctrine is platform-agnostic by design. The Contentful + Vercel POC (SUG-127) was the first attempt to actually test that claim, across three axes: does the component library render without Sanity? Does the content model translate to a different schema? And is Vercel's DX meaningfully different from Netlify's, or just differently priced? All three phases are done. The components held. The build toolchain had two gaps — both found and fixed by the first real external consumer of the DS package. The content model transferred. The vendor verdict: keep Netlify for Vite, use Vercel for Next.js. The hybrid model is confirmed.
Three Questions, One POC
The monorepo PRD makes an architectural promise: migrating from Sanity to another headless CMS should be “straightforward and contained, requiring changes primarily within a bounded content adapter layer.” The CMS canonical PRD §5.5 makes the same claim for the content model. SUG-127 was the first deliberate attempt to test those claims rather than take them on faith.
- Component library agnosticism — do Button, Card, Grid render correctly in a Next.js app with no Sanity dependency?
- Content model portability — can Contentful’s content types mirror the atomic model the Sanity schema uses? (See the schema ERD.)
- Vendor comparison — Contentful vs Sanity, Vercel vs Netlify: deployment DX, build performance, pricing model, integration complexity.
The epic was structured as a guided build. Bex executed in Contentful’s UI and Vercel’s dashboard; the epic instructions provided step-by-step context and CLI commands. Claude handled all git operations. Bex would like it noted that she configured the environment variables herself and is very proud of this.
Challenge 1 — ERR_PACKAGE_PATH_NOT_EXPORTED
ERR_PACKAGE_PATH_NOT_EXPORTED: Package subpath './styles.css' is not defined by "exports" in .../packages/design-system/package.json
The design system's package.json had an exports map covering "." (the JS entry) but missing "./styles.css". In a monorepo, this doesn't matter: workspace tools bypass the exports map entirely and resolve straight to source. Outside the monorepo, the exports map is law. The styles entry had been absent since the DS was first packaged. No build had failed. No linter had warned. workspace:* had been hiding the gap since day one.
One-line fix. Decision 11 in the ADR. See the component registry for the full artifact surface.
Challenge 2 — tsup and the Vanishing CSS
Adding the exports entry exposed the second challenge. The dist/ artifact, built by tsup, didn't contain the CSS file.
tsup's default configuration treats CSS Modules as a bundling implementation detail and silently drops the standalone CSS output. No error. No warning. A clean build, and nothing in dist/. You spend a while convinced you've misconfigured something before accepting that the tool is doing exactly what it was designed to do, which is not what you needed.
Decision 12: migrate from tsup to esbuild directly, with entryPoints that name the CSS file as a separate output. The config is deliberately minimal: compile TypeScript, copy CSS, produce ESM. No magic. Every assumption is visible.
Phase 1 — What it Proved
Button, Card, Grid render identically in the Next.js app and the Sanity-backed app. No component source changes were required. Decision 10 ("use client" at the page level) was the only React Server Component accommodation. One line.
The components passed. The packaging had two challenges, both fixed. Here is what that means.
In a monorepo, when apps/contentful-poc declares a dependency on packages/design-system, pnpm's workspace:* resolution points it directly at the source folder on disk. No build step. No exports map check. No dist/ artifact. The package boundary effectively doesn't exist.
This is fast and convenient. It is also why both gaps went undetected. The exports map entry for the CSS file was missing — but workspace resolution never checks the exports map. The build tool was dropping the CSS from the output — but workspace resolution never reads the build output at all. Both would have broken any app that installed the package normally, from a registry or via npm. None of the apps in this monorepo had ever done that.
The Contentful POC was the first app to consume packages/design-system as a real package: reading the exports map, building from dist/, respecting the package boundary. It hit both gaps immediately. The monorepo had been hiding them since day one — not through any fault in the code, but because workspace:* is a very helpful shortcut that skips exactly the things that matter to an external consumer.
Phase 2 — The Content Model Test: Findings
Phase 2 extended the Contentful space to the full four-bucket atomic model: tag (taxonomy), siteSettings (singleton), page + heroSection + richTextSection (sections). All four buckets rendered in the app. The most significant structural finding: Sanity sections are inline typed objects embedded in the document — fast to fetch, non-reusable. Contentful sections are linked entries — separately versioned and reusable across pages, resolved via include: 2. The discriminator path differs (entry.sys.contentType.sys.id vs _type) but the switch logic is structurally identical — a clean adapter surface.
The richer test: article sections were rewired from a monolithic body Rich Text field to sections[] linked entries. The body field was retired in the Contentful content model. Homepage and /articles archive were wired to page entries in Contentful. A serializeSections() helper serializes all linked section entries into plain data before crossing the Next.js server/client boundary — the same section-builder pattern used on the Sanity side, expressed through a different adapter.
Additional Phase 2 findings: table and code nodes in the rich text renderer were wired to DS accent tokens (same tokens as the Sanity app — zero token changes). Theme toggle works with DS-controlled light/dark mode, no system preference override. An articleListSection content type gives editors CMS-controlled article ordering; the /articles route falls back to getAllArticles() sorted by date when no articleListSection is present.
Content model verdict: the atomic strategy transfers. Structural differences — linked entries vs inline objects, include depth vs GROQ projection, flat slug vs nested slug.current — are all contained within the adapter layer. No DS component changes were required.
Phase 3 — The Vendor Verdict
The coupling point map is fully populated across 15 Architecture Decision Records. Summary: the DS is agnostic at the component and token level. The packaging layer had two gaps — both found and fixed by the first real external consumer. The content model strategy is portable; structural differences are documented, not papered over.
Vendor verdict: hybrid model. Netlify stays for the Vite/Sanity web app — no migration needed, no reason to change. Vercel for any Next.js/Contentful work. The key evaluation axes: Vercel auto-detects monorepos via turbo.json; Netlify requires netlify.toml base config for the same behaviour. Both platforms are free-tier sufficient at Sugartown's current build volume. Next.js-native features — ISR, next/image, server components — make Vercel the obvious home for that stack.
One caveat documented in the ADR: some DX wins in the eval are Next.js wins, not Vercel wins. The comparison is Vercel + Next.js vs Netlify + Vite — not a clean platform-only comparison. A Vite app on Vercel would work fine; it just wouldn't benefit from the RSC/ISR stack. The eval names this explicitly rather than letting Next.js wins inflate the Vercel score.
The POC Is Live
poc.sugartown.io is the production channel. poc-preview.sugartown.io tracks the preview/main channel. Both on Vercel, DNS CNAME via Pair (SUG-128). The full POC — article pipeline, section builder, atomic content model, theme toggle, /articles archive — is live and inspectable.
Shipped as v0.25.0 on 2026-05-25. First MINOR release to include a full app addition rather than a feature or DS primitive.
Artifacts
- Architecture Decision Record (15 decisions): docs/briefs/SUG-127-architecture-decisions.md
- Vendor Eval: docs/briefs/vendor-eval-vercel-vs-netlify.md
- Shipped epic doc: docs/shipped/SUG-127-contentful-vercel-poc-platform-vendor-evaluation.md
- Release notes v0.25.0: docs/release-notes/RELEASE_NOTES_v0.25.0.md
- CHANGELOG entry [0.25.0]: CHANGELOG.md
- Live POC (production): poc.sugartown.io
- Live POC (preview/main): poc-preview.sugartown.io
- DS component registry: sugartown.io/platform/design-system/registry
- Schema ERD: sugartown.io/platform/cms#schema-erd