☾ ☼ ☽
06:08 11/14/2024

Farcaster as a Comment System


Inspired by Paragraph I wanted to use Farcaster to add Disqus style comments to my digital garden.

Previously I handled “comments” using a Discuss on Twitter CTA that linked to the search results for the posts public URL. The Twitter API did not factor into the equation, all conversation was abstracted away from the actual content. Simple, but far from ideal.

const comments = `https://mobile.twitter.com/search?q=${encodeURIComponent(
  `https://your-domain.com/${slug}`
)}`;

The Farcaster approach is fairly straightforward — use the Searchcaster api to query for the public URL of a page. If any results are returned, display them (permissionlessly!) below the content. The one caveat is that the api updates every 30 minutes, so sometimes you need to wait before comments load on a post.

This takes two calls to get the basic functionality going. getTopLevelCasts and getMerkleRootCasts. Note: I am using Next13 for this, which extends fetch() with some additional features.

getTopLevelCasts returns any cast that directly mentions the content URL.

// getTopLevelCasts.tsx

export default async function getTopLevelCasts(path: string, slug: string) {
  const uri = `https://searchcaster.xyz/api/search?text=your-domain.com/${slug}`;

  const res = await fetch(uri, {
    next: {
      revalidate: 1 * 30,
    },
  }).then((res) => res.json());
  const casts = res.casts;
  return casts;
}

getMerkleRootCasts is used in a loop, where each topLevelCast's merkleRoot is passed in to fetch any descendant casts.

// getMerkleRootCasts.tsx

export default async function getMerkleRoot(merkleRoot: string) {
  const uri = `https://searchcaster.xyz/api/search?merkleRoot=${merkleRoot}`;
  const res = await fetch(uri, {
    next: {
      revalidate: 1 * 30,
    },
  }).then((res) => res.json());
  const casts = res.casts;
  return casts;
}

Ultimately I only need the merkleRoot from getTopLevelCasts. I’m not rendering any content from that call directly. getMerkleRootCasts returns the topLevelCasts and its descendants, which makes the list structure a bit cleaner.

// comments.tsx

<ul>
  {_topLevelComments.map((comment) => {
    return (
      <li key={comment.merkleRoot}>
        <CommentBody {...comment} />
        {comment.numReplyChildren > 0 && (
          <ul>
            {_merkleRootComments[0]
              .map((comment) => {
                return (
                  <li key={comment.merkleRoot}>
                    <CommentBody {...comment} />
                  </li>
                );
              })
              .slice(0, -1)}
          </ul>
        )}
      </li>
    );
  })}
</ul>

Comment threads can be structured various ways. This may change down the line as this digital garden grows, but it is perfectly sufficient for now. I encourage you to explore the response from the Searchcaster API and experiment with how to best display your threads.