9
0
mirror of https://github.com/donlon/cloudflare-error-page.git synced 2025-12-24 17:29:16 +00:00

11 Commits

Author SHA1 Message Date
Anthony Donlon
59c1f0dcfb editor/web: fix allowHtml param not set to false when rendering the page 2025-12-23 23:12:28 +08:00
Anthony Donlon
f90fa3501b editor/web: add Teapot preset 2025-12-23 23:03:54 +08:00
Anthony Donlon
3d470d55a4 editor/web: update codegen examples 2025-12-23 22:30:24 +08:00
Anthony Donlon
d6cf4bbedb js: fix trimming white spaces in ejs template 2025-12-23 22:17:00 +08:00
Anthony Donlon
48169fd3fa js: support error code with string type 2025-12-23 20:52:59 +08:00
Anthony Donlon
ae8849f39f editor/web: update code generator 2025-12-23 20:47:21 +08:00
Anthony Donlon
f50f81afd1 editor/web: save as filename based on page title 2025-12-23 01:57:11 +08:00
Anthony Donlon
d39409c0ca editor/web: show status code from params 2025-12-23 01:41:18 +08:00
Anthony Donlon
8be87415fe editor/web: support syntax highlighting on save as dialog 2025-12-23 01:28:17 +08:00
Anthony Donlon
fb52f6d523 editor/web: use render function from shared package 2025-12-23 00:38:34 +08:00
Anthony Donlon
5a633b5958 readme: update description for JavaScript/NodeJS 2025-12-23 00:11:19 +08:00
15 changed files with 276 additions and 237 deletions

15
.gitignore vendored
View File

@@ -1,14 +1,13 @@
.vscode/ .vscode/
.DS_Store
build/
*.egg-info/
__pycache__/
dist/
node_modules/
.venv/ .venv/
venv/ venv/
ttt/
node_modules/
build/
dist/
*.egg-info/
__pycache__/
instance/ instance/

View File

