From 25316561b6912ff38e77c48fe4eaf86db816e3df Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 13 Mar 2025 13:40:35 +0100 Subject: [PATCH 01/20] new page: flash messages --- content/building-blocks/flash-messages.lisp | 68 +++++ content/building-blocks/flash-messages.md | 269 ++++++++++++++++++++ content/building-blocks/flash-messages.png | Bin 0 -> 30516 bytes content/building-blocks/flash-template.html | 76 ++++++ 4 files changed, 413 insertions(+) create mode 100644 content/building-blocks/flash-messages.lisp create mode 100644 content/building-blocks/flash-messages.md create mode 100644 content/building-blocks/flash-messages.png create mode 100644 content/building-blocks/flash-template.html 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..080c61f --- /dev/null +++ b/content/building-blocks/flash-messages.md @@ -0,0 +1,269 @@ ++++ +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. + +![](/building-blocks/flash-messages.png) + +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 the current request, delete the `:flash` object from the session. + +## 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. diff --git a/content/building-blocks/flash-messages.png b/content/building-blocks/flash-messages.png new file mode 100644 index 0000000000000000000000000000000000000000..e5de5814ccd6e68e35f8c16aa46a82cc761a0a94 GIT binary patch literal 30516 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 literal 0 HcmV?d00001 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 %} + +
+
+
+ + + + + + From a1ec0db647fa739f341f0c0a3a6d01022625efa5 Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 13 Mar 2025 13:40:52 +0100 Subject: [PATCH 02/20] index Mastodon link --- content/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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). From ca69f49ca2f94bb14ece076e7ba41b721311010b Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 13 Mar 2025 13:48:56 +0100 Subject: [PATCH 03/20] make build: git add docs/* --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 27cafdb84fd98100f16abce55ecbab0bee3f0e4f Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 13 Mar 2025 13:50:03 +0100 Subject: [PATCH 04/20] publish (only flash-messages) --- docs/building-blocks/flash-messages.lisp | 68 ++++++++ docs/building-blocks/flash-messages.png | Bin 0 -> 30516 bytes .../building-blocks/flash-messages/index.html | 149 ++++++++++++++++++ docs/building-blocks/flash-messages/index.xml | 4 + .../building-blocks/flash-template/index.html | 13 ++ docs/building-blocks/flash-template/index.xml | 1 + 6 files changed, 235 insertions(+) create mode 100644 docs/building-blocks/flash-messages.lisp create mode 100644 docs/building-blocks/flash-messages.png create mode 100644 docs/building-blocks/flash-messages/index.html create mode 100644 docs/building-blocks/flash-messages/index.xml create mode 100644 docs/building-blocks/flash-template/index.html create mode 100644 docs/building-blocks/flash-template/index.xml diff --git a/docs/building-blocks/flash-messages.lisp b/docs/building-blocks/flash-messages.lisp new file mode 100644 index 0000000..1945bc2 --- /dev/null +++ b/docs/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/docs/building-blocks/flash-messages.png b/docs/building-blocks/flash-messages.png new file mode 100644 index 0000000000000000000000000000000000000000..e5de5814ccd6e68e35f8c16aa46a82cc761a0a94 GIT binary patch literal 30516 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 literal 0 HcmV?d00001 diff --git a/docs/building-blocks/flash-messages/index.html b/docs/building-blocks/flash-messages/index.html new file mode 100644 index 0000000..f880e22 --- /dev/null +++ b/docs/building-blocks/flash-messages/index.html @@ -0,0 +1,149 @@ +Flash messages :: Web Apps in Lisp: Know-how +

Flash messages

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:

(ql:quickload '("hunchentoot" "djula" "easy-routes"))

We also introduce a local nickname, to shorten the use of hunchentoot to ht:

(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:

(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)
(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:

(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:

;; 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 the current request, delete the :flash object from the session.

Render flash messages in templates

Set up Djula templates

Create a new flash-template.html file.

(djula:add-template-directory "./")
+(defparameter *flash-template* (djula:compile-template* "flash-template.html"))
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.

HTML template

This is our template. We use Bulma +CSS to pimp it +up and to use its notification blocks.

<!DOCTYPE html>
+<html>
+
+    <head>
+      <meta charset="utf-8">
+      <meta http-equiv="X-UA-Compatible" content="IE=edge">
+      <meta name="viewport" content="width=device-width, initial-scale=1">
+      <title>WALK - flash messages</title>
+      <!-- Bulma Version 1-->
+      <link rel="stylesheet" href="https://unpkg.com/bulma@1.0.2/css/bulma.min.css" />
+    </head>
+
+    <body>
+      <!-- START NAV -->
+      <nav class="navbar is-white">
+        <div class="container">
+          <div class="navbar-brand">
+            <a class="navbar-item brand-text" href="#">
+              Bulma Admin
+            </a>
+            <div class="navbar-burger burger" data-target="navMenu">
+              <span></span>
+            </div>
+          </div>
+          <div id="navMenu" class="navbar-menu">
+            <div class="navbar-start">
+              <a class="navbar-item" href="#">
+                Home
+              </a>
+              <a class="navbar-item" href="#">
+                Orders
+              </a>
+            </div>
+          </div>
+        </div>
+      </nav>
+      <!-- END NAV -->
+
+      <div class="container">
+        <div class="columns">
+          <div class="column is-6">
+
+            <h3 class="title is-4"> Flash messages. </h3>
+
+            <div> Click <a href="/tryflash/">/tryflash/</a> to access an URL that creates a flash message and redirects you here.</div>
+
+            {% for flash in flashes %}
+
+            <div class="notification {{ flash.first }}">
+              <button class="delete"></button>
+            {{ flash.rest }}
+            </div>
+
+            {% endfor %}
+
+          </div>
+        </div>
+      </div>
+
+    </body>
+
+    <script>
+      // JS snippet to click the delete button of the notifications.
+      // see https://bulma.io/documentation/elements/notification/
+      document.addEventListener('DOMContentLoaded', () => {
+        (document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
+          const $notification = $delete.parentNode;
+
+          $delete.addEventListener('click', () => {
+            $notification.parentNode.removeChild($notification);
+          });
+        });
+      });
+    </script>
+
+</html>

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:

(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, 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:
(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:

(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.

\ No newline at end of file diff --git a/docs/building-blocks/flash-messages/index.xml b/docs/building-blocks/flash-messages/index.xml new file mode 100644 index 0000000..a5cce86 --- /dev/null +++ b/docs/building-blocks/flash-messages/index.xml @@ -0,0 +1,4 @@ +Flash messages :: Web Apps in Lisp: Know-howhttp://example.org/building-blocks/flash-messages/index.htmlFlash 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:Hugoen-us \ No newline at end of file diff --git a/docs/building-blocks/flash-template/index.html b/docs/building-blocks/flash-template/index.html new file mode 100644 index 0000000..04595e4 --- /dev/null +++ b/docs/building-blocks/flash-template/index.html @@ -0,0 +1,13 @@ + +

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 %}
\ No newline at end of file diff --git a/docs/building-blocks/flash-template/index.xml b/docs/building-blocks/flash-template/index.xml new file mode 100644 index 0000000..95c4ee6 --- /dev/null +++ b/docs/building-blocks/flash-template/index.xml @@ -0,0 +1 @@ +<link>http://example.org/building-blocks/flash-template/index.html</link><description><!DOCTYPE html> WALK - flash messages Bulma Admin Home Orders Flash messages. Click /tryflash/ to access an URL that creates a flash message and redirects you here. {% for flash in flashes %} {{ flash.rest }} {% endfor %}</description><generator>Hugo</generator><language>en-us</language><atom:link href="http://example.org/building-blocks/flash-template/index.xml" rel="self" type="application/rss+xml"/></channel></rss> \ No newline at end of file From 1704d74cae076029b0358199b90739c713b93ff4 Mon Sep 17 00:00:00 2001 From: vindarel <vindarel@mailz.org> Date: Thu, 13 Mar 2025 13:55:36 +0100 Subject: [PATCH 05/20] publish (no choice but everything :p ) --- docs/404.html | 2 +- .../building-binaries/index.html | 8 +-- docs/building-blocks/database/index.html | 8 +-- docs/building-blocks/deployment/index.html | 8 +-- docs/building-blocks/electron/index.html | 12 ++--- .../errors-interactivity/index.html | 16 +++--- .../building-blocks/flash-messages/index.html | 12 ++--- .../building-blocks/flash-template/index.html | 8 +-- .../form-validation/index.html | 8 +-- docs/building-blocks/headers/index.html | 8 +-- docs/building-blocks/index.html | 8 +-- docs/building-blocks/index.xml | 7 ++- .../remote-debugging/index.html | 8 +-- docs/building-blocks/routing/index.html | 8 +-- docs/building-blocks/session/index.html | 8 +-- .../simple-web-server/index.html | 8 +-- docs/building-blocks/static/index.html | 8 +-- docs/building-blocks/templates/index.html | 8 +-- docs/building-blocks/user-log-in/index.html | 12 ++--- .../users-and-passwords/index.html | 8 +-- docs/building-blocks/web-views/index.html | 12 ++--- docs/categories/index.html | 6 +-- docs/css/chroma-auto.css | 4 +- docs/css/format-print.css | 4 +- docs/css/print.css | 2 +- docs/css/swagger.css | 4 +- docs/css/theme-auto.css | 4 +- docs/css/theme.css | 2 +- docs/index.html | 15 +++--- docs/index.xml | 3 +- .../isomorphic-web-frameworks/clog/index.html | 16 +++--- docs/isomorphic-web-frameworks/index.html | 8 +-- .../weblocks/index.html | 12 ++--- docs/search/index.html | 8 +-- docs/searchindex.js | 16 ++++-- docs/see-also/index.html | 8 +-- docs/sitemap.xml | 2 +- docs/tags/index.html | 6 +-- docs/tutorial/demo-foo.lisp | 51 +++++++++++++++++++ docs/tutorial/first-bonus-css/index.html | 8 +-- docs/tutorial/first-build/index.html | 8 +-- docs/tutorial/first-form/index.html | 8 +-- docs/tutorial/first-path-parameter/index.html | 8 +-- docs/tutorial/first-route/index.html | 8 +-- docs/tutorial/first-template/index.html | 8 +-- docs/tutorial/first-url-parameters/index.html | 8 +-- docs/tutorial/getting-started/index.html | 8 +-- docs/tutorial/index.html | 8 +-- docs/tutorial/myproject.asd | 6 --- docs/tutorial/quicktry.lisp | 5 ++ docs/tutorial/src/static/test.js | 1 + 51 files changed, 251 insertions(+), 189 deletions(-) create mode 100644 docs/tutorial/demo-foo.lisp create mode 100644 docs/tutorial/quicktry.lisp create mode 100644 docs/tutorial/src/static/test.js diff --git a/docs/404.html b/docs/404.html index 2810c92..e08f188 100644 --- a/docs/404.html +++ b/docs/404.html @@ -1,2 +1,2 @@ <!doctype html><html lang=en-us dir=ltr itemscope itemtype=http://schema.org/Article><head><meta charset=utf-8><meta name=viewport content="height=device-height,width=device-width,initial-scale=1,minimum-scale=1"><meta name=generator content="Hugo 0.136.0"><meta name=generator content="Relearn 7.0.1+72a875f1db967152c77914cff4d53f8fcee0e619"><meta name=description content><meta name=author content="vindarel"><meta name=twitter:card content="summary"><meta name=twitter:title content="404 Page not found :: Web Apps in Lisp: Know-how"><meta property="og:url" content="http://example.org/404.html"><meta property="og:site_name" content="Web Apps in Lisp: Know-how"><meta property="og:title" content="404 Page not found :: Web Apps in Lisp: Know-how"><meta property="og:locale" content="en_us"><meta property="og:type" content="website"><meta itemprop=name content="404 Page not found :: Web Apps in Lisp: Know-how"><title>404 Page not found :: Web Apps in Lisp: Know-how -

44

Not found

Whoops. Looks like this page doesn't exist Β―\_(ツ)_/Β―.

Go to homepage

\ No newline at end of file +

44

Not found

Whoops. Looks like this page doesn't exist Β―\_(ツ)_/Β―.

Go to homepage

\ No newline at end of file diff --git a/docs/building-blocks/building-binaries/index.html b/docs/building-blocks/building-binaries/index.html index fe91855..71a52e7 100644 --- a/docs/building-blocks/building-binaries/index.html +++ b/docs/building-blocks/building-binaries/index.html @@ -11,7 +11,7 @@ To run our Lisp code from source, as a script, we can use the --load switch from our implementation. We must ensure: to load the project’s .asd system declaration (if any) to install the required dependencies (this demands we have installed Quicklisp previously) and to run our application’s entry point. So, the recipe to run our project from sources can look like this (you can find such a recipe in our project generator):">Running and Building :: Web Apps in Lisp: Know-how -

Running and Building

Running the application from source

Info

See the tutorial.

To run our Lisp code from source, as a script, we can use the --load +

Running and Building

Running the application from source

Info

See the tutorial.

To run our Lisp code from source, as a script, we can use the --load switch from our implementation.

We must ensure:

  • to load the project’s .asd system declaration (if any)
  • to install the required dependencies (this demands we have installed Quicklisp previously)
  • and to run our application’s entry point.

So, the recipe to run our project from sources can look like this (you can find such a recipe in our project generator):

;; run.lisp
 
 (load "myproject.asd")
@@ -40,12 +40,12 @@
 stops. That happens because the server thread is started in the background, and nothing tells the binary to wait for it. We can simply sleep (for a large-enough amount of time).

(defun main ()
   (start-app :port 9003) ;; our start-app
   ;; keep the binary busy in the foreground, for binaries and SystemD.
-  (sleep most-positive-fixnum))

If you want to learn more, see… the Cookbook: scripting, command line arguments, executables.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/database/index.html b/docs/building-blocks/database/index.html index 6605479..b956828 100644 --- a/docs/building-blocks/database/index.html +++ b/docs/building-blocks/database/index.html @@ -7,7 +7,7 @@ The #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:">Connecting to a database :: Web Apps in Lisp: Know-how -

Connecting to a database

Let’s study different use cases:

  • do you have an existing database you want to read data from?
  • do you want to create a new database?
    • do you prefer CLOS orientation
    • or to write SQL queries?

We have many libraries to work with databases, let’s have a recap first.

The #database section on the awesome-cl list +

Connecting to a database

Let’s study different use cases:

  • do you have an existing database you want to read data from?
  • do you want to create a new database?
    • do you prefer CLOS orientation
    • or to write SQL queries?

We have many libraries to work with databases, let’s have a recap first.

The #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:

  • wrappers to one database engine (cl-sqlite, postmodern, cl-redis, cl-duckdb…),
  • ORMs (Mito),
  • interfaces to several DB engines (cl-dbi, cl-yesql…),
  • lispy SQL syntax (sxql…)
  • in-memory persistent object databases (bknr.datastore, cl-prevalence,…),
  • graph databases in pure Lisp (AllegroGraph, vivace-graph) or wrappers (neo4cl),
  • object stores (cl-store, cl-naive-store…)
  • and other tools (pgloader, which was re-written from Python to Common Lisp).

Table of Contents

How to query an existing database

Let’s say you have a database called db.db and you want to extract data from it.

For our example, quickload this library:

(ql:quickload "cl-dbi")

cl-dbi can connect to major @@ -66,12 +66,12 @@ routes.

Using a DB connection per request with dbi:with-connection is a good idea.

Bonus: pimp your SQLite

SQLite is a great database. It loves backward compatibility. As such, its default settings may not be optimal for a web application seeing some load. You might want to set some PRAGMA -statements (SQLite settings).

To set them, look at your DB driver how to run a raw SQL query.

With cl-dbi, this would be dbi:do-sql:

(dbi:do-sql *connection* "PRAGMA cache_size = -20000;")

Here’s a nice list of pragmas useful for web development:

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/deployment/index.html b/docs/building-blocks/deployment/index.html index f910c2b..ac87d0d 100644 --- a/docs/building-blocks/deployment/index.html +++ b/docs/building-blocks/deployment/index.html @@ -15,7 +15,7 @@ Deploying manually We can start our executable in a shell and send it to the background (C-z bg), or run it inside a tmux session. These are not the best but hey, it worksΒ©. Here’s a tmux crashcourse: start a tmux session with tmux inside a session, C-b is tmux’s modifier key. use C-b c to create a new tab, C-b n and C-b p for β€œnext” and β€œprevious” tab/window. use C-b d to detach tmux and come back to your original console. Everything you started in tmux still runs in the background. use C-g to cancel a current prompt (as in Emacs). tmux ls lists the running tmux sessions. tmux attach goes back to a running session. tmux attach -t attaches to the session named β€œname”. inside a session, use C-b $ to name the current session, so you can see it with tmux ls. Here’s a cheatsheet that was handy.">Deployment :: Web Apps in Lisp: Know-how -

Deployment

How to deploy and monitor a Common Lisp web app?

Info

We are re-using content we contributed to the Cookbook.

Deploying manually

We can start our executable in a shell and send it to the background (C-z bg), or run it inside a tmux session. These are not the best but hey, it worksΒ©.

Here’s a tmux crashcourse:

  • start a tmux session with tmux
  • inside a session, C-b is tmux’s modifier key.
    • use C-b c to create a new tab, C-b n and C-b p for “next” and “previous” tab/window.
    • use C-b d to detach tmux and come back to your original console. Everything you started in tmux still runs in the background.
    • use C-g to cancel a current prompt (as in Emacs).
  • tmux ls lists the running tmux sessions.
  • tmux attach goes back to a running session.
    • tmux attach -t <name> attaches to the session named “name”.
    • inside a session, use C-b $ to name the current session, so you can see it with tmux ls.

Here’s a cheatsheet that was handy.

Unfortunately, if your app crashes or if your server is rebooted, your apps will be stopped. We can do better.

SystemD: daemonizing, restarting in case of crashes, handling logs

This is actually a system-specific task. See how to do that on your system.

Most GNU/Linux distros now come with Systemd, so here’s a little example.

Deploying an app with Systemd is as simple as writing a configuration file:

$ emacs -nw /etc/systemd/system/my-app.service
+

Deployment

How to deploy and monitor a Common Lisp web app?

Info

We are re-using content we contributed to the Cookbook.

Deploying manually

We can start our executable in a shell and send it to the background (C-z bg), or run it inside a tmux session. These are not the best but hey, it worksΒ©.

Here’s a tmux crashcourse:

  • start a tmux session with tmux
  • inside a session, C-b is tmux’s modifier key.
    • use C-b c to create a new tab, C-b n and C-b p for “next” and “previous” tab/window.
    • use C-b d to detach tmux and come back to your original console. Everything you started in tmux still runs in the background.
    • use C-g to cancel a current prompt (as in Emacs).
  • tmux ls lists the running tmux sessions.
  • tmux attach goes back to a running session.
    • tmux attach -t <name> attaches to the session named “name”.
    • inside a session, use C-b $ to name the current session, so you can see it with tmux ls.

Here’s a cheatsheet that was handy.

Unfortunately, if your app crashes or if your server is rebooted, your apps will be stopped. We can do better.

SystemD: daemonizing, restarting in case of crashes, handling logs

This is actually a system-specific task. See how to do that on your system.

Most GNU/Linux distros now come with Systemd, so here’s a little example.

Deploying an app with Systemd is as simple as writing a configuration file:

$ emacs -nw /etc/systemd/system/my-app.service
 [Unit]
 Description=stupid simple example
 
@@ -61,12 +61,12 @@
 app on localhost.

Reload nginx (send the “reload” signal):

$ nginx -s reload
 

and that’s it: you can access your Lisp app from the outside through http://www.your-domain-name.org.

Deploying on Heroku, Digital Ocean, OVH, Deploy.sh and other services

See:

Cloud Init

You can take inspiration from this Cloud Init file for SBCL, an init file for providers supporting the cloudinit format (DigitalOcean etc).

Monitoring

See Prometheus.cl for a Grafana dashboard for SBCL and Hunchentoot metrics (memory, -threads, requests per second,…).

See cl-sentry-client for error reporting.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/electron/index.html b/docs/building-blocks/electron/index.html index a8f4933..3c76050 100644 --- a/docs/building-blocks/electron/index.html +++ b/docs/building-blocks/electron/index.html @@ -11,7 +11,7 @@ Advise: study it before discarding it. It isn’t however the only portable web view solution. See our next section. Info This page appeared first on lisp-journey: three web views for Common Lisp, cross-platform GUIs.">Electron :: Web Apps in Lisp: Know-how -

Electron

Electron is heavy, but really cross-platform, and it has many tools +

Electron

Electron is heavy, but really cross-platform, and it has many tools around it. It allows to build releases for the three major OS from your development machine, its ecosystem has tools to handle updates, etc.

Advise: study it before discarding it.

It isn’t however the only portable web view solution. See our next section.

Ceramic (old but works)

Ceramic is a set of utilities @@ -33,8 +33,8 @@ localhost:[PORT] in Ceramic/Electron. That’s it.

Ceramic wasn’t updated in five years as of date and it downloads an outdated version of Electron by default (see (defparameter *electron-version* "5.0.2")), but you can change the version yourself.

The new Neomacs project, a structural editor and web browser, is a great modern example on how to use Ceramic. Give it a look and give it a try!

What Ceramic actually does is abstracted away in the CL functions, so I think it isn’t the best to start with. We can do without it to -understand the full process, here’s how.

Electron from scratch

Here’s our web app embedded in Electron:

-

Our steps are the following:

You can also run the Lisp web app from sources, of course, without +understand the full process, here’s how.

Electron from scratch

Here’s our web app embedded in Electron:

+

Our steps are the following:

You can also run the Lisp web app from sources, of course, without building a binary, but then you’ll have to ship all the lisp sources.

main.js

The most important file to an Electron app is the main.js. The one we show below does the following:

  • it starts Electron
  • it starts our web application on the side, as a child process, from a binary name, and a port.
  • it shows our child process’ stdout and stderr
  • it opens a browser window to show our app, running on localhost.
  • it handles the close event.

Here’s our version.

console.log(`Hello from Electron πŸ‘‹`)
 
 const { app, BrowserWindow } = require('electron')
@@ -119,12 +119,12 @@
 the binary into the Electron release.

Then, if you want to communicate from the Lisp app to the Electron window, and the other way around, you’ll have to use the JavaScript layers. Ceramic might help here.

What about Tauri?

Bundling an app with Tauri will, AFAIK (I just tried quickly), involve the same steps than with Electron. Tauri might -still have less tools for it. You need the Rust toolchain.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/errors-interactivity/index.html b/docs/building-blocks/errors-interactivity/index.html index c60ce11..2296f62 100644 --- a/docs/building-blocks/errors-interactivity/index.html +++ b/docs/building-blocks/errors-interactivity/index.html @@ -7,19 +7,19 @@ not being interactive: it returns a 404 page and prints the error output on the REPL not interactive but developper friendly: it can show the lisp error message on the HTML page, as well as the full Lisp backtrace it can let the developper have the interactive debugger: in that case the framework doesn’t catch the error, it lets it pass through, and we the developper deal with it as in a normal Slime session. We see this by default:">Errors and interactivity :: Web Apps in Lisp: Know-how -

Errors and interactivity

In all frameworks, we can choose the level of interactivity.

The web framework can be interactive in different degrees. What should +

Errors and interactivity

In all frameworks, we can choose the level of interactivity.

The web framework can be interactive in different degrees. What should it do when an error happens?

  • not being interactive: it returns a 404 page and prints the error output on the REPL
  • not interactive but developper friendly: it can show the lisp error message on the HTML page,
  • as well as the full Lisp backtrace
  • it can let the developper have the interactive debugger: in that case the framework doesn’t catch the error, it lets it pass through, and -we the developper deal with it as in a normal Slime session.

We see this by default:

-

but we can also show backtraces and augment the data.

Hunchentoot

The global variables to set are

  • *show-lisp-errors-p*, nil by default
  • *show-lisp-backtraces-p*, t by default (but the backtrace won’t be shown if the previous setting is nil)
  • *catch-errors-p*, t by default, to set to nil to get the interactive debugger.

We can see the backtrace on our error page:

(setf hunchentoot:*show-lisp-errors-p* t)

-

Enhancing the backtrace in the browser

We can also the library hunchentoot-errors to augment the data we see in the stacktrace:

  • show the current request (URI, headers…)
  • show the current session (everything you stored in the session)

-

Clack errors

When you use the Clack web server, you can use Clack errors, which can also show the backtrace and the session, with a colourful output:

-

Hunchentoot’s conditions

Hunchentoot defines condition classes:

  • hunchentoot-warning, the superclass for all warnings
  • hunchentoot-error, the superclass for errors
  • parameter-error
  • bad-request

See the (light) documentation: https://edicl.github.io/hunchentoot/#conditions.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/flash-messages/index.html b/docs/building-blocks/flash-messages/index.html index f880e22..c58cfb1 100644 --- a/docs/building-blocks/flash-messages/index.html +++ b/docs/building-blocks/flash-messages/index.html @@ -11,10 +11,10 @@ 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:">Flash messages :: Web Apps in Lisp: Know-how -

Flash messages

Flash messages are temporary messages you want to show to your +

Flash messages

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 +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:

(ql:quickload '("hunchentoot" "djula" "easy-routes"))

We also introduce a local nickname, to shorten the use of hunchentoot to ht:

(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:

(defparameter *port* 9876)
 (defvar *server* nil "Our Hunchentoot acceptor")
@@ -138,12 +138,12 @@
   (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.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/flash-template/index.html b/docs/building-blocks/flash-template/index.html index 04595e4..638b766 100644 --- a/docs/building-blocks/flash-template/index.html +++ b/docs/building-blocks/flash-template/index.html @@ -1,13 +1,13 @@ -

WALK - flash messages +

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 %}
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/form-validation/index.html b/docs/building-blocks/form-validation/index.html index aa4a746..3d750aa 100644 --- a/docs/building-blocks/form-validation/index.html +++ b/docs/building-blocks/form-validation/index.html @@ -11,7 +11,7 @@ See also the cl-forms library that offers many features: automatic forms form validation with in-line error messages CSRF protection client-side validation subforms Djula and Spinneret renderers default themes an online demo for you to try etc Get them: (ql:quickload '(:clavier :cl-forms)) Form validation with the Clavier library Clavier defines validators as class instances. They come in many types, for example the 'less-than, greater-than or len validators. They may take an initialization argument.">Form validation :: Web Apps in Lisp: Know-how -

Form validation

We can recommend the clavier +

Form validation

We can recommend the clavier library for input validation.

See also the cl-forms library that offers many features:

  • automatic forms
  • form validation with in-line error messages
  • CSRF protection
  • client-side validation
  • subforms
  • Djula and Spinneret renderers
  • default themes
  • an online demo for you to try
  • etc

Get them:

(ql:quickload '(:clavier :cl-forms))

Form validation with the Clavier library

Clavier defines validators as class instances. They come in many types, for example the 'less-than, @@ -73,12 +73,12 @@ NIL ("Length of \"foo\" is less than 5")

Note that Clavier has a “validator-collection” thing, but not shown in the README, and is in our opininion too verbose in comparison to a -simple list.

\ No newline at end of file diff --git a/docs/building-blocks/headers/index.html b/docs/building-blocks/headers/index.html index 4c7fbc3..6d5520b 100644 --- a/docs/building-blocks/headers/index.html +++ b/docs/building-blocks/headers/index.html @@ -19,14 +19,14 @@ (setf (hunchentoot:header-out "HX-Trigger") "myEvent") This sets the header of the current request. USe headers-out (plural) to get an association list of headers: An alist of the outgoing http headers not including the β€˜Set-Cookie’, β€˜Content-Length’, and β€˜Content-Type’ headers. Use the functions HEADER-OUT and (SETF HEADER-OUT) to modify this slot.'>Headers :: Web Apps in Lisp: Know-how -

Headers

A quick reference.

These functions have to be used in the context of a web request.

Set headers

Use header-out to set headers, like this:

(setf (hunchentoot:header-out "HX-Trigger") "myEvent")

This sets the header of the current request.

USe headers-out (plural) to get an association list of headers:

An alist of the outgoing http headers not including the ‘Set-Cookie’, ‘Content-Length’, and ‘Content-Type’ headers. Use the functions HEADER-OUT and (SETF HEADER-OUT) to modify this slot.

Get headers

Use the header-in* and headers-in* (plural) function:

Function: (header-in* name &optional (request *request*))
+

Headers

A quick reference.

These functions have to be used in the context of a web request.

Set headers

Use header-out to set headers, like this:

(setf (hunchentoot:header-out "HX-Trigger") "myEvent")

This sets the header of the current request.

USe headers-out (plural) to get an association list of headers:

An alist of the outgoing http headers not including the ‘Set-Cookie’, ‘Content-Length’, and ‘Content-Type’ headers. Use the functions HEADER-OUT and (SETF HEADER-OUT) to modify this slot.

Get headers

Use the header-in* and headers-in* (plural) function:

Function: (header-in* name &optional (request *request*))
 

Returns the incoming header with name NAME. NAME can be a keyword (recommended) or a string.

headers-in*:

Function: (headers-in* &optional (request *request*))
-

Returns an alist of the incoming headers associated with the REQUEST object REQUEST.

Reference

Find some more here:

Returns an alist of the incoming headers associated with the REQUEST object REQUEST.

Reference

Find some more here:

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/index.html b/docs/building-blocks/index.html index b21df3e..51f5a5d 100644 --- a/docs/building-blocks/index.html +++ b/docs/building-blocks/index.html @@ -19,7 +19,7 @@ We will use the Hunchentoot web server, but we should say a few words about Clack too. Hunchentoot is a web server and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL. It provides facilities like automatic session handling (with and without cookies), logging, customizable error handling, and easy access to GET and POST parameters sent by the client.'>Building blocks :: Web Apps in Lisp: Know-how -

Building blocks

In this chapter we’ll create routes, we’ll serve +

Building blocks

In this chapter we’ll create routes, we’ll serve local files and we’ll run more than one web app in the same running image.

We’ll use those libraries:

(ql:quickload '("hunchentoot" "easy-routes" "djula" "spinneret"))
Info

You can create a web project with our project generator: cl-cookieweb.

We will use the Hunchentoot web server, but we should say a few words about Clack too.

Hunchentoot is

a web server and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL. It provides facilities like automatic session handling (with and without cookies), logging, customizable error handling, and easy access to GET and POST parameters sent by the client.

It is a software written by Edi Weitz (author of the “Common Lisp Recipes” book, of the ubiquitous cl-ppcre library and much more), it is used and @@ -55,12 +55,12 @@ and update since 2017. We present it in more details in the “Isomorphic web frameworks” appendix.

For a full list of libraries for the web, please see the awesome-cl list #network-and-internet -and Cliki.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/index.xml b/docs/building-blocks/index.xml index ff571a9..d2af33d 100644 --- a/docs/building-blocks/index.xml +++ b/docs/building-blocks/index.xml @@ -9,7 +9,7 @@ Serve local files If you followed the tutorial you know that we can create and s Remember from simple web server how Hunchentoot serves files from a www/ directory by default. We can change that. Hunchentoot Use the create-folder-dispatcher-and-handler prefix directory function. For example: -(defun serve-static-assets () "Let Hunchentoot serve static assets under the /src/static/ directory of your :myproject system. Then reference static assets with the /static/ URL prefix." (push (hunchentoot:create-folder-dispatcher-and-handler "/static/" (merge-pathnames "src/static" ;; starts without a / (asdf:system-source-directory :myproject))) ;; <- myproject hunchentoot:*dispatch-table*)) and call it in the function that starts your application:Routes and URL parametershttp://example.org/building-blocks/routing/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/routing/index.htmlI prefer the easy-routes library than pure Hunchentoot to define routes, as we did in the tutorial, so skip to its section below if you want. However, it can only be benificial to know the built-in Hunchentoot ways. +(defun serve-static-assets () "Let Hunchentoot serve static assets under the /src/static/ directory of your :myproject system. Then reference static assets with the /static/ URL prefix." (push (hunchentoot:create-folder-dispatcher-and-handler "/static/" (asdf:system-relative-pathname :myproject "src/static/")) ;; ^^^ starts without a / hunchentoot:*dispatch-table*)) and call it in the function that starts your application:Routes and URL parametershttp://example.org/building-blocks/routing/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/routing/index.htmlI prefer the easy-routes library than pure Hunchentoot to define routes, as we did in the tutorial, so skip to its section below if you want. However, it can only be benificial to know the built-in Hunchentoot ways. Info Please see the tutorial where we define routes with path parameters and where we also access URL parameters. Hunchentoot The dispatch table The first, most basic way in Hunchentoot to create a route is to add a URL -> function association in its β€œprefix dispatch” table.Templateshttp://example.org/building-blocks/templates/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/templates/index.htmlDjula - HTML markup Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation. Install it if you didn’t already: @@ -17,7 +17,10 @@ Install it if you didn’t already: (djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/")) and then we can declare and compile the ones we use, for example:: (defparameter +base.html+ (djula:compile-template* "base.html")) (defparameter +products.html+ (djula:compile-template* "products.html")) A Djula template looks like this:Sessionshttp://example.org/building-blocks/session/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/session/index.htmlWhen a web client (a user’s browser) connects to your app, you can start a session with it, and store data, the time of the session. Hunchentoot uses cookies to store the session ID, and if not possible it rewrites URLs. So, at every subsequent request by this web client, you can check if there is a session data. -You can store any Lisp object in a web session. You only have to:Headershttp://example.org/building-blocks/headers/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/headers/index.htmlA quick reference. +You can store any Lisp object in a web session. You only have to:Flash messageshttp://example.org/building-blocks/flash-messages/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/flash-messages/index.htmlFlash 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:Headershttp://example.org/building-blocks/headers/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/headers/index.htmlA quick reference. These functions have to be used in the context of a web request. Set headers Use header-out to set headers, like this: (setf (hunchentoot:header-out "HX-Trigger") "myEvent") This sets the header of the current request. diff --git a/docs/building-blocks/remote-debugging/index.html b/docs/building-blocks/remote-debugging/index.html index 2ad2996..f624a3e 100644 --- a/docs/building-blocks/remote-debugging/index.html +++ b/docs/building-blocks/remote-debugging/index.html @@ -3,7 +3,7 @@ You can not only inspect the running program, but also compile and load new code, including installing new libraries, effectively doing hot code reload. It’s up to you to decide to do it or to follow the industry’s best practices. It isn’t because you use Common Lisp that you have to make your deployed program a β€œbig ball of mud”.">Remote debugging :: Web Apps in Lisp: Know-how -

Remote debugging

You can have your software running on a machine over the network, +

Remote debugging

You can have your software running on a machine over the network, connect to it and debug it from home, from your development environment.

You can not only inspect the running program, but also compile and load new code, including installing new libraries, effectively doing @@ -53,12 +53,12 @@ and port 4006.

We can write new code:

(defun dostuff ()
   (format t "goodbye world ~a!~%" *counter*))
 (setf *counter* 0)

and eval it as usual with C-c C-c or M-x slime-eval-region for instance. The output should change.

That’s how Ron Garret debugged the Deep Space 1 spacecraft from the earth -in 1999:

We were able to debug and fix a race condition that had not shown up during ground testing. (Debugging a program running on a $100M piece of hardware that is 100 million miles away is an interesting experience. Having a read-eval-print loop running on the spacecraft proved invaluable in finding and fixing the problem.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/routing/index.html b/docs/building-blocks/routing/index.html index 10f00a2..4384cd9 100644 --- a/docs/building-blocks/routing/index.html +++ b/docs/building-blocks/routing/index.html @@ -7,7 +7,7 @@ Hunchentoot The dispatch table The first, most basic way in Hunchentoot to create a route is to add a URL -> function association in its β€œprefix dispatch” table.">Routes and URL parameters :: Web Apps in Lisp: Know-how -

Routes and URL parameters

I prefer the easy-routes library than pure Hunchentoot to define +

Routes and URL parameters

I prefer the easy-routes library than pure Hunchentoot to define routes, as we did in the tutorial, so skip to its section below if you want. However, it can only be benificial to know the built-in Hunchentoot ways.

Info

Please see the tutorial where we define routes with path parameters @@ -78,12 +78,12 @@ (setf (hunchentoot:content-type*) "text/plain") (format nil "Hey ~a you are of type ~a" name (type-of name)))

Going to http://localhost:4242/yo?name=Alice returns

Hey Alice you are of type (SIMPLE-ARRAY CHARACTER (5))
 

To automatically bind it to another type, we use default-parameter-type. It can be -one of those simple types:

  • 'string (default),
  • 'integer,
  • 'character (accepting strings of length 1 only, otherwise it is nil)
  • or 'boolean

or a compound list:

  • '(:list <type>)
  • '(:array <type>)
  • '(:hash-table <type>)

where <type> is a simple type.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/session/index.html b/docs/building-blocks/session/index.html index 15210ef..7dd2734 100644 --- a/docs/building-blocks/session/index.html +++ b/docs/building-blocks/session/index.html @@ -7,15 +7,15 @@ You can store any Lisp object in a web session. You only have to:">Sessions :: Web Apps in Lisp: Know-how -

Sessions

When a web client (a user’s browser) connects to your app, you can +

Sessions

When a web client (a user’s browser) connects to your app, you can start a session with it, and store data, the time of the session.

Hunchentoot uses cookies to store the session ID, and if not possible it rewrites URLs. So, at every subsequent request by this web client, -you can check if there is a session data.

You can store any Lisp object in a web session. You only have to:

  • start a session with (hunchentoot:start-session)
    • for example, when a user logs in successfully
  • store an object with (setf (hunchentoot:session-value 'key) val)
    • for example, store the username at the log-in
  • and get the object with (hunchentoot:session-value 'key).
    • for example, in any route where you want to check that a user is logged in. If you don’t find a session key you want, you would redirect to the login page.

See our example in the next section about log-in.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/simple-web-server/index.html b/docs/building-blocks/simple-web-server/index.html index df7e460..5070d2b 100644 --- a/docs/building-blocks/simple-web-server/index.html +++ b/docs/building-blocks/simple-web-server/index.html @@ -7,7 +7,7 @@ (defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4242)) (hunchentoot:start *acceptor*) We create an instance of easy-acceptor on port 4242 and we start it. We can now access http://127.0.0.1:4242/. You should get a welcome screen with a link to the documentation and logs to the console.">Simple web server :: Web Apps in Lisp: Know-how -

Simple web server

Before dwelving into web development, we might want to do something simple: serve some files we have on disk.

Serve local files

If you followed the tutorial you know that we can create and start a webserver like this:

(defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4242))
+

Simple web server

Before dwelving into web development, we might want to do something simple: serve some files we have on disk.

Serve local files

If you followed the tutorial you know that we can create and start a webserver like this:

(defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4242))
 (hunchentoot:start *acceptor*)

We create an instance of easy-acceptor on port 4242 and we start it. We can now access http://127.0.0.1:4242/. You should get a welcome screen with a link to the documentation and logs to the console.

Info

You can also use Roswell’s http.server from the command line:

$ ros install roswell/http.server
@@ -35,12 +35,12 @@
 (hunchentoot:start *my-acceptor*)

go to http://127.0.0.1:4444/ and see the difference.

Note that we just created another web application on a different port on the same lisp image. This is already pretty cool.

Access your server from the internet

With Hunchentoot we have nothing to do, we can see the server from the internet right away.

If you evaluate this on your VPS:

(hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242))

You can see it right away on your server’s IP.

You can use the :address parameter of Hunchentoot’s easy-acceptor to -bind it and restrict it to 127.0.0.1 (or any address) if you wish.

Stop it with (hunchentoot:stop *).

Now on the next section, we’ll create some routes to build a dynamic website.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/static/index.html b/docs/building-blocks/static/index.html index 0caeaf8..7a5b6a0 100644 --- a/docs/building-blocks/static/index.html +++ b/docs/building-blocks/static/index.html @@ -15,7 +15,7 @@ Hunchentoot Use the create-folder-dispatcher-and-handler prefix directory function. For example: (defun serve-static-assets () "Let Hunchentoot serve static assets under the /src/static/ directory of your :myproject system. Then reference static assets with the /static/ URL prefix." (push (hunchentoot:create-folder-dispatcher-and-handler "/static/" (asdf:system-relative-pathname :myproject "src/static/")) ;; ^^^ starts without a / hunchentoot:*dispatch-table*)) and call it in the function that starts your application:'>Static assets :: Web Apps in Lisp: Know-how -

Static assets

How can we serve static assets?

Remember from simple web server +

Static assets

How can we serve static assets?

Remember from simple web server how Hunchentoot serves files from a www/ directory by default. We can change that.

Hunchentoot

Use the create-folder-dispatcher-and-handler prefix directory function.

For example:

(defun serve-static-assets ()
   "Let Hunchentoot serve static assets under the /src/static/ directory
   of your :myproject system.
@@ -25,12 +25,12 @@
           (asdf:system-relative-pathname :myproject "src/static/"))
           ;;                                        ^^^ starts without a /
         hunchentoot:*dispatch-table*))

and call it in the function that starts your application:

(serve-static-assets)

Now our project’s static files located under src/static/ are served -with the /static/ prefix. Access them like this:

<img src="/static/img/banner.jpg" />

or

<script src="/static/test.js" type="text/javascript"></script>

where the file src/static/test.js could be

console.log("hello");
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/templates/index.html b/docs/building-blocks/templates/index.html index 9c41269..652a8a1 100644 --- a/docs/building-blocks/templates/index.html +++ b/docs/building-blocks/templates/index.html @@ -15,7 +15,7 @@ (ql:quickload "djula") The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like: (djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/")) and then we can declare and compile the ones we use, for example:: (defparameter +base.html+ (djula:compile-template* "base.html")) (defparameter +products.html+ (djula:compile-template* "products.html")) A Djula template looks like this:'>Templates :: Web Apps in Lisp: Know-how -

Templates

Djula - HTML markup

Djula is a port of Python’s +

Templates

Djula - HTML markup

Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation.

Install it if you didn’t already:

(ql:quickload "djula")

The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like:

(djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/"))

and then we can declare and compile the ones we use, for example::

(defparameter +base.html+ (djula:compile-template* "base.html"))
 (defparameter +products.html+ (djula:compile-template* "products.html"))

A Djula template looks like this:

{% extends "base.html" %}
@@ -64,12 +64,12 @@
    (:ol (dolist (item *shopping-list*)
           (:li (1+ (random 10)) item))))
   (:footer ("Last login: ~A" *last-login*)))

I find Spinneret easier to use than the more famous cl-who, but I -personnally prefer to use HTML templates.

Spinneret has nice features under it sleeves:

  • it warns on invalid tags and attributes
  • it can automatically number headers, given their depth
  • it pretty prints html per default, with control over line breaks
  • it understands embedded markdown
  • it can tell where in the document a generator function is (see get-html-tag)
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/user-log-in/index.html b/docs/building-blocks/user-log-in/index.html index 82b2568..8b27315 100644 --- a/docs/building-blocks/user-log-in/index.html +++ b/docs/building-blocks/user-log-in/index.html @@ -11,9 +11,9 @@ We show an example, without handling passwords yet. See the next section for passwords. We’ll build a simple log-in page to an admin/ private dashboard. What do we need to do exactly?">User log-in :: Web Apps in Lisp: Know-how -

User log-in

How do you check if a user is logged-in, and how do you do the actual log in?

We show an example, without handling passwords yet. See the next section for passwords.

We’ll build a simple log-in page to an admin/ private dashboard.

-

-

What do we need to do exactly?

  • we need a function to get a user by its ID
  • we need a function to check that a password is correct for a given user
  • we need two templates:
    • a login template
    • a template for a logged-in user
  • we need a route and we need to handle a POST request
    • when the log-in is successful, we need to store a user ID in a web session

We choose to structure our app with an admin/ URL that will show +

User log-in

How do you check if a user is logged-in, and how do you do the actual log in?

We show an example, without handling passwords yet. See the next section for passwords.

We’ll build a simple log-in page to an admin/ private dashboard.

+

+

What do we need to do exactly?

  • we need a function to get a user by its ID
  • we need a function to check that a password is correct for a given user
  • we need two templates:
    • a login template
    • a template for a logged-in user
  • we need a route and we need to handle a POST request
    • when the log-in is successful, we need to store a user ID in a web session

We choose to structure our app with an admin/ URL that will show both the login page or the “dashboard” for logged-in users.

We use these libraries:

(ql:quickload '("hunchentoot" "djula" "easy-routes"))

We still work from inside our :myproject package. You should have this at the top of your file:

(in-package :myproject)

Let’s start with the model functions.

Get users

(defun get-user (name)
   (list :name name :password "demo")) ;; <--- all our passwords are "demo"

Yes indeed, that’s a dummy function. You will add your own logic @@ -270,12 +270,12 @@ (defroute ("/account/review" :method :get) () (with-logged-in (render #p"review.html" - (list :review (get-review (gethash :user *session*))))))

and so on.

\ No newline at end of file diff --git a/docs/building-blocks/users-and-passwords/index.html b/docs/building-blocks/users-and-passwords/index.html index 79a0a14..e06c126 100644 --- a/docs/building-blocks/users-and-passwords/index.html +++ b/docs/building-blocks/users-and-passwords/index.html @@ -7,7 +7,7 @@ Creating users If you use a database, you’ll have to create at least a users table. It would typically define:">Users and passwords :: Web Apps in Lisp: Know-how -

Users and passwords

We don’t know of a Common Lisp framework that will create users and +

Users and passwords

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.

Info

Stay tuned! We are on to something.

Creating users

If you use a database, you’ll have to create at least a users @@ -50,12 +50,12 @@ (make-op := (if (integerp user) :id_user :email) - user))))))

Credit: /u/arvid on /r/learnlisp.

\ No newline at end of file diff --git a/docs/building-blocks/web-views/index.html b/docs/building-blocks/web-views/index.html index 766f2ad..d56bb25 100644 --- a/docs/building-blocks/web-views/index.html +++ b/docs/building-blocks/web-views/index.html @@ -11,7 +11,7 @@ We present two: WebUI and Webview.h, through CLOG Frame. Info This page appeared first on lisp-journey: three web views for Common Lisp, cross-platform GUIs. WebUI WebUI is a new kid in town. It is in development, it has bugs. You can view it as a wrapper around a browser window (or webview.h).">Web views: cross-platform GUIs :: Web Apps in Lisp: Know-how -

Web views: cross-platform GUIs

Web views are lightweight and cross-platform. They are nowadays a good +

Web views: cross-platform GUIs

Web views are lightweight and cross-platform. They are nowadays a good solution to ship a GUI program to your users.

We present two: WebUI and Webview.h, through CLOG Frame.

WebUI

WebUI is a new kid in town. It is in development, it has bugs. You can view it as a wrapper around a browser window (or webview.h).

However it is ligthweight, it is easy to build and we have Lisp bindings.

A few more words about it:

Use any web browser or WebView as GUI, with your preferred language in the backend and modern web technologies in the frontend, all in a lightweight portable library.

  • written in pure C
  • one header file
  • multi-platform & multi-browser
  • opens a real browser (you get the web development tools etc)
  • cross-platform webview
  • we can call JS from Common Lisp, and call Common Lisp from JS.

Think of WebUI like a WebView controller, but instead of embedding the WebView controller in your program, which makes the final program big in size, and non-portable as it needs the WebView runtimes. Instead, by using WebUI, you use a tiny static/dynamic library to run any installed web browser and use it as GUI, which makes your program small, fast, and portable. All it needs is a web browser.

your program will always run on all machines, as all it needs is an installed web browser.

Sounds compelling right?

The other good news is that Common Lisp was one of the first languages it got bindings for. How it happened: I was chating in Discord, mentioned WebUI and BAM! @garlic0x1 developed bindings:

thank you so much! (@garlic0x1 has more cool projects on GitHub you can browse. He’s also a contributor to Lem)

Here’s a simple snippet:

(defpackage :webui/examples/minimal
   (:use :cl :webui)
@@ -49,15 +49,15 @@
                            "Admin"
                            (format nil "~A/admin/" 4284)
                            ;; window dimensions (strings)
-                           "1280" "840"))

and voilΓ .

-

Now for the cross-platform part, you’ll need to build clogframe and + "1280" "840"))

and voilΓ .

+

Now for the cross-platform part, you’ll need to build clogframe and your web app on the target OS (like with any CL app). Webview.h is cross-platform. -Leave us a comment when you have a good CI setup for the three main OSes (I am studying 40ants/ci and make-common-lisp-program for now).

\ No newline at end of file diff --git a/docs/categories/index.html b/docs/categories/index.html index 2df0d11..d86fcdc 100644 --- a/docs/categories/index.html +++ b/docs/categories/index.html @@ -1,10 +1,10 @@ Categories :: Web Apps in Lisp: Know-how -

Categories

\ No newline at end of file diff --git a/docs/css/chroma-auto.css b/docs/css/chroma-auto.css index 19a9b4b..5986882 100644 --- a/docs/css/chroma-auto.css +++ b/docs/css/chroma-auto.css @@ -1,2 +1,2 @@ -@import "chroma-relearn-light.css?1736963403" screen and (prefers-color-scheme: light); -@import "chroma-relearn-dark.css?1736963403" screen and (prefers-color-scheme: dark); +@import "chroma-relearn-light.css?1741870399" screen and (prefers-color-scheme: light); +@import "chroma-relearn-dark.css?1741870399" screen and (prefers-color-scheme: dark); diff --git a/docs/css/format-print.css b/docs/css/format-print.css index 6cf26bd..a2a1772 100644 --- a/docs/css/format-print.css +++ b/docs/css/format-print.css @@ -1,5 +1,5 @@ -@import "theme-relearn-light.css?1736963403"; -@import "chroma-relearn-light.css?1736963403"; +@import "theme-relearn-light.css?1741870399"; +@import "chroma-relearn-light.css?1741870399"; #R-sidebar { display: none; diff --git a/docs/css/print.css b/docs/css/print.css index 596f39a..25a745a 100644 --- a/docs/css/print.css +++ b/docs/css/print.css @@ -1 +1 @@ -@import "format-print.css?1736963403"; +@import "format-print.css?1741870399"; diff --git a/docs/css/swagger.css b/docs/css/swagger.css index 9d68410..5af5bca 100644 --- a/docs/css/swagger.css +++ b/docs/css/swagger.css @@ -1,7 +1,7 @@ /* Styles to make Swagger-UI fit into our theme */ -@import "fonts.css?1736963403"; -@import "variables.css?1736963403"; +@import "fonts.css?1741870399"; +@import "variables.css?1741870399"; body{ line-height: 1.574; diff --git a/docs/css/theme-auto.css b/docs/css/theme-auto.css index 5162b48..4a9e15f 100644 --- a/docs/css/theme-auto.css +++ b/docs/css/theme-auto.css @@ -1,2 +1,2 @@ -@import "theme-relearn-light.css?1736963403" screen and (prefers-color-scheme: light); -@import "theme-relearn-dark.css?1736963403" screen and (prefers-color-scheme: dark); +@import "theme-relearn-light.css?1741870399" screen and (prefers-color-scheme: light); +@import "theme-relearn-dark.css?1741870399" screen and (prefers-color-scheme: dark); diff --git a/docs/css/theme.css b/docs/css/theme.css index 25cb8c6..baa37cb 100644 --- a/docs/css/theme.css +++ b/docs/css/theme.css @@ -1,4 +1,4 @@ -@import "variables.css?1736963403"; +@import "variables.css?1741870399"; @charset "UTF-8"; diff --git a/docs/index.html b/docs/index.html index 5cd0ae7..6212a65 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,16 +6,16 @@ What’s in this guide We’ll start with a tutorial that shows the essential building blocks: starting a web server defining routes grabbing URL parameters rendering templates and running our app from sources, or building a binary. We’ll build a simple page that presents a search form, filters a list of products and displays the results.">Web Apps in Lisp: Know-how -

Web Apps in Lisp: Know-how

You want to write a web application in Common Lisp and you don’t know +starting a web server defining routes grabbing URL parameters rendering templates and running our app from sources, or building a binary. We’ll build a simple page that presents a search form, filters a list of products and displays the results.">Web Apps in Lisp: Know-how +

Web Apps in Lisp: Know-how

You want to write a web application in Common Lisp and you don’t know where to start? You are a beginner in web development, or a lisp amateur looking for clear and short pointers about web dev in Lisp? You are all at the right place.

What’s in this guide

We’ll start with a tutorial that shows the essential building blocks:

  • starting a web server
  • defining routes
  • grabbing URL parameters
  • rendering templates
  • and running our app from sources, or building a binary.

We’ll build a simple page that presents a search form, filters a list of products and displays the results.

The building blocks section is organized by topics, so that with a question in mind you should be able to look at the table of contents, pick the right page and go ahead. You’ll find more tutorials, for -example in “User log-in” we build a login form.

-

We hope that it will be plenty useful to you.

Don’t hesitate to share what you’re building!

Info

πŸŽ₯ If you want to learn Common Lisp efficiently, +example in “User log-in” we build a login form.

+

We hope that it will be plenty useful to you.

Don’t hesitate to share what you’re building!

Info

πŸŽ₯ If you want to learn Common Lisp efficiently, with a code-driven approach, good news, I am creating a video course on the Udemy platform. Already more than 7 hours of content, rated @@ -24,13 +24,12 @@ also have Common Lisp tutorial videos on Youtube.

Now let’s go to the tutorial.

How to start with Common Lisp

This resource is not about learning Common Lisp the language. We expect you have a working setup, with the Quicklisp package -manager. Please refer to the CL -Cookbook.

Contact

We are @vindarel on Lisp’s Discord server and Mastodon.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/index.xml b/docs/index.xml index 77a7d66..ec12d9d 100644 --- a/docs/index.xml +++ b/docs/index.xml @@ -13,4 +13,5 @@ Weblocks was an old framework developed by Slava Akhmechet, Stephen Compall and The Ultralisp website is an example Reblocks website in production known in the CL community. It isn’t the only solution that aims at making writing interactive web apps easier, where the client logic can be brought to the back-end. See also:See alsohttp://example.org/see-also/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/see-also/index.htmlOther tutorials: Neil Munro’s Clack/Lack/Ningle tutorial the Cookbook Project skeletons and demos: -cl-cookieweb - a web project template Feather, a template for web application development, shows a functioning Hello World app with an HTML page, a JSON API, a passing test suite, a Postgres DB and DB migrations. Uses Qlot, Buildapp, SystemD for deployment. lisp-web-template-productlist, a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS. lisp-web-live-reload-example - a toy project to show how to interact with a running web app. Libraries:
\ No newline at end of file +cl-cookieweb - a web project template lisp-web-template-productlist, a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS. lisp-web-live-reload-example - a toy project to show how to interact with a running web app. Libraries: +awesome-cl \ No newline at end of file diff --git a/docs/isomorphic-web-frameworks/clog/index.html b/docs/isomorphic-web-frameworks/clog/index.html index 2f224d9..8110722 100644 --- a/docs/isomorphic-web-frameworks/clog/index.html +++ b/docs/isomorphic-web-frameworks/clog/index.html @@ -7,7 +7,7 @@ We can say the CLOG experience is mindblowing.">CLOG :: Web Apps in Lisp: Know-how -

CLOG

CLOG, the Common Lisp Omnificent +

CLOG

CLOG, the Common Lisp Omnificent GUI, follows a GUI paradigm for the web platform. You don’t write nested <div> tags, but you place elements on the page. It sends changes to the page you are working on @@ -18,8 +18,8 @@ under the fingertips.

Moreover, its API is stable. The author used a similar product built in Ada professionally for a decade, and transitioned to CLOG in Common Lisp.

So, how can we build an interactive app with CLOG?

We will build a search form that triggers a search to the back-end on -key presses, and displays results to users as they type.

We do so without writing any JavaScript.

-

Before we do so, we’ll create a list of dummy products, so than we have something to search for.

Models

Let’s create a package for this new app. I’ll “use” functions and +key presses, and displays results to users as they type.

We do so without writing any JavaScript.

+

Before we do so, we’ll create a list of dummy products, so than we have something to search for.

Models

Let’s create a package for this new app. I’ll “use” functions and macros provided by the :clog package.

(uiop:define-package :clog-search
     (:use :cl :clog))
 
@@ -174,8 +174,8 @@
   (let ((query (value obj)))
     (if (> (length query) 2)
         (display-products div (clog-search::search-products query))
-        (print "waiting for more input"))))

It works \o/

-

There are some caveats that need to be worked on:

  • if you type a search query of 4 letters quickly, our handler waits for an input of at least 2 characters, but it will be fired 2 other times. That will probably fix the blickering.

And, as you noticed:

  • we didn’t copy-paste a nice looking HTML template, so we have a bit of work with that :/

This was only an introduction. As we said, CLOG is well suited for a + (print "waiting for more input"))))

It works \o/

+

There are some caveats that need to be worked on:

  • if you type a search query of 4 letters quickly, our handler waits for an input of at least 2 characters, but it will be fired 2 other times. That will probably fix the blickering.

And, as you noticed:

  • we didn’t copy-paste a nice looking HTML template, so we have a bit of work with that :/

This was only an introduction. As we said, CLOG is well suited for a wide range of applications.

Stop the app

Use

(clog:shutdown)

Full code

;; (ql:quickload '(:clog :str))
 
 (uiop:define-package :clog-search
@@ -286,12 +286,12 @@
   (let ((query (value obj)))
     (if (> (length query) 2)
         (display-products div (search-products query))
-        (print "waiting for more input"))))

References

\ No newline at end of file diff --git a/docs/isomorphic-web-frameworks/index.html b/docs/isomorphic-web-frameworks/index.html index fbcae40..d8447a2 100644 --- a/docs/isomorphic-web-frameworks/index.html +++ b/docs/isomorphic-web-frameworks/index.html @@ -11,17 +11,17 @@ Weblocks was an old framework developed by Slava Akhmechet, Stephen Compall and Leslie Polzer. After nine calm years, it saw a very active update, refactoring and rewrite effort by Alexander Artemenko. This active project is named Reblocks. The Ultralisp website is an example Reblocks website in production known in the CL community. It isn’t the only solution that aims at making writing interactive web apps easier, where the client logic can be brought to the back-end. See also:">Isomorphic web frameworks :: Web Apps in Lisp: Know-how -

Isomorphic web frameworks

We’ll see first: Reblocks.

Weblocks was an old framework developed by Slava Akhmechet, Stephen +

Isomorphic web frameworks

We’ll see first: Reblocks.

Weblocks was an old framework developed by Slava Akhmechet, Stephen Compall and Leslie Polzer. After nine calm years, it saw a very active update, refactoring and rewrite effort by Alexander Artemenko. This active project is named Reblocks.

The Ultralisp website is an example Reblocks website in production known in the CL community.

It isn’t the only solution that aims at making writing interactive web apps easier, where the client logic can be brought to the back-end. See also:

Of course, a special mention goes to HTMX, which -is language agnostic and backend agnostic. It works well with Common Lisp.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/isomorphic-web-frameworks/weblocks/index.html b/docs/isomorphic-web-frameworks/weblocks/index.html index cbd4f15..1f542fb 100644 --- a/docs/isomorphic-web-frameworks/weblocks/index.html +++ b/docs/isomorphic-web-frameworks/weblocks/index.html @@ -7,12 +7,12 @@ Note To install Reblocks, please see its documentation.">Reblocks :: Web Apps in Lisp: Know-how -

Reblocks

Reblocks is a widgets-based and server-based framework +

Reblocks

Reblocks is a widgets-based and server-based framework with a built-in ajax update mechanism. It allows to write dynamic web applications without the need to write JavaScript or to write lisp code that would transpile to JavaScript. It is thus super -exciting. It isn’t for newcomers however.

The Reblocks’s demo we will build is a TODO app:

-

Note

To install Reblocks, please see its documentation.

Reblocks’ unit of work is the widget. They look like a class definition:

(defwidget task ()
+exciting. It isn’t for newcomers however.

The Reblocks’s demo we will build is a TODO app:

+

Note

To install Reblocks, please see its documentation.

Reblocks’ unit of work is the widget. They look like a class definition:

(defwidget task ()
    ((title
      :initarg :title
      :accessor title)
@@ -36,12 +36,12 @@
 ...

The function make-js-action creates a simple javascript function that calls the lisp one on the server, and automatically refreshes the HTML of the widgets that need it. In our example, it re-renders one -task only.

Is it appealing ? Carry on its quickstart guide.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/search/index.html b/docs/search/index.html index 13d71a6..8419943 100644 --- a/docs/search/index.html +++ b/docs/search/index.html @@ -1,11 +1,11 @@ Search :: Web Apps in Lisp: Know-how -
\ No newline at end of file diff --git a/docs/searchindex.js b/docs/searchindex.js index b489ee6..4a08e4a 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -49,8 +49,8 @@ var relearn_searchindex = [ }, { "breadcrumb": "Building blocks", - "content": "How can we serve static assets?\nRemember from simple web server how Hunchentoot serves files from a www/ directory by default. We can change that.\nHunchentoot Use the create-folder-dispatcher-and-handler prefix directory function.\nFor example:\n(defun serve-static-assets () \"Let Hunchentoot serve static assets under the /src/static/ directory of your :myproject system. Then reference static assets with the /static/ URL prefix.\" (push (hunchentoot:create-folder-dispatcher-and-handler \"/static/\" (merge-pathnames \"src/static\" ;; starts without a / (asdf:system-source-directory :myproject))) ;; \u003c- myproject hunchentoot:*dispatch-table*)) and call it in the function that starts your application:\n(serve-static-assets) Now our project’s static files located under src/static/ are served with the /static/ prefix, access them like this:\n\u003cimg src=\"/static/img/banner.jpg\" /\u003e", - "description": "How can we serve static assets?\nRemember from simple web server how Hunchentoot serves files from a www/ directory by default. We can change that.\nHunchentoot Use the create-folder-dispatcher-and-handler prefix directory function.\nFor example:\n(defun serve-static-assets () \"Let Hunchentoot serve static assets under the /src/static/ directory of your :myproject system. Then reference static assets with the /static/ URL prefix.\" (push (hunchentoot:create-folder-dispatcher-and-handler \"/static/\" (merge-pathnames \"src/static\" ;; starts without a / (asdf:system-source-directory :myproject))) ;; \u003c- myproject hunchentoot:*dispatch-table*)) and call it in the function that starts your application:", + "content": "How can we serve static assets?\nRemember from simple web server how Hunchentoot serves files from a www/ directory by default. We can change that.\nHunchentoot Use the create-folder-dispatcher-and-handler prefix directory function.\nFor example:\n(defun serve-static-assets () \"Let Hunchentoot serve static assets under the /src/static/ directory of your :myproject system. Then reference static assets with the /static/ URL prefix.\" (push (hunchentoot:create-folder-dispatcher-and-handler \"/static/\" (asdf:system-relative-pathname :myproject \"src/static/\")) ;; ^^^ starts without a / hunchentoot:*dispatch-table*)) and call it in the function that starts your application:\n(serve-static-assets) Now our project’s static files located under src/static/ are served with the /static/ prefix. Access them like this:\n\u003cimg src=\"/static/img/banner.jpg\" /\u003e or\n\u003cscript src=\"/static/test.js\" type=\"text/javascript\"\u003e\u003c/script\u003e where the file src/static/test.js could be\nconsole.log(\"hello\");", + "description": "How can we serve static assets?\nRemember from simple web server how Hunchentoot serves files from a www/ directory by default. We can change that.\nHunchentoot Use the create-folder-dispatcher-and-handler prefix directory function.\nFor example:\n(defun serve-static-assets () \"Let Hunchentoot serve static assets under the /src/static/ directory of your :myproject system. Then reference static assets with the /static/ URL prefix.\" (push (hunchentoot:create-folder-dispatcher-and-handler \"/static/\" (asdf:system-relative-pathname :myproject \"src/static/\")) ;; ^^^ starts without a / hunchentoot:*dispatch-table*)) and call it in the function that starts your application:", "tags": [], "title": "Static assets", "uri": "/building-blocks/static/index.html" @@ -87,6 +87,14 @@ var relearn_searchindex = [ "title": "Sessions", "uri": "/building-blocks/session/index.html" }, + { + "breadcrumb": "Building blocks", + "content": "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.\nThey should specially work across route redirects. So, they are typically created in the web session.\nHandling them involves those steps:\ncreate 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:\n(ql:quickload '(\"hunchentoot\" \"djula\" \"easy-routes\")) We also introduce a local nickname, to shorten the use of hunchentoot to ht:\n(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:\n(defparameter *port* 9876) (defvar *server* nil \"Our Hunchentoot acceptor\") (defun start (\u0026key (port *port*)) (format t \"~\u0026Starting the web server on port ~a~\u0026\" 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.\nThe important bits are:\nwe 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) (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)) ;; \u003c---- 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:\n(flash \"warning\" \"You are liking Lisp\") It’s easy, it’s handy, mission solved. Next.\nDelete flash messages when they are rendered For this, we use Hunchentoot’s life cycle and CLOS-orientation:\n;; 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 the current request, delete the :flash object from the session.\nRender flash messages in templates Set up Djula templates Create a new flash-template.html file.\n(djula:add-template-directory \"./\") (defparameter *flash-template* (djula:compile-template* \"flash-template.html\")) 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.\nHTML template This is our template. We use Bulma CSS to pimp it up and to use its notification blocks.\n\u003c!DOCTYPE html\u003e \u003chtml\u003e \u003chead\u003e \u003cmeta charset=\"utf-8\"\u003e \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"\u003e \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e \u003ctitle\u003eWALK - flash messages\u003c/title\u003e \u003c!-- Bulma Version 1--\u003e \u003clink rel=\"stylesheet\" href=\"https://unpkg.com/bulma@1.0.2/css/bulma.min.css\" /\u003e \u003c/head\u003e \u003cbody\u003e \u003c!-- START NAV --\u003e \u003cnav class=\"navbar is-white\"\u003e \u003cdiv class=\"container\"\u003e \u003cdiv class=\"navbar-brand\"\u003e \u003ca class=\"navbar-item brand-text\" href=\"#\"\u003e Bulma Admin \u003c/a\u003e \u003cdiv class=\"navbar-burger burger\" data-target=\"navMenu\"\u003e \u003cspan\u003e\u003c/span\u003e \u003c/div\u003e \u003c/div\u003e \u003cdiv id=\"navMenu\" class=\"navbar-menu\"\u003e \u003cdiv class=\"navbar-start\"\u003e \u003ca class=\"navbar-item\" href=\"#\"\u003e Home \u003c/a\u003e \u003ca class=\"navbar-item\" href=\"#\"\u003e Orders \u003c/a\u003e \u003c/div\u003e \u003c/div\u003e \u003c/div\u003e \u003c/nav\u003e \u003c!-- END NAV --\u003e \u003cdiv class=\"container\"\u003e \u003cdiv class=\"columns\"\u003e \u003cdiv class=\"column is-6\"\u003e \u003ch3 class=\"title is-4\"\u003e Flash messages. \u003c/h3\u003e \u003cdiv\u003e Click \u003ca href=\"/tryflash/\"\u003e/tryflash/\u003c/a\u003e to access an URL that creates a flash message and redirects you here.\u003c/div\u003e {% for flash in flashes %} \u003cdiv class=\"notification {{ flash.first }}\"\u003e \u003cbutton class=\"delete\"\u003e\u003c/button\u003e {{ flash.rest }} \u003c/div\u003e {% endfor %} \u003c/div\u003e \u003c/div\u003e \u003c/div\u003e \u003c/body\u003e \u003cscript\u003e // JS snippet to click the delete button of the notifications. // see https://bulma.io/documentation/elements/notification/ document.addEventListener('DOMContentLoaded', () =\u003e { (document.querySelectorAll('.notification .delete') || []).forEach(($delete) =\u003e { const $notification = $delete.parentNode; $delete.addEventListener('click', () =\u003e { $notification.parentNode.removeChild($notification); }); }); }); \u003c/script\u003e \u003c/html\u003e Look at\n{% for flash in flashes %} where we render our flash messages.\nDjula allows us to write {{ flash.first }} and {{ flash.rest }} to call the Lisp functions on those objects.\nWe must now create a route that renders our template.\nRoutes The /flash/ URL is the demo endpoint:\n(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.\nIn your application, you must add this parameter in all the existing routes. To make this easier, you can:\nuse Djula’s 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: (defun render (template \u0026rest 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:\n(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.\nYou should see a default notification. Click the β€œ/tryflash/” URL and you’ll see a flash message, that is deleted after use.\nRefresh the page, and you won’t see the flash message again.", + "description": "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.\nThey should specially work across route redirects. So, they are typically created in the web session.\nHandling them involves those steps:\ncreate 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:", + "tags": [], + "title": "Flash messages", + "uri": "/building-blocks/flash-messages/index.html" + }, { "breadcrumb": "Building blocks", "content": "A quick reference.\nThese functions have to be used in the context of a web request.\nSet headers Use header-out to set headers, like this:\n(setf (hunchentoot:header-out \"HX-Trigger\") \"myEvent\") This sets the header of the current request.\nUSe headers-out (plural) to get an association list of headers:\nAn alist of the outgoing http headers not including the β€˜Set-Cookie’, β€˜Content-Length’, and β€˜Content-Type’ headers. Use the functions HEADER-OUT and (SETF HEADER-OUT) to modify this slot.\nGet headers Use the header-in* and headers-in* (plural) function:\nFunction: (header-in* name \u0026optional (request *request*)) Returns the incoming header with name NAME. NAME can be a keyword (recommended) or a string.\nheaders-in*:\nFunction: (headers-in* \u0026optional (request *request*)) Returns an alist of the incoming headers associated with the REQUEST object REQUEST.\nReference Find some more here:\nhttps://common-lisp-libraries.readthedocs.io/hunchentoot/#headers-in_1", @@ -233,8 +241,8 @@ var relearn_searchindex = [ }, { "breadcrumb": "", - "content": "Other tutorials:\nNeil Munro’s Clack/Lack/Ningle tutorial the Cookbook Project skeletons and demos:\ncl-cookieweb - a web project template Feather, a template for web application development, shows a functioning Hello World app with an HTML page, a JSON API, a passing test suite, a Postgres DB and DB migrations. Uses Qlot, Buildapp, SystemD for deployment. lisp-web-template-productlist, a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS. lisp-web-live-reload-example - a toy project to show how to interact with a running web app. Libraries:\nawesome-cl", - "description": "Other tutorials:\nNeil Munro’s Clack/Lack/Ningle tutorial the Cookbook Project skeletons and demos:\ncl-cookieweb - a web project template Feather, a template for web application development, shows a functioning Hello World app with an HTML page, a JSON API, a passing test suite, a Postgres DB and DB migrations. Uses Qlot, Buildapp, SystemD for deployment. lisp-web-template-productlist, a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS. lisp-web-live-reload-example - a toy project to show how to interact with a running web app. Libraries:", + "content": "Other tutorials:\nNeil Munro’s Clack/Lack/Ningle tutorial the Cookbook Project skeletons and demos:\ncl-cookieweb - a web project template lisp-web-template-productlist, a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS. lisp-web-live-reload-example - a toy project to show how to interact with a running web app. Libraries:\nawesome-cl", + "description": "Other tutorials:\nNeil Munro’s Clack/Lack/Ningle tutorial the Cookbook Project skeletons and demos:\ncl-cookieweb - a web project template lisp-web-template-productlist, a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS. lisp-web-live-reload-example - a toy project to show how to interact with a running web app. Libraries:\nawesome-cl", "tags": [], "title": "See also", "uri": "/see-also/index.html" diff --git a/docs/see-also/index.html b/docs/see-also/index.html index 79dc80e..116db1c 100644 --- a/docs/see-also/index.html +++ b/docs/see-also/index.html @@ -11,14 +11,14 @@ Neil Munro’s Clack/Lack/Ningle tutorial the Cookbook Project skeletons and demos: cl-cookieweb - a web project template lisp-web-template-productlist, a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS. lisp-web-live-reload-example - a toy project to show how to interact with a running web app. Libraries: awesome-cl">See also :: Web Apps in Lisp: Know-how -

See also

Other tutorials:

Project skeletons and demos:

  • cl-cookieweb - a web project template
  • lisp-web-template-productlist, +

Libraries:

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 13d9c24..b5cda15 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -1 +1 @@ -http://example.org/tutorial/getting-started/index.htmlhttp://example.org/tutorial/first-route/index.htmlhttp://example.org/tutorial/first-template/index.htmlhttp://example.org/tutorial/index.htmlhttp://example.org/building-blocks/index.htmlhttp://example.org/building-blocks/simple-web-server/index.htmlhttp://example.org/building-blocks/static/index.htmlhttp://example.org/building-blocks/routing/index.htmlhttp://example.org/tutorial/first-path-parameter/index.htmlhttp://example.org/building-blocks/templates/index.htmlhttp://example.org/building-blocks/session/index.htmlhttp://example.org/building-blocks/headers/index.htmlhttp://example.org/building-blocks/errors-interactivity/index.htmlhttp://example.org/isomorphic-web-frameworks/weblocks/index.htmlhttp://example.org/tutorial/first-url-parameters/index.htmlhttp://example.org/building-blocks/database/index.htmlhttp://example.org/building-blocks/user-log-in/index.htmlhttp://example.org/building-blocks/users-and-passwords/index.htmlhttp://example.org/building-blocks/form-validation/index.htmlhttp://example.org/building-blocks/building-binaries/index.htmlhttp://example.org/isomorphic-web-frameworks/clog/index.htmlhttp://example.org/building-blocks/deployment/index.htmlhttp://example.org/tutorial/first-form/index.htmlhttp://example.org/building-blocks/remote-debugging/index.htmlhttp://example.org/tutorial/first-bonus-css/index.htmlhttp://example.org/building-blocks/electron/index.htmlhttp://example.org/tutorial/first-build/index.htmlhttp://example.org/building-blocks/web-views/index.htmlhttp://example.org/isomorphic-web-frameworks/index.htmlhttp://example.org/see-also/index.htmlhttp://example.org/categories/index.htmlhttp://example.org/tags/index.html \ No newline at end of file +http://example.org/tutorial/getting-started/index.htmlhttp://example.org/tutorial/first-route/index.htmlhttp://example.org/tutorial/first-template/index.htmlhttp://example.org/tutorial/index.htmlhttp://example.org/building-blocks/index.htmlhttp://example.org/building-blocks/simple-web-server/index.htmlhttp://example.org/building-blocks/static/index.htmlhttp://example.org/building-blocks/routing/index.htmlhttp://example.org/tutorial/first-path-parameter/index.htmlhttp://example.org/building-blocks/templates/index.htmlhttp://example.org/building-blocks/session/index.htmlhttp://example.org/building-blocks/flash-messages/index.htmlhttp://example.org/building-blocks/headers/index.htmlhttp://example.org/building-blocks/errors-interactivity/index.htmlhttp://example.org/isomorphic-web-frameworks/weblocks/index.htmlhttp://example.org/tutorial/first-url-parameters/index.htmlhttp://example.org/building-blocks/database/index.htmlhttp://example.org/building-blocks/user-log-in/index.htmlhttp://example.org/building-blocks/users-and-passwords/index.htmlhttp://example.org/building-blocks/form-validation/index.htmlhttp://example.org/building-blocks/building-binaries/index.htmlhttp://example.org/isomorphic-web-frameworks/clog/index.htmlhttp://example.org/building-blocks/deployment/index.htmlhttp://example.org/tutorial/first-form/index.htmlhttp://example.org/building-blocks/remote-debugging/index.htmlhttp://example.org/tutorial/first-bonus-css/index.htmlhttp://example.org/building-blocks/electron/index.htmlhttp://example.org/tutorial/first-build/index.htmlhttp://example.org/building-blocks/web-views/index.htmlhttp://example.org/isomorphic-web-frameworks/index.htmlhttp://example.org/see-also/index.htmlhttp://example.org/categories/index.htmlhttp://example.org/tags/index.html \ No newline at end of file diff --git a/docs/tags/index.html b/docs/tags/index.html index b41e9f3..62a865f 100644 --- a/docs/tags/index.html +++ b/docs/tags/index.html @@ -1,10 +1,10 @@ Tags :: Web Apps in Lisp: Know-how -

Tags

\ No newline at end of file diff --git a/docs/tutorial/demo-foo.lisp b/docs/tutorial/demo-foo.lisp new file mode 100644 index 0000000..2c84f66 --- /dev/null +++ b/docs/tutorial/demo-foo.lisp @@ -0,0 +1,51 @@ + +(defpackage :foo-for-ari + (:use :cl)) + +(in-package :foo-for-ari) + +(defun hello () + ;; C-c C-y + :hello) + +;; (defconstant +pi+ 3.14) ;; too cumbersome +;; (defparameter +pi+ 3.14) + +;; defvar: not modified again when using C-c C-c or (most importantly) C-c C-k or load +(defvar *server* nil + "Internal variable. Change with SETF.") + +(defvar *db-connection* nil) + +(with-db-connection (conn) + (let ((*db-connection* conn)) + …)) + +;; parameters are modified at each C-c C-c or C-c C-k or load +(defparameter *param* 2) + + +(let (foo bar) + (setf foo 1) + (print foo)) + +;; a LET scope +(let* ((foo 1) + (bar (1+ foo))) + (setf foo 3) + (print foo) + (print bar)) +;; out of the LET scope +(print foo) + +;; bind global ("special") variables/parameters +(let ((*param* 333)) + (function-using-param) + (format t "*param* is: ~a" *param*)) + +(flet ()) ;; = let + +(labels ()) ;; = let* for functions + +;; globals are thread-local: don't worry. +;; (see: Hunchentoot uses *globals* for the current request, etc) diff --git a/docs/tutorial/first-bonus-css/index.html b/docs/tutorial/first-bonus-css/index.html index 15b5755..951f4a5 100644 --- a/docs/tutorial/first-bonus-css/index.html +++ b/docs/tutorial/first-bonus-css/index.html @@ -7,7 +7,7 @@ Optionally, we may write one
with a class="container" attribute, to have better margins.'>Bonus: pimp your CSS :: Web Apps in Lisp: Know-how -

Bonus: pimp your CSS

Bonus: pimp your CSS

Don’t ask a web developer to help you with the look and feel of the +

Bonus: pimp your CSS

Bonus: pimp your CSS

Don’t ask a web developer to help you with the look and feel of the app, they will bring in hundreds of megabytes of Nodejs dependencies :S We suggest a (nearly) one-liner to get a decent CSS with no efforts: by using a class-less CSS, such as @@ -47,12 +47,12 @@ ")

Refresh http://localhost:8899/?query=one. Do you enjoy the difference?!

I see this:

Β 

However note how our root template is benefiting from the CSS, and the product page isn’t. The two pages should inherit from a base template. It’s about time we setup our templates in their own -directory.

\ No newline at end of file diff --git a/docs/tutorial/first-build/index.html b/docs/tutorial/first-build/index.html index e827014..09b1767 100644 --- a/docs/tutorial/first-build/index.html +++ b/docs/tutorial/first-build/index.html @@ -7,7 +7,7 @@ Info We didn’t have to restart any Lisp process, nor any web server.">the first build :: Web Apps in Lisp: Know-how -

the first build

We have developped a web app in Common Lisp.

At the first step, we opened a REPL and since then, without noticing, +

the first build

We have developped a web app in Common Lisp.

At the first step, we opened a REPL and since then, without noticing, we have compiled variables and function definitions pieces by pieces, step by step, with keyboard shortcuts giving immediate feedback, testing our progress on the go, running a function in the REPL or @@ -224,12 +224,12 @@ self-contained by default, without extra work: it contains the lisp implementation with its compiler and debugger, the libraries (web server and all), the templates. We can easily deploy this -app. Congrats!

Closing words

We are only scratching the surface of what we’ll want to do with a real app:

  • parse CLI args
  • handle a C-c and other signals
  • read configuration files
  • setup and use a database
  • properly use HTML templates
  • add CSS and other static assets
  • add interactivity on the web page, with or without JavaScript
  • add users login
  • add rights to the routes
  • build a REST API
  • etc

We will explore those topics in the other chapters of this guide.

We did a great job for a first app:

  • we built a Common Lisp web app
  • we created routes, used path and URL parameters
  • we defined an HTML form
  • we used HTML templates
  • we experienced the interactive nature of Common Lisp
  • we explored how to run, build and ship Common Lisp programs.

That’s a lot. You are ready for serious applications now!

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-form/index.html b/docs/tutorial/first-form/index.html index e49dd05..861dbf2 100644 --- a/docs/tutorial/first-form/index.html +++ b/docs/tutorial/first-form/index.html @@ -11,7 +11,7 @@ Do you find HTML forms boring, very boring tech? There is no escaping though, you must know the basics. Go read MDN. It’s only later that you’ll have the right to find and use libraries to do them for you. A search form Here’s a search form: (defparameter *template-root* "
") The important elements are the following:'>the first form :: Web Apps in Lisp: Know-how -

the first form

Our root page shows a list of products. We want to do better: provide +

the first form

Our root page shows a list of products. We want to do better: provide a search form.

Do you find HTML forms boring, very boring tech? There is no escaping though, you must know the basics. Go read MDN. It’s only later that you’ll have the right to find and use libraries to do them for you.

A search form

Here’s a search form:

(defparameter *template-root* "
 <form action=\"/\" method=\"GET\">
@@ -154,12 +154,12 @@
   (force-output)
   (setf *server* (make-instance 'easy-routes:easy-routes-acceptor
                                 :port (or port *port*)))
-  (hunchentoot:start *server*))

Do you also clearly see 3 different components in this app? Templates, models, routes.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-path-parameter/index.html b/docs/tutorial/first-path-parameter/index.html index f2eef2d..8973315 100644 --- a/docs/tutorial/first-path-parameter/index.html +++ b/docs/tutorial/first-path-parameter/index.html @@ -15,7 +15,7 @@ Each product detail will be available on the URL /product/n where n is the product ID. Add links To begin with, let’s add links to the list of products: (defparameter *template-root* " Lisp web app ") Carefully observe that, in the href, we had to escape the quotes :/ This is the shortcoming of defining Djula templates as strings in .lisp files. It’s best to move them to their own directory and own files.'>the first path parameter :: Web Apps in Lisp: Know-how -

the first path parameter

So far we have 1 route that displays all our products.

We will make each product line clickable, to open a new page, that will show more details.

Each product detail will be available on the URL /product/n where n is the product ID.

To begin with, let’s add links to the list of products:

(defparameter *template-root* "
+

the first path parameter

So far we have 1 route that displays all our products.

We will make each product line clickable, to open a new page, that will show more details.

Each product detail will be available on the URL /product/n where n is the product ID.

To begin with, let’s add links to the list of products:

(defparameter *template-root* "
 <title> Lisp web app </title>
 <body>
   <ul>
@@ -77,12 +77,12 @@
 
 (easy-routes:defroute product-route ("/product/:n") (&path (n 'integer))
   (render *template-product* :product (get-product n)))

That’s better. Usually all my routes have this form: name, arguments, -call to some sort of render function with a template and arguments.

I’d like to carry on with features but let’s have a word about URL parameters.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-route/index.html b/docs/tutorial/first-route/index.html index 783147b..fca7b23 100644 --- a/docs/tutorial/first-route/index.html +++ b/docs/tutorial/first-route/index.html @@ -15,7 +15,7 @@ Let’s start with a couple variables. ;; still in src/myproject.lisp (defvar *server* nil "Server instance (Hunchentoot acceptor).") (defparameter *port* 8899 "The application port.") Now hold yourself and let’s write our first route: (easy-routes:defroute root ("/") () "hello app") It only returns a string. We’ll use HTML templates in a second.'>the first route :: Web Apps in Lisp: Know-how -

the first route

It’s time we create a web app!

Our first route

Our very first route will only respond with a “hello world”.

Let’s start with a couple variables.

;; still in src/myproject.lisp
+

the first route

It’s time we create a web app!

Our first route

Our very first route will only respond with a “hello world”.

Let’s start with a couple variables.

;; still in src/myproject.lisp
 (defvar *server* nil
   "Server instance (Hunchentoot acceptor).")
 
@@ -38,12 +38,12 @@
 never have to wait for stuff re-compiling or re-loading. You’ll
 compile your code with a shortcut and have instant feedback. SBCL
 warns us on bad syntax, undefined variables and other typos, some type
-mismatches, unreachable code (meaning we might have an issue), etc.

To stop the app, use (hunchentoot:stop *server*). You can put this in a “stop-app” function.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-template/index.html b/docs/tutorial/first-template/index.html index 7c405fa..bb38485 100644 --- a/docs/tutorial/first-template/index.html +++ b/docs/tutorial/first-template/index.html @@ -15,7 +15,7 @@ We’ll use Djula HTML templates. Usually, templates go into their templates/ directory. But, we will start out by defining our first template inside our .lisp file: ;; scr/myproject.lisp (defparameter *template-root* " Lisp web app hello app ") Compile this variable as you go, with C-c C-c (or call again load from any REPL).'>the first template :: Web Apps in Lisp: Know-how -

the first template

Our route only returns a string:

(easy-routes:defroute root ("/") ()
+

the first template

Our route only returns a string:

(easy-routes:defroute root ("/") ()
     "hello app")

Can we have it return a template?

We’ll use Djula HTML templates.

Usually, templates go into their templates/ directory. But, we will start out by defining our first template inside our .lisp file:

;; scr/myproject.lisp
 (defparameter *template-root* "
@@ -131,12 +131,12 @@
   (force-output)
   (setf *server* (make-instance 'easy-routes:easy-routes-acceptor
                                 :port (or port *port*)))
-  (hunchentoot:start *server*))
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-url-parameters/index.html b/docs/tutorial/first-url-parameters/index.html index 29b0105..5eb5a67 100644 --- a/docs/tutorial/first-url-parameters/index.html +++ b/docs/tutorial/first-url-parameters/index.html @@ -15,7 +15,7 @@ We want a debug URL parameter that will show us more data. The URL will accept a ?debug=t part. We have this product route: (easy-routes:defroute product-route ("/product/:n") (&path (n 'integer)) (render *template-product* :product (get-product n))) With or without the &path part, either is good for now, so let’s remove it for clarity:'>the first URL parameter :: Web Apps in Lisp: Know-how -

the first URL parameter

As of now we can access URLs like these: +

the first URL parameter

As of now we can access URLs like these: http://localhost:8899/product/0 where 0 is a path parameter.

We will soon need URL parameters so let’s add one to our routes.

We want a debug URL parameter that will show us more data. The URL will accept a ?debug=t part.

We have this product route:

(easy-routes:defroute product-route ("/product/:n") (&path (n 'integer))
@@ -90,12 +90,12 @@
                                 :port port))
   (hunchentoot:start *server*))

Everything is contained in one file, and we can run everything from sources or we can build a self-contained -binary. Pretty cool!

Before we do so, we’ll add a great feature: searching for products.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/getting-started/index.html b/docs/tutorial/getting-started/index.html index 70eaf80..642274c 100644 --- a/docs/tutorial/getting-started/index.html +++ b/docs/tutorial/getting-started/index.html @@ -11,7 +11,7 @@ define a list of products, stored in a dummy database, we’ll have an index page with a search form, we’ll display search results, and have one page per product to see it in details. But everything starts with a project (a system in Lisp parlance) definition. Setting up the project Let’s create a regular Common Lisp project. We could start straihgt from the REPL, but we’ll need a project definition to save our project dependencies, and other metadata. Such a project is an ASDF file.">Getting Started :: Web Apps in Lisp: Know-how -

Getting Started

In this application we will:

  • define a list of products, stored in a dummy database,
  • we’ll have an index page with a search form,
  • we’ll display search results,
  • and have one page per product to see it in details.

But everything starts with a project (a system in Lisp parlance) definition.

Setting up the project

Let’s create a regular Common Lisp project.

We could start straihgt from the REPL, but we’ll need a project +

Getting Started

In this application we will:

  • define a list of products, stored in a dummy database,
  • we’ll have an index page with a search form,
  • we’ll display search results,
  • and have one page per product to see it in details.

But everything starts with a project (a system in Lisp parlance) definition.

Setting up the project

Let’s create a regular Common Lisp project.

We could start straihgt from the REPL, but we’ll need a project definition to save our project dependencies, and other metadata. Such a project is an ASDF file.

Create the file myproject.asd at the project root:

(asdf:defsystem "myproject"
   :version "0.1"
@@ -65,12 +65,12 @@
         collect (list i
                       (format nil "Product nb ~a" i)
                       9.99)))

this returns:

((0 "Product nb 0" 9.99) (1 "Product nb 1" 9.99) (2 "Product nb 2" 9.99)
- (3 "Product nb 3" 9.99) (4 "Product nb 4" 9.99))

That’s not much, but that’s enough to show content in a web app, which we’ll do next.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/index.html b/docs/tutorial/index.html index 9ae1f90..ba891fc 100644 --- a/docs/tutorial/index.html +++ b/docs/tutorial/index.html @@ -11,17 +11,17 @@ In this first tutorial we will build a simple app that shows a web form that will search and display a list of products. In doing so, we will see many necessary building blocks to write web apps in Lisp: how to start a server how to create routes how to define and use path and URL parameters how to define HTML templates how to run and build the app, from our editor and from the terminal. In doing so, we’ll experience the interactive nature of Common Lisp and we’ll learn important commands: running sbcl from the command line with a couple options, what is load, how to interactively compile our app with a keyboard shortcut, how to structure a project with an .asd definition and a package, etc.">Tutorial part 1 :: Web Apps in Lisp: Know-how -

Tutorial part 1

Are you ready to build a web app in Common Lisp?

In this first tutorial we will build a simple app that shows a web +

Tutorial part 1

Are you ready to build a web app in Common Lisp?

In this first tutorial we will build a simple app that shows a web form that will search and display a list of products.

In doing so, we will see many necessary building blocks to write web apps in Lisp:

  • how to start a server
  • how to create routes
  • how to define and use path and URL parameters
  • how to define HTML templates
  • how to run and build the app, from our editor and from the terminal.

In doing so, we’ll experience the interactive nature of Common Lisp and we’ll learn important commands: running sbcl from the command line with a couple options, what is load, how to interactively compile our app with a keyboard shortcut, how to structure a project -with an .asd definition and a package, etc.

We expect that you have Quicklisp installed. If not, please see the Cookbook: getting started.

Now let’s get started.

But beware. There is no going back.

(look at the menu and don’t miss the small previous-next arrows on the top right)

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/myproject.asd b/docs/tutorial/myproject.asd index cb2dbcd..13b14e0 100644 --- a/docs/tutorial/myproject.asd +++ b/docs/tutorial/myproject.asd @@ -1,9 +1,3 @@ - - -;; this is from -;; https://web-apps-in-lisp.github.io/tutorial/getting-started/index.html -;; -;; C-c C-k (asdf:defsystem "myproject" :version "0.1" :author "me" diff --git a/docs/tutorial/quicktry.lisp b/docs/tutorial/quicktry.lisp new file mode 100644 index 0000000..001ac6c --- /dev/null +++ b/docs/tutorial/quicktry.lisp @@ -0,0 +1,5 @@ + +(defpackage :foo-test-2 + (:use :cl)) + +(IN-PACKAGE :foo-test-2) diff --git a/docs/tutorial/src/static/test.js b/docs/tutorial/src/static/test.js new file mode 100644 index 0000000..702f428 --- /dev/null +++ b/docs/tutorial/src/static/test.js @@ -0,0 +1 @@ +console.log("hello"); From 7871b075f41527b87000e5ce1d1c3642ae9b8c8a Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 13 Mar 2025 14:04:19 +0100 Subject: [PATCH 06/20] flash: link to full code on GH --- content/building-blocks/flash-messages.md | 2 ++ docs/building-blocks/flash-messages/index.html | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/content/building-blocks/flash-messages.md b/content/building-blocks/flash-messages.md index 080c61f..11f2e20 100644 --- a/content/building-blocks/flash-messages.md +++ b/content/building-blocks/flash-messages.md @@ -267,3 +267,5 @@ 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 diff --git a/docs/building-blocks/flash-messages/index.html b/docs/building-blocks/flash-messages/index.html index c58cfb1..6acf2b4 100644 --- a/docs/building-blocks/flash-messages/index.html +++ b/docs/building-blocks/flash-messages/index.html @@ -138,12 +138,12 @@ (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.

\ No newline at end of file From a92253f16d05f6fd459d09df5457750bbe08ba2a Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 20 Mar 2025 12:11:49 +0100 Subject: [PATCH 07/20] flash messages: discuss API calls, Fetch "omit" cookies --- content/building-blocks/flash-messages.md | 55 ++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/content/building-blocks/flash-messages.md b/content/building-blocks/flash-messages.md index 11f2e20..824ba85 100644 --- a/content/building-blocks/flash-messages.md +++ b/content/building-blocks/flash-messages.md @@ -99,7 +99,18 @@ For this, we use Hunchentoot's life cycle and CLOS-orientation: (ht:delete-session-value :flash)) ``` -which means: after we have handled the current request, delete the `:flash` object from the session. +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 @@ -269,3 +280,45 @@ 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. From 948d874489cf0ae4ce828e37f8e1909cf976e6fa Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 20 Mar 2025 12:14:16 +0100 Subject: [PATCH 08/20] publish --- .../building-blocks/flash-messages/index.html | 35 ++++++++++++++----- .../building-blocks/flash-template/index.html | 8 ++--- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/docs/building-blocks/flash-messages/index.html b/docs/building-blocks/flash-messages/index.html index 6acf2b4..26b5fcf 100644 --- a/docs/building-blocks/flash-messages/index.html +++ b/docs/building-blocks/flash-messages/index.html @@ -10,11 +10,11 @@ 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:">Flash messages :: Web Apps in Lisp: Know-how -

Flash messages

Flash messages are temporary messages you want to show to your +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:">Flash messages :: Web Apps in Lisp: Know-how +

Flash messages

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 +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:

(ql:quickload '("hunchentoot" "djula" "easy-routes"))

We also introduce a local nickname, to shorten the use of hunchentoot to ht:

(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:

(defparameter *port* 9876)
 (defvar *server* nil "Our Hunchentoot acceptor")
@@ -40,7 +40,9 @@
           (cons (cons type message) flash))))

Now, inside any route, we can call this function to add a flash message to the session:

(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:

;; 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 the current request, delete the :flash object from the session.

Render flash messages in templates

Set up Djula templates

Create a new flash-template.html file.

(djula:add-template-directory "./")
+  (ht:delete-session-value :flash))

which means: after we have handled a request, delete the +:flash object from the session.

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.

Render flash messages in templates

Set up Djula templates

Create a new flash-template.html file.

(djula:add-template-directory "./")
 (defparameter *flash-template* (djula:compile-template* "flash-template.html"))
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 @@ -138,12 +140,29 @@ (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.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/flash-template/index.html b/docs/building-blocks/flash-template/index.html index 638b766..9293c18 100644 --- a/docs/building-blocks/flash-template/index.html +++ b/docs/building-blocks/flash-template/index.html @@ -1,13 +1,13 @@ -

WALK - flash messages +

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 %}
\ No newline at end of file + 
\ No newline at end of file From bce8772e49f1e9b90dfbc602dd8c195e1db3db4c Mon Sep 17 00:00:00 2001 From: vindarel Date: Sat, 22 Mar 2025 09:41:34 +0100 Subject: [PATCH 09/20] templates: warn add-template-directory type error --- content/building-blocks/templates.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 From cba120780fc0bc91a639d0a06d472d748f7890b1 Mon Sep 17 00:00:00 2001 From: vindarel Date: Sat, 22 Mar 2025 09:43:28 +0100 Subject: [PATCH 10/20] build --- docs/building-blocks/templates/index.html | 26 ++++++++++++++--------- docs/building-blocks/templates/index.xml | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/building-blocks/templates/index.html b/docs/building-blocks/templates/index.html index 652a8a1..a81e7ea 100644 --- a/docs/building-blocks/templates/index.html +++ b/docs/building-blocks/templates/index.html @@ -2,23 +2,29 @@ Install it if you didn’t already: (ql:quickload "djula") The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like: (djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/")) and then we can declare and compile the ones we use, for example:: -(defparameter +base.html+ (djula:compile-template* "base.html")) (defparameter +products.html+ (djula:compile-template* "products.html")) A Djula template looks like this:'>Templates :: Web Apps in Lisp: Know-how -

Templates

Djula - HTML markup

Djula is a port of Python’s +(defparameter +base.html+ (djula:compile-template* "base.html")) (defparameter +products.html+ (djula:compile-template* "products.html")) Info If you get an error message when calling add-template-directory like this'>Templates :: Web Apps in Lisp: Know-how +

Templates

Djula - HTML markup

Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation.

Install it if you didn’t already:

(ql:quickload "djula")

The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like:

(djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/"))

and then we can declare and compile the ones we use, for example::

(defparameter +base.html+ (djula:compile-template* "base.html"))
-(defparameter +products.html+ (djula:compile-template* "products.html"))

A Djula template looks like this:

{% extends "base.html" %}
+(defparameter +products.html+ (djula:compile-template* "products.html"))
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/.

A Djula template looks like this:

{% extends "base.html" %}
 {% block title %} Products page {% endblock %}
 {% block content %}
   <ul>
@@ -46,7 +52,7 @@
                           nil
                           :products (products)

Djula is, along with its companion access library, one of -the most downloaded libraries of Quicklisp.

Djula filters

Filters are only waiting for the developers to define their own, so we should have a work about them.

They allow to modify how a variable is displayed. Djula comes with +the most downloaded libraries of Quicklisp.

Djula filters

Filters are only waiting for the developers to define their own, so we should have a word about them.

They allow to modify how a variable is displayed. Djula comes with a good set of built-in filters and they are well documented. They are not to be confused with tags.

They look like this: {{ var | lower }}, where lower is an existing filter, which renders the text into lowercase.

Filters sometimes take arguments. For example: {{ var | add:2 }} calls the add filter with arguments var and 2.

Moreover, it is very easy to define custom filters. All we have to do @@ -64,12 +70,12 @@ (:ol (dolist (item *shopping-list*) (:li (1+ (random 10)) item)))) (:footer ("Last login: ~A" *last-login*)))

I find Spinneret easier to use than the more famous cl-who, but I -personnally prefer to use HTML templates.

Spinneret has nice features under it sleeves:

  • it warns on invalid tags and attributes
  • it can automatically number headers, given their depth
  • it pretty prints html per default, with control over line breaks
  • it understands embedded markdown
  • it can tell where in the document a generator function is (see get-html-tag)
\ No newline at end of file diff --git a/docs/building-blocks/templates/index.xml b/docs/building-blocks/templates/index.xml index 67419f9..07b2e29 100644 --- a/docs/building-blocks/templates/index.xml +++ b/docs/building-blocks/templates/index.xml @@ -2,4 +2,4 @@ Install it if you didn’t already: (ql:quickload "djula") The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like: (djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/")) and then we can declare and compile the ones we use, for example:: -(defparameter +base.html+ (djula:compile-template* "base.html")) (defparameter +products.html+ (djula:compile-template* "products.html")) A Djula template looks like this:Hugoen-us \ No newline at end of file +(defparameter +base.html+ (djula:compile-template* "base.html")) (defparameter +products.html+ (djula:compile-template* "products.html")) Info If you get an error message when calling add-template-directory like thisHugoen-us \ No newline at end of file From 2a99faec00176c8dada9727578ac455d865069af Mon Sep 17 00:00:00 2001 From: vindarel Date: Wed, 7 May 2025 19:03:05 +0200 Subject: [PATCH 11/20] users passwords links++ --- content/building-blocks/database.md | 2 +- .../building-blocks/users-and-passwords.md | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) 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/users-and-passwords.md b/content/building-blocks/users-and-passwords.md index fd657a6..363e606 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 either write some glue Lisp code. -{{% notice info %}} +You can also turn to an external tool (such as Keycloak) 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. From ff63d78c9097bbbc0f7e800b1207d047f00b7e9d Mon Sep 17 00:00:00 2001 From: vindarel Date: Wed, 7 May 2025 19:03:44 +0200 Subject: [PATCH 12/20] minor build++ --- content/tutorial/first-build.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 From d3b51c32bf61ab75cb303352b88ba5ac318d520a Mon Sep 17 00:00:00 2001 From: vindarel Date: Wed, 7 May 2025 19:06:56 +0200 Subject: [PATCH 13/20] build --- docs/404.html | 2 +- .../building-binaries/index.html | 8 ++--- docs/building-blocks/database/index.html | 12 +++---- docs/building-blocks/deployment/index.html | 8 ++--- docs/building-blocks/electron/index.html | 12 +++---- .../errors-interactivity/index.html | 16 ++++----- docs/building-blocks/flash-messages.lisp | 26 ++++++++++++-- .../building-blocks/flash-messages/index.html | 12 +++---- .../building-blocks/flash-template/index.html | 8 ++--- .../form-validation/index.html | 8 ++--- docs/building-blocks/headers/index.html | 8 ++--- docs/building-blocks/index.html | 8 ++--- docs/building-blocks/index.xml | 7 ++-- .../remote-debugging/index.html | 8 ++--- docs/building-blocks/routing/index.html | 8 ++--- docs/building-blocks/session/index.html | 8 ++--- .../simple-web-server/index.html | 8 ++--- docs/building-blocks/static/index.html | 8 ++--- docs/building-blocks/templates/index.html | 10 +++--- docs/building-blocks/user-log-in/index.html | 12 +++---- .../users-and-passwords/index.html | 36 ++++++++++--------- .../users-and-passwords/index.xml | 5 +-- docs/building-blocks/web-views/index.html | 12 +++---- docs/categories/index.html | 6 ++-- docs/css/chroma-auto.css | 4 +-- docs/css/format-print.css | 4 +-- docs/css/print.css | 2 +- docs/css/swagger.css | 4 +-- docs/css/theme-auto.css | 4 +-- docs/css/theme.css | 2 +- docs/index.html | 14 ++++---- .../isomorphic-web-frameworks/clog/index.html | 16 ++++----- docs/isomorphic-web-frameworks/index.html | 8 ++--- .../weblocks/index.html | 12 +++---- docs/search/index.html | 8 ++--- docs/searchindex.js | 12 +++---- docs/see-also/index.html | 8 ++--- docs/tags/index.html | 6 ++-- docs/tutorial/first-bonus-css/index.html | 8 ++--- docs/tutorial/first-build/index.html | 8 ++--- docs/tutorial/first-form/index.html | 8 ++--- docs/tutorial/first-path-parameter/index.html | 8 ++--- docs/tutorial/first-route/index.html | 8 ++--- docs/tutorial/first-template/index.html | 8 ++--- docs/tutorial/first-url-parameters/index.html | 8 ++--- docs/tutorial/getting-started/index.html | 8 ++--- docs/tutorial/index.html | 8 ++--- docs/tutorial/templates/base/index.html | 10 ++++++ docs/tutorial/templates/base/index.xml | 1 + 49 files changed, 240 insertions(+), 203 deletions(-) create mode 100644 docs/tutorial/templates/base/index.html create mode 100644 docs/tutorial/templates/base/index.xml diff --git a/docs/404.html b/docs/404.html index e08f188..7679e3b 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 Β―\_(ツ)_/Β―.

Go to homepage

\ No newline at end of file +

44

Not found

Whoops. Looks like this page doesn't exist Β―\_(ツ)_/Β―.

Go to homepage

\ No newline at end of file diff --git a/docs/building-blocks/building-binaries/index.html b/docs/building-blocks/building-binaries/index.html index 71a52e7..295cbad 100644 --- a/docs/building-blocks/building-binaries/index.html +++ b/docs/building-blocks/building-binaries/index.html @@ -11,7 +11,7 @@ To run our Lisp code from source, as a script, we can use the --load switch from our implementation. We must ensure: to load the project’s .asd system declaration (if any) to install the required dependencies (this demands we have installed Quicklisp previously) and to run our application’s entry point. So, the recipe to run our project from sources can look like this (you can find such a recipe in our project generator):">Running and Building :: Web Apps in Lisp: Know-how -

Running and Building

Running the application from source

Info

See the tutorial.

To run our Lisp code from source, as a script, we can use the --load +

Running and Building

Running the application from source

Info

See the tutorial.

To run our Lisp code from source, as a script, we can use the --load switch from our implementation.

We must ensure:

  • to load the project’s .asd system declaration (if any)
  • to install the required dependencies (this demands we have installed Quicklisp previously)
  • and to run our application’s entry point.

So, the recipe to run our project from sources can look like this (you can find such a recipe in our project generator):

;; run.lisp
 
 (load "myproject.asd")
@@ -40,12 +40,12 @@
 stops. That happens because the server thread is started in the background, and nothing tells the binary to wait for it. We can simply sleep (for a large-enough amount of time).

(defun main ()
   (start-app :port 9003) ;; our start-app
   ;; keep the binary busy in the foreground, for binaries and SystemD.
-  (sleep most-positive-fixnum))

If you want to learn more, see… the Cookbook: scripting, command line arguments, executables.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/database/index.html b/docs/building-blocks/database/index.html index b956828..97f743d 100644 --- a/docs/building-blocks/database/index.html +++ b/docs/building-blocks/database/index.html @@ -6,8 +6,8 @@ do you have an existing database you want to read data from? do you want to create a new database? do you prefer CLOS orientation or to write SQL queries? We have many libraries to work with databases, let’s have a recap first. The #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:">Connecting to a database :: Web Apps in Lisp: Know-how -

Connecting to a database

Let’s study different use cases:

  • do you have an existing database you want to read data from?
  • do you want to create a new database?
    • do you prefer CLOS orientation
    • or to write SQL queries?

We have many libraries to work with databases, let’s have a recap first.

The #database section on the awesome-cl list +The #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:">Connecting to a database :: Web Apps in Lisp: Know-how +

Connecting to a database

Let’s study different use cases:

  • do you have an existing database you want to read data from?
  • do you want to create a new database?
    • do you prefer CLOS orientation
    • or to write SQL queries?

We have many libraries to work with databases, let’s have a recap first.

The #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:

  • wrappers to one database engine (cl-sqlite, postmodern, cl-redis, cl-duckdb…),
  • ORMs (Mito),
  • interfaces to several DB engines (cl-dbi, cl-yesql…),
  • lispy SQL syntax (sxql…)
  • in-memory persistent object databases (bknr.datastore, cl-prevalence,…),
  • graph databases in pure Lisp (AllegroGraph, vivace-graph) or wrappers (neo4cl),
  • object stores (cl-store, cl-naive-store…)
  • and other tools (pgloader, which was re-written from Python to Common Lisp).

Table of Contents

How to query an existing database

Let’s say you have a database called db.db and you want to extract data from it.

For our example, quickload this library:

(ql:quickload "cl-dbi")

cl-dbi can connect to major @@ -61,17 +61,17 @@ (email :col-type (or (:varchar 128) :null))))

Once we create the table, we can create and insert user rows with methods such as create-dao:

(mito:create-dao 'user :name "Eitaro Fukamachi" :email "e.arrows@gmail.com")

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.

How to integrate the databases into the web frameworks

The web frameworks / web servers we use in this guide do not need +documentation and to the Cookbook (links below).

How to integrate the databases into the web frameworks

The web frameworks / web servers we use in this guide do not need anything special. Just use a DB driver and fetch results in your routes.

Using a DB connection per request with dbi:with-connection is a good idea.

Bonus: pimp your SQLite

SQLite is a great database. It loves backward compatibility. As such, its default settings may not be optimal for a web application seeing some load. You might want to set some PRAGMA -statements (SQLite settings).

To set them, look at your DB driver how to run a raw SQL query.

With cl-dbi, this would be dbi:do-sql:

(dbi:do-sql *connection* "PRAGMA cache_size = -20000;")

Here’s a nice list of pragmas useful for web development:

References

\ No newline at end of file diff --git a/docs/building-blocks/deployment/index.html b/docs/building-blocks/deployment/index.html index ac87d0d..77e36f3 100644 --- a/docs/building-blocks/deployment/index.html +++ b/docs/building-blocks/deployment/index.html @@ -15,7 +15,7 @@ Deploying manually We can start our executable in a shell and send it to the background (C-z bg), or run it inside a tmux session. These are not the best but hey, it worksΒ©. Here’s a tmux crashcourse: start a tmux session with tmux inside a session, C-b is tmux’s modifier key. use C-b c to create a new tab, C-b n and C-b p for β€œnext” and β€œprevious” tab/window. use C-b d to detach tmux and come back to your original console. Everything you started in tmux still runs in the background. use C-g to cancel a current prompt (as in Emacs). tmux ls lists the running tmux sessions. tmux attach goes back to a running session. tmux attach -t attaches to the session named β€œname”. inside a session, use C-b $ to name the current session, so you can see it with tmux ls. Here’s a cheatsheet that was handy.">Deployment :: Web Apps in Lisp: Know-how -

Deployment

How to deploy and monitor a Common Lisp web app?

Info

We are re-using content we contributed to the Cookbook.

Deploying manually

We can start our executable in a shell and send it to the background (C-z bg), or run it inside a tmux session. These are not the best but hey, it worksΒ©.

Here’s a tmux crashcourse:

  • start a tmux session with tmux
  • inside a session, C-b is tmux’s modifier key.
    • use C-b c to create a new tab, C-b n and C-b p for “next” and “previous” tab/window.
    • use C-b d to detach tmux and come back to your original console. Everything you started in tmux still runs in the background.
    • use C-g to cancel a current prompt (as in Emacs).
  • tmux ls lists the running tmux sessions.
  • tmux attach goes back to a running session.
    • tmux attach -t <name> attaches to the session named “name”.
    • inside a session, use C-b $ to name the current session, so you can see it with tmux ls.

Here’s a cheatsheet that was handy.

Unfortunately, if your app crashes or if your server is rebooted, your apps will be stopped. We can do better.

SystemD: daemonizing, restarting in case of crashes, handling logs

This is actually a system-specific task. See how to do that on your system.

Most GNU/Linux distros now come with Systemd, so here’s a little example.

Deploying an app with Systemd is as simple as writing a configuration file:

$ emacs -nw /etc/systemd/system/my-app.service
+

Deployment

How to deploy and monitor a Common Lisp web app?

Info

We are re-using content we contributed to the Cookbook.

Deploying manually

We can start our executable in a shell and send it to the background (C-z bg), or run it inside a tmux session. These are not the best but hey, it worksΒ©.

Here’s a tmux crashcourse:

  • start a tmux session with tmux
  • inside a session, C-b is tmux’s modifier key.
    • use C-b c to create a new tab, C-b n and C-b p for “next” and “previous” tab/window.
    • use C-b d to detach tmux and come back to your original console. Everything you started in tmux still runs in the background.
    • use C-g to cancel a current prompt (as in Emacs).
  • tmux ls lists the running tmux sessions.
  • tmux attach goes back to a running session.
    • tmux attach -t <name> attaches to the session named “name”.
    • inside a session, use C-b $ to name the current session, so you can see it with tmux ls.

Here’s a cheatsheet that was handy.

Unfortunately, if your app crashes or if your server is rebooted, your apps will be stopped. We can do better.

SystemD: daemonizing, restarting in case of crashes, handling logs

This is actually a system-specific task. See how to do that on your system.

Most GNU/Linux distros now come with Systemd, so here’s a little example.

Deploying an app with Systemd is as simple as writing a configuration file:

$ emacs -nw /etc/systemd/system/my-app.service
 [Unit]
 Description=stupid simple example
 
@@ -61,12 +61,12 @@
 app on localhost.

Reload nginx (send the “reload” signal):

$ nginx -s reload
 

and that’s it: you can access your Lisp app from the outside through http://www.your-domain-name.org.

Deploying on Heroku, Digital Ocean, OVH, Deploy.sh and other services

See:

Cloud Init

You can take inspiration from this Cloud Init file for SBCL, an init file for providers supporting the cloudinit format (DigitalOcean etc).

Monitoring

See Prometheus.cl for a Grafana dashboard for SBCL and Hunchentoot metrics (memory, -threads, requests per second,…).

See cl-sentry-client for error reporting.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/electron/index.html b/docs/building-blocks/electron/index.html index 3c76050..036fa6a 100644 --- a/docs/building-blocks/electron/index.html +++ b/docs/building-blocks/electron/index.html @@ -11,7 +11,7 @@ Advise: study it before discarding it. It isn’t however the only portable web view solution. See our next section. Info This page appeared first on lisp-journey: three web views for Common Lisp, cross-platform GUIs.">Electron :: Web Apps in Lisp: Know-how -

Electron

Electron is heavy, but really cross-platform, and it has many tools +

Electron

Electron is heavy, but really cross-platform, and it has many tools around it. It allows to build releases for the three major OS from your development machine, its ecosystem has tools to handle updates, etc.

Advise: study it before discarding it.

It isn’t however the only portable web view solution. See our next section.

Ceramic (old but works)

Ceramic is a set of utilities @@ -33,8 +33,8 @@ localhost:[PORT] in Ceramic/Electron. That’s it.

Ceramic wasn’t updated in five years as of date and it downloads an outdated version of Electron by default (see (defparameter *electron-version* "5.0.2")), but you can change the version yourself.

The new Neomacs project, a structural editor and web browser, is a great modern example on how to use Ceramic. Give it a look and give it a try!

What Ceramic actually does is abstracted away in the CL functions, so I think it isn’t the best to start with. We can do without it to -understand the full process, here’s how.

Electron from scratch

Here’s our web app embedded in Electron:

-

Our steps are the following:

You can also run the Lisp web app from sources, of course, without +understand the full process, here’s how.

Electron from scratch

Here’s our web app embedded in Electron:

+

Our steps are the following:

You can also run the Lisp web app from sources, of course, without building a binary, but then you’ll have to ship all the lisp sources.

main.js

The most important file to an Electron app is the main.js. The one we show below does the following:

  • it starts Electron
  • it starts our web application on the side, as a child process, from a binary name, and a port.
  • it shows our child process’ stdout and stderr
  • it opens a browser window to show our app, running on localhost.
  • it handles the close event.

Here’s our version.

console.log(`Hello from Electron πŸ‘‹`)
 
 const { app, BrowserWindow } = require('electron')
@@ -119,12 +119,12 @@
 the binary into the Electron release.

Then, if you want to communicate from the Lisp app to the Electron window, and the other way around, you’ll have to use the JavaScript layers. Ceramic might help here.

What about Tauri?

Bundling an app with Tauri will, AFAIK (I just tried quickly), involve the same steps than with Electron. Tauri might -still have less tools for it. You need the Rust toolchain.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/errors-interactivity/index.html b/docs/building-blocks/errors-interactivity/index.html index 2296f62..8722885 100644 --- a/docs/building-blocks/errors-interactivity/index.html +++ b/docs/building-blocks/errors-interactivity/index.html @@ -7,19 +7,19 @@ not being interactive: it returns a 404 page and prints the error output on the REPL not interactive but developper friendly: it can show the lisp error message on the HTML page, as well as the full Lisp backtrace it can let the developper have the interactive debugger: in that case the framework doesn’t catch the error, it lets it pass through, and we the developper deal with it as in a normal Slime session. We see this by default:">Errors and interactivity :: Web Apps in Lisp: Know-how -

Errors and interactivity

In all frameworks, we can choose the level of interactivity.

The web framework can be interactive in different degrees. What should +

Errors and interactivity

In all frameworks, we can choose the level of interactivity.

The web framework can be interactive in different degrees. What should it do when an error happens?

  • not being interactive: it returns a 404 page and prints the error output on the REPL
  • not interactive but developper friendly: it can show the lisp error message on the HTML page,
  • as well as the full Lisp backtrace
  • it can let the developper have the interactive debugger: in that case the framework doesn’t catch the error, it lets it pass through, and -we the developper deal with it as in a normal Slime session.

We see this by default:

-

but we can also show backtraces and augment the data.

Hunchentoot

The global variables to set are

  • *show-lisp-errors-p*, nil by default
  • *show-lisp-backtraces-p*, t by default (but the backtrace won’t be shown if the previous setting is nil)
  • *catch-errors-p*, t by default, to set to nil to get the interactive debugger.

We can see the backtrace on our error page:

(setf hunchentoot:*show-lisp-errors-p* t)

-

Enhancing the backtrace in the browser

We can also the library hunchentoot-errors to augment the data we see in the stacktrace:

  • show the current request (URI, headers…)
  • show the current session (everything you stored in the session)

-

Clack errors

When you use the Clack web server, you can use Clack errors, which can also show the backtrace and the session, with a colourful output:

-

Hunchentoot’s conditions

Hunchentoot defines condition classes:

  • hunchentoot-warning, the superclass for all warnings
  • hunchentoot-error, the superclass for errors
  • parameter-error
  • bad-request

See the (light) documentation: https://edicl.github.io/hunchentoot/#conditions.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/flash-messages.lisp b/docs/building-blocks/flash-messages.lisp index 1945bc2..f50c426 100644 --- a/docs/building-blocks/flash-messages.lisp +++ b/docs/building-blocks/flash-messages.lisp @@ -33,6 +33,21 @@ ;;; delete flash after it is used. (defmethod ht:handle-request :after (acceptor request) + (log:warn "----- deleting flash messages") + ) + +(defmacro with-flash-messages ((messages) &body body) + `(let ((,messages (ht:session-value :flash))) + (prog1 + (progn + ,@body) + (ht:delete-session-value :flash)))) + +(with-flash-messages (msgs) + (print msgs)) + +(let ((messages (ht:session-value :flash))) + (print messages) (ht:delete-session-value :flash)) #+another-solution @@ -47,15 +62,20 @@ (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.")))) + (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."))))) #+another-solution (render *flash-template*) ) +(easy-routes:defroute flash-steal-route ("/api/steal/") () + "api result") + (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.") + (sleep 10) (ht:redirect "/flash/")) (defun start (&key (port *port*)) diff --git a/docs/building-blocks/flash-messages/index.html b/docs/building-blocks/flash-messages/index.html index 26b5fcf..f5909d9 100644 --- a/docs/building-blocks/flash-messages/index.html +++ b/docs/building-blocks/flash-messages/index.html @@ -11,10 +11,10 @@ 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:">Flash messages :: Web Apps in Lisp: Know-how -

Flash messages

Flash messages are temporary messages you want to show to your +

Flash messages

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 +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:

(ql:quickload '("hunchentoot" "djula" "easy-routes"))

We also introduce a local nickname, to shorten the use of hunchentoot to ht:

(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:

(defparameter *port* 9876)
 (defvar *server* nil "Our Hunchentoot acceptor")
@@ -157,12 +157,12 @@
                              :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.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/flash-template/index.html b/docs/building-blocks/flash-template/index.html index 9293c18..f60adda 100644 --- a/docs/building-blocks/flash-template/index.html +++ b/docs/building-blocks/flash-template/index.html @@ -1,13 +1,13 @@ -

WALK - flash messages +

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 %}
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/form-validation/index.html b/docs/building-blocks/form-validation/index.html index 3d750aa..7a57cab 100644 --- a/docs/building-blocks/form-validation/index.html +++ b/docs/building-blocks/form-validation/index.html @@ -11,7 +11,7 @@ See also the cl-forms library that offers many features: automatic forms form validation with in-line error messages CSRF protection client-side validation subforms Djula and Spinneret renderers default themes an online demo for you to try etc Get them: (ql:quickload '(:clavier :cl-forms)) Form validation with the Clavier library Clavier defines validators as class instances. They come in many types, for example the 'less-than, greater-than or len validators. They may take an initialization argument.">Form validation :: Web Apps in Lisp: Know-how -

Form validation

We can recommend the clavier +

Form validation

We can recommend the clavier library for input validation.

See also the cl-forms library that offers many features:

  • automatic forms
  • form validation with in-line error messages
  • CSRF protection
  • client-side validation
  • subforms
  • Djula and Spinneret renderers
  • default themes
  • an online demo for you to try
  • etc

Get them:

(ql:quickload '(:clavier :cl-forms))

Form validation with the Clavier library

Clavier defines validators as class instances. They come in many types, for example the 'less-than, @@ -73,12 +73,12 @@ NIL ("Length of \"foo\" is less than 5")

Note that Clavier has a “validator-collection” thing, but not shown in the README, and is in our opininion too verbose in comparison to a -simple list.

\ No newline at end of file diff --git a/docs/building-blocks/headers/index.html b/docs/building-blocks/headers/index.html index 6d5520b..b55ecb8 100644 --- a/docs/building-blocks/headers/index.html +++ b/docs/building-blocks/headers/index.html @@ -19,14 +19,14 @@ (setf (hunchentoot:header-out "HX-Trigger") "myEvent") This sets the header of the current request. USe headers-out (plural) to get an association list of headers: An alist of the outgoing http headers not including the β€˜Set-Cookie’, β€˜Content-Length’, and β€˜Content-Type’ headers. Use the functions HEADER-OUT and (SETF HEADER-OUT) to modify this slot.'>Headers :: Web Apps in Lisp: Know-how -

Headers

A quick reference.

These functions have to be used in the context of a web request.

Set headers

Use header-out to set headers, like this:

(setf (hunchentoot:header-out "HX-Trigger") "myEvent")

This sets the header of the current request.

USe headers-out (plural) to get an association list of headers:

An alist of the outgoing http headers not including the ‘Set-Cookie’, ‘Content-Length’, and ‘Content-Type’ headers. Use the functions HEADER-OUT and (SETF HEADER-OUT) to modify this slot.

Get headers

Use the header-in* and headers-in* (plural) function:

Function: (header-in* name &optional (request *request*))
+

Headers

A quick reference.

These functions have to be used in the context of a web request.

Set headers

Use header-out to set headers, like this:

(setf (hunchentoot:header-out "HX-Trigger") "myEvent")

This sets the header of the current request.

USe headers-out (plural) to get an association list of headers:

An alist of the outgoing http headers not including the ‘Set-Cookie’, ‘Content-Length’, and ‘Content-Type’ headers. Use the functions HEADER-OUT and (SETF HEADER-OUT) to modify this slot.

Get headers

Use the header-in* and headers-in* (plural) function:

Function: (header-in* name &optional (request *request*))
 

Returns the incoming header with name NAME. NAME can be a keyword (recommended) or a string.

headers-in*:

Function: (headers-in* &optional (request *request*))
-

Returns an alist of the incoming headers associated with the REQUEST object REQUEST.

Reference

Find some more here:

Returns an alist of the incoming headers associated with the REQUEST object REQUEST.

Reference

Find some more here:

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/index.html b/docs/building-blocks/index.html index 51f5a5d..f065bc6 100644 --- a/docs/building-blocks/index.html +++ b/docs/building-blocks/index.html @@ -19,7 +19,7 @@ We will use the Hunchentoot web server, but we should say a few words about Clack too. Hunchentoot is a web server and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL. It provides facilities like automatic session handling (with and without cookies), logging, customizable error handling, and easy access to GET and POST parameters sent by the client.'>Building blocks :: Web Apps in Lisp: Know-how -

Building blocks

In this chapter we’ll create routes, we’ll serve +

Building blocks

In this chapter we’ll create routes, we’ll serve local files and we’ll run more than one web app in the same running image.

We’ll use those libraries:

(ql:quickload '("hunchentoot" "easy-routes" "djula" "spinneret"))
Info

You can create a web project with our project generator: cl-cookieweb.

We will use the Hunchentoot web server, but we should say a few words about Clack too.

Hunchentoot is

a web server and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL. It provides facilities like automatic session handling (with and without cookies), logging, customizable error handling, and easy access to GET and POST parameters sent by the client.

It is a software written by Edi Weitz (author of the “Common Lisp Recipes” book, of the ubiquitous cl-ppcre library and much more), it is used and @@ -55,12 +55,12 @@ and update since 2017. We present it in more details in the “Isomorphic web frameworks” appendix.

For a full list of libraries for the web, please see the awesome-cl list #network-and-internet -and Cliki.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/index.xml b/docs/building-blocks/index.xml index d2af33d..7af6b7c 100644 --- a/docs/building-blocks/index.xml +++ b/docs/building-blocks/index.xml @@ -15,7 +15,7 @@ Hunchentoot The dispatch table The first, most basic way in Hunchentoot to creat Install it if you didn’t already: (ql:quickload "djula") The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like: (djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/")) and then we can declare and compile the ones we use, for example:: -(defparameter +base.html+ (djula:compile-template* "base.html")) (defparameter +products.html+ (djula:compile-template* "products.html")) A Djula template looks like this:Sessionshttp://example.org/building-blocks/session/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/session/index.htmlWhen a web client (a user’s browser) connects to your app, you can start a session with it, and store data, the time of the session. +(defparameter +base.html+ (djula:compile-template* "base.html")) (defparameter +products.html+ (djula:compile-template* "products.html")) Info If you get an error message when calling add-template-directory like thisSessionshttp://example.org/building-blocks/session/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/session/index.htmlWhen a web client (a user’s browser) connects to your app, you can start a session with it, and store data, the time of the session. Hunchentoot uses cookies to store the session ID, and if not possible it rewrites URLs. So, at every subsequent request by this web client, you can check if there is a session data. You can store any Lisp object in a web session. You only have to:Flash messageshttp://example.org/building-blocks/flash-messages/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/flash-messages/index.htmlFlash 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. @@ -32,8 +32,9 @@ do you have an existing database you want to read data from? do you want to crea The #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:User log-inhttp://example.org/building-blocks/user-log-in/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/user-log-in/index.htmlHow do you check if a user is logged-in, and how do you do the actual log in? We show an example, without handling passwords yet. See the next section for passwords. We’ll build a simple log-in page to an admin/ private dashboard. -What do we need to do exactly?Users and passwordshttp://example.org/building-blocks/users-and-passwords/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/users-and-passwords/index.htmlWe 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. -Info Stay tuned! We are on to something. +What do we need to do exactly?Users and passwordshttp://example.org/building-blocks/users-and-passwords/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/users-and-passwords/index.htmlWe don’t know of a Common Lisp framework that will create users and roles for you and protect your routes all at the same time. We have building blocks but you’ll have to either write some glue Lisp code. +You can also turn to an external tool (such as Keycloak) that will provide all the industrial-grade user management. +If you like the Mito ORM, look at mito-auth and mito-email-auth. Creating users If you use a database, you’ll have to create at least a users table. It would typically define:Form validationhttp://example.org/building-blocks/form-validation/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/form-validation/index.htmlWe can recommend the clavier library for input validation. See also the cl-forms library that offers many features: automatic forms form validation with in-line error messages CSRF protection client-side validation subforms Djula and Spinneret renderers default themes an online demo for you to try etc Get them: diff --git a/docs/building-blocks/remote-debugging/index.html b/docs/building-blocks/remote-debugging/index.html index f624a3e..b3e836e 100644 --- a/docs/building-blocks/remote-debugging/index.html +++ b/docs/building-blocks/remote-debugging/index.html @@ -3,7 +3,7 @@ You can not only inspect the running program, but also compile and load new code, including installing new libraries, effectively doing hot code reload. It’s up to you to decide to do it or to follow the industry’s best practices. It isn’t because you use Common Lisp that you have to make your deployed program a β€œbig ball of mud”.">Remote debugging :: Web Apps in Lisp: Know-how -

Remote debugging

You can have your software running on a machine over the network, +

Remote debugging

You can have your software running on a machine over the network, connect to it and debug it from home, from your development environment.

You can not only inspect the running program, but also compile and load new code, including installing new libraries, effectively doing @@ -53,12 +53,12 @@ and port 4006.

We can write new code:

(defun dostuff ()
   (format t "goodbye world ~a!~%" *counter*))
 (setf *counter* 0)

and eval it as usual with C-c C-c or M-x slime-eval-region for instance. The output should change.

That’s how Ron Garret debugged the Deep Space 1 spacecraft from the earth -in 1999:

We were able to debug and fix a race condition that had not shown up during ground testing. (Debugging a program running on a $100M piece of hardware that is 100 million miles away is an interesting experience. Having a read-eval-print loop running on the spacecraft proved invaluable in finding and fixing the problem.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/routing/index.html b/docs/building-blocks/routing/index.html index 4384cd9..4525f5b 100644 --- a/docs/building-blocks/routing/index.html +++ b/docs/building-blocks/routing/index.html @@ -7,7 +7,7 @@ Hunchentoot The dispatch table The first, most basic way in Hunchentoot to create a route is to add a URL -> function association in its β€œprefix dispatch” table.">Routes and URL parameters :: Web Apps in Lisp: Know-how -

Routes and URL parameters

I prefer the easy-routes library than pure Hunchentoot to define +

Routes and URL parameters

I prefer the easy-routes library than pure Hunchentoot to define routes, as we did in the tutorial, so skip to its section below if you want. However, it can only be benificial to know the built-in Hunchentoot ways.

Info

Please see the tutorial where we define routes with path parameters @@ -78,12 +78,12 @@ (setf (hunchentoot:content-type*) "text/plain") (format nil "Hey ~a you are of type ~a" name (type-of name)))

Going to http://localhost:4242/yo?name=Alice returns

Hey Alice you are of type (SIMPLE-ARRAY CHARACTER (5))
 

To automatically bind it to another type, we use default-parameter-type. It can be -one of those simple types:

  • 'string (default),
  • 'integer,
  • 'character (accepting strings of length 1 only, otherwise it is nil)
  • or 'boolean

or a compound list:

  • '(:list <type>)
  • '(:array <type>)
  • '(:hash-table <type>)

where <type> is a simple type.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/session/index.html b/docs/building-blocks/session/index.html index 7dd2734..be272c6 100644 --- a/docs/building-blocks/session/index.html +++ b/docs/building-blocks/session/index.html @@ -7,15 +7,15 @@ You can store any Lisp object in a web session. You only have to:">Sessions :: Web Apps in Lisp: Know-how -

Sessions

When a web client (a user’s browser) connects to your app, you can +

Sessions

When a web client (a user’s browser) connects to your app, you can start a session with it, and store data, the time of the session.

Hunchentoot uses cookies to store the session ID, and if not possible it rewrites URLs. So, at every subsequent request by this web client, -you can check if there is a session data.

You can store any Lisp object in a web session. You only have to:

  • start a session with (hunchentoot:start-session)
    • for example, when a user logs in successfully
  • store an object with (setf (hunchentoot:session-value 'key) val)
    • for example, store the username at the log-in
  • and get the object with (hunchentoot:session-value 'key).
    • for example, in any route where you want to check that a user is logged in. If you don’t find a session key you want, you would redirect to the login page.

See our example in the next section about log-in.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/simple-web-server/index.html b/docs/building-blocks/simple-web-server/index.html index 5070d2b..72697b2 100644 --- a/docs/building-blocks/simple-web-server/index.html +++ b/docs/building-blocks/simple-web-server/index.html @@ -7,7 +7,7 @@ (defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4242)) (hunchentoot:start *acceptor*) We create an instance of easy-acceptor on port 4242 and we start it. We can now access http://127.0.0.1:4242/. You should get a welcome screen with a link to the documentation and logs to the console.">Simple web server :: Web Apps in Lisp: Know-how -

Simple web server

Before dwelving into web development, we might want to do something simple: serve some files we have on disk.

Serve local files

If you followed the tutorial you know that we can create and start a webserver like this:

(defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4242))
+

Simple web server

Before dwelving into web development, we might want to do something simple: serve some files we have on disk.

Serve local files

If you followed the tutorial you know that we can create and start a webserver like this:

(defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4242))
 (hunchentoot:start *acceptor*)

We create an instance of easy-acceptor on port 4242 and we start it. We can now access http://127.0.0.1:4242/. You should get a welcome screen with a link to the documentation and logs to the console.

Info

You can also use Roswell’s http.server from the command line:

$ ros install roswell/http.server
@@ -35,12 +35,12 @@
 (hunchentoot:start *my-acceptor*)

go to http://127.0.0.1:4444/ and see the difference.

Note that we just created another web application on a different port on the same lisp image. This is already pretty cool.

Access your server from the internet

With Hunchentoot we have nothing to do, we can see the server from the internet right away.

If you evaluate this on your VPS:

(hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242))

You can see it right away on your server’s IP.

You can use the :address parameter of Hunchentoot’s easy-acceptor to -bind it and restrict it to 127.0.0.1 (or any address) if you wish.

Stop it with (hunchentoot:stop *).

Now on the next section, we’ll create some routes to build a dynamic website.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/static/index.html b/docs/building-blocks/static/index.html index 7a5b6a0..c4d7322 100644 --- a/docs/building-blocks/static/index.html +++ b/docs/building-blocks/static/index.html @@ -15,7 +15,7 @@ Hunchentoot Use the create-folder-dispatcher-and-handler prefix directory function. For example: (defun serve-static-assets () "Let Hunchentoot serve static assets under the /src/static/ directory of your :myproject system. Then reference static assets with the /static/ URL prefix." (push (hunchentoot:create-folder-dispatcher-and-handler "/static/" (asdf:system-relative-pathname :myproject "src/static/")) ;; ^^^ starts without a / hunchentoot:*dispatch-table*)) and call it in the function that starts your application:'>Static assets :: Web Apps in Lisp: Know-how -

Static assets

How can we serve static assets?

Remember from simple web server +

Static assets

How can we serve static assets?

Remember from simple web server how Hunchentoot serves files from a www/ directory by default. We can change that.

Hunchentoot

Use the create-folder-dispatcher-and-handler prefix directory function.

For example:

(defun serve-static-assets ()
   "Let Hunchentoot serve static assets under the /src/static/ directory
   of your :myproject system.
@@ -25,12 +25,12 @@
           (asdf:system-relative-pathname :myproject "src/static/"))
           ;;                                        ^^^ starts without a /
         hunchentoot:*dispatch-table*))

and call it in the function that starts your application:

(serve-static-assets)

Now our project’s static files located under src/static/ are served -with the /static/ prefix. Access them like this:

<img src="/static/img/banner.jpg" />

or

<script src="/static/test.js" type="text/javascript"></script>

where the file src/static/test.js could be

console.log("hello");
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/templates/index.html b/docs/building-blocks/templates/index.html index a81e7ea..73720ed 100644 --- a/docs/building-blocks/templates/index.html +++ b/docs/building-blocks/templates/index.html @@ -15,7 +15,7 @@ (ql:quickload "djula") The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like: (djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/")) and then we can declare and compile the ones we use, for example:: (defparameter +base.html+ (djula:compile-template* "base.html")) (defparameter +products.html+ (djula:compile-template* "products.html")) Info If you get an error message when calling add-template-directory like this'>Templates :: Web Apps in Lisp: Know-how -

Templates

Djula - HTML markup

Djula is a port of Python’s +

Templates

Djula - HTML markup

Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation.

Install it if you didn’t already:

(ql:quickload "djula")

The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like:

(djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/"))

and then we can declare and compile the ones we use, for example::

(defparameter +base.html+ (djula:compile-template* "base.html"))
 (defparameter +products.html+ (djula:compile-template* "products.html"))
Info

If you get an error message when calling add-template-directory like this

The value
@@ -52,7 +52,7 @@
                           nil
                           :products (products)

Djula is, along with its companion access library, one of -the most downloaded libraries of Quicklisp.

Djula filters

Filters are only waiting for the developers to define their own, so we should have a word about them.

They allow to modify how a variable is displayed. Djula comes with +the most downloaded libraries of Quicklisp.

Djula filters

Filters are only waiting for the developers to define their own, so we should have a work about them.

They allow to modify how a variable is displayed. Djula comes with a good set of built-in filters and they are well documented. They are not to be confused with tags.

They look like this: {{ var | lower }}, where lower is an existing filter, which renders the text into lowercase.

Filters sometimes take arguments. For example: {{ var | add:2 }} calls the add filter with arguments var and 2.

Moreover, it is very easy to define custom filters. All we have to do @@ -70,12 +70,12 @@ (:ol (dolist (item *shopping-list*) (:li (1+ (random 10)) item)))) (:footer ("Last login: ~A" *last-login*)))

I find Spinneret easier to use than the more famous cl-who, but I -personnally prefer to use HTML templates.

Spinneret has nice features under it sleeves:

  • it warns on invalid tags and attributes
  • it can automatically number headers, given their depth
  • it pretty prints html per default, with control over line breaks
  • it understands embedded markdown
  • it can tell where in the document a generator function is (see get-html-tag)
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/user-log-in/index.html b/docs/building-blocks/user-log-in/index.html index 8b27315..9919748 100644 --- a/docs/building-blocks/user-log-in/index.html +++ b/docs/building-blocks/user-log-in/index.html @@ -11,9 +11,9 @@ We show an example, without handling passwords yet. See the next section for passwords. We’ll build a simple log-in page to an admin/ private dashboard. What do we need to do exactly?">User log-in :: Web Apps in Lisp: Know-how -

User log-in

How do you check if a user is logged-in, and how do you do the actual log in?

We show an example, without handling passwords yet. See the next section for passwords.

We’ll build a simple log-in page to an admin/ private dashboard.

-

-

What do we need to do exactly?

  • we need a function to get a user by its ID
  • we need a function to check that a password is correct for a given user
  • we need two templates:
    • a login template
    • a template for a logged-in user
  • we need a route and we need to handle a POST request
    • when the log-in is successful, we need to store a user ID in a web session

We choose to structure our app with an admin/ URL that will show +

User log-in

How do you check if a user is logged-in, and how do you do the actual log in?

We show an example, without handling passwords yet. See the next section for passwords.

We’ll build a simple log-in page to an admin/ private dashboard.

+

+

What do we need to do exactly?

  • we need a function to get a user by its ID
  • we need a function to check that a password is correct for a given user
  • we need two templates:
    • a login template
    • a template for a logged-in user
  • we need a route and we need to handle a POST request
    • when the log-in is successful, we need to store a user ID in a web session

We choose to structure our app with an admin/ URL that will show both the login page or the “dashboard” for logged-in users.

We use these libraries:

(ql:quickload '("hunchentoot" "djula" "easy-routes"))

We still work from inside our :myproject package. You should have this at the top of your file:

(in-package :myproject)

Let’s start with the model functions.

Get users

(defun get-user (name)
   (list :name name :password "demo")) ;; <--- all our passwords are "demo"

Yes indeed, that’s a dummy function. You will add your own logic @@ -270,12 +270,12 @@ (defroute ("/account/review" :method :get) () (with-logged-in (render #p"review.html" - (list :review (get-review (gethash :user *session*))))))

and so on.

\ No newline at end of file diff --git a/docs/building-blocks/users-and-passwords/index.html b/docs/building-blocks/users-and-passwords/index.html index e06c126..87274e3 100644 --- a/docs/building-blocks/users-and-passwords/index.html +++ b/docs/building-blocks/users-and-passwords/index.html @@ -1,16 +1,20 @@ -Users and passwords :: Web Apps in Lisp: Know-how -

Users and passwords

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.

Info

Stay tuned! We are on to something.

Creating users

If you use a database, you’ll have to create at least a users +Users and passwords :: Web Apps in Lisp: Know-how +

Users and passwords

We don’t know of a Common Lisp framework that will create users and +roles for you and protect your routes all at the same time. We have +building blocks but you’ll have to either write some glue Lisp code.

You can also turn to an external tool (such as Keycloak) that will +provide all the industrial-grade user management.

If you like the Mito ORM, look at mito-auth and mito-email-auth.

Creating users

If you use a database, you’ll have to create at least a users table. It would typically define:

  • a unique ID (integer, primary key)
  • a name (varchar)
  • an email (varchar)
  • a password (varchar (and encrypted))
  • optionally, a key to the table listing roles.

You can start with this:

CREATE TABLE users (
   id INTEGER PRIMARY KEY,
   username VARCHAR(255),
@@ -50,12 +54,12 @@
                                   (make-op := (if (integerp user)
                                                   :id_user
                                                   :email)
-                                           user))))))

Credit: /u/arvid on /r/learnlisp.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/users-and-passwords/index.xml b/docs/building-blocks/users-and-passwords/index.xml index 0bd70ae..b2f1993 100644 --- a/docs/building-blocks/users-and-passwords/index.xml +++ b/docs/building-blocks/users-and-passwords/index.xml @@ -1,3 +1,4 @@ -Users and passwords :: Web Apps in Lisp: Know-howhttp://example.org/building-blocks/users-and-passwords/index.htmlWe 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. -Info Stay tuned! We are on to something. +Users and passwords :: Web Apps in Lisp: Know-howhttp://example.org/building-blocks/users-and-passwords/index.htmlWe don’t know of a Common Lisp framework that will create users and roles for you and protect your routes all at the same time. We have building blocks but you’ll have to either write some glue Lisp code. +You can also turn to an external tool (such as Keycloak) that will provide all the industrial-grade user management. +If you like the Mito ORM, look at mito-auth and mito-email-auth. Creating users If you use a database, you’ll have to create at least a users table. It would typically define:Hugoen-us \ No newline at end of file diff --git a/docs/building-blocks/web-views/index.html b/docs/building-blocks/web-views/index.html index d56bb25..30b5454 100644 --- a/docs/building-blocks/web-views/index.html +++ b/docs/building-blocks/web-views/index.html @@ -11,7 +11,7 @@ We present two: WebUI and Webview.h, through CLOG Frame. Info This page appeared first on lisp-journey: three web views for Common Lisp, cross-platform GUIs. WebUI WebUI is a new kid in town. It is in development, it has bugs. You can view it as a wrapper around a browser window (or webview.h).">Web views: cross-platform GUIs :: Web Apps in Lisp: Know-how -

Web views: cross-platform GUIs

Web views are lightweight and cross-platform. They are nowadays a good +

Web views: cross-platform GUIs

Web views are lightweight and cross-platform. They are nowadays a good solution to ship a GUI program to your users.

We present two: WebUI and Webview.h, through CLOG Frame.

WebUI

WebUI is a new kid in town. It is in development, it has bugs. You can view it as a wrapper around a browser window (or webview.h).

However it is ligthweight, it is easy to build and we have Lisp bindings.

A few more words about it:

Use any web browser or WebView as GUI, with your preferred language in the backend and modern web technologies in the frontend, all in a lightweight portable library.

  • written in pure C
  • one header file
  • multi-platform & multi-browser
  • opens a real browser (you get the web development tools etc)
  • cross-platform webview
  • we can call JS from Common Lisp, and call Common Lisp from JS.

Think of WebUI like a WebView controller, but instead of embedding the WebView controller in your program, which makes the final program big in size, and non-portable as it needs the WebView runtimes. Instead, by using WebUI, you use a tiny static/dynamic library to run any installed web browser and use it as GUI, which makes your program small, fast, and portable. All it needs is a web browser.

your program will always run on all machines, as all it needs is an installed web browser.

Sounds compelling right?

The other good news is that Common Lisp was one of the first languages it got bindings for. How it happened: I was chating in Discord, mentioned WebUI and BAM! @garlic0x1 developed bindings:

thank you so much! (@garlic0x1 has more cool projects on GitHub you can browse. He’s also a contributor to Lem)

Here’s a simple snippet:

(defpackage :webui/examples/minimal
   (:use :cl :webui)
@@ -49,15 +49,15 @@
                            "Admin"
                            (format nil "~A/admin/" 4284)
                            ;; window dimensions (strings)
-                           "1280" "840"))

and voilΓ .

-

Now for the cross-platform part, you’ll need to build clogframe and + "1280" "840"))

and voilΓ .

+

Now for the cross-platform part, you’ll need to build clogframe and your web app on the target OS (like with any CL app). Webview.h is cross-platform. -Leave us a comment when you have a good CI setup for the three main OSes (I am studying 40ants/ci and make-common-lisp-program for now).

\ No newline at end of file diff --git a/docs/categories/index.html b/docs/categories/index.html index d86fcdc..d930d77 100644 --- a/docs/categories/index.html +++ b/docs/categories/index.html @@ -1,10 +1,10 @@ Categories :: Web Apps in Lisp: Know-how -

Categories

\ No newline at end of file diff --git a/docs/css/chroma-auto.css b/docs/css/chroma-auto.css index 5986882..b0f8794 100644 --- a/docs/css/chroma-auto.css +++ b/docs/css/chroma-auto.css @@ -1,2 +1,2 @@ -@import "chroma-relearn-light.css?1741870399" screen and (prefers-color-scheme: light); -@import "chroma-relearn-dark.css?1741870399" screen and (prefers-color-scheme: dark); +@import "chroma-relearn-light.css?1746637581" screen and (prefers-color-scheme: light); +@import "chroma-relearn-dark.css?1746637581" screen and (prefers-color-scheme: dark); diff --git a/docs/css/format-print.css b/docs/css/format-print.css index a2a1772..dc8356b 100644 --- a/docs/css/format-print.css +++ b/docs/css/format-print.css @@ -1,5 +1,5 @@ -@import "theme-relearn-light.css?1741870399"; -@import "chroma-relearn-light.css?1741870399"; +@import "theme-relearn-light.css?1746637581"; +@import "chroma-relearn-light.css?1746637581"; #R-sidebar { display: none; diff --git a/docs/css/print.css b/docs/css/print.css index 25a745a..6aa9907 100644 --- a/docs/css/print.css +++ b/docs/css/print.css @@ -1 +1 @@ -@import "format-print.css?1741870399"; +@import "format-print.css?1746637581"; diff --git a/docs/css/swagger.css b/docs/css/swagger.css index 5af5bca..51b71f2 100644 --- a/docs/css/swagger.css +++ b/docs/css/swagger.css @@ -1,7 +1,7 @@ /* Styles to make Swagger-UI fit into our theme */ -@import "fonts.css?1741870399"; -@import "variables.css?1741870399"; +@import "fonts.css?1746637581"; +@import "variables.css?1746637581"; body{ line-height: 1.574; diff --git a/docs/css/theme-auto.css b/docs/css/theme-auto.css index 4a9e15f..50df42a 100644 --- a/docs/css/theme-auto.css +++ b/docs/css/theme-auto.css @@ -1,2 +1,2 @@ -@import "theme-relearn-light.css?1741870399" screen and (prefers-color-scheme: light); -@import "theme-relearn-dark.css?1741870399" screen and (prefers-color-scheme: dark); +@import "theme-relearn-light.css?1746637581" screen and (prefers-color-scheme: light); +@import "theme-relearn-dark.css?1746637581" screen and (prefers-color-scheme: dark); diff --git a/docs/css/theme.css b/docs/css/theme.css index baa37cb..58de19a 100644 --- a/docs/css/theme.css +++ b/docs/css/theme.css @@ -1,4 +1,4 @@ -@import "variables.css?1741870399"; +@import "variables.css?1746637581"; @charset "UTF-8"; diff --git a/docs/index.html b/docs/index.html index 6212a65..0c6a732 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,16 +6,16 @@ What’s in this guide We’ll start with a tutorial that shows the essential building blocks: starting a web server defining routes grabbing URL parameters rendering templates and running our app from sources, or building a binary. We’ll build a simple page that presents a search form, filters a list of products and displays the results.">Web Apps in Lisp: Know-how -

Web Apps in Lisp: Know-how

You want to write a web application in Common Lisp and you don’t know +starting a web server defining routes grabbing URL parameters rendering templates and running our app from sources, or building a binary. We’ll build a simple page that presents a search form, filters a list of products and displays the results.">Web Apps in Lisp: Know-how +

Web Apps in Lisp: Know-how

You want to write a web application in Common Lisp and you don’t know where to start? You are a beginner in web development, or a lisp amateur looking for clear and short pointers about web dev in Lisp? You are all at the right place.

What’s in this guide

We’ll start with a tutorial that shows the essential building blocks:

  • starting a web server
  • defining routes
  • grabbing URL parameters
  • rendering templates
  • and running our app from sources, or building a binary.

We’ll build a simple page that presents a search form, filters a list of products and displays the results.

The building blocks section is organized by topics, so that with a question in mind you should be able to look at the table of contents, pick the right page and go ahead. You’ll find more tutorials, for -example in “User log-in” we build a login form.

-

We hope that it will be plenty useful to you.

Don’t hesitate to share what you’re building!

Info

πŸŽ₯ If you want to learn Common Lisp efficiently, +example in “User log-in” we build a login form.

+

We hope that it will be plenty useful to you.

Don’t hesitate to share what you’re building!

Info

πŸŽ₯ If you want to learn Common Lisp efficiently, with a code-driven approach, good news, I am creating a video course on the Udemy platform. Already more than 7 hours of content, rated @@ -24,12 +24,12 @@ also have Common Lisp tutorial videos on Youtube.

Now let’s go to the tutorial.

How to start with Common Lisp

This resource is not about learning Common Lisp the language. We expect you have a working setup, with the Quicklisp package -manager. Please refer to the CL Cookbook.

Contact

We are @vindarel on Lisp’s Discord server and Mastodon.

This project’s GitHub Discussions are open.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/isomorphic-web-frameworks/clog/index.html b/docs/isomorphic-web-frameworks/clog/index.html index 8110722..eaf30dc 100644 --- a/docs/isomorphic-web-frameworks/clog/index.html +++ b/docs/isomorphic-web-frameworks/clog/index.html @@ -7,7 +7,7 @@ We can say the CLOG experience is mindblowing.">CLOG :: Web Apps in Lisp: Know-how -

CLOG

CLOG, the Common Lisp Omnificent +

CLOG

CLOG, the Common Lisp Omnificent GUI, follows a GUI paradigm for the web platform. You don’t write nested <div> tags, but you place elements on the page. It sends changes to the page you are working on @@ -18,8 +18,8 @@ under the fingertips.

Moreover, its API is stable. The author used a similar product built in Ada professionally for a decade, and transitioned to CLOG in Common Lisp.

So, how can we build an interactive app with CLOG?

We will build a search form that triggers a search to the back-end on -key presses, and displays results to users as they type.

We do so without writing any JavaScript.

-

Before we do so, we’ll create a list of dummy products, so than we have something to search for.

Models

Let’s create a package for this new app. I’ll “use” functions and +key presses, and displays results to users as they type.

We do so without writing any JavaScript.

+

Before we do so, we’ll create a list of dummy products, so than we have something to search for.

Models

Let’s create a package for this new app. I’ll “use” functions and macros provided by the :clog package.

(uiop:define-package :clog-search
     (:use :cl :clog))
 
@@ -174,8 +174,8 @@
   (let ((query (value obj)))
     (if (> (length query) 2)
         (display-products div (clog-search::search-products query))
-        (print "waiting for more input"))))

It works \o/

-

There are some caveats that need to be worked on:

  • if you type a search query of 4 letters quickly, our handler waits for an input of at least 2 characters, but it will be fired 2 other times. That will probably fix the blickering.

And, as you noticed:

  • we didn’t copy-paste a nice looking HTML template, so we have a bit of work with that :/

This was only an introduction. As we said, CLOG is well suited for a + (print "waiting for more input"))))

It works \o/

+

There are some caveats that need to be worked on:

  • if you type a search query of 4 letters quickly, our handler waits for an input of at least 2 characters, but it will be fired 2 other times. That will probably fix the blickering.

And, as you noticed:

  • we didn’t copy-paste a nice looking HTML template, so we have a bit of work with that :/

This was only an introduction. As we said, CLOG is well suited for a wide range of applications.

Stop the app

Use

(clog:shutdown)

Full code

;; (ql:quickload '(:clog :str))
 
 (uiop:define-package :clog-search
@@ -286,12 +286,12 @@
   (let ((query (value obj)))
     (if (> (length query) 2)
         (display-products div (search-products query))
-        (print "waiting for more input"))))

References

\ No newline at end of file diff --git a/docs/isomorphic-web-frameworks/index.html b/docs/isomorphic-web-frameworks/index.html index d8447a2..0b58a39 100644 --- a/docs/isomorphic-web-frameworks/index.html +++ b/docs/isomorphic-web-frameworks/index.html @@ -11,17 +11,17 @@ Weblocks was an old framework developed by Slava Akhmechet, Stephen Compall and Leslie Polzer. After nine calm years, it saw a very active update, refactoring and rewrite effort by Alexander Artemenko. This active project is named Reblocks. The Ultralisp website is an example Reblocks website in production known in the CL community. It isn’t the only solution that aims at making writing interactive web apps easier, where the client logic can be brought to the back-end. See also:">Isomorphic web frameworks :: Web Apps in Lisp: Know-how -

Isomorphic web frameworks

We’ll see first: Reblocks.

Weblocks was an old framework developed by Slava Akhmechet, Stephen +

Isomorphic web frameworks

We’ll see first: Reblocks.

Weblocks was an old framework developed by Slava Akhmechet, Stephen Compall and Leslie Polzer. After nine calm years, it saw a very active update, refactoring and rewrite effort by Alexander Artemenko. This active project is named Reblocks.

The Ultralisp website is an example Reblocks website in production known in the CL community.

It isn’t the only solution that aims at making writing interactive web apps easier, where the client logic can be brought to the back-end. See also:

Of course, a special mention goes to HTMX, which -is language agnostic and backend agnostic. It works well with Common Lisp.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/isomorphic-web-frameworks/weblocks/index.html b/docs/isomorphic-web-frameworks/weblocks/index.html index 1f542fb..06e54a5 100644 --- a/docs/isomorphic-web-frameworks/weblocks/index.html +++ b/docs/isomorphic-web-frameworks/weblocks/index.html @@ -7,12 +7,12 @@ Note To install Reblocks, please see its documentation.">Reblocks :: Web Apps in Lisp: Know-how -

Reblocks

Reblocks is a widgets-based and server-based framework +

Reblocks

Reblocks is a widgets-based and server-based framework with a built-in ajax update mechanism. It allows to write dynamic web applications without the need to write JavaScript or to write lisp code that would transpile to JavaScript. It is thus super -exciting. It isn’t for newcomers however.

The Reblocks’s demo we will build is a TODO app:

-

Note

To install Reblocks, please see its documentation.

Reblocks’ unit of work is the widget. They look like a class definition:

(defwidget task ()
+exciting. It isn’t for newcomers however.

The Reblocks’s demo we will build is a TODO app:

+

Note

To install Reblocks, please see its documentation.

Reblocks’ unit of work is the widget. They look like a class definition:

(defwidget task ()
    ((title
      :initarg :title
      :accessor title)
@@ -36,12 +36,12 @@
 ...

The function make-js-action creates a simple javascript function that calls the lisp one on the server, and automatically refreshes the HTML of the widgets that need it. In our example, it re-renders one -task only.

Is it appealing ? Carry on its quickstart guide.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/search/index.html b/docs/search/index.html index 8419943..4d06239 100644 --- a/docs/search/index.html +++ b/docs/search/index.html @@ -1,11 +1,11 @@ Search :: Web Apps in Lisp: Know-how -
\ No newline at end of file diff --git a/docs/searchindex.js b/docs/searchindex.js index 4a08e4a..a852a00 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -73,8 +73,8 @@ var relearn_searchindex = [ }, { "breadcrumb": "Building blocks", - "content": "Djula - HTML markup Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation.\nInstall it if you didn’t already:\n(ql:quickload \"djula\") The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like:\n(djula:add-template-directory (asdf:system-relative-pathname \"myproject\" \"templates/\")) and then we can declare and compile the ones we use, for example::\n(defparameter +base.html+ (djula:compile-template* \"base.html\")) (defparameter +products.html+ (djula:compile-template* \"products.html\")) A Djula template looks like this:\n{% extends \"base.html\" %} {% block title %} Products page {% endblock %} {% block content %} \u003cul\u003e {% for product in products %} \u003cli\u003e\u003ca href=\"{{ product.id }}\"\u003e{{ product.name }}\u003c/a\u003e\u003c/li\u003e {% endfor %} \u003c/ul\u003e {% endblock %} This template actually inherits a first one, base.html, which can be:\n\u003chtml\u003e \u003chead\u003e \u003cmeta charset=\"utf-8\"\u003e \u003clink rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css\"\u003e \u003ctitle\u003e{% block title %} My Lisp app {% endblock %}\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003cdiv class=\"container\"\u003e {% block content %} {% endblock %} \u003c/div\u003e \u003c/body\u003e \u003c/html\u003e This base template defines two blocks: one for the page title, one for the page content. A template that wants to inherit this base template will use {% extends \"base.html\" %} and replace each blocks with {% block content %} … {‰ endblock %}.\nAt last, to render the template, call djula:render-template* inside a route.\n(easy-routes:defroute root (\"/\" :method :get) () (djula:render-template* +products.html+ nil :products (products) Djula is, along with its companion access library, one of the most downloaded libraries of Quicklisp.\nDjula filters Filters are only waiting for the developers to define their own, so we should have a work about them.\nThey allow to modify how a variable is displayed. Djula comes with a good set of built-in filters and they are well documented. They are not to be confused with tags.\nThey look like this: {{ var | lower }}, where lower is an existing filter, which renders the text into lowercase.\nFilters sometimes take arguments. For example: {{ var | add:2 }} calls the add filter with arguments var and 2.\nMoreover, it is very easy to define custom filters. All we have to do is to use the def-filter macro, which takes the variable as first argument, and which can take more optional arguments.\nIts general form is:\n(def-filter :myfilter-name (var arg) ;; arg is optional (body)) and it is used like this: {{ var | myfilter-name }}.\nHere’s how the add filter is defined:\n(def-filter :add (it n) (+ it (parse-integer n))) Once you have written a custom filter, you can use it right away throughout the application.\nFilters are very handy to move non-trivial formatting or logic from the templates to the backend.\nSpinneret - lispy templates Spinneret is a β€œlispy” HTML5 generator. It looks like this:\n(with-page (:title \"Home page\") (:header (:h1 \"Home page\")) (:section (\"~A, here is *your* shopping list: \" *user-name*) (:ol (dolist (item *shopping-list*) (:li (1+ (random 10)) item)))) (:footer (\"Last login: ~A\" *last-login*))) I find Spinneret easier to use than the more famous cl-who, but I personnally prefer to use HTML templates.\nSpinneret has nice features under it sleeves:\nit warns on invalid tags and attributes it can automatically number headers, given their depth it pretty prints html per default, with control over line breaks it understands embedded markdown it can tell where in the document a generator function is (see get-html-tag)", - "description": "Djula - HTML markup Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation.\nInstall it if you didn’t already:\n(ql:quickload \"djula\") The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like:\n(djula:add-template-directory (asdf:system-relative-pathname \"myproject\" \"templates/\")) and then we can declare and compile the ones we use, for example::\n(defparameter +base.html+ (djula:compile-template* \"base.html\")) (defparameter +products.html+ (djula:compile-template* \"products.html\")) A Djula template looks like this:", + "content": "Djula - HTML markup Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation.\nInstall it if you didn’t already:\n(ql:quickload \"djula\") The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like:\n(djula:add-template-directory (asdf:system-relative-pathname \"myproject\" \"templates/\")) and then we can declare and compile the ones we use, for example::\n(defparameter +base.html+ (djula:compile-template* \"base.html\")) (defparameter +products.html+ (djula:compile-template* \"products.html\")) Info If you get an error message when calling add-template-directory like this\nThe 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/.\nA Djula template looks like this:\n{% extends \"base.html\" %} {% block title %} Products page {% endblock %} {% block content %} \u003cul\u003e {% for product in products %} \u003cli\u003e\u003ca href=\"{{ product.id }}\"\u003e{{ product.name }}\u003c/a\u003e\u003c/li\u003e {% endfor %} \u003c/ul\u003e {% endblock %} This template actually inherits a first one, base.html, which can be:\n\u003chtml\u003e \u003chead\u003e \u003cmeta charset=\"utf-8\"\u003e \u003clink rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css\"\u003e \u003ctitle\u003e{% block title %} My Lisp app {% endblock %}\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003cdiv class=\"container\"\u003e {% block content %} {% endblock %} \u003c/div\u003e \u003c/body\u003e \u003c/html\u003e This base template defines two blocks: one for the page title, one for the page content. A template that wants to inherit this base template will use {% extends \"base.html\" %} and replace each blocks with {% block content %} … {‰ endblock %}.\nAt last, to render the template, call djula:render-template* inside a route.\n(easy-routes:defroute root (\"/\" :method :get) () (djula:render-template* +products.html+ nil :products (products) Djula is, along with its companion access library, one of the most downloaded libraries of Quicklisp.\nDjula filters Filters are only waiting for the developers to define their own, so we should have a work about them.\nThey allow to modify how a variable is displayed. Djula comes with a good set of built-in filters and they are well documented. They are not to be confused with tags.\nThey look like this: {{ var | lower }}, where lower is an existing filter, which renders the text into lowercase.\nFilters sometimes take arguments. For example: {{ var | add:2 }} calls the add filter with arguments var and 2.\nMoreover, it is very easy to define custom filters. All we have to do is to use the def-filter macro, which takes the variable as first argument, and which can take more optional arguments.\nIts general form is:\n(def-filter :myfilter-name (var arg) ;; arg is optional (body)) and it is used like this: {{ var | myfilter-name }}.\nHere’s how the add filter is defined:\n(def-filter :add (it n) (+ it (parse-integer n))) Once you have written a custom filter, you can use it right away throughout the application.\nFilters are very handy to move non-trivial formatting or logic from the templates to the backend.\nSpinneret - lispy templates Spinneret is a β€œlispy” HTML5 generator. It looks like this:\n(with-page (:title \"Home page\") (:header (:h1 \"Home page\")) (:section (\"~A, here is *your* shopping list: \" *user-name*) (:ol (dolist (item *shopping-list*) (:li (1+ (random 10)) item)))) (:footer (\"Last login: ~A\" *last-login*))) I find Spinneret easier to use than the more famous cl-who, but I personnally prefer to use HTML templates.\nSpinneret has nice features under it sleeves:\nit warns on invalid tags and attributes it can automatically number headers, given their depth it pretty prints html per default, with control over line breaks it understands embedded markdown it can tell where in the document a generator function is (see get-html-tag)", + "description": "Djula - HTML markup Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation.\nInstall it if you didn’t already:\n(ql:quickload \"djula\") The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like:\n(djula:add-template-directory (asdf:system-relative-pathname \"myproject\" \"templates/\")) and then we can declare and compile the ones we use, for example::\n(defparameter +base.html+ (djula:compile-template* \"base.html\")) (defparameter +products.html+ (djula:compile-template* \"products.html\")) Info If you get an error message when calling add-template-directory like this", "tags": [], "title": "Templates", "uri": "/building-blocks/templates/index.html" @@ -89,7 +89,7 @@ var relearn_searchindex = [ }, { "breadcrumb": "Building blocks", - "content": "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.\nThey should specially work across route redirects. So, they are typically created in the web session.\nHandling them involves those steps:\ncreate 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:\n(ql:quickload '(\"hunchentoot\" \"djula\" \"easy-routes\")) We also introduce a local nickname, to shorten the use of hunchentoot to ht:\n(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:\n(defparameter *port* 9876) (defvar *server* nil \"Our Hunchentoot acceptor\") (defun start (\u0026key (port *port*)) (format t \"~\u0026Starting the web server on port ~a~\u0026\" 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.\nThe important bits are:\nwe 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) (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)) ;; \u003c---- 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:\n(flash \"warning\" \"You are liking Lisp\") It’s easy, it’s handy, mission solved. Next.\nDelete flash messages when they are rendered For this, we use Hunchentoot’s life cycle and CLOS-orientation:\n;; 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 the current request, delete the :flash object from the session.\nRender flash messages in templates Set up Djula templates Create a new flash-template.html file.\n(djula:add-template-directory \"./\") (defparameter *flash-template* (djula:compile-template* \"flash-template.html\")) 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.\nHTML template This is our template. We use Bulma CSS to pimp it up and to use its notification blocks.\n\u003c!DOCTYPE html\u003e \u003chtml\u003e \u003chead\u003e \u003cmeta charset=\"utf-8\"\u003e \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"\u003e \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e \u003ctitle\u003eWALK - flash messages\u003c/title\u003e \u003c!-- Bulma Version 1--\u003e \u003clink rel=\"stylesheet\" href=\"https://unpkg.com/bulma@1.0.2/css/bulma.min.css\" /\u003e \u003c/head\u003e \u003cbody\u003e \u003c!-- START NAV --\u003e \u003cnav class=\"navbar is-white\"\u003e \u003cdiv class=\"container\"\u003e \u003cdiv class=\"navbar-brand\"\u003e \u003ca class=\"navbar-item brand-text\" href=\"#\"\u003e Bulma Admin \u003c/a\u003e \u003cdiv class=\"navbar-burger burger\" data-target=\"navMenu\"\u003e \u003cspan\u003e\u003c/span\u003e \u003c/div\u003e \u003c/div\u003e \u003cdiv id=\"navMenu\" class=\"navbar-menu\"\u003e \u003cdiv class=\"navbar-start\"\u003e \u003ca class=\"navbar-item\" href=\"#\"\u003e Home \u003c/a\u003e \u003ca class=\"navbar-item\" href=\"#\"\u003e Orders \u003c/a\u003e \u003c/div\u003e \u003c/div\u003e \u003c/div\u003e \u003c/nav\u003e \u003c!-- END NAV --\u003e \u003cdiv class=\"container\"\u003e \u003cdiv class=\"columns\"\u003e \u003cdiv class=\"column is-6\"\u003e \u003ch3 class=\"title is-4\"\u003e Flash messages. \u003c/h3\u003e \u003cdiv\u003e Click \u003ca href=\"/tryflash/\"\u003e/tryflash/\u003c/a\u003e to access an URL that creates a flash message and redirects you here.\u003c/div\u003e {% for flash in flashes %} \u003cdiv class=\"notification {{ flash.first }}\"\u003e \u003cbutton class=\"delete\"\u003e\u003c/button\u003e {{ flash.rest }} \u003c/div\u003e {% endfor %} \u003c/div\u003e \u003c/div\u003e \u003c/div\u003e \u003c/body\u003e \u003cscript\u003e // JS snippet to click the delete button of the notifications. // see https://bulma.io/documentation/elements/notification/ document.addEventListener('DOMContentLoaded', () =\u003e { (document.querySelectorAll('.notification .delete') || []).forEach(($delete) =\u003e { const $notification = $delete.parentNode; $delete.addEventListener('click', () =\u003e { $notification.parentNode.removeChild($notification); }); }); }); \u003c/script\u003e \u003c/html\u003e Look at\n{% for flash in flashes %} where we render our flash messages.\nDjula allows us to write {{ flash.first }} and {{ flash.rest }} to call the Lisp functions on those objects.\nWe must now create a route that renders our template.\nRoutes The /flash/ URL is the demo endpoint:\n(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.\nIn your application, you must add this parameter in all the existing routes. To make this easier, you can:\nuse Djula’s 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: (defun render (template \u0026rest 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:\n(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.\nYou should see a default notification. Click the β€œ/tryflash/” URL and you’ll see a flash message, that is deleted after use.\nRefresh the page, and you won’t see the flash message again.", + "content": "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.\nThey should specially work across route redirects. So, they are typically created in the web session.\nHandling them involves those steps:\ncreate 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:\n(ql:quickload '(\"hunchentoot\" \"djula\" \"easy-routes\")) We also introduce a local nickname, to shorten the use of hunchentoot to ht:\n(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:\n(defparameter *port* 9876) (defvar *server* nil \"Our Hunchentoot acceptor\") (defun start (\u0026key (port *port*)) (format t \"~\u0026Starting the web server on port ~a~\u0026\" 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.\nThe important bits are:\nwe 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) (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)) ;; \u003c---- 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:\n(flash \"warning\" \"You are liking Lisp\") It’s easy, it’s handy, mission solved. Next.\nDelete flash messages when they are rendered For this, we use Hunchentoot’s life cycle and CLOS-orientation:\n;; 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.\nWarning If your application sends API requests in JavaScript, they can delete flash messages without you noticing. Read more below.\nAn external API request (from the command line for example) is not concerned, as it doesn’t carry Hunchentoot session cookies.\nRender flash messages in templates Set up Djula templates Create a new flash-template.html file.\n(djula:add-template-directory \"./\") (defparameter *flash-template* (djula:compile-template* \"flash-template.html\")) 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.\nHTML template This is our template. We use Bulma CSS to pimp it up and to use its notification blocks.\n\u003c!DOCTYPE html\u003e \u003chtml\u003e \u003chead\u003e \u003cmeta charset=\"utf-8\"\u003e \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"\u003e \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e \u003ctitle\u003eWALK - flash messages\u003c/title\u003e \u003c!-- Bulma Version 1--\u003e \u003clink rel=\"stylesheet\" href=\"https://unpkg.com/bulma@1.0.2/css/bulma.min.css\" /\u003e \u003c/head\u003e \u003cbody\u003e \u003c!-- START NAV --\u003e \u003cnav class=\"navbar is-white\"\u003e \u003cdiv class=\"container\"\u003e \u003cdiv class=\"navbar-brand\"\u003e \u003ca class=\"navbar-item brand-text\" href=\"#\"\u003e Bulma Admin \u003c/a\u003e \u003cdiv class=\"navbar-burger burger\" data-target=\"navMenu\"\u003e \u003cspan\u003e\u003c/span\u003e \u003c/div\u003e \u003c/div\u003e \u003cdiv id=\"navMenu\" class=\"navbar-menu\"\u003e \u003cdiv class=\"navbar-start\"\u003e \u003ca class=\"navbar-item\" href=\"#\"\u003e Home \u003c/a\u003e \u003ca class=\"navbar-item\" href=\"#\"\u003e Orders \u003c/a\u003e \u003c/div\u003e \u003c/div\u003e \u003c/div\u003e \u003c/nav\u003e \u003c!-- END NAV --\u003e \u003cdiv class=\"container\"\u003e \u003cdiv class=\"columns\"\u003e \u003cdiv class=\"column is-6\"\u003e \u003ch3 class=\"title is-4\"\u003e Flash messages. \u003c/h3\u003e \u003cdiv\u003e Click \u003ca href=\"/tryflash/\"\u003e/tryflash/\u003c/a\u003e to access an URL that creates a flash message and redirects you here.\u003c/div\u003e {% for flash in flashes %} \u003cdiv class=\"notification {{ flash.first }}\"\u003e \u003cbutton class=\"delete\"\u003e\u003c/button\u003e {{ flash.rest }} \u003c/div\u003e {% endfor %} \u003c/div\u003e \u003c/div\u003e \u003c/div\u003e \u003c/body\u003e \u003cscript\u003e // JS snippet to click the delete button of the notifications. // see https://bulma.io/documentation/elements/notification/ document.addEventListener('DOMContentLoaded', () =\u003e { (document.querySelectorAll('.notification .delete') || []).forEach(($delete) =\u003e { const $notification = $delete.parentNode; $delete.addEventListener('click', () =\u003e { $notification.parentNode.removeChild($notification); }); }); }); \u003c/script\u003e \u003c/html\u003e Look at\n{% for flash in flashes %} where we render our flash messages.\nDjula allows us to write {{ flash.first }} and {{ flash.rest }} to call the Lisp functions on those objects.\nWe must now create a route that renders our template.\nRoutes The /flash/ URL is the demo endpoint:\n(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.\nIn your application, you must add this parameter in all the existing routes. To make this easier, you can:\nuse Djula’s 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: (defun render (template \u0026rest 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:\n(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.\nYou should see a default notification. Click the β€œ/tryflash/” URL and you’ll see a flash message, that is deleted after use.\nRefresh the page, and you won’t see the flash message again.\nfull 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:\nfetch(\"http://localhost:9876/api/\", { credentials: \"omit\" }) Otherwise, don’t use this :after method and delete flash messages explicitely in your non-API routes.\nWe could use a macro shortcut for this:\n(defmacro with-flash-messages ((messages) \u0026body body) `(let ((,messages (ht:session-value :flash))) (prog1 (progn ,@body) (ht:delete-session-value :flash)))) Use it like this:\n(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.", "description": "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.\nThey should specially work across route redirects. So, they are typically created in the web session.\nHandling them involves those steps:\ncreate 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:", "tags": [], "title": "Flash messages", @@ -129,7 +129,7 @@ var relearn_searchindex = [ }, { "breadcrumb": "Building blocks", - "content": "Let’s study different use cases:\ndo you have an existing database you want to read data from? do you want to create a new database? do you prefer CLOS orientation or to write SQL queries? We have many libraries to work with databases, let’s have a recap first.\nThe #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:\nwrappers to one database engine (cl-sqlite, postmodern, cl-redis, cl-duckdb…), ORMs (Mito), interfaces to several DB engines (cl-dbi, cl-yesql…), lispy SQL syntax (sxql…) in-memory persistent object databases (bknr.datastore, cl-prevalence,…), graph databases in pure Lisp (AllegroGraph, vivace-graph) or wrappers (neo4cl), object stores (cl-store, cl-naive-store…) and other tools (pgloader, which was re-written from Python to Common Lisp). Table of Contents\nConnect Run queries Insert rows User-level API Close connections The Mito ORM How to integrate the databases into the web frameworks Bonus: pimp your SQLite References How to query an existing database Let’s say you have a database called db.db and you want to extract data from it.\nFor our example, quickload this library:\n(ql:quickload \"cl-dbi\") cl-dbi can connect to major database engines: PostGres, SQLite, MySQL.\nOnce cl-dbi is loaded, you can access its functions with the dbi package prefix.\nConnect To connect to a database, use dbi:connect with paramaters the DB type, and its name:\n(defparameter *db-name* \"db.db\") (defvar *connection* nil \"the DB connection\") (defun connect () (if (uiop:file-exists-p *db-name*) (setf *connection* (dbi:connect :sqlite3 :database-name (get-db-name))) (format t \"The DB file ~a does not exist.\" *db-name*))) The available DB drivers are:\n:mysql :sqlite3 :postgres For the username and password, use the key arguments :username and :password.\nWhen you connect for the first time, cl-dbi will automatically quickload another dependency, depending on the driver. We advise to add the relevant one to your list of dependencies in your .asd file (or your binary will chok on a machine without Quicklisp, we learned this the hard way).\n:dbd-sqlite3 :dbd-mysql :dbd-postgres We can now run queries.\nRun queries Running a query is done is 3 steps:\nwrite the SQL query (in a string, with a lispy syntax…) dbi:prepare the query on a DB connection dbi:execute it and dbi:fetch-all results. (defparameter *select-products* \"SELECT * FROM products LIMIT 100\") (dbi:fetch-all (dbi:execute (dbi:prepare *connection* *select-products*))) This returns something like:\n((:|id| 1 :|title| \"Lisp Cookbook\" :|shelf_id| 1 :|tags_id| NIL :|cover_url| \"https://lispcookbook.github.io/cl-cookbook/orly-cover.png\" :|created_at| \"2024-11-07 22:49:23.972522Z\" :|updated_at| \"2024-12-30 20:55:51.044704Z\") (:|id| 2 :|title| \"Common Lisp Recipes\" :|shelf_id| 1 :|tags_id| NIL :|cover_url| \"\" :|created_at| \"2024-12-09 19:37:30.057172Z\" :|updated_at| \"2024-12-09 19:37:30.057172Z\")) We got a list of records where each record is a property list, a list alternating a key (as a keyword) and a value.\nNote how the keywords respect the case of our database fields with the :|id| notation.\nWith arguments, use a ? placeholder in your SQL query and give a list of arguments to dbi:execute:\n(defparameter *select-products* \"SELECT * FROM products WHERE flag = ? OR updated_at \u003e ?\") (let* ((query (dbi:prepare *connection* *select-products*)) (query (dbi:execute query (list 0 \"1984-01-01\")))) ;; \u003c--- list of arguments (loop for row = (dbi:fetch query) while row ;; process \"row\". )) Insert rows (straight from cl-dbi’s documentation)\ndbi:do-sql prepares and executes a single statement. It returns the number of rows affected. It’s typically used for non-SELECT statements.\n(dbi:do-sql *connection* \"INSERT INTO somewhere (flag, updated_at) VALUES (?, NOW())\" (list 0)) User-level API dbi offers more functions to fetch results than fetch-all.\nYou can use fetch to get one result at a time or again do-sql to run any SQL statement.\nconnect [driver-name \u0026 params] =\u003e \u003cdbi-connection\u003e connect-cached [driver-name \u0026 params] =\u003e \u003cdbi-connection\u003e disconnect [\u003cdbi-connection\u003e] =\u003e T or NIL prepare [conn sql] =\u003e \u003cdbi-query\u003e prepare-cached [conn sql] =\u003e \u003cdbi-query\u003e execute [query \u0026optional params] =\u003e something fetch [result] =\u003e a row data as plist fetch-all [result] =\u003e a list of all row data do-sql [conn sql \u0026optional params] list-all-drivers [] =\u003e (\u003cdbi-driver\u003e ..) find-driver [driver-name] =\u003e \u003cdbi-driver\u003e with-transaction [conn] begin-transaction [conn] commit [conn] rollback [conn] ping [conn] =\u003e T or NIL row-count [conn] =\u003e a number of rows modified by the last executed INSERT/UPDATE/DELETE with-connection [connection-variable-name \u0026body body] Close connections You should take care of closing the DB connection.\ndbi has a macro for that:\n(dbi:with-connection (conn :sqlite3 :database-name \"/home/fukamachi/test.db\") (let* ((query (dbi:prepare conn \"SELECT * FROM People\")) (query (dbi:execute query))) (loop for row = (dbi:fetch query) while row do (format t \"~A~%\" row)))) Inside this macro, conn binds to the current connection.\nThere is more but enough, please refer to cl-dbi’s README.\nThe Mito ORM The Mito ORM provides a nice object-oriented way to define schemas and query the database.\nIt supports SQLite3, PostgreSQL and MySQL, it has automatic migrations, db schema versioning, and more features.\nFor example, this is how one can define a user table with two columns:\n(mito:deftable user () ((name :col-type (:varchar 64)) (email :col-type (or (:varchar 128) :null)))) Once we create the table, we can create and insert user rows with methods such as create-dao:\n(mito:create-dao 'user :name \"Eitaro Fukamachi\" :email \"e.arrows@gmail.com\") Once we edit the table definition (aka the class definition), Mito will (by default) automatically migrate it.\nThere is much more to say, but we refer you to Mito’s good documentation and to the Cookbook.\nHow to integrate the databases into the web frameworks The web frameworks / web servers we use in this guide do not need anything special. Just use a DB driver and fetch results in your routes.\nUsing a DB connection per request with dbi:with-connection is a good idea.\nBonus: pimp your SQLite SQLite is a great database. It loves backward compatibility. As such, its default settings may not be optimal for a web application seeing some load. You might want to set some PRAGMA statements (SQLite settings).\nTo set them, look at your DB driver how to run a raw SQL query.\nWith cl-dbi, this would be dbi:do-sql:\n(dbi:do-sql *connection* \"PRAGMA cache_size = -20000;\") Here’s a nice list of pragmas useful for web development:\nhttps://briandouglas.ie/sqlite-defaults/ References CL Cookbook#databases Mito cl-dbi", + "content": "Let’s study different use cases:\ndo you have an existing database you want to read data from? do you want to create a new database? do you prefer CLOS orientation or to write SQL queries? We have many libraries to work with databases, let’s have a recap first.\nThe #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:\nwrappers to one database engine (cl-sqlite, postmodern, cl-redis, cl-duckdb…), ORMs (Mito), interfaces to several DB engines (cl-dbi, cl-yesql…), lispy SQL syntax (sxql…) in-memory persistent object databases (bknr.datastore, cl-prevalence,…), graph databases in pure Lisp (AllegroGraph, vivace-graph) or wrappers (neo4cl), object stores (cl-store, cl-naive-store…) and other tools (pgloader, which was re-written from Python to Common Lisp). Table of Contents\nConnect Run queries Insert rows User-level API Close connections The Mito ORM How to integrate the databases into the web frameworks Bonus: pimp your SQLite References How to query an existing database Let’s say you have a database called db.db and you want to extract data from it.\nFor our example, quickload this library:\n(ql:quickload \"cl-dbi\") cl-dbi can connect to major database engines: PostGres, SQLite, MySQL.\nOnce cl-dbi is loaded, you can access its functions with the dbi package prefix.\nConnect To connect to a database, use dbi:connect with paramaters the DB type, and its name:\n(defparameter *db-name* \"db.db\") (defvar *connection* nil \"the DB connection\") (defun connect () (if (uiop:file-exists-p *db-name*) (setf *connection* (dbi:connect :sqlite3 :database-name (get-db-name))) (format t \"The DB file ~a does not exist.\" *db-name*))) The available DB drivers are:\n:mysql :sqlite3 :postgres For the username and password, use the key arguments :username and :password.\nWhen you connect for the first time, cl-dbi will automatically quickload another dependency, depending on the driver. We advise to add the relevant one to your list of dependencies in your .asd file (or your binary will chok on a machine without Quicklisp, we learned this the hard way).\n:dbd-sqlite3 :dbd-mysql :dbd-postgres We can now run queries.\nRun queries Running a query is done is 3 steps:\nwrite the SQL query (in a string, with a lispy syntax…) dbi:prepare the query on a DB connection dbi:execute it and dbi:fetch-all results. (defparameter *select-products* \"SELECT * FROM products LIMIT 100\") (dbi:fetch-all (dbi:execute (dbi:prepare *connection* *select-products*))) This returns something like:\n((:|id| 1 :|title| \"Lisp Cookbook\" :|shelf_id| 1 :|tags_id| NIL :|cover_url| \"https://lispcookbook.github.io/cl-cookbook/orly-cover.png\" :|created_at| \"2024-11-07 22:49:23.972522Z\" :|updated_at| \"2024-12-30 20:55:51.044704Z\") (:|id| 2 :|title| \"Common Lisp Recipes\" :|shelf_id| 1 :|tags_id| NIL :|cover_url| \"\" :|created_at| \"2024-12-09 19:37:30.057172Z\" :|updated_at| \"2024-12-09 19:37:30.057172Z\")) We got a list of records where each record is a property list, a list alternating a key (as a keyword) and a value.\nNote how the keywords respect the case of our database fields with the :|id| notation.\nWith arguments, use a ? placeholder in your SQL query and give a list of arguments to dbi:execute:\n(defparameter *select-products* \"SELECT * FROM products WHERE flag = ? OR updated_at \u003e ?\") (let* ((query (dbi:prepare *connection* *select-products*)) (query (dbi:execute query (list 0 \"1984-01-01\")))) ;; \u003c--- list of arguments (loop for row = (dbi:fetch query) while row ;; process \"row\". )) Insert rows (straight from cl-dbi’s documentation)\ndbi:do-sql prepares and executes a single statement. It returns the number of rows affected. It’s typically used for non-SELECT statements.\n(dbi:do-sql *connection* \"INSERT INTO somewhere (flag, updated_at) VALUES (?, NOW())\" (list 0)) User-level API dbi offers more functions to fetch results than fetch-all.\nYou can use fetch to get one result at a time or again do-sql to run any SQL statement.\nconnect [driver-name \u0026 params] =\u003e \u003cdbi-connection\u003e connect-cached [driver-name \u0026 params] =\u003e \u003cdbi-connection\u003e disconnect [\u003cdbi-connection\u003e] =\u003e T or NIL prepare [conn sql] =\u003e \u003cdbi-query\u003e prepare-cached [conn sql] =\u003e \u003cdbi-query\u003e execute [query \u0026optional params] =\u003e something fetch [result] =\u003e a row data as plist fetch-all [result] =\u003e a list of all row data do-sql [conn sql \u0026optional params] list-all-drivers [] =\u003e (\u003cdbi-driver\u003e ..) find-driver [driver-name] =\u003e \u003cdbi-driver\u003e with-transaction [conn] begin-transaction [conn] commit [conn] rollback [conn] ping [conn] =\u003e T or NIL row-count [conn] =\u003e a number of rows modified by the last executed INSERT/UPDATE/DELETE with-connection [connection-variable-name \u0026body body] Close connections You should take care of closing the DB connection.\ndbi has a macro for that:\n(dbi:with-connection (conn :sqlite3 :database-name \"/home/fukamachi/test.db\") (let* ((query (dbi:prepare conn \"SELECT * FROM People\")) (query (dbi:execute query))) (loop for row = (dbi:fetch query) while row do (format t \"~A~%\" row)))) Inside this macro, conn binds to the current connection.\nThere is more but enough, please refer to cl-dbi’s README.\nThe Mito ORM The Mito ORM provides a nice object-oriented way to define schemas and query the database.\nIt supports SQLite3, PostgreSQL and MySQL, it has automatic migrations, db schema versioning, and more features.\nFor example, this is how one can define a user table with two columns:\n(mito:deftable user () ((name :col-type (:varchar 64)) (email :col-type (or (:varchar 128) :null)))) Once we create the table, we can create and insert user rows with methods such as create-dao:\n(mito:create-dao 'user :name \"Eitaro Fukamachi\" :email \"e.arrows@gmail.com\") Once we edit the table definition (aka the class definition), Mito will (by default) automatically migrate it.\nThere is much more to say, but we refer you to Mito’s good documentation and to the Cookbook (links below).\nHow to integrate the databases into the web frameworks The web frameworks / web servers we use in this guide do not need anything special. Just use a DB driver and fetch results in your routes.\nUsing a DB connection per request with dbi:with-connection is a good idea.\nBonus: pimp your SQLite SQLite is a great database. It loves backward compatibility. As such, its default settings may not be optimal for a web application seeing some load. You might want to set some PRAGMA statements (SQLite settings).\nTo set them, look at your DB driver how to run a raw SQL query.\nWith cl-dbi, this would be dbi:do-sql:\n(dbi:do-sql *connection* \"PRAGMA cache_size = -20000;\") Here’s a nice list of pragmas useful for web development:\nhttps://briandouglas.ie/sqlite-defaults/ References CL Cookbook#databases Mito cl-dbi", "description": "Let’s study different use cases:\ndo you have an existing database you want to read data from? do you want to create a new database? do you prefer CLOS orientation or to write SQL queries? We have many libraries to work with databases, let’s have a recap first.\nThe #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:", "tags": [], "title": "Connecting to a database", @@ -145,8 +145,8 @@ var relearn_searchindex = [ }, { "breadcrumb": "Building blocks", - "content": "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.\nInfo Stay tuned! We are on to something.\nCreating users If you use a database, you’ll have to create at least a users table. It would typically define:\na unique ID (integer, primary key) a name (varchar) an email (varchar) a password (varchar (and encrypted)) optionally, a key to the table listing roles. You can start with this:\nCREATE TABLE users ( id INTEGER PRIMARY KEY, username VARCHAR(255), email VARCHAR(255), password VARCHAR(255), ) You can run this right now with SQLite on the command line:\n$ sqlite3 db.db \"CREATE TABLE users (id INTEGER PRIMARY KEY, username VARCHAR(255), email VARCHAR(255), password VARCHAR(255))\" This creates the database if it doesn’t exist. SQLite reads SQL from the command line.\nCreate users:\n$ sqlite3 db.db \"INSERT INTO users VALUES(1,'Alice','alice@mail','xxx');\" Did it work? Run SELECT * FROM users;.\nEncrypting passwords With cl-bcrypt cl-bcrypt is a password hashing and verification library. It is as simple to use as this:\nCL-USER\u003e (defparameter *password* (bcrypt:make-password \"my-secret-password\")) *PASSWORD* and you can specify another salt, another cost factor and another algorithm identifier.\nThen you can use bcrypt:encode to get a string reprentation of the password:\nCL-USER\u003e (bcrypt:encode *password*) \"$2a$16$ClVzMvzfNyhFA94iLDdToOVeApbDppFru3JXNUyi1y1x6MkO0KzZa\" and you decode a password with decode.\nManually (with Ironclad) In this recipe we do the encryption and verification ourselves. We use the de-facto standard Ironclad cryptographic toolkit and the Babel charset encoding/decoding library.\nThe following snippet creates the password hash that should be stored in your database. Note that Ironclad expects a byte-vector, not a string.\n(defun password-hash (password) (ironclad:pbkdf2-hash-password-to-combined-string (babel:string-to-octets password))) pbkdf2 is defined in RFC2898. It uses a pseudorandom function to derive a secure encryption key based on the password.\nThe following function checks if a user is active and verifies the entered password. It returns the user-id if active and verified and nil in all other cases even if an error occurs. Adapt it to your application.\n(defun check-user-password (user password) (handler-case (let* ((data (my-get-user-data user)) (hash (my-get-user-hash data)) (active (my-get-user-active data))) (when (and active (ironclad:pbkdf2-check-password (babel:string-to-octets password) hash)) (my-get-user-id data))) (condition () nil))) And the following is an example on how to set the password on the database. Note that we use (password-hash password) to save the password. The rest is specific to the web framework and to the DB library.\n(defun set-password (user password) (with-connection (db) (execute (make-statement :update :web_user (set= :hash (password-hash password)) (make-clause :where (make-op := (if (integerp user) :id_user :email) user)))))) Credit: /u/arvid on /r/learnlisp.", - "description": "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.\nInfo Stay tuned! We are on to something.\nCreating users If you use a database, you’ll have to create at least a users table. It would typically define:", + "content": "We don’t know of a Common Lisp framework that will create users and roles for you and protect your routes all at the same time. We have building blocks but you’ll have to either write some glue Lisp code.\nYou can also turn to an external tool (such as Keycloak) that will provide all the industrial-grade user management.\nIf you like the Mito ORM, look at mito-auth and mito-email-auth.\nCreating users If you use a database, you’ll have to create at least a users table. It would typically define:\na unique ID (integer, primary key) a name (varchar) an email (varchar) a password (varchar (and encrypted)) optionally, a key to the table listing roles. You can start with this:\nCREATE TABLE users ( id INTEGER PRIMARY KEY, username VARCHAR(255), email VARCHAR(255), password VARCHAR(255), ) You can run this right now with SQLite on the command line:\n$ sqlite3 db.db \"CREATE TABLE users (id INTEGER PRIMARY KEY, username VARCHAR(255), email VARCHAR(255), password VARCHAR(255))\" This creates the database if it doesn’t exist. SQLite reads SQL from the command line.\nCreate users:\n$ sqlite3 db.db \"INSERT INTO users VALUES(1,'Alice','alice@mail','xxx');\" Did it work? Run SELECT * FROM users;.\nEncrypting passwords With cl-bcrypt cl-bcrypt is a password hashing and verification library. It is as simple to use as this:\nCL-USER\u003e (defparameter *password* (bcrypt:make-password \"my-secret-password\")) *PASSWORD* and you can specify another salt, another cost factor and another algorithm identifier.\nThen you can use bcrypt:encode to get a string reprentation of the password:\nCL-USER\u003e (bcrypt:encode *password*) \"$2a$16$ClVzMvzfNyhFA94iLDdToOVeApbDppFru3JXNUyi1y1x6MkO0KzZa\" and you decode a password with decode.\nManually (with Ironclad) In this recipe we do the encryption and verification ourselves. We use the de-facto standard Ironclad cryptographic toolkit and the Babel charset encoding/decoding library.\nThe following snippet creates the password hash that should be stored in your database. Note that Ironclad expects a byte-vector, not a string.\n(defun password-hash (password) (ironclad:pbkdf2-hash-password-to-combined-string (babel:string-to-octets password))) pbkdf2 is defined in RFC2898. It uses a pseudorandom function to derive a secure encryption key based on the password.\nThe following function checks if a user is active and verifies the entered password. It returns the user-id if active and verified and nil in all other cases even if an error occurs. Adapt it to your application.\n(defun check-user-password (user password) (handler-case (let* ((data (my-get-user-data user)) (hash (my-get-user-hash data)) (active (my-get-user-active data))) (when (and active (ironclad:pbkdf2-check-password (babel:string-to-octets password) hash)) (my-get-user-id data))) (condition () nil))) And the following is an example on how to set the password on the database. Note that we use (password-hash password) to save the password. The rest is specific to the web framework and to the DB library.\n(defun set-password (user password) (with-connection (db) (execute (make-statement :update :web_user (set= :hash (password-hash password)) (make-clause :where (make-op := (if (integerp user) :id_user :email) user)))))) Credit: /u/arvid on /r/learnlisp.\nSee also 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.", + "description": "We don’t know of a Common Lisp framework that will create users and roles for you and protect your routes all at the same time. We have building blocks but you’ll have to either write some glue Lisp code.\nYou can also turn to an external tool (such as Keycloak) that will provide all the industrial-grade user management.\nIf you like the Mito ORM, look at mito-auth and mito-email-auth.\nCreating users If you use a database, you’ll have to create at least a users table. It would typically define:", "tags": [], "title": "Users and passwords", "uri": "/building-blocks/users-and-passwords/index.html" diff --git a/docs/see-also/index.html b/docs/see-also/index.html index 116db1c..5986578 100644 --- a/docs/see-also/index.html +++ b/docs/see-also/index.html @@ -11,14 +11,14 @@ Neil Munro’s Clack/Lack/Ningle tutorial the Cookbook Project skeletons and demos: cl-cookieweb - a web project template lisp-web-template-productlist, a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS. lisp-web-live-reload-example - a toy project to show how to interact with a running web app. Libraries: awesome-cl">See also :: Web Apps in Lisp: Know-how -

See also

Other tutorials:

Project skeletons and demos:

  • cl-cookieweb - a web project template
  • lisp-web-template-productlist, +

Libraries:

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tags/index.html b/docs/tags/index.html index 62a865f..534aef1 100644 --- a/docs/tags/index.html +++ b/docs/tags/index.html @@ -1,10 +1,10 @@ Tags :: Web Apps in Lisp: Know-how -

Tags

\ No newline at end of file diff --git a/docs/tutorial/first-bonus-css/index.html b/docs/tutorial/first-bonus-css/index.html index 951f4a5..e4dbfc1 100644 --- a/docs/tutorial/first-bonus-css/index.html +++ b/docs/tutorial/first-bonus-css/index.html @@ -7,7 +7,7 @@ Optionally, we may write one
with a class="container" attribute, to have better margins.'>Bonus: pimp your CSS :: Web Apps in Lisp: Know-how -

Bonus: pimp your CSS

Bonus: pimp your CSS

Don’t ask a web developer to help you with the look and feel of the +

Bonus: pimp your CSS

Bonus: pimp your CSS

Don’t ask a web developer to help you with the look and feel of the app, they will bring in hundreds of megabytes of Nodejs dependencies :S We suggest a (nearly) one-liner to get a decent CSS with no efforts: by using a class-less CSS, such as @@ -47,12 +47,12 @@ ")

Refresh http://localhost:8899/?query=one. Do you enjoy the difference?!

I see this:

Β 

However note how our root template is benefiting from the CSS, and the product page isn’t. The two pages should inherit from a base template. It’s about time we setup our templates in their own -directory.

\ No newline at end of file diff --git a/docs/tutorial/first-build/index.html b/docs/tutorial/first-build/index.html index 09b1767..efba6eb 100644 --- a/docs/tutorial/first-build/index.html +++ b/docs/tutorial/first-build/index.html @@ -7,7 +7,7 @@ Info We didn’t have to restart any Lisp process, nor any web server.">the first build :: Web Apps in Lisp: Know-how -

the first build

We have developped a web app in Common Lisp.

At the first step, we opened a REPL and since then, without noticing, +

the first build

We have developped a web app in Common Lisp.

At the first step, we opened a REPL and since then, without noticing, we have compiled variables and function definitions pieces by pieces, step by step, with keyboard shortcuts giving immediate feedback, testing our progress on the go, running a function in the REPL or @@ -224,12 +224,12 @@ self-contained by default, without extra work: it contains the lisp implementation with its compiler and debugger, the libraries (web server and all), the templates. We can easily deploy this -app. Congrats!

Closing words

We are only scratching the surface of what we’ll want to do with a real app:

  • parse CLI args
  • handle a C-c and other signals
  • read configuration files
  • setup and use a database
  • properly use HTML templates
  • add CSS and other static assets
  • add interactivity on the web page, with or without JavaScript
  • add users login
  • add rights to the routes
  • build a REST API
  • etc

We will explore those topics in the other chapters of this guide.

We did a great job for a first app:

  • we built a Common Lisp web app
  • we created routes, used path and URL parameters
  • we defined an HTML form
  • we used HTML templates
  • we experienced the interactive nature of Common Lisp
  • we explored how to run, build and ship Common Lisp programs.

That’s a lot. You are ready for serious applications now!

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-form/index.html b/docs/tutorial/first-form/index.html index 861dbf2..5ea7daf 100644 --- a/docs/tutorial/first-form/index.html +++ b/docs/tutorial/first-form/index.html @@ -11,7 +11,7 @@ Do you find HTML forms boring, very boring tech? There is no escaping though, you must know the basics. Go read MDN. It’s only later that you’ll have the right to find and use libraries to do them for you. A search form Here’s a search form: (defparameter *template-root* "
") The important elements are the following:'>the first form :: Web Apps in Lisp: Know-how -

the first form

Our root page shows a list of products. We want to do better: provide +

the first form

Our root page shows a list of products. We want to do better: provide a search form.

Do you find HTML forms boring, very boring tech? There is no escaping though, you must know the basics. Go read MDN. It’s only later that you’ll have the right to find and use libraries to do them for you.

A search form

Here’s a search form:

(defparameter *template-root* "
 <form action=\"/\" method=\"GET\">
@@ -154,12 +154,12 @@
   (force-output)
   (setf *server* (make-instance 'easy-routes:easy-routes-acceptor
                                 :port (or port *port*)))
-  (hunchentoot:start *server*))

Do you also clearly see 3 different components in this app? Templates, models, routes.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-path-parameter/index.html b/docs/tutorial/first-path-parameter/index.html index 8973315..a5d0e91 100644 --- a/docs/tutorial/first-path-parameter/index.html +++ b/docs/tutorial/first-path-parameter/index.html @@ -15,7 +15,7 @@ Each product detail will be available on the URL /product/n where n is the product ID. Add links To begin with, let’s add links to the list of products: (defparameter *template-root* " Lisp web app ") Carefully observe that, in the href, we had to escape the quotes :/ This is the shortcoming of defining Djula templates as strings in .lisp files. It’s best to move them to their own directory and own files.'>the first path parameter :: Web Apps in Lisp: Know-how -

the first path parameter

So far we have 1 route that displays all our products.

We will make each product line clickable, to open a new page, that will show more details.

Each product detail will be available on the URL /product/n where n is the product ID.

To begin with, let’s add links to the list of products:

(defparameter *template-root* "
+

the first path parameter

So far we have 1 route that displays all our products.

We will make each product line clickable, to open a new page, that will show more details.

Each product detail will be available on the URL /product/n where n is the product ID.

To begin with, let’s add links to the list of products:

(defparameter *template-root* "
 <title> Lisp web app </title>
 <body>
   <ul>
@@ -77,12 +77,12 @@
 
 (easy-routes:defroute product-route ("/product/:n") (&path (n 'integer))
   (render *template-product* :product (get-product n)))

That’s better. Usually all my routes have this form: name, arguments, -call to some sort of render function with a template and arguments.

I’d like to carry on with features but let’s have a word about URL parameters.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-route/index.html b/docs/tutorial/first-route/index.html index fca7b23..0cf7036 100644 --- a/docs/tutorial/first-route/index.html +++ b/docs/tutorial/first-route/index.html @@ -15,7 +15,7 @@ Let’s start with a couple variables. ;; still in src/myproject.lisp (defvar *server* nil "Server instance (Hunchentoot acceptor).") (defparameter *port* 8899 "The application port.") Now hold yourself and let’s write our first route: (easy-routes:defroute root ("/") () "hello app") It only returns a string. We’ll use HTML templates in a second.'>the first route :: Web Apps in Lisp: Know-how -

the first route

It’s time we create a web app!

Our first route

Our very first route will only respond with a “hello world”.

Let’s start with a couple variables.

;; still in src/myproject.lisp
+

the first route

It’s time we create a web app!

Our first route

Our very first route will only respond with a “hello world”.

Let’s start with a couple variables.

;; still in src/myproject.lisp
 (defvar *server* nil
   "Server instance (Hunchentoot acceptor).")
 
@@ -38,12 +38,12 @@
 never have to wait for stuff re-compiling or re-loading. You’ll
 compile your code with a shortcut and have instant feedback. SBCL
 warns us on bad syntax, undefined variables and other typos, some type
-mismatches, unreachable code (meaning we might have an issue), etc.

To stop the app, use (hunchentoot:stop *server*). You can put this in a “stop-app” function.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-template/index.html b/docs/tutorial/first-template/index.html index bb38485..78457cb 100644 --- a/docs/tutorial/first-template/index.html +++ b/docs/tutorial/first-template/index.html @@ -15,7 +15,7 @@ We’ll use Djula HTML templates. Usually, templates go into their templates/ directory. But, we will start out by defining our first template inside our .lisp file: ;; scr/myproject.lisp (defparameter *template-root* " Lisp web app hello app ") Compile this variable as you go, with C-c C-c (or call again load from any REPL).'>the first template :: Web Apps in Lisp: Know-how -

the first template

Our route only returns a string:

(easy-routes:defroute root ("/") ()
+

the first template

Our route only returns a string:

(easy-routes:defroute root ("/") ()
     "hello app")

Can we have it return a template?

We’ll use Djula HTML templates.

Usually, templates go into their templates/ directory. But, we will start out by defining our first template inside our .lisp file:

;; scr/myproject.lisp
 (defparameter *template-root* "
@@ -131,12 +131,12 @@
   (force-output)
   (setf *server* (make-instance 'easy-routes:easy-routes-acceptor
                                 :port (or port *port*)))
-  (hunchentoot:start *server*))
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-url-parameters/index.html b/docs/tutorial/first-url-parameters/index.html index 5eb5a67..cc14dab 100644 --- a/docs/tutorial/first-url-parameters/index.html +++ b/docs/tutorial/first-url-parameters/index.html @@ -15,7 +15,7 @@ We want a debug URL parameter that will show us more data. The URL will accept a ?debug=t part. We have this product route: (easy-routes:defroute product-route ("/product/:n") (&path (n 'integer)) (render *template-product* :product (get-product n))) With or without the &path part, either is good for now, so let’s remove it for clarity:'>the first URL parameter :: Web Apps in Lisp: Know-how -

the first URL parameter

As of now we can access URLs like these: +

the first URL parameter

As of now we can access URLs like these: http://localhost:8899/product/0 where 0 is a path parameter.

We will soon need URL parameters so let’s add one to our routes.

We want a debug URL parameter that will show us more data. The URL will accept a ?debug=t part.

We have this product route:

(easy-routes:defroute product-route ("/product/:n") (&path (n 'integer))
@@ -90,12 +90,12 @@
                                 :port port))
   (hunchentoot:start *server*))

Everything is contained in one file, and we can run everything from sources or we can build a self-contained -binary. Pretty cool!

Before we do so, we’ll add a great feature: searching for products.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/getting-started/index.html b/docs/tutorial/getting-started/index.html index 642274c..f57c540 100644 --- a/docs/tutorial/getting-started/index.html +++ b/docs/tutorial/getting-started/index.html @@ -11,7 +11,7 @@ define a list of products, stored in a dummy database, we’ll have an index page with a search form, we’ll display search results, and have one page per product to see it in details. But everything starts with a project (a system in Lisp parlance) definition. Setting up the project Let’s create a regular Common Lisp project. We could start straihgt from the REPL, but we’ll need a project definition to save our project dependencies, and other metadata. Such a project is an ASDF file.">Getting Started :: Web Apps in Lisp: Know-how -

Getting Started

In this application we will:

  • define a list of products, stored in a dummy database,
  • we’ll have an index page with a search form,
  • we’ll display search results,
  • and have one page per product to see it in details.

But everything starts with a project (a system in Lisp parlance) definition.

Setting up the project

Let’s create a regular Common Lisp project.

We could start straihgt from the REPL, but we’ll need a project +

Getting Started

In this application we will:

  • define a list of products, stored in a dummy database,
  • we’ll have an index page with a search form,
  • we’ll display search results,
  • and have one page per product to see it in details.

But everything starts with a project (a system in Lisp parlance) definition.

Setting up the project

Let’s create a regular Common Lisp project.

We could start straihgt from the REPL, but we’ll need a project definition to save our project dependencies, and other metadata. Such a project is an ASDF file.

Create the file myproject.asd at the project root:

(asdf:defsystem "myproject"
   :version "0.1"
@@ -65,12 +65,12 @@
         collect (list i
                       (format nil "Product nb ~a" i)
                       9.99)))

this returns:

((0 "Product nb 0" 9.99) (1 "Product nb 1" 9.99) (2 "Product nb 2" 9.99)
- (3 "Product nb 3" 9.99) (4 "Product nb 4" 9.99))

That’s not much, but that’s enough to show content in a web app, which we’ll do next.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/index.html b/docs/tutorial/index.html index ba891fc..cefd7dc 100644 --- a/docs/tutorial/index.html +++ b/docs/tutorial/index.html @@ -11,17 +11,17 @@ In this first tutorial we will build a simple app that shows a web form that will search and display a list of products. In doing so, we will see many necessary building blocks to write web apps in Lisp: how to start a server how to create routes how to define and use path and URL parameters how to define HTML templates how to run and build the app, from our editor and from the terminal. In doing so, we’ll experience the interactive nature of Common Lisp and we’ll learn important commands: running sbcl from the command line with a couple options, what is load, how to interactively compile our app with a keyboard shortcut, how to structure a project with an .asd definition and a package, etc.">Tutorial part 1 :: Web Apps in Lisp: Know-how -

Tutorial part 1

Are you ready to build a web app in Common Lisp?

In this first tutorial we will build a simple app that shows a web +

Tutorial part 1

Are you ready to build a web app in Common Lisp?

In this first tutorial we will build a simple app that shows a web form that will search and display a list of products.

In doing so, we will see many necessary building blocks to write web apps in Lisp:

  • how to start a server
  • how to create routes
  • how to define and use path and URL parameters
  • how to define HTML templates
  • how to run and build the app, from our editor and from the terminal.

In doing so, we’ll experience the interactive nature of Common Lisp and we’ll learn important commands: running sbcl from the command line with a couple options, what is load, how to interactively compile our app with a keyboard shortcut, how to structure a project -with an .asd definition and a package, etc.

We expect that you have Quicklisp installed. If not, please see the Cookbook: getting started.

Now let’s get started.

But beware. There is no going back.

(look at the menu and don’t miss the small previous-next arrows on the top right)

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/templates/base/index.html b/docs/tutorial/templates/base/index.html new file mode 100644 index 0000000..0ef8a5a --- /dev/null +++ b/docs/tutorial/templates/base/index.html @@ -0,0 +1,10 @@ + +
\ No newline at end of file diff --git a/docs/tutorial/templates/base/index.xml b/docs/tutorial/templates/base/index.xml new file mode 100644 index 0000000..3d977f8 --- /dev/null +++ b/docs/tutorial/templates/base/index.xml @@ -0,0 +1 @@ +<link>http://example.org/tutorial/templates/base/index.html</link><description>hello base</description><generator>Hugo</generator><language>en-us</language><atom:link href="http://example.org/tutorial/templates/base/index.xml" rel="self" type="application/rss+xml"/></channel></rss> \ No newline at end of file From 40ddb0f64b0c85e65286788383292dc7ba39497b Mon Sep 17 00:00:00 2001 From: vindarel <vindarel@mailz.org> Date: Mon, 12 May 2025 01:57:06 +0200 Subject: [PATCH 14/20] fix user log in: we need easy-routes-acceptor --- content/building-blocks/user-log-in.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 () From e46e25a8b9af526798e27b0ad54f6fc9f07583d3 Mon Sep 17 00:00:00 2001 From: vindarel <vindarel@mailz.org> Date: Mon, 12 May 2025 01:59:45 +0200 Subject: [PATCH 15/20] [build] fix user log in: we need easy-routes-acceptor --- docs/building-blocks/user-log-in/index.html | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/building-blocks/user-log-in/index.html b/docs/building-blocks/user-log-in/index.html index 9919748..1f1a37d 100644 --- a/docs/building-blocks/user-log-in/index.html +++ b/docs/building-blocks/user-log-in/index.html @@ -10,10 +10,10 @@ What do we need to do exactly?"><meta property="og:locale" content="en_us"><meta property="og:type" content="article"><meta property="article:section" content="Building blocks"><meta itemprop=name content="User log-in :: Web Apps in Lisp: Know-how"><meta itemprop=description content="How do you check if a user is logged-in, and how do you do the actual log in? We show an example, without handling passwords yet. See the next section for passwords. We’ll build a simple log-in page to an admin/ private dashboard. -What do we need to do exactly?"><meta itemprop=wordCount content="1533"><title>User log-in :: Web Apps in Lisp: Know-how -

User log-in

How do you check if a user is logged-in, and how do you do the actual log in?

We show an example, without handling passwords yet. See the next section for passwords.

We’ll build a simple log-in page to an admin/ private dashboard.

-

-

What do we need to do exactly?

  • we need a function to get a user by its ID
  • we need a function to check that a password is correct for a given user
  • we need two templates:
    • a login template
    • a template for a logged-in user
  • we need a route and we need to handle a POST request
    • when the log-in is successful, we need to store a user ID in a web session

We choose to structure our app with an admin/ URL that will show +What do we need to do exactly?">User log-in :: Web Apps in Lisp: Know-how +

User log-in

How do you check if a user is logged-in, and how do you do the actual log in?

We show an example, without handling passwords yet. See the next section for passwords.

We’ll build a simple log-in page to an admin/ private dashboard.

+

+

What do we need to do exactly?

  • we need a function to get a user by its ID
  • we need a function to check that a password is correct for a given user
  • we need two templates:
    • a login template
    • a template for a logged-in user
  • we need a route and we need to handle a POST request
    • when the log-in is successful, we need to store a user ID in a web session

We choose to structure our app with an admin/ URL that will show both the login page or the “dashboard” for logged-in users.

We use these libraries:

(ql:quickload '("hunchentoot" "djula" "easy-routes"))

We still work from inside our :myproject package. You should have this at the top of your file:

(in-package :myproject)

Let’s start with the model functions.

Get users

(defun get-user (name)
   (list :name name :password "demo")) ;; <--- all our passwords are "demo"

Yes indeed, that’s a dummy function. You will add your own logic @@ -134,7 +134,10 @@ (render *template-welcome* :name name)) (t (render *template-login* :name name :error t)))) - ))

Remarks:

  • we can’t dispatch on the request type, so we use the ecase on request-method*
  • we can’t use “decorators” so we use branching
  • it isn’t very clear but name and password are only used in the POST part.
    • we can also use (hunchentoot:post-parameter "name") (the parameter as a string)
  • all this adds nesting in our function but otherwise, it’s pretty similar.

Full code

(in-package :myproject)
+      ))

Remarks:

  • we can’t dispatch on the request type, so we use the ecase on request-method*
  • we can’t use “decorators” so we use branching
  • it isn’t very clear but name and password are only used in the POST part.
    • we can also use (hunchentoot:post-parameter "name") (the parameter as a string)
  • all this adds nesting in our function but otherwise, it’s pretty similar.

Full code

(defpackage :myproject
+  (:use :cl))
+
+(in-package :myproject)
 
 ;; User-facing paramaters.
 (defparameter *port* 8899)
@@ -241,8 +244,7 @@
 ;; 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 ()
@@ -270,12 +272,12 @@
 (defroute ("/account/review" :method :get) ()
   (with-logged-in
     (render #p"review.html"
-            (list :review (get-review (gethash :user *session*))))))

and so on.

\ No newline at end of file From f9a17833f5f30780147a94f29860653022955ca3 Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 29 May 2025 13:14:31 +0200 Subject: [PATCH 16/20] mention Tesseral --- content/building-blocks/users-and-passwords.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/building-blocks/users-and-passwords.md b/content/building-blocks/users-and-passwords.md index 363e606..9ab5e75 100644 --- a/content/building-blocks/users-and-passwords.md +++ b/content/building-blocks/users-and-passwords.md @@ -5,9 +5,9 @@ weight = 130 We don't know of a Common Lisp framework that will create users and roles for you and protect your routes all at the same time. We have -building blocks but you'll have to either write some glue Lisp code. +building blocks but you'll have to write some glue Lisp code. -You can also turn to an external tool (such as Keycloak) that will +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. 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). From 4d1f542f3e13c852fe866e062f086bc2ba27ba90 Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 29 May 2025 13:17:01 +0200 Subject: [PATCH 17/20] build --- .../users-and-passwords/index.html | 26 +++++++++---------- .../users-and-passwords/index.xml | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/building-blocks/users-and-passwords/index.html b/docs/building-blocks/users-and-passwords/index.html index 87274e3..0793bf4 100644 --- a/docs/building-blocks/users-and-passwords/index.html +++ b/docs/building-blocks/users-and-passwords/index.html @@ -1,19 +1,19 @@ -Users and passwords :: Web Apps in Lisp: Know-how -

Users and passwords

We don’t know of a Common Lisp framework that will create users and +

Users and passwords

We don’t know of a Common Lisp framework that will create users and roles for you and protect your routes all at the same time. We have -building blocks but you’ll have to either write some glue Lisp code.

You can also turn to an external tool (such as Keycloak) that will +building blocks but you’ll have to write some glue Lisp code.

You can also turn to external tools (such as Keycloak or Tesseral) that will provide all the industrial-grade user management.

If you like the Mito ORM, look at mito-auth and mito-email-auth.

Creating users

If you use a database, you’ll have to create at least a users table. It would typically define:

  • a unique ID (integer, primary key)
  • a name (varchar)
  • an email (varchar)
  • a password (varchar (and encrypted))
  • optionally, a key to the table listing roles.

You can start with this:

CREATE TABLE users (
   id INTEGER PRIMARY KEY,
@@ -54,12 +54,12 @@
                                   (make-op := (if (integerp user)
                                                   :id_user
                                                   :email)
-                                           user))))))

Credit: /u/arvid on /r/learnlisp.

See also

  • 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.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/users-and-passwords/index.xml b/docs/building-blocks/users-and-passwords/index.xml index b2f1993..ce44c91 100644 --- a/docs/building-blocks/users-and-passwords/index.xml +++ b/docs/building-blocks/users-and-passwords/index.xml @@ -1,4 +1,4 @@ -Users and passwords :: Web Apps in Lisp: Know-howhttp://example.org/building-blocks/users-and-passwords/index.htmlWe don’t know of a Common Lisp framework that will create users and roles for you and protect your routes all at the same time. We have building blocks but you’ll have to either write some glue Lisp code. -You can also turn to an external tool (such as Keycloak) that will provide all the industrial-grade user management. +Users and passwords :: Web Apps in Lisp: Know-howhttp://example.org/building-blocks/users-and-passwords/index.htmlWe don’t know of a Common Lisp framework that will create users and 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. +You can also turn to external tools (such as Keycloak or Tesseral) that will provide all the industrial-grade user management. If you like the Mito ORM, look at mito-auth and mito-email-auth. Creating users If you use a database, you’ll have to create at least a users table. It would typically define:Hugoen-us \ No newline at end of file From 3bc9fb17faea71dddc9e9ae60b8be99ebc307895 Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 28 Aug 2025 12:31:48 +0200 Subject: [PATCH 18/20] PUT body parameters thanks https://github.com/mmontone/easy-routes/issues/18 --- content/building-blocks/PUT.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 content/building-blocks/PUT.md 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. From 379b16e42934a3d3fabbf64ffcefbfe5d6b960aa Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 28 Aug 2025 12:34:00 +0200 Subject: [PATCH 19/20] (minor) build.lisp with uiop:dump-image --- content/tutorial/build.lisp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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) + +|# From 1d969b1dde0239f43dec2a855fe3aa9980bbad49 Mon Sep 17 00:00:00 2001 From: vindarel Date: Thu, 28 Aug 2025 12:35:14 +0200 Subject: [PATCH 20/20] build --- docs/404.html | 2 +- .../building-binaries/index.html | 8 +++---- docs/building-blocks/database/index.html | 8 +++---- docs/building-blocks/deployment/index.html | 8 +++---- docs/building-blocks/electron/index.html | 12 +++++----- .../errors-interactivity/index.html | 16 ++++++------- .../building-blocks/flash-messages/index.html | 12 +++++----- .../building-blocks/flash-template/index.html | 8 +++---- .../form-validation/index.html | 8 +++---- docs/building-blocks/headers/index.html | 8 +++---- docs/building-blocks/index.html | 8 +++---- docs/building-blocks/index.xml | 9 ++++--- docs/building-blocks/put/index.html | 24 +++++++++++++++++++ docs/building-blocks/put/index.xml | 4 ++++ .../remote-debugging/index.html | 8 +++---- docs/building-blocks/routing/index.html | 8 +++---- docs/building-blocks/session/index.html | 8 +++---- .../simple-web-server/index.html | 8 +++---- docs/building-blocks/static/index.html | 8 +++---- docs/building-blocks/templates/index.html | 8 +++---- docs/building-blocks/user-log-in/index.html | 12 +++++----- .../users-and-passwords/index.html | 8 +++---- docs/building-blocks/web-views/index.html | 12 +++++----- docs/categories/index.html | 6 ++--- docs/css/chroma-auto.css | 4 ++-- docs/css/format-print.css | 4 ++-- docs/css/print.css | 2 +- docs/css/swagger.css | 4 ++-- docs/css/theme-auto.css | 4 ++-- docs/css/theme.css | 2 +- docs/index.html | 12 +++++----- .../isomorphic-web-frameworks/clog/index.html | 16 ++++++------- docs/isomorphic-web-frameworks/index.html | 8 +++---- .../weblocks/index.html | 12 +++++----- docs/search/index.html | 8 +++---- docs/searchindex.js | 14 ++++++++--- docs/see-also/index.html | 8 +++---- docs/sitemap.xml | 2 +- docs/tags/index.html | 6 ++--- docs/tutorial/build.lisp | 14 +++++++---- docs/tutorial/first-bonus-css/index.html | 8 +++---- docs/tutorial/first-build/index.html | 8 +++---- docs/tutorial/first-form/index.html | 8 +++---- docs/tutorial/first-path-parameter/index.html | 8 +++---- docs/tutorial/first-route/index.html | 8 +++---- docs/tutorial/first-template/index.html | 8 +++---- docs/tutorial/first-url-parameters/index.html | 8 +++---- docs/tutorial/getting-started/index.html | 8 +++---- docs/tutorial/index.html | 8 +++---- docs/tutorial/templates/base/index.html | 6 ++--- 50 files changed, 231 insertions(+), 188 deletions(-) create mode 100644 docs/building-blocks/put/index.html create mode 100644 docs/building-blocks/put/index.xml diff --git a/docs/404.html b/docs/404.html index 7679e3b..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 Β―\_(ツ)_/Β―.

Go to homepage

\ No newline at end of file +

44

Not found

Whoops. Looks like this page doesn't exist Β―\_(ツ)_/Β―.

Go to homepage

\ No newline at end of file diff --git a/docs/building-blocks/building-binaries/index.html b/docs/building-blocks/building-binaries/index.html index 295cbad..83408ec 100644 --- a/docs/building-blocks/building-binaries/index.html +++ b/docs/building-blocks/building-binaries/index.html @@ -11,7 +11,7 @@ To run our Lisp code from source, as a script, we can use the --load switch from our implementation. We must ensure: to load the project’s .asd system declaration (if any) to install the required dependencies (this demands we have installed Quicklisp previously) and to run our application’s entry point. So, the recipe to run our project from sources can look like this (you can find such a recipe in our project generator):">Running and Building :: Web Apps in Lisp: Know-how -

Running and Building

Running the application from source

Info

See the tutorial.

To run our Lisp code from source, as a script, we can use the --load +

Running and Building

Running the application from source

Info

See the tutorial.

To run our Lisp code from source, as a script, we can use the --load switch from our implementation.

We must ensure:

  • to load the project’s .asd system declaration (if any)
  • to install the required dependencies (this demands we have installed Quicklisp previously)
  • and to run our application’s entry point.

So, the recipe to run our project from sources can look like this (you can find such a recipe in our project generator):

;; run.lisp
 
 (load "myproject.asd")
@@ -40,12 +40,12 @@
 stops. That happens because the server thread is started in the background, and nothing tells the binary to wait for it. We can simply sleep (for a large-enough amount of time).

(defun main ()
   (start-app :port 9003) ;; our start-app
   ;; keep the binary busy in the foreground, for binaries and SystemD.
-  (sleep most-positive-fixnum))

If you want to learn more, see… the Cookbook: scripting, command line arguments, executables.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/database/index.html b/docs/building-blocks/database/index.html index 97f743d..d5b703c 100644 --- a/docs/building-blocks/database/index.html +++ b/docs/building-blocks/database/index.html @@ -7,7 +7,7 @@ The #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:">Connecting to a database :: Web Apps in Lisp: Know-how -

Connecting to a database

Let’s study different use cases:

  • do you have an existing database you want to read data from?
  • do you want to create a new database?
    • do you prefer CLOS orientation
    • or to write SQL queries?

We have many libraries to work with databases, let’s have a recap first.

The #database section on the awesome-cl list +

Connecting to a database

Let’s study different use cases:

  • do you have an existing database you want to read data from?
  • do you want to create a new database?
    • do you prefer CLOS orientation
    • or to write SQL queries?

We have many libraries to work with databases, let’s have a recap first.

The #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:

  • wrappers to one database engine (cl-sqlite, postmodern, cl-redis, cl-duckdb…),
  • ORMs (Mito),
  • interfaces to several DB engines (cl-dbi, cl-yesql…),
  • lispy SQL syntax (sxql…)
  • in-memory persistent object databases (bknr.datastore, cl-prevalence,…),
  • graph databases in pure Lisp (AllegroGraph, vivace-graph) or wrappers (neo4cl),
  • object stores (cl-store, cl-naive-store…)
  • and other tools (pgloader, which was re-written from Python to Common Lisp).

Table of Contents

How to query an existing database

Let’s say you have a database called db.db and you want to extract data from it.

For our example, quickload this library:

(ql:quickload "cl-dbi")

cl-dbi can connect to major @@ -66,12 +66,12 @@ routes.

Using a DB connection per request with dbi:with-connection is a good idea.

Bonus: pimp your SQLite

SQLite is a great database. It loves backward compatibility. As such, its default settings may not be optimal for a web application seeing some load. You might want to set some PRAGMA -statements (SQLite settings).

To set them, look at your DB driver how to run a raw SQL query.

With cl-dbi, this would be dbi:do-sql:

(dbi:do-sql *connection* "PRAGMA cache_size = -20000;")

Here’s a nice list of pragmas useful for web development:

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/deployment/index.html b/docs/building-blocks/deployment/index.html index 77e36f3..c352cd2 100644 --- a/docs/building-blocks/deployment/index.html +++ b/docs/building-blocks/deployment/index.html @@ -15,7 +15,7 @@ Deploying manually We can start our executable in a shell and send it to the background (C-z bg), or run it inside a tmux session. These are not the best but hey, it worksΒ©. Here’s a tmux crashcourse: start a tmux session with tmux inside a session, C-b is tmux’s modifier key. use C-b c to create a new tab, C-b n and C-b p for β€œnext” and β€œprevious” tab/window. use C-b d to detach tmux and come back to your original console. Everything you started in tmux still runs in the background. use C-g to cancel a current prompt (as in Emacs). tmux ls lists the running tmux sessions. tmux attach goes back to a running session. tmux attach -t attaches to the session named β€œname”. inside a session, use C-b $ to name the current session, so you can see it with tmux ls. Here’s a cheatsheet that was handy.">Deployment :: Web Apps in Lisp: Know-how -

Deployment

How to deploy and monitor a Common Lisp web app?

Info

We are re-using content we contributed to the Cookbook.

Deploying manually

We can start our executable in a shell and send it to the background (C-z bg), or run it inside a tmux session. These are not the best but hey, it worksΒ©.

Here’s a tmux crashcourse:

  • start a tmux session with tmux
  • inside a session, C-b is tmux’s modifier key.
    • use C-b c to create a new tab, C-b n and C-b p for “next” and “previous” tab/window.
    • use C-b d to detach tmux and come back to your original console. Everything you started in tmux still runs in the background.
    • use C-g to cancel a current prompt (as in Emacs).
  • tmux ls lists the running tmux sessions.
  • tmux attach goes back to a running session.
    • tmux attach -t <name> attaches to the session named “name”.
    • inside a session, use C-b $ to name the current session, so you can see it with tmux ls.

Here’s a cheatsheet that was handy.

Unfortunately, if your app crashes or if your server is rebooted, your apps will be stopped. We can do better.

SystemD: daemonizing, restarting in case of crashes, handling logs

This is actually a system-specific task. See how to do that on your system.

Most GNU/Linux distros now come with Systemd, so here’s a little example.

Deploying an app with Systemd is as simple as writing a configuration file:

$ emacs -nw /etc/systemd/system/my-app.service
+

Deployment

How to deploy and monitor a Common Lisp web app?

Info

We are re-using content we contributed to the Cookbook.

Deploying manually

We can start our executable in a shell and send it to the background (C-z bg), or run it inside a tmux session. These are not the best but hey, it worksΒ©.

Here’s a tmux crashcourse:

  • start a tmux session with tmux
  • inside a session, C-b is tmux’s modifier key.
    • use C-b c to create a new tab, C-b n and C-b p for “next” and “previous” tab/window.
    • use C-b d to detach tmux and come back to your original console. Everything you started in tmux still runs in the background.
    • use C-g to cancel a current prompt (as in Emacs).
  • tmux ls lists the running tmux sessions.
  • tmux attach goes back to a running session.
    • tmux attach -t <name> attaches to the session named “name”.
    • inside a session, use C-b $ to name the current session, so you can see it with tmux ls.

Here’s a cheatsheet that was handy.

Unfortunately, if your app crashes or if your server is rebooted, your apps will be stopped. We can do better.

SystemD: daemonizing, restarting in case of crashes, handling logs

This is actually a system-specific task. See how to do that on your system.

Most GNU/Linux distros now come with Systemd, so here’s a little example.

Deploying an app with Systemd is as simple as writing a configuration file:

$ emacs -nw /etc/systemd/system/my-app.service
 [Unit]
 Description=stupid simple example
 
@@ -61,12 +61,12 @@
 app on localhost.

Reload nginx (send the “reload” signal):

$ nginx -s reload
 

and that’s it: you can access your Lisp app from the outside through http://www.your-domain-name.org.

Deploying on Heroku, Digital Ocean, OVH, Deploy.sh and other services

See:

Cloud Init

You can take inspiration from this Cloud Init file for SBCL, an init file for providers supporting the cloudinit format (DigitalOcean etc).

Monitoring

See Prometheus.cl for a Grafana dashboard for SBCL and Hunchentoot metrics (memory, -threads, requests per second,…).

See cl-sentry-client for error reporting.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/electron/index.html b/docs/building-blocks/electron/index.html index 036fa6a..d8ac2ea 100644 --- a/docs/building-blocks/electron/index.html +++ b/docs/building-blocks/electron/index.html @@ -11,7 +11,7 @@ Advise: study it before discarding it. It isn’t however the only portable web view solution. See our next section. Info This page appeared first on lisp-journey: three web views for Common Lisp, cross-platform GUIs.">Electron :: Web Apps in Lisp: Know-how -

Electron

Electron is heavy, but really cross-platform, and it has many tools +

Electron

Electron is heavy, but really cross-platform, and it has many tools around it. It allows to build releases for the three major OS from your development machine, its ecosystem has tools to handle updates, etc.

Advise: study it before discarding it.

It isn’t however the only portable web view solution. See our next section.

Ceramic (old but works)

Ceramic is a set of utilities @@ -33,8 +33,8 @@ localhost:[PORT] in Ceramic/Electron. That’s it.

Ceramic wasn’t updated in five years as of date and it downloads an outdated version of Electron by default (see (defparameter *electron-version* "5.0.2")), but you can change the version yourself.

The new Neomacs project, a structural editor and web browser, is a great modern example on how to use Ceramic. Give it a look and give it a try!

What Ceramic actually does is abstracted away in the CL functions, so I think it isn’t the best to start with. We can do without it to -understand the full process, here’s how.

Electron from scratch

Here’s our web app embedded in Electron:

-

Our steps are the following:

You can also run the Lisp web app from sources, of course, without +understand the full process, here’s how.

Electron from scratch

Here’s our web app embedded in Electron:

+

Our steps are the following:

You can also run the Lisp web app from sources, of course, without building a binary, but then you’ll have to ship all the lisp sources.

main.js

The most important file to an Electron app is the main.js. The one we show below does the following:

  • it starts Electron
  • it starts our web application on the side, as a child process, from a binary name, and a port.
  • it shows our child process’ stdout and stderr
  • it opens a browser window to show our app, running on localhost.
  • it handles the close event.

Here’s our version.

console.log(`Hello from Electron πŸ‘‹`)
 
 const { app, BrowserWindow } = require('electron')
@@ -119,12 +119,12 @@
 the binary into the Electron release.

Then, if you want to communicate from the Lisp app to the Electron window, and the other way around, you’ll have to use the JavaScript layers. Ceramic might help here.

What about Tauri?

Bundling an app with Tauri will, AFAIK (I just tried quickly), involve the same steps than with Electron. Tauri might -still have less tools for it. You need the Rust toolchain.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/errors-interactivity/index.html b/docs/building-blocks/errors-interactivity/index.html index 8722885..918d64a 100644 --- a/docs/building-blocks/errors-interactivity/index.html +++ b/docs/building-blocks/errors-interactivity/index.html @@ -7,19 +7,19 @@ not being interactive: it returns a 404 page and prints the error output on the REPL not interactive but developper friendly: it can show the lisp error message on the HTML page, as well as the full Lisp backtrace it can let the developper have the interactive debugger: in that case the framework doesn’t catch the error, it lets it pass through, and we the developper deal with it as in a normal Slime session. We see this by default:">Errors and interactivity :: Web Apps in Lisp: Know-how -

Errors and interactivity

In all frameworks, we can choose the level of interactivity.

The web framework can be interactive in different degrees. What should +

Errors and interactivity

In all frameworks, we can choose the level of interactivity.

The web framework can be interactive in different degrees. What should it do when an error happens?

  • not being interactive: it returns a 404 page and prints the error output on the REPL
  • not interactive but developper friendly: it can show the lisp error message on the HTML page,
  • as well as the full Lisp backtrace
  • it can let the developper have the interactive debugger: in that case the framework doesn’t catch the error, it lets it pass through, and -we the developper deal with it as in a normal Slime session.

We see this by default:

-

but we can also show backtraces and augment the data.

Hunchentoot

The global variables to set are

  • *show-lisp-errors-p*, nil by default
  • *show-lisp-backtraces-p*, t by default (but the backtrace won’t be shown if the previous setting is nil)
  • *catch-errors-p*, t by default, to set to nil to get the interactive debugger.

We can see the backtrace on our error page:

(setf hunchentoot:*show-lisp-errors-p* t)

-

Enhancing the backtrace in the browser

We can also the library hunchentoot-errors to augment the data we see in the stacktrace:

  • show the current request (URI, headers…)
  • show the current session (everything you stored in the session)

-

Clack errors

When you use the Clack web server, you can use Clack errors, which can also show the backtrace and the session, with a colourful output:

-

Hunchentoot’s conditions

Hunchentoot defines condition classes:

  • hunchentoot-warning, the superclass for all warnings
  • hunchentoot-error, the superclass for errors
  • parameter-error
  • bad-request

See the (light) documentation: https://edicl.github.io/hunchentoot/#conditions.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/flash-messages/index.html b/docs/building-blocks/flash-messages/index.html index f5909d9..77d4ac0 100644 --- a/docs/building-blocks/flash-messages/index.html +++ b/docs/building-blocks/flash-messages/index.html @@ -11,10 +11,10 @@ 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:">Flash messages :: Web Apps in Lisp: Know-how -

Flash messages

Flash messages are temporary messages you want to show to your +

Flash messages

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 +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:

(ql:quickload '("hunchentoot" "djula" "easy-routes"))

We also introduce a local nickname, to shorten the use of hunchentoot to ht:

(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:

(defparameter *port* 9876)
 (defvar *server* nil "Our Hunchentoot acceptor")
@@ -157,12 +157,12 @@
                              :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.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/flash-template/index.html b/docs/building-blocks/flash-template/index.html index f60adda..1f3dadd 100644 --- a/docs/building-blocks/flash-template/index.html +++ b/docs/building-blocks/flash-template/index.html @@ -1,13 +1,13 @@ -

WALK - flash messages +

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 %}
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/form-validation/index.html b/docs/building-blocks/form-validation/index.html index 7a57cab..04de508 100644 --- a/docs/building-blocks/form-validation/index.html +++ b/docs/building-blocks/form-validation/index.html @@ -11,7 +11,7 @@ See also the cl-forms library that offers many features: automatic forms form validation with in-line error messages CSRF protection client-side validation subforms Djula and Spinneret renderers default themes an online demo for you to try etc Get them: (ql:quickload '(:clavier :cl-forms)) Form validation with the Clavier library Clavier defines validators as class instances. They come in many types, for example the 'less-than, greater-than or len validators. They may take an initialization argument.">Form validation :: Web Apps in Lisp: Know-how -

Form validation

We can recommend the clavier +

Form validation

We can recommend the clavier library for input validation.

See also the cl-forms library that offers many features:

  • automatic forms
  • form validation with in-line error messages
  • CSRF protection
  • client-side validation
  • subforms
  • Djula and Spinneret renderers
  • default themes
  • an online demo for you to try
  • etc

Get them:

(ql:quickload '(:clavier :cl-forms))

Form validation with the Clavier library

Clavier defines validators as class instances. They come in many types, for example the 'less-than, @@ -73,12 +73,12 @@ NIL ("Length of \"foo\" is less than 5")

Note that Clavier has a “validator-collection” thing, but not shown in the README, and is in our opininion too verbose in comparison to a -simple list.

\ No newline at end of file diff --git a/docs/building-blocks/headers/index.html b/docs/building-blocks/headers/index.html index b55ecb8..79d1925 100644 --- a/docs/building-blocks/headers/index.html +++ b/docs/building-blocks/headers/index.html @@ -19,14 +19,14 @@ (setf (hunchentoot:header-out "HX-Trigger") "myEvent") This sets the header of the current request. USe headers-out (plural) to get an association list of headers: An alist of the outgoing http headers not including the β€˜Set-Cookie’, β€˜Content-Length’, and β€˜Content-Type’ headers. Use the functions HEADER-OUT and (SETF HEADER-OUT) to modify this slot.'>Headers :: Web Apps in Lisp: Know-how -

Headers

A quick reference.

These functions have to be used in the context of a web request.

Set headers

Use header-out to set headers, like this:

(setf (hunchentoot:header-out "HX-Trigger") "myEvent")

This sets the header of the current request.

USe headers-out (plural) to get an association list of headers:

An alist of the outgoing http headers not including the ‘Set-Cookie’, ‘Content-Length’, and ‘Content-Type’ headers. Use the functions HEADER-OUT and (SETF HEADER-OUT) to modify this slot.

Get headers

Use the header-in* and headers-in* (plural) function:

Function: (header-in* name &optional (request *request*))
+

Headers

A quick reference.

These functions have to be used in the context of a web request.

Set headers

Use header-out to set headers, like this:

(setf (hunchentoot:header-out "HX-Trigger") "myEvent")

This sets the header of the current request.

USe headers-out (plural) to get an association list of headers:

An alist of the outgoing http headers not including the ‘Set-Cookie’, ‘Content-Length’, and ‘Content-Type’ headers. Use the functions HEADER-OUT and (SETF HEADER-OUT) to modify this slot.

Get headers

Use the header-in* and headers-in* (plural) function:

Function: (header-in* name &optional (request *request*))
 

Returns the incoming header with name NAME. NAME can be a keyword (recommended) or a string.

headers-in*:

Function: (headers-in* &optional (request *request*))
-

Returns an alist of the incoming headers associated with the REQUEST object REQUEST.

Reference

Find some more here:

Returns an alist of the incoming headers associated with the REQUEST object REQUEST.

Reference

Find some more here:

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/index.html b/docs/building-blocks/index.html index f065bc6..8afee6b 100644 --- a/docs/building-blocks/index.html +++ b/docs/building-blocks/index.html @@ -19,7 +19,7 @@ We will use the Hunchentoot web server, but we should say a few words about Clack too. Hunchentoot is a web server and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL. It provides facilities like automatic session handling (with and without cookies), logging, customizable error handling, and easy access to GET and POST parameters sent by the client.'>Building blocks :: Web Apps in Lisp: Know-how -

Building blocks

In this chapter we’ll create routes, we’ll serve +

Building blocks

In this chapter we’ll create routes, we’ll serve local files and we’ll run more than one web app in the same running image.

We’ll use those libraries:

(ql:quickload '("hunchentoot" "easy-routes" "djula" "spinneret"))
Info

You can create a web project with our project generator: cl-cookieweb.

We will use the Hunchentoot web server, but we should say a few words about Clack too.

Hunchentoot is

a web server and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL. It provides facilities like automatic session handling (with and without cookies), logging, customizable error handling, and easy access to GET and POST parameters sent by the client.

It is a software written by Edi Weitz (author of the “Common Lisp Recipes” book, of the ubiquitous cl-ppcre library and much more), it is used and @@ -55,12 +55,12 @@ and update since 2017. We present it in more details in the “Isomorphic web frameworks” appendix.

For a full list of libraries for the web, please see the awesome-cl list #network-and-internet -and Cliki.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/index.xml b/docs/building-blocks/index.xml index 7af6b7c..3b7fb4c 100644 --- a/docs/building-blocks/index.xml +++ b/docs/building-blocks/index.xml @@ -32,13 +32,16 @@ do you have an existing database you want to read data from? do you want to crea The #database section on the awesome-cl list is a resource listing popular libraries to work with different kind of databases. We can group them roughly in those categories:
User log-inhttp://example.org/building-blocks/user-log-in/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/user-log-in/index.htmlHow do you check if a user is logged-in, and how do you do the actual log in? We show an example, without handling passwords yet. See the next section for passwords. We’ll build a simple log-in page to an admin/ private dashboard. -What do we need to do exactly?Users and passwordshttp://example.org/building-blocks/users-and-passwords/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/users-and-passwords/index.htmlWe don’t know of a Common Lisp framework that will create users and roles for you and protect your routes all at the same time. We have building blocks but you’ll have to either write some glue Lisp code. -You can also turn to an external tool (such as Keycloak) that will provide all the industrial-grade user management. +What do we need to do exactly?Users and passwordshttp://example.org/building-blocks/users-and-passwords/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/users-and-passwords/index.htmlWe don’t know of a Common Lisp framework that will create users and 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. +You can also turn to external tools (such as Keycloak or Tesseral) that will provide all the industrial-grade user management. If you like the Mito ORM, look at mito-auth and mito-email-auth. Creating users If you use a database, you’ll have to create at least a users table. It would typically define:Form validationhttp://example.org/building-blocks/form-validation/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/form-validation/index.htmlWe can recommend the clavier library for input validation. See also the cl-forms library that offers many features: automatic forms form validation with in-line error messages CSRF protection client-side validation subforms Djula and Spinneret renderers default themes an online demo for you to try etc Get them: -(ql:quickload '(:clavier :cl-forms)) Form validation with the Clavier library Clavier defines validators as class instances. They come in many types, for example the 'less-than, greater-than or len validators. They may take an initialization argument.Running and Buildinghttp://example.org/building-blocks/building-binaries/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/building-binaries/index.htmlRunning the application from source Info See the tutorial. +(ql:quickload '(:clavier :cl-forms)) Form validation with the Clavier library Clavier defines validators as class instances. They come in many types, for example the 'less-than, greater-than or len validators. They may take an initialization argument.PUT and request parametershttp://example.org/building-blocks/put/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/put/index.htmlTo access the body parameters of a PUT request, one must add :PUT to hunchentoot:*methods-for-post-parameters*, which defaults to only (:POST): +(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.Running and Buildinghttp://example.org/building-blocks/building-binaries/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/building-binaries/index.htmlRunning the application from source Info See the tutorial. To run our Lisp code from source, as a script, we can use the --load switch from our implementation. We must ensure: to load the project’s .asd system declaration (if any) to install the required dependencies (this demands we have installed Quicklisp previously) and to run our application’s entry point. So, the recipe to run our project from sources can look like this (you can find such a recipe in our project generator):Deploymenthttp://example.org/building-blocks/deployment/index.htmlMon, 01 Jan 0001 00:00:00 +0000http://example.org/building-blocks/deployment/index.htmlHow to deploy and monitor a Common Lisp web app? diff --git a/docs/building-blocks/put/index.html b/docs/building-blocks/put/index.html new file mode 100644 index 0000000..4f5f7e4 --- /dev/null +++ b/docs/building-blocks/put/index.html @@ -0,0 +1,24 @@ +PUT and request parameters :: Web Apps in Lisp: Know-how +

PUT and request parameters

To access the body parameters of a PUT request, one must add :PUT to +hunchentoot:*methods-for-post-parameters*, which defaults to only +(:POST):

(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.

\ No newline at end of file diff --git a/docs/building-blocks/put/index.xml b/docs/building-blocks/put/index.xml new file mode 100644 index 0000000..ac509a0 --- /dev/null +++ b/docs/building-blocks/put/index.xml @@ -0,0 +1,4 @@ +PUT and request parameters :: Web Apps in Lisp: Know-howhttp://example.org/building-blocks/put/index.htmlTo access the body parameters of a PUT request, one must add :PUT to hunchentoot:*methods-for-post-parameters*, which defaults to only (:POST): +(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.Hugoen-us \ No newline at end of file diff --git a/docs/building-blocks/remote-debugging/index.html b/docs/building-blocks/remote-debugging/index.html index b3e836e..ac06d35 100644 --- a/docs/building-blocks/remote-debugging/index.html +++ b/docs/building-blocks/remote-debugging/index.html @@ -3,7 +3,7 @@ You can not only inspect the running program, but also compile and load new code, including installing new libraries, effectively doing hot code reload. It’s up to you to decide to do it or to follow the industry’s best practices. It isn’t because you use Common Lisp that you have to make your deployed program a β€œbig ball of mud”.">Remote debugging :: Web Apps in Lisp: Know-how -

Remote debugging

You can have your software running on a machine over the network, +

Remote debugging

You can have your software running on a machine over the network, connect to it and debug it from home, from your development environment.

You can not only inspect the running program, but also compile and load new code, including installing new libraries, effectively doing @@ -53,12 +53,12 @@ and port 4006.

We can write new code:

(defun dostuff ()
   (format t "goodbye world ~a!~%" *counter*))
 (setf *counter* 0)

and eval it as usual with C-c C-c or M-x slime-eval-region for instance. The output should change.

That’s how Ron Garret debugged the Deep Space 1 spacecraft from the earth -in 1999:

We were able to debug and fix a race condition that had not shown up during ground testing. (Debugging a program running on a $100M piece of hardware that is 100 million miles away is an interesting experience. Having a read-eval-print loop running on the spacecraft proved invaluable in finding and fixing the problem.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/routing/index.html b/docs/building-blocks/routing/index.html index 4525f5b..70b80fd 100644 --- a/docs/building-blocks/routing/index.html +++ b/docs/building-blocks/routing/index.html @@ -7,7 +7,7 @@ Hunchentoot The dispatch table The first, most basic way in Hunchentoot to create a route is to add a URL -> function association in its β€œprefix dispatch” table.">Routes and URL parameters :: Web Apps in Lisp: Know-how -

Routes and URL parameters

I prefer the easy-routes library than pure Hunchentoot to define +

Routes and URL parameters

I prefer the easy-routes library than pure Hunchentoot to define routes, as we did in the tutorial, so skip to its section below if you want. However, it can only be benificial to know the built-in Hunchentoot ways.

Info

Please see the tutorial where we define routes with path parameters @@ -78,12 +78,12 @@ (setf (hunchentoot:content-type*) "text/plain") (format nil "Hey ~a you are of type ~a" name (type-of name)))

Going to http://localhost:4242/yo?name=Alice returns

Hey Alice you are of type (SIMPLE-ARRAY CHARACTER (5))
 

To automatically bind it to another type, we use default-parameter-type. It can be -one of those simple types:

  • 'string (default),
  • 'integer,
  • 'character (accepting strings of length 1 only, otherwise it is nil)
  • or 'boolean

or a compound list:

  • '(:list <type>)
  • '(:array <type>)
  • '(:hash-table <type>)

where <type> is a simple type.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/session/index.html b/docs/building-blocks/session/index.html index be272c6..ea77ce2 100644 --- a/docs/building-blocks/session/index.html +++ b/docs/building-blocks/session/index.html @@ -7,15 +7,15 @@ You can store any Lisp object in a web session. You only have to:">Sessions :: Web Apps in Lisp: Know-how -

Sessions

When a web client (a user’s browser) connects to your app, you can +

Sessions

When a web client (a user’s browser) connects to your app, you can start a session with it, and store data, the time of the session.

Hunchentoot uses cookies to store the session ID, and if not possible it rewrites URLs. So, at every subsequent request by this web client, -you can check if there is a session data.

You can store any Lisp object in a web session. You only have to:

  • start a session with (hunchentoot:start-session)
    • for example, when a user logs in successfully
  • store an object with (setf (hunchentoot:session-value 'key) val)
    • for example, store the username at the log-in
  • and get the object with (hunchentoot:session-value 'key).
    • for example, in any route where you want to check that a user is logged in. If you don’t find a session key you want, you would redirect to the login page.

See our example in the next section about log-in.

References

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/simple-web-server/index.html b/docs/building-blocks/simple-web-server/index.html index 72697b2..e9f1773 100644 --- a/docs/building-blocks/simple-web-server/index.html +++ b/docs/building-blocks/simple-web-server/index.html @@ -7,7 +7,7 @@ (defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4242)) (hunchentoot:start *acceptor*) We create an instance of easy-acceptor on port 4242 and we start it. We can now access http://127.0.0.1:4242/. You should get a welcome screen with a link to the documentation and logs to the console.">Simple web server :: Web Apps in Lisp: Know-how -

Simple web server

Before dwelving into web development, we might want to do something simple: serve some files we have on disk.

Serve local files

If you followed the tutorial you know that we can create and start a webserver like this:

(defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4242))
+

Simple web server

Before dwelving into web development, we might want to do something simple: serve some files we have on disk.

Serve local files

If you followed the tutorial you know that we can create and start a webserver like this:

(defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4242))
 (hunchentoot:start *acceptor*)

We create an instance of easy-acceptor on port 4242 and we start it. We can now access http://127.0.0.1:4242/. You should get a welcome screen with a link to the documentation and logs to the console.

Info

You can also use Roswell’s http.server from the command line:

$ ros install roswell/http.server
@@ -35,12 +35,12 @@
 (hunchentoot:start *my-acceptor*)

go to http://127.0.0.1:4444/ and see the difference.

Note that we just created another web application on a different port on the same lisp image. This is already pretty cool.

Access your server from the internet

With Hunchentoot we have nothing to do, we can see the server from the internet right away.

If you evaluate this on your VPS:

(hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242))

You can see it right away on your server’s IP.

You can use the :address parameter of Hunchentoot’s easy-acceptor to -bind it and restrict it to 127.0.0.1 (or any address) if you wish.

Stop it with (hunchentoot:stop *).

Now on the next section, we’ll create some routes to build a dynamic website.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/static/index.html b/docs/building-blocks/static/index.html index c4d7322..c9347f8 100644 --- a/docs/building-blocks/static/index.html +++ b/docs/building-blocks/static/index.html @@ -15,7 +15,7 @@ Hunchentoot Use the create-folder-dispatcher-and-handler prefix directory function. For example: (defun serve-static-assets () "Let Hunchentoot serve static assets under the /src/static/ directory of your :myproject system. Then reference static assets with the /static/ URL prefix." (push (hunchentoot:create-folder-dispatcher-and-handler "/static/" (asdf:system-relative-pathname :myproject "src/static/")) ;; ^^^ starts without a / hunchentoot:*dispatch-table*)) and call it in the function that starts your application:'>Static assets :: Web Apps in Lisp: Know-how -

Static assets

How can we serve static assets?

Remember from simple web server +

Static assets

How can we serve static assets?

Remember from simple web server how Hunchentoot serves files from a www/ directory by default. We can change that.

Hunchentoot

Use the create-folder-dispatcher-and-handler prefix directory function.

For example:

(defun serve-static-assets ()
   "Let Hunchentoot serve static assets under the /src/static/ directory
   of your :myproject system.
@@ -25,12 +25,12 @@
           (asdf:system-relative-pathname :myproject "src/static/"))
           ;;                                        ^^^ starts without a /
         hunchentoot:*dispatch-table*))

and call it in the function that starts your application:

(serve-static-assets)

Now our project’s static files located under src/static/ are served -with the /static/ prefix. Access them like this:

<img src="/static/img/banner.jpg" />

or

<script src="/static/test.js" type="text/javascript"></script>

where the file src/static/test.js could be

console.log("hello");
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/templates/index.html b/docs/building-blocks/templates/index.html index 73720ed..681671f 100644 --- a/docs/building-blocks/templates/index.html +++ b/docs/building-blocks/templates/index.html @@ -15,7 +15,7 @@ (ql:quickload "djula") The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like: (djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/")) and then we can declare and compile the ones we use, for example:: (defparameter +base.html+ (djula:compile-template* "base.html")) (defparameter +products.html+ (djula:compile-template* "products.html")) Info If you get an error message when calling add-template-directory like this'>Templates :: Web Apps in Lisp: Know-how -

Templates

Djula - HTML markup

Djula is a port of Python’s +

Templates

Djula - HTML markup

Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation.

Install it if you didn’t already:

(ql:quickload "djula")

The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like:

(djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/"))

and then we can declare and compile the ones we use, for example::

(defparameter +base.html+ (djula:compile-template* "base.html"))
 (defparameter +products.html+ (djula:compile-template* "products.html"))
Info

If you get an error message when calling add-template-directory like this

The value
@@ -70,12 +70,12 @@
    (:ol (dolist (item *shopping-list*)
           (:li (1+ (random 10)) item))))
   (:footer ("Last login: ~A" *last-login*)))

I find Spinneret easier to use than the more famous cl-who, but I -personnally prefer to use HTML templates.

Spinneret has nice features under it sleeves:

  • it warns on invalid tags and attributes
  • it can automatically number headers, given their depth
  • it pretty prints html per default, with control over line breaks
  • it understands embedded markdown
  • it can tell where in the document a generator function is (see get-html-tag)
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/user-log-in/index.html b/docs/building-blocks/user-log-in/index.html index 1f1a37d..937602c 100644 --- a/docs/building-blocks/user-log-in/index.html +++ b/docs/building-blocks/user-log-in/index.html @@ -11,9 +11,9 @@ We show an example, without handling passwords yet. See the next section for passwords. We’ll build a simple log-in page to an admin/ private dashboard. What do we need to do exactly?">User log-in :: Web Apps in Lisp: Know-how -

User log-in

How do you check if a user is logged-in, and how do you do the actual log in?

We show an example, without handling passwords yet. See the next section for passwords.

We’ll build a simple log-in page to an admin/ private dashboard.

-

-

What do we need to do exactly?

  • we need a function to get a user by its ID
  • we need a function to check that a password is correct for a given user
  • we need two templates:
    • a login template
    • a template for a logged-in user
  • we need a route and we need to handle a POST request
    • when the log-in is successful, we need to store a user ID in a web session

We choose to structure our app with an admin/ URL that will show +

User log-in

How do you check if a user is logged-in, and how do you do the actual log in?

We show an example, without handling passwords yet. See the next section for passwords.

We’ll build a simple log-in page to an admin/ private dashboard.

+

+

What do we need to do exactly?

  • we need a function to get a user by its ID
  • we need a function to check that a password is correct for a given user
  • we need two templates:
    • a login template
    • a template for a logged-in user
  • we need a route and we need to handle a POST request
    • when the log-in is successful, we need to store a user ID in a web session

We choose to structure our app with an admin/ URL that will show both the login page or the “dashboard” for logged-in users.

We use these libraries:

(ql:quickload '("hunchentoot" "djula" "easy-routes"))

We still work from inside our :myproject package. You should have this at the top of your file:

(in-package :myproject)

Let’s start with the model functions.

Get users

(defun get-user (name)
   (list :name name :password "demo")) ;; <--- all our passwords are "demo"

Yes indeed, that’s a dummy function. You will add your own logic @@ -272,12 +272,12 @@ (defroute ("/account/review" :method :get) () (with-logged-in (render #p"review.html" - (list :review (get-review (gethash :user *session*))))))

and so on.

\ No newline at end of file diff --git a/docs/building-blocks/users-and-passwords/index.html b/docs/building-blocks/users-and-passwords/index.html index 0793bf4..91d5a61 100644 --- a/docs/building-blocks/users-and-passwords/index.html +++ b/docs/building-blocks/users-and-passwords/index.html @@ -11,7 +11,7 @@ You can also turn to external tools (such as Keycloak or Tesseral) that will provide all the industrial-grade user management. If you like the Mito ORM, look at mito-auth and mito-email-auth. Creating users If you use a database, you’ll have to create at least a users table. It would typically define:">Users and passwords :: Web Apps in Lisp: Know-how -

Users and passwords

We don’t know of a Common Lisp framework that will create users and +

Users and passwords

We don’t know of a Common Lisp framework that will create users and 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.

You can also turn to external tools (such as Keycloak or Tesseral) that will provide all the industrial-grade user management.

If you like the Mito ORM, look at mito-auth and mito-email-auth.

Creating users

If you use a database, you’ll have to create at least a users @@ -54,12 +54,12 @@ (make-op := (if (integerp user) :id_user :email) - user))))))

Credit: /u/arvid on /r/learnlisp.

See also

  • 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.

\ No newline at end of file diff --git a/docs/building-blocks/web-views/index.html b/docs/building-blocks/web-views/index.html index 30b5454..97477b4 100644 --- a/docs/building-blocks/web-views/index.html +++ b/docs/building-blocks/web-views/index.html @@ -11,7 +11,7 @@ We present two: WebUI and Webview.h, through CLOG Frame. Info This page appeared first on lisp-journey: three web views for Common Lisp, cross-platform GUIs. WebUI WebUI is a new kid in town. It is in development, it has bugs. You can view it as a wrapper around a browser window (or webview.h).">Web views: cross-platform GUIs :: Web Apps in Lisp: Know-how -

Web views: cross-platform GUIs

Web views are lightweight and cross-platform. They are nowadays a good +

Web views: cross-platform GUIs

Web views are lightweight and cross-platform. They are nowadays a good solution to ship a GUI program to your users.

We present two: WebUI and Webview.h, through CLOG Frame.

WebUI

WebUI is a new kid in town. It is in development, it has bugs. You can view it as a wrapper around a browser window (or webview.h).

However it is ligthweight, it is easy to build and we have Lisp bindings.

A few more words about it:

Use any web browser or WebView as GUI, with your preferred language in the backend and modern web technologies in the frontend, all in a lightweight portable library.

  • written in pure C
  • one header file
  • multi-platform & multi-browser
  • opens a real browser (you get the web development tools etc)
  • cross-platform webview
  • we can call JS from Common Lisp, and call Common Lisp from JS.

Think of WebUI like a WebView controller, but instead of embedding the WebView controller in your program, which makes the final program big in size, and non-portable as it needs the WebView runtimes. Instead, by using WebUI, you use a tiny static/dynamic library to run any installed web browser and use it as GUI, which makes your program small, fast, and portable. All it needs is a web browser.

your program will always run on all machines, as all it needs is an installed web browser.

Sounds compelling right?

The other good news is that Common Lisp was one of the first languages it got bindings for. How it happened: I was chating in Discord, mentioned WebUI and BAM! @garlic0x1 developed bindings:

thank you so much! (@garlic0x1 has more cool projects on GitHub you can browse. He’s also a contributor to Lem)

Here’s a simple snippet:

(defpackage :webui/examples/minimal
   (:use :cl :webui)
@@ -49,15 +49,15 @@
                            "Admin"
                            (format nil "~A/admin/" 4284)
                            ;; window dimensions (strings)
-                           "1280" "840"))

and voilΓ .

-

Now for the cross-platform part, you’ll need to build clogframe and + "1280" "840"))

and voilΓ .

+

Now for the cross-platform part, you’ll need to build clogframe and your web app on the target OS (like with any CL app). Webview.h is cross-platform. -Leave us a comment when you have a good CI setup for the three main OSes (I am studying 40ants/ci and make-common-lisp-program for now).

\ No newline at end of file diff --git a/docs/categories/index.html b/docs/categories/index.html index d930d77..986b977 100644 --- a/docs/categories/index.html +++ b/docs/categories/index.html @@ -1,10 +1,10 @@ Categories :: Web Apps in Lisp: Know-how -

Categories

\ No newline at end of file diff --git a/docs/css/chroma-auto.css b/docs/css/chroma-auto.css index b0f8794..4ba36c4 100644 --- a/docs/css/chroma-auto.css +++ b/docs/css/chroma-auto.css @@ -1,2 +1,2 @@ -@import "chroma-relearn-light.css?1746637581" screen and (prefers-color-scheme: light); -@import "chroma-relearn-dark.css?1746637581" screen and (prefers-color-scheme: dark); +@import "chroma-relearn-light.css?1756377279" screen and (prefers-color-scheme: light); +@import "chroma-relearn-dark.css?1756377279" screen and (prefers-color-scheme: dark); diff --git a/docs/css/format-print.css b/docs/css/format-print.css index dc8356b..ef96558 100644 --- a/docs/css/format-print.css +++ b/docs/css/format-print.css @@ -1,5 +1,5 @@ -@import "theme-relearn-light.css?1746637581"; -@import "chroma-relearn-light.css?1746637581"; +@import "theme-relearn-light.css?1756377279"; +@import "chroma-relearn-light.css?1756377279"; #R-sidebar { display: none; diff --git a/docs/css/print.css b/docs/css/print.css index 6aa9907..421377a 100644 --- a/docs/css/print.css +++ b/docs/css/print.css @@ -1 +1 @@ -@import "format-print.css?1746637581"; +@import "format-print.css?1756377279"; diff --git a/docs/css/swagger.css b/docs/css/swagger.css index 51b71f2..36ef8e5 100644 --- a/docs/css/swagger.css +++ b/docs/css/swagger.css @@ -1,7 +1,7 @@ /* Styles to make Swagger-UI fit into our theme */ -@import "fonts.css?1746637581"; -@import "variables.css?1746637581"; +@import "fonts.css?1756377279"; +@import "variables.css?1756377279"; body{ line-height: 1.574; diff --git a/docs/css/theme-auto.css b/docs/css/theme-auto.css index 50df42a..29f6632 100644 --- a/docs/css/theme-auto.css +++ b/docs/css/theme-auto.css @@ -1,2 +1,2 @@ -@import "theme-relearn-light.css?1746637581" screen and (prefers-color-scheme: light); -@import "theme-relearn-dark.css?1746637581" screen and (prefers-color-scheme: dark); +@import "theme-relearn-light.css?1756377279" screen and (prefers-color-scheme: light); +@import "theme-relearn-dark.css?1756377279" screen and (prefers-color-scheme: dark); diff --git a/docs/css/theme.css b/docs/css/theme.css index 58de19a..7172e76 100644 --- a/docs/css/theme.css +++ b/docs/css/theme.css @@ -1,4 +1,4 @@ -@import "variables.css?1746637581"; +@import "variables.css?1756377279"; @charset "UTF-8"; diff --git a/docs/index.html b/docs/index.html index 0c6a732..fbb4b68 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,15 +7,15 @@ starting a web server defining routes grabbing URL parameters rendering templates and running our app from sources, or building a binary. We’ll build a simple page that presents a search form, filters a list of products and displays the results.">Web Apps in Lisp: Know-how -

Web Apps in Lisp: Know-how

You want to write a web application in Common Lisp and you don’t know +

Web Apps in Lisp: Know-how

You want to write a web application in Common Lisp and you don’t know where to start? You are a beginner in web development, or a lisp amateur looking for clear and short pointers about web dev in Lisp? You are all at the right place.

What’s in this guide

We’ll start with a tutorial that shows the essential building blocks:

  • starting a web server
  • defining routes
  • grabbing URL parameters
  • rendering templates
  • and running our app from sources, or building a binary.

We’ll build a simple page that presents a search form, filters a list of products and displays the results.

The building blocks section is organized by topics, so that with a question in mind you should be able to look at the table of contents, pick the right page and go ahead. You’ll find more tutorials, for -example in “User log-in” we build a login form.

-

We hope that it will be plenty useful to you.

Don’t hesitate to share what you’re building!

Info

πŸŽ₯ If you want to learn Common Lisp efficiently, +example in “User log-in” we build a login form.

+

We hope that it will be plenty useful to you.

Don’t hesitate to share what you’re building!

Info

πŸŽ₯ If you want to learn Common Lisp efficiently, with a code-driven approach, good news, I am creating a video course on the Udemy platform. Already more than 7 hours of content, rated @@ -24,12 +24,12 @@ also have Common Lisp tutorial videos on Youtube.

Now let’s go to the tutorial.

How to start with Common Lisp

This resource is not about learning Common Lisp the language. We expect you have a working setup, with the Quicklisp package -manager. Please refer to the CL Cookbook.

Contact

We are @vindarel on Lisp’s Discord server and Mastodon.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/isomorphic-web-frameworks/clog/index.html b/docs/isomorphic-web-frameworks/clog/index.html index eaf30dc..140249b 100644 --- a/docs/isomorphic-web-frameworks/clog/index.html +++ b/docs/isomorphic-web-frameworks/clog/index.html @@ -7,7 +7,7 @@ We can say the CLOG experience is mindblowing.">CLOG :: Web Apps in Lisp: Know-how -

CLOG

CLOG, the Common Lisp Omnificent +

CLOG

CLOG, the Common Lisp Omnificent GUI, follows a GUI paradigm for the web platform. You don’t write nested <div> tags, but you place elements on the page. It sends changes to the page you are working on @@ -18,8 +18,8 @@ under the fingertips.

Moreover, its API is stable. The author used a similar product built in Ada professionally for a decade, and transitioned to CLOG in Common Lisp.

So, how can we build an interactive app with CLOG?

We will build a search form that triggers a search to the back-end on -key presses, and displays results to users as they type.

We do so without writing any JavaScript.

-

Before we do so, we’ll create a list of dummy products, so than we have something to search for.

Models

Let’s create a package for this new app. I’ll “use” functions and +key presses, and displays results to users as they type.

We do so without writing any JavaScript.

+

Before we do so, we’ll create a list of dummy products, so than we have something to search for.

Models

Let’s create a package for this new app. I’ll “use” functions and macros provided by the :clog package.

(uiop:define-package :clog-search
     (:use :cl :clog))
 
@@ -174,8 +174,8 @@
   (let ((query (value obj)))
     (if (> (length query) 2)
         (display-products div (clog-search::search-products query))
-        (print "waiting for more input"))))

It works \o/

-

There are some caveats that need to be worked on:

  • if you type a search query of 4 letters quickly, our handler waits for an input of at least 2 characters, but it will be fired 2 other times. That will probably fix the blickering.

And, as you noticed:

  • we didn’t copy-paste a nice looking HTML template, so we have a bit of work with that :/

This was only an introduction. As we said, CLOG is well suited for a + (print "waiting for more input"))))

It works \o/

+

There are some caveats that need to be worked on:

  • if you type a search query of 4 letters quickly, our handler waits for an input of at least 2 characters, but it will be fired 2 other times. That will probably fix the blickering.

And, as you noticed:

  • we didn’t copy-paste a nice looking HTML template, so we have a bit of work with that :/

This was only an introduction. As we said, CLOG is well suited for a wide range of applications.

Stop the app

Use

(clog:shutdown)

Full code

;; (ql:quickload '(:clog :str))
 
 (uiop:define-package :clog-search
@@ -286,12 +286,12 @@
   (let ((query (value obj)))
     (if (> (length query) 2)
         (display-products div (search-products query))
-        (print "waiting for more input"))))

References

\ No newline at end of file diff --git a/docs/isomorphic-web-frameworks/index.html b/docs/isomorphic-web-frameworks/index.html index 0b58a39..308af6a 100644 --- a/docs/isomorphic-web-frameworks/index.html +++ b/docs/isomorphic-web-frameworks/index.html @@ -11,17 +11,17 @@ Weblocks was an old framework developed by Slava Akhmechet, Stephen Compall and Leslie Polzer. After nine calm years, it saw a very active update, refactoring and rewrite effort by Alexander Artemenko. This active project is named Reblocks. The Ultralisp website is an example Reblocks website in production known in the CL community. It isn’t the only solution that aims at making writing interactive web apps easier, where the client logic can be brought to the back-end. See also:">Isomorphic web frameworks :: Web Apps in Lisp: Know-how -

Isomorphic web frameworks

We’ll see first: Reblocks.

Weblocks was an old framework developed by Slava Akhmechet, Stephen +

Isomorphic web frameworks

We’ll see first: Reblocks.

Weblocks was an old framework developed by Slava Akhmechet, Stephen Compall and Leslie Polzer. After nine calm years, it saw a very active update, refactoring and rewrite effort by Alexander Artemenko. This active project is named Reblocks.

The Ultralisp website is an example Reblocks website in production known in the CL community.

It isn’t the only solution that aims at making writing interactive web apps easier, where the client logic can be brought to the back-end. See also:

Of course, a special mention goes to HTMX, which -is language agnostic and backend agnostic. It works well with Common Lisp.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/isomorphic-web-frameworks/weblocks/index.html b/docs/isomorphic-web-frameworks/weblocks/index.html index 06e54a5..2a3229d 100644 --- a/docs/isomorphic-web-frameworks/weblocks/index.html +++ b/docs/isomorphic-web-frameworks/weblocks/index.html @@ -7,12 +7,12 @@ Note To install Reblocks, please see its documentation.">Reblocks :: Web Apps in Lisp: Know-how -

Reblocks

Reblocks is a widgets-based and server-based framework +

Reblocks

Reblocks is a widgets-based and server-based framework with a built-in ajax update mechanism. It allows to write dynamic web applications without the need to write JavaScript or to write lisp code that would transpile to JavaScript. It is thus super -exciting. It isn’t for newcomers however.

The Reblocks’s demo we will build is a TODO app:

-

Note

To install Reblocks, please see its documentation.

Reblocks’ unit of work is the widget. They look like a class definition:

(defwidget task ()
+exciting. It isn’t for newcomers however.

The Reblocks’s demo we will build is a TODO app:

+

Note

To install Reblocks, please see its documentation.

Reblocks’ unit of work is the widget. They look like a class definition:

(defwidget task ()
    ((title
      :initarg :title
      :accessor title)
@@ -36,12 +36,12 @@
 ...

The function make-js-action creates a simple javascript function that calls the lisp one on the server, and automatically refreshes the HTML of the widgets that need it. In our example, it re-renders one -task only.

Is it appealing ? Carry on its quickstart guide.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/search/index.html b/docs/search/index.html index 4d06239..3b5c4cf 100644 --- a/docs/search/index.html +++ b/docs/search/index.html @@ -1,11 +1,11 @@ Search :: Web Apps in Lisp: Know-how -
\ No newline at end of file diff --git a/docs/searchindex.js b/docs/searchindex.js index a852a00..c8f6043 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -137,7 +137,7 @@ var relearn_searchindex = [ }, { "breadcrumb": "Building blocks", - "content": "How do you check if a user is logged-in, and how do you do the actual log in?\nWe show an example, without handling passwords yet. See the next section for passwords.\nWe’ll build a simple log-in page to an admin/ private dashboard.\nWhat do we need to do exactly?\nwe need a function to get a user by its ID we need a function to check that a password is correct for a given user we need two templates: a login template a template for a logged-in user we need a route and we need to handle a POST request when the log-in is successful, we need to store a user ID in a web session We choose to structure our app with an admin/ URL that will show both the login page or the β€œdashboard” for logged-in users.\nWe use these libraries:\n(ql:quickload '(\"hunchentoot\" \"djula\" \"easy-routes\")) We still work from inside our :myproject package. You should have this at the top of your file:\n(in-package :myproject) Let’s start with the model functions.\nGet users (defun get-user (name) (list :name name :password \"demo\")) ;; \u003c--- all our passwords are \"demo\" Yes indeed, that’s a dummy function. You will add your own logic later, we focus on the web stack. Here we return a user object, a plist with a name and a password. So to speak.\n(defun valid-user-p (name password) (let ((user (get-user name))) (and user (string= name (getf user :name)) (string= password (getf user :password))))) Look, what if we stored our own name and password in a file? No need of a DB for a personal or a toy web app.\nIn creds.lisp-expr:\n(:name \"me\" :password \"yadadada\") the β€œ.lisp-expr” is just a convention, so that your tools won’t see it as a lisp source.\nRead it back in with uiop:read-file-form:\n(defparameter *me* (uiop:read-file-form \"creds.lisp-expr\")) (getf *me* :name) ;; =\u003e \"me\" Cool? my 2c.\nTemplates: login, welcome For convenience we again define our templates as strings.\n;;; Templates. ;;; XXX: we have to escape the quotes in our string templates. When they are in files we don't. (defparameter *template-login* \" \u003chtml lang=en\u003e \u003chead\u003e \u003cmeta charset=UTF-8\u003e \u003ctitle\u003eLogin\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003cdiv\u003e Login form. \u003c/div\u003e \u003cdiv\u003e Any user name is valid. The password is \\\"demo\\\". \u003c/div\u003e {% if error %} \u003cp style=\\\"color: red;\\\"\u003eInvalid username or password\u003c/p\u003e {% endif %} \u003cform method=post action=\\\"/admin/\\\"\u003e \u003cp\u003eUsername: {% if name %} \u003cinput type=text name=name value=\\\"{{ name }}\\\"\u003e {% else %} \u003cinput type=text name=name\u003e {% endif %} \u003cp\u003ePassword: \u003cinput type=password name=password\u003e \u003cp\u003e \u003cinput type=submit value=\\\"Log In\\\"\u003e \u003c/form\u003e \u003c/body\u003e \u003c/html\u003e \" ) (defparameter *template-welcome* \" \u003chtml lang=en\u003e \u003chead\u003e \u003cmeta charset=UTF-8\u003e \u003ctitle\u003eWelcome\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003ch1\u003eWelcome, {{ name }}!\u003c/h1\u003e \u003cdiv\u003eYou are logged in to your admin dashboard.\u003c/div\u003e \u003ca href=\\\"/admin/logout\\\"\u003eLog out\u003c/a\u003e \u003c/body\u003e \u003c/html\u003e \") Please refer to the demo to understand how we use them (and their shortcomings). We need the render function (see also the full code below).\nViews: are we logged in? You can start with this route:\n(defun loggedin-p () (hunchentoot:session-value 'name)) ;; GET (easy-routes:defroute admin-route (\"/admin/\" :method :get) () (if (loggedin-p) (render *template-welcome* :name (hunchentoot:session-value 'name)) (render *template-login*)) We are simply querying the session for the user name. If it’s present, that means we have established it at login.\nNow is a great time to use easy-routes’ β€œdecorators” (see the Routing section).\nWe can shorten the route to this:\n(defun @auth (next) (log:info \"checking session\") (if (loggedin-p) (funcall next) (render *template-login*))) ;; GET (easy-routes:defroute admin-route (\"/admin/\" :method :get :decorators ((@auth))) () (render *template-welcome* :name (hunchentoot:session-value 'name))) Yes, ((@auth)) is between 2 (( )) because that will be useful. We can call β€œdecorators” with arguments.\nThe two routes are strictly equivalent, but the second one allows to offload and refactor logic to other functions.\nFirst test Please see the tutorial for how to start a web server.\nIf you compiled the routes while a connection to a web server is active, then your route is accessible.\nVisit http://localhost:8899/admin/, you should see the login form.\nWe didn’t handle the POST request yet.\nlogin: POST request ;; POST (easy-routes:defroute admin-route/POST (\"/admin/\" :method :post) (name password) (cond ((valid-user-p name password) (hunchentoot:start-session) (setf (hunchentoot:session-value 'name) name) (render *template-welcome* :name name)) (t (render *template-login* :name name :error t)))) Beware of this gotcha: the route names must be unique. Otherwise, you will override your previous route definition. We name it admin-route/POST.\nOur login HTML defines two inputs:\n\u003cinput type=text name=name\u003e \u003cinput type=text name=password\u003e that’s why we declared those as POST parameters in the route with (name password).\nOur valid-user-p function only checks that the password equals β€œdemo”.\nDepending on the result, we display the login page again, with an error message, or we display our welcome page and right before we do these important steps:\nwe start a session and we store our user ID We are logged in o/\nLogout Notice the logout button in the welcome page.\nLet’s define the logout route:\n(hunchentoot:define-easy-handler (logout :uri \"/admin/logout\") () (hunchentoot:delete-session-value 'name) (hunchentoot:redirect \"/admin/\")) We have to delete our user’s ID! That’s the step not to forget.\nWe could also delete the current session object altogether with:\n(hunchentoot:remove-session (hunchentoot:*session*)) that depends if you stored more data. The *session* β€œglobal” object is the session in the context of the current request.\nAt last we redirect to the admin/ URL, which is going to check the user ID in the session, which isn’t present anymore, and thus show the login form.\nAnd we’ve gone circle.\nRedirect and generate an URL by name We just used a redirect.\nYou will notice that the routes /admin and /admin/ are different. We set up a quick redirect.\n(hunchentoot:define-easy-handler (admin2 :uri \"/admin\") () (hunchentoot:redirect \"/admin/\")) but wait, did we copy an URL by name? We can instead use\n(easy-routes:genurl 'admin-route) ;; \"/admin/\" We also have genurl* to generate an absolute URL:\n(easy-routes:genurl* 'admin-route) ;; \"http://localhost/admin/\" These functions accept arguments to set the PATH and URL parameters.\nHunchentoot code This is the equivalent Hunchentoot route:\n(hunchentoot:define-easy-handler (admin :uri \"/dashboard/\") (name password) (ecase (hunchentoot:request-method*) (:get (if (loggedin-p) (render *template-welcome*) (render *template-login*))) (:post (cond ((valid-user-p name password) (hunchentoot:start-session) (setf (hunchentoot:session-value 'name) name) (render *template-welcome* :name name)) (t (render *template-login* :name name :error t)))) )) Remarks:\nwe can’t dispatch on the request type, so we use the ecase on request-method* we can’t use β€œdecorators” so we use branching it isn’t very clear but name and password are only used in the POST part. we can also use (hunchentoot:post-parameter \"name\") (the parameter as a string) all this adds nesting in our function but otherwise, it’s pretty similar. Full code (in-package :myproject) ;; User-facing paramaters. (defparameter *port* 8899) ;; Internal variables. (defvar *server* nil) ;;; Models. (defun get-user (name) (list :name name :password \"demo\")) ;; \u003c--- all our passwords are \"demo\" (defun valid-user-p (name password) (let ((user (get-user name))) (and user (string= name (getf user :name)) (string= password (getf user :password))))) ;;; Templates. ;;; XXX: we have to escape the quotes in our string templates. When they are in files we don't. (defparameter *template-login* \" \u003chtml lang=en\u003e \u003chead\u003e \u003cmeta charset=UTF-8\u003e \u003ctitle\u003eLogin\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003cdiv\u003e Login form. \u003c/div\u003e \u003cdiv\u003e Any user name is valid. The password is \\\"demo\\\". \u003c/div\u003e {% if error %} \u003cp style=\\\"color: red;\\\"\u003eInvalid username or password\u003c/p\u003e {% endif %} \u003cform method=post action=\\\"/admin/\\\"\u003e \u003cp\u003eUsername: {% if name %} \u003cinput type=text name=name value=\\\"{{ name }}\\\"\u003e {% else %} \u003cinput type=text name=name\u003e {% endif %} \u003cp\u003ePassword: \u003cinput type=password name=password\u003e \u003cp\u003e \u003cinput type=submit value=\\\"Log In\\\"\u003e \u003c/form\u003e \u003c/body\u003e \u003c/html\u003e \" ) (defparameter *template-welcome* \" \u003chtml lang=en\u003e \u003chead\u003e \u003cmeta charset=UTF-8\u003e \u003ctitle\u003eWelcome\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003ch1\u003eWelcome, {{ name }}!\u003c/h1\u003e \u003cdiv\u003eYou are logged in to your admin dashboard.\u003c/div\u003e \u003ca href=\\\"/admin/logout\\\"\u003eLog out\u003c/a\u003e \u003c/body\u003e \u003c/html\u003e \") (defun render (template \u0026rest args) (apply #'djula:render-template* (djula:compile-string template) nil args)) ;; Views. (defun loggedin-p () (hunchentoot:session-value 'name)) (defun @auth (next) (if (loggedin-p) (funcall next) (render *template-login*))) ;; GET (easy-routes:defroute admin-route (\"/admin/\" :method :get :decorators ((@auth))) () (render *template-welcome* :name (hunchentoot:session-value 'name))) ;; POST (easy-routes:defroute admin-route/POST (\"/admin/\" :method :post) (name password) (cond ((valid-user-p name password) (hunchentoot:start-session) (setf (hunchentoot:session-value 'name) name) (render *template-welcome* :name name)) (t (render *template-login* :name name :error t)))) (hunchentoot:define-easy-handler (logout :uri \"/admin/logout\") () (hunchentoot:delete-session-value 'name) (hunchentoot:redirect (easy-routes:genurl 'admin-route))) ;; Server. (defun start-server (\u0026key (port *port*)) (format t \"~\u0026Starting the login demo on port ~a~\u0026\" port) (unless *server* (setf *server* (make-instance 'hunchentoot:easy-acceptor :port port))) (hunchentoot:start *server*)) (defun stop-server () (hunchentoot:stop *server*)) Caveman In Caveman, *session* is a hash-table that represents the session’s data. Here are our login and logout functions:\n(defun login (user) \"Log the user into the session\" (setf (gethash :user *session*) user)) (defun logout () \"Log the user out of the session.\" (setf (gethash :user *session*) nil)) We define a simple predicate:\n(defun logged-in-p () (gethash :user cm:*session*)) We don’t know a mechanism as easy-routes’ β€œdecorators” but we define a with-logged-in macro:\n(defmacro with-logged-in (\u0026body body) `(if (logged-in-p) (progn ,@body) (render #p\"login.html\" '(:message \"Please log-in to access this page.\")))) If the user isn’t logged in, there will be nothing stored in the session store, and we render the login page. When all is well, we execute the macro’s body. We use it like this:\n(defroute \"/account/logout\" () \"Show the log-out page, only if the user is logged in.\" (with-logged-in (logout) (render #p\"logout.html\"))) (defroute (\"/account/review\" :method :get) () (with-logged-in (render #p\"review.html\" (list :review (get-review (gethash :user *session*)))))) and so on.", + "content": "How do you check if a user is logged-in, and how do you do the actual log in?\nWe show an example, without handling passwords yet. See the next section for passwords.\nWe’ll build a simple log-in page to an admin/ private dashboard.\nWhat do we need to do exactly?\nwe need a function to get a user by its ID we need a function to check that a password is correct for a given user we need two templates: a login template a template for a logged-in user we need a route and we need to handle a POST request when the log-in is successful, we need to store a user ID in a web session We choose to structure our app with an admin/ URL that will show both the login page or the β€œdashboard” for logged-in users.\nWe use these libraries:\n(ql:quickload '(\"hunchentoot\" \"djula\" \"easy-routes\")) We still work from inside our :myproject package. You should have this at the top of your file:\n(in-package :myproject) Let’s start with the model functions.\nGet users (defun get-user (name) (list :name name :password \"demo\")) ;; \u003c--- all our passwords are \"demo\" Yes indeed, that’s a dummy function. You will add your own logic later, we focus on the web stack. Here we return a user object, a plist with a name and a password. So to speak.\n(defun valid-user-p (name password) (let ((user (get-user name))) (and user (string= name (getf user :name)) (string= password (getf user :password))))) Look, what if we stored our own name and password in a file? No need of a DB for a personal or a toy web app.\nIn creds.lisp-expr:\n(:name \"me\" :password \"yadadada\") the β€œ.lisp-expr” is just a convention, so that your tools won’t see it as a lisp source.\nRead it back in with uiop:read-file-form:\n(defparameter *me* (uiop:read-file-form \"creds.lisp-expr\")) (getf *me* :name) ;; =\u003e \"me\" Cool? my 2c.\nTemplates: login, welcome For convenience we again define our templates as strings.\n;;; Templates. ;;; XXX: we have to escape the quotes in our string templates. When they are in files we don't. (defparameter *template-login* \" \u003chtml lang=en\u003e \u003chead\u003e \u003cmeta charset=UTF-8\u003e \u003ctitle\u003eLogin\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003cdiv\u003e Login form. \u003c/div\u003e \u003cdiv\u003e Any user name is valid. The password is \\\"demo\\\". \u003c/div\u003e {% if error %} \u003cp style=\\\"color: red;\\\"\u003eInvalid username or password\u003c/p\u003e {% endif %} \u003cform method=post action=\\\"/admin/\\\"\u003e \u003cp\u003eUsername: {% if name %} \u003cinput type=text name=name value=\\\"{{ name }}\\\"\u003e {% else %} \u003cinput type=text name=name\u003e {% endif %} \u003cp\u003ePassword: \u003cinput type=password name=password\u003e \u003cp\u003e \u003cinput type=submit value=\\\"Log In\\\"\u003e \u003c/form\u003e \u003c/body\u003e \u003c/html\u003e \" ) (defparameter *template-welcome* \" \u003chtml lang=en\u003e \u003chead\u003e \u003cmeta charset=UTF-8\u003e \u003ctitle\u003eWelcome\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003ch1\u003eWelcome, {{ name }}!\u003c/h1\u003e \u003cdiv\u003eYou are logged in to your admin dashboard.\u003c/div\u003e \u003ca href=\\\"/admin/logout\\\"\u003eLog out\u003c/a\u003e \u003c/body\u003e \u003c/html\u003e \") Please refer to the demo to understand how we use them (and their shortcomings). We need the render function (see also the full code below).\nViews: are we logged in? You can start with this route:\n(defun loggedin-p () (hunchentoot:session-value 'name)) ;; GET (easy-routes:defroute admin-route (\"/admin/\" :method :get) () (if (loggedin-p) (render *template-welcome* :name (hunchentoot:session-value 'name)) (render *template-login*)) We are simply querying the session for the user name. If it’s present, that means we have established it at login.\nNow is a great time to use easy-routes’ β€œdecorators” (see the Routing section).\nWe can shorten the route to this:\n(defun @auth (next) (log:info \"checking session\") (if (loggedin-p) (funcall next) (render *template-login*))) ;; GET (easy-routes:defroute admin-route (\"/admin/\" :method :get :decorators ((@auth))) () (render *template-welcome* :name (hunchentoot:session-value 'name))) Yes, ((@auth)) is between 2 (( )) because that will be useful. We can call β€œdecorators” with arguments.\nThe two routes are strictly equivalent, but the second one allows to offload and refactor logic to other functions.\nFirst test Please see the tutorial for how to start a web server.\nIf you compiled the routes while a connection to a web server is active, then your route is accessible.\nVisit http://localhost:8899/admin/, you should see the login form.\nWe didn’t handle the POST request yet.\nlogin: POST request ;; POST (easy-routes:defroute admin-route/POST (\"/admin/\" :method :post) (name password) (cond ((valid-user-p name password) (hunchentoot:start-session) (setf (hunchentoot:session-value 'name) name) (render *template-welcome* :name name)) (t (render *template-login* :name name :error t)))) Beware of this gotcha: the route names must be unique. Otherwise, you will override your previous route definition. We name it admin-route/POST.\nOur login HTML defines two inputs:\n\u003cinput type=text name=name\u003e \u003cinput type=text name=password\u003e that’s why we declared those as POST parameters in the route with (name password).\nOur valid-user-p function only checks that the password equals β€œdemo”.\nDepending on the result, we display the login page again, with an error message, or we display our welcome page and right before we do these important steps:\nwe start a session and we store our user ID We are logged in o/\nLogout Notice the logout button in the welcome page.\nLet’s define the logout route:\n(hunchentoot:define-easy-handler (logout :uri \"/admin/logout\") () (hunchentoot:delete-session-value 'name) (hunchentoot:redirect \"/admin/\")) We have to delete our user’s ID! That’s the step not to forget.\nWe could also delete the current session object altogether with:\n(hunchentoot:remove-session (hunchentoot:*session*)) that depends if you stored more data. The *session* β€œglobal” object is the session in the context of the current request.\nAt last we redirect to the admin/ URL, which is going to check the user ID in the session, which isn’t present anymore, and thus show the login form.\nAnd we’ve gone circle.\nRedirect and generate an URL by name We just used a redirect.\nYou will notice that the routes /admin and /admin/ are different. We set up a quick redirect.\n(hunchentoot:define-easy-handler (admin2 :uri \"/admin\") () (hunchentoot:redirect \"/admin/\")) but wait, did we copy an URL by name? We can instead use\n(easy-routes:genurl 'admin-route) ;; \"/admin/\" We also have genurl* to generate an absolute URL:\n(easy-routes:genurl* 'admin-route) ;; \"http://localhost/admin/\" These functions accept arguments to set the PATH and URL parameters.\nHunchentoot code This is the equivalent Hunchentoot route:\n(hunchentoot:define-easy-handler (admin :uri \"/dashboard/\") (name password) (ecase (hunchentoot:request-method*) (:get (if (loggedin-p) (render *template-welcome*) (render *template-login*))) (:post (cond ((valid-user-p name password) (hunchentoot:start-session) (setf (hunchentoot:session-value 'name) name) (render *template-welcome* :name name)) (t (render *template-login* :name name :error t)))) )) Remarks:\nwe can’t dispatch on the request type, so we use the ecase on request-method* we can’t use β€œdecorators” so we use branching it isn’t very clear but name and password are only used in the POST part. we can also use (hunchentoot:post-parameter \"name\") (the parameter as a string) all this adds nesting in our function but otherwise, it’s pretty similar. Full code (defpackage :myproject (:use :cl)) (in-package :myproject) ;; User-facing paramaters. (defparameter *port* 8899) ;; Internal variables. (defvar *server* nil) ;;; Models. (defun get-user (name) (list :name name :password \"demo\")) ;; \u003c--- all our passwords are \"demo\" (defun valid-user-p (name password) (let ((user (get-user name))) (and user (string= name (getf user :name)) (string= password (getf user :password))))) ;;; Templates. ;;; XXX: we have to escape the quotes in our string templates. When they are in files we don't. (defparameter *template-login* \" \u003chtml lang=en\u003e \u003chead\u003e \u003cmeta charset=UTF-8\u003e \u003ctitle\u003eLogin\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003cdiv\u003e Login form. \u003c/div\u003e \u003cdiv\u003e Any user name is valid. The password is \\\"demo\\\". \u003c/div\u003e {% if error %} \u003cp style=\\\"color: red;\\\"\u003eInvalid username or password\u003c/p\u003e {% endif %} \u003cform method=post action=\\\"/admin/\\\"\u003e \u003cp\u003eUsername: {% if name %} \u003cinput type=text name=name value=\\\"{{ name }}\\\"\u003e {% else %} \u003cinput type=text name=name\u003e {% endif %} \u003cp\u003ePassword: \u003cinput type=password name=password\u003e \u003cp\u003e \u003cinput type=submit value=\\\"Log In\\\"\u003e \u003c/form\u003e \u003c/body\u003e \u003c/html\u003e \" ) (defparameter *template-welcome* \" \u003chtml lang=en\u003e \u003chead\u003e \u003cmeta charset=UTF-8\u003e \u003ctitle\u003eWelcome\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003ch1\u003eWelcome, {{ name }}!\u003c/h1\u003e \u003cdiv\u003eYou are logged in to your admin dashboard.\u003c/div\u003e \u003ca href=\\\"/admin/logout\\\"\u003eLog out\u003c/a\u003e \u003c/body\u003e \u003c/html\u003e \") (defun render (template \u0026rest args) (apply #'djula:render-template* (djula:compile-string template) nil args)) ;; Views. (defun loggedin-p () (hunchentoot:session-value 'name)) (defun @auth (next) (if (loggedin-p) (funcall next) (render *template-login*))) ;; GET (easy-routes:defroute admin-route (\"/admin/\" :method :get :decorators ((@auth))) () (render *template-welcome* :name (hunchentoot:session-value 'name))) ;; POST (easy-routes:defroute admin-route/POST (\"/admin/\" :method :post) (name password) (cond ((valid-user-p name password) (hunchentoot:start-session) (setf (hunchentoot:session-value 'name) name) (render *template-welcome* :name name)) (t (render *template-login* :name name :error t)))) (hunchentoot:define-easy-handler (logout :uri \"/admin/logout\") () (hunchentoot:delete-session-value 'name) (hunchentoot:redirect (easy-routes:genurl 'admin-route))) ;; Server. (defun start-server (\u0026key (port *port*)) (format t \"~\u0026Starting the login demo on port ~a~\u0026\" port) (setf *server* (make-instance 'easy-routes:easy-routes-acceptor :port port)) (hunchentoot:start *server*)) (defun stop-server () (hunchentoot:stop *server*)) Caveman In Caveman, *session* is a hash-table that represents the session’s data. Here are our login and logout functions:\n(defun login (user) \"Log the user into the session\" (setf (gethash :user *session*) user)) (defun logout () \"Log the user out of the session.\" (setf (gethash :user *session*) nil)) We define a simple predicate:\n(defun logged-in-p () (gethash :user cm:*session*)) We don’t know a mechanism as easy-routes’ β€œdecorators” but we define a with-logged-in macro:\n(defmacro with-logged-in (\u0026body body) `(if (logged-in-p) (progn ,@body) (render #p\"login.html\" '(:message \"Please log-in to access this page.\")))) If the user isn’t logged in, there will be nothing stored in the session store, and we render the login page. When all is well, we execute the macro’s body. We use it like this:\n(defroute \"/account/logout\" () \"Show the log-out page, only if the user is logged in.\" (with-logged-in (logout) (render #p\"logout.html\"))) (defroute (\"/account/review\" :method :get) () (with-logged-in (render #p\"review.html\" (list :review (get-review (gethash :user *session*)))))) and so on.", "description": "How do you check if a user is logged-in, and how do you do the actual log in?\nWe show an example, without handling passwords yet. See the next section for passwords.\nWe’ll build a simple log-in page to an admin/ private dashboard.\nWhat do we need to do exactly?", "tags": [], "title": "User log-in", @@ -145,8 +145,8 @@ var relearn_searchindex = [ }, { "breadcrumb": "Building blocks", - "content": "We don’t know of a Common Lisp framework that will create users and roles for you and protect your routes all at the same time. We have building blocks but you’ll have to either write some glue Lisp code.\nYou can also turn to an external tool (such as Keycloak) that will provide all the industrial-grade user management.\nIf you like the Mito ORM, look at mito-auth and mito-email-auth.\nCreating users If you use a database, you’ll have to create at least a users table. It would typically define:\na unique ID (integer, primary key) a name (varchar) an email (varchar) a password (varchar (and encrypted)) optionally, a key to the table listing roles. You can start with this:\nCREATE TABLE users ( id INTEGER PRIMARY KEY, username VARCHAR(255), email VARCHAR(255), password VARCHAR(255), ) You can run this right now with SQLite on the command line:\n$ sqlite3 db.db \"CREATE TABLE users (id INTEGER PRIMARY KEY, username VARCHAR(255), email VARCHAR(255), password VARCHAR(255))\" This creates the database if it doesn’t exist. SQLite reads SQL from the command line.\nCreate users:\n$ sqlite3 db.db \"INSERT INTO users VALUES(1,'Alice','alice@mail','xxx');\" Did it work? Run SELECT * FROM users;.\nEncrypting passwords With cl-bcrypt cl-bcrypt is a password hashing and verification library. It is as simple to use as this:\nCL-USER\u003e (defparameter *password* (bcrypt:make-password \"my-secret-password\")) *PASSWORD* and you can specify another salt, another cost factor and another algorithm identifier.\nThen you can use bcrypt:encode to get a string reprentation of the password:\nCL-USER\u003e (bcrypt:encode *password*) \"$2a$16$ClVzMvzfNyhFA94iLDdToOVeApbDppFru3JXNUyi1y1x6MkO0KzZa\" and you decode a password with decode.\nManually (with Ironclad) In this recipe we do the encryption and verification ourselves. We use the de-facto standard Ironclad cryptographic toolkit and the Babel charset encoding/decoding library.\nThe following snippet creates the password hash that should be stored in your database. Note that Ironclad expects a byte-vector, not a string.\n(defun password-hash (password) (ironclad:pbkdf2-hash-password-to-combined-string (babel:string-to-octets password))) pbkdf2 is defined in RFC2898. It uses a pseudorandom function to derive a secure encryption key based on the password.\nThe following function checks if a user is active and verifies the entered password. It returns the user-id if active and verified and nil in all other cases even if an error occurs. Adapt it to your application.\n(defun check-user-password (user password) (handler-case (let* ((data (my-get-user-data user)) (hash (my-get-user-hash data)) (active (my-get-user-active data))) (when (and active (ironclad:pbkdf2-check-password (babel:string-to-octets password) hash)) (my-get-user-id data))) (condition () nil))) And the following is an example on how to set the password on the database. Note that we use (password-hash password) to save the password. The rest is specific to the web framework and to the DB library.\n(defun set-password (user password) (with-connection (db) (execute (make-statement :update :web_user (set= :hash (password-hash password)) (make-clause :where (make-op := (if (integerp user) :id_user :email) user)))))) Credit: /u/arvid on /r/learnlisp.\nSee also 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.", - "description": "We don’t know of a Common Lisp framework that will create users and roles for you and protect your routes all at the same time. We have building blocks but you’ll have to either write some glue Lisp code.\nYou can also turn to an external tool (such as Keycloak) that will provide all the industrial-grade user management.\nIf you like the Mito ORM, look at mito-auth and mito-email-auth.\nCreating users If you use a database, you’ll have to create at least a users table. It would typically define:", + "content": "We don’t know of a Common Lisp framework that will create users and 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.\nYou can also turn to external tools (such as Keycloak or Tesseral) that will provide all the industrial-grade user management.\nIf you like the Mito ORM, look at mito-auth and mito-email-auth.\nCreating users If you use a database, you’ll have to create at least a users table. It would typically define:\na unique ID (integer, primary key) a name (varchar) an email (varchar) a password (varchar (and encrypted)) optionally, a key to the table listing roles. You can start with this:\nCREATE TABLE users ( id INTEGER PRIMARY KEY, username VARCHAR(255), email VARCHAR(255), password VARCHAR(255), ) You can run this right now with SQLite on the command line:\n$ sqlite3 db.db \"CREATE TABLE users (id INTEGER PRIMARY KEY, username VARCHAR(255), email VARCHAR(255), password VARCHAR(255))\" This creates the database if it doesn’t exist. SQLite reads SQL from the command line.\nCreate users:\n$ sqlite3 db.db \"INSERT INTO users VALUES(1,'Alice','alice@mail','xxx');\" Did it work? Run SELECT * FROM users;.\nEncrypting passwords With cl-bcrypt cl-bcrypt is a password hashing and verification library. It is as simple to use as this:\nCL-USER\u003e (defparameter *password* (bcrypt:make-password \"my-secret-password\")) *PASSWORD* and you can specify another salt, another cost factor and another algorithm identifier.\nThen you can use bcrypt:encode to get a string reprentation of the password:\nCL-USER\u003e (bcrypt:encode *password*) \"$2a$16$ClVzMvzfNyhFA94iLDdToOVeApbDppFru3JXNUyi1y1x6MkO0KzZa\" and you decode a password with decode.\nManually (with Ironclad) In this recipe we do the encryption and verification ourselves. We use the de-facto standard Ironclad cryptographic toolkit and the Babel charset encoding/decoding library.\nThe following snippet creates the password hash that should be stored in your database. Note that Ironclad expects a byte-vector, not a string.\n(defun password-hash (password) (ironclad:pbkdf2-hash-password-to-combined-string (babel:string-to-octets password))) pbkdf2 is defined in RFC2898. It uses a pseudorandom function to derive a secure encryption key based on the password.\nThe following function checks if a user is active and verifies the entered password. It returns the user-id if active and verified and nil in all other cases even if an error occurs. Adapt it to your application.\n(defun check-user-password (user password) (handler-case (let* ((data (my-get-user-data user)) (hash (my-get-user-hash data)) (active (my-get-user-active data))) (when (and active (ironclad:pbkdf2-check-password (babel:string-to-octets password) hash)) (my-get-user-id data))) (condition () nil))) And the following is an example on how to set the password on the database. Note that we use (password-hash password) to save the password. The rest is specific to the web framework and to the DB library.\n(defun set-password (user password) (with-connection (db) (execute (make-statement :update :web_user (set= :hash (password-hash password)) (make-clause :where (make-op := (if (integerp user) :id_user :email) user)))))) Credit: /u/arvid on /r/learnlisp.\nSee also 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.", + "description": "We don’t know of a Common Lisp framework that will create users and 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.\nYou can also turn to external tools (such as Keycloak or Tesseral) that will provide all the industrial-grade user management.\nIf you like the Mito ORM, look at mito-auth and mito-email-auth.\nCreating users If you use a database, you’ll have to create at least a users table. It would typically define:", "tags": [], "title": "Users and passwords", "uri": "/building-blocks/users-and-passwords/index.html" @@ -159,6 +159,14 @@ var relearn_searchindex = [ "title": "Form validation", "uri": "/building-blocks/form-validation/index.html" }, + { + "breadcrumb": "Building blocks", + "content": "To access the body parameters of a PUT request, one must add :PUT to hunchentoot:*methods-for-post-parameters*, which defaults to only (:POST):\n(push :put hunchentoot:*methods-for-post-parameters*) This parameter:\nis a list of the request method types (as keywords) for which Hunchentoot will try to compute POST-PARAMETERS.\nNo such setting is required with Lack and Ningle.", + "description": "To access the body parameters of a PUT request, one must add :PUT to hunchentoot:*methods-for-post-parameters*, which defaults to only (:POST):\n(push :put hunchentoot:*methods-for-post-parameters*) This parameter:\nis a list of the request method types (as keywords) for which Hunchentoot will try to compute POST-PARAMETERS.\nNo such setting is required with Lack and Ningle.", + "tags": [], + "title": "PUT and request parameters", + "uri": "/building-blocks/put/index.html" + }, { "breadcrumb": "Building blocks", "content": "Running the application from source Info See the tutorial.\nTo run our Lisp code from source, as a script, we can use the --load switch from our implementation.\nWe must ensure:\nto load the project’s .asd system declaration (if any) to install the required dependencies (this demands we have installed Quicklisp previously) and to run our application’s entry point. So, the recipe to run our project from sources can look like this (you can find such a recipe in our project generator):\n;; run.lisp (load \"myproject.asd\") (ql:quickload \"myproject\") (in-package :myproject) (handler-case (myproject::start-app :port (ignore-errors (parse-integer (uiop:getenv \"PROJECT_PORT\")))) (error (c) (format *error-output* \"~\u0026An error occured: ~a~\u0026\" c) (uiop:quit 1))) In addition we have allowed the user to set the application’s port with an environment variable.\nWe can run the file like so:\nsbcl --load run.lisp After loading the project, the web server is started in the background. We are offered the usual Lisp REPL, from which we can interact with the running application.\nWe can also connect to the running application from our preferred editor, from home, and compile the changes in our editor to the running instance. See the following section: connecting to a remote lisp image on the Cookbook.\nBuilding a self-contained executable Info See the tutorial.\nAs for all Common Lisp applications, we can bundle our web app in one single executable, including the assets. It makes deployment very easy: copy it to your server and run it.\n$ ./my-web-app Hunchentoot server is started. Listening on localhost:9003. See this recipe on scripting#for-web-apps.\nAs for any executable, you need this in your .asd file:\n:build-operation \"program-op\" ;; leave as is :build-pathname \"\u003cbinary-name\u003e\" :entry-point \"\u003cmy-package:main-function\u003e\" and you build the binary with (asdf:make :myproject).\nHowever, you might find that as soon as you start your app, its stops. That happens because the server thread is started in the background, and nothing tells the binary to wait for it. We can simply sleep (for a large-enough amount of time).\n(defun main () (start-app :port 9003) ;; our start-app ;; keep the binary busy in the foreground, for binaries and SystemD. (sleep most-positive-fixnum)) If you want to learn more, see… the Cookbook: scripting, command line arguments, executables.", diff --git a/docs/see-also/index.html b/docs/see-also/index.html index 5986578..920720b 100644 --- a/docs/see-also/index.html +++ b/docs/see-also/index.html @@ -11,14 +11,14 @@ Neil Munro’s Clack/Lack/Ningle tutorial the Cookbook Project skeletons and demos: cl-cookieweb - a web project template lisp-web-template-productlist, a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS. lisp-web-live-reload-example - a toy project to show how to interact with a running web app. Libraries: awesome-cl">See also :: Web Apps in Lisp: Know-how -

See also

Other tutorials:

Project skeletons and demos:

  • cl-cookieweb - a web project template
  • lisp-web-template-productlist, +

Libraries:

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/sitemap.xml b/docs/sitemap.xml index b5cda15..2384cb0 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -1 +1 @@ -http://example.org/tutorial/getting-started/index.htmlhttp://example.org/tutorial/first-route/index.htmlhttp://example.org/tutorial/first-template/index.htmlhttp://example.org/tutorial/index.htmlhttp://example.org/building-blocks/index.htmlhttp://example.org/building-blocks/simple-web-server/index.htmlhttp://example.org/building-blocks/static/index.htmlhttp://example.org/building-blocks/routing/index.htmlhttp://example.org/tutorial/first-path-parameter/index.htmlhttp://example.org/building-blocks/templates/index.htmlhttp://example.org/building-blocks/session/index.htmlhttp://example.org/building-blocks/flash-messages/index.htmlhttp://example.org/building-blocks/headers/index.htmlhttp://example.org/building-blocks/errors-interactivity/index.htmlhttp://example.org/isomorphic-web-frameworks/weblocks/index.htmlhttp://example.org/tutorial/first-url-parameters/index.htmlhttp://example.org/building-blocks/database/index.htmlhttp://example.org/building-blocks/user-log-in/index.htmlhttp://example.org/building-blocks/users-and-passwords/index.htmlhttp://example.org/building-blocks/form-validation/index.htmlhttp://example.org/building-blocks/building-binaries/index.htmlhttp://example.org/isomorphic-web-frameworks/clog/index.htmlhttp://example.org/building-blocks/deployment/index.htmlhttp://example.org/tutorial/first-form/index.htmlhttp://example.org/building-blocks/remote-debugging/index.htmlhttp://example.org/tutorial/first-bonus-css/index.htmlhttp://example.org/building-blocks/electron/index.htmlhttp://example.org/tutorial/first-build/index.htmlhttp://example.org/building-blocks/web-views/index.htmlhttp://example.org/isomorphic-web-frameworks/index.htmlhttp://example.org/see-also/index.htmlhttp://example.org/categories/index.htmlhttp://example.org/tags/index.html \ No newline at end of file +http://example.org/tutorial/getting-started/index.htmlhttp://example.org/tutorial/first-route/index.htmlhttp://example.org/tutorial/first-template/index.htmlhttp://example.org/tutorial/index.htmlhttp://example.org/building-blocks/index.htmlhttp://example.org/building-blocks/simple-web-server/index.htmlhttp://example.org/building-blocks/static/index.htmlhttp://example.org/building-blocks/routing/index.htmlhttp://example.org/tutorial/first-path-parameter/index.htmlhttp://example.org/building-blocks/templates/index.htmlhttp://example.org/building-blocks/session/index.htmlhttp://example.org/building-blocks/flash-messages/index.htmlhttp://example.org/building-blocks/headers/index.htmlhttp://example.org/building-blocks/errors-interactivity/index.htmlhttp://example.org/isomorphic-web-frameworks/weblocks/index.htmlhttp://example.org/tutorial/first-url-parameters/index.htmlhttp://example.org/building-blocks/database/index.htmlhttp://example.org/building-blocks/user-log-in/index.htmlhttp://example.org/building-blocks/users-and-passwords/index.htmlhttp://example.org/building-blocks/form-validation/index.htmlhttp://example.org/building-blocks/put/index.htmlhttp://example.org/building-blocks/building-binaries/index.htmlhttp://example.org/isomorphic-web-frameworks/clog/index.htmlhttp://example.org/building-blocks/deployment/index.htmlhttp://example.org/tutorial/first-form/index.htmlhttp://example.org/building-blocks/remote-debugging/index.htmlhttp://example.org/tutorial/first-bonus-css/index.htmlhttp://example.org/building-blocks/electron/index.htmlhttp://example.org/tutorial/first-build/index.htmlhttp://example.org/building-blocks/web-views/index.htmlhttp://example.org/isomorphic-web-frameworks/index.htmlhttp://example.org/see-also/index.htmlhttp://example.org/categories/index.htmlhttp://example.org/tags/index.html \ No newline at end of file diff --git a/docs/tags/index.html b/docs/tags/index.html index 534aef1..035056a 100644 --- a/docs/tags/index.html +++ b/docs/tags/index.html @@ -1,10 +1,10 @@ Tags :: Web Apps in Lisp: Know-how -

Tags

\ No newline at end of file diff --git a/docs/tutorial/build.lisp b/docs/tutorial/build.lisp index 960e710..b8ba969 100644 --- a/docs/tutorial/build.lisp +++ b/docs/tutorial/build.lisp @@ -4,8 +4,12 @@ (setf uiop:*image-entry-point* #'myproject::main) -(uiop:dump-image "myproject" - :executable t - ;; :toplevel #'myproject::main - ;; :entry-point #'myproject::main - :compression 9) +(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/docs/tutorial/first-bonus-css/index.html b/docs/tutorial/first-bonus-css/index.html index e4dbfc1..d01d82a 100644 --- a/docs/tutorial/first-bonus-css/index.html +++ b/docs/tutorial/first-bonus-css/index.html @@ -7,7 +7,7 @@ Optionally, we may write one
with a class="container" attribute, to have better margins.'>Bonus: pimp your CSS :: Web Apps in Lisp: Know-how -

Bonus: pimp your CSS

Bonus: pimp your CSS

Don’t ask a web developer to help you with the look and feel of the +

Bonus: pimp your CSS

Bonus: pimp your CSS

Don’t ask a web developer to help you with the look and feel of the app, they will bring in hundreds of megabytes of Nodejs dependencies :S We suggest a (nearly) one-liner to get a decent CSS with no efforts: by using a class-less CSS, such as @@ -47,12 +47,12 @@ ")

Refresh http://localhost:8899/?query=one. Do you enjoy the difference?!

I see this:

Β 

However note how our root template is benefiting from the CSS, and the product page isn’t. The two pages should inherit from a base template. It’s about time we setup our templates in their own -directory.

\ No newline at end of file diff --git a/docs/tutorial/first-build/index.html b/docs/tutorial/first-build/index.html index efba6eb..ca02c7b 100644 --- a/docs/tutorial/first-build/index.html +++ b/docs/tutorial/first-build/index.html @@ -7,7 +7,7 @@ Info We didn’t have to restart any Lisp process, nor any web server.">the first build :: Web Apps in Lisp: Know-how -

the first build

We have developped a web app in Common Lisp.

At the first step, we opened a REPL and since then, without noticing, +

the first build

We have developped a web app in Common Lisp.

At the first step, we opened a REPL and since then, without noticing, we have compiled variables and function definitions pieces by pieces, step by step, with keyboard shortcuts giving immediate feedback, testing our progress on the go, running a function in the REPL or @@ -224,12 +224,12 @@ self-contained by default, without extra work: it contains the lisp implementation with its compiler and debugger, the libraries (web server and all), the templates. We can easily deploy this -app. Congrats!

Closing words

We are only scratching the surface of what we’ll want to do with a real app:

  • parse CLI args
  • handle a C-c and other signals
  • read configuration files
  • setup and use a database
  • properly use HTML templates
  • add CSS and other static assets
  • add interactivity on the web page, with or without JavaScript
  • add users login
  • add rights to the routes
  • build a REST API
  • etc

We will explore those topics in the other chapters of this guide.

We did a great job for a first app:

  • we built a Common Lisp web app
  • we created routes, used path and URL parameters
  • we defined an HTML form
  • we used HTML templates
  • we experienced the interactive nature of Common Lisp
  • we explored how to run, build and ship Common Lisp programs.

That’s a lot. You are ready for serious applications now!

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-form/index.html b/docs/tutorial/first-form/index.html index 5ea7daf..dfeae03 100644 --- a/docs/tutorial/first-form/index.html +++ b/docs/tutorial/first-form/index.html @@ -11,7 +11,7 @@ Do you find HTML forms boring, very boring tech? There is no escaping though, you must know the basics. Go read MDN. It’s only later that you’ll have the right to find and use libraries to do them for you. A search form Here’s a search form: (defparameter *template-root* "
") The important elements are the following:'>the first form :: Web Apps in Lisp: Know-how -

the first form

Our root page shows a list of products. We want to do better: provide +

the first form

Our root page shows a list of products. We want to do better: provide a search form.

Do you find HTML forms boring, very boring tech? There is no escaping though, you must know the basics. Go read MDN. It’s only later that you’ll have the right to find and use libraries to do them for you.

A search form

Here’s a search form:

(defparameter *template-root* "
 <form action=\"/\" method=\"GET\">
@@ -154,12 +154,12 @@
   (force-output)
   (setf *server* (make-instance 'easy-routes:easy-routes-acceptor
                                 :port (or port *port*)))
-  (hunchentoot:start *server*))

Do you also clearly see 3 different components in this app? Templates, models, routes.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-path-parameter/index.html b/docs/tutorial/first-path-parameter/index.html index a5d0e91..d24f1bb 100644 --- a/docs/tutorial/first-path-parameter/index.html +++ b/docs/tutorial/first-path-parameter/index.html @@ -15,7 +15,7 @@ Each product detail will be available on the URL /product/n where n is the product ID. Add links To begin with, let’s add links to the list of products: (defparameter *template-root* " Lisp web app ") Carefully observe that, in the href, we had to escape the quotes :/ This is the shortcoming of defining Djula templates as strings in .lisp files. It’s best to move them to their own directory and own files.'>the first path parameter :: Web Apps in Lisp: Know-how -

the first path parameter

So far we have 1 route that displays all our products.

We will make each product line clickable, to open a new page, that will show more details.

Each product detail will be available on the URL /product/n where n is the product ID.

To begin with, let’s add links to the list of products:

(defparameter *template-root* "
+

the first path parameter

So far we have 1 route that displays all our products.

We will make each product line clickable, to open a new page, that will show more details.

Each product detail will be available on the URL /product/n where n is the product ID.

To begin with, let’s add links to the list of products:

(defparameter *template-root* "
 <title> Lisp web app </title>
 <body>
   <ul>
@@ -77,12 +77,12 @@
 
 (easy-routes:defroute product-route ("/product/:n") (&path (n 'integer))
   (render *template-product* :product (get-product n)))

That’s better. Usually all my routes have this form: name, arguments, -call to some sort of render function with a template and arguments.

I’d like to carry on with features but let’s have a word about URL parameters.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-route/index.html b/docs/tutorial/first-route/index.html index 0cf7036..da7e864 100644 --- a/docs/tutorial/first-route/index.html +++ b/docs/tutorial/first-route/index.html @@ -15,7 +15,7 @@ Let’s start with a couple variables. ;; still in src/myproject.lisp (defvar *server* nil "Server instance (Hunchentoot acceptor).") (defparameter *port* 8899 "The application port.") Now hold yourself and let’s write our first route: (easy-routes:defroute root ("/") () "hello app") It only returns a string. We’ll use HTML templates in a second.'>the first route :: Web Apps in Lisp: Know-how -

the first route

It’s time we create a web app!

Our first route

Our very first route will only respond with a “hello world”.

Let’s start with a couple variables.

;; still in src/myproject.lisp
+

the first route

It’s time we create a web app!

Our first route

Our very first route will only respond with a “hello world”.

Let’s start with a couple variables.

;; still in src/myproject.lisp
 (defvar *server* nil
   "Server instance (Hunchentoot acceptor).")
 
@@ -38,12 +38,12 @@
 never have to wait for stuff re-compiling or re-loading. You’ll
 compile your code with a shortcut and have instant feedback. SBCL
 warns us on bad syntax, undefined variables and other typos, some type
-mismatches, unreachable code (meaning we might have an issue), etc.

To stop the app, use (hunchentoot:stop *server*). You can put this in a “stop-app” function.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-template/index.html b/docs/tutorial/first-template/index.html index 78457cb..ef58888 100644 --- a/docs/tutorial/first-template/index.html +++ b/docs/tutorial/first-template/index.html @@ -15,7 +15,7 @@ We’ll use Djula HTML templates. Usually, templates go into their templates/ directory. But, we will start out by defining our first template inside our .lisp file: ;; scr/myproject.lisp (defparameter *template-root* " Lisp web app hello app ") Compile this variable as you go, with C-c C-c (or call again load from any REPL).'>the first template :: Web Apps in Lisp: Know-how -

the first template

Our route only returns a string:

(easy-routes:defroute root ("/") ()
+

the first template

Our route only returns a string:

(easy-routes:defroute root ("/") ()
     "hello app")

Can we have it return a template?

We’ll use Djula HTML templates.

Usually, templates go into their templates/ directory. But, we will start out by defining our first template inside our .lisp file:

;; scr/myproject.lisp
 (defparameter *template-root* "
@@ -131,12 +131,12 @@
   (force-output)
   (setf *server* (make-instance 'easy-routes:easy-routes-acceptor
                                 :port (or port *port*)))
-  (hunchentoot:start *server*))
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/first-url-parameters/index.html b/docs/tutorial/first-url-parameters/index.html index cc14dab..f682ffd 100644 --- a/docs/tutorial/first-url-parameters/index.html +++ b/docs/tutorial/first-url-parameters/index.html @@ -15,7 +15,7 @@ We want a debug URL parameter that will show us more data. The URL will accept a ?debug=t part. We have this product route: (easy-routes:defroute product-route ("/product/:n") (&path (n 'integer)) (render *template-product* :product (get-product n))) With or without the &path part, either is good for now, so let’s remove it for clarity:'>the first URL parameter :: Web Apps in Lisp: Know-how -

the first URL parameter

As of now we can access URLs like these: +

the first URL parameter

As of now we can access URLs like these: http://localhost:8899/product/0 where 0 is a path parameter.

We will soon need URL parameters so let’s add one to our routes.

We want a debug URL parameter that will show us more data. The URL will accept a ?debug=t part.

We have this product route:

(easy-routes:defroute product-route ("/product/:n") (&path (n 'integer))
@@ -90,12 +90,12 @@
                                 :port port))
   (hunchentoot:start *server*))

Everything is contained in one file, and we can run everything from sources or we can build a self-contained -binary. Pretty cool!

Before we do so, we’ll add a great feature: searching for products.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/getting-started/index.html b/docs/tutorial/getting-started/index.html index f57c540..02867e0 100644 --- a/docs/tutorial/getting-started/index.html +++ b/docs/tutorial/getting-started/index.html @@ -11,7 +11,7 @@ define a list of products, stored in a dummy database, we’ll have an index page with a search form, we’ll display search results, and have one page per product to see it in details. But everything starts with a project (a system in Lisp parlance) definition. Setting up the project Let’s create a regular Common Lisp project. We could start straihgt from the REPL, but we’ll need a project definition to save our project dependencies, and other metadata. Such a project is an ASDF file.">Getting Started :: Web Apps in Lisp: Know-how -

Getting Started

In this application we will:

  • define a list of products, stored in a dummy database,
  • we’ll have an index page with a search form,
  • we’ll display search results,
  • and have one page per product to see it in details.

But everything starts with a project (a system in Lisp parlance) definition.

Setting up the project

Let’s create a regular Common Lisp project.

We could start straihgt from the REPL, but we’ll need a project +

Getting Started

In this application we will:

  • define a list of products, stored in a dummy database,
  • we’ll have an index page with a search form,
  • we’ll display search results,
  • and have one page per product to see it in details.

But everything starts with a project (a system in Lisp parlance) definition.

Setting up the project

Let’s create a regular Common Lisp project.

We could start straihgt from the REPL, but we’ll need a project definition to save our project dependencies, and other metadata. Such a project is an ASDF file.

Create the file myproject.asd at the project root:

(asdf:defsystem "myproject"
   :version "0.1"
@@ -65,12 +65,12 @@
         collect (list i
                       (format nil "Product nb ~a" i)
                       9.99)))

this returns:

((0 "Product nb 0" 9.99) (1 "Product nb 1" 9.99) (2 "Product nb 2" 9.99)
- (3 "Product nb 3" 9.99) (4 "Product nb 4" 9.99))

That’s not much, but that’s enough to show content in a web app, which we’ll do next.

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/index.html b/docs/tutorial/index.html index cefd7dc..b356acb 100644 --- a/docs/tutorial/index.html +++ b/docs/tutorial/index.html @@ -11,17 +11,17 @@ In this first tutorial we will build a simple app that shows a web form that will search and display a list of products. In doing so, we will see many necessary building blocks to write web apps in Lisp: how to start a server how to create routes how to define and use path and URL parameters how to define HTML templates how to run and build the app, from our editor and from the terminal. In doing so, we’ll experience the interactive nature of Common Lisp and we’ll learn important commands: running sbcl from the command line with a couple options, what is load, how to interactively compile our app with a keyboard shortcut, how to structure a project with an .asd definition and a package, etc.">Tutorial part 1 :: Web Apps in Lisp: Know-how -

Tutorial part 1

Are you ready to build a web app in Common Lisp?

In this first tutorial we will build a simple app that shows a web +

Tutorial part 1

Are you ready to build a web app in Common Lisp?

In this first tutorial we will build a simple app that shows a web form that will search and display a list of products.

In doing so, we will see many necessary building blocks to write web apps in Lisp:

  • how to start a server
  • how to create routes
  • how to define and use path and URL parameters
  • how to define HTML templates
  • how to run and build the app, from our editor and from the terminal.

In doing so, we’ll experience the interactive nature of Common Lisp and we’ll learn important commands: running sbcl from the command line with a couple options, what is load, how to interactively compile our app with a keyboard shortcut, how to structure a project -with an .asd definition and a package, etc.

We expect that you have Quicklisp installed. If not, please see the Cookbook: getting started.

Now let’s get started.

But beware. There is no going back.

(look at the menu and don’t miss the small previous-next arrows on the top right)

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/tutorial/templates/base/index.html b/docs/tutorial/templates/base/index.html index 0ef8a5a..8b88d3b 100644 --- a/docs/tutorial/templates/base/index.html +++ b/docs/tutorial/templates/base/index.html @@ -1,10 +1,10 @@ -
\ No newline at end of file