使用 NextJS(或不使用)跳入 Webmentions

Avatar of Atila Fassina
Atila Fassina

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费信用额度!

Webmention 是 W3C 建议,最近一次发布是在 2017 年 1 月 12 日。 那么,什么是 Webmention 呢? 它被描述为…


[…] 当您在网站上提及某个 URL 时,通知该 URL 的一种简单方法。 从接收者的角度来看,这是一种在其他网站提及它时请求通知的方法。

简而言之,这是一种让网站知道它在某个地方被某人以某种方式提及的方式。 Webmention 规范还将其描述为网站让其他人知道它引用了他们的方式。 这基本上归结为您的网站是一个活跃的社交媒体渠道,从其他渠道(例如 Twitter、Instagram、Mastodon、Facebook 等)传达信息。

网站如何实施 Webmentions? 在某些情况下,例如 WordPress,这与安装几个插件一样简单。 其他情况可能并不那么简单,但仍然非常直接。 事实上,我们现在就来做吧!

这是我们的计划

  1. 声明一个接收 Webmentions 的端点
  2. 将社交媒体互动处理成 Webmentions
  3. 将这些提及纳入网站/应用程序
  4. 设置传出的 Webmentions

幸运的是,我们有一些服务可以使事情变得极其简单。 嗯,除了第三点,但嘿,这并不糟糕,我会逐步介绍我在自己的 atila.io 网站上是如何做的。

我的网站是一个服务器端博客,它是预渲染的,并使用 NextJS 编写。 我选择在客户端发出 Webmention 请求; 因此,它将轻松地在任何其他 React 应用程序中以及在任何其他 JavaScript 应用程序中只需很少的重构即可正常工作。

步骤 1:声明一个接收 Webmentions 的端点

为了拥有一个可用于接受 Webmentions 的端点,我们需要编写脚本并将其添加到自己的服务器中,或者使用像 Webmention.io 这样的服务(这就是我所做的)。

Webmention.io 是免费的,您只需要确认对注册域名的所有权。 验证可以通过多种方式进行。 我通过在我的网站中的链接(指向我的社交媒体个人资料)中添加 rel="me" 属性来进行验证。 它只需要一个这样的链接,但我继续为我的所有帐户都这样做了。

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

以这种方式进行验证,我们还需要确保在该 Twitter 个人资料中有一个指向我们网站的链接。 完成此操作后,我们可以返回 Webmention.io 并添加 URL。

这为我们提供了一个用于接受 Webmentions 的端点! 我们现在需要做的就是将其作为 <link> 标签连接到我们网页的 <head> 中,以便收集这些提及。

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

请记住,将 {user} 替换为您的 Webmention.io 用户名。

步骤 2:将社交媒体互动处理成 Webmentions

我们已经准备好让 Webmentions 开始流动! 但是等等,我们有一个小问题:实际上没有人使用它们。 我的意思是,我用,你用,Max Böck 用,Swyx 用,还有… 仅此而已。 所以,现在我们需要开始将所有这些有趣的社交媒体互动转换成 Webmentions。

猜猜看? 有一个很棒的免费服务可以做到这一点。 不过请注意:你最好开始 喜欢 IndieWeb,因为我们很快就会参与其中。

Bridgy 连接了我们所有联合的内容,并将它们转换成合适的 Webmentions,这样我们就可以使用它们。 通过 SSO,我们可以逐一整理好我们的每个个人资料。

步骤 3:将这些提及纳入网站/应用程序

现在轮到我们做一些繁重的工作了。 当然,第三方服务可以处理我们所有的数据,但使用它们并显示它们仍然由我们决定。

我们将将其分解成几个阶段。 首先,我们将获取 Webmentions 的数量。 然后,我们将获取这些提及本身。 然后,我们将这些数据连接到 NextJS(但您不必这样做),并显示它们。

获取提及的数量

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

这是一个从 Webmention.io 端点获取的示例对象。 我对响应进行了格式化,使其更适合我们的需求。 我会在稍后介绍我是如何做到的,但以下是我们将获得的对象

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

端点位于

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

没有它,请求不会失败,但数据也不会来。 Max BöckSwyx 都将喜欢与转发以及提及与回复组合在一起。 在 Twitter 中,它们是相似的。

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,
  }
}

获取实际的提及

