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

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,18 @@
# 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 = ''
# MIME type of page icon
PAGE_ICON_TYPE = 'image/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 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 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,29 @@ 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 '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="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-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);
} }
@@ -199,8 +199,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>
<input class="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" /> <label for="err_browser" class="ms-1 small">
<label for="err_browser" class="ms-1 small">Error here</label> <input class="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" />
Error here
</label>
</div> </div>
</div> </div>
@@ -241,9 +243,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>
<input class="form-check-input" type="radio" name="error_source" id="err_cloudflare" <label for="err_cloudflare" class="ms-1 small">
value="cloudflare" /> <input class="form-check-input" type="radio" name="error_source" id="err_cloudflare"
<label for="err_cloudflare" class="ms-1 small">Error here</label> value="cloudflare" />
Error here
</label>
</div> </div>
</div> </div>
@@ -284,8 +288,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>
<input class="form-check-input" type="radio" name="error_source" id="err_host" value="host" /> <label for="err_host" class="ms-1 small">
<label for="err_host" class="ms-1 small">Error here</label> <input class="form-check-input" type="radio" name="error_source" id="err_host" value="host" />
Error here
</label>
</div> </div>
</div> </div>
@@ -326,8 +332,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>
<input id="more_hidden" class="form-check-input" type="checkbox" /> <label for="more_hidden" class="ms-1 small">
<label for="more_hidden" class="ms-1 small">Hidden</label> <input id="more_hidden" class="form-check-input" type="checkbox" />
Hidden
</label>
</div> </div>
</div> </div>
@@ -426,4 +434,5 @@
</div> </div>
</div> </div>
</body> </body>
</html>
</html>

View File

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

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

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