From 744ae19f0936a50cf8ba06f3b918195858812bea Mon Sep 17 00:00:00 2001 From: Jun Date: Wed, 10 Dec 2025 13:02:21 +1030 Subject: [PATCH 1/4] Node ver. --- nodejs/.gitignore | 6 ++ nodejs/.npmignore | 8 ++ nodejs/LICENSE | 9 ++ nodejs/README.md | 141 +++++++++++++++++++++++++ nodejs/examples/example.js | 32 ++++++ nodejs/examples/test-compatibility.js | 32 ++++++ nodejs/package-lock.json | 142 ++++++++++++++++++++++++++ nodejs/package.json | 45 ++++++++ nodejs/src/index.ts | 123 ++++++++++++++++++++++ nodejs/templates/error.ejs | 110 ++++++++++++++++++++ nodejs/tsconfig.json | 20 ++++ 11 files changed, 668 insertions(+) create mode 100644 nodejs/.gitignore create mode 100644 nodejs/.npmignore create mode 100644 nodejs/LICENSE create mode 100644 nodejs/README.md create mode 100644 nodejs/examples/example.js create mode 100644 nodejs/examples/test-compatibility.js create mode 100644 nodejs/package-lock.json create mode 100644 nodejs/package.json create mode 100644 nodejs/src/index.ts create mode 100644 nodejs/templates/error.ejs create mode 100644 nodejs/tsconfig.json diff --git a/nodejs/.gitignore b/nodejs/.gitignore new file mode 100644 index 0000000..a0a7fe7 --- /dev/null +++ b/nodejs/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.log +*.html +.DS_Store +examples/*.html diff --git a/nodejs/.npmignore b/nodejs/.npmignore new file mode 100644 index 0000000..31b5499 --- /dev/null +++ b/nodejs/.npmignore @@ -0,0 +1,8 @@ +src/ +tsconfig.json +*.ts +!*.d.ts +examples/ +node_modules/ +*.log +.DS_Store diff --git a/nodejs/LICENSE b/nodejs/LICENSE new file mode 100644 index 0000000..8b201ea --- /dev/null +++ b/nodejs/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 Anthony Donlon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/nodejs/README.md b/nodejs/README.md new file mode 100644 index 0000000..3d37467 --- /dev/null +++ b/nodejs/README.md @@ -0,0 +1,141 @@ +# Cloudflare Error Page Generator (Node.js/TypeScript) + +Carbon copy of the original Python version. + +## Installation + +```bash +npm install cloudflare-error-page +``` + +Or install from GitHub: + +```bash +npm install git+https://github.com/donlon/cloudflare-error-page.git#main:nodejs +``` + +## Quick Start + +```typescript +import { render } from 'cloudflare-error-page'; +import * as fs from 'fs'; + +const errorPage = render({ + browser_status: { status: 'ok' }, + cloudflare_status: { status: 'error', status_text: 'Error' }, + host_status: { status: 'ok', location: 'example.com' }, + error_source: 'cloudflare', + what_happened: '

There is an internal server error on Cloudflare\'s network.

', + what_can_i_do: '

Please try again in a few minutes.

', +}); + +fs.writeFileSync('error.html', errorPage); +``` + +## API Reference + +### `render(params: ErrorPageParams, allowHtml?: boolean): string` + +Generates an HTML error page based on the provided parameters. + +#### Parameters + +- `params`: An object containing error page configuration +- `allowHtml` (optional): Whether to allow HTML in `what_happened` and `what_can_i_do` fields. Default: `true` + +#### ErrorPageParams Interface + +```typescript +interface ErrorPageParams { + // Basic information + error_code?: number; // Default: 500 + title?: string; // Default: 'Internal server error' + html_title?: string; // Default: '{error_code}: {title}' + time?: string; // Auto-generated if not provided + ray_id?: string; // Auto-generated if not provided + client_ip?: string; // Default: '1.1.1.1' + + // Status for each component + browser_status?: StatusItem; + cloudflare_status?: StatusItem; + host_status?: StatusItem; + + // Error source indicator + error_source?: 'browser' | 'cloudflare' | 'host'; + + // Content sections + what_happened?: string; // HTML content + what_can_i_do?: string; // HTML content + + // Optional customization + more_information?: MoreInformation; + perf_sec_by?: PerfSecBy; + creator_info?: CreatorInfo; +} + +interface StatusItem { + status?: 'ok' | 'error'; + status_text?: string; // Default: 'Working' or 'Error' + status_text_color?: string; // CSS color + location?: string; + name?: string; +} +``` + +## Examples + +### Basic Error Page + +```typescript +import { render } from 'cloudflare-error-page'; + +const html = render({ + cloudflare_status: { status: 'error' }, + error_source: 'cloudflare', + what_happened: '

Something went wrong.

', + what_can_i_do: '

Try again later.

', +}); +``` + +### Express.js Integration + +```typescript +import express from 'express'; +import { render } from 'cloudflare-error-page'; + +const app = express(); + +app.use((err, req, res, next) => { + const errorPage = render({ + error_code: err.status || 500, + title: err.message || 'Internal server error', + cloudflare_status: { status: 'ok' }, + host_status: { + status: 'error', + location: req.hostname + }, + error_source: 'host', + what_happened: `

${err.message}

`, + what_can_i_do: '

Please try again or contact support.

', + }); + + res.status(err.status || 500).send(errorPage); +}); +``` + +## TypeScript Support + +This package includes full TypeScript type definitions. Import types as needed: + +```typescript +import { render, ErrorPageParams, StatusItem } from 'cloudflare-error-page'; +``` + +## License + +MIT + +## Related + +- [Python version](https://github.com/donlon/cloudflare-error-page) +- [Online Editor](https://virt.moe/cloudflare-error-page/editor/) diff --git a/nodejs/examples/example.js b/nodejs/examples/example.js new file mode 100644 index 0000000..ba428b1 --- /dev/null +++ b/nodejs/examples/example.js @@ -0,0 +1,32 @@ +import { render } from '../dist/index.js'; +import * as fs from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Generate an error page +const errorPage = render({ + browser_status: { + status: 'ok', + }, + cloudflare_status: { + status: 'error', + status_text: 'Error', + }, + host_status: { + status: 'ok', + location: 'example.com', + }, + error_source: 'cloudflare', + + what_happened: '

There is an internal server error on Cloudflare\'s network.

', + what_can_i_do: '

Please try again in a few minutes.

', +}); + +const outputPath = join(__dirname, 'error.html'); +fs.writeFileSync(outputPath, errorPage); + +console.log(`Error page generated: ${outputPath}`); +console.log('Open the file in your browser to view it.'); diff --git a/nodejs/examples/test-compatibility.js b/nodejs/examples/test-compatibility.js new file mode 100644 index 0000000..640c1d0 --- /dev/null +++ b/nodejs/examples/test-compatibility.js @@ -0,0 +1,32 @@ +import { render } from '../dist/index.js'; +import * as fs from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Test all JSON configs from oringinal examples directory +const testConfigs = [ + { file: '../../examples/default.json', output: 'test-default.html' }, + { file: '../../examples/working.json', output: 'test-working.html' }, + { file: '../../examples/catastrophic.json', output: 'test-catastrophic.html' } +]; + +testConfigs.forEach(({ file, output }) => { + try { + const configPath = join(__dirname, file); + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + + console.log(`Testing: ${file}`); + console.log(`Config keys: ${Object.keys(config).join(', ')}`); + + const html = render(config); + + const outputPath = join(__dirname, output); + fs.writeFileSync(outputPath, html); + } catch (error) { + console.error(`Something went wrong: ${file}`); + console.error(`Error: ${error.message}\n`); + } +}); diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json new file mode 100644 index 0000000..6c3a013 --- /dev/null +++ b/nodejs/package-lock.json @@ -0,0 +1,142 @@ +{ + "name": "cloudflare-error-page", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cloudflare-error-page", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "ejs": "^3.1.10" + }, + "devDependencies": { + "@types/ejs": "^3.1.5", + "@types/node": "^20.0.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz", + "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/nodejs/package.json b/nodejs/package.json new file mode 100644 index 0000000..e3e9336 --- /dev/null +++ b/nodejs/package.json @@ -0,0 +1,45 @@ +{ + "name": "cloudflare-error-page", + "version": "0.0.1", + "description": "Cloudflare Error Page Generator", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "tsc", + "prepublishOnly": "pnpm run build", + "example": "node examples/example.js" + }, + "keywords": [ + "cloudflare", + "error-page", + "error", + "http", + "html", + "template" + ], + "author": "Anthony Donlon", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/donlon/cloudflare-error-page.git", + "directory": "nodejs" + }, + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "ejs": "^3.1.10" + }, + "devDependencies": { + "@types/ejs": "^3.1.5", + "@types/node": "^20.0.0", + "typescript": "^5.3.0" + }, + "files": [ + "dist/**/*", + "templates/**/*", + "README.md", + "LICENSE" + ] +} diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts new file mode 100644 index 0000000..6e5565c --- /dev/null +++ b/nodejs/src/index.ts @@ -0,0 +1,123 @@ +import * as fs from "fs"; +import * as path from "path"; +import { fileURLToPath } from "url"; +import * as ejs from "ejs"; +import * as crypto from "crypto"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export interface StatusItem { + status?: "ok" | "error"; + status_text?: string; + status_text_color?: string; + location?: string; + name?: string; +} + +export interface MoreInformation { + hidden?: boolean; + link?: string; + text?: string; + for?: string; +} + +export interface PerfSecBy { + link?: string; + text?: string; +} + +export interface CreatorInfo { + hidden?: boolean; + link?: string; + text?: string; +} + +export interface ErrorPageParams { + error_code?: number; + title?: string; + html_title?: string; + time?: string; + ray_id?: string; + client_ip?: string; + + browser_status?: StatusItem; + cloudflare_status?: StatusItem; + host_status?: StatusItem; + + error_source?: "browser" | "cloudflare" | "host"; + + what_happened?: string; + what_can_i_do?: string; + + more_information?: MoreInformation; + perf_sec_by?: PerfSecBy; + creator_info?: CreatorInfo; +} + +/** + * Fill default parameters if not provided + */ +function fillParams(params: ErrorPageParams): ErrorPageParams { + const filledParams = { ...params }; + + if (!filledParams.time) { + const now = new Date(); + filledParams.time = + now.toISOString().replace("T", " ").substring(0, 19) + " UTC"; + } + + if (!filledParams.ray_id) { + filledParams.ray_id = crypto.randomBytes(8).toString("hex"); + } + + return filledParams; +} + +/** + * Escape HTML special characters + */ +function escapeHtml(text: string): string { + const htmlEscapeMap: Record = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }; + return text.replace(/[&<>"']/g, (char) => htmlEscapeMap[char] || char); +} + +/** + * Render a customized Cloudflare error page + * @param params - The parameters for the error page + * @param allowHtml - Whether to allow HTML in what_happened and what_can_i_do fields (default: true) + * @returns The rendered HTML string + */ +export function render( + params: ErrorPageParams, + allowHtml: boolean = true +): string { + let processedParams = fillParams(params); + + if (!allowHtml) { + processedParams = { ...processedParams }; + if (processedParams.what_happened) { + processedParams.what_happened = escapeHtml(processedParams.what_happened); + } + if (processedParams.what_can_i_do) { + processedParams.what_can_i_do = escapeHtml(processedParams.what_can_i_do); + } + } + + // Load EJS template + const templatePath = path.join(__dirname, "..", "templates", "error.ejs"); + + const template = fs.readFileSync(templatePath, "utf-8"); + + const rendered = ejs.render(template, { params: processedParams }); + + return rendered; +} + +export default render; diff --git a/nodejs/templates/error.ejs b/nodejs/templates/error.ejs new file mode 100644 index 0000000..95aee8b --- /dev/null +++ b/nodejs/templates/error.ejs @@ -0,0 +1,110 @@ +{# Note: This is generated with scripts/inline_resources.py. Please do not edit this file manually. #} + + + + + + +<% const error_code = params.error_code || 500; %> +<% const title = params.title || 'Internal server error'; %> +<% const html_title = params.html_title || (error_code + ': ' + title); %> +<%= html_title %> + + + + + + + + +
+
+
+

