Последняя версия моего веб-сайта построена на Next.js, в частности, на замечательном начальном блоге Tailwind/Next.js Тимоти Лина. Я немного изменил его, изменив цветовую схему, убрав такие компоненты, как аналитика, комментарии и некоторые другие, а также создав несколько новых страниц (например, мою страницу сейчас). В рамках этого процесса я хотел добавить в шаблон поддержку веб-упоминаний, интегрировав упоминания с Mastodon, Medium.com и других доступных источников.

Чтобы начать это, вам нужно войти в систему и создать учетную запись в webmention.io и Bridgy. Первый предоставляет вам пару метатегов, которые собирают веб-упоминания, второй связывает ваш сайт с социальными сетями.

После того, как вы добавили соответствующие теги из webmention.io, подключили нужные учетные записи к Bridgy и получили некоторые упоминания на этих сайтах, вы сможете получить доступ к этим упоминаниям через их API. Для моих целей (и ваших, если вы выберете тот же подход) это выглядит как следующий маршрут API Next.js:

import loadWebmentions from '@/lib/webmentions'

export default async function handler(req, res) {
    const target = req.query.target
    const response = await loadWebmentions(target)
    res.json(response)
}

Вы можете увидеть мои упоминания на живом маршруте здесь.

Я решил отображать упоминания о моих постах (бусты, на языке Mastodon), лайки и комментарии. Для бустов я отрисовываю счет, для лайков я отрисовываю аватарку, а для упоминаний я отрисовываю комментарий полностью. Компонент, который обрабатывает это, выглядит следующим образом:

import siteMetadata from '@/data/siteMetadata'
import { Heart, Rocket } from '@/components/icons'
import { Spin } from '@/components/Loading'
import { useRouter } from 'next/router'
import { useJson } from '@/hooks/useJson'
import Link from 'next/link'
import Image from 'next/image'
import { formatDate } from '@/utils/formatters'

const WebmentionsCore = () => {
    const { asPath } = useRouter()
    const { response, error } = useJson(`/api/webmentions?target=${siteMetadata.siteUrl}${asPath}`)
    const webmentions = response?.children
    const hasLikes =
        webmentions?.filter((mention) => mention['wm-property'] === 'like-of').length > 0
    const hasComments =
        webmentions?.filter((mention) => mention['wm-property'] === 'in-reply-to').length > 0
    const boostsCount = webmentions?.filter(
        (mention) =>
            mention['wm-property'] === 'repost-of' || mention['wm-property'] === 'mention-of'
    ).length
    const hasBoosts = boostsCount > 0
    const hasMention = hasLikes || hasComments || hasBoosts

    if (error) return null
    if (!response) return <Spin className="my-2 flex justify-center" />

    const Boosts = () => {
        return (
            <div className="flex flex-row items-center">
                <div className="mr-2 h-5 w-5">
                    <Rocket />
                </div>
                {` `}
                <span className="text-sm">{boostsCount}</span>
            </div>
        )
    }

    const Likes = () => (
        <>
            <div className="flex flex-row items-center">
                <div className="mr-2 h-5 w-5">
                    <Heart />
                </div>
                <ul className="ml-2 flex flex-row">
                    {webmentions?.map((mention) => {
                        if (mention['wm-property'] === 'like-of')
                            return (
                                <li key={mention['wm-id']} className="-ml-2">
                                    <Link
                                        href={mention.url}
                                        target="_blank"
                                        rel="noopener noreferrer"
                                    >
                                        <Image
                                            className="h-10 w-10 rounded-full border border-primary-500 dark:border-gray-500"
                                            src={mention.author.photo}
                                            alt={mention.author.name}
                                            width="40"
                                            height="40"
                                        />
                                    </Link>
                                </li>
                            )
                    })}
                </ul>
            </div>
        </>
    )

    const Comments = () => {
        return (
            <>
                {webmentions?.map((mention) => {
                    if (mention['wm-property'] === 'in-reply-to') {
                        return (
                            <Link
                                className="border-bottom flex flex-row items-center border-gray-100 pb-4"
                                key={mention['wm-id']}
                                href={mention.url}
                                target="_blank"
                                rel="noopener noreferrer"
                            >
                                <Image
                                    className="h-12 w-12 rounded-full border border-primary-500 dark:border-gray-500"
                                    src={mention.author.photo}
                                    alt={mention.author.name}
                                    width="48"
                                    height="48"
                                />
                                <div className="ml-3">
                                    <p className="text-sm">{mention.content?.text}</p>
                                    <p className="mt-1 text-xs">{formatDate(mention.published)}</p>
                                </div>
                            </Link>
                        )
                    }
                })}
            </>
        )
    }

    return (
        <>
            {hasMention ? (
                <div className="text-gray-500 dark:text-gray-100">
                    <h4 className="pt-3 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:text-2xl md:leading-10 ">
                        Webmentions
                    </h4>
                    {hasBoosts ? (
                        <div className="pt-2 pb-4">
                            <Boosts />
                        </div>
                    ) : null}
                    {hasLikes ? (
                        <div className="pt-2 pb-4">
                            <Likes />
                        </div>
                    ) : null}
                    {hasComments ? (
                        <div className="pt-2 pb-4">
                            <Comments />
                        </div>
                    ) : null}
                </div>
            ) : null}
        </>
    )
}

export default WebmentionsCore

Мы получаем URL-адрес сообщения из фиксированного URL-адреса сайта в метаданных моего сайта, URI из маршрутизатора Next.js, объединяем их и передаем в качестве пути API к моему хуку useJson, который обертывает useSWR:

import { useEffect, useState } from 'react'
import useSWR from 'swr'

export const useJson = (url: string, props?: any) => {
    const [response, setResponse] = useState<any>({})

    const fetcher = (url: string) =>
        fetch(url)
            .then((res) => res.json())
            .catch()
    const { data, error } = useSWR(url, fetcher, { fallbackData: props, refreshInterval: 30000 })

    useEffect(() => {
        setResponse(data)
    }, [data, setResponse])

    return {
        response,
        error,
    }
}

Параметр target сужает возвращаемые упоминания до тех, которые имеют отношение к текущему сообщению. Получив соответствующий ответ от службы, мы оцениваем данные, чтобы определить, какие типы упоминаний у нас есть, создаем компоненты JSX для их отображения и условно отображаем их на основе наличия соответствующих данных об упоминаниях.

Компонент WebmentionsCore динамически загружается в каждую запись с использованием следующего родительского компонента:

import dynamic from 'next/dynamic'
import { Spin } from '@/components/Loading'

const Webmentions = dynamic(() => import('@/components/webmentions/WebmentionsCore'), {
    ssr: false,
    loading: () => <Spin className="my-2 flex justify-center" />,
})

export default Webmentions

Окончательное отображение выглядит так:

Первоначально опубликовано на https://coryd.dev 18 февраля 2023 г.