Webmentions are a powerful tool for adding decentralized social interactions, such as comments, likes, reposts, and replies, directly on your website. If you're building a dynamic site with Next.js, integrating Webmentions can help encourage cross-site conversations, boost SEO, and enhance user engagement. In this guide, I will show you how to implement Webmentions into your Next.js project with PostgreSQL for storing and displaying them.
What Are Webmentions?
Webmention is an open web standard (W3C Recommendation) that enables decentralized cross-site interactions.
In simpler terms, Webmentions allow users to interact with your content across the web by leaving comments, likes, reposts and other responses on other sites. These interactions enrich your site’s user experience, and they help establish meaningful connections with others.
When you link to a webpage, you can send a Webmention notification. If the receiving site supports Webmentions, it may display your post as a comment, like, or response—enabling rich cross-site conversations.
Why Should You Use Webmentions?
- Encourage Engagement: Promote cross-site discussions and interactions.
- Boost Social Proof: Display interactions from well-known sources to establish credibility.
- Enhance SEO: Webmentions generate backlinks, improving visibility and search engine rankings.
Here’s an example of how Webmentions appear on my site:
Sorry, somehow the image couldn't be found :(
You can check a live version of this in the Webmentions section of this article: [/articles/fetching-and-storing-activities-from-garmin-connect-with-strapi-and-visualizing-them-with-next-js#replies].
A typical webmentions structure in JSON looks like this:
jsonCopy code{ "type": "entry", "author": { "type": "card", "name": "Some Name", "photo": "URL to author image", "url": "URL to author profile" }, "url": "Webmention URL", "wm-received": "Date of Webmention", "wm-id": 1876563, "wm-source": "Source URL", "wm-target": "Target URL", "wm-property": "Type of mention (e.g., like-of, repost-of)", "wm-private": false }
To keep your Webmentions accessible even if an external service is discontinued, it’s a good idea to store them locally. In this tutorial, we’ll guide you through setting up a PostgreSQL database to store Webmentions and display them dynamically in your Next.js app.
Setting Up PostgreSQL
Before we dive into Webmentions, ensure you have PostgreSQL installed on your server. If not, check on of these guides.
Once PostgreSQL is ready:
- Create a New Database:
bashCopy code# Create a new database for storing Webmentions createdb personalwebsite
- Create a Table to Store Webmentions: Here's the SQL structure to store Webmentions in your database:
sqlCopy code-- Define a table structure to store Webmentions -- DROP TABLE public.webmentions; CREATE TABLE public.webmentions ( id serial4 NOT NULL, wm_id int8 NOT NULL, wm_source text NOT NULL, wm_target text NOT NULL, wm_property text NOT NULL, url text NULL, author_name text NULL, author_photo text NULL, author_url text NULL, content_html text NULL, content_text text NULL, published_at timestamp NULL, received_at timestamp DEFAULT CURRENT_TIMESTAMP NULL, CONSTRAINT webmentions_pkey PRIMARY KEY (id), CONSTRAINT webmentions_wm_id_key UNIQUE (wm_id) );
- Create a Log Table for Fetching Webmentions: This table logs each fetch, allowing you to track when Webmentions were last retrieved and avoid overloading the service:
sqlCopy code-- public.webmention_fetch_log definition -- Drop table -- DROP TABLE public.webmention_fetch_log; CREATE TABLE public.webmention_fetch_log ( id serial4 NOT NULL, last_fetch timestamptz NOT NULL, CONSTRAINT webmention_fetch_log_pkey PRIMARY KEY (id) );
Using Webmention.io to Receive Webmentions
Before we store webmentions to display them we have to get them somewhere. If you dont want to implement your own Webmentions Receiver I recommend to use Webmention.io which is a service to easily receive webmentions.
Steps:
- Set Up IndieAuth for Your Site: Follow these IndieAuth setup instructions
- Sign Up for Webmention.io: Create an account at Webmention.io
- Add the Webmention Link to Your
_app.tsx
:
htmlCopy code<Head> ... <link rel="webmention" href="https://webmention.io/username/webmention" /> ... </Head>
From here on, Webmention.io will collect all the Webmentions for your site. Now, let’s create a script that fetches and stores them every ten minutes in the PostgreSQL database
Fetching Webmentions from Webmention.io API
To keep Webmentions up-to-date, we'll fetch them periodically. Here’s the logic of the script.
Sorry, somehow the image couldn't be found :(
Create a script src/utils/fetch-webmentions.js
to fetch and store Webmentions:
jsxCopy codeimport fetch from "node-fetch" import { Pool } from "pg" const pool = new Pool({ user: process.env.PGUSER, host: process.env.PGHOST, database: process.env.PGDATABASE, password: process.env.PGPASSWORD, port: process.env.PGPORT, }) function isNotOlderThanTenMinutes(date: Date) { if (!(date instanceof Date) || isNaN(date.getTime())) return false return Date.now() - date.getTime() <= 10 * 60 * 1000 } export async function fetchAndStoreWebmentions() { const client = await pool.connect() // Use a client for transaction safety try { console.log("🔄 Checking last webmention fetch...") // Get the latest fetch timestamp const { rows } = await client.query( `SELECT last_fetch FROM webmention_fetch_log ORDER BY last_fetch DESC LIMIT 1` ) const lastFetchDate = rows[0]?.last_fetch if (isNotOlderThanTenMinutes(lastFetchDate)) { console.log("✅ Webmentions are already updated!") return } // Insert new fetch timestamp const now = new Date().toISOString() await client.query(`INSERT INTO webmention_fetch_log (last_fetch) VALUES ($1)`, [now]) console.log("📌 Updated Webmentions fetch log") // Generate Webmention API URL const baseUrl = `https://webmention.io/api/mentions.jf2?domain=mxd.codes&per-page=1000&page=0&token=rxwJcoKxWCd9n3NA9SoxeQ` const webmentionsUrl = lastFetchDate instanceof Date && !isNaN(lastFetchDate.getTime()) ? `${baseUrl}&since=${lastFetchDate.toISOString()}` : baseUrl // Fetch new Webmentions from Webmention.io console.log("🔄 Fetching webmentions from Webmention.io...") const response = await fetch(webmentionsUrl) const { children: webmentions } = await response.json() if (!Array.isArray(webmentions) || webmentions.length === 0) { console.log("⚠️ No new webmentions found.") return } console.log(`📥 Processing ${webmentions.length} webmentions...`) // Prepare batch insert query const insertQuery = ` INSERT INTO webmentions ( wm_id, wm_source, wm_target, wm_property, url, author_name, author_photo, author_url, content_html, content_text, published_at, received_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (wm_id) DO NOTHING; ` for (const mention of webmentions) { const values = [ mention["wm-id"], mention["wm-source"], mention["wm-target"], mention["wm-property"], mention["url"], mention.author?.name || null, mention.author?.photo || null, mention.author?.url || null, mention.content?.html || null, mention.content?.text || null, mention.published ? new Date(mention.published) : null, new Date(mention["wm-received"]), ] await pool.query(insertQuery, values) } console.log(`✅ Stored ${webmentions.length} webmentions successfully!`) } catch (error) { console.error("❌ Error fetching or storing webmentions:", error) } finally { client.release() // Ensure client is released back to the pool } } // Run the function fetchAndStoreWebmentions()
This function can now be called everytime before webmentions are queried for a page from PostgreSQL database. Ideally, you should abstract the logic for determining whether new webmentions need to be fetched into an API layer. This prevents unnecessary database queries with every request, but this is out of scope for this article.
Retrieving Webmentions for a Page
To retrieve Webmentions dynamically for a page, we create an API route in pages/api/get-webmentions.js
. This route allows us to fetch mentions for a specific target URL stored in our PostgreSQL database.
jsxCopy codeimport { Pool } from "pg" import { fetchAndStoreWebmentions } from "@/src/utils/fetch-webmentions" const pool = new Pool({ user: process.env.PGUSER, host: process.env.PGHOST, database: process.env.PGDATABASE, password: process.env.PGPASSWORD, port: process.env.PGPORT, }) export default async function handler(req, res) { if (req.method !== "GET") return res.status(405).json({ error: "Method not allowed" }) const { target } = req.query if (!target) return res.status(400).json({ error: "Missing target URL" }) // Updating Webmentions before selecting for page url await fetchAndStoreWebmentions() const query = `SELECT wm_id, wm_source, wm_target, wm_property, url, author_name, author_photo, author_url, content_text, published_at FROM webmentions WHERE wm_target LIKE '%${target}%' ORDER BY received_at DESC;` const result = await pool.query(query) res.json(result.rows) }
Now you can call this API route and pass a query param target
with the URL to get all Webmentions for a page.
Displaying Webmentions in Next.js
To visually display Webmentions on our website, we create a dedicated React component components/Webmentions.js
. This component fetches the Webmentions from our API and renders them.
javascriptCopy codeimport { useEffect, useState } from "react"; // React component to display Webmentions for a given page const Webmentions = ({ targetUrl }) => { const [mentions, setMentions] = useState([]); useEffect(() => { // Fetch Webmentions for the target URL from the API route fetch(`/api/get-webmentions?target=${encodeURIComponent(targetUrl)}`) .then((res) => res.json()) .then((data) => setMentions(data)); }, [targetUrl]); return ( <div> <h3>Webmentions</h3> {mentions.length === 0 ? ( <p>No webmentions yet.</p> ) : ( mentions.map((mention) => ( <div className="vcard h-card p-author" key={mention.wm_id} style={{ border: "1px solid #ddd", padding: "10px", marginBottom: "10px" }}> {mention.author_photo && ( // Display author profile picture if available <img src={mention.author_photo} alt={mention.author_name} className="u-photo" style={{ width: "40px", height: "40px", borderRadius: "50%" }} /> )} <p> <strong>{mention.author_name}</strong> {mention.wm_property.replace("-", " ")} {mention.wm_property === "like-of" && " ❤️"} {mention.wm_property === "repost-of" && " 🔁"} {mention.wm_property === "in-reply-to" && " 💬"} </p> <a className="u-url" href={mention.url || mention.wm_source} target="_blank" rel="noopener noreferrer">View Mention</a> </div> )) )} </div> ); }; export default Webmentions;
I highly recommend periodically verifying the authenticity of Webmention sources to prevent spam.
Testing Webmentions
To ensure your Webmentions setup works correctly, use the following tools:
- Webmention Rocks! – A tool for testing Webmention implementations.
By integrating Webmentions into your Next.js site, you can create an interactive and engaging web community. Whether you're running a blog, portfolio, or e-commerce site, Webmentions provide a powerful way to enhance content, boost SEO, and encourage meaningful connections.
If you have created a response to this post you can send me a webmention and it will appear below the post.
More Links
- IndieWeb Webmention
- You can read more about Webmention and the IndieWeb on the https://indieweb.org/.