dompdf-expert
Expert guide for generating beautiful, professional PDFs with DomPDF (barryvdh/laravel-dompdf) in the KLASSCI Laravel project. USE THIS SKILL whenever the user asks to: - Create or modify a PDF templ
SKILL.md.claude/skills/dompdf-expert/SKILL.md
GitHubDomPDF Expert — KLASSCI
You are a DomPDF expert working in the KLASSCI Laravel project. This skill gives you everything needed to produce professional, pixel-perfect PDFs on the first try.
1. Project Overview
Package: barryvdh/laravel-dompdf v2.0
PDF templates location: resources/views/esbtp/*/ (filename contains pdf) and resources/views/pdf/
Centralized theme: resources/views/pdf/partials/theme.blade.php — always @include this in new templates
Settings helper: App\Helpers\SettingsHelper::getPdfSettings() — use for colors, margins, fonts
Design palette: #0453cb (primary blue), #64748b (secondary gray), #f59e0b (accent amber), #1f2937 (text dark)
Standard controller method:
use Barryvdh\DomPDF\Facade\Pdf;
$pdf = Pdf::loadView('esbtp.xxx.pdf-xxx', compact('data'))
->setPaper('a4', 'portrait')
->setOptions([
'dpi' => 150,
'defaultFont' => 'sans-serif',
'isRemoteEnabled' => false,
'isHtml5ParserEnabled' => true,
'isPhpEnabled' => false,
'isFontSubsettingEnabled' => true,
]);
return $pdf->download('filename.pdf');
2. DomPDF CSS Cheat Sheet — What Works vs What Doesn't
✅ SUPPORTED (Use freely)
| Feature | Notes |
|---|---|
background-color on <td> |
Works — apply to td, NOT tr (see §6) |
border, border-collapse |
Full support |
padding, margin |
Full support |
font-size, font-weight, font-style |
Full support |
text-align, vertical-align |
Full support |
width, height in px/mm/% |
Full support |
@page (margins, size) |
Full support |
page-break-before/after/inside |
Full support |
:nth-child(), :first-child |
Full support |
@font-face with .ttf |
Full support with storage_path() |
border-radius |
Supported (CSS3 partial) |
opacity |
Supported |
display: block/inline/table |
Full support |
position: fixed (for headers/footers) |
Full support |
letter-spacing, line-height |
Full support |
❌ NOT SUPPORTED — Never use these in PDF templates
| Feature | Alternative |
|---|---|
display: flex / flexbox |
Use <table> layout |
display: grid |
Use <table> layout |
transform (rotate, translate) |
Not available |
| CSS animations / transitions | Not available |
box-shadow |
Use border instead |
backdrop-filter, clip-path |
Not available |
| Bootstrap 4+ classes | Write plain CSS |
| Tailwind CSS classes | Write plain CSS |
| Media queries | Not applicable in PDF |
float layouts |
Unreliable across pages — use tables |
External <link> CSS (relative URLs) |
Use inline <style> or public_path() |
asset() helper for images |
Use public_path() or base64 |
3. Standard KLASSCI PDF Template Structure
Every new PDF template must follow this structure:
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{ $title ?? 'Document' }}</title>
<style>
/* ── Reset ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: DejaVu Sans, Arial, sans-serif;
font-size: {{ $settings['font_size'] ?? 12 }}px;
color: {{ $settings['text_color'] ?? '#1f2937' }};
line-height: 1.45;
background: #ffffff;
}
/* ── Page setup ── */
@page {
margin: {{ $settings['margin_top'] ?? 15 }}mm
{{ $settings['margin_right'] ?? 10 }}mm
{{ $settings['margin_bottom'] ?? 15 }}mm
{{ $settings['margin_left'] ?? 10 }}mm;
}
/* ── Typography ── */
h1 { font-size: 18px; color: {{ $settings['primary_color'] ?? '#0453cb' }}; margin-bottom: 8px; }
h2 { font-size: 15px; color: {{ $settings['primary_color'] ?? '#0453cb' }}; margin-bottom: 6px; }
h3 { font-size: 13px; color: #374151; margin-bottom: 4px; }
/* ── Tables ── */
table { width: 100%; border-collapse: collapse; }
/* ── Utilities ── */
.text-right { text-align: right; }
.text-center { text-align: center; }
.text-muted { color: #6b7280; font-size: 10px; }
.bold { font-weight: bold; }
.mt-8 { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.mb-8 { margin-bottom: 8px; }
.mb-16 { margin-bottom: 16px; }
.page-break { page-break-before: always; }
.no-break { page-break-inside: avoid; }
</style>
</head>
<body>
{{-- ── HEADER ── --}}
@include('pdf.partials.header', ['settings' => $settings, 'logoBase64' => $logoBase64 ?? null])
{{-- ── MAIN CONTENT ── --}}
<div style="margin-top: 16px;">
{{-- Your content here --}}
</div>
{{-- ── FOOTER ── --}}
@include('pdf.partials.footer', ['settings' => $settings])
</body>
</html>
Using the centralized theme override
{{-- After your <style> block, add: --}}
@include('pdf.partials.theme')
{{-- This injects KLASSCI brand colors from SettingsHelper, overriding any hardcoded values --}}
4. KLASSCI Settings System
Load settings in your controller
// Standard PDF settings (colors, margins, fonts, logo)
$pdfSettings = \App\Helpers\SettingsHelper::getPdfSettings();
// Pass to view
$pdf = Pdf::loadView('esbtp.xxx.pdf', [
'settings' => $pdfSettings,
'logoBase64' => $this->prepareLogoBase64($pdfSettings['logo'] ?? null),
// ... other data
]);
Available settings keys
// Colors
$settings['primary_color'] // default: '#0453cb'
$settings['secondary_color'] // default: '#64748b'
$settings['accent_color'] // default: '#f59e0b'
$settings['text_color'] // default: '#1f2937'
$settings['header_bg_color'] // default: '#0453cb'
$settings['header_text_color'] // default: '#ffffff'
// Margins (mm)
$settings['margin_top'] // default: 20
$settings['margin_bottom'] // default: 20
$settings['margin_left'] // default: 15
$settings['margin_right'] // default: 15
// Typography
$settings['font_size'] // default: 12
// Branding
$settings['show_logo'] // boolean
$settings['logo_position'] // 'left' | 'center' | 'right'
$settings['header_text'] // custom header text
$settings['footer_text'] // custom footer text
$settings['signature_director']
$settings['signature_secretary']
$settings['watermark']
5. Layout Patterns — Tables Only (No Flexbox)
2-column layout
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td width="48%" style="vertical-align: top; padding-right: 8px;">
{{-- Left column content --}}
</td>
<td width="4%"></td>{{-- spacer --}}
<td width="48%" style="vertical-align: top; padding-left: 8px;">
{{-- Right column content --}}
</td>
</tr>
</table>
3-column layout
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td width="32%" style="vertical-align: top; padding-right: 6px;">Col 1</td>
<td width="2%"></td>
<td width="32%" style="vertical-align: top; padding: 0 6px;">Col 2</td>
<td width="2%"></td>
<td width="32%" style="vertical-align: top; padding-left: 6px;">Col 3</td>
</tr>
</table>
Info card (bordered box)
<table width="100%" border="0" cellspacing="0" cellpadding="0"
style="border: 1px solid #d1d5db; border-radius: 6px; margin-bottom: 12px;">
<tr>
<td style="background-color: {{ $settings['primary_color'] ?? '#0453cb' }};
color: #ffffff;
padding: 8px 12px;
font-weight: bold;
font-size: 12px;
border-radius: 6px 6px 0 0;">
{{ $cardTitle }}
</td>
</tr>
<tr>
<td style="padding: 12px; background-color: #f9fafb;">
{{-- Card content --}}
</td>
</tr>
</table>
Key-value info row
<table width="100%" border="0" cellspacing="0" cellpadding="4">
<tr>
<td width="35%" style="color: #6b7280; font-size: 11px;">Nom :</td>
<td width="65%" style="font-weight: bold; font-size: 11px;">{{ $etudiant->nom_complet }}</td>
</tr>
<tr>
<td style="color: #6b7280; font-size: 11px;">Matricule :</td>
<td style="font-size: 11px;">{{ $etudiant->matricule }}</td>
</tr>
</table>
6. Table Styling — The Critical tr→td Fix
The most common DomPDF bug: background-color on <tr> doesn't render. Always target <td> instead.
<style>
/* ❌ WRONG — background won't appear */
.header-row { background-color: #0453cb; }
/* ✅ CORRECT — target td directly */
.header-row td {
background-color: {{ $settings['header_bg_color'] ?? '#0453cb' }};
color: {{ $settings['header_text_color'] ?? '#ffffff' }};
padding: 10px 12px;
font-weight: bold;
font-size: 11px;
border-bottom: 2px solid #0343ab;
}
/* ✅ ALSO CORRECT — inline style on td */
/* <td style="background-color: #0453cb; color: white;"> */
/* Alternating rows (zebra striping) */
.data-row-even td { background-color: #f9fafb; }
.data-row-odd td { background-color: #ffffff; }
/* Hover effect (static in PDF — use for nth-child instead) */
tbody tr:nth-child(even) td { background-color: #f3f4f6; }
tbody tr:nth-child(odd) td { background-color: #ffffff; }
</style>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 16px;">
<thead>
<tr class="header-row">
<td>Matière</td>
<td class="text-center">Note /20</td>
<td class="text-center">Coeff.</td>
<td class="text-right">Moyenne</td>
</tr>
</thead>
<tbody>
@foreach($resultats as $i => $resultat)
<tr class="{{ $i % 2 === 0 ? 'data-row-even' : 'data-row-odd' }}">
<td style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb;">
{{ $resultat->matiere->name }}
</td>
<td style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; text-align: center;">
{{ number_format($resultat->note, 2) }}
</td>
<td style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; text-align: center;">
{{ $resultat->coefficient }}
</td>
<td style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; text-align: right; font-weight: bold;">
{{ number_format($resultat->moyenne, 2) }}
</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="3" style="padding: 10px 12px; background-color: #f3f4f6; font-weight: bold; border-top: 2px solid #d1d5db;">
Moyenne Générale
</td>
<td style="padding: 10px 12px; background-color: #f3f4f6; font-weight: bold; text-align: right; border-top: 2px solid #d1d5db; color: #0453cb;">
{{ number_format($moyenneGenerale, 2) }} / 20
</td>
</tr>
</tfoot>
</table>
7. Typography & Fonts
Built-in fonts (use without any setup)
DejaVu Sans— Best choice for French text (handles accents: é, è, à, ç, ê)DejaVu SerifDejaVu Sans Mono(for code/fixed-width)Arial,Helvetica(basic, limited character set)Times New Roman,Courier(base 14)
Always use DejaVu Sans for French documents:
body {
font-family: DejaVu Sans, Arial, sans-serif;
}
Custom font via @font-face (if needed)
<style>
@font-face {
font-family: 'CustomFont';
font-weight: normal;
font-style: normal;
src: url("{{ storage_path('fonts/CustomFont-Regular.ttf') }}") format('truetype');
}
@font-face {
font-family: 'CustomFont';
font-weight: bold;
src: url("{{ storage_path('fonts/CustomFont-Bold.ttf') }}") format('truetype');
}
body { font-family: 'CustomFont', DejaVu Sans, sans-serif; }
</style>
Typography scale (KLASSCI standard)
.pdf-h1 { font-size: 20px; font-weight: bold; color: #0453cb; }
.pdf-h2 { font-size: 16px; font-weight: bold; color: #0453cb; }
.pdf-h3 { font-size: 13px; font-weight: bold; color: #374151; }
.pdf-body { font-size: 11px; line-height: 1.5; }
.pdf-small { font-size: 9px; color: #6b7280; }
.pdf-label { font-size: 10px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; }
8. Images & Logos — Always Base64 or public_path()
The golden rule: asset() generates relative URLs that DomPDF cannot resolve. Use public_path() for file system paths or encode images as base64.
In controller — prepare base64 logo
private function prepareLogoBase64(?string $logoPath): ?string
{
// Priority list of paths to try
$paths = [
$logoPath ? storage_path('app/public/' . $logoPath) : null,
public_path('images/logo.png'),
public_path('images/esbtp_logo.png'),
public_path('images/logo.jpeg'),
];
foreach (array_filter($paths) as $path) {
if (file_exists($path)) {
$mime = mime_content_type($path);
$data = base64_encode(file_get_contents($path));
return "data:{$mime};base64,{$data}";
}
}
return null;
}
private function preparePhotoBase64(?string $photoPath): ?string
{
if (!$photoPath) return null;
$fullPath = storage_path('app/public/' . $photoPath);
if (!file_exists($fullPath)) return null;
// Convert to JPEG for best PDF compatibility
$image = imagecreatefromstring(file_get_contents($fullPath));
if (!$image) return null;
ob_start();
imagejpeg($image, null, 85);
$data = base64_encode(ob_get_clean());
imagedestroy($image);
return "data:image/jpeg;base64,{$data}";
}
In Blade template — display base64 or public_path
{{-- Logo (base64, self-contained) --}}
@if($logoBase64)
<img src="{{ $logoBase64 }}"
style="max-height: 60px; max-width: 160px;"
alt="Logo">
@else
{{-- Fallback to public_path --}}
<img src="{{ public_path('images/logo.png') }}"
style="max-height: 60px; max-width: 160px;"
alt="Logo">
@endif
{{-- Student photo --}}
@if($photoBase64)
<img src="{{ $photoBase64 }}"
style="width: 80px; height: 80px; border: 2px solid #d1d5db; border-radius: 4px;"
alt="Photo">
@endif
{{-- Direct public_path for static assets --}}
<img src="{{ public_path('images/stamp.png') }}"
style="width: 100px; opacity: 0.4;"
alt="">
9. Headers, Footers & Page Numbers
Repeating header on every page (position: fixed)
<style>
/* Fixed header — repeats on every page */
#pdf-header {
position: fixed;
top: -15mm; /* Negative = in the margin area */
left: 0;
right: 0;
height: 14mm;
border-bottom: 2px solid {{ $settings['primary_color'] ?? '#0453cb' }};
}
/* Fixed footer */
#pdf-footer {
position: fixed;
bottom: -15mm;
left: 0;
right: 0;
height: 12mm;
border-top: 1px solid #d1d5db;
}
/* Push body content below header */
#content { margin-top: 5mm; }
</style>
<div id="pdf-header">
<table width="100%">
<tr>
<td>
@if($logoBase64)
<img src="{{ $logoBase64 }}" style="height: 10mm;" alt="Logo">
@endif
</td>
<td style="text-align: center; font-weight: bold; font-size: 11px;">
{{ $settings['header_text'] ?? config('app.name') }}
</td>
<td style="text-align: right; font-size: 9px; color: #6b7280;">
{{ date('d/m/Y') }}
</td>
</tr>
</table>
</div>
<div id="pdf-footer">
<table width="100%">
<tr>
<td style="font-size: 9px; color: #6b7280;">
{{ $settings['footer_text'] ?? '' }}
</td>
<td style="text-align: right; font-size: 9px; color: #6b7280;">
Page <span class="page"></span> / <span class="topage"></span>
</td>
</tr>
</table>
</div>
<div id="content">
{{-- Main content --}}
</div>
Page numbers via DomPDF canvas script
{{-- Requires isPhpEnabled = true in setOptions --}}
<script type="text/php">
if (isset($pdf)) {
$pdf->page_script(function ($pageNumber, $pageCount, $canvas, $fontMetrics) {
$font = $fontMetrics->getFont("DejaVu Sans", "normal");
$size = 9;
$color = [0.42, 0.45, 0.50]; // #6b7280
$text = "Page {$pageNumber} / {$pageCount}";
$width = $canvas->get_width();
$height = $canvas->get_height();
$textWidth = $fontMetrics->getTextWidth($text, $font, $size);
$canvas->text($width - $textWidth - 20, $height - 20, $text, $font, $size, $color);
});
}
</script>
10. Matching App Web Previews Pixel-Perfect
When the user shows a web preview and wants the PDF to look identical:
Step 1 — Inspect the web view
# Read the Blade view file for the preview page
# Identify: layout structure, colors, fonts, spacing, tables
Step 2 — Extract the color palette
Look at the view's <style> or CSS classes and map them to KLASSCI palette:
- Blue buttons/headers →
#0453cb(primary) - Gray text →
#64748b(secondary) - Green badges →
#10b981 - Amber/yellow →
#f59e0b - Dark text →
#1f2937
Step 3 — Convert layout to DomPDF-safe HTML
| Web element | PDF equivalent |
|---|---|
<div class="row"><div class="col-6"> |
<table><tr><td width="50%"> |
<div class="d-flex gap-3"> |
<table><tr><td style="padding-right: 12px;"> |
<div class="card"> |
Info card pattern (see §5) |
<span class="badge bg-success"> |
<span style="background-color: #10b981; color: #fff; padding: 2px 6px; border-radius: 3px; font-size: 9px;"> |
<h5 class="card-title"> |
<h3 style="font-size: 13px; color: #0453cb;"> |
| Bootstrap table | Remove classes, add inline styles (see §6) |
Step 4 — Map measurements
- Bootstrap
col-6=td width="48%"(with 4% spacer) - Bootstrap
col-4=td width="31%" - Bootstrap
col-3=td width="23%" - Bootstrap
p-3=padding: 16px - Bootstrap
p-2=padding: 8px - Bootstrap
gap-3=cellspacing="12"orpadding-right: 12px
Step 5 — Test at matching DPI
Use DPI 96 for on-screen comparison, DPI 150 for print quality. The visual weight of borders and text shifts with DPI — always compare at the same setting.
11. Page Breaks & Pagination
<style>
/* Force new page */
.page-break-before { page-break-before: always; }
.page-break-after { page-break-after: always; }
/* Never split this element across pages */
.keep-together { page-break-inside: avoid; }
/* Keep heading with its following content */
h2, h3 { page-break-after: avoid; }
/* Prevent orphans/widows */
p { orphans: 3; widows: 3; }
</style>
{{-- Force new page per student (e.g., in bulk bulletin generation) --}}
@foreach($bulletins as $index => $bulletin)
@if($index > 0)
<div class="page-break-before"></div>
@endif
<div class="keep-together">
{{-- Bulletin content --}}
</div>
@endforeach
{{-- Keep table header with first rows --}}
<div class="keep-together">
<h3>Résultats par matière</h3>
<table>
<thead>...</thead>
<tbody>
{{-- first few rows --}}
</tbody>
</table>
</div>
12. Performance Optimization
// Controller — performance-optimized PDF generation
$pdf = Pdf::loadView('esbtp.bulletins.pdf', $data)
->setPaper('a4', 'portrait')
->setOptions([
'dpi' => 96, // 96=web, 150=print (default for KLASSCI bulletins)
'defaultFont' => 'DejaVu Sans',
'isRemoteEnabled' => false, // Security + speed
'isHtml5ParserEnabled' => true,
'isPhpEnabled' => false, // Enable only if you need page_script()
'isFontSubsettingEnabled'=> true, // ~30% smaller files
]);
// For very large documents (>50 pages): increase PHP memory limit
ini_set('memory_limit', '512M');
set_time_limit(120);
// For bulk generation: use Laravel queues
dispatch(new GenerateBulletinPdfJob($bulletin->id));
DPI guide
| Use case | DPI | File size | Notes |
|---|---|---|---|
| Web download (email, portal) | 96 | Small | Fast generation |
| Default KLASSCI documents | 150 | Medium | Good balance |
| High-quality print | 300 | Large | Slow, use queues |
12.5. Blade Pitfalls That Crash PDF Rendering ⚠
These bugs are silent at compile time (php artisan view:cache succeeds) but crash PHP at runtime when the PDF actually renders. They affect all Blade files but PDF templates are the most common victims because they use @php(expr) shortform for logo loading.
❌ Pitfall #1 — @php(expr) shortform mixed with @php ... @endphp blocks
The bug : Laravel's Blade compiler regex (?<!@)@php(.*?)@endphp/s does NOT distinguish the shortform @php(expr) (with parens, no @endphp after) from the block form @php ... @endphp. If your file uses @php(expr) shortform AND has any @php ... @endphp block later, the regex captures @php( of the shortform as a block opening and eats everything until the next @endphp.
Symptom: the file compiles silently, but PHP runtime crashes with errors like:
syntax error, unexpected token "endforeach", expecting end of file
The @endforeach (or similar) appears orphaned because its @foreach was inside the eaten range and never compiled.
Real incident (2026-05-08, KLASSCI presentation): liste-appel-pdf.blade.php had @php($_logo = ...) at line 392 and @php $accProfile = ...; @endphp at line 472. The regex ate L392→L472, leaving the @foreach($etudiants) at L470 as text. PHP crashed at L498's <?php endforeach; ?>. 4 PDFs were affected.
Fix : NEVER use @php(expr) shortform in a file that also contains @php ... @endphp blocks. Always use the block form, even on a single line:
{{-- ❌ FRAGILE — eaten by next @endphp in file --}}
@php($_logo = \App\Helpers\SettingsHelper::resolveLogoBase64())
{{-- ✅ ROBUST — self-contained block --}}
@php $_logo = \App\Helpers\SettingsHelper::resolveLogoBase64(); @endphp
Detection :
# Quick audit on any Blade file
grep -n "@php(" resources/views/path/to/file.blade.php
# Validate compiled output catches the bug:
rm storage/framework/views/*.php
php artisan view:cache
php -l storage/framework/views/<hash>.php
# → Should output "No syntax errors detected"
❌ Pitfall #2 — @directive literal in JS/HTML comments
Blade scans the entire file for @xxx patterns, including inside JavaScript and HTML comments. Writing @can, @if, @foreach, @endif, etc. literally inside a comment triggers Blade's directive parser.
Real incident (2026-05-07): a JS comment // gating @can côté Blade in inscriptions/create.blade.php was parsed as an unclosed @can(...) directive → 500 error on the entire page.
Fix : Reformulate the comment to avoid the directive name, or escape with @@xxx (Blade outputs @xxx literal):
{{-- ❌ Triggers Blade --}}
<script>
// gating @can côté Blade
</script>
{{-- ✅ Safe (reformulation) --}}
<script>
// gating Blade côté serveur
</script>
{{-- ✅ Safe (escape with @@) --}}
<script>
// see @@can directive in the partial
</script>
Audit checklist before deploying any new Blade
# 1. Count balanced directives
grep -oE "@(if|endif|foreach|endforeach|php|endphp|can|endcan|push|endpush|once|endonce|section|endsection)\b" file.blade.php | sort | uniq -c
# Each pair must have equal counts.
# 2. Look for shortform @php( in files with later @endphp
grep -n "@php(" file.blade.php
# If found AND there's a @endphp later → convert to block form.
# 3. Look for directives in comments
grep -nE "//.*@(can|if|foreach|endif)" file.blade.php
grep -nE "<!--.*@(can|if|foreach|endif)" file.blade.php
# Either remove the @ from the comment text or escape with @@.
# 4. Force lint compiled output (catches both pitfalls above)
rm storage/framework/views/*.php
php artisan view:cache
for f in storage/framework/views/*.php; do php -l "$f" 2>&1 | grep -v "No syntax errors"; done
12.6. Pièges payés en production (bulletins Abidjan, août 2026)
Quatre bugs réels, chacun vu sur un bulletin imprimé avant d'être compris :
- Spécificité contre les resets de table.
.parent td { border:none; padding:0 }= (0,1,1) et bat TOUTE simple classe.ma-cellule= (0,1,0). Conséquence : bordure/padding d'une cellule « spéciale » silencieusement annulés. Règle : dans un tableau qui a un reset surtd, toute cellule particulière se cible.parent td.ma-cellule. Ce piège a frappé 3 fois dans la même session (filet vertical invisible, cellule sans padding, valeur collée aux deux points du libellé). - Un flag qui gate
@page. Si le template n'émet son bloc@pageque sous condition (@if($isPdfExport ?? false)), TOUT chemin de rendu PDF doit poser ce flag. Un chemin l'oubliait → DomPDF appliquait ses marges par défaut (~27mm) et les réglages de marges étaient sans effet. Vérifier avec grep que chaquePDF::loadViewdu domaine pose le flag. inline-blockn'aligne pas les lignes de base. Deux spansinline-blockcôte à côte (libellé + valeur) apparaissent décalés verticalement. Seul montage fiable : deux<td>d'une même<tr>. Largeur du libellé enempour suivrebulletin_font_size.td:first-childtraverse les tableaux imbriqués. Une règle sur.outer td:first-child(cellule photo) attrape aussi la première cellule de chaque ligne d'un tableau imbriqué dans une autre cellule. Toujours une classe explicite.
Et le rappel du §6 vécu en vrai : background sur <tr> n'est pas rendu mais color:#fff
EST hérité par les td → bandeau blanc avec texte blanc, invisible à l'impression.
13. Debugging Tips
Check if images load
{{-- Debug: show the path being used --}}
@if(config('app.debug'))
<!-- Logo path: {{ $logoBase64 ? 'base64 (ok)' : public_path('images/logo.png') }} -->
@endif
Common errors and fixes
| Error | Cause | Fix |
|---|---|---|
| Images not showing | Using asset() |
Use public_path() or base64 |
| Background on rows not showing | CSS on <tr> |
Target <tr> td or put on <td> |
| Broken layout | Using flexbox/grid | Convert to <table> layout |
| Font with accents broken | Using Arial/Helvetica | Use DejaVu Sans |
| PDF cuts element mid-way | No page-break control | Add page-break-inside: avoid |
| Colors not matching brand | Hardcoded colors | Use $settings['primary_color'] |
| Huge file size | DPI 300 + no font subsetting | Use DPI 150 + isFontSubsettingEnabled: true |
| Memory exhausted | Large dataset | Add ini_set('memory_limit', '512M') or use queues |
CSS from theme.blade.php not applying |
Missing @include |
Add @include('pdf.partials.theme') after </style> |
Test the template in browser first
// Add a /preview route alongside the /pdf route
Route::get('/bulletins/{id}/preview', function ($id) {
$bulletin = ESBTPBulletin::findOrFail($id);
// ... same data preparation as genererPDF()
return view('esbtp.bulletins.pdf-configurable', $data);
});
Browser preview uses CSS normally — fix layout issues there first, then generate PDF.
14. Ready-to-Copy Snippets
Document header block (with logo + school info + title)
<table width="100%" border="0" cellspacing="0" cellpadding="0"
style="border-bottom: 2.5px solid {{ $settings['primary_color'] ?? '#0453cb' }};
padding-bottom: 10px;
margin-bottom: 16px;">
<tr>
{{-- Left: Logo --}}
<td width="20%" style="vertical-align: middle;">
@if($logoBase64)
<img src="{{ $logoBase64 }}" style="max-height: 55px; max-width: 110px;" alt="Logo">
@endif
</td>
{{-- Center: School name + document title --}}
<td width="60%" style="text-align: center; vertical-align: middle;">
<div style="font-size: 11px; color: #6b7280; margin-bottom: 2px;">
RÉPUBLIQUE DE CÔTE D'IVOIRE
</div>
<div style="font-size: 14px; font-weight: bold; color: {{ $settings['primary_color'] ?? '#0453cb' }};">
{{ config('app.school_name', 'ESBTP') }}
</div>
<div style="font-size: 16px; font-weight: bold; color: #1f2937; margin-top: 4px;">
{{ strtoupper($documentTitle ?? 'BULLETIN DE NOTES') }}
</div>
<div style="font-size: 10px; color: #6b7280; margin-top: 2px;">
{{ $anneeUniversitaire->name ?? '' }} — {{ $periode ?? '' }}
</div>
</td>
{{-- Right: Student photo --}}
<td width="20%" style="text-align: right; vertical-align: middle;">
@if($photoBase64)
<img src="{{ $photoBase64 }}"
style="width: 65px; height: 75px; border: 1px solid #d1d5db; border-radius: 3px;"
alt="Photo">
@endif
</td>
</tr>
</table>
Badge / status indicator
@php
$badgeColors = [
'validé' => ['bg' => '#d1fae5', 'text' => '#065f46'],
'en_attente' => ['bg' => '#fef3c7', 'text' => '#92400e'],
'rejeté' => ['bg' => '#fee2e2', 'text' => '#991b1b'],
'default' => ['bg' => '#f3f4f6', 'text' => '#374151'],
];
$colors = $badgeColors[$status] ?? $badgeColors['default'];
@endphp
<span style="
display: inline-block;
background-color: {{ $colors['bg'] }};
color: {{ $colors['text'] }};
padding: 2px 8px;
border-radius: 10px;
font-size: 9px;
font-weight: bold;
border: 1px solid {{ $colors['text'] }}40;
">{{ ucfirst($status) }}</span>
Signature block (director + secretary)
<table width="100%" border="0" cellspacing="0" cellpadding="0"
style="margin-top: 30px; page-break-inside: avoid;">
<tr>
<td width="45%" style="text-align: center; vertical-align: top;">
<div style="font-size: 10px; color: #6b7280; margin-bottom: 6px;">
LE DIRECTEUR
</div>
@if($settings['signature_director'])
<img src="{{ public_path('storage/' . $settings['signature_director']) }}"
style="max-height: 45px; max-width: 120px;" alt="Signature">
@else
<div style="height: 45px;"></div>
@endif
<div style="border-top: 1px solid #374151; margin-top: 4px; padding-top: 4px; font-size: 10px; font-weight: bold;">
{{ $settings['director_name'] ?? '' }}
</div>
</td>
<td width="10%"></td>
<td width="45%" style="text-align: center; vertical-align: top;">
<div style="font-size: 10px; color: #6b7280; margin-bottom: 6px;">
LA SCOLARITÉ
</div>
@if($settings['signature_secretary'])
<img src="{{ public_path('storage/' . $settings['signature_secretary']) }}"
style="max-height: 45px; max-width: 120px;" alt="Signature">
@else
<div style="height: 45px;"></div>
@endif
<div style="border-top: 1px solid #374151; margin-top: 4px; padding-top: 4px; font-size: 10px; font-weight: bold;">
{{ $settings['secretary_name'] ?? '' }}
</div>
</td>
</tr>
</table>
Document footer
<div style="margin-top: 20px;
padding-top: 8px;
border-top: 1px solid #e5e7eb;
text-align: center;
font-size: 9px;
color: #9ca3af;
page-break-inside: avoid;">
{{ $settings['footer_text'] ?? config('app.name') }}
— Généré le {{ now()->format('d/m/Y à H:i') }}
@if($settings['watermark'])
— {{ $settings['watermark'] }}
@endif
</div>
Stat card row (KPIs)
<table width="100%" border="0" cellspacing="0" cellpadding="0" style="margin-bottom: 16px;">
<tr>
@foreach($stats as $stat)
<td width="{{ floor(100 / count($stats)) }}%"
style="padding: @if(!$loop->first)0 0 0 8px@else 0 8px 0 0@endif; vertical-align: top;">
<table width="100%" style="border: 1px solid #e5e7eb; border-radius: 6px;">
<tr>
<td style="background-color: #f9fafb;
padding: 10px 12px;
border-radius: 6px 6px 0 0;
font-size: 9px;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.3px;">
{{ $stat['label'] }}
</td>
</tr>
<tr>
<td style="padding: 8px 12px 10px;
font-size: 18px;
font-weight: bold;
color: {{ $settings['primary_color'] ?? '#0453cb' }};">
{{ $stat['value'] }}
</td>
</tr>
</table>
</td>
@endforeach
</tr>
</table>