9
0
mirror of https://github.com/donlon/cloudflare-error-page.git synced 2025-12-19 14:59:28 +00:00

Merge pull request #1 from junduck/main

Implemented ejs template version
This commit is contained in:
Anthony Donlon
2025-12-10 22:49:56 +08:00
committed by GitHub
11 changed files with 668 additions and 0 deletions

6
nodejs/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
dist/
*.log
*.html
.DS_Store
examples/*.html

8
nodejs/.npmignore Normal file
View File

@@ -0,0 +1,8 @@
src/
tsconfig.json
*.ts
!*.d.ts
examples/
node_modules/
*.log
.DS_Store

9
nodejs/LICENSE Normal file
View File

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

141
nodejs/README.md Normal file
View File

@@ -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: '<p>There is an internal server error on Cloudflare\'s network.</p>',
what_can_i_do: '<p>Please try again in a few minutes.</p>',
});
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: '<p>Something went wrong.</p>',
what_can_i_do: '<p>Try again later.</p>',
});
```
### 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: `<p>${err.message}</p>`,
what_can_i_do: '<p>Please try again or contact support.</p>',
});
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/)

View File

@@ -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: '<p>There is an internal server error on Cloudflare\'s network.</p>',
what_can_i_do: '<p>Please try again in a few minutes.</p>',
});
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.');

View File

@@ -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`);
}
});

142
nodejs/package-lock.json generated Normal file
View File

@@ -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"
}
}
}

45
nodejs/package.json Normal file
View File

@@ -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"
]
}

123
nodejs/src/index.ts Normal file
View File

@@ -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<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
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;

110
nodejs/templates/error.ejs Normal file

File diff suppressed because one or more lines are too long

20
nodejs/tsconfig.json Normal file
View File

@@ -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"]
}