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

26 Commits

Author SHA1 Message Date
Anthony Donlon
5a633b5958 readme: update description for JavaScript/NodeJS 2025-12-23 00:11:19 +08:00
Anthony Donlon
b2164729b4 js: bump version to 0.1.0 2025-12-22 23:47:00 +08:00
Anthony Donlon
dd29cf0904 move nodejs folder to javascript 2025-12-22 23:46:21 +08:00
Anthony Donlon
d9af59f14f nodejs: load template from bundled string 2025-12-22 23:43:07 +08:00
Anthony Donlon
5a5311212e python: bump package version to 0.2.0 2025-12-22 22:48:41 +08:00
Anthony Donlon
9fbcba1a34 python: separate css from html template 2025-12-22 22:28:46 +08:00
Anthony Donlon
c0e576478a editor/web: fix generating Python code 2025-12-22 22:27:36 +08:00
Anthony Donlon
6233cec91f editor/web: support exporting generated webpage 2025-12-22 22:01:58 +08:00
Anthony Donlon
728ce52529 editor: support ok/error icon in shared page 2025-12-22 21:52:50 +08:00
Anthony Donlon
d0a05329d5 editor/web: open preview in about:blank tab 2025-12-22 21:44:14 +08:00
Anthony Donlon
51b61a8a2d Merge pull request #8 from donlon/save-as-dialog
Add Save As dialog to provide language examples
2025-12-22 21:34:48 +08:00
Anthony Donlon
71f4e9507c editor/web: support responsive save as dialog 2025-12-22 21:27:47 +08:00
Anthony Donlon
38eb8d8c11 editor/web: add popovers for "Copy" buttons 2025-12-22 02:13:29 +08:00
Anthony Donlon
76a4696a40 editor/web: add Save As dialog to provide language examples 2025-12-22 01:52:12 +08:00
Anthony Donlon
21d14994de python: rename head block in base template to 'html_head' 2025-12-21 20:42:58 +08:00
Anthony Donlon
36a1119908 Merge pull request #7 from syrf109475/main
Add versioning configuration to pyproject.toml
2025-12-21 20:10:24 +08:00
syrf109475
e66b5357a4 Add versioning configuration to pyproject.toml
Fix "ValueError: Missing `tool.hatch.version` configuration" when using pip install
2025-12-21 14:54:52 +08:00
Anthony Donlon
5bc662dd06 editor/web: import bootstrap css from script 2025-12-19 01:17:13 +08:00
Anthony Donlon
7229ad7281 editor/server: set meta description from page 2025-12-19 01:10:13 +08:00
Anthony Donlon
a50b0289a0 editor: add social card images 2025-12-19 01:08:24 +08:00
Anthony Donlon
a85624031c editor/web: misc fixes 2025-12-19 00:40:56 +08:00
Anthony Donlon
df5daebe34 python: rename default_template -> base_template 2025-12-17 01:23:41 +08:00
Anthony Donlon
eb0d5a7d55 editor: support page icon 2025-12-17 01:21:43 +08:00
Anthony Donlon
245f5b1f6d python: use dynamic package version 2025-12-16 22:49:06 +08:00
Anthony Donlon
58bf0d6b79 python: support type hinting for input params 2025-12-16 22:48:32 +08:00
Anthony Donlon
ed711a2521 editor: support shortened share URLs 2025-12-16 21:39:48 +08:00
47 changed files with 1654 additions and 706 deletions

View File

