refactor(website,guide): cloudflare workers support (#11204)

This commit is contained in:
Noel
2025-10-25 02:17:59 +02:00
committed by GitHub
parent 756eac6bb1
commit 08b87d9087
62 changed files with 5143 additions and 1709 deletions

View File

@@ -16,8 +16,11 @@ pids
.env*.local
# Dist
.open-next
.next
.wrangler
public/searchIndex
public/readme
src/assets/readme
src/styles/unocss.css

View File

@@ -19,13 +19,6 @@ export default {
fullUrl: true,
},
},
experimental: {
ppr: true,
dynamicOnHover: true,
},
eslint: {
ignoreDuringBuilds: true,
},
reactCompiler: true,
typescript: {
ignoreBuildErrors: true,

View File

@@ -0,0 +1,3 @@
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
export default defineCloudflareConfig();

View File

@@ -5,14 +5,18 @@
"description": "Imagine a bot... the most popular way to build discord bots",
"private": true,
"scripts": {
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
"build:copy_readme": "cpy \"../../packages/(discord.js|brokers|builders|collection|core|formatters|next|proxy|rest|structures|util|voice|ws)/README.md\" \"src/assets/readme\" --rename='home-{{basename}}'",
"build:check": "tsc --noEmit",
"build:local": "cross-env NEXT_PUBLIC_LOCAL_DEV=true pnpm run build:prod",
"build:prod": "pnpm run build:copy_readme && pnpm run build:next",
"build:next": "next build",
"build": "pnpm run build:copy_readme && next build --webpack",
"build:search_indices": "pnpm node scripts/generateAllIndices.js",
"build:analyze": "turbo run docs --filter='@discordjs/*' --concurrency=4 && cross-env ANALYZE=true NEXT_PUBLIC_LOCAL_DEV=true pnpm run build:prod",
"preview": "next start",
"preview:cf": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy:cf": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"dev": "next dev --turbopack",
"lint": "pnpm run build:check && prettier --check . && cross-env TIMING=1 eslint --format=pretty src ",
"format": "pnpm run build:check && prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src ",
@@ -47,11 +51,12 @@
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"@opennextjs/cloudflare": "^1.11.0",
"@radix-ui/react-collapsible": "^1.1.12",
"@react-icons/all-files": "^4.1.0",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query": "^5.90.5",
"@vercel/analytics": "^1.5.0",
"@vercel/edge-config": "^1.4.0",
"@vercel/edge-config": "^1.4.3",
"@vercel/postgres": "^0.10.0",
"cloudflare": "^5.2.0",
"cmdk": "^1.1.1",
@@ -60,13 +65,13 @@
"immer": "^10.1.3",
"jotai": "^2.15.0",
"jotai-immer": "^0.4.1",
"lucide-react": "^0.545.0",
"meilisearch": "^0.50.0",
"motion": "^12.23.22",
"next": "15.6.0-canary.45",
"next-mdx-remote-client": "^2.1.6",
"lucide-react": "^0.548.0",
"meilisearch": "^0.53.0",
"motion": "^12.23.24",
"next": "^16.0.0",
"next-mdx-remote-client": "^2.1.7",
"next-themes": "^0.4.6",
"nuqs": "^2.7.1",
"nuqs": "^2.7.2",
"overlayscrollbars": "^2.12.0",
"overlayscrollbars-react": "^0.5.6",
"react": "^19.2.0",
@@ -74,20 +79,21 @@
"react-aria-components": "^1.13.0",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.0.0",
"safe-mdx": "^1.3.8",
"sharp": "^0.34.4",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.4.0",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@next/env": "^15.5.4",
"@next/env": "^16.0.0",
"@shikijs/rehype": "^3.13.0",
"@tailwindcss/postcss": "^4.1.14",
"@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.14",
"@types/node": "^22.18.8",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@tailwindcss/vite": "^4.1.16",
"@types/node": "^24.9.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"autoprefixer": "^10.4.21",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"cpy-cli": "^6.0.0",
@@ -98,15 +104,16 @@
"git-describe": "^4.1.1",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"prettier-plugin-tailwindcss": "^0.7.1",
"remark-gfm": "^4.0.1",
"remark-rehype": "^11.1.2",
"shiki": "^3.13.0",
"tailwindcss": "^4.1.14",
"tailwindcss": "^4.1.16",
"tailwindcss-react-aria-components": "^2.0.1",
"turbo": "^2.5.8",
"typescript": "^5.9.3",
"vercel": "^48.2.1"
"vercel": "^48.2.1",
"wrangler": "^4.45.0"
},
"engines": {
"node": ">=22.12.0"

View File

@@ -0,0 +1,2 @@
/_next/static/*
Cache-Control: public,max-age=31536000,immutable

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -3,8 +3,6 @@
import { ImageResponse } from 'next/og';
import { resolveKind } from '@/util/resolveNodeKind';
export const runtime = 'edge';
export const size = {
width: 1_200,
height: 630,
@@ -12,6 +10,22 @@ export const size = {
export const contentType = 'image/png';
async function loadGoogleFont(font: string, text: string) {
const url = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`;
const css = await (await fetch(url)).text();
// eslint-disable-next-line prefer-named-capture-group
const resource = /src: url\((.+)\) format\('(opentype|truetype)'\)/.exec(css);
if (resource) {
const response = await fetch(resource[1]!);
if (response.status === 200) {
return response.arrayBuffer();
}
}
throw new Error('failed to load font data');
}
export default async function Image({
params,
}: {
@@ -19,14 +33,6 @@ export default async function Image({
}) {
const { item, packageName, version } = await params;
const [fontDataBold, fontDataBlack] = await Promise.all([
fetch(new URL('../../../../../../assets/Geist-Bold.ttf', import.meta.url), {
next: { revalidate: 604_800 },
}).then(async (res) => res.arrayBuffer()),
fetch(new URL('../../../../../../assets/Geist-Black.ttf', import.meta.url), {
next: { revalidate: 604_800 },
}).then(async (res) => res.arrayBuffer()),
]);
const normalizeItem = item.split(encodeURIComponent(':')).join('.').toLowerCase();
const isMain = version === 'main';
@@ -107,13 +113,13 @@ export default async function Image({
fonts: [
{
name: 'Geist',
data: fontDataBold,
data: await loadGoogleFont('Geist:wght@700', node.displayName),
weight: 700,
style: 'normal',
},
{
name: 'Geist',
data: fontDataBlack,
data: await loadGoogleFont('Geist:wght@900', node.displayName),
weight: 900,
style: 'normal',
},

View File

@@ -1,15 +1,12 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import remarkGfm from 'remark-gfm';
import { SafeMdxRenderer } from 'safe-mdx';
import { mdxParse } from 'safe-mdx/parse';
import { DocItem } from '@/components/DocItem';
import { PACKAGES_WITH_ENTRY_POINTS } from '@/util/constants';
import { SyntaxHighlighter } from '@/components/SyntaxHighlighter';
// import { PACKAGES_WITH_ENTRY_POINTS } from '@/util/constants';
import { fetchNode } from '@/util/fetchNode';
import { parseDocsPathParams } from '@/util/parseDocsPathParams';
import { getSingletonHighlighter } from '@/util/shiki.bundle';
export async function generateMetadata({
params,
@@ -52,48 +49,38 @@ export default async function Page({
const { entryPoints: parsedEntrypoints, foundItem } = parseDocsPathParams(item);
if (!foundItem) {
const hasEntryPoint = PACKAGES_WITH_ENTRY_POINTS.includes(packageName);
// const hasEntryPoint = PACKAGES_WITH_ENTRY_POINTS.includes(packageName);
if (hasEntryPoint) {
return <>Placeholder</>;
}
// if (hasEntryPoint) {
// return <>Placeholder</>;
// }
let fileContent: string;
try {
fileContent = await readFile(join(process.cwd(), `src/assets/readme/${packageName}/home-README.md`), 'utf8');
} catch (error: any) {
if ('code' in error && error.code === 'ENOENT') {
notFound();
}
throw error;
fileContent = await fetch(`${process.env.CF_R2_README_BUCKET_URL}/${packageName}/home-README.md`).then(
async (res) => res.text(),
);
} catch {
notFound();
}
const mdast = mdxParse(fileContent);
return (
<div className="prose prose-neutral dark:prose-invert prose-a:[&>img]:inline-block prose-a:[&>img]:m-0 prose-a:[&>img[height='44']]:h-11 prose-p:my-2 prose-pre:py-3 prose-pre:rounded-sm prose-pre:px-0 prose-pre:border prose-pre:border-[#d4d4d4] dark:prose-pre:border-[#404040] prose-code:font-normal prose-a:text-[#5865F2] prose-a:no-underline prose-a:hover:text-[#3d48c3] dark:prose-a:hover:text-[#7782fa] mx-auto max-w-screen-xl px-6 py-6 break-words [&_code_span:last-of-type:empty]:hidden [&_div[align='center']_p_a+a]:ml-2">
<MDXRemote
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
[
rehypeShikiFromHighlighter,
await getSingletonHighlighter({
langs: ['typescript', 'javascript', 'shellscript'],
themes: ['github-light', 'github-dark-dimmed'],
}),
{
themes: {
light: 'github-light',
dark: 'github-dark-dimmed',
},
},
],
],
},
<SafeMdxRenderer
markdown={fileContent}
mdast={mdast}
renderNode={(node) => {
if (node.type === 'code') {
const language = node.lang ?? 'text';
return <SyntaxHighlighter code={node.value} lang={language} />;
}
return undefined;
}}
source={fileContent}
/>
</div>
);

View File

@@ -2,8 +2,6 @@
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export const size = {
width: 1_200,
height: 630,
@@ -11,11 +9,23 @@ export const size = {
export const contentType = 'image/png';
export default async function Image() {
const fontData = await fetch(new URL('../assets/Geist-Black.ttf', import.meta.url), { cache: 'force-cache' }).then(
async (res) => res.arrayBuffer(),
);
async function loadGoogleFont(font: string, text: string) {
const url = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`;
const css = await (await fetch(url)).text();
// eslint-disable-next-line prefer-named-capture-group
const resource = /src: url\((.+)\) format\('(opentype|truetype)'\)/.exec(css);
if (resource) {
const response = await fetch(resource[1]!);
if (response.status === 200) {
return response.arrayBuffer();
}
}
throw new Error('failed to load font data');
}
export default async function Image() {
return new ImageResponse(
(
<div tw="flex bg-[#121214] h-full w-full">
@@ -39,7 +49,7 @@ export default async function Image() {
fonts: [
{
name: 'Geist',
data: fontData,
data: await loadGoogleFont('Geist:wght@900', 'The most popular way to build Discord bots.'),
weight: 900,
style: 'normal',
},

View File

@@ -43,12 +43,12 @@ export function CmdK({ dependencies }: { readonly dependencies: string[] }) {
value={item.id}
>
{resolveKind(item.kind)}
<div className="flex flex-grow flex-col">
<div className="flex grow flex-col">
<span className="font-semibold wrap-anywhere">{item.name}</span>
<span className={cx('truncate text-sm', isMobile ? 'max-w-[30ch]' : 'max-w-[40ch]')}>{item.summary}</span>
<span className={cx('truncate text-xs', isMobile ? 'max-w-[30ch]' : 'max-w-[40ch]')}>{item.path}</span>
</div>
<ArrowRight aria-hidden className="flex-shrink-0" />
<ArrowRight aria-hidden className="shrink-0" />
</Command.Item>
)) ?? [];

View File

@@ -1,9 +1,9 @@
import Cloudflare from 'cloudflare';
// import Cloudflare from 'cloudflare';
import { ENV } from './env';
const client = new Cloudflare({
apiToken: process.env.CF_D1_DOCS_API_KEY,
});
// const client = new Cloudflare({
// apiToken: process.env.CF_D1_DOCS_API_KEY,
// });
export async function fetchVersions(packageName: string) {
if (ENV.IS_LOCAL_DEV) {
@@ -11,13 +11,32 @@ export async function fetchVersions(packageName: string) {
}
try {
const { result } = await client.d1.database.query(process.env.CF_D1_DOCS_ID!, {
account_id: process.env.CF_ACCOUNT_ID!,
sql: `select version from documentation where name = ? order by version desc;`,
params: [packageName],
});
// const { result } = await client.d1.database.query(process.env.CF_D1_DOCS_ID!, {
// account_id: process.env.CF_ACCOUNT_ID!,
// sql: `select version from documentation where name = ? order by version desc;`,
// params: [packageName],
// });
return (result[0]?.results as { version: string }[] | undefined) ?? [];
// return (result[0]?.results as { version: string }[] | undefined) ?? [];
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID}/d1/database/${process.env.CF_D1_DOCS_ID}/query`,
{
headers: {
Authorization: `Bearer ${process.env.CF_D1_DOCS_API_KEY}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
sql: `select version from documentation where name = ? order by version desc;`,
params: [packageName],
}),
},
);
const data = await response.json();
return data.result[0]?.results;
} catch {
return [];
}

View File

@@ -0,0 +1,19 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "discordjs-website",
"keep_names": false,
"compatibility_date": "2025-10-04",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS",
},
"observability": {
"logs": {
"enabled": true,
"head_sampling_rate": 1,
"invocation_logs": true,
},
},
}