{"id":18111894,"url":"https://github.com/chr15m/sitefox","last_synced_at":"2025-05-16T00:06:57.319Z","repository":{"id":37489151,"uuid":"411315799","full_name":"chr15m/sitefox","owner":"chr15m","description":"Node + cljs backend web framework","archived":false,"fork":false,"pushed_at":"2025-04-17T01:55:31.000Z","size":376,"stargazers_count":299,"open_issues_count":12,"forks_count":8,"subscribers_count":15,"default_branch":"main","last_synced_at":"2025-04-30T23:01:57.130Z","etag":null,"topics":["clojure","clojurescript","clojurescript-library","nbb","node","nodejs","shadow-cljs","web","webframework"],"latest_commit_sha":null,"homepage":"https://chr15m.github.io/sitefox/","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chr15m.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-28T14:27:36.000Z","updated_at":"2025-04-19T20:02:29.000Z","dependencies_parsed_at":"2024-02-21T11:31:11.637Z","dependency_job_id":"c578812b-ff13-4450-a52e-778793ff8f8a","html_url":"https://github.com/chr15m/sitefox","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chr15m%2Fsitefox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chr15m%2Fsitefox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chr15m%2Fsitefox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chr15m%2Fsitefox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chr15m","download_url":"https://codeload.github.com/chr15m/sitefox/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253632821,"owners_count":21939376,"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":["clojure","clojurescript","clojurescript-library","nbb","node","nodejs","shadow-cljs","web","webframework"],"created_at":"2024-11-01T01:08:15.878Z","updated_at":"2025-05-16T00:06:52.278Z","avatar_url":"https://github.com/chr15m.png","language":"Clojure","funding_links":[],"categories":["Clojure"],"sub_categories":[],"readme":"Web framework for ClojureScript on Node.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/logo.svg?sanitize=true\" alt=\"Sitefox logo\"\u003e\u003cbr/\u003e\n\u003c/p\u003e\n\nIn the tradition of Flask, and Sinatra.\nDesigned for indie devs who ship fast.\nBattle tested on real sites.\n\n[Philosophy](#philosophy) | [Quick start](#quick-start) | [Documentation](https://chr15m.github.io/sitefox/) | [API](#api) | [Examples](https://github.com/chr15m/sitefox/tree/main/examples) | [Community](#community)\n\n[Getting started YouTube video](https://www.youtube.com/watch?v=zjLaLXfgY34).\n\n```clojure\n(ns webserver\n  (:require\n    [promesa.core :as p]\n    [sitefox.html :refer [render]]\n    [sitefox.web :as web]))\n\n(defn root [_req res]\n  (-\u003e\u003e (render [:h1 \"Hello world!\"])\n       (.send res)))\n\n(p/let [[app host port] (web/start)]\n  (.get app \"/\" root)\n  (print \"Serving on\" (str \"http://\" host \":\" port)))\n```\n\n## Philosophy\n\n * [12 factor](https://12factor.net/).\n * 👇 Batteries included.\n\n### Batteries included\n\n * [Routing](#web-server--routes)\n * [Templates](#templates)\n * [Database + Key-value store](#database)\n * [Sessions](#sessions)\n * [Authentication](#authentication)\n * [Email](#email)\n * [Forms](#forms)\n * [Logging](#logging-and-errors)\n * [Live reloading](#live-reloading)\n\n### Environment variables\n\n * `PORT` - configure the port Sitefox web server binds to.\n * `BIND_ADDRESS` - configure the IP address Sitefox web server binds to.\n * `SMTP_SERVER` - configure the outgoing SMTP server e.g. `SMTP_SERVER=smtps://username:password@mail.someserver.com/?pool=true`.\n * `DATABASE_URL` - configure the database to connect to. Defaults to `sqlite://./database.sqlite`.\n\n## Quick start\n\nThe quickest way to start is using one of the `create` scripts which will set up an example project for you with one command.\nIf you're building a simple website without much front-end interactivity beyond form submission, the [nbb](https://github.com/babashka/nbb) create script is the way:\n\n```\nnpm init sitefox-nbb mywebsite\n```\n\nThis will create a folder called `mywebsite` containing your new project.\nNote you can use [Scittle](https://github.com/borkdude/scittle) to run cljs client-side.\n\nIf you're building a full-stack ClojureScript application the [shadow-cljs](https://github.com/shadow-cljs) create script is the way:\n\n```\nnpm init sitefox-shadow-fullstack myapp\n```\n\nThat will create a folder called `myapp` containing your new project.\n\n### Manually installing Sitefox\n\nAdd Sitefox to your project as a dependency:\n\n```\n{:deps\n {io.github.chr15m/sitefox {:git/tag \"v0.0.26\" :git/sha \"e6ea2027b5d4277917732d43d550083c8e105da9\"}}}\n```\n\nIf you're using `npm` you can install sitefox as a dependency that way.\nIf you do that you will need to add `node_modules/sitefox/src` to your classpath somehow.\n\n```\nnpm i sitefox\n```\n\n**Note**: M1 Mac users may need to set the Python version in npm like this:\n\n```\nnpm config set python python3\n```\n\nThis is because the `node-sqlite3` build sometimes fails without the setting.\nSee [this issue](https://github.com/chr15m/sitefox/issues/20#issuecomment-1248719076) for more details.\n\n\n### Example server\n\nAn example server with two routes, one of which writes values to the key-value database.\n\n```clojure\n(ns my.server\n  (:require\n    [promesa.core :as p]\n    [sitefox.web :as web]\n    [sitefox.db :refer [kv]]))\n\n(defn home-page [req res]\n  ; send a basic hello world response\n  (.send res \"Hello world!\"))\n\n(defn hello [req res]\n  ; write a value to the key-value database\n  (p/let [table (kv \"sometable\")\n          r (.write table \"key\" 42)]\n    (.json res true)))\n\n(defn setup-routes [app]\n  ; flush all routes from express\n  (web/reset-routes app)\n  ; set up an express route for \"/\"\n  (.get app \"/\" home-page)\n  ; set up an express route for \"/hello\"\n  (.post app \"/hello\" hello)\n  ; statically serve files from the \"public\" dir on \"/\"\n  ; (or from \"build\" dir in PROD mode)\n  (web/static-folder app \"/\" \"public\"))\n\n(defn main! []\n  ; create an express server and start serving\n  ; BIND_ADDRESS \u0026 PORT env vars set host \u0026 port.\n  (p/let [[app _host _port] (web/start)]\n    ; set up the routes for the first time\n    (setup-routes app)))\n```\n\nMore [Sitefox examples here](https://github.com/chr15m/sitefox/tree/main/examples).\n\n## Community\n\nIf you need support with Sitefox you can:\n\n * Join the [Clojure Slack #sitefox channel](https://app.slack.com/client/T03RZGPFR/C02LB2842UA).\n * You can also [ask a question on the GitHub discussions page](https://github.com/chr15m/sitefox/discussions).\n\n## API\n\n### Web server \u0026 routes\n\nSitefox uses the [express](https://expressjs.com) web server with sensible defaults for sessions and logging.\nSee the [express routing documentation](http://expressjs.com/en/guide/routing.html) for details.\n\nCreate a new server with `web/start` and set up a route which responds with \"Hello world!\" as follows:\n\n```clojure\n(-\u003e (web/start)\n  (.then (fn [app host port]\n    (.get app \"/myroute\"\n      (fn [req res]\n        (.send res \"Hello world!\"))))\n```\n\nSitefox comes with an optional system to reload routes when the server is changed.\nYour express routes will be reloaded every time your server code is refreshed (e.g. by a shadow-cljs build).\nIn this example the function `setup-routes` will be called when a rebuild occurs.\n\n```clojure\n(defn setup-routes [app]\n  ; flush all routes from express\n  (web/reset-routes app)\n  ; ask express to handle the route \"/\"\n  (.get app \"/\" (fn [req res] (.send res \"Hello world!\"))))\n\n; during the server setup hook up the reloader\n(reloader (partial #'setup-routes app))\n```\n\nI recommend the [promesa](https://github.com/funcool/promesa) library for managing promise control flow.\nThis example assumes require `[promesa.core :as p]`:\n\n```clojure\n(p/let [[app host port] (web/start)]\n  ; now use express `app` to set up routes and middleware\n  )\n```\n\nAlso see these examples:\n\n * [shadow-cljs server example](https://github.com/chr15m/sitefox/tree/main/examples/shadow-cljs).\n * [nbb server example](https://github.com/chr15m/sitefox/tree/main/examples/nbb).\n\n### Templates\n\nInstead of templates, Sitefox offers shortcuts for server side Reagent rendering, merged wth HTML documents.\n\n```clojure\n[sitefox.html :refer [render-into]]\n```\n\nYou can load an HTML document and render Reagent forms into a selected element:\n\n```clojure\n(def index-html (fs/readFileSync \"index.html\"))\n\n(defn component-main []\n  [:div\n   [:h1 \"Hello world!\"]\n   [:p \"This is my content.\"]])\n\n; this returns a new HTML string that can be returned\n; e.g. with (.send res)\n(render-into index-html \"main\" [component-main])\n```\n\nSitefox uses [node-html-parser](https://www.npmjs.com/package/node-html-parser) and offers shortcuts for working with HTML \u0026 Reagent:\n\n * `html/parse` is shorthand for `node-html-parser/parse`.\n * `html/render` is shorthand for Reagent's `render-to-static-markup`.\n * `html/$` is shorthand for the parser's `querySelector`.\n * `html/$$` is shorthand for the parser's `querySelectorAll`.\n\nAlso see the [templates example](https://github.com/chr15m/sitefox/tree/main/examples/templates) project.\n\n### Database\n\nSitefox makes it easy to start storing key-value data with no configuration.\nYou can transition to more structured data later if you need it.\nIt bundles [Keyv](https://github.com/lukechilds/keyv) which is a database backed key-value store.\nYou can access the key-value store through `db/kv` and the underlying database through `db/client`.\n\nSee the full [documentation for the db module](https://chr15m.github.io/sitefox/sitefox.db.html).\n\nBy default a local sqlite database is used and you can start persisting data on the server immediately without any configuration.\nOnce you move to production you can configure another database using the environment variable `DATABASE_URL`.\nFor example, to use a postgres database called \"DBNAME\" you can access it as follows (depending on your network/local setup):\n\n```\nDATABASE_URL=\"postgres://%2Fvar%2Frun%2Fpostgresql/DBNAME\"\nDATABASE_URL=postgres://someuser:somepassword@somehost:5432/DBNAME\nDATABASE_URL=postgres:///somedatabase\n```\n\nNote that you will also need to `npm install @keyv/postgres` if you want to use the Postgres backend.\n\nTo use the database and key-value interface first require the database module:\n\n```clojure\n[sitefox.db :as db]\n```\n\nNow you can use `db/kv` to write a key-value to a namespaced \"table\":\n\n```clojure\n(let [table (db/kv \"sometable\")]\n  (.set table \"key\" \"42\"))\n```\n\nRetrieve the value again:\n\n```clojure\n(-\u003e (.get table \"key\")\n  (.then (fn [val] (print val))))\n```\n\nYou can use `db/client` to access the underlying database client.\nFor example to make a query against the configured database:\n\n```clojure\n(let [c (db/client)]\n  (-\u003e (.query c \"select * from sometable WHERE x = 1\")\n    (.then (fn [rows] (print rows)))))\n```\n\nAgain, [promesa](https://github.com/funcool/promesa) is recommended for managing control flow during database operations.\n\nTo explore key-value data from the command line use sqlite and jq to filter data like this:\n\n```\nsqlite3 database.sqlite \"select * from keyv where key like 'SOMEPREFIX%';\" | cut -f 2 -d \"|\" | jq '.'\n```\n\n#### Sqlite3 full stack traces\n\nBy default the `node-sqlite3` module does not provide full stack traces with line numbers etc. when a database error occurs.\nIt's possible to [turn on verbose stack traces](https://github.com/TryGhost/node-sqlite3/wiki/Debugging) with a small performance penalty as follows:\n\n```clojure\n(ns yourapp\n  (:require\n    [\"sqlite3\" :as sqlite3]))\n\n(.verbose sqlite3)\n```\n\n#### Enabling Sqlite3 WAL mode\n\nIf you want to run sqlite3 in production you may run into the error `SQLITE_BUSY: database is locked` when performing simultaneous database operations from different clients.\nIt is possible to resolve these concurrency and locking issues by enabling [write-ahead logging mode in sqlite3](https://www.sqlite.org/wal.html) as follows:\n\n```\n(ns yourapp\n  (:require\n    [sitefox.db :refer [client]]))\n\n(p/let [c (client)\n        wal-mode-enabled (.query c \"PRAGMA journal_mode=WAL;\")]\n  (js/console.log wal-mode-enabled))\n```\n\nThis code can safely be placed in the main function of your server code.\n\n### Sessions\n\nSessions are enabled by default and each visitor to your server will have their own session.\nThe session data is persisted server side across page loads so you can use it to store authentication status for example.\nSessions are backed into a namespaced `kv` table (see the database section above).\nYou can read and write arbitrary JS data structures to the session using `req.session`.\n\nTo write a value to the session store (inside a route handler function):\n\n```clojure\n(let [session (aget req \"session\")]\n  (aset session \"myvalue\" 42))\n```\n\nTo read a value from the session store:\n\n```clojure\n(aget req \"session\" \"myvalue\")\n```\n\n### Authentication\n\nSitefox wraps the Passport library to implement authentication.\nYou can add simple email and password based authentication to your app with three function calls:\n\n```clojure\n(defn setup-routes [app]\n  (let [template (fs/readFileSync \"index.html\")]\n    (web/reset-routes app)\n    ; three calls to set up email based authentication\n    (auth/setup-auth app)\n    (auth/setup-email-based-auth app template \"main\")\n    (auth/setup-reset-password app template \"main\")\n    ; ... add your additional routes here ... ;\n    ))\n```\n\nThe `template` string passed in is an HTML document and `\"main\"` is the selector specifying where to mount the auth UI.\nThis will set up the following routes by default where you can send users to sign up, sign in, and reset their password:\n\n * `/auth/sign-in`\n * `/auth/sign-up`\n * `/auth/reset-password`\n\nIt is also possible to override the default auth UI Reagent forms and the redirect URLs to customise them with your own versions.\nSee the [auth documentation](https://chr15m.github.io/sitefox/sitefox.auth.html#var-setup-auth) for detail about how to supply your own Reagent forms.\nAlso see the [source code for the default Reagent auth forms](https://github.com/chr15m/sitefox/blob/main/src/sitefox/auth.cljs#L401) if you want to make your own.\n\nWhen a user signs up their data is persisted into the default Keyv database used by Sitefox.\nYou can retrieve the currently authenticated user's datastructure on the request object:\n\n```clojure\n(let [user (aget req \"user\")] ...)\n```\n\nYou can then update the user's data and save their data back to the database.\nThe `applied-science.js-interop` library is convenient for this (required here as `j`):\n\n```clojure\n(p/let [user (aget req \"user\")]\n  (j/assoc! user :somekey 42)\n  (auth/save-user user))\n```\n\nIf you want to create a new table it is useful to key it on the user's uuid which you can obtain with `(aget user \"id\")`.\n\nSee the [authentication example](https://github.com/chr15m/sitefox/tree/main/examples/authentication) for more detail.\n\nTo add a new authentication scheme such as username based, or 3rd party oauth, consult the [Passport docs](https://www.passportjs.org/) and [auth.cljs](https://github.com/chr15m/sitefox/blob/main/src/sitefox/auth.cljs#L210). Pull requests most welcome!\n\n### Email\n\nSitefox bundles [nodemailer](https://nodemailer.com) for sending emails.\nConfigure your outgoing SMTP server:\n\n```\nSMTP_SERVER=smtps://username:password@mail.someserver.com/?pool=true\n```\n\nThen you can use the `send-email` function as follows:\n\n```clojure\n(-\u003e (mail/send-email\n      \"test-to@example.com\"\n      \"test@example.com\"\n      \"This is my test email.\"\n      :text \"Hello, This is my first email from **Sitefox**. Thank you.\")\n    (.then js/console.log))\n```\n\nBy default sent emails are logged to `./logs/mail.log` in json-lines format.\n\nIf you don't specify an SMTP server, the email module will be in debug mode.\nNo emails will be sent, outgoing emails will be written to `/tmp` for inspection,\nand `send-email` outcomes will also be logged to the console.\n\nIf you set `SMTP_SERVER=ethereal` the ethereal.email service will be used.\nAfter running `send-email` you can print the `url` property of the result.\nYou can use the links that are printed for testing your emails in dev mode.\n\nAlso see the [send-email example](https://github.com/chr15m/sitefox/tree/main/examples/send-email) project.\n\n### Forms\n\nSee the [form validation example](https://github.com/chr15m/sitefox/tree/main/examples/form-validation) which uses [node-input-validator](https://www.npmjs.com/package/node-input-validator) and checks for CSRF problems.\n\n#### CSRF protection\n\nTo ensure you can `POST` without CSRF warnings you should create a hidden element like this (Reagent syntax):\n\n```clojure\n[:input {:name \"_csrf\" :type \"hidden\" :default-value (.csrfToken req)}]\n```\n\nIf you're making an ajax `POST` request from the client side, you should pass the CSRF token as a header.\nA valid token is available as a string at the JSON endpoint `/_csrf-token` and you can fetch it using `fetch-csrf-token`\nand add it to the headers of a fetch request as follows:\n\n```clojure\n(ns n (:require [sitefox.ui :refer [fetch-csrf-token]]))\n\n(-\u003e (fetch-csrf-token)\n    (.then (fn [token]\n             (js/fetch \"/api/endpoint\"\n                       #js {:method \"POST\"\n                            :headers #js {:Content-Type \"application/json\"\n                                          :X-XSRF-TOKEN token} ; \u003c- use token here\n                            :body (js/JSON.stringify (clj-\u003ejs some-data))}))))\n```\n\n**Note**: you can fetch the CSRF token from a client side cookie instead if you set the environment variable `SEND_CSRF_TOKEN`.\nThis was the default in previous Sitefox versions.\nWhen set, Sitefox will send the token on every GET request in the client side cookie\n`XSRF-TOKEN` and this can be retrieved with the `ui/csrf-token` function.\nThis is a valid, but less secure form of CSRF protection.\n\nIn some rare circumstances you may wish to turn off CSRF checks (for example posting to an API from a non-browser device).\nIf you know what you are doing you can use the `pre-csrf-router` to add routes which bypass the CSRF checking:\n\n```clojure\n(defn setup-routes [app]\n  ; flush all routes from express\n  (web/reset-routes app)\n  ; set up an API route bypassing CSRF checks\n  (.post (j/get app \"pre-csrf-router\") \"/api/endpoint\" endpoint-unprotected-by-csrf)\n  ; set up an express route for \"/hello\" which is protected as normal\n  (.post app \"/hello\" hello))\n```\n\n### Logging and errors\n\nBy default the web server will write to log files in the `./logs` folder.\nThese files are automatically rotated by the server. There are two types of logs:\n\n * `logs/access.log` which are standard web access logs in \"combined\" format.\n * `logs/error.log` where tracebacks are written using `tracebacks/install-traceback-handler`.\n\nTo send uncaught exceptions to the error log:\n\n```\n(def admin-email (env-required \"ADMIN_EMAIL\"))\n(def build-id (try (fs/readFileSync \"build-id.txt\") (catch :default _e \"dev\")))\n\n(install-traceback-handler admin-email build-id)\n```\n\nCreate `build-id.txt` based on the current git commit as follows:\n\n```\ngit rev-parse HEAD | cut -b -8 \u003e build-id.txt\n```\n\nIf you want to get correct ClojureScript line numbers in tracebacks require `[\"source-maps-support\" :as sourcemaps]` and then:\n\n```\n(.install sourcemaps)\n```\n\n#### 404 and 500 errors\n\nYou can use the [`web/setup-error-handler`](https://chr15m.github.io/sitefox/sitefox.db.html#var-setup-error-handler)\nfunction to serve a page for those errors based on a Reagent component you define:\n\n```clojure\n(defn component-error-page [_req error-code error]\n  [:section.error\n   [:h2 error-code \" Error\"]\n   (case error-code\n     404 [:p \"We couldn't find the page you're looking for.\"]\n     500 [:\u003c\u003e [:p \"An error occurred:\"] [:p (.toString error)]]\n     [:div \"An unknown error occurred.\"])])\n\n(web/setup-error-handler app my-html-template \"main\" component-error-page)\n```\n\nYou can combine these to catch both 500 Internal Server errors and uncaught exceptions as follow:\n\n```\n(let [traceback-handler (install-traceback-handler admin-email build-id)]\n    (web/setup-error-handler app template-app \"main\" component-error-page traceback-handler))\n```\n\n### Live reloading\n\nLive reloading is supported using both `nbb` and `shadow-cljs`.\nIt is enabled by default when using the npm create scripts.\nExamples have more details.\n\n## Who\n\nSitefox was made by [Chris McCormick](https://mccormick.cx)\n([@mccrmx on Twitter](https://twitter.com/mccrmx) and [@chris@mccormick.cx on Mastodon](https://mccormick.cx/@chris)).\nI iterated on it while building sites for myself and for clients.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchr15m%2Fsitefox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchr15m%2Fsitefox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchr15m%2Fsitefox/lists"}