Calendario: il PDF e' un file vero, generato dal server

"Scarica in PDF" non apre piu' la stampa del browser: e' un link a
/calendario/<id>.pdf che restituisce un documento disegnato dal dato con PDFKit,
con Content-Disposition attachment. Serve perche' il calendario si gira su
WhatsApp, ed e' cosi' che questo centro parla coi clienti: chi lo riceve non ha
il sito aperto davanti.

Niente browser headless: Chromium in un container da 2 vCPU costa centinaia di
megabyte e un processo per richiesta, per una griglia di sette colonne.

Le linee della griglia si tracciano in una passata a parte, dopo i riempimenti:
disegnandole riga per riga, il riempimento della riga seguente copriva quella
appena fatta e due lezioni uguali in fasce consecutive si leggevano come un
blocco unico lungo il doppio.

Quattro test: intestazione %PDF- e coda %%EOF (un documento non chiuso arriva
monco e il browser mostra una pagina bianca invece di un errore), una pagina
sola, nomi di file distinti. Un id sconosciuto e' 404, non un PDF vuoto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 16:14:02 +02:00
parent 01f49ab6d2
commit 5be0fa787c
6 changed files with 416 additions and 11 deletions
+31
View File
@@ -0,0 +1,31 @@
import type { APIRoute } from 'astro';
import { calendari } from '../../data/calendari';
import { generaCalendarioPdf, nomeFilePdf } from '../../lib/calendario-pdf';
export const prerender = false;
// Il PDF del calendario, come file scaricabile: `/calendario/<id>.pdf`.
//
// Pubblico come il resto del sito — è lo stesso orario che chiunque legge nel popup, e un file
// dietro autenticazione non si potrebbe girare su WhatsApp, che è come questo centro
// comunica coi clienti.
export const GET: APIRoute = async ({ params }) => {
const cal = calendari.find((c) => c.id === params.id);
// Un id sconosciuto è 404, non un PDF vuoto: un file che si apre e non contiene niente fa
// credere che il calendario sia vuoto.
if (!cal) return new Response('Calendario inesistente.', { status: 404 });
const pdf = await generaCalendarioPdf(cal);
return new Response(new Uint8Array(pdf), {
headers: {
'Content-Type': 'application/pdf',
// `attachment` perché il bottone dice "scarica": senza, il browser lo aprirebbe in una
// scheda e su telefono resterebbe lì, senza finire fra i file.
'Content-Disposition': `attachment; filename="${nomeFilePdf(cal)}"`,
'Content-Length': String(pdf.length),
// Il calendario cambia solo quando cambia il dato, cioè con un deploy: un'ora di cache
// basta a non rigenerarlo a ogni click, e non abbastanza da servire un orario vecchio.
'Cache-Control': 'public, max-age=3600',
},
});
};