Add website-taal-analyse scripts: rapport_generator.py

This commit is contained in:
2026-02-27 13:09:24 +00:00
parent ddff3bb9e5
commit caf6432150
@@ -0,0 +1,464 @@
"""
Website Taal-, Consistentie- & LLM-analyse — PDF Rapport Generator
====================================================================
Gebruik dit script als startpunt. Vervang de inhoud van BEVINDINGEN
met de werkelijk gevonden actiepunten. De stijlfuncties en layoutlogica
hoef je niet aan te passen.
Gebruik:
pip install reportlab --break-system-packages -q
python rapport_generator.py
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
from datetime import date
import os
# ── Configuratie — pas dit aan per opdracht ───────────────────────────────────
MERKNAAM = "HelloPrint" # Merknaam van de geanalyseerde site
DOMEINNAAM = "helloprint.com/nl-nl" # Domein / pad
DOELGROEP = "B2B klein zakelijk (tot 10 medewerkers)"
LOCALE = "nl-NL"
OUTPUT = f"/mnt/user-data/outputs/{MERKNAAM}_Taal_Consistentie_LLM_{date.today().strftime('%Y%m%d')}.pdf"
# ─────────────────────────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT, pagesize=A4,
rightMargin=2.2*cm, leftMargin=2.2*cm,
topMargin=2.5*cm, bottomMargin=2.5*cm,
)
# ── Kleurenpalet ──────────────────────────────────────────────────────────────
GROEN = colors.HexColor("#1a6b2f")
GROEN_L = colors.HexColor("#eaf5ec")
BLAUW = colors.HexColor("#1a3a6b")
BLAUW_L = colors.HexColor("#e8eef5")
PAARS = colors.HexColor("#5b2d8e")
PAARS_L = colors.HexColor("#f3eefb")
TEAL = colors.HexColor("#0e6b5e")
TEAL_L = colors.HexColor("#e6f4f2")
ROOD = colors.HexColor("#b03030")
ROOD_L = colors.HexColor("#fdf0ee")
ORANJE = colors.HexColor("#c05c00")
ORANJE_L = colors.HexColor("#fef5e7")
DONKER = colors.HexColor("#1a1a1a")
GRIJS = colors.HexColor("#f4f4f4")
GRIJS2 = colors.HexColor("#e8e8e8")
LICHT = colors.HexColor("#777777")
WIT = colors.white
# ── Stijlen ───────────────────────────────────────────────────────────────────
def ps(name, **kw):
return ParagraphStyle(name, **kw)
BODY = ps("B", fontSize=9.5, leading=14, fontName="Helvetica", textColor=DONKER, spaceAfter=4)
SMALL = ps("S", fontSize=8.5, leading=12, fontName="Helvetica", textColor=LICHT)
H1 = ps("H1", fontSize=13, leading=17, fontName="Helvetica-Bold", textColor=BLAUW, spaceBefore=16, spaceAfter=5)
H2 = ps("H2", fontSize=10.5,leading=14, fontName="Helvetica-Bold", textColor=DONKER, spaceBefore=8, spaceAfter=3)
# ── Thema- en prioriteitsconfiguratie ─────────────────────────────────────────
THEMA = {
"TAAL": (BLAUW, BLAUW_L, "T"),
"CONSISTENTIE": (GROEN, GROEN_L, "C"),
"LLM": (PAARS, PAARS_L, "L"),
"NAV": (TEAL, TEAL_L, "N"),
}
PRIO = {
"HOOG": ROOD,
"MIDDEL": ORANJE,
"LAAG": GROEN,
}
_tellers = {k: 0 for k in THEMA}
def omslagrij(tekst, stijlkw, achtergrond):
"""Eén gekleurde rij voor het omslagblok."""
t = Table([[Paragraph(tekst, ps(f"omslag_{hash(tekst)}", **stijlkw))]], colWidths=[17*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), achtergrond),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 20),
("RIGHTPADDING", (0,0), (-1,-1), 20),
]))
return t
def actiepunt(thema, prio, titel, probleem, vindplaats, actie):
"""
Geeft een opgemaakte actiepunt-blok terug als ReportLab Flowable.
Parameters
----------
thema : "TAAL" | "CONSISTENTIE" | "LLM" | "NAV"
prio : "HOOG" | "MIDDEL" | "LAAG"
titel : Korte probleemtitel (string)
probleem : Uitleg van het probleem (mag HTML-tags bevatten)
vindplaats : Exacte locatie op de pagina
actie : Concreet advies (mag HTML-tags bevatten)
"""
_tellers[thema] += 1
nr = _tellers[thema]
tk, tl, afk = THEMA[thema]
pk = PRIO[prio]
uid = f"{thema}{nr}"
def badge(tekst, breedte, achtergrond):
t = Table([[Paragraph(f"<b>{tekst}</b>",
ps(f"bdg_{uid}_{tekst}", fontSize=8, fontName="Helvetica-Bold",
textColor=WIT, leading=10, alignment=TA_CENTER))]],
colWidths=[breedte])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), achtergrond),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 2),
("RIGHTPADDING", (0,0), (-1,-1), 2),
("ROUNDEDCORNERS", [3]),
]))
return t
labels = Table([[badge(f"{afk}{nr}", 0.9*cm, tk),
Spacer(0.25*cm, 1),
badge(prio, 1.4*cm, pk)]],
colWidths=[1*cm, 0.35*cm, 1.5*cm])
labels.setStyle(TableStyle([
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
]))
inhoud = [
Paragraph(f"<b>{titel}</b>",
ps(f"ti_{uid}", fontSize=10.5, fontName="Helvetica-Bold",
textColor=DONKER, leading=14, spaceAfter=3)),
Paragraph(f"<b>Probleem:</b> {probleem}",
ps(f"pr_{uid}", fontSize=9, fontName="Helvetica",
textColor=DONKER, leading=13, spaceAfter=2)),
Paragraph(f"<b>Vindplaats:</b> {vindplaats}",
ps(f"vp_{uid}", fontSize=8.5, fontName="Helvetica",
textColor=LICHT, leading=12, spaceAfter=3)),
Paragraph(f"\u25B8 <b>Actie:</b> {actie}",
ps(f"ac_{uid}", fontSize=9, fontName="Helvetica-Bold",
textColor=tk, leading=13)),
]
blok = Table([[labels, inhoud]], colWidths=[3.1*cm, 13.9*cm])
blok.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("BACKGROUND", (0,0), (-1,-1), tl),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING", (0,0), (-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("ROUNDEDCORNERS", [5]),
]))
return KeepTogether([blok, Spacer(1, 7)])
def nav_tabel(items, kolombreedtes=(3.2*cm, 1.3*cm, 12.5*cm)):
"""
Geeft een opgemaakte navigatiebeoordelingstabel terug.
items = lijst van tuples:
(nav_naam, symbool, kleur, toelichting)
symbool: "✓" | "△" | "✗"
kleur: GROEN | ORANJE | ROOD
"""
data = [[
Paragraph("<b>Menu-item</b>",
ps("nv_h1", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12)),
Paragraph("<b>Oordeel</b>",
ps("nv_h2", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)),
Paragraph("<b>Toelichting</b>",
ps("nv_h3", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12)),
]]
for naam, symbool, kleur, toelichting in items:
uid = naam.replace(" ", "_")
data.append([
Paragraph(f"<b>{naam}</b>",
ps(f"nv_naam_{uid}", fontSize=9, fontName="Helvetica-Bold", textColor=DONKER, leading=12)),
Paragraph(symbool,
ps(f"nv_sym_{uid}", fontSize=11, fontName="Helvetica-Bold",
textColor=kleur, leading=14, alignment=TA_CENTER)),
Paragraph(toelichting,
ps(f"nv_toe_{uid}", fontSize=8.5, fontName="Helvetica", textColor=DONKER, leading=12)),
])
t = Table(data, colWidths=list(kolombreedtes))
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), BLAUW),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WIT, GRIJS]),
("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#dddddd")),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
return t
def mapping_tabel(rijen, header_kleur=TEAL):
"""
Vergelijkingstabel nav-item vs. pagina-naam vs. status.
rijen = lijst van tuples:
(nav_naam, pagina_naam, status_tekst, status_kleur)
"""
data = [[
Paragraph("<b>Nav-item</b>",
ps("mp_h1", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12)),
Paragraph("<b>Naam op overzichtspagina</b>",
ps("mp_h2", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12)),
Paragraph("<b>Status</b>",
ps("mp_h3", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)),
]]
for nav, pagina, status, kleur in rijen:
uid = nav.replace(" ", "_")
data.append([
Paragraph(nav, ps(f"mp_n_{uid}", fontSize=9, fontName="Helvetica-Bold", textColor=DONKER, leading=12)),
Paragraph(pagina, ps(f"mp_p_{uid}", fontSize=9, fontName="Helvetica", textColor=DONKER, leading=12)),
Paragraph(status, ps(f"mp_s_{uid}", fontSize=8.5, fontName="Helvetica-Bold",
textColor=kleur, leading=12, alignment=TA_CENTER)),
])
t = Table(data, colWidths=[3.5*cm, 7.5*cm, 6*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), header_kleur),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WIT, GRIJS]),
("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#dddddd")),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
return t
def prioriteitsmatrix(acties):
"""
Slottabel met alle actiepunten, thema, impact en doorlooptijd.
acties = lijst van tuples:
(nr, omschrijving, thema_label, impact_kleur, doorlooptijd)
"""
data = [[
Paragraph("<b>#</b>", ps("pm_h0", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)),
Paragraph("<b>Actie</b>", ps("pm_h1", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12)),
Paragraph("<b>Thema</b>", ps("pm_h2", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)),
Paragraph("<b>Impact</b>", ps("pm_h3", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)),
Paragraph("<b>Doorlooptijd</b>", ps("pm_h4", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)),
]]
for nr, omschr, thema_lbl, impact_kleur, tijd in acties:
data.append([
Paragraph(str(nr), ps(f"pm_nr_{nr}", fontSize=9, fontName="Helvetica-Bold", textColor=DONKER, leading=12, alignment=TA_CENTER)),
Paragraph(omschr, ps(f"pm_om_{nr}", fontSize=9, fontName="Helvetica", textColor=DONKER, leading=13)),
Paragraph(thema_lbl, ps(f"pm_th_{nr}", fontSize=8.5, fontName="Helvetica", textColor=LICHT, leading=12, alignment=TA_CENTER)),
Paragraph("&#9632;", ps(f"pm_ic_{nr}", fontSize=12, fontName="Helvetica-Bold", textColor=impact_kleur, leading=14, alignment=TA_CENTER)),
Paragraph(tijd, ps(f"pm_ti_{nr}", fontSize=8.5, fontName="Helvetica", textColor=LICHT, leading=12, alignment=TA_CENTER)),
])
t = Table(data, colWidths=[0.8*cm, 9.2*cm, 2.4*cm, 1.8*cm, 2.8*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), BLAUW),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WIT, GRIJS]),
("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#e0e0e0")),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 7),
("RIGHTPADDING", (0,0), (-1,-1), 7),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
return t
# ══════════════════════════════════════════════════════════════════════════════
# RAPPORT — pas de inhoud hieronder aan per opdracht
# ══════════════════════════════════════════════════════════════════════════════
story = []
# ── Omslag ────────────────────────────────────────────────────────────────────
story.append(omslagrij(
"Analyse rapport",
dict(fontSize=9.5, fontName="Helvetica", textColor=colors.HexColor("#a8c0e0"), leading=14),
BLAUW))
story.append(omslagrij(
"Taal &bull; Consistentie &bull; LLM-vindbaarheid",
dict(fontSize=24, fontName="Helvetica-Bold", textColor=WIT, leading=30),
BLAUW))
story.append(omslagrij(
f"{DOMEINNAAM} &nbsp;|&nbsp; Doelgroep: {DOELGROEP} &nbsp;|&nbsp; Locale: {LOCALE}",
dict(fontSize=10, fontName="Helvetica", textColor=colors.HexColor("#9ab8d8"), leading=14),
BLAUW))
story.append(Spacer(1, 4))
meta = Table([[
Paragraph(f"Datum: {date.today().strftime('%d %B %Y')}", SMALL),
Paragraph(f"Pagina: {DOMEINNAAM}", SMALL),
Paragraph("Opgesteld door: Claude (Anthropic)", SMALL),
]], colWidths=[5.5*cm, 6.5*cm, 5*cm])
meta.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), GRIJS),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
]))
story.append(meta)
story.append(Spacer(1, 14))
story.append(Paragraph(
f"Dit rapport analyseert {DOMEINNAAM} op taal, consistentie en LLM-vindbaarheid. "
f"De primaire doelgroep is {DOELGROEP.lower()}. "
"Elk knelpunt heeft een concreet actiepunt met vindplaats en aanbeveling.",
BODY))
story.append(Spacer(1, 10))
# ── SECTIE 1 — TAAL ───────────────────────────────────────────────────────────
story.append(Paragraph("1 &nbsp; Taal — zuiverheid en geschiktheid voor de doelgroep", H1))
story.append(HRFlowable(width="100%", thickness=1.5, color=BLAUW, spaceAfter=8))
story.append(Paragraph(
"Vul hier een korte inleiding in over de algemene taalstijl van de geanalyseerde pagina.",
BODY))
story.append(Spacer(1, 8))
# Voeg hier actiepunten toe met de actiepunt()-functie:
story.append(actiepunt(
"TAAL", "HOOG",
"Titel van het taalprobleem",
"Beschrijving van het probleem. Waarom is dit een probleem voor de doelgroep?",
"Exacte locatie op de pagina (bijv. 'Hoofdnavigatie — item 3', '&lt;title&gt;-tag', 'H1-heading')",
"Concreet advies. Geef bij voorkeur een voorbeeldtekst: 'Vervang X door Y.'"))
# ── SECTIE 2 — MENUNAMEN ──────────────────────────────────────────────────────
story.append(Paragraph("2 &nbsp; Menunamen — helderheid voor de bezoeker", H1))
story.append(HRFlowable(width="100%", thickness=1.5, color=BLAUW, spaceAfter=8))
story.append(Paragraph(
"Beoordeling van elk navigatie-item vanuit het perspectief van de doelgroep.",
BODY))
story.append(Spacer(1, 8))
# Voorbeeld nav_tabel() — pas items aan op basis van de geanalyseerde navigatie:
story.append(nav_tabel([
("Menu-item 1", "✓", GROEN, "Helder en herkenbaar."),
("Menu-item 2", "△", ORANJE, "Dubbelzinnig of te vaag. Toelichting."),
("Menu-item 3", "✗", ROOD, "Problematisch. Reden + aanbeveling."),
]))
story.append(Spacer(1, 10))
# ── SECTIE 3 — CONSISTENTIE ───────────────────────────────────────────────────
story.append(Paragraph("3 &nbsp; Consistentie — toon, aanspreekvorm en categorienamen", H1))
story.append(HRFlowable(width="100%", thickness=1.5, color=BLAUW, spaceAfter=8))
story.append(Paragraph("Inleiding consistentie.", BODY))
story.append(Spacer(1, 8))
story.append(actiepunt(
"CONSISTENTIE", "HOOG",
"Consistentieprobleem",
"Beschrijving.", "Vindplaats.", "Actie."))
# ── SECTIE 4 — LLM-VINDBAARHEID ───────────────────────────────────────────────
story.append(Paragraph("4 &nbsp; LLM-vindbaarheid", H1))
story.append(HRFlowable(width="100%", thickness=1.5, color=BLAUW, spaceAfter=8))
story.append(Paragraph("Inleiding LLM.", BODY))
story.append(Spacer(1, 8))
story.append(actiepunt(
"LLM", "HOOG",
"LLM-probleem",
"Beschrijving.", "Vindplaats.", "Actie."))
# ── SECTIE 5 — NAVIGATIESTRUCTUUR (optioneel) ─────────────────────────────────
# Voeg deze sectie toe als je ook een overzichts-/categoriepagina hebt geanalyseerd.
#
# story.append(PageBreak())
# story.append(Paragraph("5 &nbsp; Navigatiestructuur — nav vs. overzichtspagina", H1))
# story.append(HRFlowable(width="100%", thickness=1.5, color=TEAL, spaceAfter=8))
# story.append(Paragraph("5.1 Naamvergelijking", H2))
# story.append(mapping_tabel([
# ("Nav-item 1", "Paginanaam 1", "✓ Consistent", GROEN),
# ("Nav-item 2", "Andere naam op pagina", "✗ Mismatch", ROOD),
# ("Nav-item 3", "Subcategorie onder 'Overig'", "△ Inconsistent", ORANJE),
# ]))
# ── SECTIE 6 — PRIORITEITSOVERZICHT ──────────────────────────────────────────
story.append(PageBreak())
story.append(Paragraph("Prioriteitsoverzicht — wat als eerste aanpakken", H1))
story.append(HRFlowable(width="100%", thickness=1.5, color=BLAUW, spaceAfter=10))
# Vul aan met alle actiepunten uit het rapport:
story.append(prioriteitsmatrix([
("1", "Omschrijving actie 1", "TAAL", ROOD, "1 dag"),
("2", "Omschrijving actie 2", "CONS", ROOD, "1 dag"),
("3", "Omschrijving actie 3", "LLM", ROOD, "1–2 wkn"),
("4", "Omschrijving actie 4", "NAV", ORANJE, "1 week"),
("5", "Omschrijving actie 5", "LLM", GROEN, "Doorlopend"),
]))
story.append(Spacer(1, 8))
# Legenda
legenda_impact = Table([[
Paragraph("&#9632; Hoog — direct zichtbaar, groot effect",
ps("leg1", fontSize=8.5, fontName="Helvetica", textColor=ROOD, leading=12)),
Paragraph("&#9632; Middel — merkbaar effect, planbaar",
ps("leg2", fontSize=8.5, fontName="Helvetica", textColor=ORANJE, leading=12)),
Paragraph("&#9632; Laag — finetuning / continu",
ps("leg3", fontSize=8.5, fontName="Helvetica", textColor=GROEN, leading=12)),
]], colWidths=[6*cm, 6*cm, 5*cm])
legenda_impact.setStyle(TableStyle([
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
]))
story.append(legenda_impact)
story.append(Spacer(1, 10))
legenda_thema = Table([[
Paragraph("T = Taal", ps("lt1", fontSize=8.5, fontName="Helvetica", textColor=BLAUW, leading=12)),
Paragraph("C = Consistentie", ps("lt2", fontSize=8.5, fontName="Helvetica", textColor=GROEN, leading=12)),
Paragraph("L = LLM", ps("lt3", fontSize=8.5, fontName="Helvetica", textColor=PAARS, leading=12)),
Paragraph("N = Navigatie", ps("lt4", fontSize=8.5, fontName="Helvetica", textColor=TEAL, leading=12)),
]], colWidths=[4*cm, 4*cm, 4*cm, 5*cm])
legenda_thema.setStyle(TableStyle([
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
]))
story.append(legenda_thema)
story.append(Spacer(1, 20))
# ── Footer ────────────────────────────────────────────────────────────────────
story.append(HRFlowable(width="100%", thickness=0.8, color=GRIJS2, spaceAfter=8))
footer_t = Table([[
Paragraph(f"{MERKNAAM} — Taal, Consistentie &amp; LLM-vindbaarheid — Vertrouwelijk", SMALL),
Paragraph(f"Opgesteld {date.today().strftime('%d-%m-%Y')} | Claude (Anthropic)",
ps("footer_r", fontSize=8, textColor=LICHT, fontName="Helvetica",
leading=11, alignment=TA_RIGHT)),
]], colWidths=[9*cm, 8*cm])
footer_t.setStyle(TableStyle([
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(footer_t)
# ── Bouw PDF ──────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF klaar: {OUTPUT}")