Anatomy of a Deployable Framework

Building a modern web framework from scratch means deciding how to handle both rendering strategies and deployment targets. The Build Output API from Vercel provides a convenient contract: if your framework can emit the right folder structure and configuration files, Vercel handles the infrastructure.

This post walks through constructing a minimal React-based framework that supports static generation, incremental static regeneration, server-side rendering, and edge middleware. The example pages we’ll target are a landing page, a product listing, and a personalized “popular” page.

The demo framework is deliberately simplified. A production-ready framework would need deeper bundling optimizations, caching layers, type-checking pipelines, and more. Here, the goal is a functional baseline and a clear understanding of how each feature maps to the deployed output.

The Pages and Their Performance Requirements

Landing Page

The landing page is largely static content with a single hero image. Two optimizations matter here:

  • Image optimization: Serve images in modern formats like .webp or .avif, and explicitly set dimensions to prevent Cumulative Layout Shift.
  • Edge caching: A static HTML file can be cached by the CDN. The Vercel Edge Network handles this automatically for files in the static output folder.

Products Page

Demo website's product page

A product listing needs fresh data but shouldn’t compile on every request. This is a clear case for Incremental Static Regeneration (ISR): prerender the page, serve it from the edge cache, and regenerate it in the background when the configured expiration passes.

For this page, many product images are displayed, so lazy loading non-critical assets becomes important for first paint performance.

Displaying personalized recommendations requires access to the request context. Two tools apply:

  • Routing Middleware for request-time redirects based on headers.
  • Vercel Functions for server-rendered HTML that depends on backend logic.

Framework Structure and Configuration

Pages live in a pages/ directory. Each module exports a React component as the default export and a pageConfig object that declares the rendering strategy:

  • static for build-time HTML,
  • ssr for request-time rendering,
  • prerender for ISR (with an optional expiration and fallback),
  • edge for middleware-style logic.

This object drives how the build step emits files to .vercel/output.

Building Static Rendering

For a static page, HTML is generated at build time. ReactDOM Server’s renderToString accepts a component and returns its HTML string. Scripts needed for hydration are not static, so a bundler produces a JavaScript file per page that hydrates the markup after load.

A createStaticFile function orchestrates this: it reads the component, generates the hydration bundle, computes the HTML, and writes a final file that includes a deferred script reference. The outputs land in .vercel/output/static, the folder that Vercel’s Build Output API reads when preparing the deployment.

ISR with Functions and Prerender Config

Enabling ISR on the products page requires four artifacts:

  • products.func/index.js: the serverless function that renders and regenerates HTML,
  • products.func/vc-config.js: runtime environment configuration,
  • products.prerender-config.json: the triggering policy for revalidation,
  • products.prerender-fallback.html: a fallback snapshot served while the page is rebuilding.

A serverless function must contain the application code and dependencies. Bundling everything into a single handler file simplifies the deployment footprint. This function is triggered on cache misses or after the expiration interval, and it produces fresh markup that replaces the cached version.

The framework exposes two helpers for this. createServerlessFunction generates the function folder and bundles both server and client logic. createPrerender layers on top of it, writing the prerender configuration with the expiration value and the fallback static file path.

The resulting function generically renders any passed component to HTML. When deployed, calls to the function trigger regeneration, and the per-region edge cache serves the prior HTML until fresh output is ready.

Rendering at the Edge

Because React is isomorphic and doesn’t depend on Node.js libraries, it can run in the Edge Runtime. Edge Functions resemble serverless functions, but their runtime differs — they execute closer to the user.

To enable Edge Server-Rendering, we create a createEdgeFunction helper that generates the necessary files. It calls generateEdgeBundle to bundle the required code, then writes a .vc-config.json file that specifies the edge runtime and points to an entrypoint file rather than a handler.

The bundling process mirrors the serverless approach, except we now care about values on the req object. To pass those as a prop to the edge-rendered page, we dynamically create the React element from the original page’s file path.

After bundling, the resulting entrypoint passes the req prop to the page and returns a new Response object containing the generated HTML.

Server-Side Rendering via Functions

Vercel Functions handle both server-rendered pages and data-fetching endpoints. Server-Side Rendering uses the same Serverless Function mechanism as Incremental Static Regeneration — the difference is when the function runs and whether its responses are cached.

For SSR, we create a function that generates the page’s HTML on every single request. The implementation is a subset of the ISR code: we only call createServerlessFunction to produce both a lambda (serverless) bundle and a client-side bundle.

With ISR, the lambda only executes when a user requests a page whose cache is older than the revalidate value, after which Vercel regenerates the page automatically. With SSR, the function is invoked on each request, and Vercel doesn’t cache the responses — so every response is unique.

Image Optimization

Vercel optimizes images automatically when the src points to /_vercel/image?url= and the proper configuration is in place. To make this easy for framework users, we support a vercel.config.js file where image settings can be defined.

We export an Image component that rewrites the src to the /_vercel/image path and includes the right height and width attributes based on the viewport. You can then use it like a normal img tag. The only required setup is configuration in vercel.config.js — such as the external domain, image size, and desired modern format. Our framework reads this file and writes its contents to .vercel/output/config.json.

Once that configuration exists, any image using our custom Image component benefits from Vercel’s Automatic Image Optimization.

Putting It All Together

With the rendering patterns and image optimization supported, we can traverse the pages directory and invoke the relevant functions to create the required files. Static assets — images, CSS, JavaScript — from the project’s public folder are copied to .vercel/output/static, and the .vercel/output/config.json file is generated from the project’s vercel.config.js.

The result is a valid .vercel/output folder that can be deployed to Vercel, leveraging the platform’s edge features.

Modern frameworks like Next.js provide all of this out of the box, so their users never deal with these details. But for independent developers or framework authors who want to integrate with Vercel directly, the Build Output API makes it straightforward to build and deploy any project.

The implementation described here is intentionally minimal and should not be used in production — it does the bare minimum. It does, however, illustrate just how much work modern frameworks handle for us. The full demo implementation is available in this GitHub repository, and Astro’s Vercel integration offers another example of the Build Output API in practice.