mirror of
https://github.com/donlon/cloudflare-error-page.git
synced 2026-01-04 15:31:41 +00:00
Compare commits
19 Commits
react
...
python-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a5311212e | ||
|
|
9fbcba1a34 | ||
|
|
c0e576478a | ||
|
|
6233cec91f | ||
|
|
728ce52529 | ||
|
|
d0a05329d5 | ||
|
|
51b61a8a2d | ||
|
|
71f4e9507c | ||
|
|
38eb8d8c11 | ||
|
|
76a4696a40 | ||
|
|
21d14994de | ||
|
|
36a1119908 | ||
|
|
e66b5357a4 | ||
|
|
5bc662dd06 | ||
|
|
7229ad7281 | ||
|
|
a50b0289a0 | ||
|
|
a85624031c | ||
|
|
df5daebe34 | ||
|
|
eb0d5a7d55 |
17
.gitignore
vendored
17
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
@@ -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">
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>')
|
||||
|
||||
@@ -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)
|
||||
|
||||
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 |
@@ -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">>> 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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
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)
|
||||
@@ -1,23 +0,0 @@
|
||||
function AboutCaptions() {
|
||||
return (
|
||||
<>
|
||||
<div className="mt-2 text-center">
|
||||
>> 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
1
editor/web/src/react-app-env.d.ts
vendored
1
editor/web/src/react-app-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -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
|
||||
|
||||
@@ -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
8
javascript/.gitignore
vendored
8
javascript/.gitignore
vendored
@@ -1,8 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.tgz
|
||||
|
||||
*.log
|
||||
*.html
|
||||
.DS_Store
|
||||
examples/*.html
|
||||
1
javascript/src/templates/.gitignore
vendored
1
javascript/src/templates/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
main.css
|
||||
@@ -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"
|
||||
|
||||
1
resources/styles/.gitignore
vendored
1
resources/styles/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
main.css
|
||||
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__)))
|
||||
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'),
|
||||
|
||||
Reference in New Issue
Block a user