+ <%= title %> + Error code <%= error_code %> +

+ <% const more_info = params.more_information || {}; %> + <% if (!more_info.hidden) { %> +
+ Visit <%= more_info.text || 'cloudflare.com' %> for <%= more_info.for || 'more information' %>. +
+ <% } %> +
<%= params.time %>
+
+
+
+
+ <% + const items = [ + { id: 'browser', icon: 'browser', default_location: 'You', default_name: 'Browser' }, + { id: 'cloudflare', icon: 'cloud', default_location: 'San Francisco', default_name: 'Cloudflare' }, + { id: 'host', icon: 'server', default_location: 'Website', default_name: 'Host' } + ]; + items.forEach(({ id, icon, default_location, default_name }) => { + const item = params[id + '_status'] || {}; + const status = item.status || 'ok'; + let text_color; + if (item.status_text_color) { + text_color = item.status_text_color; + } else if (status === 'ok') { + text_color = '#9bca3e'; + } else if (status === 'error') { + text_color = '#bd2426'; + } + const status_text = item.status_text || (status === 'ok' ? 'Working' : 'Error'); + const is_error_source = params.error_source === id; + %> +
+
+ + +
+ <%= item.location || default_location %> +

><%= item.name || default_name %>

+ <%= status_text %> +
+ <% }); %> +
+
+
+ +
+
+
+