在获取响应之前,请注意,响应是分页的,端点在查询中接受三个参数

  • page:请求的页面
  • per-page:页面上要显示的提及数量
  • target:获取 Webmentions 的 URL

一旦我们点击 https://webmention.io/api/mentions 并传递这些参数,成功响应将是一个对象,它只有一个键 links,这是一个包含与以下类型匹配的提及的 array

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
}

以上数据足够显示我们网站上的类似评论的节列表。 以下是 TypeScript 中的 fetch 请求

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
}

将所有内容连接到 NextJS

我们将暂时在 NextJS 中工作。 如果你没有使用 NextJS 甚至没有 Web 应用程序,也没关系。 我们已经拥有所有数据,因此那些没有在 NextJS 中工作的人可以简单地跳到步骤 4。 我们其他人将在那里与你汇合。

从版本 9.3.0 开始,NextJS 有三种不同的方法可以获取数据

  1. getStaticProps:在构建时获取数据
  2. getStaticPaths:根据获取的数据指定要预渲染的动态路由
  3. getServerSideProps:在每次请求时获取数据

现在是决定我们将在哪个时间点发出第一个获取提及的请求的时刻。 我们可以使用第一批提及在服务器上预渲染数据,或者我们可以将整个过程放到客户端。 我选择去客户端。

如果你也打算去客户端,我建议使用 SWR。 它是由 Vercel 团队构建的自定义钩子,它提供了良好的缓存、错误和加载状态——它甚至支持 React.Suspense

显示 Webmention 计数

许多博客在两个地方显示帖子上的评论数量:在博客文章的顶部(就像这个)以及在底部,就在评论列表的正上方。 让我们对 Webmentions 遵循相同的模式。

首先,让我们为计数创建一个组件

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>
  )
}

由于 SWR,即使我们使用 WebmentionsCounter 组件的两个实例,也只发出一个请求,并且它们都从相同的缓存中获益。

请随时查看我的源代码,了解发生了什么

显示提及

现在我们已经放置了组件,是时候让所有社交汁液流动起来!

在撰写本文时,useSWRpages 尚未记录。再加上 webmention.io 端点在响应中不提供集合信息(即没有偏移量或总页数),我找不到在这里使用 SWR 的方法。

因此,我目前的实现使用一个状态来保存当前页,另一个状态来处理提及 array,以及 useEffect 来处理请求。“加载更多”按钮在最后一次请求返回空数组后被禁用。

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>
    )}
    </>
  )
}

代码已简化,以便专注于本文主题。再次提醒,请随意查看完整的实现。

步骤 4:处理出站提及

感谢 Remy Sharp,处理从一个网站到其他网站的出站提及非常容易,并为每个可能的用例或偏好提供了选项。

最快速和最简单的方法是前往 Webmention.app,获取 API 令牌,并设置 Web 钩子。现在,如果您已将 RSS 提要到位,那么使用 IFTT 小程序甚至部署钩子,同样也很容易。

如果您想避免为此功能使用另一个第三方服务(我完全理解),Remy 已经开源了一个名为 wm 的 CLI 包,它可以作为构建后脚本运行。

但这还不够处理出站提及。为了让我们的提及包含的不仅仅是源 URL,我们需要将 微格式 添加到我们的信息中。微格式是关键,因为它是一种标准化的方式,使网站能够以 Webmention 启用网站可以消费的方式分发内容。

从最基本的意义上讲,微格式是标记中的一种基于类的表示法,它为每个部分提供额外的语义含义。在博客文章的情况下,我们将使用两种微格式。

  • h-entry:帖子条目
  • h-card:帖子的作者

h-entry 所需的大多数信息通常都在页面的标题中,因此标题组件最终可能看起来像这样。

<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>

就是这样。如果您使用的是 JSX,请记住用 className 替换 classdatetime 是驼峰式大小写(dateTime),并且可以使用新的 Date('2020-04-22').toISOString() 函数。

h-card 也是如此。在大多数情况下(像我一样),作者信息在文章下方。以下是我的页面页脚的外观。

<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>

现在,每当我们从这篇博客文章发送出站提及时,它都会将完整信息显示给接收者。

总结

我希望这篇文章能帮助您更多地了解 Webmentions(甚至了解 IndieWeb 整体),也许还能帮助您将此功能添加到您自己的网站或应用程序中。如果它做到了,请考虑将这篇文章分享到您的网络中。我将不胜感激!😉

参考资料

进一步阅读