webmentions 0.1.10
A general-purpose library to add Webmentions support to your website
Requires Python: >=3.8
Keywords: webmentions, microformats, indieweb, federation

![Webmentions](https://s3.fabiomanganiello.com/fabio/img/webmentions.png)

[![build](https://github.com/blacklight/webmentions/actions/workflows/build.yml/badge.svg)](https://github.com/blacklight/webmentions/actions/workflows/build.yml)
[![Coverage Badge](https://app.codacy.com/project/badge/Coverage/80a5b14c9beb4680a02477c7bd5a3df3)](https://app.codacy.com/gh/blacklight/webmentions/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/80a5b14c9beb4680a02477c7bd5a3df3)](https://app.codacy.com/gh/blacklight/webmentions/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)

[📖 Full reference](https://docs.webmentions.work)


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

This library implements support for
[Webmentions](https://www.w3.org/TR/2016/WD-webmention-20160112/), 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:

```bash
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](https://git.platypush.tech/blacklight/webmentions/src/branch/main/src/python/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

```bash
pip install "webmentions[db,fastapi]"
```

```python
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

```bash
pip install "webmentions[db,flask]"
```

```python
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:

```python
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.

```python
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:

```bash
pip install "webmentions[file]"
```

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

```python
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.

You can also pass a `throttle_seconds` parameter to `FileSystemMonitor` to
limit the rate at which Webmentions are processed upon file updates (default:
10 seconds).

#### 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:

```python
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 custom handlers 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.

```python
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:

```python
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](https://www.w3schools.com/html/html5_semantic_elements.asp)
and the [microformats2](https://microformats.org/wiki/microformats2) specification,
in particular the [`h-card`](https://microformats.org/wiki/h-card) and
[`h-entry`](https://microformats.org/wiki/h-entry) elements.

```html
<!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>
```

A more detailed (but not exhaustive, as the Microformats2 specification is quite flexible) example of a page that
works well with Webmentions is available under
[semantic-mention-example.html](./src/python/examples/semantic-mention-example.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.

## Rendering Webmentions

You can retrieve Webmentions for a given URL either by:

- Directly calling `WebmentionsHandler.retrieve_webmentions(resource, direction)` to get a list of
  `Webmention` objects (for example `handler.retrieve_webmentions("https://example.com/posts/example-post",
  WebmentionDirection.IN)` will return all the incoming Webmentions for the provided URL).

- If you registered a `GET /webmentions` endpoint (either through `bind_webmentions` or by implementing it yourself),
  you can call that endpoint with the `resource` and `direction` parameters to get a JSON response with the
  Webmentions for that URL.

The library provides a
[`WebmentionsHandler.render_webmentions()`](https://docs.webmentions.work/api/webmentions.handlers.html#webmentions.handlers.WebmentionsHandler.render_webmentions)
helper that automatically converts the Webmentions into a safe `Markup` element that you can import and render into your
page.

The default template should probably suffice for most of the cases.

Minimal example (using FastAPI and Jinja2):

```python
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from webmentions import WebmentionsHandler
from webmentions.server.adapters.fastapi import bind_webmentions

base_url = "https://example.com"
app = FastAPI()
handler = WebmentionsHandler(...)
bind_webmentions(app, handler)

# ...

@app.get("/articles/{article_id}")
def article(request, article_id: int):
    templates = Jinja2Templates(directory="templates")
    mentions = handler.retrieve_webmentions(
      f"{base_url}/articles/{article_id}",
      WebmentionDirection.IN,
    )

    rendered_mentions = handler.render_webmentions(mentions)
    return templates.TemplateResponse(
      "article.html",
      {
        "request": request,
        "article_id": article_id,
        "mentions": rendered_mentions,
      },
    )
```

Then your `article.html` template should look something like this:

```html
<!doctype html>
<html>
  <head>
    <title>Example article</title>
  </head>
  <body>
    <main>
      <article class="h-entry">
        <h1 class="p-name">Example article</h1>
        <time class="dt-published" datetime="2026-02-07T21:03:00+01:00">
          Feb 7, 2026
        </time>
        <div class="e-content">
          <p>Your article content goes here.</p>
        </div>
      </article>

      {{ mentions }}
    </div>
  </body>
</html>
```

This will render `mentions` directly in your template, with some theme-aware
reasonable styling.

An [example and self-documented Jinja
template](./src/python/examples/mention-render-jinja-example.html) is also
available if you want to customize the rendering of Webmentions in your pages.

## Microformats support

When processing incoming Webmentions, this library will try to parse the source
document using [microformats2](https://microformats.org/wiki/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`:

```python
mention.to_dict()["metadata"]
```

```json
{
  "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`):

```json
{
  "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

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

## Development

Install development dependencies and enable pre-commit hooks:

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

Run all hooks manually:

```bash
pre-commit run --all-files
```