What happened?

+ <%- params.what_happened || '

There is an internal server error on Cloudflare\'s network.

' %> +
+
+

What can I do?

+ <%- params.what_can_i_do || '

Please try again in a few minutes.

' %> +
+
+
+ + +
+
+ + + diff --git a/nodejs/tsconfig.json b/nodejs/tsconfig.json new file mode 100644 index 0000000..24d9f1d --- /dev/null +++ b/nodejs/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020"], + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "examples"] +} From 5685358660054a0dc694f3a4fd4f4efedfc25485 Mon Sep 17 00:00:00 2001 From: Anthony Donlon Date: Wed, 10 Dec 2025 22:36:18 +0800 Subject: [PATCH 2/4] nodejs/template: fix comment in error page template --- nodejs/templates/error.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/templates/error.ejs b/nodejs/templates/error.ejs index 95aee8b..be59625 100644 --- a/nodejs/templates/error.ejs +++ b/nodejs/templates/error.ejs @@ -1,4 +1,4 @@ -{# Note: This is generated with scripts/inline_resources.py. Please do not edit this file manually. #} +<%# Note: This file is generated with scripts/inline_resources.py. Please do not edit manually. %> From ae6cf996b85d4c43b1dfd2a583215179c7bdfdf0 Mon Sep 17 00:00:00 2001 From: Anthony Donlon Date: Wed, 10 Dec 2025 22:36:27 +0800 Subject: [PATCH 3/4] nodejs/template: merge some updates from web editor --- nodejs/templates/error.ejs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nodejs/templates/error.ejs b/nodejs/templates/error.ejs index be59625..d64391e 100644 --- a/nodejs/templates/error.ejs +++ b/nodejs/templates/error.ejs @@ -31,7 +31,7 @@ Visit <%= more_info.text || 'cloudflare.com' %> for <%= more_info.for || 'more information' %>. <% } %> -
<%= params.time %>
+
<%= params.time %>
@@ -49,9 +49,9 @@ if (item.status_text_color) { text_color = item.status_text_color; } else if (status === 'ok') { - text_color = '#9bca3e'; + text_color = '#9bca3e'; // text-green-success } else if (status === 'error') { - text_color = '#bd2426'; + text_color = '#bd2426'; // text-red-error } const status_text = item.status_text || (status === 'ok' ? 'Working' : 'Error'); const is_error_source = params.error_source === id; @@ -62,7 +62,7 @@
<%= item.location || default_location %> -

><%= item.name || default_name %>

+

><%= item.name || default_name %>

<%= status_text %>
<% }); %> From 3ba9f55a6c1a0415c92d36f37dbe31d53d5e46ff Mon Sep 17 00:00:00 2001 From: Anthony Donlon Date: Wed, 10 Dec 2025 22:40:35 +0800 Subject: [PATCH 4/4] nodejs/template: fix creator_info not hidden on default config --- nodejs/templates/error.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/templates/error.ejs b/nodejs/templates/error.ejs index d64391e..91f47a9 100644 --- a/nodejs/templates/error.ejs +++ b/nodejs/templates/error.ejs @@ -97,7 +97,7 @@ Performance & security by <%= perf_sec_by.text || 'Cloudflare' %> <% const creator_info = params.creator_info || {}; %> - <% if (!creator_info.hidden) { %> + <% if (!(creator_info.hidden ?? true)) { %> Created with <%= creator_info.text %> <% } %>