9
0
mirror of https://github.com/donlon/cloudflare-error-page.git synced 2025-12-19 14:59:28 +00:00
Files
cloudflare-error-page/editor/resources/index.html
2025-11-21 09:21:06 +08:00

815 lines
27 KiB
HTML

<!-- 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" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Cloudflare Error Page Editor</title>
<meta name="description" content="Online editor to create customized Cloudflare-styled error pages.">
<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/cloudflare-error-page/editor/" />
<meta property="og:description" content="Online editor to create customized Cloudflare-styled error pages" />
<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-styled error pages" />
<script src="https://cdn.jsdelivr.net/npm/ejs@3.1.10/ejs.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<script>
(async function () {
/*
Editor logic
- initialConfig: provided default config (from your JSON)
- render(): placeholder that generates HTML and writes to iframe.srcdoc
- inputs call render() on change
- "Open in new tab" opens the rendered HTML in a new window using a blob URL
*/
let template;
let postponeRendering = false;
fetch('template.ejs')
.then(response => {
if (!response.ok) {
alert('failed to fetch template');
}
return response.text(); // Returns a Promise
})
.then(templateContent => {
template = ejs.compile(templateContent)
if (postponeRendering) {
render()
}
})
// can be changed if specified by '?from=<name>'
let initialConfig = {
"title": "Internal server error",
"error_code": 500,
"more_information": {
"hidden": false,
"text": "cloudflare.com",
},
"browser_status": {
"status": "ok",
"location": "You",
"name": "Browser",
"status_text": "Working"
},
"cloudflare_status": {
"status": "error",
"location": "San Francisco",
"name": "Cloudflare",
"status_text": "Error"
},
"host_status": {
"status": "ok",
"location": "Website",
"name": "Host",
"status_text": "Working"
},
"error_source": "cloudflare",
"what_happened": "There is an internal server error on Cloudflare's network.",
"what_can_i_do": "Please try again in a few minutes.",
};
// Demo presets (content brief — replace or expand as needed)
const PRESETS = {
"default": structuredClone(initialConfig),
"empty": {
"error_code": 500,
},
"catastrophic": {
"title": "Catastrophic infrastructure failure",
"error_code": 500,
"browser_status": {
"status": "error",
"status_text": "Out of Memory"
},
"cloudflare_status": {
"status": "error",
"location": "Everywhere",
"status_text": "Error"
},
"host_status": {
"status": "error",
"location": "example.com",
"status_text": "On Fire"
},
"error_source": "cloudflare",
"what_happened": "There is a catastrophic failure.",
"what_can_i_do": "Please try again in a few years.",
},
"working": {
"title": "Web server is working",
"error_code": 200,
"more_information": {
"hidden": true
},
"browser_status": {
"status": "ok",
"status_text": "Seems Working"
},
"cloudflare_status": {
"status": "ok",
"status_text": "Often Working"
},
"host_status": {
"status": "ok",
"location": "example.com",
"status_text": "Almost Working"
},
"error_source": "host",
"what_happened": "This site is still working. And it looks great.",
"what_can_i_do": "Visit the site before it crashes someday."
},
};
if (window.location.search == '') {
window.history.pushState('sss', '', '?s');
}
function extractUrlParam(str, key) {
const urlParams = new URLSearchParams(str)
return urlParams.get(key)
}
function getDefaultPresetName() {
const key = 'from';
let name = extractUrlParam(window.location.search, key);
if (!name) {
name = extractUrlParam(window.location.hash.substr(1), key)
}
return name
}
const defaultPresetName = getDefaultPresetName();
if (defaultPresetName && defaultPresetName.indexOf('/') < 0) {
fetch(`../s/${defaultPresetName}`, {
headers: {
'Accept': 'application/json'
},
}).then(response => {
if (!response.ok) {
throw new Error('failed to get preset');
}
return response.json();
}).then(result => {
if (result.status != 'ok') {
return
}
console.log(result.parameters)
initialConfig = result.parameters
loadConfig(initialConfig);
render()
})
}
/* Utilities */
function $(id) { return document.getElementById(id); }
/* Fill form from config */
function loadConfig(cfg) {
$('title').value = cfg.title ?? '';
$('error_code').value = cfg.error_code ?? '';
$('more_hidden').checked = !!(cfg.more_information && cfg.more_information.hidden);
$('more_text').value = cfg.more_information?.text ?? '';
$('more_link').value = cfg.more_information?.link ?? '';
$('browser_status').value = cfg.browser_status?.status ?? 'ok';
$('browser_location').value = cfg.browser_status?.location ?? '';
$('browser_name').value = cfg.browser_status?.name ?? '';
$('browser_status_text').value = cfg.browser_status?.status_text ?? '';
$('cloudflare_status').value = cfg.cloudflare_status?.status ?? 'ok';
$('cloudflare_location').value = cfg.cloudflare_status?.location ?? '';
$('cloudflare_name').value = cfg.cloudflare_status?.name ?? '';
$('cloudflare_status_text').value = cfg.cloudflare_status?.status_text ?? '';
$('host_status').value = cfg.host_status?.status ?? 'ok';
$('host_location').value = cfg.host_status?.location ?? '';
$('host_name').value = cfg.host_status?.name ?? '';
$('host_status_text').value = cfg.host_status?.status_text ?? '';
if (cfg.error_source === 'browser') $('err_browser').checked = true;
else if (cfg.error_source === 'cloudflare') $('err_cloudflare').checked = true;
else $('err_host').checked = true;
$('what_happened').value = cfg.what_happened ?? '';
$('what_can_i_do').value = cfg.what_can_i_do ?? '';
$('perf_text').value = cfg.perf_sec_by?.text ?? '';
$('perf_link').value = cfg.perf_sec_by?.link ?? '';
}
/* Read config from form inputs */
function readConfig() {
return {
title: $('title').value,
error_code: Number($('error_code').value) || 500,
more_information: {
hidden: !!$('more_hidden').checked,
text: $('more_text').value,
link: $('more_link').value
},
browser_status: {
status: $('browser_status').value,
location: $('browser_location').value,
name: $('browser_name').value,
status_text: $('browser_status_text').value
},
cloudflare_status: {
status: $('cloudflare_status').value,
location: $('cloudflare_location').value,
name: $('cloudflare_name').value,
status_text: $('cloudflare_status_text').value
},
host_status: {
status: $('host_status').value,
location: $('host_location').value,
name: $('host_name').value,
status_text: $('host_status_text').value
},
error_source: (document.querySelector('input[name="error_source"]:checked') || { value: 'host' }).value,
what_happened: $('what_happened').value,
what_can_i_do: $('what_can_i_do').value,
perf_sec_by: {
text: $('perf_text').value,
link: $('perf_link').value
}
};
}
function formatUtcTimestamp() {
const d = new Date();
const year = d.getUTCFullYear();
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
const day = String(d.getUTCDate()).padStart(2, "0");
const hours = String(d.getUTCHours()).padStart(2, "0");
const minutes = String(d.getUTCMinutes()).padStart(2, "0");
const seconds = String(d.getUTCSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} UTC`;
}
function renderEjs(params, use_cdn = true) {
return template({
params: params,
resources_use_cdn: use_cdn
})
}
/* Basic render: build HTML string from config and put into iframe.srcdoc */
function render() {
const cfg = readConfig();
window.lastCfg = cfg
if (!template) {
postponeRendering = true
return
}
cfg.time = formatUtcTimestamp()
cfg.ray_id = '0123456789abcdef'
cfg.client_ip = '1.1.1.1'
let pageHtml = renderEjs(cfg)
// Write into iframe
const iframe = $('previewFrame');
iframe.srcdoc = pageHtml;
// store last rendered HTML for "open in new tab"
lastRenderedHtml = pageHtml;
updateStatusBlockStyles();
}
/* Open in new tab: create blob and open */
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');
// revoke after some time to avoid memory leak
setTimeout(() => URL.revokeObjectURL(url), 15_000);
}
function createShareableLink() {
$('shareLink').value = ''
fetch('../s/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'parameters': window.lastCfg
})
})
.then(response => {
if (!response.ok) {
alert('failed to create link');
}
return response.json();
})
.then(result => {
if (result.status != 'ok') {
alert('failed to create link');
return
}
$('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 link = document.createElement('a')
const url = URL.createObjectURL(file)
link.href = url
link.download = file.name
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
}
/* Wire up events */
document.addEventListener('DOMContentLoaded', () => {
// initialize form values from initialConfig
loadConfig(initialConfig);
render();
// On preset change, load preset and render
$('presetSelect').addEventListener('change', e => {
const p = e.target.value;
if (PRESETS[p]) loadConfig(PRESETS[p]);
render();
});
// 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() });
$('btnCopyLink').addEventListener('click', () => {
const field = $('shareLink');
field.select();
field.setSelectionRange(0, field.value.length);
navigator.clipboard.writeText(field.value).then(() => {
// No notification required unless you want one
});
});
// 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();
}, 250));
// for radio change events (error_source)
if (inp.type === 'radio') inp.addEventListener('change', () => { render(); });
});
});
/* Update status block colors based on selected status and error_source */
function updateStatusBlockStyles() {
const browserOk = $('browser_status').value === 'ok';
const cfOk = $('cloudflare_status').value === 'ok';
const hostOk = $('host_status').value === 'ok';
setBlockClass('block_browser', browserOk ? 'status-ok' : 'status-error');
setBlockClass('block_cloudflare', cfOk ? 'status-ok' : 'status-error');
setBlockClass('block_host', hostOk ? 'status-ok' : 'status-error');
}
function setBlockClass(id, cls) {
const el = $(id);
if (!el) return;
el.classList.remove('status-ok', 'status-error');
el.classList.add(cls);
}
/* Simple debounce */
function debounce(fn, wait) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
}
})()
</script>
<style>
body {
zoom: 90%;
}
/* 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;
}
}
/* 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;
min-height: 100%;
}
/* Compact form: label and control same row */
.form-row {
display: flex;
gap: .3rem;
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 {
width: 100%;
height: 100%;
border: 1px solid #ddd;
flex: 1 1 auto;
min-height: 360px;
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;
}
</style>
</head>
<body class="bg-light">
<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">Default</option>
<option value="empty">Empty</option>
<option value="catastrophic">Catastrophic failure</option>
<option value="working">Server working</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" type="number" class="form-control form-control-sm" />
</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>
<input class="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" />
<label for="err_browser" class="ms-1 small">Error here</label>
</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>
<input class="form-check-input" type="radio" name="error_source" id="err_cloudflare"
value="cloudflare" />
<label for="err_cloudflare" class="ms-1 small">Error here</label>
</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>
<input class="form-check-input" type="radio" name="error_source" id="err_host" value="host" />
<label for="err_host" class="ms-1 small">Error here</label>
</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="mt-3 mb-3">
<h6 class="mb-2">Visit ... for more information</h6>
<div class="status-block">
<div class="form-row">
<label>Hidden</label>
<div class="control d-flex align-items-center gap-2">
<input id="more_hidden" class="form-check-input" type="checkbox" />
<!-- <small class="text-muted">If checked, the "visit ..." line is hidden</small> -->
</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>
</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 id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button>
<button id="btnExport" class="btn btn-sm btn-primary">Export JSON</button>
</div>
<button id="btnShare" class="btn btn-sm btn-primary">Create shareable link</button>
<div class="mt-2">
<div class="input-group input-group-sm">
<input id="shareLink" class="form-control" readonly />
<button id="btnCopyLink" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
</div>
<div class="fw-semibold 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 the 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>
<iframe id="previewFrame" class="preview-frame" sandbox="allow-scripts"></iframe>
</div>
</div>
</div>
<!-- JS: Bootstrap bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>