TL;DR, Réponse Rapide
10 min de lectureUn générateur de feeds Bluesky est un service web qui implémente une seule requête de l'AT Protocol, app.bsky.feed.getFeedSkeleton, et renvoie une liste d'AT-URIs de publications sans aucun contenu dedans. L'AppView de Bluesky hydrate ces URIs en vraies publications avant même que le client les voie. Le feed lui-même est déclaré par un enregistrement app.bsky.feed.generator placé dans le repo de son auteur, qui n'exige que did, displayName et createdAt, et qui pointe vers le DID du service. Le service prouve qu'il possède ce DID avec un .well-known/did.json portant une entrée de service de type BskyFeedGenerator. Les requêtes arrivent avec un JWT signé par la clé de signature du repo de la personne qui interroge, et limit est plafonné à 100.
Qu'est-ce qu'un générateur de feeds Bluesky ?
Chaque feed personnalisé de l'app Bluesky est servi par un générateur de feeds Bluesky, un simple service HTTPS qui répond à une seule requête et ne renvoie rien d'autre qu'une liste d'adresses de publications. Il ne détient aucun texte de publication, aucun avatar et aucun compteur de likes. Il classe, et c'est tout.
Le kit de démarrage officiel le dit sans détour : « 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. »
Ce partage constitue toute la conception. Votre service décide quelles publications et dans quel ordre. L'infrastructure de Bluesky transforme les adresses en publications affichables.
Quel lexicon un générateur de feeds implémente-t-il ?
Un seul : app.bsky.feed.getFeedSkeleton. Le lexicon le décrit comme « 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. »
Quatre lexicons entourent ce flux, et savoir quel côté implémente lequel vous épargne une journée de débogage :
| Lexicon | Type | Implémenté par | Rôle |
|---|---|---|---|
app.bsky.feed.generator | record | le repo de l'auteur du feed | Déclare que le feed existe et nomme le DID du service |
app.bsky.feed.getFeedSkeleton | query | votre générateur de feeds | Renvoie la liste classée des URIs de publications |
app.bsky.feed.describeFeedGenerator | query | votre générateur de feeds | Énumère les URIs de feed que ce service dessert |
app.bsky.feed.getFeed | query | l'AppView de Bluesky | Renvoie le feed hydraté que le client affiche réellement |
describeFeedGenerator n'est explicitement pas une méthode de l'AppView : « Does not require auth; implemented by Feed Generator services (not App View). » L'appeler sur l'AppView renvoie {"error":"MethodNotImplemented","message":"Method Not Implemented"}, alors que le même appel sur le service Discover de Bluesky à discover.bsky.app renvoie toutes les URIs de feed que ce service héberge.
Que met-on dans l'enregistrement app.bsky.feed.generator ?
C'est l'enregistrement qui fait apparaître le feed dans l'app, et il vit dans votre propre repo plutôt que sur votre serveur. Le lexicon le présente comme un « Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository. »
| Champ | Type | Obligatoire | Contrainte |
|---|---|---|---|
did | string, format: did | oui | Le DID du service qui dessert le feed |
displayName | string | oui | maxGraphemes: 24, maxLength: 240 |
createdAt | string, format: datetime | oui | |
description | string | non | maxGraphemes: 300, maxLength: 3000 |
descriptionFacets | array | non | entrées app.bsky.richtext.facet |
avatar | blob | non | accept: image/png, image/jpeg, maxSize: 1000000 |
acceptsInteractions | boolean | non | Inscrit le feed à app.bsky.feed.sendInteractions |
contentMode | string | non | contentModeUnspecified ou contentModeVideo |
Trois champs sont obligatoires et aucun n'est l'algorithme. Notez le plafond de 24 graphèmes sur displayName, assez court pour que la plupart des noms de feed soient tronqués avant que vous le remarquiez, et le plafond de 1 000 000 d'octets sur l'avatar. La distinction entre graphème et octet derrière ces nombres est celle-là même qui fait trébucher sur les offsets d'octets des facets Bluesky.
contentMode est le seul champ qui change le rendu. app.bsky.feed.defs#contentModeVideo est un token qui « Declares the feed generator returns posts containing app.bsky.embed.video embeds, » et bascule le client vers le lecteur vidéo plein écran.
Le rkey de l'enregistrement devient l'identifiant public du feed. Le feed Discover de Bluesky lui-même s'adresse comme at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot, où le DID appartient au compte bsky.app et whats-hot est la clé de l'enregistrement.
Comment Bluesky trouve-t-il mon service ?
Par un document DID portant une entrée de service de type BskyFeedGenerator. L'enregistrement donne un DID à Bluesky. Résoudre ce DID donne une URL à Bluesky.
Le kit de démarrage met cela en place sous forme de did:web, si bien que votre propre domaine sert le document à /.well-known/did.json. Le service Discover de Bluesky en production fait pareil, et vous pouvez le récupérer :
{
"id": "did:web:discover.bsky.app",
"service": [
{
"id": "#bsky_fg",
"type": "BskyFeedGenerator",
"serviceEndpoint": "https://discover.bsky.app"
}
]
}L'id #bsky_fg et le type BskyFeedGenerator sont deux chaînes figées. Trompez-vous sur l'une ou l'autre et le feed ne résout vers rien.
Deux contraintes d'hébergement viennent du README et non d'une spécification. « Your feed will need to be accessible at the value supplied to the FEEDGEN_HOSTNAME environment variable, » et « The service must be set up to respond to HTTPS queries over port 443. » Le HTTP simple n'est pas une option, un port non standard non plus.
Le kit de démarrage signale aussi un piège de migration à peser avant de choisir un domaine : vous voudrez peut-être un did:plc plutôt qu'un did:web « if you expect this Feed Generator to be long-standing and possibly migrating domains. » Un did:web, c'est votre nom d'hôte. Changez d'hébergeur et tout enregistrement publié qui pointe dessus casse. Un did:plc survit au déménagement.
AdaptlyPost
Essai gratuit de 7 jours
Analyses multiplateforme
Boîte sociale
Assistant IA

