Build jerboa.sh site

ober

bf54f76d29cfbf60e89b0d0f4328994e4fc4deac

diff --git a/Makefile b/Makefile
index b6cc27d..3520b0a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
 JERBOA_HOME ?= $(HOME)/mine/jerboa
-SCHEME ?= $(JERBOA_HOME)/.chez/bin/scheme
-JERBUILD ?= $(JERBOA_HOME)/jerbuild.ss
+JERBOA ?= jerboa
+JERBUILD ?= $(JERBOA) jerbuild
+UNAME_S := $(shell uname -s)
 
 BUILD_DIR ?= build
 SRC_STAGE := $(BUILD_DIR)/src
@@ -9,11 +10,15 @@ LIBDIRS := $(LIB_STAGE):$(JERBOA_HOME)/lib
 ENTRY ?= secure-site.ss
 BINARY_OUTPUT ?= dist/jerboa-sinatra-site
 STATIC_BINARY_OUTPUT ?= dist/jerboa-sinatra-site-linux-amd64
-PROJECT_LIBDIRS := $(abspath $(LIB_STAGE)):$(JERBOA_HOME)/lib
+PROJECT_LIBDIRS := $(abspath $(LIB_STAGE))
+BINARY_NATIVE_LDFLAGS ?= $(if $(filter Darwin,$(UNAME_S)),-lc++,)
 STATIC_TARGET_MACHINE ?= ta6le
+STATIC_RUST_TARGET ?= x86_64-unknown-linux-musl
 STATIC_CHEZ_PREFIX ?= $(JERBOA_HOME)/.chez-cross-$(STATIC_TARGET_MACHINE)
+STATIC_CSV_DIR ?= $(firstword $(wildcard $(STATIC_CHEZ_PREFIX)/lib/csv*/$(STATIC_TARGET_MACHINE)))
 STATIC_XPATCH ?= $(JERBOA_HOME)/build/chez/xc-$(STATIC_TARGET_MACHINE)/s/xpatch
 STATIC_MUSL_CC ?= x86_64-linux-musl-gcc
+STATIC_OS_LIBS ?= -lm -ldl -lpthread -static -no-pie -lstdc++
 
 .PHONY: build test example binary static-binary clean distclean
 
@@ -22,31 +27,24 @@ build:
 	mkdir -p $(SRC_STAGE)/sinatra $(LIB_STAGE)
 	cp sinatra.ss sinatra-test.ss $(SRC_STAGE)/
 	cp sinatra/*.ss $(SRC_STAGE)/sinatra/
-	$(SCHEME) --libdirs $(JERBOA_HOME)/lib --script $(JERBUILD) $(SRC_STAGE) $(LIB_STAGE) --force
+	$(JERBUILD) transpile $(SRC_STAGE) $(LIB_STAGE) --force
 
 test: build
-	$(SCHEME) --libdirs $(LIBDIRS) --script test-runner.ss
+	$(JERBUILD) exec --libdirs $(LIBDIRS) test-runner.ss
 
 binary: build
 	mkdir -p $(dir $(BINARY_OUTPUT))
-	BINARY_LIBDIRS="$(PROJECT_LIBDIRS)" $(JERBOA_HOME)/support/build-binary.sh $(ENTRY) $(BINARY_OUTPUT)
+	JERBOA_HOME="$(JERBOA_HOME)" $(JERBUILD) binary --static-native $(foreach flag,$(BINARY_NATIVE_LDFLAGS),--extra-ldflag $(flag)) --libdirs "$(PROJECT_LIBDIRS)" $(ENTRY) $(BINARY_OUTPUT)
 
 static-binary: build
-	@ls "$(STATIC_CHEZ_PREFIX)"/lib/csv*/"$(STATIC_TARGET_MACHINE)"/libkernel.a >/dev/null 2>&1 || { echo "ERROR: static Chez libkernel not found under $(STATIC_CHEZ_PREFIX); override STATIC_CHEZ_PREFIX or build the cross Chez first" >&2; exit 1; }
+	@test -n "$(STATIC_CSV_DIR)" && test -f "$(STATIC_CSV_DIR)/libkernel.a" || { echo "ERROR: static Chez libkernel not found under $(STATIC_CHEZ_PREFIX); override STATIC_CHEZ_PREFIX or build the cross Chez first" >&2; exit 1; }
 	@test -f "$(STATIC_XPATCH)" || { echo "ERROR: xpatch not found at $(STATIC_XPATCH); override STATIC_XPATCH or build the cross Chez first" >&2; exit 1; }
 	@command -v "$(STATIC_MUSL_CC)" >/dev/null 2>&1 || { echo "ERROR: $(STATIC_MUSL_CC) not on PATH" >&2; exit 1; }
 	mkdir -p $(dir $(STATIC_BINARY_OUTPUT))
