This is a Clojure and ClojureScript implementation of Micropub, the W3C standard with which apps post to a website, e.g. a note from your phone or a photo from a desktop app. It covers both sides: the endpoint and the media endpoint of a site, and a client that posts to them.
The same code runs on the JVM, in Node and in the browser.
This library was spun out of indieblog, which runs simon.grays.blog and takes posts from Micropub clients. It was developed with assistance from frontier LLMs.
It requires Clojure 1.11+ and Java 11+. For the latest release, add it
from Clojars to the
:deps in your deps.edn:
dk.simongray/micropub-clj {:mvn/version "0.3.1"}
For changes that aren't released yet, use the SHA of the latest commit on
master instead:
dk.simongray/micropub-clj
{:git/url "https://github.com/simongray/micropub-clj"
:git/sha "…"}
For ClojureScript, shadow-cljs only reads Git dependencies from
deps.edn, so also set :deps true in your shadow-cljs.edn.
An endpoint is a Ring handler. Give it the access tokens of an IndieAuth server and functions that store your posts:
(require '[dk.simongray.micropub.server :as server])
(def posts
(atom {}))
(defn create!
"Store the entry of the create `action` and return the new post's URL."
[{:keys [type properties] :as action}]
(let [url (str "https://simon.grays.blog/posts/" (inc (count @posts)))]
(swap! posts assoc url {:type type :properties properties})
{:url url}))
(def opts
{:me-pred #{"https://simon.grays.blog/"}
:tokens tokens
:create! create!
:source (fn [{:keys [url]}] (get @posts url))
:update! (fn [{:keys [url entry]}] (swap! posts assoc url entry) nil)
:delete! (fn [{:keys [url]}] (swap! posts dissoc url) nil)})
(def endpoint
(server/handler opts))
The :tokens are the token store of an indieauth-clj server in the same
process. For a server elsewhere, give its :introspection-endpoint and a
:resource-token instead. The :me-pred says whose tokens may post.
The handler checks the token and its scope, then calls your function
with an action like this one, plus the :token-info and the
:ring-request:
{:action :create
:type ["h-entry"]
:properties {:content ["Hello"]
:category ["indieweb" "clojure"]}
:commands {:mp-slug ["hello"]}}
A :create! returns the new post's :url, and the handler answers 201.
For an update, the handler reads the post with :source, applies the
update with micropub/apply-update and passes the result to :update!
as the :entry. That way you only map entries to your posts and back,
and the same :source answers clients that edit posts.
To refuse an action, return an error such as {:error {:code "invalid_request" :reason "No post at that URL"}}. The handler answers
with Micropub's JSON.
Name the endpoint in a Link header of your home page, or in its head:
<link rel="micropub" href="https://simon.grays.blog/micropub">
Clients upload photos and other files to a media endpoint. Make one from
the same options and an :upload! that stores a file and returns its
URL:
(require '[clojure.java.io :as io])
(defn upload!
"Store the `file` under a random name and return its URL."
[{:keys [tempfile] :as file}]
(let [name (str (random-uuid))]
(io/copy tempfile (io/file "media" name))
(str "https://simon.grays.blog/media/" name)))
(def media-endpoint
(server/media-handler (assoc opts :upload! upload!)))
Clients find it in q=config, so give its URL to the Micropub endpoint as
the :media-endpoint. The handlers don't parse multipart bodies
themselves, so give your server multipart middleware such as Ring's
wrap-multipart-params, which keeps large files out of memory. Given an
:upload!, the Micropub endpoint also takes multipart creates. It
uploads each file with :upload! and puts the URL in the file's place.
Your functions get every property and mp- command as data, so most
extensions (e.g. post-status, visibility and mp-slug) need nothing from
the library. Put the post types and syndication targets for q=config in
:config, and answer other queries with :queries:
(assoc opts
:config {:post-types [{:type "note" :name "Note"}]
:syndicate-to []}
:queries {:category (fn [query] {:categories ["indieweb" "clojure"]})})
A token with only the draft scope can create and update drafts, as the
draft scope extension
proposes. The other defaults, e.g. the scopes of each action, are in
server/default-options.
Find a site's endpoint, and send it actions with the access token you got from IndieAuth:
(require '[dk.simongray.micropub :as micropub])
(micropub/discover! "https://simon.grays.blog/")
;; => {:status 200 :endpoint "https://simon.grays.blog/micropub"}
(micropub/call! (micropub/request "https://simon.grays.blog/micropub" token
{:type ["h-entry"]
:properties {:content ["Hello"]}}))
;; => {:status 201 :url "https://simon.grays.blog/posts/2026/hello"}
The client sends a create form-encoded when it can, since every server
accepts that, and as JSON otherwise. For a query, e.g. q=config to find
the media endpoint, use micropub/query-request. A server's error comes
back under :error, with Micropub's :code and :reason.
To post a photo, upload it to the media endpoint and put its URL in the post:
(micropub/call! (micropub/upload-request "https://simon.grays.blog/media" token
{:content jpeg-bytes
:filename "sunset.jpg"
:content-type "image/jpeg"}))
;; => {:status 201 :url "https://simon.grays.blog/assets/2026-10-13-sunset.jpg"}
A server without a media endpoint takes the files with the create
instead. Give the create its :files, e.g. {:photo [file]}, and the
client sends it as multipart.
:send you give it..cljc and depends on
wary-fetch for requests and
JSON, webcrypto-clj for
bytes, html-pieces for pages,
whatwg-url for URLs,
whatwg-mime for media types
and indieauth-clj for tokens.clojure -X:test # the tests on the JVM
npm install # once, for the Node tests
clojure -M:cljs compile test # the tests in Node
clojure -M:cljs compile browser-test # for a browser, from target/browser-test
On the JVM, a test also runs the client against the endpoint over HTTP on localhost. doc/micropub-rocks.md says which tests of micropub.rocks the tests cover.
The micropub-clj project is licensed under the MIT licence.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |