Jumping Into Webmentions With NextJS (or Not)

Avatar of Atila Fassina
Atila Fassina on

Webmention is a W3C recommendation last published on January 12, 2017. And what exactly is a Webmention? It’s described as…


[…] a simple way to notify any URL when you mention it on your site. From the receiver’s perspective, it’s a way to request notifications when other sites mention it.

In a nutshell, it’s a way of letting a website know it has been mentioned somewhere, by someone, in some way. The Webmention spec also describes it as a way for a website to let others know it cited them. What that basically bails down to is that your website is an active social media channel, channeling communication from other channels (e.g. Twitter, Instagram, Mastodon, Facebook, etc.).

How does a site implement Webmentions? In some cases, like WordPress, it’s as trivial as installing a couple of plugins. Other cases may not be quite so simple, but it’s still pretty straightforward. In fact, let’s do that now!

Here’s our plan

  1. Declare an endpoint to receive Webmentions
  2. Process social media interactions to Webmentions
  3. Get those mentions into a website/app
  4. Set the outbound Webmentions

Luckily for us, there are services in place that make things extremely simple. Well, except that third point, but hey, it’s not so bad and I’ll walk through how I did it on my own atila.io site.

My site is a server-side blog that’s pre-rendered and written with NextJS. I have opted to make Webmention requests client-side; therefore, it will work easily in any other React app and with very little refactoring in any other JavaScript application.

Step 1: Declare an endpoint to receive Webmentions

In order to have an endpoint we can use to accept Webmentions, we need to either write the script and add to our own server, or use a service such as Webmention.io (which is what I did).

Webmention.io is free and you only need to confirm ownership over the domain you register. Verification can happen a number of ways. I did it by adding a rel="me" attribute to a link in my website to my social media profiles. It only takes one such link, but I went ahead and did it for all of my accounts.

<a
  href="https://twitter.com/atilafassina"
  target="_blank"
  rel="me noopener noreferrer"
>
  @AtilaFassina
</a>

Verifying this way, we also need to make sure there’s a link pointing back to our website in that Twitter profile. Once we’ve done that, we can head back to Webmention.io and add the URL.

This gives us an endpoint for accepting Webmentions! All we need to do now is wire it up as <link> tags in the <head> of our webpages in order to collect those mentions.

<head>
  <link rel="webmention" href="https://webmention.io/{user}/webmention" />
  <link rel="pingback" href="https://webmention.io/{user}/xmlrpc" />
  <!-- ... -->
</head>

Remember to replace {user} with your Webmention.io username.

Step 2: Process social media interactions into Webmentions

We are ready for the Webmentions to start flowing! But wait, we have a slight problem: nobody actually uses them. I mean, I do, you do, Max Böck does, Swyx does, and… that’s about it. So, now we need to start converting all those juicy social media interactions into Webmentions.

And guess what? There’s an awesome free service for it. Fair warning though: you’d better start loving the IndieWeb because we’re about to get all up in it.

Bridgy connects all our syndicated content and converts them into proper Webmentions so we can consume it. With a SSO, we can get each of our profiles lined up, one by one.

Step 3: Get those mentions into a website/app

Now it’s our turn to do some heavy lifting. Sure, third-party services can handle all our data, but it’s still up to us to use it and display it.

We’re going to break this up into a few stages. First, we’ll get the number of Webmentions. From there, we’ll fetch the mentions themselves. Then we’ll hook that data up to NextJS (but you don’t have to), and display it.

Get the number of mentions

type TMentionsCountResponse = {
  count: number
  type: {
    like: number
    mention: number
    reply: number
    repost: number
  }
}

That is an example of an object we get back from the Webmention.io endpoint. I formatted the response a bit to better suit our needs. I’ll walk through how I did that in just a bit, but here’s the object we will get:

type TMentionsCount = {
  mentions: number
  likes: number
  total: number
}

The endpoint is located at:

https://webmention.io/api/count.json?target=${post_url}

The request will not fail without it, but the data won’t come either. Both Max Böck and Swyx combine likes with reposts and mentions with replies. In Twitter, they are analogous.

const getMentionsCount = async (postURL: string): TMentionsCount => {
  const resp = await fetch(
    `https://webmention.io/api/count.json?target=${postURL}/`
  )
  const { type, count } = await resp.json()


  return {
    likes: type.like + type.repost,
    mentions: type.mention + type.reply,
    total: count,
  }
}

Get the actual mentions

Before getting to the response, please note that the response is paginated, where the endpoint accepts three parameters in the query:

  • page: the page being requested
  • per-page: the number of mentions to display on the page
  • target: the URL where Webmentions are being fetched

Once we hit https://webmention.io/api/mentions and pass the these params, the successful response will be an object with a single key links which is an array of mentions matching the type below:

type TMention = {
  source: string
  verified: boolean
  verified_date: string // date string
  id: number
  private: boolean
  data: {
    author: {
      name: string
      url: string
      photo: string // url, hosted in webmention.io
    }
    url: string
    name: string
    content: string // encoded HTML
    published: string // date string
    published_ts: number // ms
  }
  activity: {
    type: 'link' | 'reply' | 'repost' | 'like'
    sentence: string // pure text, shortened
    sentence_html: string // encoded html
  }
  target: string
}

The above data is more than enough to show a comment-like section list on our site. Here’s how the fetch request looks in TypeScript:

const getMentions = async (
  page: string,
  postsPerPage: number,
  postURL: string
): { links: TWebMention[] } => {
  const resp = await fetch(
    `https://webmention.io/api/mentions?page=${page}&per-page=${postsPerPage}&target=${postURL}`
  )
  const list = await resp.json()
  return list.links
}

Hook it all up in NextJS