Que renvoie getFeedSkeleton ?
Un curseur et un tableau d'entrées app.bsky.feed.defs#skeletonFeedPost, chacune étant une AT-URI plus un contexte facultatif. Voici une vraie réponse du service Discover de Bluesky, réduite à deux éléments :
{
"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"
}
]
}Seul post est obligatoire dans chaque entrée. feedContext est facultatif et plafonné à maxLength: 2000, décrit dans le lexicon comme « Context that will be passed through to client and may be passed to feed generator back alongside interactions. » Bluesky s'en sert pour des étiquettes de sujet, comme le montre la chaîne th-nature_animals_pets ci-dessus. La réponse peut aussi porter reqId, un « Unique identifier per request, » plafonné à 100 caractères.
Le paramètre limit a un plancher et un plafond documentés : minimum: 1, maximum: 100, default: 50, et l'AppView les applique. Demander 101 renvoie :
{ "error": "InvalidRequest", "message": "Invalid app.bsky.feed.getFeed params: integer too big (maximum 100, got 101)" }La pagination repose sur un curseur opaque que votre service invente. Le kit de démarrage est précis sur la forme attendue : « 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. »
Comment la requête est-elle authentifiée ?
Par un JWT signé avec la clé de signature du repo de la personne qui interroge, et le kit de démarrage détaille l'en-tête comme la charge utile :
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
};Le vérifier est facultatif, et la consigne dépend de ce que fait votre feed : « 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. » Un feed qui se personnalise selon les abonnements sans contrôler l'iss est un feed que n'importe qui peut interroger au nom de n'importe qui.
Le service Discover de Bluesky répond aux requêtes getFeedSkeleton non authentifiées par un vrai squelette, ce qui est un moyen peu coûteux de tester votre compréhension de l'endpoint.

