9
0
mirror of https://github.com/donlon/cloudflare-error-page.git synced 2026-01-04 15:31:41 +00:00

19 Commits

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

17
.gitignore vendored
View File

@@ -1,13 +1,14 @@
.vscode/
.DS_Store
build/
*.egg-info/
__pycache__/
dist/
node_modules/
.venv/
venv/
ttt/
node_modules/
build/
dist/
*.egg-info/
__pycache__/
instance/
instance/

View File

@@ -19,6 +19,10 @@ Here's an online editor to create customized error pages. Try it out [here](http
Install `cloudflare-error-page` with pip.
``` Bash
# Install from PyPI
pip install cloudflare-error-page
# Or, install the latest version from this repo
pip install git+https://github.com/donlon/cloudflare-error-page.git
```

View File

@@ -13,14 +13,14 @@ else:
from jinja2 import Environment, PackageLoader, Template, select_autoescape
env = Environment(
jinja_env = Environment(
loader=PackageLoader(__name__),
autoescape=select_autoescape(),
trim_blocks=True,
lstrip_blocks=True,
)
default_template: Template = env.get_template("error.html")
base_template: Template = jinja_env.get_template('template.html')
class ErrorPageParams(TypedDict):
@@ -31,7 +31,7 @@ class ErrorPageParams(TypedDict):
for_text: NotRequired[str] # renamed to avoid Python keyword conflict
class StatusItem(TypedDict):
status: NotRequired[Literal["ok", "error"]]
status: NotRequired[Literal['ok', 'error']]
location: NotRequired[str]
name: NotRequired[str]
status_text: NotRequired[str]
@@ -57,7 +57,7 @@ class ErrorPageParams(TypedDict):
cloudflare_status: NotRequired[StatusItem]
host_status: NotRequired[StatusItem]
error_source: NotRequired[Literal["browser", "cloudflare", "host"]]
error_source: NotRequired[Literal['browser', 'cloudflare', 'host']]
what_happened: NotRequired[str]
what_can_i_do: NotRequired[str]
@@ -85,7 +85,7 @@ def render(params: ErrorPageParams,
:return: The rendered error page as a string.
'''
if not template:
template = default_template
template = base_template
params = {**params}
@@ -97,7 +97,7 @@ def render(params: ErrorPageParams,
if not params.get('time'):
utc_now = datetime.now(timezone.utc)
params['time'] = utc_now.strftime("%Y-%m-%d %H:%M:%S UTC")
params['time'] = utc_now.strftime('%Y-%m-%d %H:%M:%S UTC')
if not params.get('ray_id'):
params['ray_id'] = secrets.token_hex(8)
if not allow_html:
@@ -106,5 +106,5 @@ def render(params: ErrorPageParams,
return template.render(params=params, *args, **kwargs)
__version__ = "0.1.0"
__all__ = ['default_template', 'render']
__version__ = '0.2.0'
__all__ = ['jinja_env', 'base_template', 'render']

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -12,8 +12,9 @@ from flask import (
)
from cloudflare_error_page import ErrorPageParams
from cloudflare_error_page import render as render_cf_error_page
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__)), '../../')
examples_dir = os.path.join(root_dir, 'examples')
@@ -50,7 +51,5 @@ def index(name: str):
if params is None:
abort(404)
fill_cf_template_params(params)
# Render the error page
return render_cf_error_page(params)
return render_extended_template(params=params)

View File

@@ -1,9 +1,11 @@
# SPDX-License-Identifier: MIT
import html
import random
import string
from typing import cast
from cloudflare_error_page import ErrorPageParams
from flask import (
Blueprint,
current_app,
@@ -13,49 +15,20 @@ from flask import (
redirect,
url_for,
)
from jinja2 import Environment, select_autoescape
from cloudflare_error_page import (
default_template as cf_template,
render as render_cf_error_page,
)
from . import (
db,
limiter,
models
models,
)
from .utils import fill_cf_template_params, sanitize_page_param_links
# root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../')
# examples_dir = os.path.join(root_dir, 'examples')
env = Environment(
autoescape=select_autoescape(),
trim_blocks=True,
lstrip_blocks=True,
from .utils import (
render_extended_template,
sanitize_page_param_links,
)
template = env.from_string('''
{% extends base %}
{% block header %}
<meta property="og:type" content="website" />
<meta property="og:site_name" content="moe::virt" />
<meta property="og:title" content="{{ html_title }}" />
<meta property="og:url" content="{{ url }}" />
<meta property="og:description" content="{{ description }}" />
<meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="moe::virt" />
<meta property="twitter:title" content="{{ html_title }}" />
<meta property="twitter:description" content="{{ description }}" />
{% endblock %}
''')
bp = Blueprint('share', __name__, url_prefix='/')
bp_short = Blueprint('share_short', __name__, url_prefix='/')
rand_charset = string.ascii_lowercase + string.digits
def get_rand_name(digits=8):
@@ -115,7 +88,7 @@ def get(name: str):
})
else:
return abort(404)
params: dict = item.params
params = cast(ErrorPageParams, item.params)
params.pop('time', None)
params.pop('ray_id', None)
params.pop('client_ip', None)
@@ -131,15 +104,9 @@ def get(name: str):
'text': 'CF Error Page Editor',
'link': request.host_url[:-1] + url_for('editor.index') + f'#from={name}',
}
fill_cf_template_params(params)
sanitize_page_param_links(params)
return render_cf_error_page(params=params,
allow_html=False,
template=template,
base=cf_template,
url=request.url,
description='Cloudflare error page')
return render_extended_template(params=params,
allow_html=False)
@bp.get('/<name>')

View File

@@ -1,11 +1,53 @@
import json
import os
import re
from typing import Any
from cloudflare_error_page import ErrorPageParams
from flask import request
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 . 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
@@ -74,3 +116,31 @@ def sanitize_page_param_links(param: ErrorPageParams):
link = perf_sec_by.get('link')
if link:
perf_sec_by['link'] = sanitize_user_link(link)
def render_extended_template(params: ErrorPageParams,
*args: Any,
**kwargs: Any) -> str:
fill_cf_template_params(params)
description = params.get('what_happened') or 'There is an internal server error on Cloudflare\'s network.'
description = re.sub(r'<\/?.*?>', '', description).strip()
status = 'ok'
cf_status_obj = params.get('cloudflare_status')
if cf_status_obj:
cf_status = cf_status_obj.get('status')
if cf_status == 'error':
status = 'error'
page_icon_url = current_app.config.get('PAGE_ICON_URL', '').replace('{status}', status)
page_icon_type = current_app.config.get('PAGE_ICON_TYPE')
page_image_url = current_app.config.get('PAGE_IMAGE_URL', '').replace('{status}', status)
return render_cf_error_page(params=params,
template=template,
base=base_template,
page_icon_url=page_icon_url,
page_icon_type=page_icon_type,
page_url=request.url,
page_description=description,
page_image_url=page_image_url,
*args,
**kwargs)

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,6 +1,12 @@
<!-- SPDX-License-Identifier: MIT -->
<!--
!!!
!!! Note: This file is vibely generated, and could be very hard to maintain.
!!!
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Cloudflare Error Page Editor</title>
@@ -8,24 +14,545 @@
<meta name="description" content="Online editor to create customized Cloudflare-style error pages.">
<meta name="keywords" content="cloudflare,error,page,editor">
<link rel="canonical" href="https://virt.moe/cferr/editor/" />
<link rel="icon" type="image/png" href="assets/icon-ok-32x32.png">
<meta property="og:type" content="website" />
<meta property="og:site_name" content="moe::virt" />
<meta property="og:title" content="Cloudflare error page editor" />
<meta property="og:url" content="https://virt.moe/cferr/editor/" />
<meta property="og:description" content="Online editor to create customized Cloudflare-style error pages" />
<meta property="og:image" content="https://virt.moe/cferr/editor/assets/icon-ok-large.png" />
<meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="moe::virt" />
<meta property="twitter:title" content="Cloudflare error page editor" />
<meta property="twitter:description" content="Online editor to create customized Cloudflare-style error pages" />
<meta property="twitter:image" content="https://virt.moe/cferr/editor/assets/icon-ok-large.png" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<script type="module" src="src/index.tsx"></script>
<script type="module" src="src/index.js"></script>
<style>
/* Layout: editor + preview */
.app {
display: flex;
flex-direction: column;
gap: 1rem;
height: 100vh;
/* padding: 0.8rem; */
}
/* On md and up, arrange horizontally: editor left, preview right */
@media (min-width: 768px) {
.app {
flex-direction: row;
align-items: stretch;
}
.editor {
flex: 0 0;
min-width: 380px;
overflow-y: auto;
}
.preview {
flex: 1 1 48%;
/* max-width: 48%; */
display: flex;
flex-direction: column;
}
iframe.preview-frame {
height: 100% !important;
}
}
/* On small screens use stacked layout: editor then iframe */
.editor {
background: #fff;
border: 1px solid #e3e6ea;
border-radius: .5rem;
padding: 0.8rem;
}
.preview {
background: #fff;
border: 1px solid #e3e6ea;
border-radius: .5rem;
padding: 0.8rem 0 0 0;
}
/* Compact form: label and control same row */
.form-row {
display: flex;
align-items: center;
margin-bottom: .6rem;
}
.form-row>label {
min-width: 6rem;
max-width: 12rem;
margin-bottom: 0;
font-weight: 600;
}
.form-row>.control {
flex: 1;
}
/* Status block styling */
.status-block {
border: 1px solid #cfeadd;
border-radius: .375rem;
padding: .75rem;
margin-bottom: .5rem;
}
.status-ok {
background: #e9f7ef;
border: 1px solid #cfeadd;
}
.status-error {
background: #fff5f5;
border: 1px solid #f3c2c2;
}
/* Iframe styling */
iframe.preview-frame {
--expanded-height: 100%;
width: 100%;
height: var(--expanded-height);
border: 1px solid #ddd;
flex: 1 1 auto;
border-radius: .375rem;
box-shadow: 0 1px 4px rgba(0, 0, 0, .06);
}
/* Controls toolbar */
.toolbar {
display: flex;
gap: .5rem;
align-items: center;
justify-content: space-between;
margin-bottom: .75rem;
}
/* Compact textarea resizing */
textarea.compact {
min-height: 80px;
resize: vertical;
}
.popover {
--bs-popover-body-padding-x: 0.7rem !important;
--bs-popover-body-padding-y: 0.5rem !important;
}
.save-as-dialog__container {
display: grid;
grid-template-areas:
"selector"
"code"
"buttons";
height: fit-content;
grid-template-columns: auto;
gap: 1em;
}
.save-as-dialog__selector {
grid-area: selector;
min-width: 10em;
}
.save-as-dialog__code {
grid-area: code;
width: 100%;
height: 100%;
min-height: 700px !important;
font-family: monospace;
font-size: 0.8em !important;
}
.save-as-dialog__buttons {
grid-area: buttons;
flex-direction: row;
}
.save-as-dialog__buttons>* {
flex: 1 1 auto;
}
@media (min-width: 576px) {
.save-as-dialog__container {
grid-template-areas:
"selector code"
"buttons code";
grid-template-columns: auto 2fr;
height: 100%;
}
.save-as-dialog__selector {
text-align: right;
}
.save-as-dialog__code {
min-height: 0 !important;
}
.save-as-dialog__buttons {
grid-area: buttons;
flex-direction: column;
justify-content: flex-end !important;
}
.save-as-dialog__buttons>* {
flex: none;
}
}
@media (min-width: 992px) {
.save-as-dialog__selector {
grid-area: selector;
min-width: 13em;
}
}
</style>
</head>
<body class="bg-light">
<div id="editor"></div>
<div class="container-fluid h-100">
<div class="app">
<!-- Editor column -->
<div class="editor">
<h5 class="form-row">Cloudflare Error Page Editor</h5>
<hr>
<div class="form-row mb-3">
<label for="presetSelect">Preset</label>
<select id="presetSelect" class="form-select form-select-sm">
<option value="default">Internal server error (Default)</option>
<option value="empty">Empty</option>
<option value="catastrophic">Catastrophic failure</option>
<option value="working">Server working</option>
<option value="consensual">Myth of consensual</option>
</select>
</div>
<hr>
<form id="editorForm" class="needs-validation" novalidate>
<!-- Basic properties -->
<div class="mb-3">
<!-- <h6 class="mb-2">Page</h6> -->
<div class="form-row">
<label for="title">Title</label>
<div class="control">
<input id="title" class="form-control form-control-sm" placeholder="Internal server error" />
</div>
</div>
<div class="form-row">
<label for="error_code">Error Code</label>
<div class="control">
<input id="error_code" class="form-control form-control-sm" placeholder="500" />
</div>
</div>
</div>
<hr>
<!-- Status blocks -->
<div class="mb-3">
<h6 class="mb-2">Status</h6>
<!-- Browser -->
<div id="block_browser" class="status-block status-ok">
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Browser</strong>
<div>
<label for="err_browser" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" />
Error here
</label>
</div>
</div>
<div class="form-row">
<label for="browser_status">Status</label>
<div class="control">
<select id="browser_status" class="form-select form-select-sm">
<option value="ok">Ok</option>
<option value="error">Error</option>
</select>
</div>
</div>
<div class="form-row">
<label for="browser_location">Location</label>
<div class="control">
<input id="browser_location" class="form-control form-control-sm" placeholder="You" />
</div>
</div>
<div class="form-row">
<label for="browser_name">Name</label>
<div class="control">
<input id="browser_name" class="form-control form-control-sm" placeholder="Browser" />
</div>
</div>
<div class="form-row">
<label for="browser_status_text">Status Text</label>
<div class="control">
<input id="browser_status_text" class="form-control form-control-sm" />
</div>
</div>
</div>
<!-- Cloudflare -->
<div id="block_cloudflare" class="status-block status-error">
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Cloudflare</strong>
<div>
<label for="err_cloudflare" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_cloudflare"
value="cloudflare" />
Error here
</label>
</div>
</div>
<div class="form-row">
<label for="cloudflare_status">Status</label>
<div class="control">
<select id="cloudflare_status" class="form-select form-select-sm">
<option value="ok">Ok</option>
<option value="error">Error</option>
</select>
</div>
</div>
<div class="form-row">
<label for="cloudflare_location">Location</label>
<div class="control">
<input id="cloudflare_location" class="form-control form-control-sm" placeholder="San Francisco" />
</div>
</div>
<div class="form-row">
<label for="cloudflare_name">Name</label>
<div class="control">
<input id="cloudflare_name" class="form-control form-control-sm" placeholder="Cloudflare" />
</div>
</div>
<div class="form-row">
<label for="cloudflare_status_text">Status Text</label>
<div class="control">
<input id="cloudflare_status_text" class="form-control form-control-sm" />
</div>
</div>
</div>
<!-- Host -->
<div id="block_host" class="status-block status-ok">
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Host</strong>
<div>
<label for="err_host" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_host" value="host" />
Error here
</label>
</div>
</div>
<div class="form-row">
<label for="host_status">Status</label>
<div class="control">
<select id="host_status" class="form-select form-select-sm">
<option value="ok">Ok</option>
<option value="error">Error</option>
</select>
</div>
</div>
<div class="form-row">
<label for="host_location">Location</label>
<div class="control">
<input id="host_location" class="form-control form-control-sm" placeholder="Website" />
</div>
</div>
<div class="form-row">
<label for="host_name">Name</label>
<div class="control">
<input id="host_name" class="form-control form-control-sm" placeholder="Host" />
</div>
</div>
<div class="form-row">
<label for="host_status_text">Status Text</label>
<div class="control">
<input id="host_status_text" class="form-control form-control-sm" />
</div>
</div>
</div>
</div>
<div class="status-block mt-3 mb-3">
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>Visit ...</strong>
<div>
<label for="more_hidden" class="ms-1 small">
<input id="more_hidden" class="form-check-input" type="checkbox" />
Hidden
</label>
</div>
</div>
<div class="form-row">
<label for="more_text">Text</label>
<div class="control">
<input id="more_text" class="form-control form-control-sm" placeholder="cloudflare.com" />
</div>
</div>
<div class="form-row">
<label for="more_link">Link</label>
<div class="control">
<input id="more_link" class="form-control form-control-sm" placeholder="https://www.cloudflare.com/" />
</div>
</div>
<div class="form-row">
<label for="more_for">For</label>
<div class="control">
<input id="more_for" class="form-control form-control-sm" placeholder="more information" />
</div>
</div>
</div>
<label for="what_happened" class="fw-semibold">What happened?</label>
<div class="control">
<textarea id="what_happened" class="form-control compact"
placeholder="There is an internal server error on Cloudflare's network."></textarea>
</div>
<!-- </div> -->
<!-- <div class=""> -->
<label for="what_can_i_do" class="fw-semibold mt-2">What can I do?</label>
<div class="control">
<textarea id="what_can_i_do" class="form-control compact"
placeholder="Please try again in a few minutes."></textarea>
</div>
<hr>
<h6 class="form-row">Performance & security by ...</h6>
<div class="form-row">
<label for="perf_text">Text</label>
<div class="control">
<input id="perf_text" class="form-control form-control-sm" placeholder="Cloudflare" />
</div>
</div>
<div class="form-row">
<label for="perf_link">Link</label>
<div class="control">
<input id="perf_link" class="form-control form-control-sm" placeholder="https://www.cloudflare.com/" />
</div>
</div>
<!-- </div> -->
<div class="d-flex gap-2 mt-2 mb-2">
<!-- <button id="btnRender" class="btn btn-sm btn-primary">Render</button> -->
<button type="button" id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button>
<!-- Button trigger modal -->
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#saveAsDialog">
Save as...
</button>
</div>
<button type="button" id="btnShare" class="btn btn-sm btn-secondary">Create shareable link</button>
<div class="mt-2">
<div class="input-group input-group-sm">
<input id="shareLink" class="form-control" readonly />
<button 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 class="mt-2 text-center">&gt;&gt; Star this project on
<a href="https://github.com/donlon/cloudflare-error-page" target="_blank">GitHub</a>
</div>
<div class="mt-2" style="font-size: 0.9em;">You can also embed this error page into your own website. See
<a href="https://github.com/donlon/cloudflare-error-page#quickstart-for-programmers"
target="_blank">Quickstart</a> in the
homepage for steps.
</div>
</form>
</div>
<!-- Preview column -->
<div class="preview">
<div class="d-flex justify-content-between align-items-center mb-1" style="padding: 0 0.8em;">
<h6><strong>Preview</strong></h6>
</div>
<!-- TODO: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. -->
<iframe id="previewFrame" class="preview-frame" sandbox="allow-scripts allow-same-origin"></iframe>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="saveAsDialog" tabindex="-1" aria-labelledby="saveAsDialogLabel">
<div class="modal-dialog modal-xl modal-fullscreen-lg-down modal-dialog-scrollable">
<div class="modal-content h-100">
<div class="modal-header">
<h5 class="modal-title" id="saveAsDialogLabel">Save As ...</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mx-2 save-as-dialog__container">
<div id="saveAsDialogTypes" class="list-group save-as-dialog__selector">
<button type="button" data-type="json" class="list-group-item list-group-item-action active">
JSON
</button>
<button type="button" data-type="python" class="list-group-item list-group-item-action">
Python Example
</button>
<button type="button" data-type="js" class="list-group-item list-group-item-action">
NodeJS Example
</button>
<button type="button" data-type="static" class="list-group-item list-group-item-action">
Static Page
</button>
</div>
<div class="d-flex gap-1 save-as-dialog__buttons">
<button type="button" class="btn btn-success" id="saveAsDialogCopyBtn" data-bs-toggle="popover"
data-bs-placement="right" data-bs-trigger="manual" data-bs-content="Copied">
Copy
</button>
<button type="button" class="btn btn-primary" id="saveAsDialogSaveBtn">
Save
</button>
</div>
<textarea id="saveAsDialogCode" class="form-control save-as-dialog__code" spellcheck="false"
readonly></textarea>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
</html>

View File

@@ -1,7 +1,6 @@
{
"name": "cloudflare-error-page-editor",
"version": "0.0.1",
"private": true,
"license": "MIT",
"scripts": {
"dev": "vite",
@@ -11,20 +10,18 @@
"preview": "npm run build && vite preview"
},
"devDependencies": {
"@types/bootstrap": "^5.2.10",
"@types/ejs": "^3.1.5",
"@types/html-minifier-terser": "^7.0.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"@types/node": "^24.10.2",
"html-minifier-terser": "^7.2.0",
"prettier": "3.7.4",
"typescript": "^5.9.3",
"vite": "^7.3.0"
"vite": "^7.2.6",
"vite-plugin-static-copy": "^3.1.4"
},
"dependencies": {
"bootstrap": "^5.3.8",
"ejs": "^3.1.10",
"react": "^19.2.3",
"react-dom": "^19.2.3"
"ejs": "^3.1.10"
}
}

View File

View File

@@ -1,16 +0,0 @@
import EditorSideBar from './components/EditorSideBar';
import PreviewFrame from './components/PreviewFrame';
import './App.css';
function App() {
return (
<div className="container-fluid h-100">
<div className="app">
<EditorSideBar />
<PreviewFrame content='' />
</div>
</div>
);
}
export default App;

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

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

View File

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

View File

@@ -0,0 +1,16 @@
import express from 'express';
import { render as render_cf_error_page } from 'cloudflare-error-page';
const app = express();
const port = 3000;
// Define a route for GET requests to the root URL
<%# TODO: format to JS-style object (key w/o parens) _%>
app.get('/', (req, res) => {
res.status(500).send(render_cf_error_page(<%-JSON.stringify(params, null, 2).replaceAll('\n', '\n ')%>));
});
// Start the server and listen on the specified port
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

View File

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

View File

@@ -0,0 +1,29 @@
<%
// Covert the parameters to Python format object
const randomKey = Math.random() + ''
const paramsArg = JSON.stringify(params, (key, value) => {
if (typeof value === 'boolean') {
return randomKey + value.toString()
} else if (value === null) {
return randomKey + 'null'
} else {
return value
}
}, 4)
.replace(`"${randomKey}true"`, 'True')
.replace(`"${randomKey}false"`, 'False')
.replace(`"${randomKey}null"`, 'None')
_%>
from flask import Flask
from cloudflare_error_page import render as render_cf_error_page
app = Flask(__name__)
# Define a route for GET requests to the root URL
@app.route('/')
def index():
# Render the error page
return render_cf_error_page(<%- paramsArg.replaceAll('\n', '\n ') %>), 500
if __name__ == '__main__':
app.run(debug=True, port=5000)

View File

@@ -1,23 +0,0 @@
function AboutCaptions() {
return (
<>
<div className="mt-2 text-center">
&gt;&gt; Star this project on{' '}
<a href="https://github.com/donlon/cloudflare-error-page" target="_blank">
GitHub
</a>{' '}
</div>
<div className="mt-2" style={{ fontSize: '0.9em' }}>
You can also embed this error page into your own website. See{' '}
<a href="https://github.com/donlon/cloudflare-error-page#quickstart-for-programmers" target="_blank">
Quickstart
</a>{' '}
in the homepage for steps.
</div>
</>
);
}
export default AboutCaptions;

View File

@@ -1,19 +0,0 @@
function EditorActions({ handleOpen, handleExport, handleShare }: { [k: string]: () => void }) {
return (
<>
<div className="d-flex gap-2 mt-2 mb-2">
<button className="btn btn-sm btn-primary" onClick={handleOpen}>
Preview in new tab
</button>
<button className="btn btn-sm btn-primary" onClick={handleExport}>
Export JSON
</button>
</div>
<button className="btn btn-sm btn-primary" onClick={handleShare}>
Create shareable link
</button>
</>
);
}
export default EditorActions;

View File

@@ -1,244 +0,0 @@
function StatusBlockEditor() {}
function EditorForm() {
return (
<form id="editorForm" className="needs-validation" noValidate>
{/* Basic properties */}
<div className="mb-3">
<div className="form-row">
<label htmlFor="title">Title</label>
<div className="control">
<input id="title" className="form-control form-control-sm" placeholder="Internal server error" />
</div>
</div>
<div className="form-row">
<label htmlFor="error_code">Error Code</label>
<div className="control">
<input id="error_code" className="form-control form-control-sm" placeholder="500" />
</div>
</div>
</div>
<hr />
{/* Status blocks */}
<div className="mb-3">
<h6 className="mb-2">Status</h6>
{/* Browser */}
<div id="block_browser" className="status-block status-ok">
<div className="d-flex justify-content-between align-items-start mb-2">
<strong>Browser</strong>
<div>
<input className="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" />
<label htmlFor="err_browser" className="ms-1 small">
Error here
</label>
</div>
</div>
<div className="form-row">
<label htmlFor="browser_status">Status</label>
<div className="control">
<select id="browser_status" className="form-select form-select-sm">
<option value="ok">Ok</option>
<option value="error">Error</option>
</select>
</div>
</div>
<div className="form-row">
<label htmlFor="browser_location">Location</label>
<div className="control">
<input id="browser_location" className="form-control form-control-sm" placeholder="You" />
</div>
</div>
<div className="form-row">
<label htmlFor="browser_name">Name</label>
<div className="control">
<input id="browser_name" className="form-control form-control-sm" placeholder="Browser" />
</div>
</div>
<div className="form-row">
<label htmlFor="browser_status_text">Status Text</label>
<div className="control">
<input id="browser_status_text" className="form-control form-control-sm" />
</div>
</div>
</div>
{/* Cloudflare */}
<div id="block_cloudflare" className="status-block status-error">
<div className="d-flex justify-content-between align-items-start mb-2">
<strong>Cloudflare</strong>
<div>
<input
className="form-check-input"
type="radio"
name="error_source"
id="err_cloudflare"
value="cloudflare"
/>
<label htmlFor="err_cloudflare" className="ms-1 small">
Error here
</label>
</div>
</div>
<div className="form-row">
<label htmlFor="cloudflare_status">Status</label>
<div className="control">
<select id="cloudflare_status" className="form-select form-select-sm">
<option value="ok">Ok</option>
<option value="error">Error</option>
</select>
</div>
</div>
<div className="form-row">
<label htmlFor="cloudflare_location">Location</label>
<div className="control">
<input id="cloudflare_location" className="form-control form-control-sm" placeholder="San Francisco" />
</div>
</div>
<div className="form-row">
<label htmlFor="cloudflare_name">Name</label>
<div className="control">
<input id="cloudflare_name" className="form-control form-control-sm" placeholder="Cloudflare" />
</div>
</div>
<div className="form-row">
<label htmlFor="cloudflare_status_text">Status Text</label>
<div className="control">
<input id="cloudflare_status_text" className="form-control form-control-sm" />
</div>
</div>
</div>
{/* Host */}
<div id="block_host" className="status-block status-ok">
<div className="d-flex justify-content-between align-items-start mb-2">
<strong>Host</strong>
<div>
<input className="form-check-input" type="radio" name="error_source" id="err_host" value="host" />
<label htmlFor="err_host" className="ms-1 small">
Error here
</label>
</div>
</div>
<div className="form-row">
<label htmlFor="host_status">Status</label>
<div className="control">
<select id="host_status" className="form-select form-select-sm">
<option value="ok">Ok</option>
<option value="error">Error</option>
</select>
</div>
</div>
<div className="form-row">
<label htmlFor="host_location">Location</label>
<div className="control">
<input id="host_location" className="form-control form-control-sm" placeholder="Website" />
</div>
</div>
<div className="form-row">
<label htmlFor="host_name">Name</label>
<div className="control">
<input id="host_name" className="form-control form-control-sm" placeholder="Host" />
</div>
</div>
<div className="form-row">
<label htmlFor="host_status_text">Status Text</label>
<div className="control">
<input id="host_status_text" className="form-control form-control-sm" />
</div>
</div>
</div>
</div>
<div className="status-block mt-3 mb-3">
<div className="d-flex justify-content-between align-items-start mb-2">
<strong>Visit ...</strong>
<div>
<input id="more_hidden" className="form-check-input" type="checkbox" />
<label htmlFor="more_hidden" className="ms-1 small">
Hidden
</label>
</div>
</div>
<div className="form-row">
<label htmlFor="more_text">Text</label>
<div className="control">
<input id="more_text" className="form-control form-control-sm" placeholder="cloudflare.com" />
</div>
</div>
<div className="form-row">
<label htmlFor="more_link">Link</label>
<div className="control">
<input id="more_link" className="form-control form-control-sm" placeholder="https://www.cloudflare.com/" />
</div>
</div>
<div className="form-row">
<label htmlFor="more_for">For</label>
<div className="control">
<input id="more_for" className="form-control form-control-sm" placeholder="more information" />
</div>
</div>
</div>
<label htmlFor="what_happened" className="fw-semibold">
What happened?
</label>
<div className="control">
<textarea
id="what_happened"
className="form-control compact"
placeholder="There is an internal server error on Cloudflare's network."
></textarea>
</div>
<label htmlFor="what_can_i_do" className="fw-semibold mt-2">
What can I do?
</label>
<div className="control">
<textarea
id="what_can_i_do"
className="form-control compact"
placeholder="Please try again in a few minutes."
></textarea>
</div>
<hr />
<h6 className="form-row">Performance & security by ...</h6>
<div className="form-row">
<label htmlFor="perf_text">Text</label>
<div className="control">
<input id="perf_text" className="form-control form-control-sm" placeholder="Cloudflare" />
</div>
</div>
<div className="form-row">
<label htmlFor="perf_link">Link</label>
<div className="control">
<input id="perf_link" className="form-control form-control-sm" placeholder="https://www.cloudflare.com/" />
</div>
</div>
</form>
);
}
export default EditorForm;

View File

@@ -1,30 +0,0 @@
import { useId, ChangeEvent } from 'react';
function EditorPresetSelector({
presetList,
handleSelect,
}: {
presetList: [string, string][];
handleSelect: (selected: string) => void;
}) {
const selectId = useId();
function handleChange(e: ChangeEvent) {
handleSelect((e.target as HTMLSelectElement).value);
}
return (
<div className="form-row mb-3">
<label htmlFor={selectId}>Preset</label>
<select id={selectId} className="form-select form-select-sm" onChange={handleChange}>
{presetList.map(([key, name]) => {
return (
<option value="key" key="key">
{name}
</option>
);
})}
</select>
</div>
);
}
export default EditorPresetSelector;

View File

@@ -1,29 +0,0 @@
import AboutCaptions from './AboutCaptions';
import EditorActions from './EditorActions';
import EditorForm from './EditorForm';
import EditorPresetSelector from './EditorPresetSelector';
import SharedLinkBox from './SharedLinkBox';
function EditorSideBar() {
// <option value="default">Internal server error (Default)</option>
// <option value="empty">Empty</option>
// <option value="catastrophic">Catastrophic failure</option>
// <option value="working">Server working</option>
// <option value="consensual">Myth of consensual</option>
return (
<>
<div className="editor">
<h5 className="form-row">Cloudflare Error Page Editor</h5>
<hr />
<EditorPresetSelector presetList={[]} handleSelect={() =>{}}/>
<hr />
<EditorForm />
<EditorActions />
<SharedLinkBox link=''/>
<AboutCaptions />
</div>
</>
);
}
export default EditorSideBar;

View File

@@ -1,41 +0,0 @@
import { useEffect, useRef } from 'react';
function PreviewFrame({ content }: { content: string }) {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
// Automatically update frame height
const iframe = iframeRef.current!;
function resizePreviewFrame() {
const height = iframe.contentWindow!.document.body.scrollHeight + 2;
iframe.style.setProperty('--expanded-height', height + 'px');
}
const observer = new ResizeObserver(() => resizePreviewFrame());
observer.observe(iframe.contentWindow!.document.body);
setInterval(resizePreviewFrame, 1000); // TODO...
return () => {
observer.disconnect();
};
}, []);
useEffect(() => {
const iframe = iframeRef.current!;
const doc = iframe.contentDocument!;
doc.open();
doc.write(content); // TODO: deprecated method
doc.close();
}, [content]);
return (
<div className="preview">
<div className="d-flex justify-content-between align-items-center mb-1" style={{ padding: '0 0.8em' }}>
<h6>
<strong>Preview</strong>
</h6>
</div>
{/* TODO: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.*/}
<iframe ref={iframeRef} className="preview-frame" sandbox="allow-scripts allow-same-origin"></iframe>
</div>
);
}
export default PreviewFrame;

View File

@@ -1,26 +0,0 @@
import { useRef } from 'react';
function SharedLinkBox({ link }: { link: string }) {
const linkRef = useRef<HTMLInputElement>(null);
function handleCopy() {
const field = linkRef.current!;
field.select();
field.setSelectionRange(0, field.value.length);
navigator.clipboard.writeText(field.value).then(() => {
// No notification required unless you want one
});
}
return (
<div className="mt-2">
<div className="input-group input-group-sm">
<input className="form-control" readOnly ref={linkRef} value={link} />
<button className="btn btn-outline-secondary" type="button" onClick={handleCopy}>
Copy
</button>
</div>
</div>
);
}
export default SharedLinkBox;

View File

@@ -1,111 +0,0 @@
/* Layout: editor + preview */
.app {
display: flex;
flex-direction: column;
gap: 1rem;
height: 100vh;
/* padding: 0.8rem; */
}
/* On md and up, arrange horizontally: editor left, preview right */
@media (min-width: 768px) {
.app {
flex-direction: row;
align-items: stretch;
}
.editor {
flex: 0 0;
min-width: 380px;
overflow-y: auto;
}
.preview {
flex: 1 1 48%;
/* max-width: 48%; */
display: flex;
flex-direction: column;
}
iframe.preview-frame {
height: 100% !important;
}
}
/* On small screens use stacked layout: editor then iframe */
.editor {
background: #fff;
border: 1px solid #e3e6ea;
border-radius: 0.5rem;
padding: 0.8rem;
}
.preview {
background: #fff;
border: 1px solid #e3e6ea;
border-radius: 0.5rem;
padding: 0.8rem 0 0 0;
}
/* Compact form: label and control same row */
.form-row {
display: flex;
gap: 0.3rem;
align-items: center;
margin-bottom: 0.6rem;
}
.form-row > label {
min-width: 6rem;
max-width: 12rem;
margin-bottom: 0;
font-weight: 600;
}
.form-row > .control {
flex: 1;
}
/* Status block styling */
.status-block {
border: 1px solid #cfeadd;
border-radius: 0.375rem;
padding: 0.75rem;
margin-bottom: 0.5rem;
}
.status-ok {
background: #e9f7ef;
border: 1px solid #cfeadd;
}
.status-error {
background: #fff5f5;
border: 1px solid #f3c2c2;
}
/* Iframe styling */
iframe.preview-frame {
--expanded-height: 100%;
width: 100%;
height: var(--expanded-height);
border: 1px solid #ddd;
flex: 1 1 auto;
min-height: 360px;
border-radius: 0.375rem;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
/* Controls toolbar */
.toolbar {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
}
/* Compact textarea resizing */
textarea.compact {
min-height: 80px;
resize: vertical;
}

View File

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

View File

@@ -1,15 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
const root = ReactDOM.createRoot(
document.getElementById('editor') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@@ -1,6 +1,6 @@
{
// Visit https://aka.ms/tsconfig to read more about this file
"include": ["src"],
"include": ["src/**/*.ts"],
"compilerOptions": {
// File Layout
// "rootDir": "./src",
@@ -37,7 +37,7 @@
"jsx": "react-jsx",
// "verbatimModuleSyntax": true,
"isolatedModules": true,
// "noUncheckedSideEffectImports": true,
"noUncheckedSideEffectImports": true,
"moduleResolution": "bundler",
"moduleDetection": "force",
"skipLibCheck": true

View File

@@ -1,27 +1,12 @@
/// <reference types="vite/types/importMeta.d.ts" />
import { defineConfig } from 'vite';
import { minify as htmlMinify } from 'html-minifier-terser';
import react from '@vitejs/plugin-react';
import { viteStaticCopy } from 'vite-plugin-static-copy';
export default defineConfig(({ mode }) => {
const baseUrl = mode === 'production' ? '' : '/editor/';
return {
plugins: [
react(),
{
name: 'html-minifier',
transformIndexHtml: {
order: 'post',
handler(html) {
return htmlMinify(html, {
collapseWhitespace: true,
removeComments: true,
minifyCSS: true,
minifyJS: true,
});
},
},
},
],
appType: 'mpa',
base: baseUrl,
build: {
@@ -36,5 +21,30 @@ export default defineConfig(({ mode }) => {
},
},
},
plugins: [
{
name: 'html-minifier',
transformIndexHtml: {
order: 'post',
handler(html) {
return htmlMinify(html, {
collapseWhitespace: true,
removeComments: true,
minifyCSS: true,
minifyJS: true,
});
},
},
},
viteStaticCopy({
targets: [
{
src: 'assets/',
dest: '',
},
],
}),
],
};
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
node_modules/
dist/
*.tgz
*.log
*.html
.DS_Store
examples/*.html

View File

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

View File

@@ -22,3 +22,9 @@ include = [
"cloudflare_error_page/*.py",
"cloudflare_error_page/templates/*",
]
[tool.hatch.build.targets.wheel.hooks.custom]
path = "scripts/hatch_build.py"
[tool.hatch.version]
path = "cloudflare_error_page/__init__.py"

View File

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

File diff suppressed because one or more lines are too long

17
scripts/hatch_build.py Normal file
View File

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

View File

@@ -4,7 +4,7 @@ from urllib.parse import quote
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:
@@ -49,7 +49,7 @@ def inline_css_resource(original_file: str, css_file: str, output_file: str):
write_file(output_file, original_data)
if __name__ == '__main__':
def generate_inlined_css():
inline_svg_resources(
os.path.join(resources_folder, 'styles/main-original.css'),
[
@@ -61,11 +61,10 @@ if __name__ == '__main__':
],
os.path.join(resources_folder, 'styles/main.css'),
)
inline_css_resource(
os.path.join(resources_folder, 'templates/error.html'),
os.path.join(resources_folder, 'styles/main.css'),
os.path.join(root, 'cloudflare_error_page/templates/error.html'),
)
if __name__ == '__main__':
generate_inlined_css()
inline_css_resource(
os.path.join(resources_folder, 'templates/error.ejs'),
os.path.join(resources_folder, 'styles/main.css'),