We’re going to work in NextJS for a moment. It’s all good if you aren’t using NextJS or even have a web app. We already have all the data, so those of you not working in NextJS can simply move ahead to Step 4. The rest of us will meet you there.

As of version 9.3.0, NextJS has three different methods for fetching data:

  1. getStaticProps: fetches data on build time
  2. getStaticPaths: specifies dynamic routes to pre-render based on the fetched data
  3. getServerSideProps: fetches data on each request

Now is the moment to decide at which point we will be making the first request for fetching mentions. We can pre-render the data on the server with the first batch of mentions, or we can make the entire thing client-side. I opted to go client-side.

If you’re going client-side as well, I recommend using SWR. It’s a custom hook built by the Vercel team that provides good caching, error and loading states — it and even supports React.Suspense.

Display the Webmention count

Many blogs show the number of comments on a post in two places: at the top of a blog post (like this one) and at the bottom, right above a list of comments. Let’s follow that same pattern for Webmentions.

First off, let’s create a component for the count:

const MentionsCounter = ({ postUrl }) => {
  const { t } = useTranslation()
  // Setting a default value for `data` because I don't want a loading state
  // otherwise you could set: if(!data) return <div>loading...</div>
  const { data = {}, error } = useSWR(postUrl, getMentionsCount)


  if (error) {
    return <ErrorMessage>{t('common:errorWebmentions')}</ErrorMessage>
  }


  // The default values cover the loading state
  const { likes = '-', mentions = '-' } = data


  return (
    <MentionCounter>
      <li>
        <Heart title="Likes" />
        <CounterData>{Number.isNaN(likes) ? 0 : likes}</CounterData>
      </li>
      <li>
        <Comment title="Mentions" />{' '}
        <CounterData>{Number.isNaN(mentions) ? 0 : mentions}</CounterData>
      </li>
    </MentionCounter>
  )
}

Thanks to SWR, even though we are using two instances of the WebmentionsCounter component, only one request is made and they both profit from the same cache.

Feel free to peek at my source code to see what’s happening:

Display the mentions

Now that we have placed the component, it’s time to get all that social juice flowing!

At of the time of this writing, useSWRpages is not documented. Add to that the fact that the webmention.io endpoint doesn’t offer collection information on a response (i.e. no offset or total number of pages), I couldn’t find a way to use SWR here.

So, my current implementation uses a state to keep the current page stored, another state to handle the mentions array, and useEffect to handle the request. The “Load More” button is disabled once the last request brings back an empty array.

const Webmentions = ({ postUrl }) => {
  const { t } = useTranslation()
  const [page, setPage] = useState(0)
  const [mentions, addMentions] = useState([])


  useEffect(() => {
    const fetchMentions = async () => {
      const olderMentions = await getMentions(page, 50, postUrl)
      addMentions((mentions) => [...mentions, ...olderMentions])
    }
    fetchMentions()
  }, [page])


  return (
    <>
      {mentions.map((mention, index) => (
        <Mention key={mention.data.author.name + index}>
          <AuthorAvatar src={mention.data.author.photo} lazy />
          <MentionContent>
            <MentionText
              data={mention.data}
              activity={mention.activity.type}
            />
          </MentionContent>
        </Mention>
      ))}
      </MentionList>
      {mentions.length > 0 && (
        <MoreButton
          type="button"
          onClick={() => {
          setPage(page + 1)
        }}
        >
        {t('common:more')}
      </MoreButton>
    )}
    </>
  )
}

The code is simplified to allow focus on the subject of this article. Again, feel free to peek at the full implementation:

Step 4: Handling outbound mentions

Thanks to Remy Sharp, handling outbound mentions from one website to others is quite easy and provides an option for each use case or preference possible.

The quickest and easiest way is to head over to Webmention.app, get an API token, and set up a web hook. Now, if you have RSS feed in place, the same thing is just as easy with an IFTT applet, or even a deploy hook.

If you prefer to avoid using yet another third-party service for this feature (which I totally get), Remy has open-sourced a CLI package called wm which can be ran as a postbuild script.

But that’s not enough to handle outbound mentions. In order for our mentions to include more than simply the originating URL, we need to add microformats to our information. Microformats are key because it’s a standardized way for sites to distribute content in a way that Webmention-enabled sites can consume.

At their most basic, microformats are a kind of class-based notations in markup that provide extra semantic meaning to each piece. In the case of a blog post, we will use two kinds of microformats:

  • h-entry: the post entry
  • h-card: the author of the post

Most of the required information for h-entry is usually in the header of the page, so the header component may end up looking something like this:

<header class="h-entry">
  <!-- the post date and time -->
  <time datetime="2020-04-22T00:00:00.000Z" class="dt-published">
    2020-04-22
  </time>
  <!-- the post title -->
  <h1 class="p-name">
    Webmentions with NextJS
  </h1>
</header>

And that’s it. If you’re writing in JSX, remember to replace class with className, that datetime is camelCase (dateTime), and that you can use the new Date('2020-04-22').toISOString() function.

It’s pretty similar for h-card. In most cases (like mine), author information is below the article. Here’s how my page’s footer looks:

<footer class="h-card">
  <!-- the author name -->
  <span class="p-author">Atila Fassina</span>
  <!-- the authot image-->
  <img
    alt="Author’s photograph: Atila Fassina"
    class="u-photo"
    src="/images/internal-avatar.jpg"
    lazy
  />
</footer>

Now, whenever we send an outbound mention from this blog post, it will display the full information to whomever is receiving it.

Wrapping up

I hope this post has helped you getting to know more about Webmentions (or even about IndieWeb as a whole), and perhaps even helped you add this feature to your own website or app. If it did, please consider sharing this post to your network. I will be super grateful! 😉

References

Further reading