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());
}

}