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

9 Commits

Author SHA1 Message Date
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
16 changed files with 342 additions and 106 deletions

View File

@@ -1,7 +1,15 @@
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
@@ -12,10 +20,56 @@ env = Environment(
lstrip_blocks=True,
)
default_template: Template = env.get_template("error.html")
base_template: Template = env.get_template("error.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,10 +85,16 @@ 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")
@@ -46,5 +106,5 @@ def render(params: dict,
return template.render(params=params, *args, **kwargs)
__all__ = ['default_template', 'render']
__version__ = "0.1.0"
__all__ = ['base_template', 'render']

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,18 @@
# 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 = ''
# MIME type of page icon
PAGE_ICON_TYPE = 'image/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 header %}
{% 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,29 @@ 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 'Cloudflare error page'
description = re.sub(r'</?.*?>', '', description).strip()
page_image_id = 'ok'
cf_status_obj = params.get('cloudflare_status')
if cf_status_obj:
cf_status = cf_status_obj.get('status')
if cf_status == 'error':
page_image_id = 'error'
page_image_url = f'https://virt.moe/cferr/editor/assets/icon-{page_image_id}-large.png'
return render_cf_error_page(params=params,
template=template,
base=base_template,
page_icon_url=current_app.config.get('PAGE_ICON_URL'),
page_icon_type=current_app.config.get('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: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

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-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);
}
@@ -199,8 +199,10 @@
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Browser</strong>
<div>
<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>
<label for="err_browser" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" />
Error here
</label>
</div>
</div>
@@ -241,9 +243,11 @@
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Cloudflare</strong>
<div>
<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>
<label for="err_cloudflare" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_cloudflare"
value="cloudflare" />
Error here
</label>
</div>
</div>
@@ -284,8 +288,10 @@
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Host</strong>
<div>
<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>
<label for="err_host" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_host" value="host" />
Error here
</label>
</div>
</div>
@@ -326,8 +332,10 @@
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Visit ...</strong>
<div>
<input id="more_hidden" class="form-check-input" type="checkbox" />
<label for="more_hidden" class="ms-1 small">Hidden</label>
<label for="more_hidden" class="ms-1 small">
<input id="more_hidden" class="form-check-input" type="checkbox" />
Hidden
</label>
</div>
</div>
@@ -426,4 +434,5 @@
</div>
</div>
</body>
</html>

View File

@@ -18,6 +18,7 @@
},
"dependencies": {
"bootstrap": "^5.3.8",
"ejs": "^3.1.10"
"ejs": "^3.1.10",
"vite-plugin-static-copy": "^3.1.4"
}
}

View File

@@ -5,8 +5,9 @@
- 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/dist/css/bootstrap.min.css';
let template = ejs.compile(templateContent);
@@ -139,7 +140,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;
}
@@ -371,15 +372,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);
@@ -419,18 +411,10 @@ $('btnCopyLink').addEventListener('click', () => {
// 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();
});
inp.addEventListener('change', () => render());
});
// Automatically update frame height

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

@@ -293,6 +293,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 +311,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 +328,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 +348,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 +444,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 +476,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 +536,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 +567,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 +591,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 +670,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 +692,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

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