Skip to content

Commit 084add5

Browse files
make site work with the Cloudflare OpenNext adapter
update the site application so that it can be build using the Cloudflare OpenNext adapter (`@opennextjs/cloudflare`) and thus deployed on Cloudflare Workers
1 parent bf334d9 commit 084add5

File tree

17 files changed

+4746
-974
lines changed

17 files changed

+4746
-974
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,11 @@ cache
3535
tsconfig.tsbuildinfo
3636

3737
dist/
38+
39+
# Ignore the blog-data json that we generate during dev and build
40+
apps/site/public/blog-data.json
41+
42+
# Cloudflare Build Output
43+
apps/site/.open-next
44+
apps/site/.wrangler
45+

CONTRIBUTING.md

+37
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Thank you for your interest in contributing to the Node.js Website. Before you p
77
- [Becoming a collaborator](#becoming-a-collaborator)
88
- [Getting started](#getting-started)
99
- [CLI Commands](#cli-commands)
10+
- [Cloudflare Deployment](#cloudflare-deployment)
1011
- [Commit Guidelines](#commit-guidelines)
1112
- [Pull Request Policy](#pull-request-policy)
1213
- [Developer's Certificate of Origin 1.1](#developers-certificate-of-origin-11)
@@ -165,6 +166,42 @@ This repository contains several scripts and commands for performing numerous ta
165166

166167
</details>
167168

169+
## Cloudflare Deployment
170+
171+
The Node.js Website can be deployed to the [Cloudflare](https://www.cloudflare.com) network using [Cloudflare Workers](https://www.cloudflare.com/en-gb/developer-platform/products/workers/) and the [OpenNext Cloudflare adapter](https://opennext.js.org/cloudflare). This section provides the necessary details for testing and deploying the website on Cloudflare.
172+
173+
### Scripts
174+
175+
Preview and deployment of the website targeting the Cloudflare network is implemented via the following two commands:
176+
177+
- `pnpm cloudflare:preview` builds the website using the OpenNext Cloudflare adapter and runs the website locally in a server simulating the Cloudflare hosting (using the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/))
178+
- `pnpm cloudflare:deploy` builds the website using the OpenNext Cloudflare adapter and deploys the website to the Cloudflare network (using the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/))
179+
180+
### Configurations
181+
182+
There are two key configuration files related to Cloudflare deployment.
183+
184+
#### Wrangler Configuration
185+
186+
This file defines the settings for the Cloudflare Worker, which serves the website.
187+
188+
For more details, refer to the [Wrangler documentation](https://developers.cloudflare.com/workers/wrangler/configuration/).
189+
190+
Key configurations include:
191+
192+
- `main`: Points to the worker generated by the OpenNext adapter.
193+
- `account_id`: Specifies the Cloudflare account ID. This is not required for local previews but is necessary for deployments. You can obtain an account ID for free by signing up at [dash.cloudflare.com](https://dash.cloudflare.com/login).
194+
- `build`: Defines the build command to generate Node.js filesystem polyfills required for the application to run on Cloudflare Workers. This uses the [`@flarelabs/wrangler-build-time-fs-assets-polyfilling`](https://github.com/flarelabs-net/wrangler-build-time-fs-assets-polyfilling) package.
195+
- `alias`: Maps aliases for the Node.js filesystem polyfills generated during the build process.
196+
- `kv_namespaces`: Contains a single KV binding definition for `NEXT_CACHE_WORKERS_KV`. This is used to implement the Next.js incremental cache. For deployments, you can create a new KV namespace in the Cloudflare dashboard and update the binding ID accordingly.
197+
198+
#### OpenNext Configuration
199+
200+
This is the configuration for the OpenNext Cloudflare adapter, for more details on such configuration please refer to the [official OpenNext documentation](https://opennext.js.org/cloudflare/get-started#4-add-an-open-nextconfigts-file).
201+
202+
The configuration present here is very standard and simply sets up incremental cache via the KV binding
203+
defined in the wrangler configuration file.
204+
168205
## Commit Guidelines
169206

170207
This project follows the [Conventional Commits][] specification.

apps/site/.stylelintignore

+4
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ lcov.info
1313

1414
# Old Styles
1515
styles/old
16+
17+
# Cloudflare Build Output
18+
.open-next
19+
.wrangler

apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts

-48
This file was deleted.

apps/site/layouts/Blog.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import type { BlogCategory } from '#site/types';
1111

1212
import styles from './layouts.module.css';
1313

14-
const getBlogCategory = async (pathname: string) => {
14+
const getBlogCategory = (pathname: string) => {
1515
// pathname format can either be: /en/blog/{category}
1616
// or /en/blog/{category}/page/{page}
1717
// hence we attempt to interpolate the full /en/blog/{category}/page/{page}
1818
// and in case of course no page argument is provided we define it to 1
1919
// note that malformed routes can't happen as they are all statically generated
2020
const [, , category = 'all', , page = 1] = pathname.split('/');
2121

22-
const { posts, pagination } = await getBlogData(
22+
const { posts, pagination } = getBlogData(
2323
category as BlogCategory,
2424
Number(page)
2525
);
@@ -38,7 +38,7 @@ const BlogLayout: FC = async () => {
3838
link: `/blog/${category}`,
3939
}));
4040

41-
const blogData = await getBlogCategory(pathname);
41+
const blogData = getBlogCategory(pathname);
4242

4343
return (
4444
<>

apps/site/next-data/blogData.ts

+10-26
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,16 @@
1-
import {
2-
ENABLE_STATIC_EXPORT,
3-
NEXT_DATA_URL,
4-
IS_NOT_VERCEL_RUNTIME_ENV,
5-
} from '#site/next.constants.mjs';
61
import type { BlogCategory, BlogPostsRSC } from '#site/types';
72

8-
const getBlogData = (
9-
cat: BlogCategory,
10-
page?: number
11-
): Promise<BlogPostsRSC> => {
12-
// When we're using Static Exports the Next.js Server is not running (during build-time)
13-
// hence the self-ingestion APIs will not be available. In this case we want to load
14-
// the data directly within the current thread, which will anyways be loaded only once
15-
// We use lazy-imports to prevent `provideBlogData` from executing on import
16-
if (ENABLE_STATIC_EXPORT || IS_NOT_VERCEL_RUNTIME_ENV) {
17-
return import('#site/next-data/providers/blogData').then(
18-
({ provideBlogPosts, providePaginatedBlogPosts }) =>
19-
page ? providePaginatedBlogPosts(cat, page) : provideBlogPosts(cat)
20-
);
21-
}
22-
23-
const fetchURL = `${NEXT_DATA_URL}blog-data/${cat}/${page ?? 0}`;
3+
import {
4+
provideBlogPosts,
5+
providePaginatedBlogPosts,
6+
} from './providers/blogData';
247

25-
// This data cannot be cached because it is continuously updated. Caching it would lead to
26-
// outdated information being shown to the user.
27-
return fetch(fetchURL)
28-
.then(response => response.text())
29-
.then(JSON.parse);
8+
const getBlogData = (cat: BlogCategory, page?: number): BlogPostsRSC => {
9+
return page && page >= 1
10+
? // This allows us to blindly get all blog posts from a given category
11+
// if the page number is 0 or something smaller than 1
12+
providePaginatedBlogPosts(cat, page)
13+
: provideBlogPosts(cat);
3014
};
3115

3216
export default getBlogData;

apps/site/next-data/providers/blogData.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { cache } from 'react';
22

3-
import generateBlogData from '#site/next-data/generators/blogData.mjs';
43
import { BLOG_POSTS_PER_PAGE } from '#site/next.constants.mjs';
4+
import { blogData } from '#site/next.json.mjs';
55
import type { BlogCategory, BlogPostsRSC } from '#site/types';
66

7-
const { categories, posts } = await generateBlogData();
8-
9-
export const provideBlogCategories = cache(() => categories);
7+
const blogPosts = cache(() =>
8+
blogData.posts.map(post => ({
9+
...post,
10+
date: new Date(post.date),
11+
}))
12+
);
1013

1114
export const provideBlogPosts = cache(
1215
(category: BlogCategory): BlogPostsRSC => {
13-
const categoryPosts = posts
16+
const categoryPosts = blogPosts()
1417
.filter(post => post.categories.includes(category))
1518
.sort((a, b) => b.date.getTime() - a.date.getTime());
1619

apps/site/next.dynamic.constants.mjs

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
'use strict';
22

3-
import {
4-
provideBlogCategories,
5-
provideBlogPosts,
6-
} from './next-data/providers/blogData';
3+
import { blogData } from '#site/next.json.mjs';
4+
5+
import { provideBlogPosts } from './next-data/providers/blogData';
76
import { BASE_PATH, BASE_URL } from './next.constants.mjs';
87
import { siteConfig } from './next.json.mjs';
98
import { defaultLocale } from './next.locales.mjs';
@@ -31,9 +30,9 @@ export const IGNORED_ROUTES = [
3130
*/
3231
export const DYNAMIC_ROUTES = new Map([
3332
// Provides Routes for all Blog Categories
34-
...provideBlogCategories().map(c => [`blog/${c}`, 'blog-category']),
33+
...blogData.categories.map(c => [`blog/${c}`, 'blog-category']),
3534
// Provides Routes for all Blog Categories w/ Pagination
36-
...provideBlogCategories()
35+
...blogData.categories
3736
// retrieves the amount of pages for each blog category
3837
.map(c => [c, provideBlogPosts(c).pagination.pages])
3938
// creates a numeric array for each page and define a pathname for

apps/site/next.json.mjs

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import _authors from './authors.json' with { type: 'json' };
44
import _siteNavigation from './navigation.json' with { type: 'json' };
5+
import _blogData from './public/blog-data.json' with { type: 'json' };
56
import _siteRedirects from './redirects.json' with { type: 'json' };
67
import _siteConfig from './site.json' with { type: 'json' };
78

@@ -16,3 +17,6 @@ export const siteRedirects = _siteRedirects;
1617

1718
/** @type {import('./types').SiteConfig} */
1819
export const siteConfig = _siteConfig;
20+
21+
/** @type {import('./types').BlogData} */
22+
export const blogData = _blogData;

apps/site/open-next.config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
2+
import incrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache';
3+
4+
export default defineCloudflareConfig({ incrementalCache });

apps/site/package.json

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
"name": "@node-core/website",
33
"type": "module",
44
"scripts": {
5-
"build": "cross-env NODE_NO_WARNINGS=1 next build --turbopack",
5+
"prebuild": "pnpm build-blog-data",
6+
"build": "cross-env NODE_NO_WARNINGS=1 next build",
67
"check-types": "tsc --noEmit",
78
"deploy": "cross-env NEXT_PUBLIC_STATIC_EXPORT=true NODE_NO_WARNINGS=1 next build",
9+
"predev": "pnpm build-blog-data",
810
"dev": "cross-env NODE_NO_WARNINGS=1 next dev",
911
"lint": "turbo run lint:md lint:snippets lint:js lint:css",
1012
"lint:css": "stylelint \"**/*.css\" --allow-empty-input --cache --cache-strategy=content --cache-location=.stylelintcache",
@@ -18,7 +20,12 @@
1820
"sync-orama": "node ./scripts/orama-search/sync-orama-cloud.mjs",
1921
"test": "turbo test:unit",
2022
"test:unit": "cross-env NODE_NO_WARNINGS=1 node --experimental-test-coverage --test-coverage-exclude=**/*.test.* --experimental-test-module-mocks --enable-source-maps --import=global-jsdom/register --import=tsx --import=tests/setup.jsx --test **/*.test.*",
21-
"test:unit:watch": "cross-env NODE_OPTIONS=\"--watch\" pnpm test:unit"
23+
"test:unit:watch": "cross-env NODE_OPTIONS=\"--watch\" pnpm test:unit",
24+
"build-blog-data": "node ./scripts/blog-data/generate.mjs",
25+
"build-blog-data:watch": "node --watch --watch-path=pages/en/blog ./scripts/blog-data/generate.mjs",
26+
"cloudflare:build:worker": "opennextjs-cloudflare build",
27+
"cloudflare:preview": "pnpm run cloudflare:build:worker && wrangler dev",
28+
"cloudflare:deploy": "pnpm run cloudflare:build:worker && wrangler deploy"
2229
},
2330
"dependencies": {
2431
"@heroicons/react": "~2.2.0",
@@ -76,7 +83,9 @@
7683
"devDependencies": {
7784
"@eslint/compat": "~1.2.8",
7885
"@eslint/eslintrc": "~3.3.1",
86+
"@flarelabs-net/wrangler-build-time-fs-assets-polyfilling": "^0.0.0",
7987
"@next/eslint-plugin-next": "15.3.1",
88+
"@opennextjs/cloudflare": "^1.0.0-beta.4",
8089
"@testing-library/user-event": "~14.6.1",
8190
"@types/semver": "~7.7.0",
8291
"eslint-config-next": "15.3.1",
@@ -107,7 +116,8 @@
107116
"typescript": "~5.8.2",
108117
"typescript-eslint": "~8.31.1",
109118
"unified": "^11.0.5",
110-
"user-agent-data-types": "0.4.2"
119+
"user-agent-data-types": "0.4.2",
120+
"wrangler": "^4.13.0"
111121
},
112122
"imports": {
113123
"#site/*": [
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { writeFileSync } from 'node:fs';
2+
3+
import generateBlogData from '../../next-data/generators/blogData.mjs';
4+
5+
const blogData = await generateBlogData();
6+
7+
writeFileSync(
8+
new URL(`../../public/blog-data.json`, import.meta.url),
9+
JSON.stringify(blogData),
10+
'utf8'
11+
);

apps/site/turbo.json

+27-1
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,39 @@
123123
"cache": false
124124
},
125125
"test:unit": {
126+
"dependsOn": ["build-blog-data"],
126127
"inputs": [
127128
"{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx,mjs}",
128129
"{app,components,layouts,pages,styles}/**/*.css",
129130
"{next-data,scripts,i18n}/**/*.{mjs,json}",
130131
"{app,pages}/**/*.{mdx,md}",
131132
"*.{md,mdx,json,ts,tsx,mjs,yml}"
132-
]
133+
],
134+
"outputs": ["coverage/**", "junit.xml"]
135+
},
136+
"build-blog-data": {
137+
"inputs": ["{pages}/**/*.{mdx,md}"],
138+
"outputs": ["public/blog-data.json"]
139+
},
140+
"cloudflare:preview": {
141+
"inputs": [
142+
"{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}",
143+
"{app,components,layouts,pages,styles}/**/*.css",
144+
"{next-data,scripts,i18n}/**/*.{mjs,json}",
145+
"{app,pages}/**/*.{mdx,md}",
146+
"*.{md,mdx,json,ts,tsx,mjs,yml}"
147+
],
148+
"outputs": [".open-next/**"]
149+
},
150+
"cloudflare:deploy": {
151+
"inputs": [
152+
"{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}",
153+
"{app,components,layouts,pages,styles}/**/*.css",
154+
"{next-data,scripts,i18n}/**/*.{mjs,json}",
155+
"{app,pages}/**/*.{mdx,md}",
156+
"*.{md,mdx,json,ts,tsx,mjs,yml}"
157+
],
158+
"outputs": [".open-next/**"]
133159
}
134160
}
135161
}

apps/site/wrangler.jsonc

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"main": ".open-next/worker.js",
3+
"name": "nodejs-website",
4+
"compatibility_date": "2024-11-07",
5+
"compatibility_flags": ["nodejs_compat"],
6+
"account_id": "8ed4d03ac99f77561d0e8c9cbcc76cb6",
7+
"minify": true,
8+
"keep_names": false,
9+
"assets": {
10+
"directory": ".open-next/assets",
11+
"binding": "ASSETS",
12+
},
13+
"observability": {
14+
"enabled": true,
15+
"head_sampling_rate": 1,
16+
},
17+
"build": {
18+
"command": "wrangler-build-time-fs-assets-polyfilling --assets pages --assets snippets --assets-output-dir .open-next/assets",
19+
},
20+
"alias": {
21+
"node:fs": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts",
22+
"node:fs/promises": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts",
23+
"fs": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts",
24+
"fs/promises": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts",
25+
},
26+
"kv_namespaces": [
27+
{
28+
"binding": "NEXT_CACHE_WORKERS_KV",
29+
"id": "32e8e26d2d2647fd96789baf83209fa9",
30+
},
31+
],
32+
}

0 commit comments

Comments
 (0)