Skip to content

Make site work with the Cloudflare OpenNext adapter #7383

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,11 @@ cache
tsconfig.tsbuildinfo

dist/

# Ignore the blog-data json that we generate during dev and build
apps/site/public/blog-data.json

# Cloudflare Build Output
apps/site/.open-next
apps/site/.wrangler

37 changes: 37 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Thank you for your interest in contributing to the Node.js Website. Before you p
- [Becoming a collaborator](#becoming-a-collaborator)
- [Getting started](#getting-started)
- [CLI Commands](#cli-commands)
- [Cloudflare Deployment](#cloudflare-deployment)
- [Commit Guidelines](#commit-guidelines)
- [Pull Request Policy](#pull-request-policy)
- [Developer's Certificate of Origin 1.1](#developers-certificate-of-origin-11)
Expand Down Expand Up @@ -165,6 +166,42 @@ This repository contains several scripts and commands for performing numerous ta

</details>

## Cloudflare Deployment

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.

### Scripts

Preview and deployment of the website targeting the Cloudflare network is implemented via the following two commands:

- `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/))
- `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/))

### Configurations

There are two key configuration files related to Cloudflare deployment.

#### Wrangler Configuration

This file defines the settings for the Cloudflare Worker, which serves the website.

For more details, refer to the [Wrangler documentation](https://developers.cloudflare.com/workers/wrangler/configuration/).

Key configurations include:

- `main`: Points to the worker generated by the OpenNext adapter.
- `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).
- `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.
- `alias`: Maps aliases for the Node.js filesystem polyfills generated during the build process.
- `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.

#### OpenNext Configuration

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).

The configuration present here is very standard and simply sets up incremental cache via the KV binding
defined in the wrangler configuration file.

## Commit Guidelines

This project follows the [Conventional Commits][] specification.
Expand Down
4 changes: 4 additions & 0 deletions apps/site/.stylelintignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ lcov.info

# Old Styles
styles/old

# Cloudflare Build Output
.open-next
.wrangler

This file was deleted.

6 changes: 3 additions & 3 deletions apps/site/layouts/Blog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import type { BlogCategory } from '#site/types';

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

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

const { posts, pagination } = await getBlogData(
const { posts, pagination } = getBlogData(
category as BlogCategory,
Number(page)
);
Expand All @@ -38,7 +38,7 @@ const BlogLayout: FC = async () => {
link: `/blog/${category}`,
}));

const blogData = await getBlogCategory(pathname);
const blogData = getBlogCategory(pathname);

return (
<>
Expand Down
36 changes: 10 additions & 26 deletions apps/site/next-data/blogData.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
import {
ENABLE_STATIC_EXPORT,
NEXT_DATA_URL,
IS_NOT_VERCEL_RUNTIME_ENV,
} from '#site/next.constants.mjs';
import type { BlogCategory, BlogPostsRSC } from '#site/types';

const getBlogData = (
cat: BlogCategory,
page?: number
): Promise<BlogPostsRSC> => {
// When we're using Static Exports the Next.js Server is not running (during build-time)
// hence the self-ingestion APIs will not be available. In this case we want to load
// the data directly within the current thread, which will anyways be loaded only once
// We use lazy-imports to prevent `provideBlogData` from executing on import
if (ENABLE_STATIC_EXPORT || IS_NOT_VERCEL_RUNTIME_ENV) {
return import('#site/next-data/providers/blogData').then(
({ provideBlogPosts, providePaginatedBlogPosts }) =>
page ? providePaginatedBlogPosts(cat, page) : provideBlogPosts(cat)
);
}

const fetchURL = `${NEXT_DATA_URL}blog-data/${cat}/${page ?? 0}`;
import {
provideBlogPosts,
providePaginatedBlogPosts,
} from './providers/blogData';

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

export default getBlogData;
13 changes: 8 additions & 5 deletions apps/site/next-data/providers/blogData.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { cache } from 'react';

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

const { categories, posts } = await generateBlogData();

export const provideBlogCategories = cache(() => categories);
const blogPosts = cache(() =>
blogData.posts.map(post => ({
...post,
date: new Date(post.date),
}))
);

export const provideBlogPosts = cache(
(category: BlogCategory): BlogPostsRSC => {
const categoryPosts = posts
const categoryPosts = blogPosts()
.filter(post => post.categories.includes(category))
.sort((a, b) => b.date.getTime() - a.date.getTime());

Expand Down
11 changes: 5 additions & 6 deletions apps/site/next.dynamic.constants.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
'use strict';

import {
provideBlogCategories,
provideBlogPosts,
} from './next-data/providers/blogData';
import { blogData } from '#site/next.json.mjs';

import { provideBlogPosts } from './next-data/providers/blogData';
import { BASE_PATH, BASE_URL } from './next.constants.mjs';
import { siteConfig } from './next.json.mjs';
import { defaultLocale } from './next.locales.mjs';
Expand Down Expand Up @@ -31,9 +30,9 @@ export const IGNORED_ROUTES = [
*/
export const DYNAMIC_ROUTES = new Map([
// Provides Routes for all Blog Categories
...provideBlogCategories().map(c => [`blog/${c}`, 'blog-category']),
...blogData.categories.map(c => [`blog/${c}`, 'blog-category']),
// Provides Routes for all Blog Categories w/ Pagination
...provideBlogCategories()
...blogData.categories
// retrieves the amount of pages for each blog category
.map(c => [c, provideBlogPosts(c).pagination.pages])
// creates a numeric array for each page and define a pathname for
Expand Down
4 changes: 4 additions & 0 deletions apps/site/next.json.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import _authors from './authors.json' with { type: 'json' };
import _siteNavigation from './navigation.json' with { type: 'json' };
import _blogData from './public/blog-data.json' with { type: 'json' };
import _siteRedirects from './redirects.json' with { type: 'json' };
import _siteConfig from './site.json' with { type: 'json' };

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

/** @type {import('./types').SiteConfig} */
export const siteConfig = _siteConfig;

/** @type {import('./types').BlogData} */
export const blogData = _blogData;
4 changes: 4 additions & 0 deletions apps/site/open-next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
import incrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache';

export default defineCloudflareConfig({ incrementalCache });
16 changes: 13 additions & 3 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
"name": "@node-core/website",
"type": "module",
"scripts": {
"build": "cross-env NODE_NO_WARNINGS=1 next build --turbopack",
"prebuild": "pnpm build-blog-data",
"build": "cross-env NODE_NO_WARNINGS=1 next build",
"check-types": "tsc --noEmit",
"deploy": "cross-env NEXT_PUBLIC_STATIC_EXPORT=true NODE_NO_WARNINGS=1 next build",
"predev": "pnpm build-blog-data",
"dev": "cross-env NODE_NO_WARNINGS=1 next dev",
"lint": "turbo run lint:md lint:js lint:css",
"lint:css": "stylelint \"**/*.css\" --allow-empty-input --cache --cache-strategy=content --cache-location=.stylelintcache",
Expand All @@ -17,7 +19,12 @@
"sync-orama": "node ./scripts/orama-search/sync-orama-cloud.mjs",
"test": "turbo test:unit",
"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.*",
"test:unit:watch": "cross-env NODE_OPTIONS=\"--watch\" pnpm test:unit"
"test:unit:watch": "cross-env NODE_OPTIONS=\"--watch\" pnpm test:unit",
"build-blog-data": "node ./scripts/blog-data/generate.mjs",
"build-blog-data:watch": "node --watch --watch-path=pages/en/blog ./scripts/blog-data/generate.mjs",
"cloudflare:build:worker": "opennextjs-cloudflare build",
"cloudflare:preview": "pnpm run cloudflare:build:worker && wrangler dev",
"cloudflare:deploy": "pnpm run cloudflare:build:worker && wrangler deploy"
},
"dependencies": {
"@heroicons/react": "~2.2.0",
Expand Down Expand Up @@ -75,7 +82,9 @@
"devDependencies": {
"@eslint/compat": "~1.2.8",
"@eslint/eslintrc": "~3.3.1",
"@flarelabs-net/wrangler-build-time-fs-assets-polyfilling": "^0.0.0",
"@next/eslint-plugin-next": "15.3.1",
"@opennextjs/cloudflare": "^1.0.0-beta.4",
"@testing-library/user-event": "~14.6.1",
"@types/semver": "~7.7.0",
"eslint-config-next": "15.3.1",
Expand Down Expand Up @@ -104,7 +113,8 @@
"tsx": "^4.19.3",
"typescript": "~5.8.2",
"typescript-eslint": "~8.31.1",
"user-agent-data-types": "0.4.2"
"user-agent-data-types": "0.4.2",
"wrangler": "^4.13.0"
},
"imports": {
"#site/*": [
Expand Down
11 changes: 11 additions & 0 deletions apps/site/scripts/blog-data/generate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { writeFileSync } from 'node:fs';

import generateBlogData from '../../next-data/generators/blogData.mjs';

const blogData = await generateBlogData();

writeFileSync(
new URL(`../../public/blog-data.json`, import.meta.url),
JSON.stringify(blogData),
'utf8'
);
28 changes: 27 additions & 1 deletion apps/site/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,39 @@
"cache": false
},
"test:unit": {
"dependsOn": ["build-blog-data"],
"inputs": [
"{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx,mjs}",
"{app,components,layouts,pages,styles}/**/*.css",
"{next-data,scripts,i18n}/**/*.{mjs,json}",
"{app,pages}/**/*.{mdx,md}",
"*.{md,mdx,json,ts,tsx,mjs,yml}"
]
],
"outputs": ["coverage/**", "junit.xml"]
},
"build-blog-data": {
"inputs": ["{pages}/**/*.{mdx,md}"],
"outputs": ["public/blog-data.json"]
},
"cloudflare:preview": {
"inputs": [
"{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}",
"{app,components,layouts,pages,styles}/**/*.css",
"{next-data,scripts,i18n}/**/*.{mjs,json}",
"{app,pages}/**/*.{mdx,md}",
"*.{md,mdx,json,ts,tsx,mjs,yml}"
],
"outputs": [".open-next/**"]
},
"cloudflare:deploy": {
"inputs": [
"{app,components,hooks,i18n,layouts,middlewares,pages,providers,types,util}/**/*.{ts,tsx}",
"{app,components,layouts,pages,styles}/**/*.css",
"{next-data,scripts,i18n}/**/*.{mjs,json}",
"{app,pages}/**/*.{mdx,md}",
"*.{md,mdx,json,ts,tsx,mjs,yml}"
],
"outputs": [".open-next/**"]
}
}
}
32 changes: 32 additions & 0 deletions apps/site/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"main": ".open-next/worker.js",
"name": "nodejs-website",
"compatibility_date": "2024-11-07",
"compatibility_flags": ["nodejs_compat"],
"account_id": "8ed4d03ac99f77561d0e8c9cbcc76cb6",
"minify": true,
"keep_names": false,
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS",
},
"observability": {
"enabled": true,
"head_sampling_rate": 1,
},
"build": {
"command": "wrangler-build-time-fs-assets-polyfilling --assets pages --assets snippets --assets-output-dir .open-next/assets",
},
"alias": {
"node:fs": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts",
"node:fs/promises": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts",
"fs": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs.ts",
"fs/promises": "./.wrangler/fs-assets-polyfilling/polyfills/node/fs/promises.ts",
},
"kv_namespaces": [
{
"binding": "NEXT_CACHE_WORKERS_KV",
"id": "32e8e26d2d2647fd96789baf83209fa9",
},
],
}
Loading
Loading