TL;DR, Resposta Rápida
10 min de leituraUm gerador de feeds do Bluesky é um serviço web que implementa uma única consulta do AT Protocol, app.bsky.feed.getFeedSkeleton, e devolve uma lista de AT-URIs de publicações sem nenhum conteúdo dentro. A AppView do Bluesky hidrata essas URIs em publicações de verdade antes de o cliente ver qualquer coisa. O feed em si é declarado por um registro app.bsky.feed.generator no repo de quem o criou, que exige apenas did, displayName e createdAt, e aponta para o DID do serviço. O serviço prova que é dono desse DID com um .well-known/did.json que traz uma entrada de serviço do tipo BskyFeedGenerator. As requisições chegam com um JWT assinado pela chave de assinatura do repo de quem consulta, e limit tem teto de 100.
O que é um gerador de feeds do Bluesky?
Todo feed personalizado do app do Bluesky é servido por um gerador de feeds do Bluesky, um serviço HTTPS simples que responde a uma única consulta e devolve nada além de uma lista de endereços de publicações. Ele não guarda texto de publicação, nem avatares, nem contagem de curtidas. Ele ordena, e só.
O kit inicial oficial diz com todas as letras: "the server receives a request from a user's server and returns a list of post URIs with some optional metadata attached. Those posts are then hydrated into full views by the requesting server and sent back to the client."
Essa divisão é o projeto inteiro. Seu serviço decide quais publicações e em que ordem. A infraestrutura do Bluesky transforma os endereços em publicações renderizáveis.
Qual lexicon um gerador de feeds implementa?
Exatamente um: app.bsky.feed.getFeedSkeleton. O lexicon o descreve como "Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service."
Quatro lexicons cercam o fluxo, e saber qual lado implementa cada um economiza um dia de depuração:
| Lexicon | Tipo | Implementado por | Para que serve |
|---|---|---|---|
app.bsky.feed.generator | record | o repo de quem cria o feed | Declara que o feed existe e nomeia o DID do serviço |
app.bsky.feed.getFeedSkeleton | query | seu gerador de feeds | Devolve a lista ordenada de URIs de publicações |
app.bsky.feed.describeFeedGenerator | query | seu gerador de feeds | Lista quais URIs de feed este serviço atende |
app.bsky.feed.getFeed | query | a AppView do Bluesky | Devolve o feed hidratado que o cliente realmente renderiza |
describeFeedGenerator explicitamente não é um método da AppView: "Does not require auth; implemented by Feed Generator services (not App View)." Chamá-lo na AppView devolve {"error":"MethodNotImplemented","message":"Method Not Implemented"}, enquanto a mesma chamada no serviço Discover do próprio Bluesky em discover.bsky.app devolve todas as URIs de feed que aquele serviço hospeda.
O que vai no registro app.bsky.feed.generator?
O registro é o que faz o feed aparecer no app, e ele mora no seu próprio repo, não no seu servidor. O lexicon o chama de um "Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository."
| Campo | Tipo | Obrigatório | Restrição |
|---|---|---|---|
did | string, format: did | sim | O DID do serviço que atende o feed |
displayName | string | sim | maxGraphemes: 24, maxLength: 240 |
createdAt | string, format: datetime | sim | |
description | string | não | maxGraphemes: 300, maxLength: 3000 |
descriptionFacets | array | não | entradas de app.bsky.richtext.facet |
avatar | blob | não | accept: image/png, image/jpeg, maxSize: 1000000 |
acceptsInteractions | boolean | não | Inscreve o feed em app.bsky.feed.sendInteractions |
contentMode | string | não | contentModeUnspecified ou contentModeVideo |
Três campos são obrigatórios e nenhum deles é o algoritmo. Repare no teto de 24 grafemas do displayName, curto o bastante para a maioria dos nomes de feed ser cortada antes de você notar, e no teto de 1.000.000 de bytes do avatar. A distinção entre grafema e byte por trás desses números é a mesma que derruba gente nos offsets de bytes dos facets do Bluesky.
contentMode é o único campo que muda a renderização. app.bsky.feed.defs#contentModeVideo é um token que "Declares the feed generator returns posts containing app.bsky.embed.video embeds," e joga o cliente no player de vídeo em tela cheia.
O rkey do registro vira o identificador público do feed. O próprio feed Discover do Bluesky é endereçado como at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot, em que o DID pertence à conta bsky.app e whats-hot é a chave do registro.
Como o Bluesky encontra meu serviço?
Por um documento DID com uma entrada de serviço do tipo BskyFeedGenerator. O registro dá um DID ao Bluesky. Resolver esse DID dá uma URL ao Bluesky.
O kit inicial monta isso como um did:web, então o seu próprio domínio serve o documento em /.well-known/did.json. O serviço Discover do Bluesky em produção faz o mesmo, e você pode baixá-lo:
{
"id": "did:web:discover.bsky.app",
"service": [
{
"id": "#bsky_fg",
"type": "BskyFeedGenerator",
"serviceEndpoint": "https://discover.bsky.app"
}
]
}O id #bsky_fg e o type BskyFeedGenerator são strings fixas, os dois. Erre qualquer um e o feed não resolve para nada.
Duas exigências de hospedagem vêm do README, não de alguma especificação. "Your feed will need to be accessible at the value supplied to the FEEDGEN_HOSTNAME environment variable," e "The service must be set up to respond to HTTPS queries over port 443." HTTP puro não é opção, e porta fora do padrão também não.
O kit inicial ainda aponta uma armadilha de migração que vale considerar antes de escolher o domínio: talvez você queira did:plc em vez de did:web "if you expect this Feed Generator to be long-standing and possibly migrating domains." Um did:web é o seu nome de host. Troque de hospedagem e todo registro publicado que aponta para ele quebra. Um did:plc sobrevive à mudança.
AdaptlyPost
Teste grátis de 7 dias
Análises multiplataforma
Caixa Social
Assistente com IA

