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.
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étodo | Descripció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' => ...] |