Skip to content

Commit

Permalink
[Multi Zones] Update the with-zones example to use App Router (vercel…
Browse files Browse the repository at this point in the history
…#67636)

This pull request updates the `with-zones` example to demonstrate how to
use zones with App Router.

---------

Co-authored-by: Delba de Oliveira <[email protected]>
  • Loading branch information
mknichel and delbaoliveira authored Jul 12, 2024
1 parent 9c55b45 commit 70a023b
Show file tree
Hide file tree
Showing 16 changed files with 170 additions and 35 deletions.
19 changes: 14 additions & 5 deletions examples/with-zones/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
# Using multiple zones
# Multi-Zone Next.js Applications

With Next.js you can use multiple apps as a single app using its [multi-zones feature](https://nextjs.org/docs/advanced-features/multi-zones). This is an example showing how to use it.
This is an example that demonstrates how to serve multiple Next.js applications from a single domain, called [Multi Zones](https://nextjs.org/docs/advanced-features/multi-zones).

- All pages should be unique across zones. For example, the `home` app should not have a `pages/blog/index.js` page.
Multi Zones are an approach to micro-frontends that separate a single large application on a domain into smaller applications that each serve a set of paths.
This is useful when there are collections of pages unrelated to the other pages in the application. By moving those pages to a separate zone, you can reduce the size of the application which improves build times and removes code that is only necessary for one of the zones.

Multi-Zone applications work by having one of the applications route requests for some paths to the other pages using the [`rewrites` feature](https://nextjs.org/docs/pages/api-reference/next-config-js/rewrites) of `next.config.js`. All URL paths should be unique across all the zones for the domain. For example:

- There are two zones in this application: `home` and `blog`.
- The `home` app is the main app and therefore it includes the rewrites that map to the `blog` app in [next.config.js](home/next.config.js)
- The `blog` app sets [`basePath`](https://nextjs.org/docs/api-reference/next.config.js/basepath) to `/blog` so that generated pages, Next.js assets and public assets are within the `/blog` subfolder.
- `home` will serve all paths that are not specifically routed to `blog`.
- `blog` will serve the `/blog` and `/blog/*` paths.
- The `blog` app sets [`basePath`](https://nextjs.org/docs/api-reference/next.config.js/basepath) to `/blog` so that generated pages, Next.js assets and public assets are unique to the `blog` zone and won't conflict with anything from the other zones.

NOTE: A `basePath` will prefix all pages in the application with the `basePath` automatically, including relative links. If you have many pages that don't share the same path prefix (for example, `/home` and `/blog` live in the same zone), you can use [`assetPrefix`](https://nextjs.org/docs/app/api-reference/next-config-js/assetPrefix) to add a unique prefix for Next.js assets without affecting the other pages.

## How to use

Expand Down Expand Up @@ -46,7 +55,7 @@ cd blog
yarn && yarn dev
```

The `blog` app should be up and running in [http://localhost:4000](http://localhost:4000)!
The `blog` app should be up and running in [http://localhost:4000/blog](http://localhost:4000/blog)!

## Preview

Expand Down
16 changes: 16 additions & 0 deletions examples/with-zones/blog/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const metadata = {
title: "Next.js - Blog Zone",
description: "Next.js example for Multi Zones",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ export default function Blog() {
<h3>This is our blog</h3>
<ul>
<li>
<Link href="/post/1">Post 1</Link>
<Link href="/blog/post/1">Post 1</Link>
</li>
<li>
<Link href="/post/2">Post 2</Link>
<Link href="/blog/post/2">Post 2</Link>
</li>
</ul>
<Link href="/">Home</Link>
Expand Down
11 changes: 11 additions & 0 deletions examples/with-zones/blog/app/post/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Link from "next/link";

export default function Post({ params: { id } }) {
return (
<div>
<h3>Post #{id}</h3>
<p>Lorem ipsum</p>
<Link href="/blog">Back to blog</Link>
</div>
);
}
8 changes: 8 additions & 0 deletions examples/with-zones/blog/next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath: "/blog",
experimental: {
// Next.js will automatically prefix `basePath` to client side links which
// is useful when all links are relative to the `basePath` of this
// application. This option opts out of that behavior, which can be useful
// if you want to link outside of your zone, such as linking to
// "/" from "/blog" (the `basePath` for this application).
manualClientBasePath: true,
},
};

module.exports = nextConfig;
1 change: 1 addition & 0 deletions examples/with-zones/blog/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"name": "blog",
"private": true,
"scripts": {
"dev": "next dev -p 4000",
Expand Down
14 changes: 0 additions & 14 deletions examples/with-zones/blog/pages/post/[id].tsx

This file was deleted.

9 changes: 7 additions & 2 deletions examples/with-zones/blog/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
File renamed without changes.
16 changes: 16 additions & 0 deletions examples/with-zones/home/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const metadata = {
title: "Next.js - Home Zone",
description: "Next.js example for Multi Zones",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Link from "next/link";
import dynamic from "next/dynamic";

const Header = dynamic(import("../components/Header"));
import Header from "../components/Header";

export default function Home() {
return (
Expand Down
15 changes: 15 additions & 0 deletions examples/with-zones/home/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const nextJest = require("next/jest");

const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
});

// Add any custom config to be passed to Jest
const customJestConfig = {
testEnvironment: "jsdom",
testMatch: ["./**/*.test.{ts,tsx}"],
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig);
8 changes: 2 additions & 6 deletions examples/with-zones/home/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ const { BLOG_URL } = process.env;
const nextConfig = {
async rewrites() {
return [
{
source: "/:path*",
destination: `/:path*`,
},
{
source: "/blog",
destination: `${BLOG_URL}/blog`,
},
{
source: "/blog/:path*",
destination: `${BLOG_URL}/blog/:path*`,
source: "/blog/:path+",
destination: `${BLOG_URL}/blog/:path+`,
},
];
},
Expand Down
7 changes: 6 additions & 1 deletion examples/with-zones/home/package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
{
"name": "home",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"test": "jest --watch"
},
"dependencies": {
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/jest": "29.5.11",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"jest": "29.7.0",
"path-to-regexp": "6.2.1",
"typescript": "^4.9.3"
}
}
64 changes: 64 additions & 0 deletions examples/with-zones/home/test/next-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Test file for the rewrites in next.config.js. This is useful to test the logic for rewriting
* paths to multi zones to make sure that the logic is correct before deploying the application.
*/

import { type MatchResult, compile, match } from "path-to-regexp";
import nextConfig from "../next.config.js";

function getDestination(destination: string, pathMatch: MatchResult): string {
const hasDifferentHost = destination.startsWith("https://");
if (hasDifferentHost) {
const destinationUrl = new URL(destination);
destinationUrl.pathname = compile(destinationUrl.pathname, {
encode: encodeURIComponent,
})(pathMatch.params);
return destinationUrl.toString();
}
return compile(destination, {
encode: encodeURIComponent,
})(pathMatch.params);
}

const BLOG_URL = "https://with-zones-blog.vercel.app";

describe("next.config.js test", () => {
describe("rewrites", () => {
let rewrites: Awaited<ReturnType<NonNullable<typeof nextConfig.rewrites>>>;

beforeAll(async () => {
process.env.BLOG_URL = BLOG_URL;
rewrites = await nextConfig.rewrites!();
});

function getRewrittenUrl(path: string): string | undefined {
const allRewrites =
"beforeFiles" in rewrites
? [...rewrites.beforeFiles, ...rewrites.afterFiles]
: rewrites;
for (const rewrite of allRewrites) {
if (rewrite.has?.length) {
continue;
}
const rewriteMatch = match(rewrite.source)(path);
if (rewriteMatch) {
return getDestination(rewrite.destination, rewriteMatch);
}
}
return undefined;
}

it("non blog pages are not rewritten", () => {
expect(getRewrittenUrl("/")).toEqual(undefined);
expect(getRewrittenUrl("/blog-not")).toEqual(undefined);
expect(getRewrittenUrl("/blog2")).toEqual(undefined);
});

it("/blog is rewritten to child zone", () => {
expect(getRewrittenUrl("/blog")).toEqual(`${BLOG_URL}/blog`);
expect(getRewrittenUrl("/blog/post/1")).toEqual(
`${BLOG_URL}/blog/post/1`,
);
});
});
});
9 changes: 7 additions & 2 deletions examples/with-zones/home/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

0 comments on commit 70a023b

Please sign in to comment.