SkillAgentSearch skills...

Stasis

Some Clojure functions for creating static websites.

Install / Use

/learn @magnars/Stasis
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<img align="right" src="logos/stasis-square.png" width="115" height="115"> Stasis Build Status

A Clojure library of tools for developing static web sites.

Breaking change in 2.0

  • Stasis now only accepts paths that end in a file extension or a slash /.

    Stasis exports paths without a file extension as directories with an index.html file. Most web servers will respond to the slash-less request with a redirect to the URL including a slash. This redirect is entirely avoidable by just linking to the right URL in the first place.

    This change should help you avoid these needless redirects, increasing the speed of your site further.

Install

Add [stasis "2023.11.21"] to :dependencies in your project.clj.

Stasis is a stable library - it will never (again) change it's public API in a breaking way, and will never (intentionally) introduce other breaking changes.

Check out the change log.

Another static site framework? Why?

Well, that's exactly it. I didn't want to use a framework. I don't like the restrained feeling I get when using them. I prefer coding things over messing around with configuration files.

I want to

  • code my own pages
  • set up my own configuration
  • choose my own templating library

Stasis is a collection of functions that are useful when creating static web sites.

No more. There are no batteries included.

If you want a framework that makes it really quick and easy to create a blog, you should take a look at these:

  • misaki is a Jekyll inspired static site generator in Clojure.
  • Madness is a static site generator, based on Enlive and Bootstrap.
  • Static is a simple static site generator written in Clojure.
  • Ecstatic creates static web pages and blog posts from Hiccup templates and Markdown.
  • incise is an extensible static site generator written in Clojure.
  • Cryogen is a static sites generator written in Clojure

They generally come with a folder where you put your blog posts in some templating language, and a set of configuration options about how to set up your blog. They often generate code for you to tweak.

Usage

The core of Stasis is two functions: serve-pages and export-pages. Both take a map from path to contents:

(def pages {"/index.html" "<h1>Welcome!</h1>"})

The basic use case is to serve these live on a local server while developing - and then exporting them as static pages to deploy on some server.

Serving live pages locally

Stasis can create a Ring handler to serve your pages.

(ns example
  (:require [stasis.core :as stasis]))

(def app (stasis/serve-pages pages))

Add Ring as a dependecy and Lein-Ring as a plugin, and point Ring to your app in project.clj.

:ring {:handler example/app}

and start it with lein ring server-headless.

Exporting the pages

To export, just give Stasis some pages and a target directory:

(defn export []
  (stasis/empty-directory! target-dir)
  (stasis/export-pages pages target-dir))

In this example we're also emptying the target-dir first, to ensure old pages are removed.

When you've got this function, you can create an alias for leiningen:

:aliases {"build-site" ["run" "-m" "example/export"]}

and run lein build-site on the command line. No need for a lein plugin.

Livelier live pages

Let's say you want to dynamically determine the set of pages - maybe based on files in a folder. You'll want those to show up without restarting.

To be fully live, instead pass serve-pages a get-pages function:

(defn get-pages []
  (merge {"/index.html" "<h1>Welcome!</h1>"}
         (get-product-pages)
         (get-people-pages)))

(def app (stasis/serve-pages get-pages))

Do I have to build every single page for each request?

No. That's potentially quite a lot of parsing for a large site.

(def pages
  {"/index.html" (fn [context] (str "<h1>Welcome to " (:uri context) "!</h1>"))})

Since we're dynamically building everything for each request, having a function around the contents means you don't have to build out the entire site every time.

Stasis passes a context to each page. When it's served live as a Ring app the context is actually the Ring request, and as such contains the given :uri. Stasis' export-pages makes sure to add :uri to the context too.

You can also pass in configuration options that are included on the context:

(defn my-config {:some "options"})

(def app (stasis/serve-pages get-pages my-config))

(stasis/export-pages pages target-dir my-config)

These are then available when rendering your page.

By including :stasis/ignore-nil-pages? true in the config map, Stasis will ignore any page values that are nil or return nil: serve-pages will return a 404 Not Found HTTP response and export-pages will skip that page. This can be useful when deciding if a page should exist is expensive. For example, if you're using a draft? value to toggle page rendering in your Markdown frontmatter, you can avoid having to parse every Markdown page in the site for every request by using this option:

(def pages
  {"/index.html" "<a href=\"/foobar.html\">This page may or may not exist!</a>")
   "/foobar.html" (fn [_] (let [[frontmatter body] (parse-md "foobar.md")]
                            (when-not (:draft? frontmatter) body)))})

(defn my-config {:stasis/ignore-nil-pages? true})

(def app (stasis/serve-pages pages my-config))

(stasis/export-pages pages target-dir my-config)

Finally, some Ring middlewares put values on the request to be used in rendering. This supports that. Read on:

But what about stylesheets, images and javascript?

Yeah, Stasis doesn't really concern itself with that, since it doesn't have to.

In its simplest form, you can add some JavaScript and CSS to the map of pages. It'll be served and exported just fine. Which is good if you want to dynamically create some JSON, for instance.

But for your CSS, JavaScript and images, I recommend a frontend optimization library. You can use any asset lib that hooks into Ring and lets you export the optimized assets to disk.

I use Optimus. To get you started, here's an example:

(ns example
  (:require [ring.middleware.content-type :refer [wrap-content-type]]
            [stasis.core :as stasis]
            [optimus.prime :as optimus]
            [optimus.assets :as assets]
            [optimus.optimizations :as optimizations]
            [optimus.strategies :refer [serve-live-assets]]
            [optimus.export]
            [example.app :refer [get-pages target-dir]]))

(defn get-assets []
  (assets/load-assets "public" ["/styles/all.css"
                                #"/photos/.*\.jpg"]))

(def app (-> (stasis/serve-pages get-pages)
             (optimus/wrap get-assets optimizations/all serve-live-assets)
             wrap-content-type))

(defn export []
  (let [assets (optimizations/all (get-assets) {})
        pages (get-pages)]
    (stasis/empty-directory! target-dir)
    (optimus.export/save-assets assets target-dir)
    (stasis/export-pages pages target-dir {:optimus-assets assets})))

I create a function to get all the assets, and then add the Optimus Ring middleware to my app. I want to serve assets live, but still have them optimized - this lets my dev environment be as similar to prod as possible.

Then I simply tell Optimus to export its assets into the same target dir as Stasis.

Notice that I add :optimus-assets to the config map passed to stasis/export-pages, which will then be available on the context map passed to each page-generating function. This mirrors what optimus/wrap does on the live Ring server, and allows for linking to assets by their original path.

That's all the detail I'll go into here, but you can read more about all the ways Optimus helps you with frontend performance optimization in its extensive README.

So, what else does Stasis have to offer?

This is about everything you need to start building static sites. But Stasis does come with a few more tools.

slurp-directory

You'll probably create a folder to hold a list of pages, posts, products or people at some point. Read them all in with slurp-directory.

(def articles (slurp-directory "resources/articles/" #"\.md$"))

This gives us a map {"/relative/path.md" "file contents"}. The relative path can be useful if we're creating URLs based on file names.

Here's another example:

(def products (->> (slurp-directory "resources/products/" #"\.edn$")
                   (vals)
                   (map read-string)))

This matches all edn-files in resources/products/, slurps in their contents and transforms it to a list of Clojure data structures.

Fun fact: This is a valid code:

(def app (serve-pages (slurp-directory "resources/pages/" #"\.html$")))

It would serve all .html files in that folder, matching the URL structure to files on disk.

Like slurp, slurp-directory can also receive optional arguments such as :encoding:

(slurp-directory "resources/articles/" #"\.md$" :encoding "UTF-8")

slurp-resources

Just like slurp-directory, except it reads off the class path instead of directly from disk. For performance reasons the .m2 folder is excluded. Open an issue if that causes you pain.

merge-page-sources

You might have several sources for pages that need to be merged into the final `pa

Related Skills

View on GitHub
GitHub Stars354
CategoryDevelopment
Updated3d ago
Forks27

Languages

Clojure

Security Score

80/100

Audited on Apr 2, 2026

No findings