Enhancing Social Interactions: Implementing Webmentions with Next.js and PostgreSQL

Enhancing Social Interactions: Implementing Webmentions with Next.js and PostgreSQL

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:

json
Copy 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:
bash
Copy 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:
sql
Copy 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:
sql
Copy 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:

  1. Set Up IndieAuth for Your Site: Follow these IndieAuth setup instructions
  2. Sign Up for Webmention.io: Create an account at Webmention.io
  3. Add the Webmention Link to Your _app.tsx:
html
Copy 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:

jsx
Copy code
import 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.

jsx
Copy code
import { 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.

javascript
Copy code
import { 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:

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

Table of Contents