Building Custom BlueSky Feeds with Next.js

Learn how to create and host your own custom BlueSky feeds using Next.js and deploy them for free


What are Custom BlueSky Feeds?

Custom feeds on BlueSky allow you to create personalized content streams based on specific criteria. Unlike the default algorithmic feed, custom feeds give you complete control over what content appears, making them powerful tools for content curation and discovery.

Sky Assistant Feeds

My project Sky Assistant is a tool that allows you to create custom feeds based on your own "assistants" which filter content based on your own logic or using AI!
It is built with Next.js and deployed on Vercel.

This guide will show you how to build a custom feed using Next.js and deploy it to your existing project but isn't going to go into the details of how to filter messages or read from the BlueSky firehose. If you are just interested creating a custom feed without code then try out Sky Assistant.

Example Feed created with Sky Assistant(and the code below): AI Agent Feed

Feeds and Next.js

Much of the officially supported BlueSky documentation shows how to build feeds using a custom dedicated server. However, if you are like me you already have a Next.js project hosted on Vercel or another platform.

This guide will show you how to build a custom feed using Next.js and deploy it to your existing project.

BlueSky Feeds

One Next.js project can host multiple feeds. Most of the real logic is in the feed generator, which uses the feed name to get the posts to display in the feed. This guide will focus on how to setup the feed generator but not the logic for getting the posts.
There is ample documentation on how to do that but if you are interested in dicussing it please reach out.

The Setup

Assumptions

  • You have a Next.js project hosted on Vercel or another platform.
  • You have a basic understanding of Next.js and BlueSky.
  • You are using the app router. (Although I'm sure it could be done with the pages router)
  • You have an account on BlueSky.

Disclaimer

This guide is not an official BlueSky guide and is not supported by Bluesky. It is a guide to help you get started with custom feeds using Next.js. I'm not really an expert, but this is what worked for me. If you have any questions or suggestions, please let me know.

One Time Setup

In your app directory, create a new folder called .well-known and insider there create the folder did.json. Then create a route.ts file inside the did.json folder. So you should have the following structure:

/app/.well-known/did.json/route.ts

In that file put the following code:


export async function GET() {
    const HOSTNAME = /* your domain  e.g. skyassistant.app*/; 
 
  return Response.json({
    '@context': ['https://www.w3.org/ns/did/v1'],
    'id': `did:web:${HOSTNAME}`,
    'service': [
      {
        'id': '#bsky_fg',
        'type': 'BskyFeedGenerator',
        'serviceEndpoint': `https://${HOSTNAME}`
      }
    ]
  })
}

Next you will create a folders:

/app/xrpc/app.bsky.feed.describeFeedGenerator
/app/xrpc/app.bsky.feed.getFeedSkeleton

Inside of /app/xrpc/app.bsky.feed.describeFeedGenerator create a route.ts file. This is where the uri of all your feeds will be listed.

import { getAllPublishedFeeds } from "@/utils/feedutil";
import config from "@/config";

export const dynamic = 'force-dynamic'


//uri format at://did:plc:bwc6trk3o3h2mgsy2egxxxxx/app.bsky.feed.generator/testfeedapsquared

export async function GET() {
    const HOSTNAME = config.domainName; 
    const SERVICE_DID = `did:web:${HOSTNAME}`;
 
     /* this function should get the list of all feeds  you are hosting, this is returned when we register a feed */
    const feedInfo = await getAllPublishedFeeds(); 

    const feedUris = feedInfo.map(f => ({
        uri: f.feedUri,
    }));

    return Response.json({
        did: SERVICE_DID,
        feeds: feedUris,
    });

}

Inside of /app/xrpc/app.bsky.feed.getFeedSkeleton create a route.ts file. This is what will be called when the feed is accessed.


export async function GET(request: Request) {
    const HOSTNAME = config.domainName; 
    const SERVICE_DID = `did:web:${HOSTNAME}`;
 
    const { searchParams } = new URL(request.url);
    const params: Record<string, string> = {};
    
    searchParams.forEach((value, key) => {
        params[key] = value;
    });
    

    const didResolver = new DidResolver({
        plcUrl: 'https://plc.directory',
    })

    const feedUri = new AtUri(params.feed);
    const feedId = feedUri.rkey;


    /* conditionalize this logic based on the feedId */

    if (feedId === 'test1') {
      /* this is where you would get the posts for the feed , likely from the database 
         for each different feedId you would return different set of posts in a different order based on your logic
      */
        return Response.json({
            feed: [
                { post: `at://did:plc:bwc6trk3o3h2mgsy2exxxxx/app.bsky.feed.post/3ld5wpccc3s78` },
                { post: `at://did:plc:bwc6trk3o3h2mgsy2egxxxxx/app.bsky.feed.post/3ld5wpccc3s77` },
            ]
        });
    } else if (feedId === 'test2') {
        return Response.json({
            feed: [
                { post: `at://did:plc:b36trk3o3h2mgsy2eg2xxx/app.bsky.feed.post/3ld5xxxcc3s23` },
            ]
        });
    }
}

Registering the Feed

Registering a feed is done by calling the putRecord endpoint.
This can be done via a command line or you can create an API endpoint. Here is a sample function that will do it.

export async function publishFeed(feedInfo: FeedInfo): Promise<{success: boolean, data: any}> {

    const HOSTNAME = config.domainName; 
    const SERVICE_DID = `did:web:${HOSTNAME}`;

    /* get the AT Protocol Agent  for the user you want to register the feed as */
    const agent = await getSkyAssistantAgent();

    /* all of these are just string values you can provider for you feed */
    const recordName = feedInfo.feedID;
    const displayName = feedInfo.feedLabel
    const description = feedInfo.feedDescription

   const record = {
    repo: agent.did,
    collection: 'app.bsky.feed.generator',
    rkey: recordName,
    record: {
      did: SERVICE_DID,
      displayName: displayName,
      description: description,
      //avatar: avatarRef,
      createdAt: new Date().toISOString(),
    },
  };

  const resp = await agent.com.atproto.repo.putRecord(record);

  if (resp.success) {
    /* store in the database the feedUri */
    await setFeedPublished(feedInfo.feedID, resp.data.uri);
  }

  return {success: resp.success, data: resp.data};
}

At this point you can deploy your project and register the feed.

For each feed you want to add you will need to call the publishFeed function once and update the logic in the getFeedSkeleton function to return the posts for that feed.

Conclusion

This is a rough guide to get you started. I'm sure there are many ways to improve it. If you have any questions or suggestions, please let me know.