O que getFeedSkeleton devolve?
Um cursor e um array de entradas app.bsky.feed.defs#skeletonFeedPost, cada uma delas uma AT-URI mais contexto opcional. Esta é uma resposta real do serviço Discover do Bluesky, cortada em dois itens:
{
"cursor": "eyJvIjoiMjAyNi0wOS0xMVQxMDo0OTowNi42NTI4NDk4NzVaIiwibiI6IjIwMjYtMDktMTFUMjI6NDk6MDYuNjUyODQ5ODc1WiIsInNzIjoiMjAyNi0wOS0xMVQyMjo0OTowNi42NTI4NDk4NzVaIiwibHUiOiIyMDI2LTA5LTExVDIyOjQ5OjA2LjY1NjYyMzMwM1oiLCJwZyI6MSwibSI6IkFRQUFBQUFBQUFBSkFBQUFPakFBQUFJQUFBRHNWZ0FBa1ZjQUFCZ0FBQUFhQUFBQUtMTTNrUT09In0=",
"feed": [
{
"feedContext": "th-nature_animals_pets",
"post": "at://did:plc:mpvivcve3y2fdlw7pmq7niwz/app.bsky.feed.post/3mvb4r2qh4k2r"
},
{
"feedContext": "th-science_research",
"post": "at://did:plc:fb57h2fswm7s2qqzxflizlk7/app.bsky.feed.post/3mvasir35ec2e"
}
]
}Em cada entrada, só post é obrigatório. feedContext é opcional e limitado a maxLength: 2000, descrito no lexicon como "Context that will be passed through to client and may be passed to feed generator back alongside interactions." O Bluesky usa isso para rótulos de tema, como mostra a string th-nature_animals_pets acima. A resposta também pode carregar reqId, um "Unique identifier per request," limitado a 100 caracteres.
O parâmetro limit tem piso e teto documentados: minimum: 1, maximum: 100, default: 50, e a AppView faz valer isso. Pedir 101 devolve:
{ "error": "InvalidRequest", "message": "Invalid app.bsky.feed.getFeed params: integer too big (maximum 100, got 101)" }A paginação corre sobre um cursor opaco que o seu serviço inventa. O kit inicial é específico sobre o formato que quer: "We strongly encourage that the cursor be unique per feed item to prevent unexpected behavior in pagination. We recommend, for instance, a compound cursor with a timestamp + a CID."
Como a requisição é autenticada?
Com um JWT assinado pela chave de assinatura do repo de quem consulta, e o kit inicial detalha tanto o cabeçalho quanto o payload:
const header = {
type: 'JWT',
alg: 'ES256K', // (key algorithm) - in this case secp256k1
};
const payload = {
iss: 'did:example:alice', // (issuer) the requesting user's DID
aud: 'did:example:feedGenerator', // (audience) the DID of the Feed Generator
exp: 1683643619, // (expiration) unix timestamp in seconds
};Verificá-lo é opcional, e a orientação depende do que o seu feed faz: "If you are creating a generic feed that does not differ for different users, you do not need to check auth. But if a user's state (such as follows or likes) is taken into account, we strongly encourage you to validate their auth token." Um feed que personaliza por quem a pessoa segue sem checar o iss é um feed que qualquer um consulta se passando por qualquer outro.
O serviço Discover do próprio Bluesky responde a requisições getFeedSkeleton sem autenticação com um esqueleto de verdade, um jeito barato de testar o seu entendimento do endpoint.

