mirror of
https://github.com/donlon/cloudflare-error-page.git
synced 2025-12-24 09:19:23 +00:00
Compare commits
22 Commits
python-v0.
...
python-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a5311212e | ||
|
|
9fbcba1a34 | ||
|
|
c0e576478a | ||
|
|
6233cec91f | ||
|
|
728ce52529 | ||
|
|
d0a05329d5 | ||
|
|
51b61a8a2d | ||
|
|
71f4e9507c | ||
|
|
38eb8d8c11 | ||
|
|
76a4696a40 | ||
|
|
21d14994de | ||
|
|
36a1119908 | ||
|
|
e66b5357a4 | ||
|
|
5bc662dd06 | ||
|
|
7229ad7281 | ||
|
|
a50b0289a0 | ||
|
|
a85624031c | ||
|
|
df5daebe34 | ||
|
|
eb0d5a7d55 | ||
|
|
245f5b1f6d | ||
|
|
58bf0d6b79 | ||
|
|
ed711a2521 |
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
BIN
editor/web/assets/icon-error-32x32.png
Normal file
BIN
editor/web/assets/icon-error-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 676 B |
BIN
editor/web/assets/icon-error-large-white.png
Normal file
BIN
editor/web/assets/icon-error-large-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
editor/web/assets/icon-ok-32x32.png
Normal file
BIN
editor/web/assets/icon-ok-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 708 B |
BIN
editor/web/assets/icon-ok-large-white.png
Normal file
BIN
editor/web/assets/icon-ok-large-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -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>
|
||||||
<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 +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>
|
||||||
<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 +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>
|
||||||
<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 +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>
|
||||||
<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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="modal fade" id="saveAsDialog" tabindex="-1" aria-labelledby="saveAsDialogLabel">
|
||||||
|
<div class="modal-dialog modal-xl modal-fullscreen-lg-down modal-dialog-scrollable">
|
||||||
|
<div class="modal-content h-100">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="saveAsDialogLabel">Save As ...</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mx-2 save-as-dialog__container">
|
||||||
|
<div id="saveAsDialogTypes" class="list-group save-as-dialog__selector">
|
||||||
|
<button type="button" data-type="json" class="list-group-item list-group-item-action active">
|
||||||
|
JSON
|
||||||
|
</button>
|
||||||
|
<button type="button" data-type="python" class="list-group-item list-group-item-action">
|
||||||
|
Python Example
|
||||||
|
</button>
|
||||||
|
<button type="button" data-type="js" class="list-group-item list-group-item-action">
|
||||||
|
NodeJS Example
|
||||||
|
</button>
|
||||||
|
<button type="button" data-type="static" class="list-group-item list-group-item-action">
|
||||||
|
Static Page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-1 save-as-dialog__buttons">
|
||||||
|
<button type="button" class="btn btn-success" id="saveAsDialogCopyBtn" data-bs-toggle="popover"
|
||||||
|
data-bs-placement="right" data-bs-trigger="manual" data-bs-content="Copied">
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="saveAsDialogSaveBtn">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea id="saveAsDialogCode" class="form-control save-as-dialog__code" spellcheck="false"
|
||||||
|
readonly></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
@@ -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
4
editor/web/src/assets.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*?raw' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
26
editor/web/src/codegen/index.ts
Normal file
26
editor/web/src/codegen/index.ts
Normal 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);
|
||||||
16
editor/web/src/codegen/js.ejs
Normal file
16
editor/web/src/codegen/js.ejs
Normal 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}`);
|
||||||
|
});
|
||||||
1
editor/web/src/codegen/json.ejs
Normal file
1
editor/web/src/codegen/json.ejs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<%-JSON.stringify(params, null, 4)%>
|
||||||
29
editor/web/src/codegen/python.ejs
Normal file
29
editor/web/src/codegen/python.ejs
Normal 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)
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
1
resources/styles/main.css
Normal file
1
resources/styles/main.css
Normal file
File diff suppressed because one or more lines are too long
17
scripts/hatch_build.py
Normal file
17
scripts/hatch_build.py
Normal 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)
|
||||||
@@ -4,7 +4,7 @@ from urllib.parse import quote
|
|||||||
|
|
||||||
|
|
||||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
resources_folder = os.path.join(root,'resources')
|
resources_folder = os.path.join(root, 'resources')
|
||||||
|
|
||||||
|
|
||||||
def read_file(path: str) -> str:
|
def read_file(path: str) -> str:
|
||||||
@@ -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'),
|
||||||
|
|||||||
Reference in New Issue
Block a user