{"id":13619956,"url":"https://github.com/fukamachi/caveman","last_synced_at":"2025-04-05T20:17:07.133Z","repository":{"id":2011123,"uuid":"1414042","full_name":"fukamachi/caveman","owner":"fukamachi","description":"Lightweight web application framework for Common Lisp.","archived":false,"fork":false,"pushed_at":"2024-05-26T14:19:07.000Z","size":968,"stargazers_count":785,"open_issues_count":47,"forks_count":62,"subscribers_count":66,"default_branch":"master","last_synced_at":"2025-02-18T20:58:28.800Z","etag":null,"topics":["common-lisp","framework","web"],"latest_commit_sha":null,"homepage":"http://8arrow.org/caveman/","language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fukamachi.png","metadata":{"files":{"readme":"README.markdown","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2011-02-26T07:50:03.000Z","updated_at":"2025-02-17T07:17:56.000Z","dependencies_parsed_at":"2022-08-06T11:16:47.382Z","dependency_job_id":null,"html_url":"https://github.com/fukamachi/caveman","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fukamachi%2Fcaveman","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fukamachi%2Fcaveman/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fukamachi%2Fcaveman/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fukamachi%2Fcaveman/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fukamachi","download_url":"https://codeload.github.com/fukamachi/caveman/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247393569,"owners_count":20931813,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["common-lisp","framework","web"],"created_at":"2024-08-01T21:00:50.511Z","updated_at":"2025-04-05T20:17:07.114Z","avatar_url":"https://github.com/fukamachi.png","language":"Common Lisp","funding_links":[],"categories":["Common Lisp","Interfaces to other package managers"],"sub_categories":["Clack plugins"],"readme":"# Caveman2 - Lightweight web application framework\n\n[![Build Status](https://travis-ci.org/fukamachi/caveman.svg?branch=master)](https://travis-ci.org/fukamachi/caveman)\n\n## Usage\n\n```common-lisp\n(defparameter *web* (make-instance '\u003capp\u003e))\n\n@route GET \"/\"\n(defun index ()\n  (render #P\"index.tmpl\"))\n\n@route GET \"/hello\"\n(defun say-hello (\u0026key (|name| \"Guest\"))\n  (format nil \"Hello, ~A\" |name|))\n```\n\n## About Caveman2\n\n### What's different from Caveman \"1\"?\n\nEverything. Caveman2 was written from scratch.\n\nThese are noteworthy points.\n\n* Is based on [ningle](http://8arrow.org/ningle/)\n* Has database integration\n* Uses new, separate configuration system ([Envy](https://github.com/fukamachi/envy))\n* Has new routing macro\n\n### The reason I wrote it from scratch:\n\nOne of the most frequently asked questions was \"Which should I use: ningle or Caveman? What are the differences?\" I think these were asked so frequently because Caveman and ningle were too similar. Both of them are called \"micro\", and had no database support.\n\nWith Caveman2, Caveman is no longer a \"micro\" web application framework. It supports CL-DBI, and has database connection management by default. Caveman has started growing up.\n\n## Design Goal\n\nCaveman is intended to be a collection of common parts of web applications. With Caveman2, I use three rules to make decisions:\n\n* Be extensible.\n* Be practical.\n* Don't force anything.\n\n## Quickstart\n\nYou came here because you're interested in living like a caveman, right? This isn't Disneyland, but we can start here. Let's get into a cave!\n\n### Installation\n\nCaveman2 is now available on [Quicklisp](https://www.quicklisp.org/beta/).\n\n```common-lisp\n(ql:quickload :caveman2)\n```\n\n### Generating a project skeleton\n\n```common-lisp\n(caveman2:make-project #P\"/path/to/myapp/\"\n                       :author \"\u003cYour full name\u003e\")\n;-\u003e writing /path/to/myapp/.gitignore\n;   writing /path/to/myapp/README.markdown\n;   writing /path/to/myapp/app.lisp\n;   writing /path/to/myapp/db/schema.sql\n;   writing /path/to/myapp/shlyfile.lisp\n;   writing /path/to/myapp/myapp-test.asd\n;   writing /path/to/myapp/myapp.asd\n;   writing /path/to/myapp/src/config.lisp\n;   writing /path/to/myapp/src/db.lisp\n;   writing /path/to/myapp/src/main.lisp\n;   writing /path/to/myapp/src/view.lisp\n;   writing /path/to/myapp/src/web.lisp\n;   writing /path/to/myapp/static/css/main.css\n;   writing /path/to/myapp/t/myapp.lisp\n;   writing /path/to/myapp/templates/_errors/404.html\n;   writing /path/to/myapp/templates/index.tmpl\n;   writing /path/to/myapp/templates/layout/default.tmpl\n```\n\n### Start a server\nThis is an example that assumes that the name of your application is \"myapp\".\nBefore starting the server, you must first load your app.\n\n```common-lisp\n(ql:quickload :myapp)\n```\n\nYour application has functions named `start` and `stop` to start/stop your web application. \n\n```common-lisp\n(myapp:start :port 8080)\n```\n\nAs Caveman is based on Clack/Lack, you can choose which server to run on -- Hunchentoot, Woo or Wookie, etc.\n\n```common-lisp\n(myapp:start :server :hunchentoot :port 8080)\n(myapp:start :server :fcgi :port 8080)\n```\n\nI recommend you use Hunchentoot on a local machine, and use Woo in a production environment.\n\nYou can also start your application by using [clackup command](https://github.com/fukamachi/clack/blob/master/roswell/clackup.ros).\n\n    $ ros install clack\n    $ which clackup\n    /Users/nitro_idiot/.roswell/bin/clackup\n\n    $ APP_ENV=development clackup --server :fcgi --port 8080 app.lisp\n\n### Routing\n\nCaveman2 provides 2 ways to define a route -- `@route` and `defroute`. You can use either.\n\n`@route` is an annotation macro, defined by using [cl-annot](https://github.com/arielnetworks/cl-annot). It takes a method, a URL-string, and a function.\n\n```common-lisp\n@route GET \"/\"\n(defun index ()\n  ...)\n\n;; A route with no name.\n@route GET \"/welcome\"\n(lambda (\u0026key (|name| \"Guest\"))\n  (format nil \"Welcome, ~A\" |name|))\n```\n\nThis is similar to Caveman1's `@url` except for its argument list. You don't have to specify an argument when it is not required.\n\n`defroute` is just a macro. It provides the same functionality as `@route`.\n\n```common-lisp\n(defroute index \"/\" ()\n  ...)\n\n;; A route with no name.\n(defroute \"/welcome\" (\u0026key (|name| \"Guest\"))\n  (format nil \"Welcome, ~A\" |name|))\n```\n\nSince Caveman bases on ningle, Caveman also has the [Sinatra](http://www.sinatrarb.com/)-like routing system.\n\n```common-lisp\n;; GET request (default)\n@route GET \"/\" (lambda () ...)\n(defroute (\"/\" :method :GET) () ...)\n\n;; POST request\n@route POST \"/\" (lambda () ...)\n(defroute (\"/\" :method :POST) () ...)\n\n;; PUT request\n@route PUT \"/\" (lambda () ...)\n(defroute (\"/\" :method :PUT) () ...)\n\n;; DELETE request\n@route DELETE \"/\" (lambda () ...)\n(defroute (\"/\" :method :DELETE) () ...)\n\n;; OPTIONS request\n@route OPTIONS \"/\" (lambda () ...)\n(defroute (\"/\" :method :OPTIONS) () ...)\n\n;; For all methods\n@route ANY \"/\" (lambda () ...)\n(defroute (\"/\" :method :ANY) () ...)\n```\n\nRoute patterns may contain \"keywords\" to put the value into the argument.\n\n```common-lisp\n(defroute \"/hello/:name\" (\u0026key name)\n  (format nil \"Hello, ~A\" name))\n```\n\nThe above controller will be invoked when you access \"/hello/Eitaro\" or \"/hello/Tomohiro\", and `name` will be \"Eitaro\" or \"Tomohiro\", as appropriate.\n\n`(\u0026key name)` is almost same as a lambda list of Common Lisp, except it always allows other keys.\n\n```common-lisp\n(defroute \"/hello/:name\" (\u0026rest params \u0026key name)\n  ;; ...\n  )\n```\n\nRoute patterns may also contain \"wildcard\" parameters. They are accessible by using `splat`.\n\n```common-lisp\n(defroute \"/say/*/to/*\" (\u0026key splat)\n  ; matches /say/hello/to/world\n  (format nil \"~A\" splat))\n;=\u003e (hello world)\n\n(defroute \"/download/*.*\" (\u0026key splat)\n  ; matches /download/path/to/file.xml\n  (format nil \"~A\" splat)) \n;=\u003e (path/to/file xml)\n```\n\nIf you'd like to write use a regular expression in a URL rule, `:regexp t` should work.\n\n```common-lisp\n(defroute (\"/hello/([\\\\w]+)\" :regexp t) (\u0026key captures)\n  (format nil \"Hello, ~A!\" (first captures)))\n```\n\nNormally, routes are tested for a match in the order they are defined, and only the first route matched is invoked, with the following routes being ignored. However, a route can continue testing for matches in the list, by including `next-route`.\n\n```common-lisp\n(defroute \"/guess/:who\" (\u0026key who)\n  (if (string= who \"Eitaro\")\n      \"You got me!\"\n      (next-route)))\n\n(defroute \"/guess/*\" ()\n  \"You missed!\")\n```\n\nYou can return following formats as the result of `defroute`.\n\n* String\n* Pathname\n* Clack's response list (containing Status, Headers and Body)\n\n### Redirection\n\nRedirect to another route with`(redirect \"url\")`. A second optional argument is the status code, 302 by default.\n\n### Reverse URLs\n\nWhen you defined routes with names, you can find the URL from a name with `(url-for route-name \u0026rest params)`.\n\nThe function will throw an error if no route is found.\n\n### More helper functions\n\nSee also:\n\n- `add-query-parameters base-url params`\n\n\n### Structured query/post parameters\n\nParameter keys containing square brackets (\"[\" \u0026 \"]\") will be parsed as structured parameters. You can access the parsed parameters as `_parsed` in routers.\n\n```html\n\u003cform action=\"/edit\"\u003e\n  \u003cinput type=\"name\" name=\"person[name]\" /\u003e\n  \u003cinput type=\"name\" name=\"person[email]\" /\u003e\n  \u003cinput type=\"name\" name=\"person[birth][year]\" /\u003e\n  \u003cinput type=\"name\" name=\"person[birth][month]\" /\u003e\n  \u003cinput type=\"name\" name=\"person[birth][day]\" /\u003e\n\u003c/form\u003e\n```\n\n```common-lisp\n(defroute \"/edit\" (\u0026key _parsed)\n  (format nil \"~S\" (cdr (assoc \"person\" _parsed :test #'string=))))\n;=\u003e \"((\\\"name\\\" . \\\"Eitaro\\\") (\\\"email\\\" . \\\"e.arrows@gmail.com\\\") (\\\"birth\\\" . ((\\\"year\\\" . 2000) (\\\"month\\\" . 1) (\\\"day\\\" . 1))))\"\n\n;; With assoc-utils\n(ql:quickload :assoc-utils)\n(import 'assoc-utils:aget)\n(defroute \"/edit\" (\u0026key _parsed)\n  (format nil \"~S\" (aget _parsed \"person\")))\n```\n\nBlank keys mean they have multiple values.\n\n```html\n\u003cform action=\"/add\"\u003e\n  \u003cinput type=\"text\" name=\"items[][name]\" /\u003e\n  \u003cinput type=\"text\" name=\"items[][price]\" /\u003e\n\n  \u003cinput type=\"text\" name=\"items[][name]\" /\u003e\n  \u003cinput type=\"text\" name=\"items[][price]\" /\u003e\n\n  \u003cinput type=\"submit\" value=\"Add\" /\u003e\n\u003c/form\u003e\n```\n\n```common-lisp\n(defroute \"/add\" (\u0026key _parsed)\n  (format nil \"~S\" (assoc \"items\" _parsed :test #'string=)))\n;=\u003e \"(((\\\"name\\\" . \\\"WiiU\\\") (\\\"price\\\" . \\\"30000\\\")) ((\\\"name\\\" . \\\"PS4\\\") (\\\"price\\\" . \\\"69000\\\")))\"\n```\n\n### Templates\n\nCaveman uses [Djula](http://mmontone.github.io/djula/djula/) as its default templating engine.\n\n```html\n{% extends \"layouts/default.html\" %}\n{% block title %}Users | MyApp{% endblock %}\n{% block content %}\n\u003cdiv id=\"main\"\u003e\n  \u003cul\u003e\n  {% for user in users %}\n    \u003cli\u003e\u003ca href=\"{{ user.url }}\"\u003e{{ user.name }}\u003c/a\u003e\u003c/li\u003e\n  {% endfor %}\n  \u003c/ul\u003e\n\u003c/div\u003e\n{% endblock %}\n```\n\n```common-lisp\n(import 'myapp.view:render)\n\n(render #P\"users.html\"\n        '(:users ((:url \"/id/1\"\n                   :name \"nitro_idiot\")\n                  (:url \"/id/2\"\n                   :name \"meymao\"))\n          :has-next-page T))\n```\n\nIf you want to get something from a database or execute a function using [Djula](http://mmontone.github.io/djula/) you must explicity call `list` when passing the arguments to render so that the code executes.\n\n```common-lisp\n(import 'myapp.view:render)\n\n(render #P\"users.html\"\n        (list :users (get-users-from-db)))\n```\n\n### JSON API\n\nThis is an example of a JSON API.\n\n```common-lisp\n(defroute \"/user.json\" (\u0026key |id|)\n  (let ((person (find-person-from-db |id|)))\n    ;; person =\u003e (:|name| \"Eitaro Fukamachi\" :|email| \"e.arrows@gmail.com\")\n    (render-json person)))\n\n;=\u003e {\"name\":\"Eitaro Fukamachi\",\"email\":\"e.arrows@gmail.com\"}\n```\n\n`render-json` is a part of a skeleton project. You can find its code in \"src/view.lisp\".\n\n### Static file\n\nImages, CSS, JS, favicon.ico and robot.txt in \"static/\" directory will be served by default.\n\n```\n/images/logo.png =\u003e {PROJECT_ROOT}/static/images/logo.png\n/css/main.css    =\u003e {PROJECT_ROOT}/static/css/main.css\n/js/app/index.js =\u003e {PROJECT_ROOT}/static/js/app/index.js\n/robot.txt       =\u003e {PROJECT_ROOT}/static/robot.txt\n/favicon.ico     =\u003e {PROJECT_ROOT}/static/favicon.ico\n```\n\nYou can change these rules by rewriting \"PROJECT_ROOT/app.lisp\". See [Clack.Middleware.Static](http://quickdocs.org/clack/api#package-CLACK.MIDDLEWARE.STATIC) for detail.\n\n### Configuration\n\nCaveman adopts [Envy](https://github.com/fukamachi/envy) as a configuration switcher. This allows definition of  multiple configurations and switching between them according to an environment variable.\n\nThis is a typical example:\n\n```common-lisp\n(defpackage :myapp.config\n  (:use :cl\n        :envy))\n(in-package :myapp.config)\n\n(setf (config-env-var) \"APP_ENV\")\n\n(defconfig :common\n  `(:application-root ,(asdf:component-pathname (asdf:find-system :myapp))))\n\n(defconfig |development|\n  `(:debug T\n    :databases\n    ((:maindb :sqlite3 :database-name ,(merge-pathnames #P\"test.db\"\n                                                        *application-root*)))))\n\n(defconfig |production|\n  '(:databases\n    ((:maindb :mysql :database-name \"myapp\" :username \"whoami\" :password \"1234\")\n     (:workerdb :mysql :database-name \"jobs\" :username \"whoami\" :password \"1234\"))))\n\n(defconfig |staging|\n  `(:debug T\n    ,@|production|))\n```\n\nEvery configuration is a property list. You can choose the configuration which to use by setting `APP_ENV`.\n\nTo get a value from the current configuration, call `myapp.config:config` with the key you want.\n\n```common-lisp\n(import 'myapp.config:config)\n\n(setf (osicat:environment-variable \"APP_ENV\") \"development\")\n(config :debug)\n;=\u003e T\n```\n\n### Database\n\nWhen you add `:databases` to the configuration, Caveman enables database support. `:databases` is an association list of database settings.\n\n```common-lisp\n(defconfig |production|\n  '(:databases\n    ((:maindb :mysql :database-name \"myapp\" :username \"whoami\" :password \"1234\")\n     (:workerdb :mysql :database-name \"jobs\" :username \"whoami\" :password \"1234\"))))\n```\n\n`db` in a package `myapp.db` is a function for connecting to each databases configured the above. Here is an example.\n\n```common-lisp\n(use-package '(:myapp.db :sxql :datafly))\n\n(defun search-adults ()\n  (with-connection (db)\n    (retrieve-all\n      (select :*\n        (from :person)\n        (where (:\u003e= :age 20))))))\n```\n\nThe connection is alive during the Lisp session, and will be reused in every HTTP request.\n\n`retrieve-all` and the query language came from [datafly](https://github.com/fukamachi/datafly) and [SxQL](https://github.com/fukamachi/sxql). See those sets of documentation for more information.\n\n### Set HTTP headers or HTTP status\n\nThere are several special variables available during a HTTP request. `*request*` and `*response*` represent a request and a response. If you are familiar with [Clack](http://clacklisp.org/), these are instances of subclasses of [Clack.Request](http://quickdocs.org/clack/api#package-CLACK.REQUEST) and [Clack.Response](http://quickdocs.org/clack/api#package-CLACK.RESPONSE).\n\n```common-lisp\n(use-package :caveman2)\n\n;; Get a value of Referer header.\n(http-referer *request*)\n\n;; Set Content-Type header.\n(setf (getf (response-headers *response*) :content-type) \"application/json\")\n\n;; Set HTTP status.\n(setf (status *response*) 304)\n```\n\nIf you would like to set Content-Type \"application/json\" for all \"*.json\" requests, `next-route` can be used.\n\n```common-lisp\n(defroute \"/*.json\" ()\n  (setf (getf (response-headers *response*) :content-type) \"application/json\")\n  (next-route))\n\n(defroute \"/user.json\" () ...)\n(defroute \"/search.json\" () ...)\n(defroute (\"/new.json\" :method :POST) () ...)\n```\n\n### Using session\n\nSession data is for memorizing user-specific data. `*session*` is a hash table that stores session data.\n\nThis example increments `:counter` in the session, and displays it for each visitor.\n\n```common-lisp\n(defroute \"/counter\" ()\n  (format nil \"You came here ~A times.\"\n          (incf (gethash :counter *session* 0))))\n```\n\nCaveman2 stores session data in-memory by default. To change this, specify `:store` to `:session` in \"PROJECT_ROOT/app.lisp\".\n\nThis example uses RDBMS to store session data.\n\n```diff\n      '(:backtrace\n        :output (getf (config) :error-log))\n      nil)\n- :session\n+ (:session\n+  :store (make-dbi-store :connector (lambda ()\n+                                      (apply #'dbi:connect\n+                                             (myapp.db:connection-settings)))))\n  (if (productionp)\n      nil\n      (lambda (app)\n```\n\nNOTE: Don't forget to add `:lack-session-store-dbi` as `:depends-on` of your app. It is not a part of Clack/Lack.\n\nSee the source code of Lack.Session.Store.DBi for more information.\n\n- [Lack.Session.Store.Dbi](https://github.com/fukamachi/lack/blob/master/src/middleware/session/store/dbi.lisp)\n\n### Throw an HTTP status code\n\n```common-lisp\n(import 'caveman2:throw-code)\n\n(defroute (\"/auth\" :method :POST) (\u0026key |name| |password|)\n  (unless (authorize |name| |password|)\n    (throw-code 403)))\n```\n\n### Specify error pages\n\nTo specify error pages for 404, 500 or such, define a method `on-exception` of your app.\n\n```common-lisp\n(defmethod on-exception ((app \u003cweb\u003e) (code (eql 404)))\n  (declare (ignore app code))\n  (merge-pathnames #P\"_errors/404.html\"\n                   *template-directory*))\n```\n\n\n### Hot Deployment\n\nThough Caveman doesn't have a feature for hot deployment, [Server::Starter](http://search.cpan.org/~kazuho/Server-Starter-0.15/lib/Server/Starter.pm) -- a Perl module -- makes it easy.\n\n    $ APP_ENV=production start_server --port 8080 -- clackup --server :fcgi app.lisp\n\nNOTE: Server::Starter requires the server to support binding on a specific fd, which means only `:fcgi` and `:woo` are the ones work with `start_server` command.\n\nTo restart the server, send HUP signal (`kill -HUP \u003cpid\u003e`) to the `start_server` process.\n\n### Error Log\n\nCaveman outputs error backtraces to a file which is specified at `:error-log` in your configuration.\n\n```common-lisp\n(defconfig |default|\n  `(:error-log #P\"/var/log/apps/myapp_error.log\"\n    :databases\n    ((:maindb :sqlite3 :database-name ,(merge-pathnames #P\"myapp.db\"\n                                                        *application-root*)))))\n```\n\n## Use another templating library\n\n### CL-WHO\n\n```common-lisp\n(import 'cl-who:with-html-output-to-string)\n\n(defroute \"/\" ()\n  (with-html-output-to-string (output nil :prologue t)\n    (:html\n      (:head (:title \"Welcome to Caveman!\"))\n      (:body \"Blah blah blah.\"))))\n;=\u003e \"\u003c!DOCTYPE html PUBLIC \\\"-//W3C//DTD XHTML 1.0 Strict//EN\\\" \\\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\\\"\u003e\n;   \u003chtml\u003e\u003chead\u003e\u003ctitle\u003eWelcome to Caveman!\u003c/title\u003e\u003c/head\u003e\u003cbody\u003eBlah blah blah.\u003c/body\u003e\u003c/html\u003e\"\n```\n\n* [CL-WHO Website](http://weitz.de/cl-who/)\n\n### CL-Markup\n\n```common-lisp\n(import 'cl-markup:xhtml)\n\n(defroute \"/\" ()\n  (xhtml\n    (:head (:title \"Welcome to Caveman!\"))\n    (:body \"Blah blah blah.\")))\n;=\u003e \"\u003c?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?\u003e\u003c!DOCTYPE html PUBLIC \\\"-//W3C//DTD XHTML 1.0 Transitional//EN\\\" \\\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\\\"\u003e\u003chtml\u003e\u003chead\u003e\u003ctitle\u003eWelcome to Caveman!\u003c/title\u003e\u003c/head\u003e\u003cbody\u003eBlah blah blah.\u003c/body\u003e\u003c/html\u003e\"\n```\n\n* [CL-Markup repository](https://github.com/arielnetworks/cl-markup)\n\n### cl-closure-template\n\n```html\n{namespace myapp.view}\n\n{template renderIndex}\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003ctitle\u003e\"Welcome to Caveman!\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  Blah blah blah.\n\u003c/body\u003e\n\u003c/html\u003e\n{/template}\n```\n\n```common-lisp\n(import 'myapp.config:*template-directory*)\n\n(closure-template:compile-cl-templates (merge-pathnames #P\"index.tmpl\"\n                                                        *template-directory*))\n\n(defroute \"/\" ()\n  (myapp.view:render-index))\n```\n\n* [cl-closure-template](http://quickdocs.org/cl-closure-template/)\n* [Closure Templates Documentation](https://developers.google.com/closure/templates/docs/overview)\n\n\u003c!-- Commenting out because these are old.\n\n## Use another database library\n\n### CLSQL\n\nYou can use Lack.Middleware.Clsql to use CLSQL in Clack compliant application.\n\nIn Caveman, add the middleware to `builder` in \"PROJECT_ROOT/app.lisp\".\n\n```common-lisp\n(ql:quickload :clack-middleware-clsql)\n(import 'clack.middleware.clsql:\u003cclack-middleware-clsql\u003e)\n\n(builder\n (\u003cclack-middleware-clsql\u003e\n  :database-type :mysql\n  :connection-spec '(\"localhost\" \"db\" \"fukamachi\" \"password\"))\n *web*)\n```\n\n* [Clack.Middleware.Clsql](http://quickdocs.org/clack/api#system-clack-middleware-clsql)\n* [CLSQL: Common Lisp SQL Interface](http://clsql.b9.com/)\n\n### Postmodern\n\nYou can use Clack.Middleware.Postmodern to use Postmodern in Clack compliant application.\n\nIn Caveman, add the middleware to `builder` in \"PROJECT_ROOT/app.lisp\".\n\n\n```common-lisp\n(ql:quickload :clack-middleware-postmodern)\n(import 'clack.middleware.postmodern:\u003cclack-middleware-postmodern\u003e)\n\n(builder\n (\u003cclack-middleware-postmodern\u003e\n  :database \"database-name\"\n  :user \"database-user\"\n  :password \"database-password\"\n  :host \"remote-address\")\n *web*)\n```\n\n* [Clack.Middleware.Postmodern](http://quickdocs.org/clack/api#system-clack-middleware-postmodern)\n* [Postmodern](http://marijnhaverbeke.nl/postmodern/)\n\n--\u003e\n\n## See Also\n\n* [Clack](http://clacklisp.org/) - Web application environment.\n* [Lack](https://github.com/fukamachi/lack) - The core of Clack.\n* [ningle](http://8arrow.org/ningle/) - Super micro web application framework that Caveman is based on.\n* [Djula](http://mmontone.github.io/djula/) - HTML Templating engine.\n* [CL-DBI](http://8arrow.org/cl-dbi/) - Database-independent interface library.\n* [SxQL](http://8arrow.org/sxql/) - SQL builder library.\n* [Envy](https://github.com/fukamachi/envy) - Configuration switcher.\n* [Roswell](https://github.com/snmsts/roswell) - Common Lisp implementation manager.\n\n## Author\n\n* Eitaro Fukamachi (e.arrows@gmail.com)\n\n# License\n\nLicensed under the LLGPL License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffukamachi%2Fcaveman","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffukamachi%2Fcaveman","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffukamachi%2Fcaveman/lists"}