-	BINARY_LIBDIRS="$(PROJECT_LIBDIRS)" \
-	JERBOA_HOME="$(JERBOA_HOME)" \
-	JERBOA_MUSL_CHEZ_PREFIX="$(STATIC_CHEZ_PREFIX)" \
-	JERBOA_XPATCH="$(STATIC_XPATCH)" \
-	TARGET_MACHINE="$(STATIC_TARGET_MACHINE)" \
-	MUSL_CC="$(STATIC_MUSL_CC)" \
-	SCHEME="$(SCHEME)" \
-	$(JERBOA_HOME)/support/build-static-script.sh $(ENTRY) $(STATIC_BINARY_OUTPUT)
+	JERBOA_HOME="$(JERBOA_HOME)" $(JERBUILD) binary --static-native --cc "$(STATIC_MUSL_CC)" --rust-target "$(STATIC_RUST_TARGET)" --csv-dir "$(STATIC_CSV_DIR)" --xpatch "$(STATIC_XPATCH)" --os-libs "$(STATIC_OS_LIBS)" --libdirs "$(PROJECT_LIBDIRS)" $(ENTRY) $(STATIC_BINARY_OUTPUT)
 
 example: build
-	$(SCHEME) --libdirs $(LIBDIRS) --script example.ss
+	$(JERBUILD) exec --libdirs $(LIBDIRS) example.ss
 
 clean:
 	rm -rf $(BUILD_DIR)
diff --git a/example.ss b/example.ss
index 5b3c1e6..9aac67c 100644
--- a/example.ss
+++ b/example.ss
@@ -1,11 +1,9 @@
-#!chezscheme
 ;;; example.ss - Jerboa-Sinatra feature showcase
 ;;;
 ;;; Run with: make example
 ;;; Then visit: http://127.0.0.1:4567/
 