- Sem follows, sem likes, sem estado por usuário
- A verificação de auth é opcional
- O ranking lê o DID do iss
- Sem verificação, qualquer um pode consultar como qualquer um
De onde vêm as publicações?
Do firehose, em quase toda implementação real: "For most use cases, we recommend subscribing to the firehose at com.atproto.sync.subscribeRepos. This websocket will send you every record that is published on the network."
Como um gerador de feeds nunca entrega conteúdo, ele pode jogar quase tudo isso fora. "Unless your algorithm is intended to provide posts you missed or something similar, you can likely garbage collect any data that is older than 48 hours."
Os três formatos que o kit inicial esboça cobrem a maioria dos feeds que as pessoas constroem. Um feed do que está bombando soma curtidas por publicação e devolve publicações recentes acima de um limiar. Um feed de comunidade monta uma lista de DIDs e devolve tudo o que eles publicam. Um feed temático passa o texto da publicação por um comparador de palavras-chave ou por um modelo. O firehose fica ao lado dos demais limites de taxa da API do Bluesky dentro dos quais você vai trabalhar.
O que os docs deixam indefinido?
Várias coisas, e essas lacunas são onde geradores de feeds quebram em produção, não em teste.
O erro declarado não bate com o erro entregue. Tanto getFeedSkeleton quanto getFeed declaram exatamente um erro nos seus lexicons, UnknownFeed. Peça à AppView um feed que não existe e você recebe {"error":"InvalidRequest","message":"could not find feed"}. Peça ao serviço Discover do próprio Bluesky e você recebe {"message":"no such feed: \"nope-not-real\""}, sem campo error nenhum. Dois formatos em produção, e nenhum deles com o nome documentado.
O kit inicial é mais velho que o lexicon que ele descreve. A seção sobre os metadados do esqueleto diz "for now, the only defined reason is a repost, but this is open to extension." O skeletonFeedPost atual tem uma união reason com dois membros, #skeletonReasonRepost e #skeletonReasonPin. Desenvolva contra o tipo Reason do README e você não vai saber que fixar publicação existe.
AdaptlyPost
Teste grátis de 7 dias
Análises multiplataforma
Caixa Social
Assistente com IA
A história da hidratação também se mexeu. O README diz "In the future, the PDS will hydrate the feed with the help of an App View, but for now, the PDS handles hydration itself," enquanto o lexicon de getFeed hoje diz secamente "Implemented by App View."
Nenhum limite é publicado sobre o esqueleto em si. limit limita uma requisição a 100 itens, mas o array feed da resposta não carrega maxLength, e nada diz o que acontece se um gerador devolver mais do que foi pedido. getFeedGenerator devolve isOnline e isValid, descritos como se o serviço "has been online recently, or else seems to be inactive" e se ele "is compatible with the record declaration." Nem "recently" nem "compatible" estão definidos em lugar nenhum.
Como eu publico o feed?
Escrevendo o registro com com.atproto.repo.putRecord no seu próprio repo. O script de publicação do kit inicial faz exatamente isso, com collection em app.bsky.feed.generator, o rkey no nome curto que vira a URL do feed, e did em did:web:$FEEDGEN_HOSTNAME, a menos que você tenha informado um DID de serviço próprio. Mudar o nome, a descrição ou o avatar é a mesma chamada com o mesmo rkey. Não há fila de aprovação.
Nada disso substitui publicar do jeito comum. Um gerador de feeds decide o que quem lê vai ver; colocar as suas próprias publicações na rede continua sendo agendar publicações no Bluesky por uma escrita de registro normal, o que o AdaptlyPost resolve junto com as outras redes em que publica, com o engajamento caindo depois no analytics do Bluesky.
Perguntas frequentes
Preciso de um servidor para rodar um gerador de feeds do Bluesky?
Sim. O registro do feed mora no seu repo, mas getFeedSkeleton tem de ser respondido por um serviço hospedado por você, em HTTPS na porta 443, no nome de host para onde o seu documento DID aponta. Não existe jeito de definir um feed personalizado só por dentro do Bluesky.
Um gerador de feeds consegue ver o conteúdo das publicações?
Só o que ele mesmo coleta, em geral do firehose. O esqueleto que ele devolve carrega AT-URIs e mais nada, e a hidratação acontece do lado do Bluesky depois que o seu serviço já respondeu.
Qual é a diferença entre getFeed e getFeedSkeleton?
getFeedSkeleton é seu e devolve entradas skeletonFeedPost, que são URIs pelados. getFeed é da AppView e devolve entradas feedViewPost, que são publicações completas com perfis de autor e contagens. Os clientes chamam getFeed. O seu serviço, nunca.
Como sei se o meu feed está quebrado?
Chame app.bsky.feed.getFeedGenerator com a AT-URI do seu feed. Ele devolve isOnline e isValid ao lado da visão do feed. O Bluesky não publica quão recentemente um serviço precisa ter respondido para contar como online, então trate um false como um empurrão para conferir os seus próprios logs.
Posso cobrar por um feed ou restringir quem vê?
O protocolo te dá o gancho e nenhuma cobrança. A autenticação é opcional no getFeedSkeleton, e quando existe o JWT carrega o DID de quem consulta na claim iss, então filtrar por uma lista de DIDs é direto. O pagamento, e como alguém recusado é avisado, ficam inteiramente por conta do seu serviço.
Como eu meço o desempenho de um feed?
O Bluesky publica um número só sobre um feed, o likeCount da sua generatorView. Todo o resto sobre como o feed se sai você tem de registrar por conta própria, a partir das requisições que chegam ao seu serviço. Essa escassez é a mesma tratada em o Bluesky tem analytics.
O que acontece se um cliente pedir mais de 100 itens a um feed?
A AppView impõe esse teto sozinha. Pedir um limit de 101 ao getFeed devolve um erro InvalidRequest dizendo que o inteiro é grande demais, com máximo de 100. O parâmetro limit tem mínimo 1, máximo 100 e padrão 50, então um gerador de feeds nunca precisa construir essa rejeição por conta própria.
Por que o starter kit recomenda did:plc em vez de did:web para alguns geradores de feeds?
Um did:web é literalmente o seu domínio, então trocar de domínio quebra todo registro publicado que aponta para ele. Um did:plc sobrevive a uma mudança, por isso o starter kit recomenda isso para qualquer feed pensado para durar ou com chance de migrar.
Como deve ser o cursor de paginação de um gerador de feeds?
O lexicon declara cursor como uma string simples, sem nenhuma restrição, e o starter kit chama o valor de opaco e inteiramente uma escolha do gerador. O starter kit recomenda um cursor composto, um timestamp junto com um CID, e insiste que o cursor seja único por item do feed para evitar bugs de paginação.
O que o acceptsInteractions faz no registro app.bsky.feed.generator?
Definir acceptsInteractions como true inscreve o feed em app.bsky.feed.sendInteractions, permitindo que os clientes informem com quais posts um usuário interagiu. É um campo opcional e booleano, e nada na documentação o exige para um feed funcionar.
Coloque isso em prática com o AdaptlyPost
Este artigo foi útil para você?
Conte-nos o que você achou!
Veja-nos mais no Google
Um clique marca a AdaptlyPost como fonte preferida e nossos artigos passam a aparecer mais acima nas suas Principais notícias, no modo IA e nas visões gerais com IA.
Antes de ir...
AdaptlyPost
Agende seu conteúdo em todas as plataformas
Gerencie todas as suas contas de redes sociais em um só lugar com o AdaptlyPost.
Análises multiplataforma
Caixa Social
Assistente com IA
Termos relacionados do glossário


