From a1176f2df1f1eb8de4aaf1c64b9dcb7ee3513f25 Mon Sep 17 00:00:00 2001 From: Automatic website updater <> Date: Mon, 13 Apr 2026 11:24:04 +0100 Subject: [PATCH 02/12] Automatic website update --- index.html | 780 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 780 insertions(+) create mode 100644 index.html diff --git a/index.html b/index.html new file mode 100644 index 0000000..4c8e568 --- /dev/null +++ b/index.html @@ -0,0 +1,780 @@ + + + +
+ +Safsaf is a web framework for Guile Scheme, built on +Guile Fibers using the +Guile Knots web +server. +
+ + + + +This chapter explains how the pieces of Safsaf fit together. Each +section covers one concept with a short code example. For the full +list of parameters and options, see API. +
+ + +A Safsaf application is a list of routes passed to run-safsaf.
+Each route binds an HTTP method and URL pattern to a handler procedure.
+The handler receives a Guile <request> and a body port, and
+returns two values: a response and a body.
+
(use-modules (safsaf)
+ (safsaf router)
+ (safsaf response-helpers))
+
+(define routes
+ (list
+ (route 'GET '() index-page
+ #:name 'index)
+ (route 'GET '("hello" name) hello-page)
+ (route '* '* (lambda (request body-port)
+ (not-found-response)))))
+
+(define (index-page request body-port)
+ (html-response '(h1 "Welcome")))
+
+(define (hello-page request body-port)
+ (let ((name (assoc-ref (current-route-params) 'name)))
+ (text-response (string-append "Hello, " name "!"))))
+
+(run-safsaf routes #:port 8080)
+The last route should be a catch-all ('* method, '*
+pattern) so that every request is handled. run-safsaf sets up
+a Fibers scheduler, starts the HTTP server, and blocks until Ctrl-C.
+
+Next: Handler Wrappers, Previous: Getting Started, Up: Guidance [Contents][Index]
+Route patterns are lists of segments. A string matches literally, a
+symbol captures that segment into current-route-params, and a
+two-element list (predicate name) captures only when
+predicate returns true. A dotted tail captures the remaining
+path.
+
;; Literal: /about
+(route 'GET '("about") about-handler)
+
+;; Capture: /users/:id
+(route 'GET '("users" id) show-user)
+
+;; Predicate: /posts/:id where id is numeric
+(route 'GET '("posts" (,string->number id)) show-post)
+
+;; Wildcard (rest): /files/* — captures remaining segments
+(route 'GET '("files" . path) serve-file)
+route-group nests routes under a shared prefix:
+
(route-group '("api" "v1")
+ (route 'GET '("users") api-list-users)
+ (route 'GET '("users" id) api-show-user))
+This matches /api/v1/users and /api/v1/users/:id.
+
Give a route a #:name and use path-for to generate its
+URL, so paths are never hard-coded. The first argument is always a
+route group:
+
(define my-routes
+ (route-group '()
+ (route 'GET '("posts" id) show-post #:name 'show-post)))
+
+(define all-routes
+ (list my-routes
+ (route '* '* (lambda (r b) (not-found-response)))))
+
+;; In a handler or template:
+(path-for my-routes 'show-post '((id . "42")))
+;; => "/posts/42"
+path-for also accepts #:query and #:fragment
+keyword arguments.
+
A handler wrapper is a procedure that takes a handler and returns a
+new handler. It can transform the request on the way in and the
+response on the way out. Apply wrappers to a route tree with
+wrap-routes.
+
(wrap-routes routes + (make-exceptions-handler-wrapper #:dev? #t) + logging-handler-wrapper) +
When multiple wrappers are given, the first wraps outermost — it runs +first on the request and last on the response. In the example above, +exceptions catches errors from the logging wrapper and the inner +handler. +
+Apply wrappers to part of the route tree by wrapping a group +separately: +
+(define api-routes
+ (wrap-routes
+ (route-group '("api")
+ (route 'GET '("items") api-list-items))
+ cors-handler-wrapper))
+
+(define all-routes
+ (wrap-routes
+ (list api-routes
+ (route 'GET '() index-page)
+ (route '* '* (lambda (r b) (not-found-response))))
+ logging-handler-wrapper))
+Here CORS headers are added only to /api/* routes, while
+logging applies to everything.
+
security-headers-handler-wrapper appends its headers to
+the response rather than replacing existing ones. If a handler sets
+X-Frame-Options itself, both values will appear in the response.
+To avoid duplication, either omit the header from the wrapper (pass
+#:frame-options #f) or do not set it in the handler.
+
make-max-body-size-handler-wrapper checks the
+Content-Length header and rejects requests that exceed the
+limit with a 413 response. However, it does not limit chunked
+transfer-encoded requests that lack Content-Length. For
+untrusted networks, use a reverse proxy (e.g. Nginx’s
+client_max_body_size) to enforce size limits at the transport
+level.
+
+Next: Request Parsing, Previous: Handler Wrappers, Up: Guidance [Contents][Index]
+Safsaf provides helpers that return (values response body)
+directly:
+
;; HTML — streams an SXML tree
+(html-response '(div (h1 "Hello") (p "world")))
+
+;; JSON — takes a JSON string
+(json-response (scm->json-string '(("ok" . #t))))
+
+;; Plain text
+(text-response "pong")
+
+;; Redirect (default 303 See Other)
+(redirect-response "/login")
+(redirect-response "/new-item" #:code 302)
+
+;; Error responses
+(not-found-response)
+(bad-request-response "Missing field")
+(forbidden-response)
+html-response, json-response, text-response, and
+redirect-response accept #:code and #:headers for
+overrides. The error helpers (not-found-response, etc.) accept
+#:headers but have a fixed status code.
+
For content negotiation, use negotiate-content-type:
+
(define (show-item request body-port) + (let ((item (fetch-item (assoc-ref (current-route-params) 'id)))) + (case (negotiate-content-type request + '(text/html application/json)) + ((application/json) + (json-response (scm->json-string (item->alist item)))) + (else + (html-response `(div (h1 ,(item-title item)))))))) +
+Next: Parameter Parsing, Previous: Responses, Up: Guidance [Contents][Index]
+parse-form-body reads a URL-encoded POST body and returns an
+alist of string pairs:
+
(define (handle-login request body-port) + (let* ((form (parse-form-body request body-port)) + (username (assoc-ref form "username")) + (password (assoc-ref form "password"))) + (if (valid-credentials? username password) + (redirect-response "/dashboard") + (text-response "Invalid login" #:code 401)))) +
parse-query-string extracts query parameters from the request
+URL:
+
(let ((qs (parse-query-string request))) + (assoc-ref qs "page")) ;; => "2" or #f +
For file uploads, use parse-multipart-body:
+
(let* ((parts (parse-multipart-body request body-port)) + (form (multipart-text-fields parts)) + (file (parts-ref parts "avatar"))) + ;; form is an alist of text fields + ;; file is a <part> record — read its body with (part-body file) + ...) +
Read cookies with request-cookie-ref or
+request-cookies. Set them via response headers with
+set-cookie-header and delete-cookie-header:
+
(request-cookie-ref request "theme") ;; => "dark" or #f + +(text-response "ok" + #:headers (list (set-cookie-header "theme" "dark" + #:path "/" + #:http-only #t))) +
+Next: Sessions, Previous: Request Parsing, Up: Guidance [Contents][Index]
+parse-params validates and transforms raw form or query data
+according to a declarative spec. Each spec entry names a parameter,
+a processor (a procedure that converts a string or returns an
+<invalid-param>), and options like #:required or
+#:default.
+
(let ((params (parse-params + `((page ,as-integer #:default 1) + (per-page ,as-integer #:default 20) + (q ,as-string)) + (parse-query-string request)))) + (assq-ref params 'page)) ;; => 1 (integer, not string) +
Built-in processors: as-string, as-integer,
+as-number, as-checkbox, as-one-of,
+as-matching, as-predicate.
+
For POST forms, use parse-form-params instead — it
+automatically checks the CSRF token (from
+csrf-handler-wrapper) before parsing:
+
(let* ((form (parse-form-body request body-port)) + (params (parse-form-params + `((title ,as-string #:required) + (body ,as-string #:required)) + form))) + (if (any-invalid-params? params) + ;; Re-render the form with errors + (render-form (field-errors params 'title) + (field-errors params 'body)) + ;; Proceed + (create-item! (assq-ref params 'title) + (assq-ref params 'body)))) +
any-invalid-params? returns #t if any value failed
+validation. field-errors returns a list of error message
+strings for a given field, suitable for rendering next to form inputs.
+
+Next: Templating, Previous: Parameter Parsing, Up: Guidance [Contents][Index]
+Sessions use HMAC-signed cookies via (webutils sessions).
+Set up a session config and apply the wrapper:
+
(define session-mgr + (make-session-config "my-secret-key" + #:cookie-name "my-session")) + +(define routes + (wrap-routes my-routes + (make-session-handler-wrapper session-mgr))) +
Inside a handler, (current-session) returns the session data
+(an alist) or #f if no valid session exists.
+
To set session data, include a session-set header in the
+response. To delete, use session-delete:
+
;; Set session +(redirect-response "/" + #:headers (list (session-set session-mgr + '((user-id . 42))))) + +;; Read session +(let ((user-id (and (current-session) + (assoc-ref (current-session) 'user-id)))) + ...) + +;; Delete session +(redirect-response "/" + #:headers (list (session-delete session-mgr))) +
+Next: Static Files, Previous: Sessions, Up: Guidance [Contents][Index]
+write-shtml-as-html/streaming works like htmlprag’s
+write-shtml-as-html, but any procedure in the SHTML tree is
+called as (proc port) and can write dynamic content directly.
+
streaming-html-response wraps this into a response: give it an
+SHTML tree (with optional procedure slots) and it returns
+(values response body) ready for a handler.
+
(define (base-layout title content-proc) + `(*TOP* + (*DECL* DOCTYPE html) + (html + (head (title ,title)) + (body + (nav (a (@ (href "/")) "Home")) + (main ,content-proc) + (footer (p "Footer")))))) +
The layout is plain SHTML with a procedure in the content-proc
+position. Use streaming-html-response to send it:
+
(define (index-page request body-port) + (streaming-html-response + (base-layout "Home" + (lambda (port) + (write-shtml-as-html + `(div (h1 "Welcome") + (p "Content goes here.")) + port))))) +
You can also call write-shtml-as-html/streaming directly when
+you need to write SHTML with procedure slots to an arbitrary port.
+
+Previous: Templating, Up: Guidance [Contents][Index]
+make-static-handler returns a handler that serves files from a
+directory. Pair it with a wildcard route:
+
(route-group '("static")
+ (route 'GET '(. path)
+ (make-static-handler "./public"
+ #:cache-control '((max-age . 3600)))))
+This serves /static/css/style.css from
+./public/css/style.css. The handler supports
+If-Modified-Since for 304 responses.
+
+Next: Version History, Previous: Guidance, Up: Overview [Contents][Index]
+The following is the list of modules provided by this library. +
+ + +Return a 405 Method Not Allowed response with an Allow header listing +ALLOWED-METHODS. +
+Start a Safsaf web server. +
+ROUTES is a list of routes and route-groups (as returned by component +constructors). The last route must be a catch-all so that every request +is handled. +
+HEAD requests are handled automatically: when no explicit HEAD route +matches, the matching GET handler runs and its response body is +discarded. Explicit HEAD routes always take precedence. +
+When METHOD-NOT-ALLOWED? is #t (the default), requests that match a +route’s path but not its method receive a 405 response with an Allow +header. METHOD-NOT-ALLOWED-HANDLER is a procedure (request +allowed-methods) -> (values response body) that produces the 405 +response; the default returns plain text. +
+When called outside a Fibers scheduler, sets up a scheduler, starts the +HTTP server, and blocks until Ctrl-C. When called inside an existing +scheduler (e.g. within run-fibers), just starts the HTTP server and +returns immediately — the caller manages the lifecycle. +
++Next: Copying Information, Previous: API, Up: Overview [Contents][Index]
++Next: Concept Index, Previous: Version History, Up: Overview [Contents][Index]
+Copyright © 2026 Christopher Baines <mail@cbaines.net> +
+This library is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation; either version 3 of the +License, or (at your option) any later version. +
+ + + ++Next: Data Type Index, Previous: Copying Information, Up: Overview [Contents][Index]
++Next: Procedure Index, Previous: Concept Index, Up: Overview [Contents][Index]
++Next: Variable Index, Previous: Data Type Index, Up: Overview [Contents][Index]
+| Jump to: | D + +R + + |
|---|
| Index Entry | Section | ||
|---|---|---|---|
| | |||
| D | |||
default-method-not-allowed-handler: | safsaf | ||
| | |||
| R | |||
run-safsaf: | safsaf | ||
| | |||
| Jump to: | D + +R + + |
|---|
+Previous: Procedure Index, Up: Overview [Contents][Index]
+
+
+
hello
")))) + + (test "single proc slot" + (let ((out (render + `(div ,(lambda (port) (display "dynamic" port)))))) + (is (string-contains out "dynamic")))) + + (test "multiple slots in order" + (let ((out (render + `(div ,(lambda (port) (display "AAA" port)) + ,(lambda (port) (display "BBB" port)))))) + (let ((a (string-contains out "AAA")) + (b (string-contains out "BBB"))) + (is a) + (is b) + (is (< a b))))) + + (test "static content between slots preserved" + (let ((out (render + `(div ,(lambda (port) (display "X" port)) + (hr) + ,(lambda (port) (display "Y" port)))))) + (is (string-contains out "