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

22 Commits

Author SHA1 Message Date
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
29 changed files with 730 additions and 268 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. Install `cloudflare-error-page` with pip.
``` Bash ``` 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 pip install git+https://github.com/donlon/cloudflare-error-page.git
``` ```

View File

@@ -1,21 +1,75 @@
import html import html
import secrets import secrets
import sys
from datetime import datetime, timezone 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 from jinja2 import Environment, PackageLoader, Template, select_autoescape
env = Environment( jinja_env = Environment(
loader=PackageLoader(__name__), loader=PackageLoader(__name__),
autoescape=select_autoescape(), autoescape=select_autoescape(),
trim_blocks=True, trim_blocks=True,
lstrip_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, allow_html: bool = True,
template: Template | None = None, template: Template | None = None,
*args: Any, *args: Any,
@@ -31,13 +85,19 @@ def render(params: dict,
:return: The rendered error page as a string. :return: The rendered error page as a string.
''' '''
if not template: if not template:
template = default_template template = base_template
params = {**params} 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'): if not params.get('time'):
utc_now = datetime.now(timezone.utc) 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'): if not params.get('ray_id'):
params['ray_id'] = secrets.token_hex(8) params['ray_id'] = secrets.token_hex(8)
if not allow_html: if not allow_html:
@@ -46,5 +106,5 @@ def render(params: dict,
return template.render(params=params, *args, **kwargs) return template.render(params=params, *args, **kwargs)
__version__ = '0.2.0'
__all__ = ['default_template', 'render'] __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 http-equiv="X-UA-Compatible" content="IE=Edge" />
<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" />
{% block header %}{% endblock %} {% block html_head %}{% endblock %}
<!-- @INLINE_CSS_HERE@ --> <style>
{% if html_style %}
{# Support custom stylesheet #}
{{ html_style | safe }}
{% else %}
{# Default stylesheet #}
{% include 'main.css' %}
{% endif %}
</style>
</head> </head>
<body> <body>
<div id="cf-wrapper"> <div id="cf-wrapper">

View File

@@ -92,9 +92,11 @@ def create_app(test_config=None) -> Flask:
return '', 204 return '', 204
url_prefix = app.config.get('URL_PREFIX', '') 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(editor.bp, url_prefix=f'{url_prefix}/editor')
app.register_blueprint(examples.bp, url_prefix=f'{url_prefix}/examples') 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, 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 return app

View File

@@ -1,6 +1,21 @@
# Url prefix for app urls # Url prefix for app urls
URL_PREFIX = '' 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 # Set to true if trust X-Forwarded-For/X-Forwarded-Proto header
BEHIND_PROXY = true BEHIND_PROXY = true

View File

@@ -11,8 +11,10 @@ from flask import (
redirect, redirect,
) )
from cloudflare_error_page import render as render_cf_error_page from cloudflare_error_page import ErrorPageParams
from .utils import fill_cf_template_params from .utils import (
render_extended_template,
)
root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../') root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../')
examples_dir = os.path.join(root_dir, 'examples') examples_dir = os.path.join(root_dir, 'examples')
@@ -22,7 +24,7 @@ bp = Blueprint('examples', __name__, url_prefix='/')
param_cache: dict[str, dict] = {} 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) name = re.sub(r'[^\w]', '', name)
params = param_cache.get(name) params = param_cache.get(name)
if params is not None: if params is not None:
@@ -49,7 +51,5 @@ def index(name: str):
if params is None: if params is None:
abort(404) abort(404)
fill_cf_template_params(params)
# Render the error page # 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 # SPDX-License-Identifier: MIT
import html
import random import random
import string import string
from typing import cast
from cloudflare_error_page import ErrorPageParams
from flask import ( from flask import (
Blueprint, Blueprint,
current_app,
request, request,
abort, abort,
jsonify, jsonify,
redirect,
url_for, 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 ( from . import (
db, db,
limiter, limiter,
models models,
) )
from .utils import (
from .utils import fill_cf_template_params, sanitize_page_param_links render_extended_template,
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,
) )
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 = Blueprint('share', __name__, url_prefix='/')
bp_short = Blueprint('share_short', __name__, url_prefix='/')
rand_charset = string.ascii_lowercase + string.digits rand_charset = string.ascii_lowercase + string.digits
@@ -81,7 +57,8 @@ def create():
# TODO: strip unused params # TODO: strip unused params
try: try:
item = models.Item() 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 item.params = params
db.session.add(item) db.session.add(item)
db.session.commit() db.session.commit()
@@ -93,12 +70,12 @@ def create():
return jsonify({ return jsonify({
'status': 'ok', 'status': 'ok',
'name': item.name, '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 # TODO: better way to handle this
}) })
@bp.get('/<name>') @bp_short.get('/<name>')
def get(name: str): def get(name: str):
accept = request.headers.get('Accept', '') accept = request.headers.get('Accept', '')
is_json = 'application/json' in accept is_json = 'application/json' in accept
@@ -111,7 +88,7 @@ def get(name: str):
}) })
else: else:
return abort(404) return abort(404)
params: dict = item.params params = cast(ErrorPageParams, item.params)
params.pop('time', None) params.pop('time', None)
params.pop('ray_id', None) params.pop('ray_id', None)
params.pop('client_ip', None) params.pop('client_ip', None)
@@ -127,12 +104,15 @@ def get(name: str):
'text': 'CF Error Page Editor', 'text': 'CF Error Page Editor',
'link': request.host_url[:-1] + url_for('editor.index') + f'#from={name}', 'link': request.host_url[:-1] + url_for('editor.index') + f'#from={name}',
} }
fill_cf_template_params(params)
sanitize_page_param_links(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, @bp.get('/<name>')
template=template, def get_redir(name: str):
base=cf_template, short_share_url = current_app.config.get('SHORT_SHARE_URL', False)
url=request.url, if short_share_url:
description='Cloudflare error page') return redirect(f'../{name}', code=308)
else:
return get(name=name)

