How do I add Webmentions to a static site?
Implementing Webmentions on static sites built with Jekyll, Hugo, Eleventy, Next.js, and other generators.
Static sites present unique challenges for Webmentions since they can't run server-side code. However, with hosted services and build-time integration, you can get full Webmention support that's actually simpler than dynamic sites.
The Static Site Strategy
Since your site is static HTML/CSS/JS, you'll use:
- Webmentions.com - Hosted endpoint for receiving
- JavaScript - Client-side display of mentions
- Build-time fetching (optional) - Bake mentions into static HTML
- GitHub Actions/Netlify Functions (optional) - Send mentions
Quick Start (Any Static Site)
Step 1: Set Up Webmentions.com
- Visit webmentions.com
- Create an account and add your domain
- Get your endpoint URL
Step 2: Add Endpoints to Your HTML
In your site's <head> (usually in a layout/template):
<!-- Webmention endpoint (required) -->
<link rel="webmention" href="https://receive.webmentions.com/yourdomain.com" />
<!-- Pingback endpoint (optional - captures mentions from legacy WordPress sites) -->
<link rel="pingback" href="https://receive.webmentions.com/xmlrpc/yourdomain.com" />
Replace yourdomain.com with your actual domain. The pingback endpoint is optional but recommended to capture mentions from older blogs.
Step 3: Display Mentions with JavaScript
Add this to your post template:
<div id="webmentions"></div>
<script>
async function loadWebmentions() {
const targetUrl = window.location.href;
const apiUrl = `https://api.webmentions.com/mentions?target=${encodeURIComponent(targetUrl)}`;
try {
const response = await fetch(apiUrl);
const data = await response.json();
displayWebmentions(data.mentions || []);
} catch (error) {
console.error('Failed to load webmentions:', error);
}
}
function displayWebmentions(mentions) {
const container = document.getElementById('webmentions');
if (mentions.length === 0) {
breakpoint-innerHTML = '<p>No mentions yet.</p>';
return;
}
const html = mentions.map(mention => {
const author = mention.author || {};
const type = mention['wm-property'] || 'mention';
return `
<div class="webmention webmention-${type}">
${author.photo ? `<img src="${author.photo}" alt="${author.name || 'Avatar'}">` : ''}
<div class="webmention-content">
<strong>${author.name || 'Someone'}</strong>
<span class="webmention-type">${formatType(type)}</span>
${mention.content ? `<p>${mention.content.text || mention.content.html}</p>` : ''}
<time datetime="${mention.published}">${formatDate(mention.published)}</time>
</div>
</div>
`;
}).join('');
breakpoint-innerHTML = `<h3>Mentions</h3>${html}`;
}
function formatType(type) {
const types = {
'in-reply-to': 'replied',
'like-of': 'liked this',
'repost-of': 'reposted this',
'bookmark-of': 'bookmarked this',
'mention-of': 'mentioned this',
};
return types[type] || 'mentioned this';
}
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString();
}
// Load on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadWebmentions);
} else {
loadWebmentions();
}
</script>
Step 4: Add CSS
.webmention {
display: flex;
gap: 1rem;
padding: 1rem;
border-left: 3px solid #ddd;
margin: 1rem 0;
}
.webmention img {
width: 48px;
height: 48px;
border-radius: 50%;
}
.webmention-content {
flex: 1;
}
.webmention-type {
color: #666;
font-size: 0.9em;
}
.webmention-like {
border-left-color: #e94a4a;
}
.webmention-repost {
border-left-color: #4ae290;
}
.webmention-in-reply-to {
border-left-color: #4a90e2;
}
Done! Your static site now receives and displays Webmentions.
Generator-Specific Implementations
Jekyll
Using the Plugin:
# Gemfile
gem 'jekyll-webmention_io'
# _config.yml
webmentions:
username: yourdomain.com
cache_folder: _data
cache_bad_uris_for: 5
legacy_domains:
- http://olddomain.com
<!-- In your post layout -->
{% webmentions page.url %}
Build-Time Fetching:
# _config.yml
plugins:
- jekyll-webmention_io
webmentions:
username: yourdomain.com
Run: jekyll build (fetches mentions during build)
Hugo
Using a Module:
# Install webmention module
hugo mod get github.com/yourname/hugo-webmentions
{{/* layouts/_default/single.html */}}
{{ $url := .Permalink }}
{{ $webmentions := getJSON (print "https://api.webmentions.com/mentions?target=" $url) }}
{{ with $webmentions.mentions }}
<section class="webmentions">
<h3>Mentions</h3>
{{ range . }}
<div class="webmention">
<img src="{{ .author.photo }}" alt="{{ .author.name }}">
<strong>{{ .author.name }}</strong>
<span>{{ .type }}</span>
{{ with .content.html }}
<p>{{ . | safeHTML }}</p>
{{ end }}
</div>
{{ end }}
</section>
{{ end }}
Fetching at Build Time:
# In package.json scripts
"build": "node fetch-webmentions.js && hugo"
// fetch-webmentions.js
const fs = require('fs');
const fetch = require('node-fetch');
async function fetchMentions() {
const response = await fetch(
'https://api.webmentions.com/mentions?domain=yourdomain.com',
{
headers: { Authorization: `Bearer ${process.env.WEBMENTIONS_API_KEY}` }
}
);
const data = await response.json();
fs.writeFileSync(
'data/webmentions.json',
JSON.stringify(data, null, 2)
);
}
fetchMentions();
Eleventy
Using Our API:
// .eleventy.js
module.exports = function(eleventyConfig) {
eleventyConfig.addGlobalData('webmentions', async () => {
const response = await fetch(
'https://api.webmentions.com/mentions?domain=yourdomain.com',
{
headers: { Authorization: `Bearer ${process.env.WEBMENTIONS_TOKEN}` }
}
);
return response.json();
});
};
{# In your post template #}
{% webmentions page.url %}
Custom Filter Implementation:
// .eleventy.js
const fetch = require('node-fetch');
module.exports = function(eleventyConfig) {
eleventyConfig.addFilter('webmentionsForUrl', function(webmentions, url) {
return (webmentions.mentions || []).filter(m => m.target === url);
});
};
Next.js / React
Webmentions Component:
// components/Webmentions.tsx
import { useEffect, useState } from 'react';
interface Webmention {
author: { name: string; photo: string; url: string };
content?: { text: string; html: string };
published: string;
type: string;
}
export function Webmentions({ targetUrl }: { targetUrl: string }) {
const [mentions, setMentions] = useState<Webmention[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchMentions() {
try {
const apiUrl = `https://api.webmentions.com/mentions?target=${encodeURIComponent(targetUrl)}`;
const response = await fetch(apiUrl);
const data = await response.json();
setMentions(data.mentions || []);
} catch (error) {
console.error('Failed to load webmentions:', error);
} finally {
setLoading(false);
}
}
fetchMentions();
}, [targetUrl]);
if (loading) return <p>Loading mentions...</p>;
if (mentions.length === 0) return <p>No mentions yet.</p>;
return (
<section className="webmentions">
<h3>Mentions</h3>
{mentions.map((mention, i) => (
<div key={i} className={`webmention webmention-${mention.type}`}>
{mention.author?.photo && (
<img src={mention.author.photo} alt={mention.author.name} />
)}
<div>
<strong>{mention.author?.name || 'Someone'}</strong>
<span> {formatType(mention.type)}</span>
{mention.content && <p>{mention.content.text}</p>}
<time>{new Date(mention.published).toLocaleDateString()}</time>
</div>
</div>
))}
</section>
);
}
function formatType(type: string) {
const types: Record<string, string> = {
'reply': 'replied',
'like': 'liked this',
'repost': 'reposted this',
};
return types[type] || 'mentioned this';
}
Usage:
// pages/posts/[slug].tsx
import { Webmentions } from '@/components/Webmentions';
export default function Post({ post }: { post: Post }) {
const url = `https://yourdomain.com/posts/${post.slug}`;
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<Webmentions targetUrl={url} />
</article>
);
}
Server-Side Rendering (SSR):
// pages/posts/[slug].tsx
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
const url = `https://yourdomain.com/posts/${params.slug}`;
// Fetch webmentions at build time
const response = await fetch(
`https://api.webmentions.com/mentions?target=${encodeURIComponent(url)}`
);
const data = await response.json();
return {
props: {
post,
webmentions: data.mentions || [],
},
revalidate: 60, // Revalidate every minute
};
}
Gatsby
Using Our API:
// gatsby-node.js
exports.sourceNodes = async ({ actions, createNodeId, createContentDigest }) => {
const response = await fetch(
'https://api.webmentions.com/mentions?domain=yourdomain.com',
{
headers: { Authorization: `Bearer ${process.env.WEBMENTIONS_TOKEN}` }
}
);
const data = await response.json();
for (const mention of data.mentions || []) {
actions.createNode({
...mention,
id: createNodeId(`webmention-${mention.id}`),
internal: {
type: 'Webmention',
contentDigest: createContentDigest(mention),
},
});
}
};
Sending Webmentions from Static Sites
Option 1: GitHub Actions
# .github/workflows/send-webmentions.yml
name: Send Webmentions
on:
push:
branches: [main]
jobs:
send:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm install webmention
- run: node send-webmentions.js
// send-webmentions.js
const { send } = require('webmention');
const fs = require('fs');
const glob = require('glob');
async function sendWebmentions() {
// Find all new/modified posts
const files = glob.sync('content/posts/**/*.md');
for (const file of files) {
const content = fs.readFileSync(file, 'utf8');
const links = extractLinks(content);
for (const link of links) {
try {
await send({
source: `https://yourdomain.com/posts/${getSlug(file)}`,
target: link,
});
console.log(`Sent webmention to ${link}`);
} catch (error) {
console.error(`Failed to send to ${link}:`, error);
}
}
}
}
sendWebmentions();
Option 2: Netlify Function
// netlify/functions/send-webmentions.js
const { send } = require('webmention');
exports.handler = async (event) => {
const { source, targets } = JSON.parse(event.body);
const results = await Promise.all(
targets.map(target => send({ source, target }))
);
return {
statusCode: 200,
body: JSON.stringify({ sent: results.length }),
};
};
Option 3: Client-Side Sending
// Send from browser (for testing only)
async function sendWebmentionFromBrowser(source, target) {
// Discover endpoint
const response = await fetch(target);
const html = await response.text();
const match = html.match(/<link rel="webmention" href="([^"]+)"/);
if (!match) {
console.log('No webmention endpoint found');
return;
}
const endpoint = new URL(match[1], target).href;
// Send mention
await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ source, target }),
});
}
Caching Strategies
Service Worker Caching
// sw.js
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Cache Webmentions.com API responses
if (url.hostname === 'api.webmentions.com') {
event.respondWith(
caches.open('webmentions-v1').then(cache => {
return cache.match(event.request).then(response => {
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
})
);
}
});
Local Storage Caching
async function loadWebmentionsWithCache(targetUrl) {
const cacheKey = `webmentions-${targetUrl}`;
const cached = localStorage.getItem(cacheKey);
const cacheTime = localStorage.getItem(`${cacheKey}-time`);
// Use cache if less than 1 hour old
if (cached && cacheTime && Date.now() - parseInt(cacheTime) < 3600000) {
displayWebmentions(JSON.parse(cached));
return;
}
// Fetch fresh data
const apiUrl = `https://api.webmentions.com/mentions?target=${encodeURIComponent(targetUrl)}`;
const response = await fetch(apiUrl);
const data = await response.json();
const mentions = data.mentions || [];
// Update cache
localStorage.setItem(cacheKey, JSON.stringify(mentions));
localStorage.setItem(`${cacheKey}-time`, Date.now().toString());
displayWebmentions(mentions);
}
Performance Tips
- Lazy load mentions: Only fetch when user scrolls to comments section
- Paginate: Limit initial display, load more on demand
- Cache aggressively: Mentions don't change frequently
- Build-time fetching: Bake into static HTML when possible
- Service workers: Offline access to previously loaded mentions
Best Practices
- ✅ Add microformats to your content
- ✅ Set appropriate cache headers
- ✅ Handle loading and error states
- ✅ Make mentions responsive
- ✅ Test with different mention types
- ✅ Provide fallback for no JavaScript
- ✅ Use semantic HTML for accessibility
Static sites with Webmentions get the best of both worlds: blazing fast performance and rich social interactions, all while remaining simple and maintainable.
Related FAQs
How do I implement Webmentions on my site?
Step-by-step guide to adding Webmention support to your website, from beginner to advanced.
How do I add Webmentions to WordPress?
Complete guide to implementing Webmentions on WordPress sites using plugins and custom solutions.
How do I handle spam Webmentions?
Strategies and techniques for preventing, detecting, and moderating spam Webmentions on your website.