Skip to content

Commit

Permalink
[Next.js] Better Vercel deployment (#6441)
Browse files Browse the repository at this point in the history
Co-authored-by: Steve Piercy <[email protected]>
  • Loading branch information
sneridagh and stevepiercy authored Oct 26, 2024
1 parent 715503d commit f576c13
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 46 deletions.
95 changes: 63 additions & 32 deletions apps/nextjs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Plone on Next.js

This is a proof of concept of a [Next.js](https://nextjs.org) app, using the app router and the `@plone/client` and `@plone/components` library. This is intended to serve as both a playground for the development of both packages and as demo of Plone using Next.js.
This is a proof of concept of a [Next.js](https://nextjs.org) app, using the app router and the `@plone/client` and `@plone/components` library.
This is intended to serve as both a playground for the development of both packages and as a demo of Plone using Next.js.

> [!WARNING]
> This package or app is experimental.
Expand All @@ -9,7 +10,7 @@ This is a proof of concept of a [Next.js](https://nextjs.org) app, using the app
## Development

To start, from the root of the monorepo:
To start, from the root of the monorepo, issue the following commands in a shell session.

```shell
pnpm install
Expand All @@ -25,65 +26,93 @@ make backend-docker-start

## Deployment at Vercel


We introduce an environment variable `API_SERVER_URL`.
You need to create this environment variable in the Vercel deployment's control panel, specifying the URL where your backend API server is deployed, and the route where the API is located, as shown.
For deploying your app at Vercel, you need to create the environment variable `API_SERVER_URL` in Vercel's deployment control panel, specifying the URL where your backend API server is deployed, and the route where the API is located, as shown.

```shell
API_SERVER_URL=https://my-server-DNS-name.tld/api
```

For production deployments, you will need to force the deployment URL, otherwise you will have issues with CORS.
To do so, set another environment variable for the production URL, `NEXT_PRODUCTION_URL`.
This URL needs to be scheme-less, without `http` or `https`, and consist only of the domain name:

```shell
NEXT_PRODUCTION_URL=my-nextjs-production-DNS-name.tld
```

### Application rewrite configuragtion

To avoid issues with CORS and maintain the server counterpart private, our Next.js app should have a rewrite, configured as follows:
To avoid issues with CORS and maintain the server counterpart private, your Next.js app should have a rewrite, configured as follows:

```jsx
const nextConfig = {
// Rewrite to the backend to avoid CORS
async rewrites() {
const apiServerURL =
process.env.API_SERVER_URL ||
'http://localhost:8080/Plone/%2B%2Bapi%2B%2B';
let apiServerURL, vhmRewriteRule;
if (
process.env.API_SERVER_URL &&
(process.env.NEXT_PRODUCTION_URL || process.env.NEXT_PUBLIC_VERCEL_URL)
) {
// We are in Vercel
apiServerURL = process.env.API_SERVER_URL;
vhmRewriteRule = `/VirtualHostBase/https/${
process.env.NEXT_PRODUCTION_URL
? // We are in the production deployment
process.env.NEXT_PRODUCTION_URL
: // We are in the preview deployment
process.env.NEXT_PUBLIC_VERCEL_URL
}%3A443/Plone/%2B%2Bapi%2B%2B/VirtualHostRoot`;
} else if (process.env.API_SERVER_URL) {
// We are in development
apiServerURL = process.env.API_SERVER_URL;
vhmRewriteRule =
'/VirtualHostBase/http/localhost%3A3000/Plone/%2B%2Bapi%2B%2B/VirtualHostRoot';
} else {
// We are in development and the API_SERVER_URL is not set, so we use a local backend
apiServerURL = 'http://localhost:8080';
vhmRewriteRule =
'/VirtualHostBase/http/localhost%3A3000/Plone/%2B%2Bapi%2B%2B/VirtualHostRoot';
}

return [
{
source: '/\\+\\+api\\+\\+/:slug*',
destination:
`${apiServerURL}/VirtualHostBase/https/${process.env.NEXT_PUBLIC_VERCEL_URL}%3A443/Plone/%2B%2Bapi%2B%2B/VirtualHostRoot/:slug*`,
`${apiServerURL}${vhmRewriteRule}/:slug*`,
},
];
},
};
```

Plone Client uses the `++api++` prefix as default, so we should create a redirect in our app pointing to the API server, but using Plone's traditional virtual host management configuration.
Plone Client uses the `++api++` prefix as default, so you should create a redirect in your app pointing to the API server, but using Plone's traditional virtual host management configuration.

Next.js rewrites are picky on the `destination` field, because its rewrite library does not support URLs with regular expression operators.
Therefore, we can't use the usual `++api++` route for the rewrite.
This will allow us to infer the current server URL—even in deployed branches and pull requests—without touching the rewrite rules.
We will fallback to configure a `api` route in our reverse proxy of choice.
Next.js rewrites are picky with the `destination` field, because its rewrite library does not support URLs with regular expression operators.
Therefore, you can't use the usual `++api++` route for the rewrite.
This will allow you to infer the current server URL—even in deployed branches and pull requests—without touching the rewrite rules.
You will fallback to configure a `api` route in your reverse proxy of choice.

### Plone backend

You have to deploy the Plone backend elsewhere, since Vercel is serverless oriented.
We need to set up the rewrite rule in Next.js's `rewrite` feature as shown in the previous section.
You need to set up the rewrite rule in Next.js's `rewrite` feature as shown in the previous section.

We will fallback to configure an `api` route in our reverse proxy of choice.
You will fallback to configure an `api` route in your reverse proxy of choice.

For example, if we use `traefik`:
For example, if you use `traefik`:

```yaml
## VHM rewrite /api/ (Plone Next.js)
- "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.regex=^/api($$|/.*)"
## We remove the incoming /api and just use the path
- "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.replacement=$$1"

## /api router
- traefik.http.routers.rt-backend-api.rule=Host(`my_server_DNS_name`) && PathPrefix(`/api`)
- traefik.http.routers.rt-backend-api.entrypoints=https
- traefik.http.routers.rt-backend-api.tls=true
- traefik.http.routers.rt-backend-api.service=svc-backend
- traefik.http.routers.rt-backend-api.middlewares=gzip,mw-backend-vhm-api
## VHM rewrite /api/ (Plone Next.js)
- "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.regex=^/api($$|/.*)"
## We remove the incoming /api and just use the path
- "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.replacement=$$1"

## /api router
- traefik.http.routers.rt-backend-api.rule=Host(`my_server_DNS_name`) && PathPrefix(`/api`)
- traefik.http.routers.rt-backend-api.entrypoints=https
- traefik.http.routers.rt-backend-api.tls=true
- traefik.http.routers.rt-backend-api.service=svc-backend
- traefik.http.routers.rt-backend-api.middlewares=gzip,mw-backend-vhm-api
```
## About this app
Expand All @@ -100,16 +129,18 @@ pnpm dev

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
You can start editing the page by modifying `app/page.tsx`.
The page auto-updates as you edit the file.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and its API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/).
Your feedback and contributions are welcome!

## Deploy on Vercel

Expand Down
27 changes: 18 additions & 9 deletions apps/nextjs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import path from 'path';
// import path from 'path';

/** @type {import('next').NextConfig} */
const nextConfig = {
Expand All @@ -21,17 +21,26 @@ const nextConfig = {
// Rewrite to the backend to avoid CORS
async rewrites() {
let apiServerURL, vhmRewriteRule;
if (process.env.API_SERVER_URL) {
apiServerURL = process.env.API_SERVER_URL;
vhmRewriteRule = `/VirtualHostBase/https/${process.env.NEXT_PUBLIC_VERCEL_URL}%3A443/Plone/%2B%2Bapi%2B%2B/VirtualHostRoot`;
} else if (
if (
process.env.API_SERVER_URL &&
!process.env.NEXT_PUBLIC_VERCEL_URL
(process.env.NEXT_PRODUCTION_URL || process.env.NEXT_PUBLIC_VERCEL_URL)
) {
throw new Error(
'API_SERVER_URL set and NEXT_PUBLIC_VERCEL_URL not present.',
);
// We are in Vercel
apiServerURL = process.env.API_SERVER_URL;
vhmRewriteRule = `/VirtualHostBase/https/${
process.env.NEXT_PRODUCTION_URL
? // We are in the production deployment
process.env.NEXT_PRODUCTION_URL
: // We are in the preview deployment
process.env.NEXT_PUBLIC_VERCEL_URL
}%3A443/Plone/%2B%2Bapi%2B%2B/VirtualHostRoot`;
} else if (process.env.API_SERVER_URL) {
// We are in development
apiServerURL = process.env.API_SERVER_URL;
vhmRewriteRule =
'/VirtualHostBase/http/localhost%3A3000/Plone/%2B%2Bapi%2B%2B/VirtualHostRoot';
} else {
// We are in development and the API_SERVER_URL is not set, so we use a local backend
apiServerURL = 'http://localhost:8080';
vhmRewriteRule =
'/VirtualHostBase/http/localhost%3A3000/Plone/%2B%2Bapi%2B%2B/VirtualHostRoot';
Expand Down
21 changes: 16 additions & 5 deletions apps/nextjs/src/app/config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import config from '@plone/registry';
import type { ConfigType } from '@plone/registry';
import { slate } from '@plone/blocks';
import { blocksConfig } from '@plone/blocks';

const settings = {
apiPath: process.env.NEXT_PUBLIC_VERCEL_URL
? // Vercel does not prepend the schema to the NEXT_PUBLIC_VERCEL_URL automatic env var
`https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: 'http://localhost:3000',
const settings: Partial<ConfigType['settings']> = {
slate,
};

if (process.env.NEXT_PUBLIC_VERCEL_URL) {
// This app is at Vercel
if (process.env.NEXT_PRODUCTION_URL) {
// This app is in a production deployment, so set the apiPath to the production URL
settings.apiPath = process.env.NEXT_PRODUCTION_URL;
} else {
// This app is in a preview deployment, so set the apiPath to the Vercel URL
settings.apiPath = `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
}
} else {
// This app is in development, so set the apiPath to localhost
settings.apiPath = 'http://localhost:3000/';
}

// @ts-expect-error Improve typings
config.set('settings', settings);

Expand Down

0 comments on commit f576c13

Please sign in to comment.