diff --git a/Makefile b/Makefile
index 2b107ce..50a12fe 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ run:
./hugo serve
build:
- ./hugo build --gc --minify && cp -r public/* docs && echo "copied to docs/. Done."
+ ./hugo build --gc --minify && cp -r public/* docs && git add docs/* && echo "copied to docs/. Done."
publish:
git commit -m "publish" && git push
diff --git a/content/_index.md b/content/_index.md
index d8b5163..c22fd65 100644
--- a/content/_index.md
+++ b/content/_index.md
@@ -55,7 +55,7 @@ Cookbook](https://lispcookbook.github.io/cl-cookbook/).
## Contact
-We are @vindarel on [Lisp's Discord server](https://discord.gg/hhk46CE) and Mastodon.
+We are @vindarel on [Lisp's Discord server](https://discord.gg/hhk46CE) and [Mastodon](https://framapiaf.org/@vindarel).
diff --git a/content/building-blocks/PUT.md b/content/building-blocks/PUT.md
new file mode 100644
index 0000000..36b0b01
--- /dev/null
+++ b/content/building-blocks/PUT.md
@@ -0,0 +1,18 @@
++++
+title = "PUT and request parameters"
+weight = 145
++++
+
+To access the body parameters of a PUT request, one must add `:PUT` to
+`hunchentoot:*methods-for-post-parameters*`, which defaults to only
+`(:POST)`:
+
+```lisp
+(push :put hunchentoot:*methods-for-post-parameters*)
+```
+
+This parameter:
+
+> is a list of the request method types (as keywords) for which Hunchentoot will try to compute POST-PARAMETERS.
+
+No such setting is required with Lack and Ningle.
diff --git a/content/building-blocks/database.md b/content/building-blocks/database.md
index 8cd4376..ce7b80d 100644
--- a/content/building-blocks/database.md
+++ b/content/building-blocks/database.md
@@ -232,7 +232,7 @@ Once we edit the table definition (aka the class definition), Mito
will (by default) automatically migrate it.
There is much more to say, but we refer you to Mito's good
-documentation and to the Cookbook.
+documentation and to the Cookbook (links below).
## How to integrate the databases into the web frameworks
diff --git a/content/building-blocks/flash-messages.lisp b/content/building-blocks/flash-messages.lisp
new file mode 100644
index 0000000..1945bc2
--- /dev/null
+++ b/content/building-blocks/flash-messages.lisp
@@ -0,0 +1,68 @@
+
+;;;
+;;; Demo of flash messages:
+;;; anywhere in a route, easily add flash messages to the session.
+;;; They are rendered at the next rendering of a template.
+;;; They are removed from the session once rendered.
+;;;
+
+;; inspired by https://github.com/rudolfochrist/booker/blob/main/app/controllers.lisp
+
+(uiop:add-package-local-nickname :ht :hunchentoot)
+
+(djula:add-template-directory ".")
+
+(defparameter *flash-template* (djula:compile-template* "flash-template.html"))
+
+(defparameter *port* 9876)
+
+(defvar *server* nil "Our Hunchentoot acceptor")
+
+(defun flash (type message)
+ "Add a flash message in the session.
+
+ TYPE: can be anything as you do what you want with it in the template.
+ Here, it is a string that represents the Bulma CSS class for notifications: is-primary, is-warning etc.
+ MESSAGE: string"
+ (let* ((session (ht:start-session))
+ (flash (ht:session-value :flash session)))
+ (setf (ht:session-value :flash session)
+ ;; With a cons, REST returns 1 element
+ ;; (when with a list, REST returns a list)
+ (cons (cons type message) flash))))
+
+;;; delete flash after it is used.
+(defmethod ht:handle-request :after (acceptor request)
+ (ht:delete-session-value :flash))
+
+#+another-solution
+(defun render (template &rest args)
+ (apply
+ #'djula:render-template* template nil
+ (list*
+ :flashes (or (ht:session-value :flash)
+ (list (cons "is-primary" "No more flash messages were found in the session. This is a default notification.")))
+ args)))
+
+
+(easy-routes:defroute flash-route ("/flash/" :method :get) ()
+ #-another-solution
+ (djula:render-template* *flash-template* nil
+ :flashes (or (ht:session-value :flash)
+ (list (cons "is-primary" "No more flash messages were found in the session. This is a default notification."))))
+ #+another-solution
+ (render *flash-template*)
+ )
+
+(easy-routes:defroute flash-redirect-route ("/tryflash/") ()
+ (flash "is-warning" "This is a warning message held in the session. It should appear only once: reload this page and you won't see the flash message again.")
+ (ht:redirect "/flash/"))
+
+(defun start (&key (port *port*))
+ (format t "~&Starting the web server on port ~a~&" port)
+ (force-output)
+ (setf *server* (make-instance 'easy-routes:easy-routes-acceptor :port port))
+ (ht:start *server*))
+
+(defun stop ()
+ (ht:stop *server*))
diff --git a/content/building-blocks/flash-messages.md b/content/building-blocks/flash-messages.md
new file mode 100644
index 0000000..824ba85
--- /dev/null
+++ b/content/building-blocks/flash-messages.md
@@ -0,0 +1,324 @@
++++
+title = "Flash messages"
+weight = 36
++++
+
+Flash messages are temporary messages you want to show to your
+users. They should be displayed once, and only once: on a subsequent
+page load, they don't appear anymore.
+
+
+
+They should specially work across route redirects. So, they are
+typically created in the web session.
+
+Handling them involves those steps:
+
+- create a message in the session
+ - have a quick and easy function to do this
+- give them as arguments to the template when rendering it
+- have some HTML to display them in the templates
+- remove the flash messages from the session.
+
+## Getting started
+
+If you didn't follow the tutorial, quickload those libraries:
+
+```lisp
+(ql:quickload '("hunchentoot" "djula" "easy-routes"))
+```
+
+We also introduce a local nickname, to shorten the use of `hunchentoot` to `ht`:
+
+```lisp
+(uiop:add-package-local-nickname :ht :hunchentoot)
+```
+
+Add this in your .lisp file if you didn't already, they
+are typical for our web demos:
+
+~~~lisp
+(defparameter *port* 9876)
+(defvar *server* nil "Our Hunchentoot acceptor")
+
+(defun start (&key (port *port*))
+ (format t "~&Starting the web server on port ~a~&" port)
+ (force-output)
+ (setf *server* (make-instance 'easy-routes:easy-routes-acceptor :port port))
+ (ht:start *server*))
+
+(defun stop ()
+ (ht:stop *server*))
+~~~
+
+
+## Create flash messages in the session
+
+This is our core function to quickly pile up a flash message to the web session.
+
+The important bits are:
+
+- we ensure to create a web session with `ht:start-session`.
+- the `:flash` session object stores *a list* of flash messages.
+- we decided that a flash messages holds those properties:
+ - its type (string)
+ - its message (string)
+
+
+~~~lisp
+(defun flash (type message)
+ "Add a flash message in the session.
+
+ TYPE: can be anything as you do what you want with it in the template.
+ Here, it is a string that represents the Bulma CSS class for notifications: is-primary, is-warning etc.
+ MESSAGE: string"
+ (let* ((session (ht:start-session)) ;; <---- ensure we started a web session
+ (flash (ht:session-value :flash session)))
+ (setf (ht:session-value :flash session)
+ ;; With a cons, REST returns 1 element
+ ;; (when with a list, REST returns a list)
+ (cons (cons type message) flash))))
+~~~
+
+Now, inside any route, we can call this function to add a flash message to the session:
+
+```lisp
+(flash "warning" "You are liking Lisp")
+```
+
+It's easy, it's handy, mission solved. Next.
+
+## Delete flash messages when they are rendered
+
+For this, we use Hunchentoot's life cycle and CLOS-orientation:
+
+```lisp
+;; delete flash after it is used.
+;; thanks to https://github.com/rudolfochrist/booker/blob/main/app/controllers.lisp for the tip.
+(defmethod ht:handle-request :after (acceptor request)
+ (ht:delete-session-value :flash))
+```
+
+which means: after we have handled a request, delete the
+`:flash` object from the session.
+
+{{% notice warning %}}
+
+If your application sends API requests in JavaScript, they can delete flash messages without you noticing. Read more below.
+
+An external API request (from the command line for example) is not
+concerned, as it doesn't carry Hunchentoot session cookies.
+
+{{% /notice %}}
+
+
+## Render flash messages in templates
+
+### Set up Djula templates
+
+Create a new `flash-template.html` file.
+
+~~~lisp
+(djula:add-template-directory "./")
+(defparameter *flash-template* (djula:compile-template* "flash-template.html"))
+~~~
+
+{{% notice info %}}
+
+You might need to change the current working
+directory of your Lisp REPL to the directory of your .lisp file, so
+that `djula:compile-template*` can find your template. Use the short
+command `,cd` or `(swank:set-default-directory "/home/you/path/to/app/")`.
+See also `asdf:system-relative-pathname system directory`.
+
+{{% /notice %}}
+
+### HTML template
+
+This is our template. We use [Bulma
+CSS](https://bulma.io/documentation/elements/notification/) to pimp it
+up and to use its notification blocks.
+
+```html
+
+
+
+
+
+
+
+ WALK - flash messages
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Flash messages.
+
+
Click /tryflash/ to access an URL that creates a flash message and redirects you here.
+
+ {% for flash in flashes %}
+
+
+
+ {{ flash.rest }}
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+```
+
+Look at
+
+```
+{% for flash in flashes %}
+```
+
+where we render our flash messages.
+
+Djula allows us to write `{{ flash.first }}` and `{{ flash.rest }}` to
+call the Lisp functions on those objects.
+
+We must now create a route that renders our template.
+
+
+## Routes
+
+The `/flash/` URL is the demo endpoint:
+
+```lisp
+(easy-routes:defroute flash-route ("/flash/" :method :get) ()
+ (djula:render-template* *flash-template* nil
+ :flashes (or (ht:session-value :flash)
+ (list (cons "is-primary" "No more flash messages were found in the session. This is a default notification.")))))
+```
+
+It is here that we pass the flash messages as a parameter to the template.
+
+In your application, you must add this parameter in all the existing
+routes. To make this easier, you can:
+- use Djula's [default template variables](https://mmontone.github.io/djula/djula/Variables.html#Default-template-variables), but our parameters are to be found dynamically in the current request's session, so we can instead
+- create a "render" function of ours that calls `djula:render-template*` and always adds the `:flash` parameter. Use `apply`:
+
+~~~lisp
+(defun render (template &rest args)
+ (apply
+ #'djula:render-template* template nil
+ ;; All arguments must be in a list.
+ (list*
+ :flashes (or (ht:session-value :flash)
+ (list (cons "is-primary" "No more flash messages were found in the session. This is a default notification.")))
+ args)))
+~~~
+
+Finally, this is the route that creates a flash message:
+
+```lisp
+(easy-routes:defroute flash-redirect-route ("/tryflash/") ()
+ (flash "is-warning" "This is a warning message held in the session. It should appear only once: reload this page and you won't see the flash message again.")
+ (ht:redirect "/flash/"))
+```
+
+## Demo
+
+Start the app with `(start)` if you didn't start Hunchentoot already,
+otherwise it was enough to compile the new routes.
+
+You should see a default notification. Click the "/tryflash/" URL and
+you'll see a flash message, that is deleted after use.
+
+Refresh the page, and you won't see the flash message again.
+
+- full code: https://github.com/web-apps-in-lisp/web-apps-in-lisp.github.io/blob/master/content/building-blocks/flash-messages.lisp
+
+## Discussing: Flash messages and API calls
+
+Our `:after` method on the Hunchentoot request lifecycle will delete
+flash messages for any request that carries the session cookies. If
+your application makes API calls, you can use the Fetch method with
+the `{credentials: "omit"}` parameter:
+
+~~~javascript
+fetch("http://localhost:9876/api/", {
+ credentials: "omit"
+})
+~~~
+
+Otherwise, don't use this `:after` method and delete flash messages
+explicitely in your non-API routes.
+
+We could use a macro shortcut for this:
+
+
+~~~lisp
+(defmacro with-flash-messages ((messages) &body body)
+ `(let ((,messages (ht:session-value :flash)))
+ (prog1
+ (progn
+ ,@body)
+ (ht:delete-session-value :flash))))
+~~~
+
+Use it like this:
+
+~~~lisp
+(easy-routes:defroute flash-route ("/flash/" :method :get) ()
+ (with-flash-messages (messages)
+ (djula:render-template* *flash-template* nil
+ :flashes (or messages
+ (list (cons "is-primary" "No more flash messages were found in the session. This is a default notification."))))))
+~~~
+
+We want our macro to return the result of `djula:render-template*`,
+and *not* the result of `ht:delete-session-value`, that is nil, hence
+the "prog1/ progn" dance.
diff --git a/content/building-blocks/flash-messages.png b/content/building-blocks/flash-messages.png
new file mode 100644
index 0000000..e5de581
Binary files /dev/null and b/content/building-blocks/flash-messages.png differ
diff --git a/content/building-blocks/flash-template.html b/content/building-blocks/flash-template.html
new file mode 100644
index 0000000..dac19d5
--- /dev/null
+++ b/content/building-blocks/flash-template.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+ WALK - flash messages
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Flash messages.
+
+
Click /tryflash/ to access an URL that creates a flash message and redirects you here.
+
+ {% for flash in flashes %}
+
+
+
+ {{ flash.rest }}
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
diff --git a/content/building-blocks/templates.md b/content/building-blocks/templates.md
index 69e8ee8..6abbf6f 100644
--- a/content/building-blocks/templates.md
+++ b/content/building-blocks/templates.md
@@ -29,6 +29,25 @@ and then we can declare and compile the ones we use, for example::
(defparameter +products.html+ (djula:compile-template* "products.html"))
~~~
+{{% notice info %}}
+
+If you get an error message when calling `add-template-directory` like this
+
+```
+The value
+ #P"/home/user/…/project/src/templates/index.html"
+
+is not of type
+ STRING
+from the function type declaration.
+ [Condition of type TYPE-ERROR]
+```
+
+then update your Quicklisp dist or clone Djula in `~/quicklisp/local-projects/`.
+
+{{% /notice %}}
+
+
A Djula template looks like this:
```html
diff --git a/content/building-blocks/user-log-in.md b/content/building-blocks/user-log-in.md
index eed9dc2..b2924bc 100644
--- a/content/building-blocks/user-log-in.md
+++ b/content/building-blocks/user-log-in.md
@@ -326,6 +326,9 @@ Remarks:
## Full code
```lisp
+(defpackage :myproject
+ (:use :cl))
+
(in-package :myproject)
;; User-facing paramaters.
@@ -433,8 +436,7 @@ Remarks:
;; Server.
(defun start-server (&key (port *port*))
(format t "~&Starting the login demo on port ~a~&" port)
- (unless *server*
- (setf *server* (make-instance 'hunchentoot:easy-acceptor :port port)))
+ (setf *server* (make-instance 'easy-routes:easy-routes-acceptor :port port))
(hunchentoot:start *server*))
(defun stop-server ()
diff --git a/content/building-blocks/users-and-passwords.md b/content/building-blocks/users-and-passwords.md
index fd657a6..9ab5e75 100644
--- a/content/building-blocks/users-and-passwords.md
+++ b/content/building-blocks/users-and-passwords.md
@@ -4,15 +4,13 @@ weight = 130
+++
We don't know of a Common Lisp framework that will create users and
-roles for you and protect your routes. You'll have to either write
-some Lisp, either use an external tool (such as Keycloak) that will
-provide all the user management.
+roles for you and protect your routes all at the same time. We have
+building blocks but you'll have to write some glue Lisp code.
-{{% notice info %}}
+You can also turn to external tools (such as [Keycloak](https://www.keycloak.org/) or [Tesseral](https://tesseral.com/)) that will
+provide all the industrial-grade user management.
-Stay tuned! We are on to something.
-
-{{% /notice %}}
+If you like the Mito ORM, look at [mito-auth](https://github.com/fukamachi/mito-auth/) and [mito-email-auth](https://github.com/40ants/mito-email-auth).
## Creating users
@@ -133,3 +131,12 @@ library.
```
*Credit: `/u/arvid` on [/r/learnlisp](https://www.reddit.com/r/learnlisp/comments/begcf9/can_someone_give_me_an_eli5_on_hiw_to_encrypt_and/)*.
+
+## See also
+
+* [cl-authentic](https://github.com/charJe/cl-authentic) - Password management for Common Lisp (web) applications. [LLGPL][8].
+ - safe password storage: cleartext-free, using your choice of hash algorithm through ironclad, storage in an SQL database,
+ - password reset mechanism with one-time tokens (suitable for mailing to users for confirmation),
+ - user creation optionally with confirmation tokens (suitable for mailing to users),
+
+and more on the awesome-cl list.
diff --git a/content/tutorial/build.lisp b/content/tutorial/build.lisp
index 68c1a00..b8ba969 100644
--- a/content/tutorial/build.lisp
+++ b/content/tutorial/build.lisp
@@ -2,7 +2,14 @@
(ql:quickload "myproject")
-(sb-ext:save-lisp-and-die "myproject"
- :executable t
- :toplevel #'myproject::main
- :compression 9)
+(setf uiop:*image-entry-point* #'myproject::main)
+
+(uiop:dump-image "myproject" :executable t :compression 9)
+
+#|
+;; The same as:
+
+(sb-ext:save-lisp-and-die "myproject" :executable t :compression 9
+ :toplevel #'myproject::main)
+
+|#
diff --git a/content/tutorial/first-build.md b/content/tutorial/first-build.md
index 288de51..110d3bd 100644
--- a/content/tutorial/first-build.md
+++ b/content/tutorial/first-build.md
@@ -341,9 +341,12 @@ Building binaries with SBCL is done with the function
`sb-ext:save-lisp-and-die` (it lives in the `sb-ext` SBCL module, that
is available by default).
-Other implementations don't define the exact same function, that's why
-we need a compatibility layer, which is provided by ASDF. We show this
-method in the Cookbook and later in this guide.
+Other implementations don't define the exact same function, for
+instance on Clozure CL the function is `ccl:save-application`. That's
+why we'll want a compatibility layer to write a portable script across
+implementations. It is as always provided by ASDF with
+`uiop:dump-image` and also with a system declaration in the .asd
+files.
SBCL binaries are portable from and to the same operating system:
build on GNU/Linux, run on GNU/Linux. Or build on a CI system on the 3
diff --git a/docs/404.html b/docs/404.html
index 2810c92..272602c 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -1,2 +1,2 @@
404 Page not found :: Web Apps in Lisp: Know-how
-
44
Not found
Whoops. Looks like this page doesn't exist ¯\_(ツ)_/¯.