- Pas de follows, pas de likes, pas d'état propre à l'utilisateur
- La vérification d'auth est facultative
- Le classement lit le DID de iss
- Sans vérification, n'importe qui peut interroger en se faisant passer pour n'importe qui
D'où viennent les publications ?
Du firehose, dans la quasi-totalité des implémentations réelles : « 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. »
Comme un générateur de feeds ne sert jamais de contenu, il peut jeter presque tout. « 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. »
Les trois formes esquissées par le kit de démarrage couvrent la plupart des feeds que les gens construisent. Un feed des tendances compte les likes par publication et renvoie les publications récentes au-dessus d'un seuil. Un feed de communauté compile une liste de DIDs et renvoie tout ce que ces comptes publient. Un feed thématique passe le texte des publications dans un détecteur de mots-clés ou un modèle. Le firehose côtoie les autres limites de débit de l'API Bluesky dans lesquelles vous allez travailler.
Que laissent-ils indéfini dans les docs ?
Plusieurs choses, et ces trous sont l'endroit où les générateurs de feeds cassent en production plutôt qu'en test.
L'erreur déclarée ne correspond pas à l'erreur livrée. getFeedSkeleton comme getFeed déclarent exactement une erreur dans leurs lexicons, UnknownFeed. Demandez à l'AppView un feed qui n'existe pas et vous obtenez {"error":"InvalidRequest","message":"could not find feed"}. Demandez-le au service Discover de Bluesky et vous obtenez {"message":"no such feed: \"nope-not-real\""}, sans aucun champ error. Deux formes en production, et aucune ne porte le nom documenté.
Le kit de démarrage est plus vieux que le lexicon qu'il décrit. Sa section sur les métadonnées du squelette dit « for now, the only defined reason is a repost, but this is open to extension. » Le skeletonFeedPost actuel a une union reason à deux membres, #skeletonReasonRepost et #skeletonReasonPin. Développez contre le type Reason du README et vous ignorerez que l'épinglage existe.
AdaptlyPost
Essai gratuit de 7 jours
Analyses multiplateforme
Boîte sociale
Assistant IA
L'histoire de l'hydratation a bougé elle aussi. Le README dit « In the future, the PDS will hydrate the feed with the help of an App View, but for now, the PDS handles hydration itself, » tandis que le lexicon de getFeed dit désormais platement « Implemented by App View. »
Aucune limite n'est publiée sur le squelette lui-même. limit plafonne une requête à 100 éléments, mais le tableau feed de la réponse ne porte aucun maxLength, et rien ne dit ce qui se passe si un générateur en renvoie plus que demandé. getFeedGenerator renvoie isOnline et isValid, décrits comme le fait que le service « has been online recently, or else seems to be inactive » et qu'il « is compatible with the record declaration. » Ni « recently » ni « compatible » ne sont définis nulle part.
Comment publier le feed ?
En écrivant l'enregistrement avec com.atproto.repo.putRecord dans votre propre repo. Le script de publication du kit de démarrage fait exactement cela, avec collection réglé sur app.bsky.feed.generator, le rkey réglé sur le nom court qui devient l'URL du feed, et did réglé sur did:web:$FEEDGEN_HOSTNAME sauf si vous avez fourni votre propre DID de service. Changer le nom, la description ou l'avatar, c'est le même appel avec le même rkey. Il n'y a pas de file d'approbation.
Rien de tout cela ne remplace la publication ordinaire. Un générateur de feeds décide de ce qu'un lecteur voit ; faire arriver vos propres publications sur le réseau reste l'affaire de programmer des publications Bluesky via une écriture d'enregistrement normale, ce que AdaptlyPost gère aux côtés des autres réseaux où il publie, l'engagement atterrissant ensuite dans les analytics Bluesky.
Foire aux questions
Ai-je besoin d'un serveur pour faire tourner un générateur de feeds Bluesky ?
Oui. L'enregistrement du feed vit dans votre repo, mais getFeedSkeleton doit être servi par un service que vous hébergez, en HTTPS sur le port 443, au nom d'hôte vers lequel pointe votre document DID. Il n'existe aucun moyen de définir un feed personnalisé entièrement à l'intérieur de Bluesky.
Un générateur de feeds peut-il voir le contenu des publications ?
Seulement ce qu'il collecte lui-même, en général depuis le firehose. Le squelette qu'il renvoie ne porte que des AT-URIs, et l'hydratation se fait côté Bluesky après que votre service a répondu.
Quelle est la différence entre getFeed et getFeedSkeleton ?
getFeedSkeleton est le vôtre et renvoie des entrées skeletonFeedPost, c'est-à-dire de simples URIs. getFeed est celui de l'AppView et renvoie des entrées feedViewPost, c'est-à-dire des publications complètes avec profils d'auteur et compteurs. Les clients appellent getFeed. Votre service, jamais.
Comment savoir si mon feed est cassé ?
Appelez app.bsky.feed.getFeedGenerator avec l'AT-URI de votre feed. Il renvoie isOnline et isValid à côté de la vue du feed. Bluesky ne publie pas depuis combien de temps un service doit avoir répondu pour compter comme en ligne, traitez donc un false comme une invitation à consulter vos propres logs.
Puis-je faire payer un feed ou restreindre qui le voit ?
Le protocole vous donne le point d'accroche et aucune facturation. L'authentification est facultative sur getFeedSkeleton, et lorsqu'elle est présente le JWT porte le DID du demandeur dans le claim iss, filtrer sur une liste de DIDs est donc simple. Le paiement, et la façon dont on annonce son refus à quelqu'un, sont entièrement laissés à votre service.
Comment mesurer la performance d'un feed ?
Bluesky publie un seul chiffre sur un feed, le likeCount de sa generatorView. Tout le reste de la performance du feed, c'est à vous de le journaliser, à partir des requêtes qui arrivent sur votre propre service. Ce manque est exactement celui traité dans Bluesky a-t-il des analytics.
Que se passe-t-il si un client demande plus de 100 éléments à un feed ?
L'AppView impose elle-même ce plafond. Demander un limit de 101 à getFeed renvoie une erreur InvalidRequest indiquant que l'entier est trop grand, avec un maximum de 100. Le paramètre limit a un minimum de 1, un maximum de 100 et une valeur par défaut de 50, donc un générateur de feeds n'a jamais à construire ce rejet lui-même.
Pourquoi le starter kit recommande-t-il did:plc plutôt que did:web pour certains générateurs de feeds ?
Un did:web est littéralement ton nom de domaine, donc changer de domaine casse tous les enregistrements publiés qui pointent vers lui. Un did:plc survit à un déménagement, c'est pourquoi le starter kit le conseille pour tout feed censé durer ou risquant de migrer.
Comment construire le curseur de pagination d'un générateur de feeds ?
Le lexicon déclare cursor comme une simple chaîne sans contrainte, et le starter kit qualifie la valeur d'opaque et entièrement laissée au choix du générateur. Le starter kit recommande un curseur composé, un horodatage associé à un CID, et insiste sur le fait qu'il doit être unique par élément du feed pour éviter les bugs de pagination.
Que fait acceptsInteractions dans l'enregistrement app.bsky.feed.generator ?
Mettre acceptsInteractions à true inscrit le feed dans app.bsky.feed.sendInteractions, ce qui permet aux clients de signaler avec quelles publications un utilisateur a interagi. Le champ est optionnel et booléen, rien dans la doc ne l'exige pour qu'un feed fonctionne.
Mettez cela en pratique avec AdaptlyPost
Cet article vous a-t-il été utile ?
Dites-nous ce que vous en pensez !
Nous voir plus souvent sur Google
Un clic définit AdaptlyPost comme source préférée. Nos articles remontent alors dans vos À la une, en mode IA et dans les aperçus IA.
Avant de partir...
AdaptlyPost
Planifiez vos contenus sur toutes les plateformes
Gérez tous vos comptes de réseaux sociaux en un seul endroit avec AdaptlyPost.
Analyses multiplateforme
Boîte sociale
Assistant IA
Termes connexes du glossaire