-(import (chezscheme)
-        (jerboa runtime)
+(import (jerboa runtime)
         (sinatra))
 
 ;; Configuration
diff --git a/secure-site.ss b/secure-site.ss
index f76243d..8589367 100644
--- a/secure-site.ss
+++ b/secure-site.ss
@@ -1,31 +1,205 @@
-#!chezscheme
-
-(import (chezscheme)
-        (only (std misc thread) thread-sleep!)
+(import (only (std misc thread) thread-sleep!)
         (sinatra)
         (sinatra security))
 
 (set-option! "environment" "production")
 (set-option! "static" #f)
 
+(define logo-uri
+  "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAyNCIgaGVpZ2h0PSIxMDI0IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgYXJpYS1sYWJlbGxlZGJ5PSJ0aXRsZSBkZXNjIj4KICA8dGl0bGUgaWQ9InRpdGxlIj5KZXJib2EgbGFuZ3VhZ2UgbG9nbzwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2MiPkEgY2FydG9vbiBqZXJib2EgbWFzY290IHdpdGggbGFyZ2UgZWFycywgbG9uZyBoaW5kIGxlZ3MsIGEgY3VydmVkIHRhaWwsIGEgbGFtYmRhIGJhZGdlLCBhbmQgdGhlIEplcmJvYSB3b3JkbWFyay48L2Rlc2M+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImZ1ck1haW4iIHgxPSIzMDIiIHkxPSIyNDgiIHgyPSI3MjAiIHkyPSI3NTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZGZmNGZmIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMC40OCIgc3RvcC1jb2xvcj0iIzhiYjlkNiIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM1MjcxOGYiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImZ1ckRhcmsiIHgxPSI1MDgiIHkxPSIyMTQiIHgyPSI3NTIiIHkyPSI3MTAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjNmY5ZWMyIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJmNGM2NSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmVsbHkiIHgxPSIzOTIiIHkxPSI0MjEiIHgyPSI2MDAiIHkyPSI3MjkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZjRmYmZmIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2M2ZTNmMyIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZWFySW5uZXIiIHgxPSIzNDQiIHkxPSIxMTEiIHgyPSI2ODAiIHkyPSIzNTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZGNlY2ZmIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzhjYWFkMiIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxmaWx0ZXIgaWQ9InNvZnRTaGFkb3ciIHg9Ii0yMCUiIHk9Ii0yMCUiIHdpZHRoPSIxNDAlIiBoZWlnaHQ9IjE0MCUiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CiAgICAgIDxmZURyb3BTaGFkb3cgZHg9IjAiIGR5PSIxOCIgc3RkRGV2aWF0aW9uPSIxOCIgZmxvb2QtY29sb3I9IiMwYjFkMmMiIGZsb29kLW9wYWNpdHk9IjAuMiIvPgogICAgPC9maWx0ZXI+CiAgICA8c3R5bGU+CiAgICAgIC5vdXRsaW5lIHsgc3Ryb2tlOiAjMTMyMzM0OyBzdHJva2Utd2lkdGg6IDE4OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0KICAgICAgLmRldGFpbCB7IHN0cm9rZTogIzEzMjMzNDsgc3Ryb2tlLXdpZHRoOiAxMjsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyB9CiAgICAgIC5maW5lIHsgc3Ryb2tlOiAjMTMyMzM0OyBzdHJva2Utd2lkdGg6IDg7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CgogIDxwYXRoIGQ9Ik0xOTkgODc1YzcyLTMzIDU1MC0zMyA2MjIgMCIgY2xhc3M9ImRldGFpbCIgb3BhY2l0eT0iMC4xNiIvPgoKICA8ZyBmaWx0ZXI9InVybCgjc29mdFNoYWRvdykiPgogICAgPHBhdGggZD0iTTcwOCA1NzVjNTcgMjIgMTExIDMyIDE1OSAyMSA2My0xNCA5MC01OCA3Ni0xMDAtMTItMzctNTUtNDktOTItMjgtMjUgMTQtNDMgNDMtMzUgNzIgNiAyMiAyNiAzNCA1MCAzMyIgY2xhc3M9ImRldGFpbCIgc3Ryb2tlPSIjMTMyMzM0Ii8+CiAgICA8cGF0aCBkPSJNNzA5IDU3NGM2MyAzMSAxMTkgNDMgMTY0IDI4IDU0LTE4IDc1LTYwIDYzLTk2LTEwLTI5LTQ1LTM4LTc0LTIyLTIwIDExLTMzIDM0LTI4IDUzIDQgMTcgMTkgMjQgMzYgMjMiIHN0cm9rZT0iI2M4ZTZmNiIgc3Ryb2tlLXdpZHRoPSIzMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CgogICAgPHBhdGggZD0iTTU0OCAxOTFjMjItNzMgNzctMTI5IDEyMy0xMjcgNDEgMiA1NyA1MSAzNCAxMTItMjAgNTQtNzEgMTExLTEyOSAxNDMiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CiAgICA8cGF0aCBkPSJNNTgyIDIwM2MxOC00NSA1Mi04MiA3OS04NiAyMi0zIDMwIDIwIDE4IDU1LTEyIDM1LTQ3IDc0LTg0IDk4IiBmaWxsPSJ1cmwoI2VhcklubmVyKSIgY2xhc3M9ImZpbmUiLz4KCiAgICA8cGF0aCBkPSJNNDMwIDIwNWMtMzItNzEtODgtMTE4LTEzMS0xMTAtNDAgNy00OCA1OC0xOCAxMTUgMjYgNTEgODMgMTAxIDE0NSAxMjQiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CiAgICA8cGF0aCBkPSJNMzk3IDIxOWMtMjMtNDMtNjEtNzQtODgtNzQtMjIgMS0yNyAyNS0xMSA1NyAxNiAzMyA1NSA2NyA5NSA4NSIgZmlsbD0idXJsKCNlYXJJbm5lcikiIGNsYXNzPSJmaW5lIi8+CgogICAgPHBhdGggZD0iTTYxNiA2NjljNTAgNDUgMTAyIDc2IDE2MiA3NSA0Mi0xIDY4IDE5IDY4IDQ3IDAgMzUtNDMgNTctOTcgNDUtNjgtMTUtMTIwLTU4LTE2Mi0xMjYiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CiAgICA8cGF0aCBkPSJNNjU1IDcyNGMzMyAyNiA2OCA0MCAxMDUgMzkgMjgtMSA0NiA5IDQ3IDI1IDEgMjAtMjggMzAtNjYgMjEtNDAtOS03OC0zOC0xMTMtODUiIGZpbGw9IiNkOWVmZmEiIGNsYXNzPSJmaW5lIi8+CgogICAgPHBhdGggZD0iTTM3NCA2NDFjLTYxIDM3LTEwOSA4Ni0xMzUgMTUxLTE4IDQ2LTU1IDU4LTgzIDM2LTM1LTI3LTIyLTgzIDI5LTEyMSA1NC00MSAxMDktNzQgMTY0LTk5IiBmaWxsPSJ1cmwoI2Z1ck1haW4pIiBjbGFzcz0ib3V0bGluZSIvPgogICAgPHBhdGggZD0iTTMxNyA2ODNjLTM2IDI4LTYzIDYzLTgxIDEwNS0xMCAyNC0yOCAzMi00MiAyMi0xOS0xNC0xMC00MyAyMi02OCAzNi0yNyA3Mi00OSAxMDgtNjYiIGZpbGw9IiNkOWVmZmEiIGNsYXNzPSJmaW5lIi8+CgogICAgPHBhdGggZD0iTTU3NSAzNjVjODQgNDAgMTQxIDEyNCAxMzkgMjE4LTQgMTM1LTkzIDIyMy0yMjEgMjE5LTEyNi01LTIxNy05OC0yMTEtMjI1IDQtOTIgNjEtMTc0IDE0NS0yMTIiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CiAgICA8cGF0aCBkPSJNNDEwIDQ2OWMtNTAgNTEtNzAgMTQyLTM5IDIxMSAyOCA2MiA3NiA5NSAxMzMgOTcgNjEgMiAxMTMtMzAgMTQzLTkyIDMyLTY5IDgtMTY0LTQxLTIxNi0yMiA3MS02MyAxMDktMTAwIDEwOS0zNiAwLTc0LTM3LTk2LTEwOVoiIGZpbGw9InVybCgjYmVsbHkpIiBjbGFzcz0iZGV0YWlsIi8+CgogICAgPHBhdGggZD0iTTUxMyAxODljMTAwIDAgMTc0IDcyIDE3NCAxNzQgMCAxMDQtNzYgMTg0LTE3NyAxODQtMTA2IDAtMTg0LTc2LTE4NC0xODEgMC0xMDUgNzgtMTc3IDE4Ny0xNzdaIiBmaWxsPSJ1cmwoI2Z1ck1haW4pIiBjbGFzcz0ib3V0bGluZSIvPgogICAgPHBhdGggZD0iTTQ0OCA0MTRjMjEgMzkgMTA5IDQxIDEzMiAxIDMgNTEtMjEgODgtNjUgODktNDUgMS03Mi0zNi02Ny05MFoiIGZpbGw9IiNmNmZiZmYiIGNsYXNzPSJmaW5lIi8+CiAgICA8cGF0aCBkPSJNNTEyIDM4NmMxOCAwIDM0IDkgMzQgMjIgMCAxNC0xNyAzMC0zNCAzMC0xOCAwLTM0LTE2LTM0LTMwIDAtMTMgMTYtMjIgMzQtMjJaIiBmaWxsPSIjMTMyMzM0Ii8+CiAgICA8cGF0aCBkPSJNNTAzIDQzNWMtMTQgMTgtMzEgMjYtNTEgMjYiIGNsYXNzPSJmaW5lIi8+CiAgICA8cGF0aCBkPSJNNTIyIDQzNWMxNSAxOCAzMiAyNiA1MiAyNiIgY2xhc3M9ImZpbmUiLz4KCiAgICA8Y2lyY2xlIGN4PSI0MzgiIGN5PSIzMzUiIHI9IjI0IiBmaWxsPSIjMTMyMzM0Ii8+CiAgICA8Y2lyY2xlIGN4PSI1ODUiIGN5PSIzMzUiIHI9IjI0IiBmaWxsPSIjMTMyMzM0Ii8+CiAgICA8Y2lyY2xlIGN4PSI0NDciIGN5PSIzMjYiIHI9IjciIGZpbGw9IiNmOGZjZmYiLz4KICAgIDxjaXJjbGUgY3g9IjU5NCIgY3k9IjMyNiIgcj0iNyIgZmlsbD0iI2Y4ZmNmZiIvPgogICAgPHBhdGggZD0iTTM0OSAzNzFjLTM4LTE1LTc1LTEzLTExMSA0IiBjbGFzcz0iZmluZSIvPgogICAgPHBhdGggZD0iTTM1MCA0MDdjLTQzIDAtNzkgMTEtMTEwIDM1IiBjbGFzcz0iZmluZSIvPgogICAgPHBhdGggZD0iTTY3NSAzNjljMzgtMTYgNzUtMTYgMTEyIDAiIGNsYXNzPSJmaW5lIi8+CiAgICA8cGF0aCBkPSJNNjc2IDQwNWM0NC0yIDgxIDggMTEzIDMxIiBjbGFzcz0iZmluZSIvPgoKICAgIDxwYXRoIGQ9Ik0zOTkgNTYwYy01MCA1LTg0IDM0LTEwNyA3NyIgY2xhc3M9ImRldGFpbCIvPgogICAgPHBhdGggZD0iTTYyNiA1NjBjNDkgNSA4MiAzMyAxMDQgNzQiIGNsYXNzPSJkZXRhaWwiLz4KICAgIDxwYXRoIGQ9Ik0zNjcgNTc1Yy0xOCAyMi0yOSA0NS0zMyA3MCIgY2xhc3M9ImZpbmUiLz4KICAgIDxwYXRoIGQ9Ik02NTkgNTc1YzE4IDIyIDI5IDQ1IDMzIDcwIiBjbGFzcz0iZmluZSIvPgoKICAgIDxwYXRoIGQ9Ik0zMTUgNDg5Yy0zNi0yLTU4IDE1LTY3IDUwIDI1LTE0IDUwLTE4IDc1LTkiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CiAgICA8cGF0aCBkPSJNNzA4IDQ4OWMzNi0yIDU4IDE1IDY3IDUwLTI1LTE0LTUwLTE4LTc1LTkiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CgogICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjE5IDYyNykgcm90YXRlKC0xMykiPgogICAgICA8Y2lyY2xlIGN4PSIwIiBjeT0iMCIgcj0iNzAiIGZpbGw9IiNlZWY4ZmYiIGNsYXNzPSJkZXRhaWwiLz4KICAgICAgPHRleHQgeD0iMCIgeT0iMzkiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJHZW9yZ2lhLCBUaW1lcyBOZXcgUm9tYW4sIHNlcmlmIiBmb250LXNpemU9IjExMiIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzEzMjMzNCIgc3Ryb2tlPSIjMTMyMzM0IiBzdHJva2Utd2lkdGg9IjIiPiYjOTU1OzwvdGV4dD4KICAgIDwvZz4KICA8L2c+CgogIDxnIGFyaWEtbGFiZWw9IkplcmJvYSI+CiAgICA8dGV4dCB4PSI1MTIiIHk9Ijk1NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkF2ZW5pciBOZXh0LCBUcmVidWNoZXQgTVMsIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzNiIgZm9udC13ZWlnaHQ9IjkwMCIgbGV0dGVyLXNwYWNpbmc9IjAiIGZpbGw9IiMxMzIzMzQiIHN0cm9rZT0iI2U5ZjZmZiIgc3Ryb2tlLXdpZHRoPSIxMiIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgcGFpbnQtb3JkZXI9InN0cm9rZSBmaWxsIj5KZXJib2E8L3RleHQ+CiAgPC9nPgogIDxwYXRoIGQ9Ik0yMzUgNzg0Yy00OCAyNi03NSA1OC03NSA5NiAwIDQ2IDQwIDgzIDExMiAxMTAiIGNsYXNzPSJkZXRhaWwiIG9wYWNpdHk9IjAuMzgiLz4KICA8cGF0aCBkPSJNNzg5IDc4NGM0OCAyNiA3NSA1OCA3NSA5NiAwIDQ2LTQwIDgzLTExMiAxMTAiIGNsYXNzPSJkZXRhaWwiIG9wYWNpdHk9IjAuMzgiLz4KPC9zdmc+Cg==")
+
+(define site-css
+  "html{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;color:#2c3e50;background:#fff}body{margin:0;font-size:16px;line-height:1.7}a{color:#2f8f68;text-decoration:none}a:hover{text-decoration:underline}.navbar{position:sticky;top:0;z-index:10;height:3.6rem;background:#fff;border-bottom:1px solid #eaecef;display:flex;align-items:center;justify-content:space-between;padding:0 1.5rem;box-sizing:border-box}.brand{display:flex;align-items:center;color:#2c3e50;font-size:1.3rem;font-weight:650}.brand-mark{width:2.25rem;height:2.25rem;margin-right:.7rem}.nav{display:flex;gap:1.1rem;align-items:center;font-size:.95rem}.nav-link{color:#3a5169}.nav-link.active{color:#2f8f68;font-weight:650}.repo-link{border:1px solid #d4dde6;border-radius:6px;padding:.25rem .65rem;color:#3a5169}.home{max-width:980px;margin:0 auto;padding:0 2rem}.hero{text-align:center;padding:2.1rem 0 1rem}.hero img{display:block;max-width:270px;width:54vw;max-height:270px;margin:0 auto 1.1rem}.hero h1{font-size:3.25rem;line-height:1.08;margin:.7rem 0;color:#1f2d3a;letter-spacing:0}.description{max-width:43rem;margin:1rem auto 1.5rem;font-size:1.45rem;line-height:1.35;color:#5e7891}.actions{display:flex;gap:.8rem;justify-content:center;flex-wrap:wrap}.action-button{display:inline-block;background:#2f9f74;color:#fff;padding:.72rem 1.35rem;border-radius:4px;border-bottom:1px solid #267e5d;font-size:1.08rem}.action-button:hover{background:#37ad80;text-decoration:none}.secondary-button{display:inline-block;color:#3a5169;border:1px solid #cfd8e3;padding:.72rem 1.1rem;border-radius:4px}.secondary-button:hover{text-decoration:none;border-color:#91a4b7}.features{border-top:1px solid #eaecef;margin-top:2.3rem;padding:1.4rem 0 0;display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between}.feature{flex:1 1 30%;max-width:30%;padding-bottom:1.4rem}.feature h2{font-size:1.35rem;font-weight:560;color:#3a5169;margin:.75rem 0 .25rem}.feature p,.feature li{color:#4e6e8e}.feature ul{padding-left:1.2rem;margin:.45rem 0 0}.content{border-top:1px solid #eaecef;margin-top:2.2rem;padding:2rem 0 3rem}.content h1{font-size:2.3rem;line-height:1.16;color:#1f2d3a;margin:0 0 1rem}.content h2{font-size:1.45rem;color:#3a5169;margin:2rem 0 .45rem}.lead{font-size:1.25rem;color:#5e7891;max-width:48rem}.columns{display:grid;grid-template-columns:repeat(3,1fr);gap:1.1rem;margin-top:1.3rem}.panel{border:1px solid #e1e7ee;border-radius:6px;padding:1rem;background:#fff}.panel h3{margin:.1rem 0 .4rem;color:#2c3e50}.panel p{margin:.35rem 0;color:#4e6e8e}.panel ul{margin:.35rem 0;padding-left:1.15rem;color:#4e6e8e}.code{background:#282c34;color:#f8f8f2;border-radius:6px;padding:1rem 1.2rem;overflow:auto;line-height:1.45;font-family:\"SFMono-Regular\",Consolas,monospace;font-size:.92rem}.muted{color:#6f8194}.footer{border-top:1px solid #eaecef;color:#6f8194;text-align:center;padding:2rem;margin-top:1.2rem}.route-list{display:grid;grid-template-columns:repeat(2,1fr);gap:1rem}.label{font-size:.78rem;text-transform:uppercase;color:#7b8da0;letter-spacing:.05em}.badges{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.65rem}.badge{border:1px solid #d7e0e9;border-radius:999px;padding:.15rem .55rem;color:#4e6e8e;font-size:.86rem}@media(max-width:760px){.navbar{height:auto;min-height:3.6rem;align-items:flex-start;gap:.5rem;flex-direction:column;padding:.7rem 1rem}.nav{width:100%;overflow:auto;padding-bottom:.2rem}.home{padding:0 1.1rem}.hero{padding-top:1.3rem}.hero h1{font-size:2.35rem}.description{font-size:1.15rem}.features{display:block}.feature{max-width:100%}.columns,.route-list{grid-template-columns:1fr}.content h1{font-size:1.9rem}}")
+
+(define site-security-headers
+  (list
+    (cons "Strict-Transport-Security" "max-age=31536000; includeSubDomains")
+    (cons "Content-Security-Policy" "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; object-src 'none'; script-src 'none'; style-src 'self'; img-src 'self' data:")
+    (cons "X-Content-Type-Options" "nosniff")
+    (cons "X-Frame-Options" "DENY")
+    (cons "Referrer-Policy" "no-referrer")
+    (cons "Permissions-Policy" "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()")
+    (cons "Cross-Origin-Opener-Policy" "same-origin")
+    (cons "Cross-Origin-Resource-Policy" "same-origin")))
+
+(set-option! "force-headers" site-security-headers)
+
+(define (nav-link key href label active)
+  (string-append "<a class=\"nav-link"
+                 (if (string=? key active) " active" "")
+                 "\" href=\"" href "\">" label "</a>"))
+
+(define (layout title active body)
+  (string-append
+    "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">"
+    "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
+    "<title>" title " | Jerboa</title>"
+    "<meta name=\"description\" content=\"Jerboa language, runtime, package, and documentation site.\">"
+    "<link rel=\"stylesheet\" href=\"/assets/site.css\"></head><body>"
+    "<header class=\"navbar\"><a class=\"brand\" href=\"/\">"
+    "<img class=\"brand-mark\" src=\"" logo-uri "\" alt=\"Jerboa logo\"><span>Jerboa</span></a>"
+    "<nav class=\"nav\" aria-label=\"Main\">"
+    (nav-link "home" "/" "Home" active)
+    (nav-link "guide" "/guide/" "Guide" active)
+    (nav-link "docs" "/docs/" "Docs" active)
+    (nav-link "packages" "/packages/" "Packages" active)
+    (nav-link "repos" "/repos/" "Repos" active)
+    (nav-link "security" "/security/" "Security" active)
+    "<a class=\"repo-link\" href=\"https://git.sr.ht/~lisp/jerboa\">Source</a>"
+    "</nav></header><main class=\"home\">" body
+    "</main><footer class=\"footer\">Jerboa is served by jerboa-sinatra on Jerboa HTTPSD.</footer></body></html>"))
+
 (define home-page
-  "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>jerboa-sinatra</title><style>body{margin:0;font-family:system-ui,sans-serif;background:#0d1117;color:#f0f6fc;display:grid;min-height:100vh;place-items:center}main{max-width:42rem;padding:2rem}h1{font-size:2.5rem;margin:0 0 1rem}p{line-height:1.6;color:#c9d1d9}</style></head><body><main><h1>jerboa-sinatra</h1><p>This page is embedded in the executable and served over rustls-backed HTTPS with strict response headers.</p></main></body></html>")
+  (layout
+    "Home"
+    "home"
+    (string-append
+      "<section class=\"hero\" aria-labelledby=\"main-title\">"
+      "<img src=\"" logo-uri "\" alt=\"Jerboa mascot logo\">"
+      "<h1 id=\"main-title\">Jerboa</h1>"
+      "<p class=\"description\">A secure, native, batteries-included language and runtime for building static binaries, network services, tools, and packages.</p>"
+      "<p class=\"actions\"><a class=\"action-button\" href=\"/guide/\">Get Started</a><a class=\"secondary-button\" href=\"/docs/\">Read the Docs</a></p>"
+      "</section>"
+      "<section class=\"features\" aria-label=\"Jerboa features\">"
+      "<div class=\"feature\"><h2>Built for Programs That Ship</h2><p>Jerboa has a whole-program binary path, cross-build support, and a standard library shaped for real services.</p></div>"
+      "<div class=\"feature\"><h2>Native Rust Boundary</h2><p>Crypto, TLS, HTTP parsing, regex, compression, and selected OS features live behind one audited Rust native backend.</p></div>"
+      "<div class=\"feature\"><h2>Security First</h2><ul><li>rustls HTTPSD</li><li>strict request framing</li><li>Landlock, seccomp, Capsicum, seatbelt</li></ul></div>"
+      "<div class=\"feature\"><h2>Web and Network Stack</h2><p>HTTP clients, HTTPS servers, fibers, WebSocket, DNS, SMTP, SSH, S3, gRPC, JSON-RPC, and routing are in-tree.</p></div>"
+      "<div class=\"feature\"><h2>Tooling and Packages</h2><p>One command dispatches the REPL, builder, package manager, MCP server, and LSP server.</p></div>"
+      "<div class=\"feature\"><h2>It Speaks Jerboa</h2><div class=\"code\"><pre>(import (jerboa prelude) (std net httpsd))\n\n(def (main)\n  (displayln \"ship it\"))</pre></div></div>"
+      "</section>")))
+
+(define guide-page
+  (layout
+    "Guide"
+    "guide"
+    (string-append
+      "<section class=\"content\"><h1>Get Started</h1>"
+      "<p class=\"lead\">Jerboa uses one command for the language runtime, builder, packages, LSP, and service tooling.</p>"
+      "<h2>Run a file</h2><div class=\"code\"><pre>jerboa app.ss</pre></div>"
+      "<h2>Build a project</h2><div class=\"code\"><pre>jerboa jerbuild transpile src lib --force\njerboa jerbuild exec --libdirs lib app.ss\njerboa jerbuild binary --libdirs lib app.ss dist/app</pre></div>"
+      "<h2>Serve HTTPS</h2><div class=\"code\"><pre>(import (std net httpsd))\n\n(httpsd-start 8443 handler \"fullchain.pem\" \"privkey.pem\")</pre></div>"
+      "<div class=\"columns\"><div class=\"panel\"><h3>1. Write</h3><p>Use Jerboa modules, records, macros, fibers, HTTP handlers, and package manifests.</p></div>"
+      "<div class=\"panel\"><h3>2. Verify</h3><p>Run project tests through Jerboa tooling and keep native boundaries explicit.</p></div>"
+      "<div class=\"panel\"><h3>3. Ship</h3><p>Build a native binary or a static Linux artifact with Jerboa's binary pipeline.</p></div></div>"
+      "</section>")))
+
+(define docs-page
+  (layout
+    "Docs"
+    "docs"
+    (string-append
+      "<section class=\"content\"><h1>Documentation</h1>"
+      "<p class=\"lead\">Start with the language guide, then move into build, security, packages, native Rust integration, and web services.</p>"
+      "<div class=\"route-list\">"
+      "<div class=\"panel\"><span class=\"label\">Language</span><h3>Jerboa Language</h3><p>Reader syntax, modules, macros, records, matching, typed work, and runtime conventions.</p><a href=\"https://git.sr.ht/~lisp/jerboa/tree/master/item/docs/JERBOA-LANG.md\">Open docs</a></div>"
+      "<div class=\"panel\"><span class=\"label\">Build</span><h3>Single Binaries</h3><p>Whole-program builds, boot embedding, static Linux builds, and deployment notes.</p><a href=\"https://git.sr.ht/~lisp/jerboa/tree/master/item/docs/single-binary.md\">Open docs</a></div>"
+      "<div class=\"panel\"><span class=\"label\">Security</span><h3>Hardening</h3><p>Use the secure native backend, capability modules, audit helpers, and binary hardening tools.</p><a href=\"/security/\">Security page</a></div>"
+      "<div class=\"panel\"><span class=\"label\">Packages</span><h3>jpkg</h3><p>Secure package manifests, deterministic artifacts, capability declarations, and local links.</p><a href=\"/packages/\">Package page</a></div>"
+      "</div></section>")))
+
+(define packages-page
+  (layout
+    "Packages"
+    "packages"
+    (string-append
+      "<section class=\"content\"><h1>Packages</h1>"
+      "<p class=\"lead\">Jerboa packages are explicit data: manifests, dependencies, capabilities, and deterministic build outputs.</p>"
+      "<div class=\"code\"><pre>jerboa pkg init\njerboa pkg pack\njerboa pkg verify\njerboa pkg install</pre></div>"
+      "<div class=\"columns\"><div class=\"panel\"><h3>Secure by Default</h3><p>Installs are data operations. Build and native privileges are declared instead of assumed.</p></div>"
+      "<div class=\"panel\"><h3>Local Development</h3><p>Use local links for active packages while keeping publish and verify paths strict.</p></div>"
+      "<div class=\"panel\"><h3>Curated Ecosystem</h3><p>Core packages live beside Jerboa projects for web, DNS, shell, editor, crypto, database, and network tools.</p></div></div>"
+      "</section>")))
+
+(define repos-page
+  (layout
+    "Repos"
+    "repos"
+    (string-append
+      "<section class=\"content\"><h1>Repositories</h1>"
+      "<p class=\"lead\">The core repository is joined by focused Jerboa projects for services, tools, and integrations.</p>"
+      "<div class=\"route-list\">"
+      "<div class=\"panel\"><h3>jerboa</h3><p>Core language, runtime, standard library, native Rust backend, package manager, docs, and tests.</p><a href=\"https://git.sr.ht/~lisp/jerboa\">git.sr.ht/~lisp/jerboa</a></div>"
+      "<div class=\"panel\"><h3>jerboa-sinatra</h3><p>Sinatra-style web framework and this site entrypoint.</p><a href=\"https://git.sr.ht/~lisp/jerboa-sinatra\">git.sr.ht/~lisp/jerboa-sinatra</a></div>"
+      "<div class=\"panel\"><h3>jerboa-dns</h3><p>DNS service work with a narrow WASM-sandboxed parsing model.</p><a href=\"https://git.sr.ht/~lisp/jerboa-dns\">git.sr.ht/~lisp/jerboa-dns</a></div>"
+      "<div class=\"panel\"><h3>jerboa-shell</h3><p>Jerboa shell and systems tooling.</p><a href=\"https://git.sr.ht/~lisp/jerboa-shell\">git.sr.ht/~lisp/jerboa-shell</a></div>"
+      "<div class=\"panel\"><h3>jerboa-code</h3><p>Editor and coding-agent oriented tooling.</p><a href=\"https://git.sr.ht/~lisp/jerboa-code\">git.sr.ht/~lisp/jerboa-code</a></div>"
+      "<div class=\"panel\"><h3>jerboa-emacs</h3><p>Editor integration and language workflow support.</p><a href=\"https://git.sr.ht/~lisp/jerboa-emacs\">git.sr.ht/~lisp/jerboa-emacs</a></div>"
+      "</div></section>")))
+
+(define security-page
+  (layout
+    "Security"
+    "security"
+    (string-append
+      "<section class=\"content\"><h1>Security Model</h1>"
+      "<p class=\"lead\">Jerboa narrows risky boundaries, favors Rust for protocol and crypto edges, and ships explicit OS confinement modules.</p>"
+      "<div class=\"columns\"><div class=\"panel\"><h3>HTTPSD</h3><ul><li>rustls TLS</li><li>Rust request parsing</li><li>duplicate Content-Length rejected</li><li>Transfer-Encoding rejected until supported</li><li>mTLS through client-ca:</li></ul></div>"
+      "<div class=\"panel\"><h3>Native Boundary</h3><p>Crypto, TLS, parser, compression, regex, packet, and selected OS features converge in libjerboa_native.</p></div>"
+      "<div class=\"panel\"><h3>Operating System Controls</h3><p>Landlock, seccomp, Capsicum, seatbelt, secure memory, audit, taint, and capability modules are available in-tree.</p></div></div>"
+      "<h2>About WASM</h2><p>Jerboa uses WASM when the security boundary is narrow enough to justify it, as in DNS parsing. HTTPSD starts with Rust parsing and strict request policy; a WASM parser can be added later if benchmarks and threat modeling justify the extra runtime boundary.</p>"
+      "</section>")))
 
 (before
-  (secure-headers!))
+  (secure-headers! site-security-headers))
+
+(GET "/assets/site.css"
+  (content-type! "text/css; charset=utf-8")
+  (cache-control! "public, max-age=3600")
+  site-css)
 
 (GET "/"
   (content-type! "text/html; charset=utf-8")
   home-page)
 
+(GET "/guide"
+  (content-type! "text/html; charset=utf-8")
+  guide-page)
+
+(GET "/guide/"
+  (content-type! "text/html; charset=utf-8")
+  guide-page)
+
+(GET "/docs"
+  (content-type! "text/html; charset=utf-8")
+  docs-page)
+
+(GET "/docs/"
+  (content-type! "text/html; charset=utf-8")
+  docs-page)
+
+(GET "/packages"
+  (content-type! "text/html; charset=utf-8")
+  packages-page)
+
+(GET "/packages/"
+  (content-type! "text/html; charset=utf-8")
+  packages-page)
+
+(GET "/repos"
+  (content-type! "text/html; charset=utf-8")
+  repos-page)
+
+(GET "/repos/"
+  (content-type! "text/html; charset=utf-8")
+  repos-page)
+
+(GET "/security"
+  (content-type! "text/html; charset=utf-8")
+  security-page)
+
+(GET "/security/"
+  (content-type! "text/html; charset=utf-8")
+  security-page)
+
 (GET "/healthz"
   (content-type! "text/plain; charset=utf-8")
   "ok\n")
 
 (not-found
   (status! 404)
-  (content-type! "text/plain; charset=utf-8")
-  "not found\n")
+  (content-type! "text/html; charset=utf-8")
+  (layout "Not Found" "" "<section class=\"content\"><h1>Not Found</h1><p class=\"lead\">That Jerboa page is not here.</p><p><a href=\"/\">Return home</a></p></section>"))
 
 (define (env-number name default)
   (let ((value (getenv name)))
diff --git a/sinatra/handler-test.ss b/sinatra/handler-test.ss
index 1782a07..5ac7514 100644
--- a/sinatra/handler-test.ss
+++ b/sinatra/handler-test.ss
@@ -2,6 +2,7 @@
         (std text json)
         (std net request)
         (std net httpd)
+        (prefix (std net thread-httpd) th:)
         (std format)
         (sinatra app)
         (sinatra handler)
@@ -98,6 +99,17 @@
               (check (request-status resp) => 200)
               (check (request-text resp) => "ok"))))))
 