View File

@@ -1,9 +1,53 @@
import json import json
import os 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 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 loc_data: dict = None
@@ -31,13 +75,13 @@ def get_cf_location(loc: str):
return data.get('city') 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 # Get the real Ray ID / data center location from Cloudflare header
ray_id_loc = request.headers.get('Cf-Ray') ray_id_loc = request.headers.get('Cf-Ray')
if ray_id_loc: if ray_id_loc:
params['ray_id'] = ray_id_loc[:16] 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: if cf_status is None:
cf_status = params['cloudflare_status'] = {} cf_status = params['cloudflare_status'] = {}
if not cf_status.get('location'): if not cf_status.get('location'):
@@ -61,7 +105,7 @@ def sanitize_user_link(link: str):
return '#' + link return '#' + link
def sanitize_page_param_links(param: dict): def sanitize_page_param_links(param: ErrorPageParams):
more_info = param.get('more_information') more_info = param.get('more_information')
if more_info: if more_info:
link = more_info.get('link') link = more_info.get('link')
@@ -72,3 +116,31 @@ def sanitize_page_param_links(param: dict):
link = perf_sec_by.get('link') link = perf_sec_by.get('link')
if link: if link:
perf_sec_by['link'] = sanitize_user_link(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="description" content="Online editor to create customized Cloudflare-style error pages.">
<meta name="keywords" content="cloudflare,error,page,editor"> <meta name="keywords" content="cloudflare,error,page,editor">
<link rel="canonical" href="https://virt.moe/cferr/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:type" content="website" />
<meta property="og:site_name" content="moe::virt" /> <meta property="og:site_name" content="moe::virt" />
<meta property="og:title" content="Cloudflare error page editor" /> <meta property="og:title" content="Cloudflare error page editor" />
<meta property="og:url" content="https://virt.moe/cferr/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: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:card" content="summary" />
<meta property="twitter:site" content="moe::virt" /> <meta property="twitter:site" content="moe::virt" />
<meta property="twitter:title" content="Cloudflare error page editor" /> <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: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" /> <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> <script type="module" src="src/index.js"></script>
<style> <style>
/* Layout: editor + preview */ /* Layout: editor + preview */
@@ -60,6 +61,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
iframe.preview-frame { iframe.preview-frame {
height: 100% !important; height: 100% !important;
} }
@@ -83,7 +85,6 @@
/* Compact form: label and control same row */ /* Compact form: label and control same row */
.form-row { .form-row {
display: flex; display: flex;
gap: .3rem;
align-items: center; align-items: center;
margin-bottom: .6rem; margin-bottom: .6rem;
} }
@@ -124,7 +125,6 @@
height: var(--expanded-height); height: var(--expanded-height);
border: 1px solid #ddd; border: 1px solid #ddd;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 360px;
border-radius: .375rem; border-radius: .375rem;
box-shadow: 0 1px 4px rgba(0, 0, 0, .06); box-shadow: 0 1px 4px rgba(0, 0, 0, .06);
} }
@@ -143,6 +143,80 @@
min-height: 80px; min-height: 80px;
resize: vertical; 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> </style>
</head> </head>
@@ -199,8 +273,10 @@
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<strong>Browser</strong> <strong>Browser</strong>
<div> <div>
<label for="err_browser" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" /> <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>
</div> </div>
@@ -241,9 +317,11 @@
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<strong>Cloudflare</strong> <strong>Cloudflare</strong>
<div> <div>
<label for="err_cloudflare" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_cloudflare" <input class="form-check-input" type="radio" name="error_source" id="err_cloudflare"
value="cloudflare" /> value="cloudflare" />
<label for="err_cloudflare" class="ms-1 small">Error here</label> Error here
</label>
</div> </div>
</div> </div>
@@ -284,8 +362,10 @@
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<strong>Host</strong> <strong>Host</strong>
<div> <div>
<label for="err_host" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_host" value="host" /> <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>
</div> </div>
@@ -326,8 +406,10 @@
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<strong>Visit ...</strong> <strong>Visit ...</strong>
<div> <div>
<label for="more_hidden" class="ms-1 small">
<input id="more_hidden" class="form-check-input" type="checkbox" /> <input id="more_hidden" class="form-check-input" type="checkbox" />
<label for="more_hidden" class="ms-1 small">Hidden</label> Hidden
</label>
</div> </div>
</div> </div>
@@ -388,15 +470,19 @@
<div class="d-flex gap-2 mt-2 mb-2"> <div class="d-flex gap-2 mt-2 mb-2">
<!-- <button id="btnRender" class="btn btn-sm btn-primary">Render</button> --> <!-- <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 type="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 trigger modal -->
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#saveAsDialog">
Save as...
</button>
</div> </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="mt-2">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input id="shareLink" class="form-control" readonly /> <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>
</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. --> <!-- 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> <iframe id="previewFrame" class="preview-frame" sandbox="allow-scripts allow-same-origin"></iframe>
</div> </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>
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -10,11 +10,15 @@
"preview": "npm run build && vite preview" "preview": "npm run build && vite preview"
}, },
"devDependencies": { "devDependencies": {
"@types/bootstrap": "^5.2.10",
"@types/ejs": "^3.1.5",
"@types/html-minifier-terser": "^7.0.2",
"@types/node": "^24.10.2", "@types/node": "^24.10.2",
"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",
"vite": "^7.2.6" "vite": "^7.2.6",
"vite-plugin-static-copy": "^3.1.4"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.3.8", "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 - 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 ejs from 'ejs';
import templateContent from './template.ejs?raw' 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); let template = ejs.compile(templateContent);
@@ -139,7 +144,7 @@ function getDefaultPresetName() {
name = extractUrlParam(window.location.hash.substring(1), key); name = extractUrlParam(window.location.hash.substring(1), key);
} }
if (name) { if (name) {
name = name.replace(/[^\w\d]/g, '') name = name.replace(/[^\w\d]/g, '');
} }
return name; return name;
} }
@@ -300,10 +305,8 @@ function render() {
let lastRenderedHtml = ''; let lastRenderedHtml = '';
function openInNewTab() { function openInNewTab() {
if (!lastRenderedHtml) render(); if (!lastRenderedHtml) render();
const blob = new Blob([lastRenderedHtml], { type: 'text/html' }); const wnd = window.open()
const url = URL.createObjectURL(blob); wnd.document.documentElement.innerHTML = lastRenderedHtml
window.open(url, '_blank', 'noopener');
// note that this url won't be revoked
} }
function createShareableLink() { function createShareableLink() {
@@ -331,22 +334,6 @@ function createShareableLink() {
$('shareLink').value = result.url; $('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() { function resizePreviewFrame() {
const iframe = $('previewFrame'); const iframe = $('previewFrame');
const height = iframe.contentWindow.document.body.scrollHeight + 2; const height = iframe.contentWindow.document.body.scrollHeight + 2;
@@ -371,15 +358,6 @@ function setBlockClass(id, cls) {
el.classList.add(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 */ /* Wire up events */
// initialize form values from initialConfig // initialize form values from initialConfig
loadConfig(initialConfig); loadConfig(initialConfig);
@@ -395,42 +373,29 @@ $('presetSelect').addEventListener('change', (e) => {
// Render / Open button handlers // Render / Open button handlers
// $('btnRender').addEventListener('click', e => { e.preventDefault(); render(); }); // $('btnRender').addEventListener('click', e => { e.preventDefault(); render(); });
$('btnOpen').addEventListener('click', (e) => { $('btnOpen').addEventListener('click', (e) => {
e.preventDefault();
openInNewTab(); openInNewTab();
}); });
$('btnShare').addEventListener('click', (e) => { $('btnShare').addEventListener('click', (e) => {
e.preventDefault();
createShareableLink(); createShareableLink();
}); });
$('btnExport').addEventListener('click', (e) => { const shareLinkPopover = new Popover($('btnCopyLink'));
e.preventDefault();
exportJSON();
});
$('btnCopyLink').addEventListener('click', () => { $('btnCopyLink').addEventListener('click', () => {
const field = $('shareLink'); const field = $('shareLink');
field.select(); field.select();
field.setSelectionRange(0, field.value.length);
navigator.clipboard.writeText(field.value).then(() => { navigator.clipboard.writeText(field.value).then(() => {
// No notification required unless you want one shareLinkPopover.show();
setTimeout(() => {
shareLinkPopover.hide();
}, 2000);
}); });
}); });
// Input change -> render // Input change -> render
const inputs = document.querySelectorAll('#editorForm input, #editorForm textarea, #editorForm select'); const inputs = document.querySelectorAll('#editorForm input, #editorForm textarea, #editorForm select');
inputs.forEach((inp) => { inputs.forEach((inp) => {
inp.addEventListener( inp.addEventListener('input', () => render());
'input',
debounce(() => {
// Update status block color classes for quick visual feedback in the editor
render();
}, 200)
);
// for radio change events (error_source) // for radio change events (error_source)
if (inp.type === 'radio') if (inp.type === 'radio') inp.addEventListener('change', () => render());
inp.addEventListener('change', () => {
render();
});
}); });
// Automatically update frame height // Automatically update frame height
@@ -439,3 +404,103 @@ const iframe = $('previewFrame');
observer.observe(iframe.contentWindow.document.body); observer.observe(iframe.contentWindow.document.body);
// resizePreviewFrame() // resizePreviewFrame()
setInterval(resizePreviewFrame, 1000); // TODO... 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" /> /// <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 { minify as htmlMinify } from 'html-minifier-terser';
import process from 'node:process'; import { viteStaticCopy } from 'vite-plugin-static-copy';
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const baseUrl = mode === 'production' ? '' : '/editor/'; const baseUrl = mode === 'production' ? '' : '/editor/';
@@ -17,7 +16,7 @@ export default defineConfig(({ mode }) => {
server: { server: {
port: 3000, port: 3000,
proxy: { proxy: {
'/s': { '/s/': {
target: 'http://localhost:5000', 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/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@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": "@rollup/rollup-android-arm-eabi@4.53.3":
version "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" 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" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe"
integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ== 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": "@types/estree@1.0.8":
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== 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": "@types/node@^24.10.2":
version "24.10.2" version "24.10.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.2.tgz#82a57476a19647d8f2c7750d0924788245e39b26" 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" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== 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: async@^3.2.6:
version "3.2.6" version "3.2.6"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" 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" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 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: bootstrap@^5.3.8:
version "5.3.8" version "5.3.8"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.8.tgz#6401a10057a22752d21f4e19055508980656aeed" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.8.tgz#6401a10057a22752d21f4e19055508980656aeed"
@@ -315,6 +350,13 @@ brace-expansion@^2.0.1:
dependencies: dependencies:
balanced-match "^1.0.0" 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: buffer-from@^1.0.0:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" 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" pascal-case "^3.1.2"
tslib "^2.0.3" 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: clean-css@~5.3.2:
version "5.3.3" version "5.3.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd"
@@ -409,11 +466,25 @@ filelist@^1.0.4:
dependencies: dependencies:
minimatch "^5.0.1" 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: fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3" version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== 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: html-minifier-terser@^7.2.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942" 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" relateurl "^0.2.7"
terser "^5.15.1" 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: jake@^10.8.5:
version "10.9.4" version "10.9.4"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6" 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" lower-case "^2.0.2"
tslib "^2.0.3" 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: param-case@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" 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" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== 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: picomatch@^4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" 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" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA== 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: relateurl@^0.2.7:
version "0.2.7" version "0.2.7"
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
@@ -575,6 +692,13 @@ tinyglobby@^0.2.15:
fdir "^6.5.0" fdir "^6.5.0"
picomatch "^4.0.3" 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: tslib@^2.0.3:
version "2.8.1" version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" 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" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== 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: vite@^7.2.6:
version "7.2.7" version "7.2.7"
resolved "https://registry.yarnpkg.com/vite/-/vite-7.2.7.tgz#0789a4c3206081699f34a9ecca2dda594a07478e" resolved "https://registry.yarnpkg.com/vite/-/vite-7.2.7.tgz#0789a4c3206081699f34a9ecca2dda594a07478e"

View File

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

View File

@@ -4,7 +4,6 @@ build-backend = "hatchling.build"
[project] [project]
name = "cloudflare-error-page" name = "cloudflare-error-page"
version = "0.1.0"
description = "A customizable Cloudflare error page generator" description = "A customizable Cloudflare error page generator"
authors = [{ name = "Anthony Donlon" }] authors = [{ name = "Anthony Donlon" }]
license = { text = "MIT" } license = { text = "MIT" }
@@ -13,6 +12,7 @@ requires-python = ">=3.10"
dependencies = [ dependencies = [
"jinja2>=3.0" "jinja2>=3.0"
] ]
dynamic = [ "version" ]
[project.urls] [project.urls]
Homepage = "https://github.com/donlon/cloudflare-error-page" Homepage = "https://github.com/donlon/cloudflare-error-page"
@@ -22,3 +22,9 @@ include = [
"cloudflare_error_page/*.py", "cloudflare_error_page/*.py",
"cloudflare_error_page/templates/*", "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) write_file(output_file, original_data)
if __name__ == '__main__': def generate_inlined_css():
inline_svg_resources( inline_svg_resources(
os.path.join(resources_folder, 'styles/main-original.css'), os.path.join(resources_folder, 'styles/main-original.css'),
[ [
@@ -61,11 +61,10 @@ if __name__ == '__main__':
], ],
os.path.join(resources_folder, 'styles/main.css'), 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'), if __name__ == '__main__':
os.path.join(root, 'cloudflare_error_page/templates/error.html'), generate_inlined_css()
)
inline_css_resource( inline_css_resource(
os.path.join(resources_folder, 'templates/error.ejs'), os.path.join(resources_folder, 'templates/error.ejs'),
os.path.join(resources_folder, 'styles/main.css'), os.path.join(resources_folder, 'styles/main.css'),