A general-purpose library to add Webmentions support to your website
  • Python 99.9%
  • Makefile 0.1%
Find a file
Fabio Manganiello d12a59c0ff
Some checks are pending
build / build (push) Waiting to run
coverage / coverage (push) Waiting to run
publish-docs / publish-docs (push) Waiting to run
publish-pypi / publish (push) Waiting to run
Bump version: 0.1.6 → 0.1.7
2026-02-09 01:24:51 +01:00
.github/workflows ci(docs): Added workflow to rebuild and publish docs on new version 2026-02-08 22:08:58 +01:00
docs docs(index): Added link to the repo (and its mirror) 2026-02-08 23:51:32 +01:00
src/python Bump version: 0.1.6 → 0.1.7 2026-02-09 01:24:51 +01:00
tests/python feat(file-monitor): A more consistent start/stop API + context manager 2026-02-09 01:00:14 +01:00
.flake8 build(lint): Added black and flake8 pre-commit hooks 2026-02-08 23:39:33 +01:00
.gitignore docs(sphinx): Added auto-generated Sphinx documentation under docs/ 2026-02-08 22:08:32 +01:00
.pre-commit-config.yaml build(lint): Added black and flake8 pre-commit hooks 2026-02-08 23:39:33 +01:00
CHANGELOG.md Bump version: 0.1.6 → 0.1.7 2026-02-09 01:24:51 +01:00
LICENSE Initial commit 2026-02-05 00:33:45 +01:00
Makefile docs(sphinx): Added auto-generated Sphinx documentation under docs/ 2026-02-08 22:08:32 +01:00
pyproject.toml Bump version: 0.1.6 → 0.1.7 2026-02-09 01:24:51 +01:00
README.md feat(file-monitor): A more consistent start/stop API + context manager 2026-02-09 01:00:14 +01:00

Webmentions

build Coverage Badge Codacy Badge

📖 Full reference

A general-purpose library to add Webmentions support to your website.

This library implements support for Webmentions, a W3C recommendation for federated mentions, comments and reactions on the Web.

The underlying mechanism of Webmentions is relatively simple:

  • Alice publishes an article A1 on her blog A

  • Bob reads A1 and mentions it in an article B1 on his blog B

  • If both A and B support Webmentions, then:

    • A exposes an HTTP endpoint to receive mention notifications, and that endpoint is advertised on A1 as a rel="webmention" link in the page or a Link header in the HTTP response.

    • When Bob saves B1, its blog will scan for links in the page, discover which services expose a Webmention endpoint, and send a Webmention to A to notify them of the mention.

    • A will receive the notification and, if it accepts comments from B, the comment or the reaction will be rendered as a response on A1.

Installation

For the base installation:

pip install webmentions

Usage

This library provides the bindings to send and process Webmentions. It is agnostic about how the Webmentions are stored and rendered, but it provides a few helpers for common frameworks.

Some examples are provided under the examples directory.

Receiving Webmentions