@@ -8,7 +8,7 @@ This project creates customized error pages that mimics the well-known Cloudflar
## Online Editor ## Online Editor
Here's an online editor to create customized error pages. Try it out [here](https://virt.moe/cferr/editor/). Here's an online editor to create customized error pages and example server apps. Try it out [here](https://virt.moe/cferr/editor/).
![Editor](https://github.com/donlon/cloudflare-error-page/blob/images/editor.png?raw=true) ![Editor](https://github.com/donlon/cloudflare-error-page/blob/images/editor.png?raw=true)
@@ -66,12 +66,52 @@ You can also see live demo [here](https://virt.moe/cferr/examples/default).
A demo server using Flask is also available in [flask_demo.py](examples/flask_demo.py). A demo server using Flask is also available in [flask_demo.py](examples/flask_demo.py).
### Node.js/NPM ### JavaScript/NodeJS
A Node.js package is available in [nodejs](nodejs) folder. However currently it supports only Node.js but not web browsers, Install the `cloudflare-error-page` package using npm:
and we plan to refactor it into a shared package, so it can work in both environments.
(Thanks [@junduck](https://github.com/junduck) for creating this.) ``` Bash
npm install cloudflare-error-page
```
The following example demonstrates how to create a simple Express server and return the error page to visitors.
``` JavaScript
import express from 'express';
import { render as render_cf_error_page } from 'cloudflare-error-page';
const app = express();
const port = 3000;
// Define a route for GET requests to the root URL
app.get('/', (req, res) => {
res.status(500).send(render_cf_error_page({
title: "Internal server error",
// Browser status is ok
browser_status: {
status: 'ok',
},
// Cloudflare status is error
cloudflare_status: {
status: 'error',
status_text: 'Error',
},
// Host status is also ok
host_status: {
status: 'ok',
location: 'example.com',
},
error_source: "cloudflare",
}));
});
// Start the server and listen on the specified port
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
```
(Thanks [@junduck](https://github.com/junduck) for creating the original NodeJS version.)
### PHP ### PHP

View File

@@ -170,8 +170,10 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 700px !important; min-height: 700px !important;
max-height: 1200px !important;
font-family: monospace; font-family: monospace;
font-size: 0.8em !important; font-size: 0.8em !important;
overflow: hidden;
} }
.save-as-dialog__buttons { .save-as-dialog__buttons {
@@ -236,6 +238,7 @@
<option value="empty">Empty</option> <option value="empty">Empty</option>
<option value="catastrophic">Catastrophic failure</option> <option value="catastrophic">Catastrophic failure</option>
<option value="working">Server working</option> <option value="working">Server working</option>
<option value="teapot">Teapot</option>
<option value="consensual">Myth of consensual</option> <option value="consensual">Myth of consensual</option>
</select> </select>
</div> </div>
@@ -473,7 +476,7 @@
<button type="button" id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button> <button type="button" id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button>
<!-- Button trigger modal --> <!-- Button trigger modal -->
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#saveAsDialog"> <button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#saveAsDialog">
Save as... Save as & Examples ...
</button> </button>
</div> </div>
@@ -530,10 +533,10 @@
Python Example Python Example
</button> </button>
<button type="button" data-type="js" class="list-group-item list-group-item-action"> <button type="button" data-type="js" class="list-group-item list-group-item-action">
NodeJS Example JavaScript Example
</button> </button>
<button type="button" data-type="static" class="list-group-item list-group-item-action"> <button type="button" data-type="static" class="list-group-item list-group-item-action">
Static Page Static Webpage
</button> </button>
</div> </div>
<div class="d-flex gap-1 save-as-dialog__buttons"> <div class="d-flex gap-1 save-as-dialog__buttons">
@@ -545,8 +548,9 @@
Save Save
</button> </button>
</div> </div>
<textarea id="saveAsDialogCode" class="form-control save-as-dialog__code" spellcheck="false" <div class="border rounded save-as-dialog__code">
readonly></textarea> <pre id="saveAsDialogCode" class="px-2 py-1 h-100"></pre>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -14,6 +14,7 @@
"@types/ejs": "^3.1.5", "@types/ejs": "^3.1.5",
"@types/html-minifier-terser": "^7.0.2", "@types/html-minifier-terser": "^7.0.2",
"@types/node": "^24.10.2", "@types/node": "^24.10.2",
"@types/prismjs": "^1.26.5",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"prettier": "3.7.4", "prettier": "3.7.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
@@ -22,6 +23,8 @@
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"ejs": "^3.1.10" "cloudflare-error-page": "../../javascript",
"ejs": "^3.1.10",
"prismjs": "^1.30.0"
} }
} }

View File

@@ -1,4 +1,5 @@
import ejs from 'ejs'; import ejs from 'ejs';
import { ErrorPageParams } from 'cloudflare-error-page';
import jsTemplate from './js.ejs?raw'; import jsTemplate from './js.ejs?raw';
import jsonTemplate from './json.ejs?raw'; import jsonTemplate from './json.ejs?raw';
@@ -6,21 +7,98 @@ import pythonTemplate from './python.ejs?raw';
interface CodeGen { interface CodeGen {
name: string; name: string;
generate(params: any): string; language: string;
generate(params: ErrorPageParams): string;
} }
class EjsCodeGen implements CodeGen { class EjsCodeGen implements CodeGen {
name: string; name: string;
language: string;
private template: ejs.TemplateFunction; private template: ejs.TemplateFunction;
constructor(name: string, templateContent: any) { constructor(name: string, language: string, templateContent: string) {
this.name = name; this.name = name;
this.language = language;
this.template = ejs.compile(templateContent); this.template = ejs.compile(templateContent);
} }
generate(params: any): string { protected prepareTemplateArgs(params: ErrorPageParams): Record<string, any> {
return this.template({ params }); return {};
}
generate(params: ErrorPageParams): string {
const moreArgs = this.prepareTemplateArgs(params);
return this.template({ params, ...moreArgs });
} }
} }
export const jsCodeGen = new EjsCodeGen('NodeJS Example', jsTemplate); function getErrorCode(error_code?: string | number) {
export const jsonCodeGen = new EjsCodeGen('JSON', jsonTemplate); const errorCode = error_code || '';
export const pythonCodeGen = new EjsCodeGen('Python Example', pythonTemplate); return /\d{3}/.test(errorCode + '') ? errorCode : 500;
}
class JSCodeGen extends EjsCodeGen {
constructor(templateContent: string) {
super('JavaScript Example', 'javascript', templateContent);
}
private formatJSArgs(params: ErrorPageParams): string {
params = { ...params };
const rayIdKey = Math.random() + '';
const clientIpKey = Math.random() + '';
params.ray_id = rayIdKey;
params.client_ip = clientIpKey;
const paramsArg = JSON.stringify(params, null, 2)
.replace(`"${rayIdKey}"`, "(req.get('Cf-Ray') ?? '').substring(0, 16)")
.replace(`"${clientIpKey}"`, "req.get('X-Forwarded-For') || req.socket.remoteAddress");
return paramsArg;
}
protected prepareTemplateArgs(params: ErrorPageParams): Record<string, any> {
return {
errorCode: getErrorCode(params.error_code),
// TODO: format to JS-style object (key w/o parens)
indentedParams: this.formatJSArgs(params).replaceAll('\n', '\n '),
};
}
}
class PythonCodeGen extends EjsCodeGen {
constructor(templateContent: string) {
super('Python Example', 'python', templateContent);
}
private formatPythonArgs(params: ErrorPageParams): string {
// Covert the parameters to Python format object
params = { ...params };
const randomKey = Math.random() + '';
const rayIdKey = Math.random() + '';
const clientIpKey = Math.random() + '';
params.ray_id = rayIdKey;
params.client_ip = clientIpKey;
const paramsArg = JSON.stringify(
params,
(key, value) => {
if (typeof value === 'boolean') {
return randomKey + value.toString();
} else if (value === null) {
return randomKey + 'null';
} else {
return value;
}
},
4
)
.replace(`"${randomKey}true"`, 'True')
.replace(`"${randomKey}false"`, 'False')
.replace(`"${randomKey}null"`, 'None')
.replace(`"${rayIdKey}"`, 'request.headers.get("Cf-Ray", "")[:16]')
.replace(`"${clientIpKey}"`, 'request.headers.get("X-Forwarded-For") or request.remote_addr');
return paramsArg;
}
protected prepareTemplateArgs(params: ErrorPageParams): Record<string, any> {
return {
errorCode: getErrorCode(params.error_code),
// TODO: format to JS-style object (key w/o parens)
indentedParams: this.formatPythonArgs(params).replaceAll('\n', '\n '),
};
}
}
export const jsCodeGen = new JSCodeGen(jsTemplate);
export const jsonCodeGen = new EjsCodeGen('JSON', 'json', jsonTemplate);
export const pythonCodeGen = new PythonCodeGen(pythonTemplate);

View File

@@ -1,13 +1,21 @@
#!/usr/bin/env node
/*
* JavaScript Example: Render and serve the Cloudflare error page using Express server
*
* Prerequisits:
* npm install express cloudflare-error-page
*/
import express from 'express'; import express from 'express';
import { render as render_cf_error_page } from 'cloudflare-error-page'; import { render as render_cf_error_page } from 'cloudflare-error-page';
const app = express(); const app = express();
const port = 3000; const port = 3000;
<%# TODO: use the library as middleware? _%>
// Define a route for GET requests to the root URL // Define a route for GET requests to the root URL
<%# TODO: format to JS-style object (key w/o parens) _%>
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.status(500).send(render_cf_error_page(<%-JSON.stringify(params, null, 2).replaceAll('\n', '\n ')%>)); res.status(<%= errorCode %>).send(render_cf_error_page(<%- indentedParams %>));
}); });
// Start the server and listen on the specified port // Start the server and listen on the specified port

View File

@@ -1 +1 @@
<%-JSON.stringify(params, null, 4)%> <%- JSON.stringify(params, null, 4) %>

View File

@@ -1,29 +1,22 @@
<% #!/usr/bin/env python3
// Covert the parameters to Python format object #
const randomKey = Math.random() + '' # Python Example: Render and serve the Cloudflare error page using Flask server
const paramsArg = JSON.stringify(params, (key, value) => { #
if (typeof value === 'boolean') { # Prerequisits:
return randomKey + value.toString() # pip install flask cloudflare-error-page
} else if (value === null) { #
return randomKey + 'null'
} else { from flask import Flask, request
return value
}
}, 4)
.replace(`"${randomKey}true"`, 'True')
.replace(`"${randomKey}false"`, 'False')
.replace(`"${randomKey}null"`, 'None')
_%>
from flask import Flask
from cloudflare_error_page import render as render_cf_error_page from cloudflare_error_page import render as render_cf_error_page
app = Flask(__name__) app = Flask(__name__)
<%# TODO: use the library as middleware? _%>
# Define a route for GET requests to the root URL # Define a route for GET requests to the root URL
@app.route('/') @app.route("/")
def index(): def index():
# Render the error page # Render the error page
return render_cf_error_page(<%- paramsArg.replaceAll('\n', '\n ') %>), 500 return render_cf_error_page(<%- indentedParams %>), <%= errorCode %>
if __name__ == '__main__': if __name__ == "__main__":
app.run(debug=True, port=5000) app.run(debug=True, port=5000)

View File

@@ -5,17 +5,19 @@
- inputs call render() on change - inputs call render() on change
- "Open in new tab" opens the rendered HTML in a new window using a blob URL - "Open in new tab" opens the rendered HTML in a new window using a blob URL
*/ */
import ejs from 'ejs';
import templateContent from './template.ejs?raw';
import 'bootstrap/js/src/modal.js'; import 'bootstrap/js/src/modal.js';
import Popover from 'bootstrap/js/src/popover.js'; import Popover from 'bootstrap/js/src/popover.js';
import Prism from 'prismjs';
import 'prismjs/components/prism-json.js';
import 'prismjs/components/prism-python.js';
import { render as render_cf_error_page } from 'cloudflare-error-page';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import 'prismjs/themes/prism.css';
import { jsCodeGen, jsonCodeGen, pythonCodeGen } from './codegen'; import { jsCodeGen, jsonCodeGen, pythonCodeGen } from './codegen';
let template = ejs.compile(templateContent);
// can be changed if specified by '?from=<name>' // can be changed if specified by '?from=<name>'
let initialConfig = { let initialConfig = {
title: 'Internal server error', title: 'Internal server error',
@@ -101,6 +103,31 @@ const PRESETS = {
what_happened: 'This site is still working. And it looks great.', what_happened: 'This site is still working. And it looks great.',
what_can_i_do: 'Visit the site before it crashes someday.', what_can_i_do: 'Visit the site before it crashes someday.',
}, },
teapot: {
title: "I'm a teapot",
error_code: '418',
more_information: {
text: 'rfc2324',
link: 'https://www.rfc-editor.org/rfc/rfc2324',
},
browser_status: {
status: 'ok',
location: 'You',
status_text: 'Working',
},
cloudflare_status: {
status: 'ok',
status_text: 'Working',
},
host_status: {
status: 'ok',
location: 'Teapot',
status_text: 'Working',
},
error_source: 'host',
what_happened: "The server can not brew coffee for it is a teapot.",
what_can_i_do: 'Please try a different coffee machine.',
},
consensual: { consensual: {
title: 'The Myth Of "Consensual" Internet', title: 'The Myth Of "Consensual" Internet',
error_code: 'lmao', error_code: 'lmao',
@@ -255,24 +282,9 @@ function readConfig() {
}, },
}; };
} }
function formatUtcTimestamp() {
const d = new Date();
const year = d.getUTCFullYear();
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
const day = String(d.getUTCDate()).padStart(2, '0');
const hours = String(d.getUTCHours()).padStart(2, '0');
const minutes = String(d.getUTCMinutes()).padStart(2, '0');
const seconds = String(d.getUTCSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} UTC`;
}
function renderEjs(params) { function renderEjs(params) {
return template({ return render_cf_error_page(params, false);
params: params,
});
} }
/* Basic render: build HTML string from config and put into iframe.srcdoc */ /* Basic render: build HTML string from config and put into iframe.srcdoc */
@@ -280,9 +292,8 @@ function render() {
const cfg = readConfig(); const cfg = readConfig();
window.lastCfg = cfg; window.lastCfg = cfg;
cfg.time = formatUtcTimestamp(); // TODO: input box for Ray ID / Client IP?
cfg.ray_id = '0123456789abcdef'; cfg.ray_id = '0123456789abcdef';
cfg.client_ip = '1.1.1.1';
if (Number.isNaN(Number(cfg.error_code))) { if (Number.isNaN(Number(cfg.error_code))) {
cfg.html_title = cfg.title || 'Internal server error'; cfg.html_title = cfg.title || 'Internal server error';
} }
@@ -305,8 +316,8 @@ function render() {
let lastRenderedHtml = ''; let lastRenderedHtml = '';
function openInNewTab() { function openInNewTab() {
if (!lastRenderedHtml) render(); if (!lastRenderedHtml) render();
const wnd = window.open() const wnd = window.open();
wnd.document.documentElement.innerHTML = lastRenderedHtml wnd.document.documentElement.innerHTML = lastRenderedHtml;
} }
function createShareableLink() { function createShareableLink() {
@@ -445,15 +456,21 @@ function updateSaveAsDialog(e) {
} }
const params = { ...lastCfg }; const params = { ...lastCfg };
delete params.time; delete params.time;
delete params.ray_id;
let language;
if (codegen) { if (codegen) {
saveAsContent = codegen.generate(params); saveAsContent = codegen.generate(params);
language = codegen.language;
} else if (saveAsType == 'static') { } else if (saveAsType == 'static') {
render() // rerender the page render(); // rerender the page
saveAsContent = lastRenderedHtml; saveAsContent = lastRenderedHtml;
language = 'html';
} else { } else {
throw new Error('unexpected saveAsType=' + saveAsType) throw new Error('unexpected saveAsType=' + saveAsType);
} }
$('saveAsDialogCode').value = saveAsContent; const html = Prism.highlight(saveAsContent, Prism.languages[language], language);
$('saveAsDialogCode').innerHTML = html;
$('saveAsDialogCode').scrollTop = 0; $('saveAsDialogCode').scrollTop = 0;
document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => { document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
@@ -475,32 +492,37 @@ document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
const saveAsDialogCopyPopover = new Popover($('saveAsDialogCopyBtn')); const saveAsDialogCopyPopover = new Popover($('saveAsDialogCopyBtn'));
$('saveAsDialogCopyBtn').addEventListener('click', (e) => { $('saveAsDialogCopyBtn').addEventListener('click', (e) => {
const field = $('saveAsDialogCode'); navigator.clipboard.writeText(saveAsContent).then(() => {
field.select();
// field.setSelectionRange(0, field.value.length);
navigator.clipboard.writeText(field.value).then(() => {
saveAsDialogCopyPopover.show(); saveAsDialogCopyPopover.show();
setTimeout(() => { setTimeout(() => {
saveAsDialogCopyPopover.hide(); saveAsDialogCopyPopover.hide();
}, 2000); }, 2000);
}); });
}); });
function toSnakeCase(str, separator = '_') {
return str
.toLowerCase()
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '_') // keep letters & numbers from ALL languages
.replace(/^_+|_+$/g, '')
.replaceAll('_', separator);
}
const saveAsExtensions = {
js: 'js',
json: 'json',
python: 'py',
static: 'html',
};
$('saveAsDialogSaveBtn').addEventListener('click', (e) => { $('saveAsDialogSaveBtn').addEventListener('click', (e) => {
let saveName = ''; let name = lastCfg.title || 'Internal server error';
switch (saveAsType) { switch (saveAsType) {
case 'js': case 'js':
saveName = 'cf-error-page-example.js';
break;
case 'json':
saveName = 'cf-error-page-params.json';
break;
case 'python': case 'python':
saveName = 'cf_error_page_example.py'; name += ' example';
break; break;
case 'static':
saveName = 'cf_error_page.html';
break;
// TODO: name output files using page title
} }
const separator = saveAsType == 'python' ? '_' : '-';
const ext = saveAsExtensions[saveAsType] ?? txt;
const saveName = `${toSnakeCase(name, separator)}.${ext}`;
saveFile(saveAsContent, saveName); saveFile(saveAsContent, saveName);
}); });

File diff suppressed because one or more lines are too long

View File

@@ -13,6 +13,9 @@ export default defineConfig(({ mode }) => {
minify: true, minify: true,
sourcemap: true, sourcemap: true,
}, },
resolve: {
dedupe: ['ejs'],
},
server: { server: {
port: 3000, port: 3000,
proxy: { proxy: {

View File

@@ -310,6 +310,11 @@
dependencies: dependencies:
undici-types "~7.16.0" undici-types "~7.16.0"
"@types/prismjs@^1.26.5":
version "1.26.5"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.5.tgz#72499abbb4c4ec9982446509d2f14fb8483869d6"
integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==
acorn@^8.15.0: acorn@^8.15.0:
version "8.15.0" version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
@@ -392,6 +397,11 @@ clean-css@~5.3.2:
dependencies: dependencies:
source-map "~0.6.0" source-map "~0.6.0"
cloudflare-error-page@../../javascript:
version "0.1.0"
dependencies:
ejs "^3.1.10"
commander@^10.0.0: commander@^10.0.0:
version "10.0.1" version "10.0.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
@@ -613,6 +623,11 @@ prettier@3.7.4:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA== integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==
prismjs@^1.30.0:
version "1.30.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.30.0.tgz#d9709969d9d4e16403f6f348c63553b19f0975a9"
integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==
readdirp@~3.6.0: readdirp@~3.6.0:
version "3.6.0" version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"

View File

@@ -11,7 +11,7 @@ npm install cloudflare-error-page
Or install from GitHub: Or install from GitHub:
```bash ```bash
npm install git+https://github.com/donlon/cloudflare-error-page.git#main:nodejs npm install git+https://github.com/donlon/cloudflare-error-page.git#javascriptnodejs
``` ```
## Quick Start ## Quick Start
@@ -34,7 +34,7 @@ fs.writeFileSync('error.html', errorPage);
## API Reference ## API Reference
### `render(params: ErrorPageParams, allowHtml?: boolean): string` ### `render(params: ErrorPageParams, allowHtml?: boolean, moreArgs?: { [name: string]: any; }): string`
Generates an HTML error page based on the provided parameters. Generates an HTML error page based on the provided parameters.
@@ -42,13 +42,14 @@ Generates an HTML error page based on the provided parameters.
- `params`: An object containing error page configuration - `params`: An object containing error page configuration
- `allowHtml` (optional): Whether to allow HTML in `what_happened` and `what_can_i_do` fields. Default: `true` - `allowHtml` (optional): Whether to allow HTML in `what_happened` and `what_can_i_do` fields. Default: `true`
- `moreArgs` (optional): More arguments passed to the ejs template
#### ErrorPageParams Interface #### ErrorPageParams Interface
```typescript ```typescript
interface ErrorPageParams { interface ErrorPageParams {
// Basic information // Basic information
error_code?: number; // Default: 500 error_code?: string | number; // Default: 500
title?: string; // Default: 'Internal server error' title?: string; // Default: 'Internal server error'
html_title?: string; // Default: '{error_code}: {title}' html_title?: string; // Default: '{error_code}: {title}'
time?: string; // Auto-generated if not provided time?: string; // Auto-generated if not provided

View File

@@ -30,7 +30,7 @@ export interface CreatorInfo {
} }
export interface ErrorPageParams { export interface ErrorPageParams {
error_code?: number; error_code?: string | number;
title?: string; title?: string;
html_title?: string; html_title?: string;
time?: string; time?: string;
@@ -53,7 +53,6 @@ export interface ErrorPageParams {
// Load EJS template // Load EJS template
export const baseTemplate: ejs.TemplateFunction = ejs.compile(templateString); export const baseTemplate: ejs.TemplateFunction = ejs.compile(templateString);
/** /**
* Generate random hex string for ray-id * Generate random hex string for ray-id
*/ */
@@ -70,7 +69,7 @@ function genHexString(digits: number): string {
* Render a customized Cloudflare error page * Render a customized Cloudflare error page
* @param params - The parameters for the 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) * @param allowHtml - Whether to allow HTML in what_happened and what_can_i_do fields (default: true)
* @param moreArgs - More arguments passed to ejs template * @param moreArgs - More arguments passed to the ejs template
* @returns The rendered HTML string * @returns The rendered HTML string
*/ */
export function render( export function render(

View File

@@ -4,9 +4,9 @@
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]--> <!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]--> <!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]-->
<head> <head>
<% const error_code = params.error_code || 500; %> <% const error_code = params.error_code || 500; -%>
<% const title = params.title || 'Internal server error'; %> <% const title = params.title || 'Internal server error'; -%>
<% const html_title = params.html_title || (error_code + ': ' + title); %> <% const html_title = params.html_title || (error_code + ': ' + title); -%>
<title><%= html_title %></title> <title><%= html_title %></title>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
@@ -14,7 +14,7 @@
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<style> <style>
<%- html_style %> <%- html_style -%>
</style> </style>
</head> </head>
<body> <body>
@@ -25,18 +25,18 @@
<span class="inline-block"><%= title %></span> <span class="inline-block"><%= title %></span>
<span class="code-label">Error code <%= error_code %></span> <span class="code-label">Error code <%= error_code %></span>
</h1> </h1>
<% const more_info = params.more_information || {}; %> <%_ const more_info = params.more_information || {}; -%>
<% if (!more_info.hidden) { %> <%_ if (!more_info.hidden) { -%>
<div> <div>
Visit <a href="<%= more_info.link || 'https://www.cloudflare.com/' %>" target="_blank" rel="noopener noreferrer"><%= more_info.text || 'cloudflare.com' %></a> for <%= more_info.for || 'more information' %>. Visit <a href="<%= more_info.link || 'https://www.cloudflare.com/' %>" target="_blank" rel="noopener noreferrer"><%= more_info.text || 'cloudflare.com' %></a> for <%= more_info.for || 'more information' %>.
</div> </div>
<% } %> <%_ } -%>
<div class="<%= more_info.hidden ? '' : 'mt-3' %>"><%= params.time %></div> <div class="<%= more_info.hidden ? '' : 'mt-3' %>"><%= params.time %></div>
</header> </header>
<div class="my-8 bg-gradient-gray"> <div class="my-8 bg-gradient-gray">
<div class="w-240 lg:w-full mx-auto"> <div class="w-240 lg:w-full mx-auto">
<div class="clearfix md:px-8"> <div class="clearfix md:px-8">
<% <%_
const items = [ const items = [
{ id: 'browser', icon: 'browser', default_location: 'You', default_name: 'Browser' }, { id: 'browser', icon: 'browser', default_location: 'You', default_name: 'Browser' },
{ id: 'cloudflare', icon: 'cloud', default_location: 'San Francisco', default_name: 'Cloudflare' }, { id: 'cloudflare', icon: 'cloud', default_location: 'San Francisco', default_name: 'Cloudflare' },
@@ -55,7 +55,7 @@
} }
const status_text = item.status_text || (status === 'ok' ? 'Working' : 'Error'); const status_text = item.status_text || (status === 'ok' ? 'Working' : 'Error');
const is_error_source = params.error_source === id; const is_error_source = params.error_source === id;
%> -%>
<div id="cf-<%= id %>-status" class="<%= is_error_source ? 'cf-error-source' : '' %> relative w-1/3 md:w-full py-15 md:p-0 md:py-8 md:text-left md:border-solid md:border-0 md:border-b md:border-gray-400 overflow-hidden float-left md:float-none text-center"> <div id="cf-<%= id %>-status" class="<%= is_error_source ? 'cf-error-source' : '' %> relative w-1/3 md:w-full py-15 md:p-0 md:py-8 md:text-left md:border-solid md:border-0 md:border-b md:border-gray-400 overflow-hidden float-left md:float-none text-center">
<div class="relative mb-10 md:m-0"> <div class="relative mb-10 md:m-0">
<span class="cf-icon-<%= icon %> block md:hidden h-20 bg-center bg-no-repeat"></span> <span class="cf-icon-<%= icon %> block md:hidden h-20 bg-center bg-no-repeat"></span>
@@ -65,7 +65,7 @@
<h3 class="md:inline-block mt-3 md:mt-0 text-2xl text-gray-600 font-light leading-1.3" <%- (item.name || default_name) === 'Cloudflare' ? 'style="color: #2f7bbf;"' : '' %>><%= item.name || default_name %></h3> <h3 class="md:inline-block mt-3 md:mt-0 text-2xl text-gray-600 font-light leading-1.3" <%- (item.name || default_name) === 'Cloudflare' ? 'style="color: #2f7bbf;"' : '' %>><%= item.name || default_name %></h3>
<span class="leading-1.3 text-2xl" style="color: <%= text_color %>"><%= status_text %></span> <span class="leading-1.3 text-2xl" style="color: <%= text_color %>"><%= status_text %></span>
</div> </div>
<% }); %> <%_ }); -%>
</div> </div>
</div> </div>
</div> </div>
@@ -95,12 +95,11 @@
</span> </span>
<% const perf_sec_by = params.perf_sec_by || {}; %> <% const perf_sec_by = params.perf_sec_by || {}; %>
<span class="cf-footer-item sm:block sm:mb-1"><span>Performance &amp; security by</span> <a rel="noopener noreferrer" href="<%= perf_sec_by.link || 'https://www.cloudflare.com/' %>" id="brand_link" target="_blank"><%= perf_sec_by.text || 'Cloudflare' %></a></span> <span class="cf-footer-item sm:block sm:mb-1"><span>Performance &amp; security by</span> <a rel="noopener noreferrer" href="<%= perf_sec_by.link || 'https://www.cloudflare.com/' %>" id="brand_link" target="_blank"><%= perf_sec_by.text || 'Cloudflare' %></a></span>
<%_ const creator_info = params.creator_info || {}; _%>
<% const creator_info = params.creator_info || {}; %> <%_ if (!(creator_info.hidden ?? true)) { _%>
<% if (!(creator_info.hidden ?? true)) { %>
<span class="cf-footer-separator sm:hidden">&bull;</span> <span class="cf-footer-separator sm:hidden">&bull;</span>
<span class="cf-footer-item sm:block sm:mb-1">Created with <a href="<%= creator_info.link %>" target="_blank"><%= creator_info.text %></a></span> <span class="cf-footer-item sm:block sm:mb-1">Created with <a href="<%= creator_info.link %>" target="_blank"><%= creator_info.text %></a></span>
<% } %> <%_ } -%>
</p> </p>
</div><!-- /.error-footer --> </div><!-- /.error-footer -->
</div> </div>