Quan algú comparteix una pàgina teva a LinkedIn o a WhatsApp, el que es veu és una imatge de 1200 per 630 amb el títol a sobre. Generar-la sembla un problema de disseny i en realitat és de tipografia: gairebé tot es redueix a decidir a quina mida cap un titular que no saps quant mesurarà. Els exemples van en Laravel, que és una de les nostres especialitats a AndorraDev, però el problema i les decisions són les mateixes en qualsevol stack.
Un navegador sencer per pintar una imatge
La solució més estesa és maquetar la targeta en HTML i fer-li una captura amb Puppeteer o Playwright. Funciona, i té una lògica difícil de discutir: ja saps escriure HTML i CSS, així que el disseny et surt en deu minuts.
El preu apareix després. Estàs ficant Node i uns tres-cents megues de Chromium en un projecte PHP, a la imatge de Docker, al desplegament i a qualsevol màquina on algú hagi de regenerar les targetes. Per a un lloc que en genera una per article, és una infraestructura considerable a canvi d'un rectangle amb text.
L'alternativa que fem servir és més avorrida: una plantilla SVG amb buits, i un binari que la converteix a PNG.
apt install librsvg2-bin
Això instal·la rsvg-convert, que rasteritza un SVG en una crida. L'altra opció és resvg, un binari de Rust autocontingut amb millor suport d'SVG2. Qualsevol dels dos és un executable, no un entorn.
La plantilla és un SVG amb buits
Un SVG és text, així que la plantilla s'omple amb una substitució de cadenes, igual que una vista.
<text x="80" y="{{title_baseline}}" font-size="{{title_font_size}}" fill="#ffffff">
{{title_tspans}}
</text>
No hi ha res màgic, i aquesta és la gràcia: el disseny l'obres en qualsevol editor de vectors i el toques sense recompilar res.
Mesurar el text és la feina de debò
Aquí hi ha el noranta per cent del problema. Un SVG no sap ajustar text: no hi ha overflow, no hi ha salt de línia automàtic, no hi ha "fes-ho més petit si no cap". Has de decidir tu quantes línies ocupa el titular i a quina mida, i escriure cada línia com un <tspan>.
El que gairebé tothom fa és estimar per nombre de caràcters: a 82 píxels n'hi caben vint, a 72 n'hi caben vint-i-tres. És ràpid i està malament, perquè les lletres no mesuren el mateix. Amb un titular normal cola; amb un ple de majúscules o de paraules amples, se surt de la targeta sense que ningú se n'adoni fins que la veu publicada.
El mesurament honest el fa GD, que ja ve amb PHP:
$box = imagettfbbox($size, 0, $fontPath, $text);
$width = abs($box[2] - $box[0]);
I aquí hi ha un parany que ens va costar una estona trobar. GD interpreta la mida com a punts i renderitza a 96 ppp, mentre que el font-size d'un SVG és en píxels d'usuari. Les amplades surten un terç inflades. Ho vam comprovar rasteritzant una mostra i mesurant la taca de tinta del PNG:
imagettfbbox diu: 940 px
amplada renderitzada: 705 px
Exactament 0,75, que és 72 partit per 96. Sense convertir, tots els titulars s'ajusten més petits del que caldria i qualsevol cosa que col·loquis a partir d'una amplada mesurada cau desplaçada. Amb la conversió, la mateixa mostra dona 704 contra 705 reals.
Comprova el teu mesurament contra el render de debò abans de fiar-te'n. Genera una imatge amb una cadena coneguda, obre-la amb qualsevol biblioteca i mira el requadre de tinta. Si no coincideix amb el que et diu la teva funció de mesura, tens un factor d'escala amagat i tots els teus ajustos estan esbiaixats en la mateixa direcció.
Col·locar un bloc del qual no saps l'alçada
Un titular pot ocupar una línia o tres, i l'SVG no sap sumar. Si fixes la posició de la primera línia, un titular curt deixa un buit enorme a sota i un de llarg se't va contra el peu.
La solució és calcular la línia base en PHP i ancorar el bloc per baix, de manera que l'última línia caigui sempre sobre la mateixa regla. I exposar també on acaba el bloc, perquè el que va a sota pengi d'allà en comptes d'una posició fixa:
<g transform="translate(0 {{title_bottom}})">
<text x="80" y="52" font-size="{{subtitle_font_size}}">{{subtitle_tspans}}</text>
</g>
Amb això, un subtítol guarda la mateixa distància amb un titular d'una línia i amb un de tres, que és el que fa que una col·lecció de targetes sembli dissenyada i no generada.
El fons és una capa més
Aquesta és la part que sol empènyer la gent de tornada al navegador: posar una foto darrere del text. En SVG no cal res especial, perquè una imatge s'incrusta com a URI de dades.
<image x="0" y="0" width="1200" height="630"
preserveAspectRatio="xMidYMid slice" href="data:image/jpeg;base64,..."/>
I com que tot acaba sent un fitxer local, tant li fa d'on vingui: un color pla, una foto d'Unsplash descarregada i desada a la memòria cau, o un PNG que hagis generat amb un model d'imatge. La plantilla no canvia. A sobre hi va un degradat que enfosqueix més el costat del text que l'altre, i el titular es llegeix sobre qualsevol foto.
Una capa opcional ha de desaparèixer, no quedar-se buida
Aquest ens va costar una integració contínua en vermell i val la pena explicar-lo, perquè és el tipus d'error que només apareix a la màquina que sí que té el binari.
Si la plantilla porta l'<image> del fons sempre i li passes una cadena buida quan no hi ha foto, l'SVG resultant conté això:
<image x="0" y="0" width="1200" height="630" href="" xlink:href=""/>
I librsvg avorta. No retorna un error llegible: el procés mor amb el senyal 6, que en el millor dels casos t'arriba com una excepció estranya de la teva biblioteca de processos.
La lliçó és de disseny, no de biblioteca: "sense fons" no pot voler dir "un fons sense origen". La capa opcional ha de desaparèixer sencera de la plantilla, amb una secció condicional, no renderitzar-se buida.
Les fonts són l'error que no avisa
rsvg-convert resol font-family a través de fontconfig, així que si la teva tipografia de marca no està instal·lada a la màquina, cau a una altra sense dir res. La targeta es genera, no falla res, i simplement no s'assembla a la resta del teu web.
Es pot instal·lar la font a la imatge de Docker, però això obliga a reconstruir-la. La via que menys molesta és apuntar fontconfig a les fonts que el teu projecte ja serveix:
<fontconfig>
<dir prefix="relative">../../public/fonts</dir>
<dir>/usr/share/fonts</dir>
</fontconfig>
Aquest fitxer substitueix la configuració del sistema en comptes de sumar-s'hi, així que cal llistar també els directoris del sistema o et quedes sense cap altra font. El prefix="relative" resol rutes contra el mateix fitxer, amb la qual cosa val igual al teu portàtil que dins del contenidor.
I un detall que s'oblida: mesura amb la mateixa font amb què renderitzes. Mesurar amb una i pintar amb una altra descentra l'ajust sense que res ho delati.
Regenerar només el que ha canviat
Amb cinc-centes targetes, regenerar-les totes a cada desplegament és temps llençat. El natural és saltar-se les que ja existeixen, i això funciona fins que canvies alguna cosa que estava impresa dins d'una targeta que ja és al disc.
Ens va passar amb la data de publicació: en moure la data d'un article, la seva targeta continuava ensenyant la vella perquè el fitxer existia. La resposta correcta no és recordar-se d'esborrar-la, és desar una empremta de tot el que entra al dibuix, inclosa la mateixa plantilla. Si edites l'SVG i l'empremta no ho contempla, canvies el disseny i no es regenera res.
Genera les targetes en local i puja-les al repositori. Fer-ho sota demanda et fica un render al camí crític d'una petició i una dependència binària al contenidor web, a canvi de res: una targeta només canvia quan canvia el contingut, i això ho saps tu abans que el servidor.
Què hi ha al mercat i en què es diferencia
Les opcions se separen per quanta infraestructura et demanen a canvi de quanta llibertat de disseny et donen.
Puppeteer o Playwright són la referència i maquetes en HTML, que és el major avantatge que pot tenir una solució d'aquestes. A canvi, Node i Chromium a tot arreu.
Satori, de Vercel, converteix JSX a SVG i està molt ben resolt, però viu a l'ecosistema de Node.
Intervention Image i GD a pèl són el que s'acaba fent servir en PHP per a això. Funcionen si la targeta és plana, i es fan incòmodes així que vols capes, degradats o retalls, perquè cada element és una crida de codi en lloc d'una etiqueta.
Laracards, que és el que mantenim, aposta per SVG més un binari: el disseny s'edita com un vector i l'ajust de text va mesurat. Si ja tens Node al projecte i maquetar en HTML et compensa, Satori o Puppeteer són eleccions perfectament raonables.
I si no treballes amb Laravel
Els criteris no depenen del llenguatge:
- Mesura el text de debò i comprova el mesurament contra el render. Comptar caràcters se surt de la targeta amb el primer titular ample.
- Calcula fora de l'SVG tot el que necessiti aritmètica. Un format vectorial no sap sumar, així que les línies base es resolen abans.
- Que una capa opcional desaparegui, no que es quedi buida. Un element sense origen és la diferència entre una imatge i un procés mort.
- Fixa les fonts explícitament. Un fallback silenciós no trenca res i t'arruïna la coherència de marca.
- Decideix què regenera una targeta. Si és l'existència del fitxer, el dia que canviïs la plantilla no en canviarà cap.
On és el codi
El paquet és a Packagist, el codi a GitHub i la documentació del paquet recull les plantilles, els fons i la configuració. És MIT i no depèn de res més que de GD i un binari.
Les targetes d'aquest blog estan fetes així, i ho treballem a desenvolupament Laravel i disseny web. Si comparteixes contingut sovint i els teus enllaços surten sense imatge o amb una de genèrica, explica'ns com publiques i et diem què encaixa.