Por que Bluesky facets byteStart byteEnd contam bytes, não caracteres
Os offsets Bluesky facets byteStart byteEnd contam bytes UTF-8, não índices UTF-16 do JavaScript. O aviso do lexicon, um exemplo e código que acerta.


Por que o limite de tamanho de imagem do Bluesky é de 2,000,000 bytes
O Bluesky limita cada imagem de post a 2,000,000 bytes, definidos pelo maxSize no lexicon images. Avatares e banners param em 1,000,000 bytes.


Convertendo o limite de taxa da API do Bluesky em posts por hora
O limite de taxa da API do Bluesky em escritas é um orçamento de pontos, não de requisições: 5,000 pontos por hora, 3 por post, ou 1,666 posts por hora.
Artigos Relacionados


Os dois registros por trás de um handle de domínio personalizado no Bluesky
Um handle de domínio personalizado no Bluesky precisa de um registro: TXT em _atproto ou texto puro em /.well-known/atproto-did. Os valores e as TLDs vetadas.


Meta fixa em 1.000 o limite de caracteres do alt_text na API do Instagram
Meta limita a 1.000 o limite de caracteres do alt_text na API do Instagram e o restringe a imagens estáticas. Reels e stories não aceitam texto alternativo.


Por trás da etiqueta de IA do TikTok existem duas etiquetas
A etiqueta de IA do TikTok vem de duas formas: uma que você aplica com is_aigc e outra automática, de efeitos de IA ou C2PA, que não dá para remover.

