메인 콘텐츠로 건너뛰기

개요

Feed Generator는 Bluesky에서 동작하는 “알고리즘적 피드”의 구조입니다. 특정 키워드나 사용자 조건에 맞춘 자체 피드를 공개할 수 있습니다. laravel-bluesky를 사용하면 Laravel 애플리케이션에서 Feed Generator를 손쉽게 구현할 수 있습니다.
공식 튜토리얼: 커스텀 피드 만들기
공식 스타터 키트: bluesky-social/feed-generator

FeedGenerator 알고리즘 등록

가장 단순한 사용 방법은 AppServiceProvider::boot()에서 알고리즘을 클로저로 등록하는 것입니다.
// AppServiceProvider::boot()에서 등록

use Illuminate\Http\Request;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\FeedGenerator\FeedGenerator;

FeedGenerator::register(name: 'artisan', algo: function(int $limit, ?string $cursor, ?string $user, Request $request): array {
    // 구현은 자유롭게 결정하세요.

    // API의 일시적인 제한으로 인해 인증이 필요
    $response = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'))
                       ->searchPosts(q: '#laravel', until: $cursor, limit: $limit);

    $cursor = data_get($response->collect('posts')->last(), 'indexedAt');

    $feed = $response->collect('posts')->map(function(array $post) {
        return ['post' => data_get($post, 'uri')];
    })->toArray();

    // Request 객체를 사용해 사용자의 상태에 따른 결과를 반환할 수도 있습니다.
    info('user: '.$user); // 요청한 사용자의 DID. 'did:plc:***'
    info('header', $request->header());

    return compact('cursor', 'feed');
});
name은 URL 안전 문자열을 사용하세요. 알고리즘의 반환값은 cursorfeed를 포함하는 배열입니다.
[
    'cursor' => '',
    'feed' => [
       ['post' => 'at://'],
       ['post' => 'at://'],
    ],
]
패키지가 필요로 하는 라우트는 모두 자동으로 등록됩니다.
  • http://localhost/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:web:example.com/app.bsky.feed.generator/artisan
  • http://localhost/xrpc/app.bsky.feed.describeFeedGenerator
  • http://localhost/.well-known/did.json
  • Service DID는 현재 URL에서 자동 생성됩니다(예: did:web:example.com).
직접 결정할 것은 FeedGenerator의 name과 구현 내용뿐입니다.

피드 공개(명령어 생성)

Laravel 앱에 FeedGenerator를 구현한 것만으로는 Bluesky에 공개되지 않습니다. publishFeedGenerator를 호출하는 명령어를 만들어 실행합니다.
1

명령어 생성

php artisan make:command PublishGeneratorCommand
2

명령어 구현

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Generator;

class PublishGeneratorCommand extends Command
{
    protected $signature = 'bluesky:publish-generator';

    protected $description = 'Bluesky에 FeedGenerator를 공개';

    public function handle()
    {
        $generator = Generator::create(did: 'did:web:example.com', displayName: 'Feed name')
                              ->description('Feed description');

        $res = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'))
                      ->publishFeedGenerator(name: 'artisan', generator: $generator);

        dump($res->json());

        return 0;
    }
}
3

명령어 실행

php artisan bluesky:publish-generator
성공하면 Bluesky 프로필의 피드 목록에 링크가 추가됩니다. publishFeedGenerator는 정보를 갱신만 하기 때문에 몇 번이든 실행할 수 있습니다.

다중 FeedGenerator 생성

name을 바꿔 register를 여러 번 호출하기만 하면 여러 피드를 생성할 수 있습니다.
// AppServiceProvider::boot()

use Revolution\Bluesky\FeedGenerator\FeedGenerator;

FeedGenerator::register(name: 'feed1', algo: function() {
    // feed1의 구현
});

FeedGenerator::register(name: 'feed2', algo: function() {
    // feed2의 구현
});
공개 명령어에서도 동일하게 여러 번 publishFeedGenerator를 호출합니다.
// PublishGeneratorCommand

Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'));

$generator1 = Generator::create(did: 'did:web:example.com', displayName: 'Feed 1')
                       ->description('Feed 1');
Bluesky::publishFeedGenerator(name: 'feed1', generator: $generator1);

$generator2 = Generator::create(did: 'did:web:example.com', displayName: 'Feed 2')
                       ->description('Feed 2');
Bluesky::publishFeedGenerator(name: 'feed2', generator: $generator2);

알고리즘 클래스의 분리

클로저 대신 독립된 클래스를 사용하면 코드를 정리하기 쉬워집니다. FeedGeneratorAlgorithm 컨트랙트를 구현한 callable 클래스를 생성하고, AppServiceProvider에 등록합니다.
// 원하는 위치에 생성

namespace App\FeedGenerator;

use Illuminate\Http\Request;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Contracts\FeedGeneratorAlgorithm;

class ArtisanFeed implements FeedGeneratorAlgorithm
{
    public function __invoke(int $limit, ?string $cursor, ?string $user, Request $request): array
    {
        // API의 일시적인 제한으로 인해 인증이 필요
        $response = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'))
            ->searchPosts(q: '#laravel', until: $cursor, limit: $limit);

        $cursor = data_get($response->collect('posts')->last(), 'indexedAt');

        $feed = $response->collect('posts')->map(function (array $post) {
            return ['post' => data_get($post, 'uri')];
        })->toArray();

        info('user: '.$user);
        info('header', $request->header());

        return compact('cursor', 'feed');
    }
}
// AppServiceProvider::boot()

use Revolution\Bluesky\FeedGenerator\FeedGenerator;
use App\FeedGenerator\ArtisanFeed;

FeedGenerator::register(name: 'artisan', algo: ArtisanFeed::class);

인증

공식 스타터 키트의 인증 기능은 기본적으로 활성화되어 있습니다. 비활성화하려면 validateAuthUsing에 단순히 사용자 DID를 반환하는 클로저를 전달합니다.
// AppServiceProvider::boot()

use Illuminate\Http\Request;
use Revolution\Bluesky\Crypto\JsonWebToken;
use Revolution\Bluesky\FeedGenerator\FeedGenerator;

FeedGenerator::validateAuthUsing(function (?string $jwt, Request $request): ?string {
    [, $payload] = JsonWebToken::explode($jwt);
    return data_get($payload, 'iss');
});
피드가 계정의 “언어 설정”에 영향을 받습니다. FeedGenerator가 게시를 취득하고 있음에도 Bluesky에서 피드가 표시되지 않는 경우에는 계정의 언어 설정을 확인해 주세요.

고급 사용법

Artisan 명령어와 태스크 스케줄을 사용해 게시를 데이터베이스에 저장하고, 알고리즘에서는 DB에서 취득하기만 하도록 하면 API 호출 없이 빠른 피드를 실현할 수 있습니다.
// 알고리즘에서 DB로부터 피드를 반환하는 예시

FeedGenerator::register(name: 'cached-feed', algo: function(int $limit, ?string $cursor): array {
    $query = \App\Models\Post::query()
        ->orderByDesc('indexed_at')
        ->limit($limit);

    if ($cursor) {
        $query->where('indexed_at', '<', $cursor);
    }

    $posts = $query->get();

    $cursor = $posts->last()?->indexed_at;

    $feed = $posts->map(fn ($post) => ['post' => $post->uri])->toArray();

    return compact('cursor', 'feed');
});
// 스케줄로 정기적으로 게시를 수집하는 예시(routes/console.php)

use Illuminate\Support\Facades\Schedule;

Schedule::command('bluesky:collect-posts')->everyFiveMinutes();
마지막 수정일 2026년 7월 13일