Webmentions¶
Webmention is a W3C recommendation that enables cross-site conversations. When you link to someone else’s content, you can send them a webmention to notify them. Similarly, when others link to your content, they can send you webmentions that you can display as comments, likes, or reposts.
Features¶
Receiving webmentions: Accept and validate incoming webmentions
Sending webmentions: Automatically discover and notify linked sites
Microformats2 parsing: Extract semantic content from webmentions
Spam protection: Pluggable spam checker interface
Display templates: Show webmentions as comments, likes, and reposts
Management commands: Send webmentions from the command line
Quick Start¶
Add the webmention endpoint to your base template:
{% load webmention_tags %}
<head>
{% webmention_endpoint_link %}
</head>
Display webmentions on your pages:
{% load webmention_tags %}
{% show_webmentions request.build_absolute_uri %}
Send webmentions when you publish content:
python manage.py send_webmentions https://mysite.com/new-post/
Configuration¶
Add these settings to your Django settings:
# Required: URL resolver to map URLs to content objects
INDIEWEB_URL_RESOLVER = 'myproject.webmention_config.MyURLResolver'
# Required: Spam checker (use default or implement your own)
INDIEWEB_SPAM_CHECKER = 'indieweb.interfaces.NoOpSpamChecker'
# Optional: Comment adapter to convert webmentions to comments
INDIEWEB_COMMENT_ADAPTER = 'myproject.webmention_config.MyCommentAdapter'
Implementing URL Resolution¶
Create a URL resolver to map target URLs to your content objects:
# myproject/webmention_config.py
from indieweb.interfaces import URLResolver
from myapp.models import BlogPost
from urllib.parse import urlparse
class MyURLResolver(URLResolver):
def resolve(self, target_url: str) -> Any | None:
"""Resolve a URL to a content object."""
parsed = urlparse(target_url)
path = parsed.path
# Example: match /blog/post-slug/
if path.startswith('/blog/'):
slug = path.strip('/').split('/')[-1]
try:
return BlogPost.objects.get(slug=slug)
except BlogPost.DoesNotExist:
pass
return None
def get_absolute_url(self, content_object: Any) -> str:
"""Get the absolute URL for a content object."""
if hasattr(content_object, 'get_absolute_url'):
return content_object.get_absolute_url()
return ''
Implementing Spam Protection¶
Create a custom spam checker:
from indieweb.interfaces import SpamChecker
from indieweb.models import Webmention
class MySpamChecker(SpamChecker):
def check(self, webmention: Webmention) -> dict[str, Any]:
"""Check if a webmention is spam."""
# Implement your spam detection logic
spam_keywords = ['casino', 'viagra', 'lottery']
content_lower = webmention.content.lower()
is_spam = any(kw in content_lower for kw in spam_keywords)
return {
'is_spam': is_spam,
'confidence': 0.9 if is_spam else 0.1,
'details': 'Keyword-based detection'
}
Target URL Matching¶
When receiving a Webmention, django-indieweb fetches the source page and
verifies that it links to the submitted target URL before marking the
Webmention as verified. Target verification parses HTML href attributes
from the source page and compares them with a conservative canonical URL
matching policy. The stored Webmention.source_url and
Webmention.target_url remain the submitted values; canonicalization is
used only while matching.
The same matching policy is also used when parsing microformats2 target
properties such as u-in-reply-to, u-like-of, and u-repost-of, so
reply/like/repost classification still works when the source page uses a
common URL variant.
Supported matching variants:
URL fragments are ignored, so
https://mysite.com/post#commentsmatcheshttps://mysite.com/post.URL scheme and host case are ignored, while path case remains significant.
A leading
www.hostname is treated as equivalent to the bare hostname.One trailing slash on non-root paths is treated as equivalent.
Query parameters are compared independent of order. Duplicate query key/value pairs are preserved and must still match.
The receiver does not resolve relative source links during target verification,
does not follow redirects as part of this matching step, and does not broaden
endpoint domain validation. The submitted target must still pass the
Webmention endpoint’s domain check before processing begins. Userinfo and
explicit ports, if present, must match exactly; default ports are not
normalized away.
HTTP Redirects¶
django-indieweb follows HTTP redirects explicitly for Webmention network
requests. Redirect handling is bounded to 5 redirects per request and only
continues to http and https URLs. Redirect chains that exceed the
limit, redirect to another scheme, loop until the limit is reached, or raise a
network/client error are treated as request failures.
Receive-side source fetches follow redirects before validating the source
document. The submitted Webmention.source_url and Webmention.target_url
are preserved exactly as submitted, but the final fetched source URL is used as
the microformats2 parsing base URL. This means relative author and photo URLs
from a redirected source page resolve against the page that actually returned
the HTML.
After redirects, the receive-side source response must still be HTTP 200
with a text/html content type and must link to the submitted target under
the target matching policy above. A final 410 Gone, non-200 response,
non-HTML response, missing target link, unsupported redirect, or excessive
redirect chain marks the Webmention failed through the same failed-state
path described below.
Send-side endpoint discovery follows redirects for both the initial HEAD
request and the fallback GET request. Relative Webmention endpoints found
in Link headers or HTML <link rel="webmention"> / <a
rel="webmention"> elements are resolved against the final target page URL
after redirects, not the originally requested URL.
When sending outgoing Webmentions, source-content fetches also follow the same
bounded redirect policy. Endpoint delivery POST requests follow redirects
with the original POST method and source/target form payload
preserved for every followed redirect status (301, 302, 303,
307, and 308). The final endpoint response is considered successful
only when it returns HTTP 200, 201, or 202; redirect errors return
the existing {"success": False, ...} result shape.
Reprocessing and Source Removal¶
Incoming Webmentions are keyed by the submitted source and target URLs.
When the same pair is processed again, django-indieweb updates the existing
Webmention row rather than creating a duplicate.
If a previously verified source returns 410 Gone during reprocessing, the
row is marked failed and verified_at is cleared. The submitted
source_url and target_url are preserved, and previously parsed fields
such as author, content, HTML content, mention type, and published date are
left intact so applications can keep historical display or moderation context.
The same failed-state update is used when a source fetch succeeds with
text/html but the source page no longer links to the submitted target. This
represents a removed Webmention: it is no longer considered currently verified,
but the stored parsed fields are not destructively cleared.
Other processing failures, including non-200 responses, non-HTML responses,
and fetch errors, also mark the Webmention failed and clear verified_at.
Those failures are not treated as explicit source-removal signals in the
documentation because they may be transient. If the source later returns valid
HTML that links to the target again, reprocessing can mark the existing row
verified and assign a fresh verified_at timestamp.
Spam reclassification also clears verified_at. The source may still link to
the target, but a spam row is not advertised as currently verified, and
previously parsed author, content, published, and mention-type fields are
preserved.
Template Usage¶
Basic Usage¶
{% load webmention_tags %}
{# Add endpoint discovery to your base template #}
{% webmention_endpoint_link %}
{# Show all webmentions for current page #}
{% show_webmentions request.build_absolute_uri %}
{# Show only replies #}
{% show_webmentions request.build_absolute_uri mention_type="reply" %}
{# Get webmention count #}
{% webmention_count request.build_absolute_uri as count %}
<p>This post has {{ count }} responses.</p>
Custom Templates¶
You can override the default templates by creating your own:
indieweb/webmentions.html- Main containerindieweb/webmention_types/like.html- Like templateindieweb/webmention_types/reply.html- Reply templateindieweb/webmention_types/repost.html- Repost templateindieweb/webmention_types/mention.html- Generic mention template
Management Commands¶
send_webmentions¶
Send webmentions for all links in a post:
# Send webmentions for a URL
python manage.py send_webmentions https://mysite.com/new-post/
# Provide content directly
python manage.py send_webmentions https://mysite.com/new-post/ \
--content '<p>Check out <a href="https://example.com">this site</a>!</p>'
# Dry run to see what would be sent
python manage.py send_webmentions https://mysite.com/new-post/ --dry-run
Signals¶
The webmention_received signal is sent when a webmention is processed:
from django.dispatch import receiver
from indieweb.signals import webmention_received
@receiver(webmention_received)
def handle_webmention(sender, webmention, created, **kwargs):
if created and webmention.status == 'verified':
# Send notification, update cache, etc.
print(f"New webmention from {webmention.source_url}")
Models¶
The Webmention model stores all webmention data:
from indieweb.models import Webmention
# Get all verified webmentions for a URL
webmentions = Webmention.objects.filter(
target_url='https://mysite.com/post/',
status='verified'
)
# Get webmentions by type
likes = webmentions.filter(mention_type='like')
replies = webmentions.filter(mention_type='reply')
Fields:
source_url- The URL that links to your contenttarget_url- Your URL that was linked tostatus- pending, verified, failed, or spammention_type- mention, like, reply, or repostauthor_name,author_url,author_photo- Author infocontent,content_html- The mention contentpublished- When the mention was publishedcreated,modified- Timestampsverified_at- When the mention was verifiedspam_check_result- JSON field with spam check details
Testing Webmentions¶
You can test your webmention implementation using webmention.rocks:
Follow the test suite to verify your implementation
Use the discovery tests to check endpoint detection
Use the receiving tests to verify your endpoint
Troubleshooting¶
Common issues and solutions:
- Webmentions not being received
Check that your webmention endpoint is discoverable
Verify the endpoint URL is correct in your Link header/tag
Check Django logs for any errors
- Target URL validation failing
Ensure
django.contrib.sitesis configured correctlyCheck that the Site domain matches your production domain
- Microformats not being parsed
Verify the source page has proper microformats2 markup
Use a validator like https://indiewebify.me/
- Author name shows as URL or profile picture is missing
The source page may use URL references for author data (e.g.,
<data class="p-author" value="https://example.com/author"></data>)This is valid microformats2 markup following the authorship algorithm
Django-indieweb automatically looks for a matching h-card on the same page with the referenced URL
If the entry has no explicit author, same-page
rel=authorlinks and one unambiguous page-level h-card can also provide author dataEnsure the source page includes a separate h-card with matching URL, name, and photo properties
The h-card may be nested in structures like h-feeds - the parser searches recursively
Example services using this pattern: feed.city, some Mastodon webmention bridges
If no matching h-card is found, the URL will be displayed as the name (fallback behavior)
Limitation: Django-indieweb does not currently fetch remote author URLs; author data must be present in the fetched source document
- Spam checker rejecting valid webmentions
Review your spam checker implementation
Check the
spam_check_resultfield for details
API Reference¶
See API Reference for detailed API documentation of all webmention-related classes and functions.