A batteries-included Clojure toolkit for building static web sites. It combines the excellent Stasis with some useful tools, and provides the wiring so you can focus on coding your pages - exactly the way you want.
With tools.deps:
no.cjohansen/powerpack {:mvn/version "2024.12.01"}
With Leiningen:
[no.cjohansen/powerpack "2024.12.01"]
Powerpack is based on several sites built on roughly the same architecture over a 5 year period. The specific Powerpack code-base has been developed alongside a 5000 page static page and has been thoroughly put through its paces. APIs are stable, development experience is top notch, and the production export is truly production ready. Use at will!
Stasis Powerpack reads your raw content from Markdown (including mapdown) and EDN files (by default, you can include more file types as needed) into an in-memory Datomic database, and configures the web server with an asset optimization pipeline, image manipulation, live reload during development, and optionally more stuff - like code syntax highlighting.
Powerpack doesn't have any opinions on how you construct pages, there are no
themes or design. It's still a toolkit, but the overall dataflow is lined up for
you, and the web server is tuned well for both development and production. It
comes with an export
function that exports all your pages, as well as images
and assets, ready for deployment to your serverless static web host of choice.
Stasis Powerpack combines the following libraries:
In Powerpack, you put content in Markdown files and EDN files. These are read and transacted into an in-memory Datomic database. Powerpack defines a few schema attributes it needs to address pages, but other than that you'll need to define your own schema and implement some mapping to prepare raw content for the database. The files can contain whatever you want -- supporting data, a single web page, multiple web pages -- it's up to you.
Powerpack will allow browsing to any entity that has a :page/uri
. You must add
these attributes to any content you want to be browsable.
When you request a URL from the development server, Powerpack will find the page in the database and call on your code to render a response. This response can be any valid ring response. Powerpack provides some convenience library functions to help you build HTML pages, but you're free to ignore them. Before serving the page to the user, Powerpack will perform some post processing on the pages.
Powerpack currently only post processes HTML pages. When it does, images will be optimized, or passed through Imagine, open graph URLs will be qualified with your production domain name, code blocks will be highlighted, etc. Powerpack has an extension point for this post processing.
To build a site with Powerpack, you must provide some configuration, a Datomic schema and a function that renders pages. There is a comprehensive step-by-step getting started in a separate repo.
:site/title
(required)The site title, a string. Used for the default title tag.
:site/default-locale
Defaults to :en
.
The default locale for pages, a keyword. When the current page has no
:page/locale
, this is used for i18n lookups, and as the lang
attribute on
the html
element.
:site/base-url
Your production URL. This is used to qualify open graph URLs.
:datomic/schema-file
(required)Defaults to resources/schema.edn
.
The file that contains the Datomic schema.
:powerpack/content-dir
Defaults to content
.
The directory from which to read content files.
:powerpack/build-dir
Defaults to target/powerpack
.
The directory to export the site to.
:powerpack/render-page
A function that is called to render a page. See rendering pages.
:datomic/uri
Defaults to "datomic:mem://powerpack"
.
The Datomic database URI.
:imagine/config
Image transformation configuration, refer to the Imagine Readme.
:m1p/dictionaries
A map of {locale dictionary-files}
for i18n dictionaries. The map should be
keyed by keyword locales, e.g. :en
, and the value should be a vector of paths
to EDN files with dictionaries, e.g.:
:m1p/dictionaries {:en ["src/powerblog/pages/en.edn"
"src/powerblog/sections/en.edn"]}
Refer to the m1p Readme for information about dictionaries.
:m1p/dictionary-fns
Additional m1p dictionary functions.
:m1p/k
The keyword used for i18n interpolation. Defaults to :i18n
, as in
[:h1 [:i18n ::greeting]]
:optimus/assets
A vector of Optimus asset configurations.
The vector should contain one or more maps with the same options you would pass
to Optimus' load-assets
, e.g.:
:optimus/assets [{:public-dir "public"
:paths ["/images/logo.png"
"/images/photo.jpg"]]
:optimus/bundles
A map of Optimus bundle configurations.
During HTML post processing, CSS bundles will be added to
the head
and JavaScript bundles will be added to the end of body
.
Example:
:optimus/bundles {"app.css"
{:public-dir "public"
:paths ["/styles.css"]}}
:optimus/options
Options passed to Optimus optimizations. Can aditionally contain the following keys to control which optimizations are used:
:minify-css-assets?
defaults to true
:minify-js-assets?
defaults to true
:powerpack/asset-targets
A list of selectors for where Powerpack should look for asset URLs in your
rendered HTML pages. Powerpack uses this information to replace asset paths like
/preview-small/images/climbing.jpg
with a cache-busted version such as
/image-assets/preview-small/fb6a746aee13f753872432da49c32a1cd019a334/images/climbing.jpg
.
The list contains maps of {:selector :attr}
where :selector
is a CSS
selector to find relevant nodes, and :attr
is the string name of the attribute
that may contain asset URLs.
Example:
[{:selector ["img[src]"]
:attr "src"}
,,,
]
During HTML post processing, this is used to find all img
elements that has a src
attribute, and optimize the path in that attribute if
applicable.
The default asset targets are in powerpack.app/default-asset-targets
.
:powerpack/content-file-suffixes
Defaults to ["md" "edn"]
.
What file suffixes to grep for in :powerpack/content-dir
. If you add more
suffixes here, you must also implement powerpack.ingest/parse-file
for them,
see parsing content files.
:powerpack/create-ingest-tx
A function that receives a string file name and the parsed contents, and returns transaction data to be transacted into Datomic. See the step by step tutorial for an example.
:powerpack/dev-assets-root-path
Additional asset path to include only during development. This can be used to serve dev resources such as an unoptimized ClojureScript build during development. This path is ignored during export.
:powerpack/get-context
A function that receives no arguments, and returns a map of data to add to the
context
argument that is passed to :powerpack/render-page
. This can be used
to add external dependencies such as another database connection, the current
time (e.g. a fresh (Instant/now)
for every request), etc.
:powerpack/live-reload-route
Defaults to "/powerpack/live-reload"
.
The route that Powerpack uses for the live reload route. Can be changed if it interferes with your own URLs.
:powerpack/log-level
Defaults to :info
. May be set to :debug
.
:powerpack/on-ingested
A function that is called after Powerpack updates the database - e.g. once after
initial bootup, and whenever you edit content files. Receives the Powerpack app
and a list of derefed results from datomic.api/transact
. You can use this
information to find what data was added:
:powerpack/on-ingested
(fn [powerpack results]
;; All datoms added to the database
(mapcat :tx-data results)
;; The database before the ingest
(:db-before (first results))
;; The database after the ingest
(:db-after (last results)))
:powerpack/on-started
A function that is called after Powerpack boots up. Receives the Powerpack app as its only argument.
:powerpack/on-stopped
A function that is called after Powerpack shuts down. Receives the Powerpack app as its only argument.
:powerpack/page-post-process-fns
Additional post processor functions. A function that receives the context
and
should return a map of selector
to fn
. See the syntax
highlighter
for an example.
:powerpack/port
Defaults to 5050
.
What port to run the Powerpack app on.
:powerpack/resource-dirs
Defaults to ["resources"]
.
Directories where you have resources. Powerpack will watch these directories for
changes to live reload your app. Should correspond to the resource dirs you have
in :paths
and extra-paths
in your deps.edn
.
:powerpack/source-dirs
Defaults to ["src"]
.
Directories where you have source code. Powerpack will watch these directories
for changes to live reload your app. Should correspond to all the source dirs
you have in :paths
and extra-paths
in your deps.edn
.
:powerpack/render-page
is a function that receives two arguments: context
and page
. It should return one of the following:
context
is a map with at least the following keys:
:uri
the request URI:app/db
a Datomic database value:i18n/dictionaries
prepared m1p dictionary maps:powerpack/app
the full Powerpack app map:optimus-assets
the resolved Optimus assetspage
is a Datomic entity map that contains :page/uri
, and any other keys you
added to the page in question.
NB! When you return a full Ring response, beware that your exported static site
will not be able to specify content-type at will - those will be inferred from
the file extension in most static web servers. So if you intend to return JSON,
include .json
in the :page/uri
.
Ring responses that specify a content-type of either application/json
or
application/edn
can return arbitrary Clojure data as its :body
, and
Powerpack will automatically stringify it.
Any HTML response that is a full document (e.g. includes the html
element)
will be post processed.
Powerpack performs post processing on all HTML responses. The post processing is designed to be helpful, unobtrusive and only make relatively objective improvements. For each of the improvements Powerpack tries to make, it will only do so if you have not done anything similar yourself - e.g. Powerpack will add a meta tag to set the default viewport, but only if you haven't done so yourself.
sets the lang
attribute on the HTML element from the :page/locale
(with a
fallback to :site/default-locale
).
Sets the prefix
attribute on the HTML element with the og:
short hand for
open graph.
Adds a title element if missing, and if your page has a :page/title
. Adds
:site/title
behind a pipe after the page title.
Adds a meta tag with the charset attribute.
Adds a meta tag with name="viewport"
and content set to width=device-width, initial-scale=1.0
.
Adds open graph meta tags, if your page has one or more of the following attributes:
:open-graph/description
:open-graph/title
or :page/title
:open-graph/image
If your Optimus assets includes any of the following paths:
/favicon.ico
/favicon-16x16.png
/favicon-32x32.png
/apple-touch-icon.png
Powerpack will insert the corresponding link
elements linking to them.
Optimus CSS bundles are added to the head of the document. JavaScript bundles are added at the end of the body element.
Being a static site generator, Powerpack can't really give you server-side HTTP
redirects. But it does what it can when you give it a redirecting ring response,
and that is to generate an HTML page with a meta tag using
http-equiv="refresh"
, which causes a redirect in browsers.
To create a redirect, return a Ring response like so:
{:status 301
:headers {"location" "/elsewhere/"}}
And Powerpack will build an HTML page like so:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; url=/elsewhere/">
</head>
<body>
<a href="/elsewhere/">Redirect</a>
</body>
</html>
Powerpack parses EDN and markdown (which optionally includes
mapdown) out of the box. You can add
support for other content file types by implementing the multi-method
powerpack.ingest/parse-file
. It dispatches on the suffix as a keyword, e.g.
:edn
, and should return something that can be passed directly to
datomic.api/transact
, e.g. a vector of transactions (typically maps).
Let's say you want to keep some content in HTML files. Here's one possible way to parse them:
(require '[powerpack.ingest :as ingest])
(defmethod ingest/parse-file :html [db file-name file]
[{:page/uri (ingest/suggest-url file-name)
:page/body (slurp file)}])
I've built a few Stasis pages in my time, and I keep wiring together the same things. Over time I've accumulated a toolkit of really useful things that I always want when I do static web pages. So I thought it would be worth trying to tie them all up in a neat package.
Normally I'm very adverse to "don't call us, we'll call you" style frameworks, and especially ones that are just a grab-bag of dependencies. So why introduce another such beast into the world?
I think that if the web server is configured well enough, you typically won't need to "own" it when you make static web sites. Your focus will be on crafting content and coding pages. Because the deployment target is a bunch of static files, you won't keep adding middlewares and tweaking the setup. In other words, I suspect it might be possible to "set and forget" the server, thus leaving control of it to a framework like Powerpack might be acceptable.
Based on the experience of building a 5000 page static site with Powerpack in a two person team, I would consider this hypothesis validated.
Powerpack combines a bunch of tools in a package. However, it makes no effort to hide them or "unify" them in any way. They are reasonably configured and wired them together for you, but configuration is passed through, and no features are hidden or proxyed in any way. This should reduce the risk of Powerpack becoming a bottle neck or unnecessary layer of confusion. Obviously, it is a middleman, but the individual tools are used as transparently as possible.
Give better errors when Optimus assets are missing.
Add option to powerpack.markdown/render-html
to control which Flexmark
extensions are used.
Fix bug where using (powerpack.markdown/render-html md)
as the body of a page
would cause the page to be rendered as EDN.
Add :optimus/options
Change hiccup rendering to chassis.
NB! Potentially breaking change. It breaks my heart, but this change may cause breakage for existing sites. In particular, this change makes Powerpack HTML escape content by default. In prior versions, Powerpack would write any text content verbatim to the generated HTML. This is an unsafe default, which is why this version changes it to be escaped by default.
To render strings unescaped, use powerpack.hiccup/unescape
.
:m1p/k
.read-string
.og:image:width
and og:image:height
from the file
image dimensions. Instead, control these using the :open-graph/image-width
and :open-graph/image-height
attributes on the page entity.<link rel="canonical">
Dev-mode improvements:
Bug fix: Don't try to read external open graph image URLs from disk Bug fix: Fix a case where cross-referencing the same entity in multiple files would sometimes cause the entity to "split". Bug fix: Set Optimus' asset cache to 150ms (down from 250ms)
Fix a bug where Powerpack's internal use of Prism during development would interfere with your version of Prism.
Removed the clygments
dependency - it was only used for an undocumented
feature which is not ready for public consumption.
Automatically reboot Powerpack when the main config is updated.
Add :powerpack/on-ingested
to the public API.
Initial release
Copyright © 2023-2024 Christian Johansen and Magnar Sveen
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close