@@ -19,6 +19,10 @@ Here's an online editor to create customized error pages. Try it out [here](http
Install `cloudflare-error-page` with pip.
``` Bash
# Install from PyPI
pip install cloudflare-error-page
# Or, install the latest version from this repo
pip install git+https://github.com/donlon/cloudflare-error-page.git
```
@@ -62,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).
### 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,
and we plan to refactor it into a shared package, so it can work in both environments.
Install the `cloudflare-error-page` package using npm:
(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

View File

@@ -1,21 +1,75 @@
import html
import secrets
import sys
from datetime import datetime, timezone
from typing import Any
from typing import Any, TypedDict, Literal
if sys.version_info >= (3, 11):
from typing import NotRequired
else:
from typing import _SpecialForm
NotRequired: _SpecialForm
from jinja2 import Environment, PackageLoader, Template, select_autoescape
env = Environment(
jinja_env = Environment(
loader=PackageLoader(__name__),
autoescape=select_autoescape(),
trim_blocks=True,
lstrip_blocks=True,
)
default_template: Template = env.get_template("error.html")
base_template: Template = jinja_env.get_template('template.html')
def render(params: dict,
class ErrorPageParams(TypedDict):
class MoreInformation(TypedDict):
hidden: NotRequired[bool]
text: NotRequired[str]
link: NotRequired[str]
for_text: NotRequired[str] # renamed to avoid Python keyword conflict
class StatusItem(TypedDict):
status: NotRequired[Literal['ok', 'error']]
location: NotRequired[str]
name: NotRequired[str]
status_text: NotRequired[str]
status_text_color: NotRequired[str]
class PerfSecBy(TypedDict):
text: NotRequired[str]
link: NotRequired[str]
class CreatorInfo(TypedDict):
hidden: NotRequired[bool]
link: NotRequired[str]
text: NotRequired[str]
html_title: NotRequired[str]
title: NotRequired[str]
error_code: NotRequired[str]
time: NotRequired[str]
more_information: NotRequired[MoreInformation]
browser_status: NotRequired[StatusItem]
cloudflare_status: NotRequired[StatusItem]
host_status: NotRequired[StatusItem]
error_source: NotRequired[Literal['browser', 'cloudflare', 'host']]
what_happened: NotRequired[str]
what_can_i_do: NotRequired[str]
ray_id: NotRequired[str]
client_ip: NotRequired[str]
perf_sec_by: NotRequired[PerfSecBy]
creator_info: NotRequired[CreatorInfo]
def render(params: ErrorPageParams,
allow_html: bool = True,
template: Template | None = None,
*args: Any,
@@ -31,13 +85,19 @@ def render(params: dict,
:return: The rendered error page as a string.
'''
if not template:
template = default_template
template = base_template
params = {**params}
more_information = params.get('more_information')
if more_information:
for_text = more_information.get('for_text')
if for_text is not None:
more_information['for'] = for_text
if not params.get('time'):
utc_now = datetime.now(timezone.utc)
params['time'] = utc_now.strftime("%Y-%m-%d %H:%M:%S UTC")
params['time'] = utc_now.strftime('%Y-%m-%d %H:%M:%S UTC')
if not params.get('ray_id'):
params['ray_id'] = secrets.token_hex(8)
if not allow_html:
@@ -46,5 +106,5 @@ def render(params: dict,
return template.render(params=params, *args, **kwargs)
__all__ = ['default_template', 'render']
__version__ = '0.2.0'
__all__ = ['jinja_env', 'base_template', 'render']

File diff suppressed because one or more lines are too long

View File

@@ -13,8 +13,16 @@
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{% block header %}{% endblock %}
<!-- @INLINE_CSS_HERE@ -->
{% block html_head %}{% endblock %}
<style>
{% if html_style %}
{# Support custom stylesheet #}
{{ html_style | safe }}
{% else %}
{# Default stylesheet #}
{% include 'main.css' %}
{% endif %}
</style>
</head>
<body>
<div id="cf-wrapper">

View File

@@ -92,9 +92,11 @@ def create_app(test_config=None) -> Flask:
return '', 204
url_prefix = app.config.get('URL_PREFIX', '')
short_share_url = app.config.get('SHORT_SHARE_URL', False)
app.register_blueprint(editor.bp, url_prefix=f'{url_prefix}/editor')
app.register_blueprint(examples.bp, url_prefix=f'{url_prefix}/examples')
app.register_blueprint(share.bp, url_prefix=f'{url_prefix}/s')
app.register_blueprint(share.bp_short, url_prefix=f'{url_prefix}' + ('' if short_share_url else '/s'))
return app

View File

@@ -1,6 +1,21 @@
# Url prefix for app urls
URL_PREFIX = ''
# Digits of item name in shared links
SHARE_LINK_DIGITS = 7
# Use short share url (without '/s' path)
SHORT_SHARE_URL = false
# Icon URL for rendered pages
PAGE_ICON_URL = 'https://virt.moe/cferr/editor/assets/icon-{status}-32x32.png'
# MIME type of page icon
PAGE_ICON_TYPE = 'image/png'
# Image URL for rendered pages
PAGE_IMAGE_URL = 'https://virt.moe/cferr/editor/assets/icon-{status}-large-white.png'
# Set to true if trust X-Forwarded-For/X-Forwarded-Proto header
BEHIND_PROXY = true

View File

@@ -11,8 +11,10 @@ from flask import (
redirect,
)
from cloudflare_error_page import render as render_cf_error_page
from .utils import fill_cf_template_params
from cloudflare_error_page import ErrorPageParams
from .utils import (
render_extended_template,
)
root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../')
examples_dir = os.path.join(root_dir, 'examples')
@@ -22,7 +24,7 @@ bp = Blueprint('examples', __name__, url_prefix='/')
param_cache: dict[str, dict] = {}
def get_page_params(name: str) -> dict:
def get_page_params(name: str) -> ErrorPageParams:
name = re.sub(r'[^\w]', '', name)
params = param_cache.get(name)
if params is not None:
@@ -49,7 +51,5 @@ def index(name: str):
if params is None:
abort(404)
fill_cf_template_params(params)
# Render the error page
return render_cf_error_page(params)
return render_extended_template(params=params)

View File

@@ -1,57 +1,33 @@
# SPDX-License-Identifier: MIT
import html
import random
import string
from typing import cast
from cloudflare_error_page import ErrorPageParams
from flask import (
Blueprint,
current_app,
request,
abort,
jsonify,
redirect,
url_for,
)
from jinja2 import Environment, select_autoescape
from cloudflare_error_page import (
default_template as cf_template,
render as render_cf_error_page,
)
from . import (
db,
limiter,
models
models,
)
from .utils import fill_cf_template_params, sanitize_page_param_links
# root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../')
# examples_dir = os.path.join(root_dir, 'examples')
env = Environment(
autoescape=select_autoescape(),
trim_blocks=True,
lstrip_blocks=True,
from .utils import (
render_extended_template,
sanitize_page_param_links,
)
template = env.from_string('''
{% extends base %}
{% block header %}
<meta property="og:type" content="website" />
<meta property="og:site_name" content="moe::virt" />
<meta property="og:title" content="{{ html_title }}" />
<meta property="og:url" content="{{ url }}" />
<meta property="og:description" content="{{ description }}" />
<meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="moe::virt" />
<meta property="twitter:title" content="{{ html_title }}" />
<meta property="twitter:description" content="{{ description }}" />
{% endblock %}
''')
bp = Blueprint('share', __name__, url_prefix='/')
bp_short = Blueprint('share_short', __name__, url_prefix='/')
rand_charset = string.ascii_lowercase + string.digits
@@ -81,7 +57,8 @@ def create():
# TODO: strip unused params
try:
item = models.Item()
item.name = get_rand_name()
digits = current_app.config.get('SHARE_LINK_DIGITS', 7)
item.name = get_rand_name(digits)
item.params = params
db.session.add(item)
db.session.commit()
@@ -93,12 +70,12 @@ def create():
return jsonify({
'status': 'ok',
'name': item.name,
'url': request.host_url[:-1] + url_for('share.get', name=item.name),
'url': request.host_url[:-1] + url_for('share_short.get', name=item.name),
# TODO: better way to handle this
})
@bp.get('/<name>')
@bp_short.get('/<name>')
def get(name: str):
accept = request.headers.get('Accept', '')
is_json = 'application/json' in accept
@@ -111,7 +88,7 @@ def get(name: str):
})
else:
return abort(404)
params: dict = item.params
params = cast(ErrorPageParams, item.params)
params.pop('time', None)
params.pop('ray_id', None)
params.pop('client_ip', None)
@@ -127,12 +104,15 @@ def get(name: str):
'text': 'CF Error Page Editor',
'link': request.host_url[:-1] + url_for('editor.index') + f'#from={name}',
}
fill_cf_template_params(params)
sanitize_page_param_links(params)
return render_extended_template(params=params,
allow_html=False)
return render_cf_error_page(params=params,
allow_html=False,
template=template,
base=cf_template,
url=request.url,
description='Cloudflare error page')
@bp.get('/<name>')
def get_redir(name: str):
short_share_url = current_app.config.get('SHORT_SHARE_URL', False)
if short_share_url:
return redirect(f'../{name}', code=308)
else:
return get(name=name)

View File

@@ -1,9 +1,53 @@
import json
import os
import re
from typing import Any
from cloudflare_error_page import (
ErrorPageParams,
base_template as base_template,
render as render_cf_error_page,
)
from flask import current_app, request
from jinja2 import Environment, select_autoescape
from flask import request
from . import root_dir
env = Environment(
autoescape=select_autoescape(),
trim_blocks=True,
lstrip_blocks=True,
)
template = env.from_string('''{% extends base %}
{% block html_head %}
{% if page_icon_url %}
{% if page_icon_type %}
<link rel="icon" href="{{ page_icon_url }}" type="{{ page_icon_type }}">
{% else %}
<link rel="icon" href="{{ page_icon_url }}">
{% endif %}
{% endif %}
<meta property="og:type" content="website" />
<meta property="og:site_name" content="moe::virt" />
<meta property="og:title" content="{{ html_title }}" />
<meta property="og:url" content="{{ page_url }}" />
<meta property="og:description" content="{{ page_description }}" />
{% if page_image_url %}
<meta property="og:image" content="{{ page_image_url }}" />
{% endif %}
<meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="moe::virt" />
<meta property="twitter:title" content="{{ html_title }}" />
<meta property="twitter:description" content="{{ page_description }}" />
{% if page_image_url %}
<meta property="twitter:image" content="{{ page_image_url }}" />
{% endif %}
{% endblock %}
''')
loc_data: dict = None
@@ -31,13 +75,13 @@ def get_cf_location(loc: str):
return data.get('city')
def fill_cf_template_params(params: dict):
def fill_cf_template_params(params: ErrorPageParams):
# Get the real Ray ID / data center location from Cloudflare header
ray_id_loc = request.headers.get('Cf-Ray')
if ray_id_loc:
params['ray_id'] = ray_id_loc[:16]
cf_status: dict = params.get('cloudflare_status')
cf_status = params.get('cloudflare_status')
if cf_status is None:
cf_status = params['cloudflare_status'] = {}
if not cf_status.get('location'):
@@ -61,7 +105,7 @@ def sanitize_user_link(link: str):
return '#' + link
def sanitize_page_param_links(param: dict):
def sanitize_page_param_links(param: ErrorPageParams):
more_info = param.get('more_information')
if more_info:
link = more_info.get('link')
@@ -72,3 +116,31 @@ def sanitize_page_param_links(param: dict):
link = perf_sec_by.get('link')
if link:
perf_sec_by['link'] = sanitize_user_link(link)
def render_extended_template(params: ErrorPageParams,
*args: Any,
**kwargs: Any) -> str:
fill_cf_template_params(params)
description = params.get('what_happened') or 'There is an internal server error on Cloudflare\'s network.'
description = re.sub(r'<\/?.*?>', '', description).strip()
status = 'ok'
cf_status_obj = params.get('cloudflare_status')
if cf_status_obj:
cf_status = cf_status_obj.get('status')
if cf_status == 'error':
status = 'error'
page_icon_url = current_app.config.get('PAGE_ICON_URL', '').replace('{status}', status)
page_icon_type = current_app.config.get('PAGE_ICON_TYPE')
page_image_url = current_app.config.get('PAGE_IMAGE_URL', '').replace('{status}', status)
return render_cf_error_page(params=params,
template=template,
base=base_template,
page_icon_url=page_icon_url,
page_icon_type=page_icon_type,
page_url=request.url,
page_description=description,
page_image_url=page_image_url,
*args,
**kwargs)

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -14,22 +14,23 @@
<meta name="description" content="Online editor to create customized Cloudflare-style error pages.">
<meta name="keywords" content="cloudflare,error,page,editor">
<link rel="canonical" href="https://virt.moe/cferr/editor/" />
<link rel="icon" type="image/png" href="assets/icon-ok-32x32.png">
<meta property="og:type" content="website" />
<meta property="og:site_name" content="moe::virt" />
<meta property="og:title" content="Cloudflare error page editor" />
<meta property="og:url" content="https://virt.moe/cferr/editor/" />
<meta property="og:description" content="Online editor to create customized Cloudflare-style error pages" />
<meta property="og:image" content="https://virt.moe/cferr/editor/assets/icon-ok-large.png" />
<meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="moe::virt" />
<meta property="twitter:title" content="Cloudflare error page editor" />
<meta property="twitter:description" content="Online editor to create customized Cloudflare-style error pages" />
<meta property="twitter:image" content="https://virt.moe/cferr/editor/assets/icon-ok-large.png" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<link href="https://virt.moe/assets/cloudflare-error-page/bootstrap.min.css" rel="stylesheet">
<script type="module" src="src/index.js"></script>
<style>
/* Layout: editor + preview */
@@ -60,6 +61,7 @@
display: flex;
flex-direction: column;
}
iframe.preview-frame {
height: 100% !important;
}
@@ -83,7 +85,6 @@
/* Compact form: label and control same row */
.form-row {
display: flex;
gap: .3rem;
align-items: center;
margin-bottom: .6rem;
}
@@ -124,7 +125,6 @@
height: var(--expanded-height);
border: 1px solid #ddd;
flex: 1 1 auto;
min-height: 360px;
border-radius: .375rem;
box-shadow: 0 1px 4px rgba(0, 0, 0, .06);
}
@@ -143,6 +143,80 @@
min-height: 80px;
resize: vertical;
}
.popover {
--bs-popover-body-padding-x: 0.7rem !important;
--bs-popover-body-padding-y: 0.5rem !important;
}
.save-as-dialog__container {
display: grid;
grid-template-areas:
"selector"
"code"
"buttons";
height: fit-content;
grid-template-columns: auto;
gap: 1em;
}
.save-as-dialog__selector {
grid-area: selector;
min-width: 10em;
}
.save-as-dialog__code {
grid-area: code;
width: 100%;
height: 100%;
min-height: 700px !important;
font-family: monospace;
font-size: 0.8em !important;
}
.save-as-dialog__buttons {
grid-area: buttons;
flex-direction: row;
}
.save-as-dialog__buttons>* {
flex: 1 1 auto;
}
@media (min-width: 576px) {
.save-as-dialog__container {
grid-template-areas:
"selector code"
"buttons code";
grid-template-columns: auto 2fr;
height: 100%;
}
.save-as-dialog__selector {
text-align: right;
}
.save-as-dialog__code {
min-height: 0 !important;
}
.save-as-dialog__buttons {
grid-area: buttons;
flex-direction: column;
justify-content: flex-end !important;
}
.save-as-dialog__buttons>* {
flex: none;
}
}
@media (min-width: 992px) {
.save-as-dialog__selector {
grid-area: selector;
min-width: 13em;
}
}
</style>
</head>
@@ -199,8 +273,10 @@
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Browser</strong>
<div>
<label for="err_browser" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" />
<label for="err_browser" class="ms-1 small">Error here</label>
Error here
</label>
</div>
</div>
@@ -241,9 +317,11 @@
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Cloudflare</strong>
<div>
<label for="err_cloudflare" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_cloudflare"
value="cloudflare" />
<label for="err_cloudflare" class="ms-1 small">Error here</label>
Error here
</label>
</div>
</div>
@@ -284,8 +362,10 @@
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Host</strong>
<div>
<label for="err_host" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_host" value="host" />
<label for="err_host" class="ms-1 small">Error here</label>
Error here
</label>
</div>
</div>
@@ -326,8 +406,10 @@
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Visit ...</strong>
<div>
<label for="more_hidden" class="ms-1 small">
<input id="more_hidden" class="form-check-input" type="checkbox" />
<label for="more_hidden" class="ms-1 small">Hidden</label>
Hidden
</label>
</div>
</div>
@@ -388,15 +470,19 @@
<div class="d-flex gap-2 mt-2 mb-2">
<!-- <button id="btnRender" class="btn btn-sm btn-primary">Render</button> -->
<button id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button>
<button id="btnExport" class="btn btn-sm btn-primary">Export JSON</button>
<button type="button" id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button>
<!-- Button trigger modal -->
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#saveAsDialog">
Save as...
</button>
</div>
<button id="btnShare" class="btn btn-sm btn-primary">Create shareable link</button>
<button type="button" id="btnShare" class="btn btn-sm btn-secondary">Create shareable link</button>
<div class="mt-2">
<div class="input-group input-group-sm">
<input id="shareLink" class="form-control" readonly />
<button id="btnCopyLink" class="btn btn-outline-secondary" type="button">Copy</button>
<button type="button" id="btnCopyLink" class="btn btn-outline-secondary" data-bs-toggle="popover"
data-bs-placement="top" data-bs-trigger="manual" data-bs-content="Copied">Copy</button>
</div>
</div>
@@ -422,8 +508,51 @@
<!-- TODO: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. -->
<iframe id="previewFrame" class="preview-frame" sandbox="allow-scripts allow-same-origin"></iframe>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="saveAsDialog" tabindex="-1" aria-labelledby="saveAsDialogLabel">
<div class="modal-dialog modal-xl modal-fullscreen-lg-down modal-dialog-scrollable">
<div class="modal-content h-100">
<div class="modal-header">
<h5 class="modal-title" id="saveAsDialogLabel">Save As ...</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mx-2 save-as-dialog__container">
<div id="saveAsDialogTypes" class="list-group save-as-dialog__selector">
<button type="button" data-type="json" class="list-group-item list-group-item-action active">
JSON
</button>
<button type="button" data-type="python" class="list-group-item list-group-item-action">
Python Example
</button>
<button type="button" data-type="js" class="list-group-item list-group-item-action">
NodeJS Example
</button>
<button type="button" data-type="static" class="list-group-item list-group-item-action">
Static Page
</button>
</div>
<div class="d-flex gap-1 save-as-dialog__buttons">
<button type="button" class="btn btn-success" id="saveAsDialogCopyBtn" data-bs-toggle="popover"
data-bs-placement="right" data-bs-trigger="manual" data-bs-content="Copied">
Copy
</button>
<button type="button" class="btn btn-primary" id="saveAsDialogSaveBtn">
Save
</button>
</div>
<textarea id="saveAsDialogCode" class="form-control save-as-dialog__code" spellcheck="false"
readonly></textarea>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -10,11 +10,15 @@
"preview": "npm run build && vite preview"
},
"devDependencies": {
"@types/bootstrap": "^5.2.10",
"@types/ejs": "^3.1.5",
"@types/html-minifier-terser": "^7.0.2",
"@types/node": "^24.10.2",
"html-minifier-terser": "^7.2.0",
"prettier": "3.7.4",
"typescript": "^5.9.3",
"vite": "^7.2.6"
"vite": "^7.2.6",
"vite-plugin-static-copy": "^3.1.4"
},
"dependencies": {
"bootstrap": "^5.3.8",

4
editor/web/src/assets.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module '*?raw' {
const content: string;
export default content;
}

View File

@@ -0,0 +1,26 @@
import ejs from 'ejs';
import jsTemplate from './js.ejs?raw';
import jsonTemplate from './json.ejs?raw';
import pythonTemplate from './python.ejs?raw';
interface CodeGen {
name: string;
generate(params: any): string;
}
class EjsCodeGen implements CodeGen {
name: string;
private template: ejs.TemplateFunction;
constructor(name: string, templateContent: any) {
this.name = name;
this.template = ejs.compile(templateContent);
}
generate(params: any): string {
return this.template({ params });
}
}
export const jsCodeGen = new EjsCodeGen('NodeJS Example', jsTemplate);
export const jsonCodeGen = new EjsCodeGen('JSON', jsonTemplate);
export const pythonCodeGen = new EjsCodeGen('Python Example', pythonTemplate);

View File

@@ -0,0 +1,16 @@
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
<%# TODO: format to JS-style object (key w/o parens) _%>
app.get('/', (req, res) => {
res.status(500).send(render_cf_error_page(<%-JSON.stringify(params, null, 2).replaceAll('\n', '\n ')%>));
});
// Start the server and listen on the specified port
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

View File

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

View File

@@ -0,0 +1,29 @@
<%
// Covert the parameters to Python format object
const randomKey = Math.random() + ''
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')
_%>
from flask import Flask
from cloudflare_error_page import render as render_cf_error_page
app = Flask(__name__)
# Define a route for GET requests to the root URL
@app.route('/')
def index():
# Render the error page
return render_cf_error_page(<%- paramsArg.replaceAll('\n', '\n ') %>), 500
if __name__ == '__main__':
app.run(debug=True, port=5000)

View File

@@ -5,9 +5,14 @@
- inputs call render() on change
- "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 ejs from 'ejs';
import templateContent from './template.ejs?raw';
import 'bootstrap/js/src/modal.js';
import Popover from 'bootstrap/js/src/popover.js';
import 'bootstrap/dist/css/bootstrap.min.css';
import { jsCodeGen, jsonCodeGen, pythonCodeGen } from './codegen';
let template = ejs.compile(templateContent);
@@ -139,7 +144,7 @@ function getDefaultPresetName() {
name = extractUrlParam(window.location.hash.substring(1), key);
}
if (name) {
name = name.replace(/[^\w\d]/g, '')
name = name.replace(/[^\w\d]/g, '');
}
return name;
}
@@ -300,10 +305,8 @@ function render() {
let lastRenderedHtml = '';
function openInNewTab() {
if (!lastRenderedHtml) render();
const blob = new Blob([lastRenderedHtml], { type: 'text/html' });
const url = URL.createObjectURL(blob);
window.open(url, '_blank', 'noopener');
// note that this url won't be revoked
const wnd = window.open()
wnd.document.documentElement.innerHTML = lastRenderedHtml
}
function createShareableLink() {
@@ -331,22 +334,6 @@ function createShareableLink() {
$('shareLink').value = result.url;
});
}
function exportJSON() {
let content = JSON.stringify(lastCfg, null, 4);
const file = new File([content], 'cloudflare-error-page-params.json', {
type: 'text/plain',
});
const url = URL.createObjectURL(file);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
function resizePreviewFrame() {
const iframe = $('previewFrame');
const height = iframe.contentWindow.document.body.scrollHeight + 2;
@@ -371,15 +358,6 @@ function setBlockClass(id, cls) {
el.classList.add(cls);
}
/* Simple debounce */
function debounce(fn, wait) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
}
/* Wire up events */
// initialize form values from initialConfig
loadConfig(initialConfig);
@@ -395,42 +373,29 @@ $('presetSelect').addEventListener('change', (e) => {
// Render / Open button handlers
// $('btnRender').addEventListener('click', e => { e.preventDefault(); render(); });
$('btnOpen').addEventListener('click', (e) => {
e.preventDefault();
openInNewTab();
});
$('btnShare').addEventListener('click', (e) => {
e.preventDefault();
createShareableLink();
});
$('btnExport').addEventListener('click', (e) => {
e.preventDefault();
exportJSON();
});
const shareLinkPopover = new Popover($('btnCopyLink'));
$('btnCopyLink').addEventListener('click', () => {
const field = $('shareLink');
field.select();
field.setSelectionRange(0, field.value.length);
navigator.clipboard.writeText(field.value).then(() => {
// No notification required unless you want one
shareLinkPopover.show();
setTimeout(() => {
shareLinkPopover.hide();
}, 2000);
});
});
// Input change -> render
const inputs = document.querySelectorAll('#editorForm input, #editorForm textarea, #editorForm select');
inputs.forEach((inp) => {
inp.addEventListener(
'input',
debounce(() => {
// Update status block color classes for quick visual feedback in the editor
render();
}, 200)
);
inp.addEventListener('input', () => render());
// for radio change events (error_source)
if (inp.type === 'radio')
inp.addEventListener('change', () => {
render();
});
if (inp.type === 'radio') inp.addEventListener('change', () => render());
});
// Automatically update frame height
@@ -439,3 +404,103 @@ const iframe = $('previewFrame');
observer.observe(iframe.contentWindow.document.body);
// resizePreviewFrame()
setInterval(resizePreviewFrame, 1000); // TODO...
function saveFile(content, saveName) {
const file = new File([content], saveName, {
type: 'text/plain',
});
const url = URL.createObjectURL(file);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
let saveAsType;
let saveAsContent;
function updateSaveAsDialog(e) {
if (e) {
const target = e.target;
saveAsType = target.dataset.type;
} else {
saveAsType = 'json';
}
let codegen;
switch (saveAsType) {
case 'js':
codegen = jsCodeGen;
break;
case 'json':
codegen = jsonCodeGen;
break;
case 'python':
codegen = pythonCodeGen;
break;
}
const params = { ...lastCfg };
delete params.time;
if (codegen) {
saveAsContent = codegen.generate(params);
} else if (saveAsType == 'static') {
render() // rerender the page
saveAsContent = lastRenderedHtml;
} else {
throw new Error('unexpected saveAsType=' + saveAsType)
}
$('saveAsDialogCode').value = saveAsContent;
$('saveAsDialogCode').scrollTop = 0;
document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
const isCurrent = element.dataset.type == saveAsType;
if (isCurrent) {
element.classList.add('active');
} else {
element.classList.remove('active');
}
element.ariaCurrent = isCurrent;
});
}
$('saveAsDialog').addEventListener('show.bs.modal', (e) => {
updateSaveAsDialog();
});
document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
element.addEventListener('click', updateSaveAsDialog);
});
const saveAsDialogCopyPopover = new Popover($('saveAsDialogCopyBtn'));
$('saveAsDialogCopyBtn').addEventListener('click', (e) => {
const field = $('saveAsDialogCode');
field.select();
// field.setSelectionRange(0, field.value.length);
navigator.clipboard.writeText(field.value).then(() => {
saveAsDialogCopyPopover.show();
setTimeout(() => {
saveAsDialogCopyPopover.hide();
}, 2000);
});
});
$('saveAsDialogSaveBtn').addEventListener('click', (e) => {
let saveName = '';
switch (saveAsType) {
case 'js':
saveName = 'cf-error-page-example.js';
break;
case 'json':
saveName = 'cf-error-page-params.json';
break;
case 'python':
saveName = 'cf_error_page_example.py';
break;
case 'static':
saveName = 'cf_error_page.html';
break;
// TODO: name output files using page title
}
saveFile(saveAsContent, saveName);
});

View File

@@ -1,9 +1,8 @@
/// <reference types="vite/types/importMeta.d.ts" />
import { defineConfig, loadEnv } from 'vite';
import { defineConfig } from 'vite';
import { minify as htmlMinify } from 'html-minifier-terser';
import process from 'node:process';
import { viteStaticCopy } from 'vite-plugin-static-copy';
export default defineConfig(({ mode }) => {
const baseUrl = mode === 'production' ? '' : '/editor/';
@@ -17,7 +16,7 @@ export default defineConfig(({ mode }) => {
server: {
port: 3000,
proxy: {
'/s': {
'/s/': {
target: 'http://localhost:5000',
},
},
@@ -38,6 +37,14 @@ export default defineConfig(({ mode }) => {
},
},
},
viteStaticCopy({
targets: [
{
src: 'assets/',
dest: '',
},
],
}),
],
};
});

View File

@@ -166,6 +166,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@popperjs/core@^2.9.2":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@rollup/rollup-android-arm-eabi@4.53.3":
version "4.53.3"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz#7e478b66180c5330429dd161bf84dad66b59c8eb"
@@ -276,11 +281,28 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe"
integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==
"@types/bootstrap@^5.2.10":
version "5.2.10"
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.10.tgz#58506463bccc6602bc051487ad8d3a6458f94c6c"
integrity sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==
dependencies:
"@popperjs/core" "^2.9.2"
"@types/ejs@^3.1.5":
version "3.1.5"
resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.5.tgz#49d738257cc73bafe45c13cb8ff240683b4d5117"
integrity sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==
"@types/estree@1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/html-minifier-terser@^7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-7.0.2.tgz#2290fa13e6e49b6cc0ab0afa2d6cf6a66feedb48"
integrity sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==
"@types/node@^24.10.2":
version "24.10.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.2.tgz#82a57476a19647d8f2c7750d0924788245e39b26"
@@ -293,6 +315,14 @@ acorn@^8.15.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
async@^3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
@@ -303,6 +333,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
bootstrap@^5.3.8:
version "5.3.8"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.8.tgz#6401a10057a22752d21f4e19055508980656aeed"
@@ -315,6 +350,13 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -328,6 +370,21 @@ camel-case@^4.1.2:
pascal-case "^3.1.2"
tslib "^2.0.3"
chokidar@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
clean-css@~5.3.2:
version "5.3.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd"
@@ -409,11 +466,25 @@ filelist@^1.0.4:
dependencies:
minimatch "^5.0.1"
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
html-minifier-terser@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942"
@@ -427,6 +498,30 @@ html-minifier-terser@^7.2.0:
relateurl "^0.2.7"
terser "^5.15.1"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
jake@^10.8.5:
version "10.9.4"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6"
@@ -463,6 +558,16 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
p-map@^7.0.3:
version "7.0.4"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-7.0.4.tgz#b81814255f542e252d5729dca4d66e5ec14935b8"
integrity sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==
param-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
@@ -484,6 +589,11 @@ picocolors@^1.1.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
@@ -503,6 +613,13 @@ prettier@3.7.4:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
relateurl@^0.2.7:
version "0.2.7"
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
@@ -575,6 +692,13 @@ tinyglobby@^0.2.15:
fdir "^6.5.0"
picomatch "^4.0.3"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tslib@^2.0.3:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
@@ -590,6 +714,16 @@ undici-types@~7.16.0:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
vite-plugin-static-copy@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.4.tgz#d8365b717c2506885ca9a51457a1bcfe6f3a2bef"
integrity sha512-iCmr4GSw4eSnaB+G8zc2f4dxSuDjbkjwpuBLLGvQYR9IW7rnDzftnUjOH5p4RYR+d4GsiBqXRvzuFhs5bnzVyw==
dependencies:
chokidar "^3.6.0"
p-map "^7.0.3"
picocolors "^1.1.1"
tinyglobby "^0.2.15"
vite@^7.2.6:
version "7.2.7"
resolved "https://registry.yarnpkg.com/vite/-/vite-7.2.7.tgz#0789a4c3206081699f34a9ecca2dda594a07478e"

View File

@@ -1,19 +1,18 @@
#!/usr/bin/env python3
import os
import re
import sys
from flask import (
Flask,
request,
send_from_directory
)
# Append this directory to sys.path is not required if the package is already installed
examples_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(examples_dir))
from cloudflare_error_page import ErrorPageParams
from cloudflare_error_page import render as render_cf_error_page
app = Flask(__name__)
@@ -21,7 +20,7 @@ app = Flask(__name__)
@app.route('/')
def index():
params = {
params: ErrorPageParams = {
"title": "Internal server error",
"error_code": 500,
"browser_status": {

View File

@@ -1,5 +1,7 @@
node_modules/
dist/
*.tgz
*.log
*.html
.DS_Store

View File

@@ -11,7 +11,7 @@ npm install cloudflare-error-page
Or install from GitHub:
```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
@@ -34,7 +34,7 @@ fs.writeFileSync('error.html', errorPage);
## 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.
@@ -42,6 +42,7 @@ Generates an HTML error page based on the provided 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`
- `moreArgs` (optional): More arguments passed to the ejs template
#### ErrorPageParams Interface

677
javascript/package-lock.json generated Normal file
View File

@@ -0,0 +1,677 @@
{
"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": {
"@rollup/plugin-typescript": "^12.3.0",
"@rollup/pluginutils": "^5.3.0",
"@types/ejs": "^3.1.5",
"@types/node": "^20.0.0",
"rollup": "^4.54.0",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@rollup/plugin-typescript": {
"version": "12.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz",
"integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.14.0||^3.0.0||^4.0.0",
"tslib": "*",
"typescript": ">=3.7.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
},
"tslib": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"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/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"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/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"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/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"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/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"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/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.54.0",
"@rollup/rollup-android-arm64": "4.54.0",
"@rollup/rollup-darwin-arm64": "4.54.0",
"@rollup/rollup-darwin-x64": "4.54.0",
"@rollup/rollup-freebsd-arm64": "4.54.0",
"@rollup/rollup-freebsd-x64": "4.54.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
"@rollup/rollup-linux-arm64-musl": "4.54.0",
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
"@rollup/rollup-linux-x64-gnu": "4.54.0",
"@rollup/rollup-linux-x64-musl": "4.54.0",
"@rollup/rollup-openharmony-arm64": "4.54.0",
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
"@rollup/rollup-win32-x64-gnu": "4.54.0",
"@rollup/rollup-win32-x64-msvc": "4.54.0",
"fsevents": "~2.3.2"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"peer": true
},
"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",
"peer": true,
"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"
}
}
}

View File

@@ -1,12 +1,13 @@
{
"name": "cloudflare-error-page",
"version": "0.0.1",
"version": "0.1.0",
"description": "Cloudflare Error Page Generator",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build": "tsc",
"build": "node scripts/copy-files.js && rollup -c",
"dev": "rollup -c -w",
"prepublishOnly": "pnpm run build",
"example": "node examples/example.js"
},
@@ -32,9 +33,12 @@
"ejs": "^3.1.10"
},
"devDependencies": {
"@rollup/plugin-typescript": "^12.3.0",
"@rollup/pluginutils": "^5.3.0",
"@types/ejs": "^3.1.5",
"@types/node": "^20.0.0",
"typescript": "^5.3.0"
"rollup": "^4.54.0",
"tslib": "^2.8.1"
},
"files": [
"dist/**/*",

View File

@@ -0,0 +1,35 @@
import typescript from "@rollup/plugin-typescript";
import { createFilter } from "@rollup/pluginutils";
// import pkg from './package.json' with { type: 'json' };
function createRawImportPlugin(include: string) {
const rawFilter = createFilter(include);
return {
name: "raw-import",
transform(code: string, id: string): any {
if (rawFilter(id)) {
return {
code: `export default ${JSON.stringify(code)};`,
map: { mappings: "" },
};
}
},
};
}
export default {
input: "src/index.ts",
output: {
// file: pkg.module,
file: "dist/index.js",
format: "esm",
sourcemap: true,
},
watch: {
include: "src/**",
},
external: ["ejs"],
plugins: [typescript(), createRawImportPlugin("**/templates/**")],
};

View File

@@ -0,0 +1,14 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
// Resolve the directory of this script
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Define paths relative to scripts/copy.js
const src = path.resolve(__dirname, "../../resources/styles/main.css");
const dest = path.resolve(__dirname, "../src/templates/main.css");
// Copy file
fs.copyFileSync(src, dest);

4
javascript/src/assets.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "./templates/*" {
const content: string;
export default content;
}

101
javascript/src/index.ts Normal file
View File

@@ -0,0 +1,101 @@
import * as ejs from "ejs";
import templateString from "./templates/template.ejs";
import cssString from "./templates/main.css";
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;
}
// Load EJS template
export const baseTemplate: ejs.TemplateFunction = ejs.compile(templateString);
/**
* Generate random hex string for ray-id
*/
function genHexString(digits: number): string {
const hex = "0123456789ABCDEF";
let output = "";
for (let i = 0; i < digits; i++) {
output += hex.charAt(Math.floor(Math.random() * hex.length));
}
return output;
}
/**
* 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)
* @param moreArgs - More arguments passed to the ejs template
* @returns The rendered HTML string
*/
export function render(
params: ErrorPageParams,
allowHtml: boolean = true,
moreArgs: {
[name: string]: any;
} = {}
): string {
params = { ...params };
if (!params.time) {
const now = new Date();
params.time = now.toISOString().replace("T", " ").substring(0, 19) + " UTC";
}
if (!params.ray_id) {
params.ray_id = genHexString(16);
}
if (!allowHtml) {
params.what_happened = ejs.escapeXML(params.what_happened ?? "");
params.what_can_i_do = ejs.escapeXML(params.what_can_i_do ?? "");
}
return baseTemplate({ params, html_style: cssString, ...moreArgs });
}
export default render;

1
javascript/src/templates/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
main.css

View File

@@ -4,18 +4,18 @@
<!--[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]-->
<head>
<%
let error_code = params.error_code || 500;
let title = params.title || 'Internal server error';
let html_title_output = params.html_title || (error_code + ': ' + title);
%>
<title><%= html_title_output %></title>
<% const error_code = params.error_code || 500; %>
<% const title = params.title || 'Internal server error'; %>
<% const html_title = params.html_title || (error_code + ': ' + title); %>
<title><%= html_title %></title>
<meta charset="UTF-8" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!-- @INLINE_CSS_HERE@ -->
<style>
<%- html_style %>
</style>
</head>
<body>
<div id="cf-wrapper">
@@ -25,10 +25,10 @@ let html_title_output = params.html_title || (error_code + ': ' + title);
<span class="inline-block"><%= title %></span>
<span class="code-label">Error code <%= error_code %></span>
</h1>
<% let more_info = params.more_information || {}; %>
<% const more_info = params.more_information || {}; %>
<% if (!more_info.hidden) { %>
<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 class="<%= more_info.hidden ? '' : 'mt-3' %>"><%= params.time %></div>
@@ -36,27 +36,16 @@ let html_title_output = params.html_title || (error_code + ': ' + title);
<div class="my-8 bg-gradient-gray">
<div class="w-240 lg:w-full mx-auto">
<div class="clearfix md:px-8">
<% for (let item_id of ['browser', 'cloudflare', 'host']) { %>
<%
let icon, default_location, default_name, text_color, status_text;
if (item_id === 'browser') {
icon = 'browser';
default_location = 'You';
default_name = 'Browser';
} else if (item_id === 'cloudflare') {
icon = 'cloud';
default_location = 'San Francisco';
default_name = 'Cloudflare';
} else {
icon = 'server';
default_location = 'Website';
default_name = 'Host';
}
let item = params[item_id + '_status'] || {};
let status = item.status || 'ok';
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') {
@@ -64,27 +53,19 @@ let html_title_output = params.html_title || (error_code + ': ' + title);
} else if (status === 'error') {
text_color = '#bd2426'; // text-red-error
}
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;
%>
<div id="cf-<%= item_id %>-status" class="<% if (params.error_source === item_id) { %>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">
<span class="cf-icon-<%= icon %> block md:hidden h-20 bg-center bg-no-repeat"></span>
<span class="cf-icon-<%= status %> w-12 h-12 absolute left-1/2 md:left-auto md:right-0 md:top-0 -ml-6 -bottom-4"></span>
</div>
<span class="md:block w-full truncate"><%= item.location || default_location %></span>
<%
let _name_style;
if ((item.name || default_name) === 'Cloudflare') {
_name_style = 'style="color: #2f7bbf;"'
} else{
_name_style = ''
}
%>
<h3 class="md:inline-block mt-3 md:mt-0 text-2xl text-gray-600 font-light leading-1.3" <%-_name_style %>><%= 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>
</div>
<% } %>
<% }); %>
</div>
</div>
</div>
@@ -93,11 +74,11 @@ let html_title_output = params.html_title || (error_code + ': ' + title);
<div class="clearfix">
<div class="w-1/2 md:w-full float-left pr-6 md:pb-10 md:pr-0 leading-relaxed">
<h2 class="text-3xl font-normal leading-1.3 mb-4">What happened?</h2>
<%= (params.what_happened || 'There is an internal server error on Cloudflare\'s network.') %>
<%- params.what_happened || '<p>There is an internal server error on Cloudflare\'s network.</p>' %>
</div>
<div class="w-1/2 md:w-full float-left leading-relaxed">
<h2 class="text-3xl font-normal leading-1.3 mb-4">What can I do?</h2>
<%= (params.what_can_i_do || 'Please try again in a few minutes.') %>
<%- params.what_can_i_do || '<p>Please try again in a few minutes.</p>' %>
</div>
</div>
</div>
@@ -112,8 +93,14 @@ let html_title_output = params.html_title || (error_code + ': ' + title);
<span class="hidden" id="cf-footer-ip"><%= params.client_ip || '1.1.1.1' %></span>
<span class="cf-footer-separator sm:hidden">&bull;</span>
</span>
<% let 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>
<% const creator_info = params.creator_info || {}; %>
<% if (!(creator_info.hidden ?? true)) { %>
<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>
<% } %>
</p>
</div><!-- /.error-footer -->
</div>

142
nodejs/package-lock.json generated
View File

@@ -1,142 +0,0 @@
{
"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"
}
}
}

View File

@@ -1,123 +0,0 @@
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;

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,6 @@ build-backend = "hatchling.build"
[project]
name = "cloudflare-error-page"
version = "0.1.0"
description = "A customizable Cloudflare error page generator"
authors = [{ name = "Anthony Donlon" }]
license = { text = "MIT" }
@@ -13,6 +12,7 @@ requires-python = ">=3.10"
dependencies = [
"jinja2>=3.0"
]
dynamic = [ "version" ]
[project.urls]
Homepage = "https://github.com/donlon/cloudflare-error-page"
@@ -22,3 +22,9 @@ include = [
"cloudflare_error_page/*.py",
"cloudflare_error_page/templates/*",
]
[tool.hatch.build.targets.wheel.hooks.custom]
path = "scripts/hatch_build.py"
[tool.hatch.version]
path = "cloudflare_error_page/__init__.py"

File diff suppressed because one or more lines are too long

17
scripts/hatch_build.py Normal file
View File

@@ -0,0 +1,17 @@
import os
import sys
import shutil
from pathlib import Path
from typing import Any
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
sys.path.append(os.path.dirname(__file__))
from inline_resources import generate_inlined_css
class CustomBuildHook(BuildHookInterface):
def initialize(self, version: str, build_data: dict[str, Any]):
generate_inlined_css()
src = Path(self.root) / 'resources' / 'styles' / 'main.css'
dst = Path(self.root) / 'cloudflare_error_page' / 'templates'
shutil.copy(src, dst)

View File

@@ -49,7 +49,7 @@ def inline_css_resource(original_file: str, css_file: str, output_file: str):
write_file(output_file, original_data)
if __name__ == '__main__':
def generate_inlined_css():
inline_svg_resources(
os.path.join(resources_folder, 'styles/main-original.css'),
[
@@ -61,13 +61,7 @@ if __name__ == '__main__':
],
os.path.join(resources_folder, 'styles/main.css'),
)
inline_css_resource(
os.path.join(resources_folder, 'templates/error.html'),
os.path.join(resources_folder, 'styles/main.css'),
os.path.join(root, 'cloudflare_error_page/templates/error.html'),
)
inline_css_resource(
os.path.join(resources_folder, 'templates/error.ejs'),
os.path.join(resources_folder, 'styles/main.css'),
os.path.join(root, 'editor/web/src/template.ejs'),
)
if __name__ == '__main__':
generate_inlined_css()