惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
L
LINUX DO - 热门话题
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
爱范儿
爱范儿
D
DataBreaches.Net
Simon Willison's Weblog
Simon Willison's Weblog
S
Secure Thoughts
S
SegmentFault 最新的问题
博客园 - 【当耐特】
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 叶小钗
P
Proofpoint News Feed
The Hacker News
The Hacker News
T
ThreatConnect
N
News and Events Feed by Topic
T
Threatpost
The Register - Security
The Register - Security
WordPress大学
WordPress大学
博客园 - Franky
Recorded Future
Recorded Future
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
腾讯CDC
F
Future of Privacy Forum
F
Full Disclosure
Cyberwarzone
Cyberwarzone
J
Java Code Geeks
李成银的技术随笔
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
H
Hacker News: Front Page
人人都是产品经理
人人都是产品经理
博客园_首页
Scott Helme
Scott Helme
Google DeepMind News
Google DeepMind News
美团技术团队
Malwarebytes
Malwarebytes
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
T
The Exploit Database - CXSecurity.com
G
GRAHAM CLULEY
Recent Announcements
Recent Announcements
C
CXSECURITY Database RSS Feed - CXSecurity.com

CSS-Tricks

Revealing Text With CSS letter-spacing | CSS-Tricks Technical Writing in the AI Age | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks The State of CSS Centering in 2026 | CSS-Tricks Stack Overflow: When We Stop Asking | CSS-Tricks Cross-Document View Transitions: The Gotchas Nobody Mentions | CSS-Tricks What’s !important #11: 3D Voxel Scenes, Flying Focus, CSS Syntaxes, and More | CSS-Tricks Computing and Displaying Discounted Prices in CSS | CSS-Tricks rotateX() | CSS-Tricks rotateY() | CSS-Tricks rotateZ() | CSS-Tricks rotate() | CSS-Tricks Soon We Can Finally Banish JavaScript to the ShadowRealm | CSS-Tricks Using CSS corner-shape For Folded Corners | CSS-Tricks A Scrollytelling Gift for Mum on Mother’s Day 2026 | CSS-Tricks Google’s Prompt API | CSS-Tricks Making Zigzag CSS Layouts With a Grid + Transform Trick | CSS-Tricks Fixed-Height Cards: More Fragile Than They Look | CSS-Tricks What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, and More | CSS-Tricks The Importance of Native Randomness in CSS | CSS-Tricks contrast() | CSS-Tricks contrast-color() | CSS-Tricks Let’s Use the Nonexistent ::nth-letter Selector Now | CSS-Tricks Quick Hit #126 Recreating Apple’s Vision Pro Animation in CSS | CSS-Tricks Quick Hit #125 Enhancing Astro With a Markdown Component | CSS-Tricks Quick Hit #124 Markdown + Astro = ❤️ | CSS-Tricks Quick Hit #123 What’s !important #9: clip-path Jigsaws, View Transitions Toolkit, Name-only Containers, and More | CSS-Tricks A Well-Designed JavaScript Module System is Your First Architecture Decision | CSS-Tricks hypot() | CSS-Tricks The Radio State Machine | CSS-Tricks 7 View Transitions Recipes to Try | CSS-Tricks Quick Hit #122 Quick Hit #121 Selecting a Date Range in CSS | CSS-Tricks saturate() | CSS-Tricks justify-self | CSS-Tricks Quick Hit #120 Alternatives to the !important Keyword | CSS-Tricks Quick Hit #119 New CSS Multi-Column Layout Features in Chrome | CSS-Tricks Quick Hit #118 Making Complex CSS Shapes Using shape() | CSS-Tricks Quick Hit #117 Front-End Fools: Top 10 April Fools’ UI Pranks of All Time | CSS-Tricks Sniffing Out the CSS Olfactive API | CSS-Tricks What’s !important #8: Light/Dark Favicons, @mixin, object-view-box, and More | CSS-Tricks Quick Hit #116 Form Automation Tips for Happier User and Clients | CSS-Tricks Quick Hit #115 Generative UI Notes | CSS-Tricks Quick Hit #114 Quick Hit #113 Experimenting With Scroll-Driven corner-shape Animations | CSS-Tricks Quick Hit #112 JavaScript for Everyone: Destructuring | CSS-Tricks Quick Hit #111 Quick Hit #110 What’s !important #7: random(), Folded Corners, Anchored Container Queries, and More | CSS-Tricks 4 Reasons That Make Tailwind Great for Building Layouts | CSS-Tricks Quick Hit #109 Quick Hit #108 Abusing Customizable Selects | CSS-Tricks Quick Hit #107 The Value of z-index | CSS-Tricks Quick Hit #106 The Different Ways to Select <html> in CSS Quick Hit #105 Popover API or Dialog API: Which to Choose? Quick Hit #104 What’s !important #6: :heading, border-shape, Truncating Text From the Middle, and More Yet Another Way to Center an (Absolute) Element An Exploit ... in CSS?! Quick Hit #103 A Complete Guide to Bookmarklets Quick Hit #102 Loading Smarter: SVG vs. Raster Loaders in Modern Web Design Potentially Coming to a Browser :near() You Quick Hit #101 Distinguishing "Components" and "Utilities" in Tailwind Quick Hit #100 Spiral Scrollytelling in CSS With sibling-index() Interop 2026 Quick Hit #99 What’s !important #5: Lazy-loading iframes, Repeating corner-shape Backgrounds, and More Quick Hit #98 Making a Responsive Pyramidal Grid With Modern CSS Approximating contrast-color() With Other CSS Features Quick Hit #97 Trying to Make the Perfect Pie Chart in CSS Quick Hit #96 Quick Hit #95 CSS Bar Charts Using Modern Functions Quick Hit #94 No Hassle Visual Code Theming: Publishing an Extension Quick Hit #93
Jumping Into Webmentions With NextJS (or Not)
CSS-Tricks · 2020-06-03 · via CSS-Tricks

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.

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