Saltar al contenido principal

Qué es TextBuilder

TextBuilder es la clase para construir mediante encadenamiento de métodos los facets (anotaciones de rich text) que define el AT Protocol de Bluesky. El cuerpo de las publicaciones de Bluesky es texto plano, pero para mostrar menciones, enlaces y hashtags hay que enviar además un array facets que indica la posición (byte offset) y el tipo dentro del texto. TextBuilder calcula automáticamente esos offsets y construye el array.

Creación básica de texto

TextBuilder::make()

Crea una instancia indicando el texto inicial con TextBuilder::make(). El texto inicial se puede omitir.
use Revolution\Bluesky\RichText\TextBuilder;

$builder = TextBuilder::make(text: 'Hello Bluesky');

text()

Con text() se añade texto al final.
$builder = TextBuilder::make()
    ->text('Hello ')
    ->text('Bluesky');

// $builder->text === 'Hello Bluesky'

newLine()

Añade un salto de línea. Con count puedes indicar el número de líneas (por defecto: 1).
$builder = TextBuilder::make('línea 1')
    ->newLine()
    ->text('línea 2')
    ->newLine(count: 2)
    ->text('línea 4');

toPost()

Convierte la instancia de TextBuilder en un record Post. Se puede pasar directamente a Bluesky::post().
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\RichText\TextBuilder;

$post = TextBuilder::make('Publicación simple')->toPost();

$response = Bluesky::withToken()->post($post);

Post::build()

También puedes usar Post::build() pasándole una clausura. Devuelve un Post.
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\RichText\TextBuilder;

$post = Post::build(function (TextBuilder $builder) {
    $builder->text('Hello Bluesky');
});

$response = Bluesky::withToken()->post($post);

Añadir menciones (@mention)

Con mention() se añade un facet de mención.
$builder->mention(text: '@alice.bsky.social');

Resolución automática del DID

Si omites did, se resuelve automáticamente a partir del handle (se llama a Bluesky::resolveHandle()).
// Resuelve el DID automáticamente (se produce una llamada a la API)
$builder->mention('@alice.bsky.social');

Indicar el DID explícitamente

Si ya conoces el DID, puedes pasarlo explícitamente para evitar la llamada a la API.
// Indicar el DID directamente (recomendado: sin llamada a la API)
$builder->mention(text: '@alice.bsky.social', did: 'did:plc:xxxxxxxxxxxxxxxxxxxx');
Si vas a usar muchas menciones en producción, cachea los DIDs para reducir las llamadas a la API.

Incluir enlaces (URL)

Con link() se añade un facet de enlace.
// El propio URL se muestra como texto
$builder->link('https://laravel.com');

// Especifica por separado texto y URL
$builder->link(text: 'Web oficial de Laravel', uri: 'https://laravel.com');
Si omites uri, se usa text como URI tal cual.

Añadir hashtags

Con tag() se añade un facet de hashtag.
// Al pasar un texto que empieza por #, la etiqueta se extrae automáticamente
$builder->tag('#Laravel');

// Indica explícitamente la cadena de la etiqueta (cadena sin #)
$builder->tag(text: '#Laravel', tag: 'Laravel');

Construir texto compuesto

Puedes combinar varios facets para crear textos de publicación enriquecidos.
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\RichText\TextBuilder;

$post = TextBuilder::make('¡Nuevo artículo publicado!')
    ->newLine(count: 2)
    ->mention('@alice.bsky.social', did: 'did:plc:xxxx')
    ->text(' también quiero que lo leas.')
    ->newLine()
    ->link(text: 'Leer el artículo', uri: 'https://example.com/article/1')
    ->newLine()
    ->tag('#Laravel')
    ->text(' ')
    ->tag('#PHP')
    ->toPost();

$response = Bluesky::withToken()->post($post);

Escritura con Post::build()

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\RichText\TextBuilder;

$post = Post::build(function (TextBuilder $builder) {
    $builder->text('¡Nuevo artículo publicado!')
            ->newLine(count: 2)
            ->mention('@alice.bsky.social', did: 'did:plc:xxxx')
            ->text(' también quiero que lo leas.')
            ->newLine()
            ->link(text: 'Leer el artículo', uri: 'https://example.com/article/1')
            ->newLine()
            ->tag('#Laravel')
            ->text(' ')
            ->tag('#PHP');
});

