├── .gitignore ├── .gitattributes ├── .github └── FUNDING.yml ├── search.png ├── welcome.png ├── Makefile ├── src ├── templates │ ├── search-form.html │ ├── welcome.html │ ├── products.html │ └── base.html ├── web.lisp └── myproject.lisp ├── run.lisp ├── myproject.asd └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.fasl 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | 2 | *.html linguist-language=lisp -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [vindarel,] 2 | ko_fi: vindarel 3 | liberapay: vindarel 4 | -------------------------------------------------------------------------------- /search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vindarel/lisp-web-template-productlist/HEAD/search.png -------------------------------------------------------------------------------- /welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vindarel/lisp-web-template-productlist/HEAD/welcome.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Build a binary. 3 | build: 4 | sbcl --load myproject.asd \ 5 | --eval '(ql:quickload :myproject)' \ 6 | --eval '(asdf:make :myproject)' \ 7 | --eval '(quit)' 8 | -------------------------------------------------------------------------------- /src/templates/search-form.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 5 |

6 |
7 |
8 | -------------------------------------------------------------------------------- /src/templates/welcome.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 | 7 |
8 |
9 |
10 |

11 | Welcome on our awesome website. 12 |

13 |
14 | {% include "search-form.html" %} 15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /run.lisp: -------------------------------------------------------------------------------- 1 | " 2 | Usage: 3 | 4 | PORT=9999 sbcl --load run.lisp 5 | 6 | This loads the project's asd, loads the quicklisp dependencies, and 7 | starts the web server. 8 | 9 | Then, we are given the lisp prompt: we can interact with the running application. 10 | 11 | Another way to run the app is to build and run the executable (see README). 12 | " 13 | 14 | (load "myproject.asd") 15 | 16 | (ql:quickload "myproject") 17 | 18 | (in-package :myproject) 19 | (handler-case 20 | (myproject:start :port (parse-integer (uiop:getenv "PORT"))) 21 | (error (c) 22 | (format *error-output* "~&An error occured: ~a~&" c) 23 | (uiop:quit 1))) 24 | -------------------------------------------------------------------------------- /myproject.asd: -------------------------------------------------------------------------------- 1 | (asdf:defsystem "myproject" 2 | :version "0.1" 3 | :author "" 4 | :license "WTFPL" 5 | :depends-on (:str 6 | :cl-slug 7 | :local-time 8 | :cl-ppcre 9 | :hunchentoot 10 | :easy-routes 11 | :djula 12 | :log4cl) 13 | :components ((:module "src" 14 | :components 15 | ((:file "myproject") 16 | (:file "web")))) 17 | 18 | ;; To build a binary: 19 | :build-operation "program-op" 20 | :build-pathname "myproject" 21 | :entry-point "myproject::main" 22 | 23 | :description "A web template" 24 | ;; :long-description 25 | ;; #.(read-file-string 26 | ;; (subpathname *load-pathname* "README.md")) 27 | :in-order-to ((test-op (test-op "myproject-test")))) 28 | 29 | ;; Smaller binary. 30 | #+sb-core-compression 31 | (defmethod asdf:perform ((o asdf:image-op) (c asdf:system)) 32 | (uiop:dump-image (asdf:output-file o c) :executable t :compression t)) 33 | -------------------------------------------------------------------------------- /src/templates/products.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 | {% include "search-form.html" %} 8 |
9 | 10 |
11 | 12 | {% if no-results %} 13 |
No results !
14 | {% endif %} 15 | 16 | {% for product in products %} 17 |
18 |
19 |
20 |
21 | Cover of {{ product.title }} 22 |
23 |
24 |
25 |
26 |
27 |

{{ product.title }}

28 |

29 | 30 | {{ product.category }} 31 | 32 |

33 | 34 | {{ product.price | price }} € 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 |
44 | {% endfor %} 45 |
46 |
47 |
48 | 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /src/web.lisp: -------------------------------------------------------------------------------- 1 | (in-package :myproject) 2 | 3 | (defvar *server* nil 4 | "Server instance (Hunchentoot acceptor).") 5 | 6 | (defparameter *port* 8899 7 | "We can override it in the config file or from an environment variable.") 8 | 9 | ;; 10 | ;; Templates. 11 | ;; 12 | (djula:add-template-directory 13 | (asdf:system-relative-pathname "myproject" "src/templates/")) 14 | (defparameter +base.html+ (djula:compile-template* "base.html")) 15 | (defparameter +welcome.html+ (djula:compile-template* "welcome.html")) 16 | (defparameter +products.html+ (djula:compile-template* "products.html")) 17 | 18 | ;; 19 | ;; Routes. 20 | ;; 21 | (easy-routes:defroute root ("/" :method :get) () 22 | (djula:render-template* +welcome.html+ nil 23 | :title "My great website")) 24 | 25 | (easy-routes:defroute search-route ("/search" :method :get) (q) 26 | (let* ((products (search-products *products* 27 | (slug:asciify (str:downcase q))))) 28 | (djula:render-template* +products.html+ nil 29 | :title (format nil "My website - ~a" q) 30 | :query q 31 | :products products 32 | :no-results (zerop (length products))))) 33 | 34 | 35 | ;; 36 | ;; Start the web server. 37 | ;; 38 | (defun start-server (&key (port *port*)) 39 | (format t "~&Starting the web server on port ~a" port) 40 | (force-output) 41 | (setf *server* (make-instance 'easy-routes:easy-routes-acceptor 42 | :port (or port *port*))) 43 | (hunchentoot:start *server*)) 44 | 45 | (export 'start) 46 | (defun start (&key (port *port*) (load-init t)) 47 | (if load-init 48 | (progn 49 | (format t "Loading init file...~&") 50 | (load-init)) 51 | (format t "Skipping init file.~&")) 52 | (force-output) 53 | 54 | (format t "~&Loading the products...") 55 | (force-output) 56 | (get-products) 57 | (format t "~&Done.~&") 58 | (force-output) 59 | 60 | (start-server :port (or port *port*)) 61 | (format t "~&Ready. You can access the application!~&") 62 | (force-output)) 63 | -------------------------------------------------------------------------------- /src/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %} {{ title }} {% endblock %} 9 | 10 | 11 | 12 |
13 | 40 |
41 | 42 | 43 | {% block content %} {% endblock %} 44 | 45 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A website template for Common Lisp 2 | 3 | Start building an app quickly. 4 | 5 | * Hunchentoot, [Easy-routes](https://lispcookbook.github.io/cl-cookbook/web.html) 6 | * [Djula](https://mmontone.github.io/djula/) templates 7 | * a welcome screen 8 | * a form to search products ("product 1"…) 9 | * a list of products 10 | * run as a script or build a binary. 11 | 12 | Uses [Bulma CSS](https://bulma.io) and used [Bulma templates examples](https://bulmatemplates.github.io/bulma-templates/). 13 | 14 | See also our [cl-cookieweb](https://github.com/vindarel/cl-cookieweb) project generator. 15 | 16 | 17 | Welcome screen: 18 | 19 | ![](welcome.png) 20 | 21 | Searching products: 22 | 23 | ![welcome screen](search.png) 24 | 25 | 26 | ## How to run the app 27 | 28 | There are two possibilities to run the app from the command line: 29 | 30 | rlwrap sbcl --load run.lisp 31 | 32 | In that case, we are dropped into the Lisp REPL, so we can interact 33 | with the running application. It is specially useful to reload 34 | settings (contact information,…). 35 | 36 | Or build the binary and run it: 37 | 38 | make build 39 | ./myproject 40 | 41 | Set the port: 42 | 43 | PORT=9999 rlwrap sbcl --load run.lisp 44 | 45 | ### Config file 46 | 47 | You can use a starter configuration file: 48 | 49 | touch ~/.myproject.lisp 50 | 51 | In there, you can do anything in Lisp. You probably want to write things after `(in-package myproject)`, but that is not mandatory. For example, you can overwrite the `get-products` function. 52 | 53 | The file will be `load`'ed at startup, in the context of the `myproject` package. 54 | 55 | 56 | ## Develop 57 | 58 | Load `myproject.asd` (`C-c C-k` in Slime), `(ql:quickload :myproject)` and then `(start)`. 59 | 60 | 61 | TODO: 62 | 63 | * [X] load static files => use the regular `(hunchentoot:create-folder-dispatcher-and-handler #p"/path/to/static/")`. 64 | * anything more useful for a web app 65 | 66 | See also: 67 | 68 | * the [real world app](https://github.com/vindarel/abstock) from where I extracted this template (it reads a DB, it has a shopping basket and a validation form that sends an email to the owner). 69 | * my preliminary notes on live-reloading a lisp web app: https://github.com/vindarel/lisp-web-live-reload-example 70 | * how to connect to a remote running image with a Swank server: https://lispcookbook.github.io/cl-cookbook/debugging.html#remote-debugging 71 | * https://lispcookbook.github.io/cl-cookbook/web.html 72 | * https://github.com/CodyReichert/awesome-cl#web-frameworks 73 | 74 | 75 | ## Licence 76 | 77 | WTFPL 78 | -------------------------------------------------------------------------------- /src/myproject.lisp: -------------------------------------------------------------------------------- 1 | ;; Product objects must have: 2 | ;; - id 3 | ;; - title 4 | ;; - author 5 | ;; - price 6 | ;; - cover image url 7 | ;; - shelf 8 | ;; 9 | 10 | (defpackage myproject 11 | (:use :cl 12 | :log4cl)) 13 | 14 | (in-package :myproject) 15 | 16 | (defparameter *config-file* "~/.myproject.lisp" 17 | "lispy configuration file. Loaded with `load-init'.") 18 | (defparameter *version* "0.1") 19 | (defparameter *verbose* nil) 20 | 21 | (defvar *products* nil 22 | "List of all products.") 23 | 24 | 25 | (defun load-init () 26 | "Read the configuration variables (contact information,…) from the `*config-file*'." 27 | (let ((file (uiop:native-namestring *config-file*))) 28 | (if (uiop:file-exists-p file) 29 | (let ((*package* *package*)) 30 | (in-package myproject) 31 | (load file)) 32 | (format t "Config file ~a not found.~&" file)))) 33 | 34 | ;; 35 | ;; Get products. 36 | ;; 37 | (defun get-products () 38 | "Get all products from somewhere." 39 | (setf *products* 40 | (loop for i upto 10 41 | collect (list 42 | :|id| i 43 | :|title| (format nil "Product ~a" i) 44 | :|price| 19.99 45 | :|category| "category" 46 | :|cover| "https://via.placeholder.com/150"))) 47 | *products*) 48 | 49 | 50 | ;; 51 | ;; Search products. 52 | ;; 53 | (defun search-products (products query) 54 | "`products': plist, 55 | `query': string." 56 | ;XXX: type declarations and type checking. 57 | (let* (;; Filter by title and author(s). 58 | (result (if (not (str:blank? query)) 59 | ;; no-case: strips internal contiguous whitespace, removes accents 60 | ;; and punctuation. 61 | (let* ((query (slug:asciify query)) 62 | (query (str:replace-all " " ".*" query))) 63 | ;; Here a DB lookup. 64 | (loop for product in products 65 | for repr = (str:downcase (getf product :|title|)) 66 | when (ppcre:scan query repr) 67 | collect product)) 68 | products))) 69 | (log:info "Searched '~a' and found ~a results.~&" query (length result)) 70 | (values result 71 | (length result)))) 72 | 73 | (defun main () 74 | "Entry point of the executable." 75 | (handler-case 76 | (progn 77 | (start) 78 | ;; Put the webserver thread on the foreground 79 | ;; (otherwise the app shuts down immediately). 80 | (bt:join-thread 81 | (find-if (lambda (th) 82 | (search "hunchentoot" (bt:thread-name th))) 83 | (bt:all-threads)))) 84 | (sb-sys:interactive-interrupt () (progn 85 | (format *error-output* "User abort. Bye!~&") 86 | (uiop:quit))) 87 | (error (c) (format *error-output* "~&An error occured: ~A~&" c)))) 88 | --------------------------------------------------------------------------------