This is a Clojure and ClojureScript implementation of Webmention, the W3C standard for telling a page that another page links to it, e.g. a reply on one blog to a post on another. It covers both roles of the standard, the sender and the receiver, and reads what you need to show a mention: its kind, its author and its content.
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 sends, receives and shows its Webmentions. 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/webmention-clj {:mvn/version "0.1.0"}
For changes that aren't released yet, use the SHA of the latest commit on
master instead:
dk.simongray/webmention-clj
{:git/url "https://github.com/simongray/webmention-clj"
:git/sha "…"}
For ClojureScript, shadow-cljs only reads Git dependencies from
deps.edn, so also set :deps true in your shadow-cljs.edn.
When you publish a page, send a Webmention to each page that it links
to. The send! function finds the endpoint of the target and posts the
Webmention to it:
(require '[dk.simongray.webmention :as webmention])
(webmention/send! "https://simon.grays.blog/posts/2026/hello"
"https://webmention.rocks/test/1")
;; => {:source "https://simon.grays.blog/posts/2026/hello"
;; :target "https://webmention.rocks/test/1"
;; :endpoint "https://webmention.rocks/test/1/webmention"
;; :outcome :sent
;; :status 202}
The :outcome is :sent, :no-endpoint or :failed. To send to every
link of a page, give its HTML or Hiccup to links, leave out the links
to your own site, and send them all with send-all!:
(require '[clojure.string :as str])
(def post
"https://simon.grays.blog/posts/2026/hello")
(def html
"<p>Hi <a href='https://webmention.rocks/test/1'>there</a>")
(def targets
(->> (webmention/links html post)
(remove #(str/starts-with? % "https://simon.grays.blog/"))))
(webmention/send-all! post targets)
Keep the targets that a Webmention was sent to. When you update or delete the page, send its Webmentions again to those targets as well as to the links it has now, so that each target can update or remove the mention it shows. A deleted page should answer 410 Gone.
An endpoint is a Ring handler made from a predicate that says which targets take Webmentions, and a function of yours that runs its jobs, e.g. on a thread pool or from a queue in a database:
(def mentions
(atom {}))
(defn keep-mention!
"Verify the source of the Webmention `job`, and keep or delete its
mention."
[job]
(let [{:keys [source target outcome] :as report} (webmention/verify! job)]
(case outcome
:verified (swap! mentions assoc [source target] report)
(:unlinked :gone) (swap! mentions dissoc [source target])
:failed nil)))
(def endpoint
(webmention/handler
{:target-pred #(str/starts-with? % "https://simon.grays.blog/posts/")
:schedule! #(future (keep-mention! %))}))
Name the endpoint in a Link header of each page that takes Webmentions, or in its head:
<link rel="webmention" href="https://simon.grays.blog/webmention">
The handler checks each Webmention at once and answers 202, or 400 with
the reason in plain text. For each Webmention it takes, it gives
:schedule! a job, a map of the :source and the :target. Then
verify! fetches the source and reports whether it links the target.
When the source is an HTML page that links the target, the report also
has its :mf2, its microformats as
microformats-clj parses
them. A sender sends the same Webmention again when the source changes,
so keep one mention for each source and target.
The handler answers 201 with the URL of a status page when you give it a
:status-url-fn. With a :report! in place of :schedule!, it
verifies the source before it answers, which the standard allows but
doesn't recommend. To take Webmentions from a form on your own page,
wrap the handler, e.g. to send a browser back to the page after a 202,
or to show the reason for a 400 on a page of your own.
The mention/summary function reads what you need to show a mention
from the report of a verified source: its kind, by
Post Type Discovery, its
author, by the authorship algorithm,
and its content:
(require '[dk.simongray.webmention.mention :as mention])
(mention/summary report)
;; => {:source "https://jane.example/notes/2026/10/1"
;; :target "https://simon.grays.blog/posts/2026/hello"
;; :kind :reply
;; :url "https://jane.example/notes/2026/10/1"
;; :author {:name "Jane Doe" :url "https://jane.example/"}
;; :published "2026-10-11T12:00:00+02:00"
;; :content {:text "Nice post!"
;; :hiccup ([:p {}
;; "Nice "
;; [:a {:href "https://simon.grays.blog/…"
;; :rel "nofollow ugc"}
;; "post"]
;; "!"])}
;; :in-reply-to ["https://simon.grays.blog/posts/2026/hello"]}
The :kind is :reply, :like, :repost, :bookmark, :rsvp or
:mention. Link a mention by its :url rather than its source: when a
bridge such as Bridgy sends a Webmention for a post elsewhere, the
source is a page of the bridge.
The :hiccup of the content is sanitized by
html-pieces, with its
headings as paragraphs, its images as their alt text, and
rel="nofollow ugc" on its links. To show it in another way, give
summary an :element-fn of your own. Either way, render it with
something that escapes text, e.g. Replicant or hiccup2.core/html. For
an excerpt rather than the whole content, e.g. of a reply, give
summary a :max-length, and see its docstring for the other options.
When a page names its author by a URL alone, e.g. with
<a class="u-author" href="https://jane.example/">, the :author has
only that :url, and author! fetches the author's page for its
representative h-card:
(mention/author! report)
;; => {:name "Jane Doe"
;; :url "https://jane.example/"
;; :photo "https://jane.example/me.jpg"}
The mention/representative-card function gives the h-card of any page
whose microformats you've parsed, e.g. the homepage of someone who signs
in with their domain.
The requests of discover!, send!, verify! and mention/author!
go through http/send-public! of
wary-fetch, which only sends
to the public internet. To send them with another HTTP client, give a
:send function of your own. The options that have defaults, e.g. the
limits of each request, are in webmention/default-options, and the
docstrings describe the others.
:send of your own..cljc and depends on
wary-fetch for requests,
html-pieces for pages and
microformats-clj for
their microformats.clojure -X:test # the tests on the JVM
clojure -X:test:rocks # the discovery tests of webmention.rocks
npm install # once, for the Node tests
clojure -M:cljs compile test # the tests in Node
On the JVM, a test also runs a sender and a receiver against each other, and against the scenarios of webmention.rocks, over HTTP on localhost. The tests of webmention.rocks that need a public URL are run by hand, as doc/webmention-rocks.md describes.
The webmention-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 |