• Stars
    star
    348
  • Rank 121,840 (Top 3 %)
  • Language
    Clojure
  • Created almost 11 years ago
  • Updated 6 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Some Clojure functions for creating static websites.

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.06.03"] 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 pages map. Wouldn't it be nice if someone told you about conflicting URLs?

(defn create-pages [content]
  (merge-page-sources
   {:person-pages (create-person-pages (:people content))
    :article-pages (create-article-pages (:articles content))
    :general-pages (create-general-pages content)}))

(defn get-pages []
  (create-pages (load-content)))

So merge-page-sources takes a map. The values are the page-maps to merge. The keys in the map are only used for error reporting:

URL conflicts between :article-pages and :general-pages: #{"/about.html"}

report-differences

This entirely side-effecty function takes an old and a new map of strings, compares them and reports the differences to standard out.

Here's a code example from one of my projects:

(def export-dir "./dist")

(defn- load-export-dir []
  (stasis/slurp-directory export-dir #"\.[^.]+$"))

(defn export
  "Export the entire site as flat files to the export-dir."
  []
  (let [old-files (load-export-dir)] ;; 1.
    (stasis/empty-directory! export-dir)
    (stasis/export-pages (get-pages) export-dir)
    (println)
    (println "Export complete:")
    (stasis/report-differences old-files (load-export-dir)) ;; 2.
    (println)))
  1. We slurp the old directory into memory before emptying it

  2. After we're done exporting, pass in old and new files to print the report.

This prints something along these lines (in glorious ansi color):

Export complete:
- 260 unchanged files.
- 2 changed files:
    - /index.html
    - /troubleshooting/index.html
- 1 removed file:
    - /verified-hash/index.html
- 1 added file:
    - /verified-hashes/index.html

Dependent pages

Instead of returning a string of html for your pages, you may return a map:

{:contents "the page contents",
 :dependent-pages {"/uri" "dependent page contents"}}

These dependent pages are then also served and exported.

Why would I need that?

Most often, you don't.

However: When creating a page, you may find that you want to extract pieces of the page into yet new pages. Say you want to do some optimizations and extract inline JavaScript from pages and serve them as cacheable separate files.

Dependent pages gives you the opportunity to do this without cluttering up your page-creation logic. When the page containing JavaScript is loaded, you may alter the generated page to use a script tag that links to a separate file, and then return a page map where :dependent-pages contains the page (of JavaScript) that contains the extracted inline source.

Q & A

Can I avoid the .html endings on my pages?

Yes. Stasis will handle URLs like /projects/clojure/ by creating projects and clojure folders, and placing an index.html in it.

Beware of the implications tho. You suddenly have multiple valid URLs to the same page, which is not good for your standing with Google. There's also the case of /projects/clojure vs /projects/clojure/ that might trip you up.

It'll probably take some wrangling of your web server to get this in pristine shape.

Are there any full fledged examples to look at?

Christian Johansen wrote Building static sites in Clojure with Stasis, which is an excellent starting point.

If you just want some code to look at, check these out:

Got an open source site written in Stasis? Do let me know, and I'll add it here!

Why won't my Enlive templates update when I edit them?

Your template definitions are reloaded only when you touch the code in that namespace. Ring is unaware of the dependency on the template files.

There are some tricks in this thread. I prefer to place my templates in a separate namespace, and do this:

(defn reload-templates []
  (require 'example.templates :reload))

And then call that in my get-pages function.

How do I create an RSS feed for my blog?

No worries, it's just a bit of XML generation. Here's a working snippet from whattheemacsd.com to create an Atom feed:

clojure/data.xml does not allow default namespaces. This means that all tags must have namespace information attached. This is accomplished by adding percent-encoded Clojure namespaces to the tag keywords. The simplest way to accomplish this without static analysis warnings is to explicitly define the namespaces using :as-alias in the ns declaration (requires Clojure 1.11.0 or later).

(ns what-the-emacsd.rss
  (:require
    [clojure.data.xml :as xml]
    [xmlns.http%3a%2f%2fwww.w3.org%2f2005%2fAtom :as-alias atom]))

(defn- entry [post]
  [::atom/entry
   [::atom/title (:name post)]
   [::atom/updated (:date post)]
   [::atom/author [:name "Magnar Sveen"]]
   [::atom/link {:href (str "http://whattheemacsd.com" (:path post))}]
   [::atom/id (str "urn:whattheemacsd-com:feed:post:" (:name post))]
   [::atom/content {:type "html"} (:content post)]])

(defn atom-xml [posts]
  (xml/emit-str
   (xml/sexp-as-element
    [::atom/feed
     [::atom/id "urn:whattheemacsd-com:feed"]
     [::atom/updated (-> posts first :date)]
     [::atom/title {:type "text"} "What the .emacs.d!?"]
     [::atom/link {:rel "self" :href "http://whattheemacsd.com/atom.xml"}]
     (map entry posts)])))

If this seems like too much, well, maybe you're using the wrong static site library. But anyway, there's even a library to create RSS for you here: clj-rss.

Again, why not use one of the existing frameworks?

I think the existing frameworks are great if they fit your style. Stasis imposes no styles. There are very few decisions made for you - no markdown vs asciidoc, no enlive vs hiccup. No configuration options. You have to - and get to - make those yourself.

So, yeah ... I think Stasis would be a great starting point if you want to create the 6th static site framework to go in that list at the top. :-)

Change log

From 2.5 to 2023.06.03

  • Update ring/ring-codec to work with babashka (John Swanson)
  • Add :stasis/ignore-nil-pages? option to options map (Stel Abrego)
  • Work around incompatability between lein and clj for resources on class path

From 2.4 to 2.5

  • Binary files can now be served by Stasis. (Stephen Starkey)

From 2.3 to 2.4

  • Dependencies bumped ahead four years

From 2.2 to 2.3

  • slurp-directory and slurp-resources now take options like slurp.

From 2.1 to 2.2

From 2.0 to 2.1

  • Add report-differences

From 1.1 to 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.

From 1.0 to 1.1

  • Add slurp-resources
  • Ensure page paths are absolute (Cesar BP)
  • Fix an issue with running Stasis on Windows (Oak Nauhygon)

Contributors

Thanks!

Contribute

Yes, please do. And add tests for your feature or fix, or I'll certainly break it later.

Running the tests

lein midje will run all tests.

lein midje namespace.* will run only tests beginning with "namespace.".

lein midje :autotest will run all the tests indefinitely. It sets up a watcher on the code files. If they change, only the relevant tests will be run again.

License

Copyright Β© 2014 Magnar Sveen

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

More Repositories

1

multiple-cursors.el

Multiple cursors for emacs.
Emacs Lisp
2,294
star
2

dash.el

A modern list library for Emacs
Emacs Lisp
1,669
star
3

expand-region.el

Emacs extension to increase selected region by semantic units.
Emacs Lisp
1,367
star
4

s.el

The long lost Emacs string manipulation library.
Emacs Lisp
1,030
star
5

prone

Better exception reporting middleware for Ring.
Clojure
512
star
6

optimus

A Ring middleware for frontend performance optimization.
Clojure
364
star
7

parens-of-the-dead

A series of zombie-themed games written with Clojure and ClojureScript.
Clojure
153
star
8

mark-multiple.el

An extension to emacs that sorta lets you mark several regions at once
Emacs Lisp
113
star
9

string-edit.el

Avoid escape nightmares by editing strings in a separate buffer
Emacs Lisp
105
star
10

tagedit

A collection of paredit-like functions for editing in html-mode.
Emacs Lisp
99
star
11

multifiles.el

Work in progress: View and edit parts of multiple files in one buffer
Emacs Lisp
76
star
12

fold-this.el

Fold the active region in Emacs
Emacs Lisp
73
star
13

change-inner.el

Emacs version of vim's ci and co commands
Emacs Lisp
64
star
14

angular-snippets.el

Yasnippets for AngularJS
Emacs Lisp
45
star
15

autolint

Autolint watches your files for jslint-errors
JavaScript
43
star
16

kaocha-runner.el

An emacs package for running Kaocha tests via CIDER.
Emacs Lisp
34
star
17

smart-forward.el

semantic navigation based on expand-region.el
Emacs Lisp
31
star
18

java-time-literals

A Clojure library that defines literals for java.time classes.
Clojure
29
star
19

hardcore-mode.el

Disable arrow keys + optionally backspace and return
Emacs Lisp
28
star
20

confair

Confair is a configuration library for Clojure.
Clojure
26
star
21

parens-of-the-dead-s2

The code for Parens of the Dead season 2
Clojure
23
star
22

emacsd-reboot

Reboot of .emacs.d
Emacs Lisp
23
star
23

yesql-ghosts

Display ghostly yesql defqueries inline, in Emacs
Emacs Lisp
18
star
24

emacsrocks.com

The emacsrocks.com site implemented in Clojure with Stasis and Optimus
Clojure
16
star
25

catenate

A deprecated Ring middleware to serve concatenated static files with cache buster URLs in production.
Clojure
16
star
26

realize

Realizing clojure data structures, no more laziness
Clojure
15
star
27

test-with-files

A Clojure library to easily write tests with files.
Clojure
15
star
28

datomic-type-extensions

A Clojure library that wraps Datomic API functions to add type extensions.
Clojure
14
star
29

datoms-differ

Find the diff between two txes in datoms.
Clojure
14
star
30

datomic-snippets

Yasnippets for Datomic.
Emacs Lisp
14
star
31

mapdown

A lightweight markup format to turn strings into maps in Clojure.
Clojure
13
star
32

zombie-clj

An zombie themed web game made with Clojure and ClojureScript
Clojure
13
star
33

cljs-styles

Vendor prefixes for React inline styles with ClojureScript.
Clojure
13
star
34

html5-walker

Search and replace in HTML5 strings.
Clojure
12
star
35

what-the-emacsd

The code for http://whattheemacsd.com
HTML
11
star
36

www.parens-of-the-dead.com

The code that generates parens-of-the-dead.com
Clojure
11
star
37

kaocha-noyoda

Don't talk like yoda. This kaocha plugin lets you write `(is (= actual expected))`.
Clojure
11
star
38

Zombie-TDD

Bli med nΓ₯r jeg lager et nettspill fra scratch med javascript pΓ₯ bΓ₯de frontend og backend. Det blir zombier, mafia og testdrevet utvikling.
JavaScript
10
star
39

annoying-arrows-mode.el

Emacs gets annoyed when you navigate around your document one char at a time.
Emacs Lisp
10
star
40

helpful-loader

A Clojure library to load resources with helpful error messages.
Clojure
9
star
41

buster-mode

A minor mode for emacs to speed up development when writing tests with Buster.JS
Emacs Lisp
8
star
42

stubadub

A small stubbing library for Clojure and ClojureScript
Clojure
8
star
43

bang.el

Used to be a modern list library for Emacs (renamed to dash)
Emacs Lisp
8
star
44

optimus-angular

Angular.JS optimizations for Optimus
Clojure
8
star
45

buster.tmbundle

TextMate bundle for Buster.js
6
star
46

java-time-dte

Datomic type extensions for java.time classes
Clojure
6
star
47

optimus-img-transform

An Optimus image transformation middleware.
Clojure
6
star
48

simplezen.el

A simple subset of zencoding-mode for Emacs
Emacs Lisp
5
star
49

zombieclj-s02

Koden til ZombieCLJ Sesong 2
CSS
5
star
50

server-facade

Code and slides belonging to my JavaZone-talk about wrapping your ajax-calls to get cleaner code and nicer testing.
JavaScript
4
star
51

blockout

Recreating one of my first games for the web to learn Canvas.
JavaScript
4
star
52

naive-xml-reader

A naive Clojure library that turns XML into maps.
Clojure
4
star
53

norsk-extreme-startup

Oversatt til norsk og tilpassett FINN
Ruby
3
star
54

roll20-scripts

Scripts for roll20
JavaScript
3
star
55

optimus-jsx

A React JSX asset loader for Optimus.
JavaScript
3
star
56

crappy-jsp-mode

Seriously, this is not a good jsp-mode, it just solves some of my problems
Emacs Lisp
3
star
57

optimus-less

A LESS asset loader for Optimus.
Clojure
3
star
58

zombieclj.no

Koden bak www.zombieclj.no
Clojure
2
star
59

zombieclj-s2

Koden for andre sesong av Zombie CLJ.
CSS
2
star
60

mytomatoes.clj

mytomatoes.com rescue operation
Clojure
2
star
61

creator.js

A tiny library for creating create-methods for your objects.
JavaScript
2
star
62

buster-snippets.el

Yasnippets for the Buster test framework.
Emacs Lisp
2
star