next-zonesv0.1.0

Getting started

Requires Next.js 16.3.6, 16.3.7, 16.3.8 or 16.4.0 and Node.js 24 or later. Zones use the App Router, the Pages Router, or both; this guide uses the App Router.

The quick way#

npx @runsnip/next-zones init my-app blog shop     # a workspace: shared/, shell/, blog/, shop/, installed
cd my-app
npm run dev                                       # every zone on one next dev, with HMR: http://localhost:3000
npm run doctor                                    # checks the workspace for Zones and for dev
npx next-zones add docs                           # one more zone, at /docs

init uses the package manager that runs it (npm, yarn, pnpm or bun) and never overwrites a file. Every tsconfig extends @runsnip/next-zones/tsconfig/zone.json, and each zone keeps its own @/* → ./src/*. The steps below are what it sets up, for a workspace made by hand.

1. A workspace of zones#

One workspace (npm, yarn or pnpm), so every zone shares one node_modules:

my-app/
  package.json        workspaces: ["shared", "shell", "blog", "shop"]
  shared/             a package: the root layout, shared UI
  shell/              the zone mounted at "/"
  blog/               the zone mounted at "/blog"
  shop/               the zone mounted at "/shop"
{
  "private": true,
  "workspaces": ["shared", "shell", "blog", "shop"],
  "dependencies": { "@runsnip/next-zones": "0.x" }
}

2. Declare each zone#

Each zone's next.config.mjs (or .ts):

// shell/next.config.mjs
import { zoneConfig } from "@runsnip/next-zones/config";

export default zoneConfig({
  mount: "/",
  endpoints: { events: true, health: true, admin: true },   // Zones' own URLs, under /_next-zones (see below)
  transpilePackages: ["shared"],
});

endpoints is the shell's only. events feeds <ZoneUpdates />, health answers a supervisor, and admin is what next-zones install, pull --url and prune --url talk to: without it they get a 404. See endpoints.

// blog/next.config.mjs
import { zoneConfig } from "@runsnip/next-zones/config";

export default zoneConfig({
  mount: "/blog",
  transpilePackages: ["shared"],
});

Every route of blog lives under blog/app/blog/…. At the top of blog/app/ there may also be the root files Next needs when the zone runs alone (layout, not-found, global-error, global-not-found, error, loading, template, default, and CSS files); on Zones the shell's are used.

3. Share the root layout#

Every zone renders the same root layout, so a page looks the same however it is reached:

// shared/root-layout.tsx
import Link from "next/link";

export function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <nav><Link href="/">Home</Link> <Link href="/blog">Blog</Link> <Link href="/shop">Shop</Link></nav>
        <main>{children}</main>
      </body>
    </html>
  );
}
// blog/app/layout.tsx (the same in every zone)
import { RootLayout } from "shared/root-layout";
export default RootLayout;

4. Follow new versions in open tabs#

In the shell's root layout, render <ZoneUpdates /> once. Open tabs then pick up a newly installed zone image on their next navigation.

5. Check the workspace#

npx next-zones check .
✓ /            shell
✓ /blog        blog
✓ /shop        shop

It fails if two zones claim one mount, if no zone owns /, or if an alias collides. Run it before every build.

6. Build and run#

Give each zone a version in its package.json, then:

next-zones build          # the shell as an app, blog and shop as images, zones.json pinning their versions
next-zones start          # Zones on port 3000: the shell, with blog and shop installed

Edit this page on GitHub · Markdown