+    (test-case "force-headers applies to final response"
+      (let ((app (make-sinatra-app)))
+        (app-setting-set! app "force-headers"
+          (list (cons "Content-Security-Policy" "default-src 'self'")))
+        (sinatra-get app "/" (lambda () "ok"))
+        (let* ((handler (sinatra-handler app))
+               (resp (handler (th:make-request "GET" "/" "HTTP/1.1" '() ""))))
+          (check (th:response-status resp) => 200)
+          (check (cdr (assoc "Content-Security-Policy" (th:response-headers resp)))
+                 => "default-src 'self'"))))
+
     (test-case "multiple routes first match wins"
       (let ((app (make-sinatra-app)))
         (sinatra-get app "/test" (lambda () "first"))
diff --git a/sinatra/handler.ss b/sinatra/handler.ss
index 6d006ff..06f371f 100644
--- a/sinatra/handler.ss
+++ b/sinatra/handler.ss
@@ -84,9 +84,19 @@
                          #f)))))))
 
           ;; Return final response unless a direct static response was produced.
+          (apply-forced-headers! app sres)
           (or direct-response
               (sinatra-response-write! sres #f)))))))
 
+(def (apply-forced-headers! app sres)
+  (let ((forced (app-setting app "force-headers")))
+    (when forced
+      (for-each
+        (lambda (hdr)
+          (when (pair? hdr)
+            (sinatra-response-header-set! sres (car hdr) (cdr hdr))))
+        forced))))
+
 ;; Try all routes in order, return the first matching handler's result or #f.
 (def (try-routes app method path)
   (let loop ((routes (app-routes app)))
diff --git a/test-runner.ss b/test-runner.ss
index b2663ef..44048d0 100644
--- a/test-runner.ss
+++ b/test-runner.ss
@@ -1,7 +1,4 @@
-#!chezscheme
-
-(import (chezscheme)
-        (std test)
+(import (std test)
         (sinatra route-test)
         (sinatra context-test)
         (sinatra helpers-test)