Liking cljdoc? Tell your friends :D

webmention-clj

Clojars Project

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 whole standard. The code follows the Recommendation of 12 January 2017 and cites the section of each rule it implements. Its tests include the scenarios of the webmention.rocks test suite, run locally.
  • Plain data. Requests and reports are maps, any HTTP client can send the requests, and the endpoint is a Ring handler.
  • Nothing in the background. Each Webmention that the endpoint takes becomes a job, a plain map, that you run when and where you like.
  • Safe by default. Requests only go to the public internet and have limits on time and size, the endpoint never fetches a source while it answers, and the content of a mention is sanitized.

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.

Getting started

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.

Send

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.

Receive

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.

Show a mention

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.

Options

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.

Principles

  • Standards first. The code follows the Recommendation and cites its sections, and a comment marks each choice of its own. Where the Recommendation is unclear, the code follows the editor's draft and the discussions in its issues.
  • Plain data. Requests, jobs and reports are maps, and every function that sends takes a :send of your own.
  • One codebase. The library is written in .cljc and depends on wary-fetch for requests, html-pieces for pages and microformats-clj for their microformats.

Development

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.

License

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

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close