If you use a framework with officially supported bindings (FastAPI or Flask) then the bind_webmentions API is available to easily bind the Webmentions handler to your app, which provides:

  • A POST /webmentions endpoint to receive Webmentions
  • A GET /webmentions endpoint to return stored Webmentions (accepts resource and direction parameters)
  • A Link header to advertise the Webmention endpoint on all the text/* responses

SQLAlchemy + FastAPI

pip install "webmentions[db,fastapi]"
from fastapi import FastAPI
from webmentions import WebmentionsHandler
from webmentions.storage.adapters.db import init_db_storage
from webmentions.server.adapters.fastapi import bind_webmentions

app = FastAPI()

# Replace this with your own database URL, or an existing engine.
# Extra arguments passed to init_db_storage will be passed to create_engine
storage = init_db_storage(engine="sqlite:////tmp/webmentions.db")

# Your Webmentions handler
handler = WebmentionsHandler(
    storage=storage,
    # This should match the public base URL of your site
    base_url="https://example.com",
)

# ...Initialize your Web app here...

# Bind Webmentions to your FastAPI app
bind_webmentions(app, handler)

SQLAlchemy + Flask

pip install "webmentions[db,flask]"
from flask import Flask
from webmentions import WebmentionsHandler
from webmentions.storage.adapters.db import init_db_storage
from webmentions.server.adapters.flask import bind_webmentions

app = Flask(__name__)

# Replace this with your own database URL, or an existing engine.
# Extra arguments passed to init_db_storage will be passed to create_engine
storage = init_db_storage(engine="sqlite:////tmp/webmentions.db")

# Your Webmentions handler
handler = WebmentionsHandler(
    storage=storage,
    # This should match the public base URL of your site
    base_url="https://example.com",
)

# ...Initialize your Web app here...

# Bind Webmentions to your Flask app
bind_webmentions(app, handler)

Generic setup

If you use neither FastAPI nor Flask, you can use Webmentions by modifying your application as it follows:

  • Create a WebmentionsHandler with an attached WebmentionsStorage

  • Expose a POST endpoint for Webmentions that calls WebmentionsHandler.process_incoming_webmentions(source, target) when invoked, where source and target are the URLs of the source and the target of the Webmention, respectively, and they are passed as form parameters.

  • Ensure that all the pages that support Webmentions advertise the endpoint either by adding a Link header or by adding a rel="webmention" link to the body.

  • (Optional) Expose a GET endpoint to fetch all the stored Webmentions given a resource URL and a direction.

Example with a FastAPI app:

from fastapi import FastAPI, Form, HTTPException
from fastapi.templating import Jinja2Templates
from webmentions import WebmentionDirection, WebmentionsHandler
from webmentions.storage.adapters.db import init_db_storage

app = FastAPI()
base_url = "https://example.com"
templates = Jinja2Templates(directory="/path/to/your/templates")
handler = WebmentionsHandler(
    storage=init_db_storage(engine="sqlite:////tmp/webmentions.db"),
    base_url=base_url,
)


# Route to receive Webmentions
@app.post("/webmentions")
def webmentions(
    source: str | None = Form(default=None),
    target: str | None = Form(default=None),
):
    try:
        # Forward the Webmention to the handler
        handler.process_incoming_webmention(source, target)
    except WebmentionException as e:
        raise HTTPException(status_code=400, detail=str(e)) from e

    return {"status": "ok"}


# Route to fetch Webmentions
@app.get("/webmentions")
def webmentions(resource: str, direction: str):
    try:
        return [
            mention.to_dict()
            for mention in handler.retrieve_webmentions(
                resource,
                direction=WebmentionDirection.from_raw(direction),
            )
        ]
    except (WebmentionException, ValueError) as e:
        raise HTTPException(status_code=400, detail=str(e)) from e


# Route that serves your static pages
@app.get("/article/<path:path>")
def serve_page(path: str):
    response = templates.TemplateResponse(path, {"request": request})
    # Add a Link header to advertise the Webmention endpoint
    response.headers["Link"] = f'<{base_url}/webmentions>; rel="webmention"'
    return response

Generic storage

If you don't want to use sqlalchemy as a backend to store Webmentions, you can extend WebmentionsStorage with your own implementation and bind it to the WebmentionsHandler instance.

from webmentions import (
    Webmention,
    WebmentionDirection,
    WebmentionsHandler,
    WebmentionsStorage,
)


class MyWebmentionsStorage(WebmentionsStorage):
    def store_webmention(self, mention: Webmention):
        ...

    def delete_webmention(
        self,
        source: str,
        target: str,
        direction: WebmentionDirection,
    ):
        ...

    def retrieve_webmentions(
        self, resource: str, direction: WebmentionDirection
    ) -> list[Webmention]:
        """
        Retrieve the stored Webmentions for a given URL.

        :param resource: The URL of the resource associated to the Webmentions
        :param direction: The direction of the Webmentions (inbound or outbound)
        :return: A list of Webmentions
        """

...

storage = MyWebmentionsStorage(...)
handler = WebmentionsHandler(
    storage=storage,
    base_url="https://example.com",
)

Sending Webmentions

Filesystem monitor

If your pages are served as static files from a folder, you can use the FileSystemMonitor to automatically send Webmentions when files are added or modified.

First install webmentions with the file extra:

pip install "webmentions[file]"

Then add the following code to your app (FastAPI example):

import os

from webmentions import WebmentionsHandler
from webmentions.storage.adapters.db import init_db_storage
from webmentions.storage.adapters.file import FileSystemMonitor

app = FastAPI()
base_url = "https://example.com"
static_dir = "/srv/html/articles"
handler = WebmentionsHandler(
    storage=init_db_storage(engine="sqlite:////tmp/webmentions.db"),
    base_url=base_url,
)

# ...Initialize your Web app and bind the Webmentions handler...

# A function that takes a path to a created/modified/deleted text/* file
# and maps it to a URL on the Web server to be used as the Webmention source
def path_to_url(path: str) -> str:
    # Convert path (absolute) to a path relative to static_dir
    # and drop the extension.
    # For example, /srv/http/articles/2022/01/01/article.md
    # becomes /2022/01/01/article
    path = os.path.relpath(path, static_dir).rsplit(".", 1)[0].lstrip("/")
    # Convert the path to a URL on the Web server
    # For example, /2022/01/01/article
    # becomes https://example.com/articles/2022/01/01/article
    return f"{base_url.rstrip('/')}/articles/{path}"


# Create and start the filesystem monitor before running your app
with FileSystemMonitor(
    # This should match the base path of your static files
    root_dir=static_dir,
    handler=handler,
    file_to_url_mapper=path_to_url,
) as monitor:
  # Start the monitor before running your app
  app.run(...)

Now every time a text/* file is created, modified or deleted (supported: text, Markdown and HTML), or you run a git pull to update your static files, Webmentions for updated resources will be automatically sent to any Web server in the mentioned links that supports Webmentions.

Generic setup

If you don't store your articles or posts as static files (e.g. a database), you can still send Webmentions by explicitly calling the WebmentionsHandler.process_outgoing_webmentions whenever there are updates to your content.

For example:

from webmentions import ContentTextFormat, WebmentionsHandler

from myapp.storage import db

handler = WebmentionsHandler(...)

def save_post(post):
    db.save_post(post)

    # Note: This will process both added, updated or deleted mentions
    # in the provided post
    handler.process_outgoing_webmentions(
      source_url=post.url,
      # Optional. If not passed then the source content will be fetched
      # from the provided source_url. Otherwise, all links will be parsed
      # from the provided text. Plain text, Markdown or HTML are supported
      text=post.text,
      # Optional. If not passed then the source content type is inferred
      # either from the provided text or the source_url
      content_type=ContentTextFormat.MARKDOWN,
    )


def delete_post(post):
    db.delete_post(post)
    handler.process_outgoing_webmentions(
        source_url=post.url,
        # Pass an empty text to make sure that no links are parsed from cached
        # versions
        text="",
    )

Once process_outgoing_webmentions is hooked to your content modifications, your Webmentions will be automatically sent whenever there are updates to your pages.

Add notifications to mentions

You may want to add your custom callbacks when a Webmention is sent or received - for example to send notifications to your users when some of their content is mentioned, or to keep track of the number of mentions sent by your pages, or to perform any automated moderation or filtering when mentions are processed etc.

from webmentions import Webmention, WebmentionDirection, WebmentionsHandler
from webmentions.storage.adapters.db import init_db_storage

base_url = "https://example.com"


def on_mention_processed(mention: Webmention):
    if mention.direction == WebmentionDirection.IN:
        print(
            f"Processed incoming Webmention from {mention.source} to {mention.target}"
        )
    else:
        print(
            f"Processed outgoing Webmention from {mention.source} to {mention.target}"
        )


def on_mention_deleted(mention: Webmention):
    if mention.direction == WebmentionDirection.IN:
        print(f"Deleted incoming Webmention from {mention.source} to {mention.target}")
    else:
        print(f"Deleted outgoing Webmention from {mention.source} to {mention.target}")


handler = WebmentionsHandler(
    storage=init_db_storage(engine="sqlite:////tmp/webmentions.db"),
    base_url=base_url,
    on_mention_processed=on_mention_processed,
    on_mention_deleted=on_mention_deleted,
)

Filtering and moderation

By default any Webmention sent to your server will be processed if it matches a target on your server.

You can change this behaviour by specifying initial_mention_status on your Webmentions handler (supported: WebmentionStatus.CONFIRMED, WebmentionStatus.PENDING, WebmentionStatus.DELETED).

Note that if you pass PENDING as an initial status, you will need to explicitly approve the Webmentions to make them visible to your users.

You have two options:

  • Manually set the status flag for the Webmention on your storage
  • Pass an on_mention_processed callback to your Webmentions handler that will perform any moderation or filtering. For example:
from webmentions import (
    Webmention,
    WebmentionDirection,
    WebmentionStatus,
    WebmentionsHandler,
)
from webmentions.storage.adapters.db import init_db_storage

base_url = "https://example.com"


def on_mention_processed(mention: Webmention):
    # Don't do anything for outgoing mentions
    if mention.direction == WebmentionDirection.OUT:
        return

    # Delete Webmentions coming from notorious spam domains or authors
    if mention.direction == WebmentionDirection.IN:
        if (
            mention.source in ["https://example.com/spam"] or
            mention.author_name in ["Spam Author"]
        ):
            mention.status = WebmentionStatus.DELETED
        # Otherwise, confirm the Webmention
        else:
            mention.status = WebmentionStatus.CONFIRMED

    # Save the modified mention
    handler.storage.store_webmention(mention)


handler = WebmentionsHandler(
    storage=init_db_storage(engine="sqlite:////tmp/webmentions.db"),
    base_url=base_url,
    on_mention_processed=on_mention_processed,
    initial_mention_status=WebmentionStatus.PENDING,
)

Optimize your pages for Webmentions

The Webmentions recommendation is intentionally simple, and most of the implementations only require a source and target URL to be sent to the Webmentions endpoint.

It is then usually on the Web server that receives the notification to parse any additional metadata from the source URL, such as the title of the article, the author, the original publication date, etc.

You can add this information to your pages that support Webmentions by following the W3C recommendations for Semantic Elements and the microformats2 specification, in particular the h-card and h-entry elements.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- Advertise the Webmention endpoint -->
    <link rel="webmention" href="https://example.com/webmentions" />

    <title>Example post</title>
  </head>

  <body>
    <header>
      <!-- Author/Publisher (microformats2 h-card) -->
      <div class="h-card">
        <a class="u-url p-name" href="https://example.com">Example Author</a>
        <img class="u-photo" alt="Example Author"
             src="https://example.com/avatar.jpg" />
      </div>
    </header>

    <main>
      <!-- Post (microformats2 h-entry) -->
      <article class="h-entry">
        <h1 class="p-name">Example post</h1>
        <a class="u-url u-uid" href="https://example.com/posts/example-post">
          Permalink
        </a>
        <time class="dt-published" datetime="2026-02-07T21:03:00+01:00">
          Feb 7, 2026
        </time>
        <div class="e-content">
          <p>
            This post mentions
            <a class="u-in-reply-to" href="https://target.example.org/post/123">a target URL</a>.
            <!--
              Other supported options besides u-in-reply-to include:

                - u-like-of (for likes)
                - u-repost-of (for reposts)
                - p-rsvp (for RSVPs, supports YES, MAYBE, NO and INTERESTED)
                - p-location (for location mentions, see https://microformats.org/wiki/h-geo)

              See https://microformats.org/wiki/h-entry#Properties for a full list,
              including proposed extensions.

              Both u-like-of and u-repost-of support an optional embedded h-cite
              for more granular details about the referenced content (e.g.
              author, title or published date)
            -->
          </p>
        </div>
      </article>
    </main>
  </body>
</html>

If you render your pages following these specifications then any mention on target URLs that support Webmention will automatically include rich information such as author details, media elements, likes/reposts, location, etc.

Microformats support

When processing incoming Webmentions, this library will try to parse the source document using microformats2.

If an h-entry is found, common h-entry properties are mapped to the main Webmention fields (e.g. title, published, content, author details). Any additional microformats2 data is stored under Webmention.metadata, so storage backends and renderers can use it without requiring changes to the core model.

In particular:

  • metadata["mf2"] contains the parsed h-entry details used by the receiver (response type properties, categories, syndication links, etc.).
  • metadata["comments"] (when present) contains nested comments discovered via the comment property (e.g. u-comment h-cite patterns).

Example: a like received via an h-entry with u-like-of:

mention.to_dict()["metadata"]
{
  "mf2": {
    "type": ["h-entry"],
    "url": "https://alice.example/posts/1",
    "uid": null,
    "category": [],
    "syndication": ["https://social.example/alice/123"],
    "rsvp": null,
    "bookmark_of": [],
    "like_of": ["https://example.com/posts/target"],
    "repost_of": [],
    "in_reply_to": [],
    "follow_of": [],
    "quotation_of": []
  }
}

Example: a Webmention containing a nested comment (u-comment h-cite):

{
  "mf2": {
    "type": ["h-entry"],
    "url": "https://jane.example/posts/reply-1",
    "uid": null,
    "category": [],
    "syndication": [],
    "rsvp": null,
    "bookmark_of": [],
    "like_of": [],
    "repost_of": [],
    "in_reply_to": ["https://example.com/posts/target"],
    "follow_of": [],
    "quotation_of": []
  },
  "comments": [
    {
      "type": ["h-cite"],
      "name": null,
      "url": "https://jane.example/comments/c12",
      "published": "2026-02-07T01:02:03+00:00",
      "content": "Nice post",
      "author": {
        "name": "Jane",
        "url": "https://jane.example",
        "photo": "https://jane.example/jane.jpg"
      }
    }
  ]
}

Tests

pip install -e ".[tests]"
pytest tests

Development

Install development dependencies and enable pre-commit hooks:

pip install -e ".[dev]"
pre-commit install

Run all hooks manually:

pre-commit run --all-files