Pourquoi Bluesky facets byteStart byteEnd comptent des octets, pas des caractères
Les offsets Bluesky facets byteStart byteEnd comptent des octets UTF-8, pas les index UTF-16 de JavaScript. L'avertissement du lexicon, un exemple, du code.


Pourquoi la limite de taille d'image de Bluesky est de 2,000,000 octets
Bluesky limite chaque image de post à 2,000,000 octets, fixés par maxSize dans le lexicon images. Les avatars et bannières s'arrêtent à 1,000,000 octets.


Convertir la limite de taux de l'API Bluesky en publications par heure
La limite de taux de l'API Bluesky en écriture est un budget de points, pas un compte de requêtes : 5,000 points par heure, 3 par post, soit 1,666 posts.
Articles Connexes


Les deux enregistrements derrière un handle de domaine personnalisé Bluesky
Un handle de domaine personnalisé Bluesky exige un enregistrement : TXT sur _atproto ou texte brut sur /.well-known/atproto-did. Les valeurs et les TLD exclus.


Meta fixe à 1 000 la limite de caractères d'alt_text dans l'API Instagram
Meta fixe à 1 000 la limite de caractères d'alt_text dans l'API Instagram et le réserve aux images fixes. Reels et Stories n'acceptent aucun texte alternatif.


Derrière l'étiquette IA de TikTok se cachent deux étiquettes
L'étiquette IA de TikTok existe en deux versions : celle que vous posez avec is_aigc, et celle que TikTok ajoute via ses effets ou C2PA, impossible à retirer.

