feat(website): add the fl.narl.io site
Check and Windows preview / check (anvil) (push) Successful in 49s
Check and Windows preview / check (crucible) (push) Successful in 48s
Check and Windows preview / check (knurl) (push) Successful in 40s
Check and Windows preview / check (lathe) (push) Successful in 41s
Check and Windows preview / check (patina) (push) Successful in 56s
Check and Windows preview / check (rivet) (push) Successful in 45s
Check and Windows preview / check (temper) (push) Successful in 43s
Check and Windows preview / check (weld) (push) Successful in 1m20s
Check and Windows preview / previews (push) Successful in 1m15s
Check and Windows preview / windows (push) Successful in 2m31s

Astro site for the suite: a home page with the plugin cards, install
steps, principles, the metalwork names and the ideas on the bench, one
page per plugin, and the Impressum and privacy policy. Plugin names,
versions, download links and editor previews come from the catalog, the
wrapper manifests and docs/*/assets at build time.

Served by unprivileged nginx in Docker behind Traefik on fl.narl.io,
the same setup as proton.narl.io: no access logs, CSP 'self', no
JavaScript, legal pages filled from the environment at start.
This commit is contained in:
nvrl committed 2026-11-06 11:30:12 +01:00
1 parent 5cc3770737
commit 7e1776bb04
40 files changed
+8515

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
#!/bin/sh
# Fills the Impressum and the privacy policy with the operator's details from the environment.
# Runs from the nginx image's /docker-entrypoint.d/ hook on every start. It works from pristine
# copies, so changing a variable and restarting the container is enough.
set -eu
HTML=/usr/share/nginx/html
TEMPLATES=/opt/legal-templates
PAGES="impressum/index.html datenschutz/index.html"
missing=""
for var in IMPRESSUM_NAME IMPRESSUM_STREET IMPRESSUM_CITY IMPRESSUM_EMAIL; do
eval "value=\${$var:-}"
[ -n "$value" ] || missing="$missing $var"
done
if [ -n "$missing" ]; then
echo "40-legal-notice: missing required variables:$missing" >&2
echo "40-legal-notice: refusing to serve the site without an Impressum (see .env.example)" >&2
exit 1
fi
case "$IMPRESSUM_EMAIL" in
*@*.*) ;;
*)
echo "40-legal-notice: IMPRESSUM_EMAIL does not look like an email address" >&2
exit 1
;;
esac
# One line, HTML-escaped.
html() {
printf '%s' "$1" | tr '\r\n' ' ' |
sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g' -e 's/"/\&quot;/g' -e "s/'/\&#39;/g"
}
# Every character as a numeric entity, which keeps the address away from naive scrapers.
entities() {
printf '%s' "$1" | od -An -tu1 | tr -s ' \n' '\n\n' | sed -e '/^$/d' -e 's/.*/\&#&;/' | tr -d '\n'
}
# Escapes a replacement for sed's s|…|…| (backslash, the | delimiter and &).
repl() {
printf '%s' "$1" | sed -e 's/[\\|&]/\\&/g'
}
script=$(mktemp)
trap 'rm -f "$script"' EXIT
{
echo "s|%%IMPRESSUM_NAME%%|$(repl "$(html "$IMPRESSUM_NAME")")|g"
echo "s|%%IMPRESSUM_STREET%%|$(repl "$(html "$IMPRESSUM_STREET")")|g"
echo "s|%%IMPRESSUM_CITY%%|$(repl "$(html "$IMPRESSUM_CITY")")|g"
echo "s|%%IMPRESSUM_COUNTRY%%|$(repl "$(html "${IMPRESSUM_COUNTRY:-Deutschland}")")|g"
echo "s|%%IMPRESSUM_EMAIL%%|$(repl "$(entities "$IMPRESSUM_EMAIL")")|g"
for pair in phone:IMPRESSUM_PHONE hoster:HOSTER; do
key=${pair%%:*}
var=${pair#*:}
eval "value=\${$var:-}"
if [ -n "$value" ]; then
echo "s|%%$var%%|$(repl "$(html "$value")")|g"
else
echo "s|<p data-optional=\"$key\"[^>]*>[^<]*</p>||g"
fi
done
} >"$script"
for page in $PAGES; do
sed -f "$script" "$TEMPLATES/$page" >"$HTML/$page"
if grep -q '%%[A-Z_]*%%' "$HTML/$page"; then
echo "40-legal-notice: unfilled placeholder left in $page" >&2
exit 1
fi
done
echo "40-legal-notice: filled in the Impressum and the privacy policy"
+63
View File
@@ -0,0 +1,63 @@
# The privacy policy (src/pages/datenschutz.astro) promises that no access logs are written.
# Keep access_log off and the error log at "crit" unless that text changes with it.
worker_processes auto;
error_log /dev/stderr crit;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log off;
log_not_found off;
server_tokens off;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
charset utf-8;
charset_types text/css application/javascript image/svg+xml application/xml text/plain;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/css application/javascript image/svg+xml application/xml text/plain application/json;
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
# Relative Location headers, so redirects keep the scheme and host of the reverse proxy.
absolute_redirect off;
error_page 404 /404.html;
include /etc/nginx/security-headers.conf;
add_header Cache-Control "no-cache" always;
location /_astro/ {
include /etc/nginx/security-headers.conf;
add_header Cache-Control "public, max-age=31536000, immutable" always;
try_files $uri =404;
}
location = /healthz {
default_type text/plain;
return 200 "ok\n";
}
location / {
try_files $uri $uri/ =404;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
# Included in every location that sets its own headers, because add_header is not inherited
# into a location that has any add_header of its own.
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; manifest-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;