Caddyfile Generator
function Model() { let self = this;
this.enableAdmin = ko.observable(false);
this.addHtmlPages = ko.observable(true);
this.addHtmlPagesWithTrailingSlash = ko.observable(true);
this.redirectRootToIndexHtml = ko.observable(true);
this.enablePrometheusMetrics = ko.observable(false);
this.port = ko.observable(8080);
this.enable404 = ko.observable(true);
this.enableHtmlTemplates = ko.observable(false);
this.disableTls = ko.observable(true);
this.code = ko.observable("");
this.__skip = ["code"];
this.applyHighlight = function (elements) {
elements
.filter(x => x.tagName === "PRE")
.forEach(x => {
if (window.hljs) window.hljs.highlightElement(x);
});
};
}
document.addEventListener("DOMContentLoaded", function (event) {
hljs.addPlugin(new CopyButtonPlugin())
const model = new Model();
ko.persistChanges(model, "", { storage: new QueryStringStorage() }, 0);
ko.applyBindings(model, document.getElementById("source"));
});
Port:
Add HTML pages:
Add trailing / support:
Redirect root to index.html:
Enable 404 HTML page:
Enable HTML templates:
Fake admin:
Enable Prometheus metrics:
Disable TLS:
Generatorv1.0.2
{
no need for an admin interface
admin off
https is done by something else
auto_https off
enable prometheus metrics
servers { metrics } }
: {
Set the root directory for serving files
root * ./app/
Serve all static files including HTML, CSS, JS, images, etc.
file_server
Enable Server Side Includes for HTML files
templates { file_extensions .html }
First, rewrite URLs with trailing slash to the same path without the slash
@trailingSlash { path_regexp trailing ^(.+)/$ } rewrite @trailingSlash /{http.regexp.trailing.1}
Try to rewrite URLs to corresponding .html files if they exist
try_files {path}.html {path}
encode zstd gzip
Handle 404 errors
handle_errors { @404 { expression {http.error.status_code} == 404 } rewrite @404 /404.html file_server }
rewrite the root URL to /index.html
rewrite / /index.html
redirect the /index.html to /
redir /index.html / 301 redir /index / 301 redir /index/ / 301
Block access to the includes directory
@includes { path /includes/* }
}
mimic the admin endpoint
:2019 { respond /status "I'm up!" 200 metrics /metrics encode zstd gzip }
ko.bindingHandlers.saveHtmlContent = { init: function (element, valueAccessor) { var htmlObservable = valueAccessor();
// Function to get the inner text only (ignores HTML and comments)
function extractInnerText(htmlElement) {
return htmlElement.textContent || htmlElement.innerText;
}
// Save the inner text content
htmlObservable(extractInnerText(element));
// Set up a MutationObserver to monitor changes in the element's content
var observer = new MutationObserver(function () {
htmlObservable(extractInnerText(element));
});
// Start observing changes in the child elements
observer.observe(element, {
childList: true,
subtree: true,
characterData: true
});
// Cleanup when the element is removed
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
observer.disconnect();
});
}
};
feature-image {
display: none;
}
.entry-content { width: 100% !important; }
source {
max-width: 100%;
.form {
margin-top: 2em;
margin-bottom: 1em;
font-family: arial;
font-size: 14px;
background-color: var(--light-background);
padding: 2em;
column-count: 2;
column-gap: 2em;
& > div + div {
margin-top: 0.5em;
}
input[type="number"] {
background-color: #fff;
color: #000;
padding: 2px 2px 2px 10px;
border-radius: 4px;
}
label {
width: 180px;
display: inline-block;
}
}
pre {
padding: 2em !important;
}
}
.entry-content > p { margin-left: 0; max-width: 100%; }
@media screen and (max-width: 700px) { .form { column-count: 1 !important; }
.form br {
display: none;
}
}
ko.trackChange = (store, observable, key, echo = null) => { //initialize from stored value, or if no value is stored yet, //use the current value
const value = store.get(key);
if (value !== null) {
if (echo) echo("Restoring value for", key, value);
//restore current value
observable(value);
}
//track the changes
observable.subscribe(newValue => {
if (echo) echo("Storing new value for", key, newValue);
store.set(key, newValue);
});
};
ko.isComputed = instance => { if (!instance || !instance.__ko_proto__) { return false; }
if (instance.__ko_proto__ === ko.dependentObservable) {
return true;
}
// Walk the prototype chain
return ko.isComputed(instance.__ko_proto__);
};
const defaultOptions = Object.freeze({ storage: localStorage, traverseNonObservableProperties: true, debug: false });
ko.persistChanges = (model, prefix = "model-", options = defaultOptions, deep = 0) => { options = Object.assign({}, defaultOptions, options); options.echo = function () { if (!options.debug) return;
if (deep > 0) {
return console.log("-".repeat(deep), ...arguments);
}
console.log(...arguments);
};
const storageWrapper = {
set: (key, value) => options.storage.setItem(key, JSON.stringify(value)),
get: key => JSON.parse(options.storage.getItem(key))
};
const skip = new Set(model.__skip || []);
skip.add("__skip");
for (let n in model) {
const observable = model[n];
const key = prefix + n;
if (skip.has(n)) {
options.echo("Skipping", n, "because it is on the __skip list.");
continue;
}
if (ko.isComputed(observable)) {
options.echo("Skipping", n, "because it is computed.");
continue;
}
if (typeof observable === "function") {
if (!ko.isObservable(observable)) {
options.echo("Skipping", n, "because it is a function.");
continue;
}
ko.trackChange(storageWrapper, observable, key, options.echo);
options.echo("Tracking change for", n, "in", key);
continue;
}
if (!options.traverseNonObservableProperties) {
options.echo("Skipping", n, "because options.traverseNonObservableProperties is false.");
continue;
}
if (typeof observable === "object" && observable !== null && !Array.isArray(observable)) {
options.echo("Tracking change for object", key);
ko.persistChanges(observable, key + "-", options, deep + 1);
continue;
}
options.echo("Skipping", n, observable);
}
};
class QueryStringStorage { getItem(key) { let value = new URLSearchParams(window.location.search).get(key);
if (!value) return value;
if (value != "false" && value != "true" && value.length > 0 && !["{", "[", '"'].includes(value[0])) {
return '"' + value + '"';
}
return value;
}
setItem(key, value) {
const params = new URLSearchParams(window.location.search);
if (value && value.length > 0) {
let f = value[0];
if (f == '"') {
//string
value = value.substring(1, value.length - 1);
}
}
params.set(key, value);
window.history.replaceState({}, "", "?" + params.toString());
}
}