diff --git a/Makefile b/Makefile index 2b107ce..50a12fe 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ run: ./hugo serve build: - ./hugo build --gc --minify && cp -r public/* docs && echo "copied to docs/. Done." + ./hugo build --gc --minify && cp -r public/* docs && git add docs/* && echo "copied to docs/. Done." publish: git commit -m "publish" && git push diff --git a/content/_index.md b/content/_index.md index d8b5163..c22fd65 100644 --- a/content/_index.md +++ b/content/_index.md @@ -55,7 +55,7 @@ Cookbook](https://lispcookbook.github.io/cl-cookbook/). ## Contact -We are @vindarel on [Lisp's Discord server](https://discord.gg/hhk46CE) and Mastodon. +We are @vindarel on [Lisp's Discord server](https://discord.gg/hhk46CE) and [Mastodon](https://framapiaf.org/@vindarel). diff --git a/content/building-blocks/PUT.md b/content/building-blocks/PUT.md new file mode 100644 index 0000000..36b0b01 --- /dev/null +++ b/content/building-blocks/PUT.md @@ -0,0 +1,18 @@ ++++ +title = "PUT and request parameters" +weight = 145 ++++ + +To access the body parameters of a PUT request, one must add `:PUT` to +`hunchentoot:*methods-for-post-parameters*`, which defaults to only +`(:POST)`: + +```lisp +(push :put hunchentoot:*methods-for-post-parameters*) +``` + +This parameter: + +> is a list of the request method types (as keywords) for which Hunchentoot will try to compute POST-PARAMETERS. + +No such setting is required with Lack and Ningle. diff --git a/content/building-blocks/database.md b/content/building-blocks/database.md index 8cd4376..ce7b80d 100644 --- a/content/building-blocks/database.md +++ b/content/building-blocks/database.md @@ -232,7 +232,7 @@ Once we edit the table definition (aka the class definition), Mito will (by default) automatically migrate it. There is much more to say, but we refer you to Mito's good -documentation and to the Cookbook. +documentation and to the Cookbook (links below). ## How to integrate the databases into the web frameworks diff --git a/content/building-blocks/flash-messages.lisp b/content/building-blocks/flash-messages.lisp new file mode 100644 index 0000000..1945bc2 --- /dev/null +++ b/content/building-blocks/flash-messages.lisp @@ -0,0 +1,68 @@ + +;;; +;;; Demo of flash messages: +;;; anywhere in a route, easily add flash messages to the session. +;;; They are rendered at the next rendering of a template. +;;; They are removed from the session once rendered. +;;; + +;; inspired by https://github.com/rudolfochrist/booker/blob/main/app/controllers.lisp + +(uiop:add-package-local-nickname :ht :hunchentoot) + +(djula:add-template-directory ".") + +(defparameter *flash-template* (djula:compile-template* "flash-template.html")) + +(defparameter *port* 9876) + +(defvar *server* nil "Our Hunchentoot acceptor") + +(defun flash (type message) + "Add a flash message in the session. + + TYPE: can be anything as you do what you want with it in the template. + Here, it is a string that represents the Bulma CSS class for notifications: is-primary, is-warning etc. + MESSAGE: string" + (let* ((session (ht:start-session)) + (flash (ht:session-value :flash session))) + (setf (ht:session-value :flash session) + ;; With a cons, REST returns 1 element + ;; (when with a list, REST returns a list) + (cons (cons type message) flash)))) + +;;; delete flash after it is used. +(defmethod ht:handle-request :after (acceptor request) + (ht:delete-session-value :flash)) + +#+another-solution +(defun render (template &rest args) + (apply + #'djula:render-template* template nil + (list* + :flashes (or (ht:session-value :flash) + (list (cons "is-primary" "No more flash messages were found in the session. This is a default notification."))) + args))) + + +(easy-routes:defroute flash-route ("/flash/" :method :get) () + #-another-solution + (djula:render-template* *flash-template* nil + :flashes (or (ht:session-value :flash) + (list (cons "is-primary" "No more flash messages were found in the session. This is a default notification.")))) + #+another-solution + (render *flash-template*) + ) + +(easy-routes:defroute flash-redirect-route ("/tryflash/") () + (flash "is-warning" "This is a warning message held in the session. It should appear only once: reload this page and you won't see the flash message again.") + (ht:redirect "/flash/")) + +(defun start (&key (port *port*)) + (format t "~&Starting the web server on port ~a~&" port) + (force-output) + (setf *server* (make-instance 'easy-routes:easy-routes-acceptor :port port)) + (ht:start *server*)) + +(defun stop () + (ht:stop *server*)) diff --git a/content/building-blocks/flash-messages.md b/content/building-blocks/flash-messages.md new file mode 100644 index 0000000..824ba85 --- /dev/null +++ b/content/building-blocks/flash-messages.md @@ -0,0 +1,324 @@ ++++ +title = "Flash messages" +weight = 36 ++++ + +Flash messages are temporary messages you want to show to your +users. They should be displayed once, and only once: on a subsequent +page load, they don't appear anymore. + +![](/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 a request, delete the +`:flash` object from the session. + +{{% notice warning %}} + +If your application sends API requests in JavaScript, they can delete flash messages without you noticing. Read more below. + +An external API request (from the command line for example) is not +concerned, as it doesn't carry Hunchentoot session cookies. + +{{% /notice %}} + + +## Render flash messages in templates + +### Set up Djula templates + +Create a new `flash-template.html` file. + +~~~lisp +(djula:add-template-directory "./") +(defparameter *flash-template* (djula:compile-template* "flash-template.html")) +~~~ + +{{% notice info %}} + +You might need to change the current working +directory of your Lisp REPL to the directory of your .lisp file, so +that `djula:compile-template*` can find your template. Use the short +command `,cd` or `(swank:set-default-directory "/home/you/path/to/app/")`. +See also `asdf:system-relative-pathname system directory`. + +{{% /notice %}} + +### HTML template + +This is our template. We use [Bulma +CSS](https://bulma.io/documentation/elements/notification/) to pimp it +up and to use its notification blocks. + +```html + + + + + + + + WALK - flash messages + + + + + + + + + +
+
+
+ +

Flash messages.

+ +
Click /tryflash/ to access an URL that creates a flash message and redirects you here.
+ + {% for flash in flashes %} + +
+ + {{ flash.rest }} +
+ + {% endfor %} + +
+
+
+ + + + + + +``` + +Look at + +``` +{% for flash in flashes %} +``` + +where we render our flash messages. + +Djula allows us to write `{{ flash.first }}` and `{{ flash.rest }}` to +call the Lisp functions on those objects. + +We must now create a route that renders our template. + + +## Routes + +The `/flash/` URL is the demo endpoint: + +```lisp +(easy-routes:defroute flash-route ("/flash/" :method :get) () + (djula:render-template* *flash-template* nil + :flashes (or (ht:session-value :flash) + (list (cons "is-primary" "No more flash messages were found in the session. This is a default notification."))))) +``` + +It is here that we pass the flash messages as a parameter to the template. + +In your application, you must add this parameter in all the existing +routes. To make this easier, you can: +- use Djula's [default template variables](https://mmontone.github.io/djula/djula/Variables.html#Default-template-variables), but our parameters are to be found dynamically in the current request's session, so we can instead +- create a "render" function of ours that calls `djula:render-template*` and always adds the `:flash` parameter. Use `apply`: + +~~~lisp +(defun render (template &rest args) + (apply + #'djula:render-template* template nil + ;; All arguments must be in a list. + (list* + :flashes (or (ht:session-value :flash) + (list (cons "is-primary" "No more flash messages were found in the session. This is a default notification."))) + args))) +~~~ + +Finally, this is the route that creates a flash message: + +```lisp +(easy-routes:defroute flash-redirect-route ("/tryflash/") () + (flash "is-warning" "This is a warning message held in the session. It should appear only once: reload this page and you won't see the flash message again.") + (ht:redirect "/flash/")) +``` + +## Demo + +Start the app with `(start)` if you didn't start Hunchentoot already, +otherwise it was enough to compile the new routes. + +You should see a default notification. Click the "/tryflash/" URL and +you'll see a flash message, that is deleted after use. + +Refresh the page, and you won't see the flash message again. + +- full code: https://github.com/web-apps-in-lisp/web-apps-in-lisp.github.io/blob/master/content/building-blocks/flash-messages.lisp + +## Discussing: Flash messages and API calls + +Our `:after` method on the Hunchentoot request lifecycle will delete +flash messages for any request that carries the session cookies. If +your application makes API calls, you can use the Fetch method with +the `{credentials: "omit"}` parameter: + +~~~javascript +fetch("http://localhost:9876/api/", { + credentials: "omit" +}) +~~~ + +Otherwise, don't use this `:after` method and delete flash messages +explicitely in your non-API routes. + +We could use a macro shortcut for this: + + +~~~lisp +(defmacro with-flash-messages ((messages) &body body) + `(let ((,messages (ht:session-value :flash))) + (prog1 + (progn + ,@body) + (ht:delete-session-value :flash)))) +~~~ + +Use it like this: + +~~~lisp +(easy-routes:defroute flash-route ("/flash/" :method :get) () + (with-flash-messages (messages) + (djula:render-template* *flash-template* nil + :flashes (or messages + (list (cons "is-primary" "No more flash messages were found in the session. This is a default notification.")))))) +~~~ + +We want our macro to return the result of `djula:render-template*`, +and *not* the result of `ht:delete-session-value`, that is nil, hence +the "prog1/ progn" dance. diff --git a/content/building-blocks/flash-messages.png b/content/building-blocks/flash-messages.png new file mode 100644 index 0000000..e5de581 Binary files /dev/null and b/content/building-blocks/flash-messages.png differ diff --git a/content/building-blocks/flash-template.html b/content/building-blocks/flash-template.html new file mode 100644 index 0000000..dac19d5 --- /dev/null +++ b/content/building-blocks/flash-template.html @@ -0,0 +1,76 @@ + + + + + + + + WALK - flash messages + + + + + + + + + +
+
+
+ +

Flash messages.

+ +
Click /tryflash/ to access an URL that creates a flash message and redirects you here.
+ + {% for flash in flashes %} + +
+ + {{ flash.rest }} +
+ + {% endfor %} + +
+
+
+ + + + + + diff --git a/content/building-blocks/static.md b/content/building-blocks/static.md index bacefbd..b90b3da 100644 --- a/content/building-blocks/static.md +++ b/content/building-blocks/static.md @@ -20,9 +20,9 @@ For example: 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 + "/static/" + (asdf:system-relative-pathname :myproject "src/static/")) + ;; ^^^ starts without a / hunchentoot:*dispatch-table*)) ~~~ @@ -33,8 +33,20 @@ and call it in the function that starts your application: ``` Now our project's static files located under `src/static/` are served -with the `/static/` prefix, access them like this: +with the `/static/` prefix. Access them like this: ```html ``` + +or + +```html + +``` + +where the file `src/static/test.js` could be + +```js +console.log("hello"); +``` diff --git a/content/building-blocks/templates.md b/content/building-blocks/templates.md index 69e8ee8..6abbf6f 100644 --- a/content/building-blocks/templates.md +++ b/content/building-blocks/templates.md @@ -29,6 +29,25 @@ and then we can declare and compile the ones we use, for example:: (defparameter +products.html+ (djula:compile-template* "products.html")) ~~~ +{{% notice info %}} + +If you get an error message when calling `add-template-directory` like this + +``` +The value + #P"/home/user/…/project/src/templates/index.html" + +is not of type + STRING +from the function type declaration. + [Condition of type TYPE-ERROR] +``` + +then update your Quicklisp dist or clone Djula in `~/quicklisp/local-projects/`. + +{{% /notice %}} + + A Djula template looks like this: ```html diff --git a/content/building-blocks/user-log-in.md b/content/building-blocks/user-log-in.md index eed9dc2..b2924bc 100644 --- a/content/building-blocks/user-log-in.md +++ b/content/building-blocks/user-log-in.md @@ -326,6 +326,9 @@ Remarks: ## Full code ```lisp +(defpackage :myproject + (:use :cl)) + (in-package :myproject) ;; User-facing paramaters. @@ -433,8 +436,7 @@ Remarks: ;; Server. (defun start-server (&key (port *port*)) (format t "~&Starting the login demo on port ~a~&" port) - (unless *server* - (setf *server* (make-instance 'hunchentoot:easy-acceptor :port port))) + (setf *server* (make-instance 'easy-routes:easy-routes-acceptor :port port)) (hunchentoot:start *server*)) (defun stop-server () diff --git a/content/building-blocks/users-and-passwords.md b/content/building-blocks/users-and-passwords.md index cf74953..9ab5e75 100644 --- a/content/building-blocks/users-and-passwords.md +++ b/content/building-blocks/users-and-passwords.md @@ -4,15 +4,13 @@ weight = 130 +++ We don't know of a Common Lisp framework that will create users and -roles for you and protect your routes. You'll have to either write -some Lisp, either use an external tool (such as Keycloak) that will -provide all the user management. +roles for you and protect your routes all at the same time. We have +building blocks but you'll have to write some glue Lisp code. -{{% notice info %}} +You can also turn to external tools (such as [Keycloak](https://www.keycloak.org/) or [Tesseral](https://tesseral.com/)) that will +provide all the industrial-grade user management. -Stay tuned! We are on to something. - -{{% /notice %}} +If you like the Mito ORM, look at [mito-auth](https://github.com/fukamachi/mito-auth/) and [mito-email-auth](https://github.com/40ants/mito-email-auth). ## Creating users @@ -55,19 +53,28 @@ Did it work? Run `SELECT * FROM users;`. ## Encrypting passwords -### With cl-pass +### With cl-bcrypt -[cl-pass](https://github.com/eudoxia0/cl-pass) is a password hashing and verification library. It is as simple to use as this: +[cl-bcrypt](https://github.com/dnaeon/cl-bcrypt) is a password hashing and verification library. It is as simple to use as this: ```lisp -(cl-pass:hash "test") -;; "PBKDF2$sha256:20000$5cf6ee792cdf05e1ba2b6325c41a5f10$19c7f2ccb3880716bf7cdf999b3ed99e07c7a8140bab37af2afdc28d8806e854" -(cl-pass:check-password "test" *) -;; t -(cl-pass:check-password "nope" **) -;; nil +CL-USER> (defparameter *password* + (bcrypt:make-password "my-secret-password")) +*PASSWORD* ``` +and you can specify another salt, another cost factor and another algorithm identifier. + +Then you can use `bcrypt:encode` to get a string reprentation of the password: + +~~~lisp +CL-USER> (bcrypt:encode *password*) +"$2a$16$ClVzMvzfNyhFA94iLDdToOVeApbDppFru3JXNUyi1y1x6MkO0KzZa" +~~~ + +and you decode a password with `decode`. + + ### Manually (with Ironclad) In this recipe we do the encryption and verification ourselves. We use the de-facto standard @@ -124,3 +131,12 @@ library. ``` *Credit: `/u/arvid` on [/r/learnlisp](https://www.reddit.com/r/learnlisp/comments/begcf9/can_someone_give_me_an_eli5_on_hiw_to_encrypt_and/)*. + +## See also + +* [cl-authentic](https://github.com/charJe/cl-authentic) - Password management for Common Lisp (web) applications. [LLGPL][8]. + - safe password storage: cleartext-free, using your choice of hash algorithm through ironclad, storage in an SQL database, + - password reset mechanism with one-time tokens (suitable for mailing to users for confirmation), + - user creation optionally with confirmation tokens (suitable for mailing to users), + +and more on the awesome-cl list. diff --git a/content/isomorphic web frameworks/weblocks.md b/content/isomorphic web frameworks/weblocks.md index b856895..bc8259d 100644 --- a/content/isomorphic web frameworks/weblocks.md +++ b/content/isomorphic web frameworks/weblocks.md @@ -13,7 +13,6 @@ The Reblocks's demo we will build is a TODO app: ![](https://40ants.com/reblocks/images/docs/images/quickstart-check-task.gif) ---- {{% notice note %}} diff --git a/content/see also/_index.md b/content/see also/_index.md index 51233d2..ff7a817 100644 --- a/content/see also/_index.md +++ b/content/see also/_index.md @@ -11,10 +11,6 @@ Other tutorials: Project skeletons and demos: - [cl-cookieweb](https://github.com/vindarel/cl-cookieweb) - a web project template -- [Feather](https://hg.sr.ht/~wnortje/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](https://github.com/vindarel/lisp-web-template-productlist), a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS. - [lisp-web-live-reload-example](https://github.com/vindarel/lisp-web-live-reload-example/) - diff --git a/content/tutorial/build.lisp b/content/tutorial/build.lisp index 68c1a00..b8ba969 100644 --- a/content/tutorial/build.lisp +++ b/content/tutorial/build.lisp @@ -2,7 +2,14 @@ (ql:quickload "myproject") -(sb-ext:save-lisp-and-die "myproject" - :executable t - :toplevel #'myproject::main - :compression 9) +(setf uiop:*image-entry-point* #'myproject::main) + +(uiop:dump-image "myproject" :executable t :compression 9) + +#| +;; The same as: + +(sb-ext:save-lisp-and-die "myproject" :executable t :compression 9 + :toplevel #'myproject::main) + +|# diff --git a/content/tutorial/first-build.md b/content/tutorial/first-build.md index 288de51..110d3bd 100644 --- a/content/tutorial/first-build.md +++ b/content/tutorial/first-build.md @@ -341,9 +341,12 @@ Building binaries with SBCL is done with the function `sb-ext:save-lisp-and-die` (it lives in the `sb-ext` SBCL module, that is available by default). -Other implementations don't define the exact same function, that's why -we need a compatibility layer, which is provided by ASDF. We show this -method in the Cookbook and later in this guide. +Other implementations don't define the exact same function, for +instance on Clozure CL the function is `ccl:save-application`. That's +why we'll want a compatibility layer to write a portable script across +implementations. It is as always provided by ASDF with +`uiop:dump-image` and also with a system declaration in the .asd +files. SBCL binaries are portable from and to the same operating system: build on GNU/Linux, run on GNU/Linux. Or build on a CI system on the 3 diff --git a/docs/404.html b/docs/404.html index 4c303ad..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 d47439f..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 a93b7c7..d5b703c 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 03051b3..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 6b68c2d..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 56a747f..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.lisp b/docs/building-blocks/flash-messages.lisp new file mode 100644 index 0000000..f50c426 --- /dev/null +++ b/docs/building-blocks/flash-messages.lisp @@ -0,0 +1,88 @@ + +;;; +;;; 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) + (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 +(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 + (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*)) + (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 0000000..e5de581 Binary files /dev/null and b/docs/building-blocks/flash-messages.png differ diff --git a/docs/building-blocks/flash-messages/index.html b/docs/building-blocks/flash-messages/index.html new file mode 100644 index 0000000..77d4ac0 --- /dev/null +++ b/docs/building-blocks/flash-messages/index.html @@ -0,0 +1,168 @@ +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 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 +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.

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:

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:

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

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

\ 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..1f3dadd --- /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 diff --git a/docs/building-blocks/form-validation/index.html b/docs/building-blocks/form-validation/index.html index 8373c64..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."><meta itemprop=wordCount content="545"><title>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 3f459ea..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 c998e1a..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 ff571a9..3b7fb4c 100644 --- a/docs/building-blocks/index.xml +++ b/docs/building-blocks/index.xml @@ -9,15 +9,18 @@ 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: (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: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. @@ -29,12 +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. 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 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 caa7e9f..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 2e37643..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 fdbc9d5..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 d0bd96a..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 68df7ba..c9347f8 100644 --- a/docs/building-blocks/static/index.html +++ b/docs/building-blocks/static/index.html @@ -2,35 +2,35 @@ 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:'>Static assets :: Web Apps in Lisp: Know-how -

Static assets

How can we serve static assets?

Remember from simple web server +(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 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
+          "/static/"
+          (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" />
\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/building-blocks/static/index.xml b/docs/building-blocks/static/index.xml index 16a3469..2263cb0 100644 --- a/docs/building-blocks/static/index.xml +++ b/docs/building-blocks/static/index.xml @@ -2,4 +2,4 @@ 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:Hugoen-us \ No newline at end of file +(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:Hugoen-us \ No newline at end of file diff --git a/docs/building-blocks/templates/index.html b/docs/building-blocks/templates/index.html index cc6035d..681671f 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>
@@ -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 + 
\ 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 diff --git a/docs/building-blocks/user-log-in/index.html b/docs/building-blocks/user-log-in/index.html index 144c844..937602c 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?">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 diff --git a/docs/building-blocks/users-and-passwords/index.html b/docs/building-blocks/users-and-passwords/index.html index 989d16d..91d5a61 100644 --- a/docs/building-blocks/users-and-passwords/index.html +++ b/docs/building-blocks/users-and-passwords/index.html @@ -1,27 +1,29 @@ -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 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,
   username VARCHAR(255),
   email VARCHAR(255),
   password VARCHAR(255),
-)

You can run this right now with SQLite on the command line:

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

Create users:

$ sqlite3 db.db "INSERT INTO users VALUES(1,'Alice','alice@mail','xxx');"

Did it work? Run SELECT * FROM users;.

Encrypting passwords

With cl-pass

cl-pass is a password hashing and verification library. It is as simple to use as this:

(cl-pass:hash "test")
-;; "PBKDF2$sha256:20000$5cf6ee792cdf05e1ba2b6325c41a5f10$19c7f2ccb3880716bf7cdf999b3ed99e07c7a8140bab37af2afdc28d8806e854"
-(cl-pass:check-password "test" *)
-;; t
-(cl-pass:check-password "nope" **)
-;; nil

Manually (with Ironclad)

In this recipe we do the encryption and verification ourselves. We use the de-facto standard +)

You can run this right now with SQLite on the command line:

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

Create users:

$ sqlite3 db.db "INSERT INTO users VALUES(1,'Alice','alice@mail','xxx');"

Did it work? Run SELECT * FROM users;.

Encrypting passwords

With cl-bcrypt

cl-bcrypt is a password hashing and verification library. It is as simple to use as this:

CL-USER> (defparameter *password*
+           (bcrypt:make-password "my-secret-password"))
+*PASSWORD*

and you can specify another salt, another cost factor and another algorithm identifier.

Then you can use bcrypt:encode to get a string reprentation of the password:

CL-USER> (bcrypt:encode *password*)
+"$2a$16$ClVzMvzfNyhFA94iLDdToOVeApbDppFru3JXNUyi1y1x6MkO0KzZa"

and you decode a password with decode.

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

The following snippet creates the password hash that should be stored in your @@ -52,12 +54,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/users-and-passwords/index.xml b/docs/building-blocks/users-and-passwords/index.xml index 0bd70ae..ce44c91 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 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 diff --git a/docs/building-blocks/web-views/index.html b/docs/building-blocks/web-views/index.html index 1e51f8c..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 a16d199..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 bd184cf..4ba36c4 100644 --- a/docs/css/chroma-auto.css +++ b/docs/css/chroma-auto.css @@ -1,2 +1,2 @@ -@import "chroma-relearn-light.css?1736462513" screen and (prefers-color-scheme: light); -@import "chroma-relearn-dark.css?1736462513" 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 7ed2e26..ef96558 100644 --- a/docs/css/format-print.css +++ b/docs/css/format-print.css @@ -1,5 +1,5 @@ -@import "theme-relearn-light.css?1736462513"; -@import "chroma-relearn-light.css?1736462513"; +@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 67962bf..421377a 100644 --- a/docs/css/print.css +++ b/docs/css/print.css @@ -1 +1 @@ -@import "format-print.css?1736462513"; +@import "format-print.css?1756377279"; diff --git a/docs/css/swagger.css b/docs/css/swagger.css index 4488610..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?1736462513"; -@import "variables.css?1736462513"; +@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 bf4946d..29f6632 100644 --- a/docs/css/theme-auto.css +++ b/docs/css/theme-auto.css @@ -1,2 +1,2 @@ -@import "theme-relearn-light.css?1736462513" screen and (prefers-color-scheme: light); -@import "theme-relearn-dark.css?1736462513" 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 90825f2..7172e76 100644 --- a/docs/css/theme.css +++ b/docs/css/theme.css @@ -1,4 +1,4 @@ -@import "variables.css?1736462513"; +@import "variables.css?1756377279"; @charset "UTF-8"; diff --git a/docs/index.html b/docs/index.html index abaea7f..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,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 7d2b17a..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 879ecb3..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 4e00657..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 6f71494..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 3e175a8..c8f6043 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" @@ -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" @@ -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 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", + "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", @@ -121,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", @@ -129,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", @@ -137,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-pass cl-pass is a password hashing and verification library. It is as simple to use as this:\n(cl-pass:hash \"test\") ;; \"PBKDF2$sha256:20000$5cf6ee792cdf05e1ba2b6325c41a5f10$19c7f2ccb3880716bf7cdf999b3ed99e07c7a8140bab37af2afdc28d8806e854\" (cl-pass:check-password \"test\" *) ;; t (cl-pass:check-password \"nope\" **) ;; nil Manually (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 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" @@ -151,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.", @@ -209,7 +225,7 @@ var relearn_searchindex = [ }, { "breadcrumb": "Tutorial part 1", - "content": "We have developped a web app in Common Lisp.\nAt 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 refreshing the browser window.\nInfo We didn’t have to restart any Lisp process, nor any web server.\nThink about it, that’s awesome!\n(and yet, we didn’t talk about the interactive debugger and about unwinding the stack to resume from errors)\nHowever, it is useful to start our application from scratch once in a while:\ndid we list all our dependencies in the .asd project definition? does it work from scratch, do we have any issue with fresh data? We can run our app from sources, and we can build a self-contained binary.\nRun from sources Do you remember what we did at the beginning to load our project?\ncompile and load the .asd project definition quickload our project start the web server. That’s all we need. Let’s write these 3 commands in a new file. At the project root, create a file run.lisp, in which you copy those steps:\n;; run.lisp (load \"myproject.asd\") (ql:quickload \"myproject\") (myproject::start-server) Why the double :: in myproject::start-server? Because we didn’t export the start-server function, and because we didn’t write (in-package :myproject).\nThis works too:\n(in-package :myproject) (start-server) Now run the app from the terminal:\nsbcl --load run.lisp or use rlwrap sbcl (a “readline wrapper”) for convenience, to get REPL history and to support the arrow keys. Yeah the default SBCL REPL is barebones.\n“Address in use” error and interactive debugger You should see this output, but you shouldn’t be worried since the error message is explicit:\n$ rlwrap sbcl --load run.lisp This is SBCL 2.1.5, an implementation of ANSI Common Lisp. More information about SBCL is available at \u003chttp://www.sbcl.org/\u003e. SBCL is free software, provided as is, with absolutely no warranty. It is mostly in the public domain; some portions are provided under BSD-style licenses. See the CREDITS and COPYING files in the distribution for more information. To load \"myproject\": Load 1 ASDF system: myproject ; Loading \"myproject\" .............. Starting the web server on port 8899 While evaluating the form starting at line 5, column 0 of #P\"/home/vince/projets/web-apps-in-lisp/walk/walk-book/content/tutorial/run.lisp\": debugger invoked on a USOCKET:ADDRESS-IN-USE-ERROR in thread #\u003cTHREAD \"main thread\" RUNNING {10015484D3}\u003e: Condition USOCKET:ADDRESS-IN-USE-ERROR was signalled. Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL. restarts (invokable by number or by possibly-abbreviated name): 0: [RETRY ] Retry EVAL of current toplevel form. 1: [CONTINUE] Ignore error and continue loading file \"/home/vince/projets/web-apps-in-lisp/walk/walk-book/content/tutorial/run.lisp\". 2: [ABORT ] Abort loading file \"/home/vince/projets/web-apps-in-lisp/walk/walk-book/content/tutorial/run.lisp\". 3: Ignore runtime option --load \"run.lisp\". 4: Skip rest of --eval and --load options. 5: Skip to toplevel READ/EVAL/PRINT loop. 6: [EXIT ] Exit SBCL (calling #'EXIT, killing the process). (USOCKET:SOCKET-LISTEN #(0 0 0 0) 8899 :REUSEADDRESS T :REUSE-ADDRESS NIL :BACKLOG 50 :ELEMENT-TYPE (UNSIGNED-BYTE 8)) source: (ERROR C) 0] What you got here is the interactive debugger with “restart” actions, however presented in a kludgy form. What we get in a good Lisp IDE is way easier to use ;) To exit, read the available restarts, you have two options: type “2” or “6” in the prompt and press Enter. You now get another prompt:\n6 * This is the top-level Lisp REPL. You can type any Lisp forms in there. You can press C-d or type (quit).\nIf you wanted to avoid the debugger, you could use an SBCL switch: --non-interactive. But wait a bit before we use it together please.\nBack to our error message.\nThe meaningful bit is “Condition USOCKET:ADDRESS-IN-USE-ERROR was signalled”. usocket is the low-level library that does the networking for Hunchentoot. “Address in use” is correct: our app is already running from our editor, already using the default port number. We have to either stop the app there, either use another port.\nTo stop the app, use (hunchentoot:stop *server*) in our editor REPL and from within the project package, since *server* is a variable of ours.\nTo use another port, what would you prefer?\nwe can give the port as an argument on the command line, like --port 9000. we can find the next available port. Let’s do the latter.\nFind a port number This will be quick as we’ll use a library for that, called find-port.\nPlease quickload it and add it to the project definition.\nYou can either:\nrun (ql:quickload \"find-port\") on the REPL and add “find-port” in the .asd, in the “:depends-on” list, add “find-port” in the .asd, re-compile it with C-c C-k or a call to load, and then (ql:quickload \"myproject\"). Info You just loaded a new library without needing to restart the lisp process.\nOur .asd file now looks like:\n(asdf:defsystem \"myproject\" :version \"0.1\" :author \"me\" :license \"WTFPL\" :depends-on ( :hunchentoot ;; web server :easy-routes ;; routes facility :djula ;; HTML templates ;; utils :find-port ;; \u003c------- added ) :components ((:module \"src\" ;; a src/ subdirectory :components ( (:file \"myproject\") ;; = src/myproject.lisp ))) :description \"A list of products\") How does find-port work? On the REPL, write (find-port:find-port and look at your editor’s minibuffer, or tooltip, as it should show you the function signature. find-port takes key arguments, :min and :max.\nLet’s try:\nMYPROJECT\u003e (find-port:find-port :min 8899) 8900 That’s all we need to use.\nWhere should we call it, in the run.lisp file or in myproject.lisp?\nWe’ll create a new function in myproject.lisp. We don’t alter the start-server function which does a good job, we’ll create a new function that is responsible of dealing with the outside world.\nLet’s call this function main:\n;; myproject.lisp ;;; Top-level. (defun main () (start-server :port (find-port:find-port :min *port*))) And now we use it in the run.lisp file:\n(load \"myproject.asd\") (ql:quickload \"myproject\") (in-package :myproject) (main) Run it again:\nsbcl --load run.lisp This is SBCL 2.1.5, an implementation of ANSI Common Lisp. More information about SBCL is available at \u003chttp://www.sbcl.org/\u003e. SBCL is free software, provided as is, with absolutely no warranty. It is mostly in the public domain; some portions are provided under BSD-style licenses. See the CREDITS and COPYING files in the distribution for more information. To load \"myproject\": Load 1 ASDF system: myproject ; Loading \"myproject\" .................................................. [package myproject]. Starting the web server on port 8900 * Now the meaningful message is “Starting the web server on port 8900”.\nYou notice this last * thing? Once again, it’s the top-level Lisp prompt. You can type any Lisp form. You can interact with the running web app.\nVisit http://localhost:8900/?query=one, it works!\nYou can see HTTP logs in the console:\n127.0.0.1 - [2025-01-03 13:38:17] \"GET /?query=one HTTP/1.1\" 200 503 \"-\" \"Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0\" 127.0.0.1 - [2025-01-03 13:38:18] \"GET /favicon.ico HTTP/1.1\" 404 301 \"http://localhost:8900/?query=one\" \"Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0\" Interact with the running web app * is the Lisp prompt. What is the current package?\nType in\n*package* it responds “#\u003cPACKAGE “COMMON-LISP-USER”\u003e”.\nAre you surprised it isn’t our “myproject” package? I am with you. The explanation is that the in-package we wrote in the run.lisp file is for this file. When SBCL starts up, it starts its top-level evaluator/compiler in the cl-user package. It is asked to load our file, which it does, but once it’s done it’s back at the cl-user package.\nTo facilitate our interactions with the running app, we can of course type right now (in-package :myproject), and we can also add an argument to the command line:\nsbcl --load run.lisp --eval \"(in-package :myproject)\" Ask the value of *package* again. OK.\nWhat can we do more? We can inspect some variables and test some functions. Type in (products), it works. You can check the value of *server*. This is actually useful: we are inspecting the internals of our running web app, and we didn’t need to do anything special. Sure, we can add some logging and a monitoring dashboard. But as a second step.\nWhat can we do more? We can also re-define our functions and variables, or develop our app. Look, we can create a new route. Copy this to the REPL:\n* (easy-routes:defroute hello-route (\"/hello\") () \"hello new route\") HELLO-ROUTE and go to http://localhost:8900/hello: you just created a new route while the app was running.\nBut wait, do we really want to develop our app from this limited terminal REPL? No! If you don’t already, it’s time you understand the usefulness of the load function.\nWhat we want is to edit our .lisp source file, instead of copy-pasting stuff in the REPL, and then to reload the app. The reloading is done with\n* (load \"src/myproject.lisp\") Try it!\nYou can also use ql:quickload.\nIn doing so, you are re-discovering a less interactive way of developping.\nBuilding our first binary Running from sources is OK.\nBuilding a binary requires a bit more work but it can bring advantages:\nthe app will start faster, way faster with a binary (binaries start-up in ±2ms) it may be easier to deploy your app to a server because you don’t need to install a lisp implementation nor to set up Quicklisp on the server. it’s easier to ship your app to end users you can save working versions of your app: binary-v0.1, binary-v0.2 etc. 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).\nOther 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.\nSBCL 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 platforms. They are not truly static binaries as they rely on the GLibc. There was an ongoing patch to make them truly static, it isn’t done though.\nAn SBCL binary will weight, by default, around 80MB. With compression, they get to ±20MB. As your application grows, they’ll stay roughly this size. An app of mine, with dozens of dependencies and all the application code, templates and static assets (JS and CSS) is 35MB. LispWorks binaries, reduced in size with their tree shaker, are known to be smaller, a hello world being ±5MB, a web app around 10MB. This tree shaker isn’t in the free version.\nEnough talk, let’s do it. Create a new build.lisp file. We need these steps:\nload our app build a binary where we define an entry point save-lisp-and-die sb-ext:save-lisp-and-die expects these arguments:\na binary name, as a string. a :toplevel key argument, designing the function to run when the binary starts. :executable to set to t if we build an executable, and not a core image. :compression for a compression level (optionnal), and more. We use it like this:\n(sb-ext:save-lisp-and-die \"myproject\" :executable t :toplevel #'myproject::main) This is our build.lisp file:\n(load \"myproject.asd\") (ql:quickload \"myproject\") (sb-ext:save-lisp-and-die \"myproject\" :executable t :toplevel #'myproject::main) Now run it with\nsbcl --load build.lisp You should see:\n$ sbcl --load build.lisp This is SBCL 2.1.5, an implementation of ANSI Common Lisp. More information about SBCL is available at \u003chttp://www.sbcl.org/\u003e. SBCL is free software, provided as is, with absolutely no warranty. It is mostly in the public domain; some portions are provided under BSD-style licenses. See the CREDITS and COPYING files in the distribution for more information. To load \"myproject\": Load 1 ASDF system: myproject ; Loading \"myproject\" ............ [undoing binding stack and other enclosing state... done] [performing final GC... done] [defragmenting immobile space... (fin,inst,fdefn,code,sym)=2118+1401+24925+24417+26979... done] [saving current Lisp image into myproject: writing 0 bytes from the read-only space at 0x50000000 writing 1696 bytes from the static space at 0x50100000 writing 61177856 bytes from the dynamic space at 0x1000000000 writing 2383872 bytes from the immobile space at 0x50200000 writing 16257024 bytes from the immobile space at 0x52a00000 done] it’s done. Run ls on the project root:\n$ ls -lh total 80316 drwxrwxr-x 2 vince vince 4.0K Jan 3 14:35 src -rw-rw-r-- 1 vince vince 133 Jan 3 14:35 build.lisp -rwxr-xr-x 1 vince vince 79M Jan 3 14:29 myproject -rw-rw-r-- 1 vince vince 748 Jan 3 13:36 myproject.asd -rw-rw-r-- 1 vince vince 84 Jan 3 14:35 run.lisp Make the binary executable with chmod + myproject and run it:\n$ ./myproject Starting the web server on port 8900 $ uuuuh does that look like working?\n“The server exits right after start up!” The binary was started, it started our web app correctly, but if we are not mistaken it closed it right away O_o What’s going on?\nThis is a difference from running the app from sources or from a binary:\nfrom sources, and by default, when you run sbcl --load myapp.lisp the myapp.lisp file is loaded and executed, and then we are offered a Lisp REPL (unless we specified --non-interactive). from a binary, we are not offered a Lisp REPL. and that’s good, we are in “production mode” now. We then have to make our web server wait forever.\nJust do this, in the main function:\n(defun main () (start-server :port (find-port:find-port :min *port*)) (sleep most-positive-fixnum)) We are sleeping, I forgot, a few million years. The top-level process is sleeping, but our web server is active in its own thread in the background.\nBuild the binary again, run it again… it works :)\n$ ./myproject Starting the web server on port 8900 127.0.0.1 - [2025-01-03 14:47:40] \"GET /product/1 HTTP/1.1\" 200 54 \"http://localhost:8900/?query=on\" \"Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0\" If you have a web server, try sending the binary there and run it. The app will be available to the internet, you can access it with your VPS’s IP + the app port. It is possible to set the address of the Hunchentoot acceptor, and to restrict it to localhost, if you wish.\nOnce your app is deployed, there are a couple ways to interact with it while it is running, even from the comfort of your editor, at home (with a Swank server).\nLet’s try the binary compression.\nCore compression Compression is done with zlib. Compression levels are comprised between -1 and 9. There are small differences in the results.\nLet’s try:\n;; build.lisp (sb-ext:save-lisp-and-die \"myproject\" :executable t :toplevel #'myproject::main :compression 9) The build takes a few more seconds. You see:\n[saving current Lisp image into myproject: writing 0 bytes from the read-only space at 0x50000000 compressed 0 bytes into 8 at level 9 writing 1344 bytes from the static space at 0x50100000 compressed 32768 bytes into 422 at level 9 writing 66191360 bytes from the dynamic space at 0x1000000000 … done myproject now weights 19MB.\nBecause we used templates as strings in our lisp files, our binary is 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!\nClosing words We are only scratching the surface of what we’ll want to do with a real app:\nparse 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.\nWe did a great job for a first app:\nwe 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!", + "content": "We have developped a web app in Common Lisp.\nAt 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 refreshing the browser window.\nInfo We didn’t have to restart any Lisp process, nor any web server.\nThink about it, that’s awesome!\n(and yet, we didn’t talk about the interactive debugger and about unwinding the stack to resume from errors)\nHowever, it is useful to start our application from scratch once in a while:\ndid we list all our dependencies in the .asd project definition? does it work from scratch, do we have any issue with fresh data? We can run our app from sources, and we can build a self-contained binary.\nRun from sources Do you remember what we did at the beginning to load our project?\ncompile and load the .asd project definition quickload our project start the web server. That’s all we need. Let’s write these 3 commands in a new file. At the project root, create a file run.lisp, in which you copy those steps:\n;; run.lisp (load \"myproject.asd\") (ql:quickload \"myproject\") (myproject::start-server) Why the double :: in myproject::start-server? Because we didn’t export the start-server function, and because we didn’t write (in-package :myproject).\nThis works too:\n(in-package :myproject) (start-server) Now run the app from the terminal:\nsbcl --load run.lisp or use rlwrap sbcl (a “readline wrapper”) for convenience, to get REPL history and to support the arrow keys. Yeah the default SBCL REPL is barebones.\n“Address in use” error and interactive debugger You should see this output, but you shouldn’t be worried since the error message is explicit:\n$ rlwrap sbcl --load run.lisp This is SBCL 2.1.5, an implementation of ANSI Common Lisp. More information about SBCL is available at \u003chttp://www.sbcl.org/\u003e. SBCL is free software, provided as is, with absolutely no warranty. It is mostly in the public domain; some portions are provided under BSD-style licenses. See the CREDITS and COPYING files in the distribution for more information. To load \"myproject\": Load 1 ASDF system: myproject ; Loading \"myproject\" .............. Starting the web server on port 8899 While evaluating the form starting at line 5, column 0 of #P\"/home/vince/projets/web-apps-in-lisp/walk/walk-book/content/tutorial/run.lisp\": debugger invoked on a USOCKET:ADDRESS-IN-USE-ERROR in thread #\u003cTHREAD \"main thread\" RUNNING {10015484D3}\u003e: Condition USOCKET:ADDRESS-IN-USE-ERROR was signalled. Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL. restarts (invokable by number or by possibly-abbreviated name): 0: [RETRY ] Retry EVAL of current toplevel form. 1: [CONTINUE] Ignore error and continue loading file \"/home/vince/projets/web-apps-in-lisp/walk/walk-book/content/tutorial/run.lisp\". 2: [ABORT ] Abort loading file \"/home/vince/projets/web-apps-in-lisp/walk/walk-book/content/tutorial/run.lisp\". 3: Ignore runtime option --load \"run.lisp\". 4: Skip rest of --eval and --load options. 5: Skip to toplevel READ/EVAL/PRINT loop. 6: [EXIT ] Exit SBCL (calling #'EXIT, killing the process). (USOCKET:SOCKET-LISTEN #(0 0 0 0) 8899 :REUSEADDRESS T :REUSE-ADDRESS NIL :BACKLOG 50 :ELEMENT-TYPE (UNSIGNED-BYTE 8)) source: (ERROR C) 0] What you got here is the interactive debugger with “restart” actions, however presented in a kludgy form. What we get in a good Lisp IDE is way easier to use ;) To exit, read the available restarts, you have two options: type “2” or “6” in the prompt and press Enter. You now get another prompt:\n6 * This is the top-level Lisp REPL. You can type any Lisp forms in there. You can press C-d or type (quit).\nIf you wanted to avoid the debugger, you could use an SBCL switch: --non-interactive. But wait a bit before we use it together please.\nBack to our error message.\nThe meaningful bit is “Condition USOCKET:ADDRESS-IN-USE-ERROR was signalled”. usocket is the low-level library that does the networking for Hunchentoot. “Address in use” is correct: our app is already running from our editor, already using the default port number. We have to either stop the app there, either use another port.\nTo stop the app, use (hunchentoot:stop *server*) in our editor REPL and from within the project package, since *server* is a variable of ours.\nTo use another port, what would you prefer?\nwe can give the port as an argument on the command line, like --port 9000. we can find the next available port. Let’s do the latter.\nFind a port number This will be quick as we’ll use a library for that, called find-port.\nPlease quickload it and add it to the project definition.\nYou can either:\nrun (ql:quickload \"find-port\") on the REPL and add “find-port” in the .asd, in the “:depends-on” list, add “find-port” in the .asd, re-compile it with C-c C-k or a call to load, and then (ql:quickload \"myproject\"). Info You just loaded a new library without needing to restart the lisp process.\nOur .asd file now looks like:\n(asdf:defsystem \"myproject\" :version \"0.1\" :author \"me\" :license \"WTFPL\" :depends-on ( :hunchentoot ;; web server :easy-routes ;; routes facility :djula ;; HTML templates ;; utils :find-port ;; \u003c------- added ) :components ((:module \"src\" ;; a src/ subdirectory :components ( (:file \"myproject\") ;; = src/myproject.lisp ))) :description \"A list of products\") How does find-port work? On the REPL, write (find-port:find-port and look at your editor’s minibuffer, or tooltip, as it should show you the function signature. find-port takes key arguments, :min and :max.\nLet’s try:\nMYPROJECT\u003e (find-port:find-port :min 8899) 8900 That’s all we need to use.\nWhere should we call it, in the run.lisp file or in myproject.lisp?\nWe’ll create a new function in myproject.lisp. We don’t alter the start-server function which does a good job, we’ll create a new function that is responsible of dealing with the outside world.\nLet’s call this function main:\n;; myproject.lisp ;;; Top-level. (defun main () (start-server :port (find-port:find-port :min *port*))) And now we use it in the run.lisp file:\n(load \"myproject.asd\") (ql:quickload \"myproject\") (in-package :myproject) (main) Run it again:\nsbcl --load run.lisp This is SBCL 2.1.5, an implementation of ANSI Common Lisp. More information about SBCL is available at \u003chttp://www.sbcl.org/\u003e. SBCL is free software, provided as is, with absolutely no warranty. It is mostly in the public domain; some portions are provided under BSD-style licenses. See the CREDITS and COPYING files in the distribution for more information. To load \"myproject\": Load 1 ASDF system: myproject ; Loading \"myproject\" .................................................. [package myproject]. Starting the web server on port 8900 * Now the meaningful message is “Starting the web server on port 8900”.\nYou notice this last * thing? Once again, it’s the top-level Lisp prompt. You can type any Lisp form. You can interact with the running web app.\nVisit http://localhost:8900/?query=one, it works!\nYou can see HTTP logs in the console:\n127.0.0.1 - [2025-01-03 13:38:17] \"GET /?query=one HTTP/1.1\" 200 503 \"-\" \"Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0\" 127.0.0.1 - [2025-01-03 13:38:18] \"GET /favicon.ico HTTP/1.1\" 404 301 \"http://localhost:8900/?query=one\" \"Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0\" Interact with the running web app * is the Lisp prompt. What is the current package?\nType in\n*package* it responds “#\u003cPACKAGE “COMMON-LISP-USER”\u003e”.\nAre you surprised it isn’t our “myproject” package? I am with you. The explanation is that the in-package we wrote in the run.lisp file is for this file. When SBCL starts up, it starts its top-level evaluator/compiler in the cl-user package. It is asked to load our file, which it does, but once it’s done it’s back at the cl-user package.\nTo facilitate our interactions with the running app, we can of course type right now (in-package :myproject), and we can also add an argument to the command line:\nsbcl --load run.lisp --eval \"(in-package :myproject)\" Ask the value of *package* again. OK.\nWhat can we do more? We can inspect some variables and test some functions. Type in (products), it works. You can check the value of *server*. This is actually useful: we are inspecting the internals of our running web app, and we didn’t need to do anything special. Sure, we can add some logging and a monitoring dashboard. But as a second step.\nWhat can we do more? We can also re-define our functions and variables, or develop our app. Look, we can create a new route. Copy this to the REPL:\n* (easy-routes:defroute hello-route (\"/hello\") () \"hello new route\") HELLO-ROUTE and go to http://localhost:8900/hello: you just created a new route while the app was running.\nBut wait, do we really want to develop our app from this limited terminal REPL? No! If you don’t already, it’s time you understand the usefulness of the load function.\nWhat we want is to edit our .lisp source file, instead of copy-pasting stuff in the REPL, and then to reload the app. The reloading is done with\n* (load \"src/myproject.lisp\") Try it!\nYou can also use ql:quickload.\nIn doing so, you are re-discovering a less interactive way of developping.\nBuilding our first binary Running from sources is OK.\nBuilding a binary requires a bit more work but it can bring advantages:\nthe app will start faster, way faster with a binary (binaries start-up in ±2ms) it may be easier to deploy your app to a server because you don’t need to install a lisp implementation nor to set up Quicklisp on the server. it’s easier to ship your app to end users you can save working versions of your app: binary-v0.1, binary-v0.2 etc. 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).\nOther 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.\nSBCL 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 platforms. They are not truly static binaries as they rely on the GLibc. There was an ongoing patch to make them truly static, it isn’t done though.\nAn SBCL binary will weight, by default, around 80MB. With compression, they get to ±20MB. As your application grows, they’ll stay roughly this size. An app of mine, with dozens of dependencies and all the application code, templates and static assets (JS and CSS) is 35MB. LispWorks binaries, reduced in size with their tree shaker, are known to be smaller, a hello world being ±5MB, a web app around 10MB. This tree shaker isn’t in the free version.\nEnough talk, let’s do it. Create a new build.lisp file. We need these steps:\nload our app build a binary where we define an entry point save-lisp-and-die sb-ext:save-lisp-and-die expects these arguments:\na binary name, as a string. a :toplevel key argument, designing the function to run when the binary starts. :executable to set to t if we build an executable, and not a core image. :compression for a compression level (optionnal), and more. We use it like this:\n(sb-ext:save-lisp-and-die \"myproject\" :executable t :toplevel #'myproject::main) This is our build.lisp file:\n(load \"myproject.asd\") (ql:quickload \"myproject\") (sb-ext:save-lisp-and-die \"myproject\" :executable t :toplevel #'myproject::main) Now run it with\nsbcl --load build.lisp You should see:\n$ sbcl --load build.lisp This is SBCL 2.1.5, an implementation of ANSI Common Lisp. More information about SBCL is available at \u003chttp://www.sbcl.org/\u003e. SBCL is free software, provided as is, with absolutely no warranty. It is mostly in the public domain; some portions are provided under BSD-style licenses. See the CREDITS and COPYING files in the distribution for more information. To load \"myproject\": Load 1 ASDF system: myproject ; Loading \"myproject\" ............ [undoing binding stack and other enclosing state... done] [performing final GC... done] [defragmenting immobile space... (fin,inst,fdefn,code,sym)=2118+1401+24925+24417+26979... done] [saving current Lisp image into myproject: writing 0 bytes from the read-only space at 0x50000000 writing 1696 bytes from the static space at 0x50100000 writing 61177856 bytes from the dynamic space at 0x1000000000 writing 2383872 bytes from the immobile space at 0x50200000 writing 16257024 bytes from the immobile space at 0x52a00000 done] it’s done. Run ls on the project root:\n$ ls -lh total 80316 drwxrwxr-x 2 vince vince 4.0K Jan 3 14:35 src -rw-rw-r-- 1 vince vince 133 Jan 3 14:35 build.lisp -rwxr-xr-x 1 vince vince 79M Jan 3 14:29 myproject -rw-rw-r-- 1 vince vince 748 Jan 3 13:36 myproject.asd -rw-rw-r-- 1 vince vince 84 Jan 3 14:35 run.lisp Make the binary executable with chmod + myproject and run it:\n$ ./myproject Starting the web server on port 8900 $ uuuuh does that look like working?\n“The server exits right after start up!” The binary was started, it started our web app correctly, but if we are not mistaken it closed it right away O_o What’s going on?\nThis is a difference from running the app from sources or from a binary:\nfrom sources, and by default, when you run sbcl --load myapp.lisp the myapp.lisp file is loaded and executed, and then we are offered a Lisp REPL (unless we specified --non-interactive). from a binary, we are not offered a Lisp REPL. and that’s good, we are in “production mode” now. We then have to make our web server wait forever.\nJust do this, in the main function:\n(defun main () (start-server :port (find-port:find-port :min *port*)) (sleep most-positive-fixnum)) We are sleeping, I forgot, a few million years. The top-level process is sleeping, but our web server is active in its own thread in the background.\nBuild the binary again, run it again… it works :)\n$ ./myproject Starting the web server on port 8900 127.0.0.1 - [2025-01-03 14:47:40] \"GET /product/1 HTTP/1.1\" 200 54 \"http://localhost:8900/?query=on\" \"Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0\" If you have a web server, try sending the binary there and run it. The app will be available to the internet, you can access it with your VPS’s IP + the app port. It is possible to set the address of the Hunchentoot acceptor, and to restrict it to localhost, if you wish.\nOnce your app is deployed, there are a couple ways to interact with it while it is running, even from the comfort of your editor, at home (with a Swank server).\nLet’s try the binary compression.\nCore compression Compression is done with zlib. Compression levels are comprised between -1 and 9. There are small differences in the results.\nLet’s try:\n;; build.lisp (sb-ext:save-lisp-and-die \"myproject\" :executable t :toplevel #'myproject::main :compression 9) The build takes a few more seconds. You see:\n[saving current Lisp image into myproject: writing 0 bytes from the read-only space at 0x50000000 compressed 0 bytes into 8 at level 9 writing 1344 bytes from the static space at 0x50100000 compressed 32768 bytes into 422 at level 9 writing 66191360 bytes from the dynamic space at 0x1000000000 … done myproject now weights 19MB.\nBecause we used templates as strings in our lisp files, our binary is 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!\nClosing words We are only scratching the surface of what we’ll want to do with a real app:\nparse 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.\nWe did a great job for a first app:\nwe 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!", "description": "We have developped a web app in Common Lisp.\nAt 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 refreshing the browser window.\nInfo We didn’t have to restart any Lisp process, nor any web server.", "tags": [], "title": "the first build", @@ -233,8 +249,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 23159b5..920720b 100644 --- a/docs/see-also/index.html +++ b/docs/see-also/index.html @@ -1,23 +1,24 @@ See also :: Web Apps in Lisp: Know-how -

See also

Other tutorials:

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

Libraries:

\ No newline at end of file + 
\ No newline at end of file diff --git a/docs/see-also/index.xml b/docs/see-also/index.xml index 15e201b..08a0753 100644 --- a/docs/see-also/index.xml +++ b/docs/see-also/index.xml @@ -1,3 +1,4 @@ See also :: Web Apps in Lisp: Know-howhttp://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:Hugoen-us \ 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-clHugoen-us \ No newline at end of file diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 13d9c24..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/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 834f48d..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 68c1a00..b8ba969 100644 --- a/docs/tutorial/build.lisp +++ b/docs/tutorial/build.lisp @@ -2,7 +2,14 @@ (ql:quickload "myproject") -(sb-ext:save-lisp-and-die "myproject" - :executable t - :toplevel #'myproject::main - :compression 9) +(setf uiop:*image-entry-point* #'myproject::main) + +(uiop:dump-image "myproject" :executable t :compression 9) + +#| +;; The same as: + +(sb-ext:save-lisp-and-die "myproject" :executable t :compression 9 + :toplevel #'myproject::main) + +|# diff --git a/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 65cf610..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 e508d41..ca02c7b 100644 --- a/docs/tutorial/first-build/index.html +++ b/docs/tutorial/first-build/index.html @@ -6,8 +6,8 @@ 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 refreshing the browser window. 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, +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, 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 @@ -146,9 +146,12 @@

Try it!

You can also use ql:quickload.

In doing so, you are re-discovering a less interactive way of developping.

Building our first binary

Running from sources is OK.

Building a binary requires a bit more work but it can bring advantages:

  • the app will start faster, way faster with a binary (binaries start-up in ±2ms)
  • it may be easier to deploy your app to a server
    • because you don’t need to install a lisp implementation nor to set up Quicklisp on the server.
  • it’s easier to ship your app to end users
  • you can save working versions of your app: binary-v0.1, binary-v0.2 etc.

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.

SBCL binaries are portable from and to the same operating system: +is available by default).

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 platforms. They are not truly static binaries as they rely on the GLibc. There was an ongoing patch to make them truly static, it isn’t @@ -221,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 4efcf99..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 ef745ed..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 15c0718..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 11399da..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 e6304f1..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 a155933..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 c5e2691..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/myproject b/docs/tutorial/myproject index d2a32d6..3b23715 100644 Binary files a/docs/tutorial/myproject and b/docs/tutorial/myproject differ 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"); diff --git a/docs/tutorial/templates/base/index.html b/docs/tutorial/templates/base/index.html new file mode 100644 index 0000000..8b88d3b --- /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