$response = Bluesky::withToken()->post($post);

Integración con publicaciones

Combinado con Bluesky::post()

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\RichText\TextBuilder;

$post = TextBuilder::make('test')
    ->newLine()
    ->link('https://bsky.app/')
    ->toPost();

$response = Bluesky::withToken()->post($post);

Combinado con el canal de Notification

En el método toBluesky() de BlueskyChannel puedes usar Post::build().
use Illuminate\Notifications\Notification;
use Revolution\Bluesky\Notifications\BlueskyChannel;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\RichText\TextBuilder;

class DeployedNotification extends Notification
{
    public function __construct(
        private string $url,
    ) {}

    public function via(object $notifiable): array
    {
        return [BlueskyChannel::class];
    }

    public function toBluesky(object $notifiable): Post
    {
        return Post::build(function (TextBuilder $builder) {
            $builder->text('El despliegue ha finalizado')
                    ->newLine()
                    ->link(text: 'Ver', uri: $this->url)
                    ->newLine()
                    ->tag('#Laravel');
        });
    }
}

Detección automática de facets

También puedes detectar automáticamente @mention, URLs y #hashtag dentro del texto y añadir los facets.
use Revolution\Bluesky\RichText\TextBuilder;

$builder = TextBuilder::make('@alice.bsky.social test https://example.com #alice')
    ->detectFacets();

// Después de detectFacets() aún puedes añadir más facets
$builder->newLine()->tag('#bob');

$post = $builder->toPost();
detectFacets() se basa en expresiones regulares. Si quieres asegurar el enlace, es más fiable usar explícitamente link(), mention() o tag().

Añadir facets personalizados

Con el método facet() puedes añadir directamente un array de facet arbitrario.
use Revolution\Bluesky\RichText\TextBuilder;

$builder = TextBuilder::make();

$builder->facet([
    'index' => [
        'byteStart' => 0,
        'byteEnd' => 5,
    ],
    'features' => [
        [
            '$type' => 'app.bsky.richtext.facet#link',
            'uri' => 'https://example.com',
        ],
    ],
]);

Límite de caracteres y consideraciones

Byte offsets y graphemes

El índice de facet del AT Protocol se especifica en byte offsets UTF-8. Internamente TextBuilder usa strlen() para calcular el número de bytes. Los caracteres multibyte como el japonés o los emojis consumen varios bytes por carácter, así que el offset se determina por bytes, no por caracteres.
// 'Hello' ocupa 5 bytes en UTF-8 (1 carácter = 1 byte)
// '日本語' ocupa 9 bytes en UTF-8 (1 carácter = 3 bytes)
$builder = TextBuilder::make('日本語');
// strlen($builder->text) === 9

Límite de caracteres por publicación

Las publicaciones de Bluesky tienen un máximo de 300 caracteres en graphemes (caracteres visibles). El límite se aplica en graphemes, no en bytes, así que puedes escribir hasta 300 caracteres incluso en japonés.
use Illuminate\Support\Str;

$text = 'Este es el texto de la publicación.';

// Comprueba la longitud en graphemes
$length = Str::length($text); // equivalente a mb_strlen()
El propio TextBuilder no verifica el número de caracteres. Si envías una publicación con más de 300 graphemes, la API del AT Protocol devolverá un error.

Lista de métodos

MétodoDescripción
TextBuilder::make(string $text = '')Crea una instancia
text(string $text)Añade texto al final
newLine(int $count = 1)Añade saltos de línea
mention(string $text, ?string $did = null)Añade un facet de mención
link(string $text, ?string $uri = null)Añade un facet de enlace
tag(string $text, ?string $tag = null)Añade un facet de hashtag
detectFacets()Detecta facets automáticamente en el texto
facet(array $facet)Añade un facet personalizado
resetFacets()Resetea todos los facets
toPost()Convierte a record Post
toArray()Convierte a array ['text' => ..., 'facets' => ...]
Última modificación el 13 de julio de 2026