feat: tilføj rigtig PDF download via WeasyPrint
All checks were successful
Build and Deploy Erika CV / build-and-deploy (push) Successful in 58s

- /download route genererer PDF server-side
- PDF ser ud som hjemmesiden (ikke browser-print)
- Kompakt print-CSS til 1 A4-side
- Knap downloader direkte i stedet for at åbne print-dialog
This commit is contained in:
Henrik Jess Nielsen
2026-04-19 17:56:01 +02:00
parent 63f5fb4555
commit ff48f31ae5
4 changed files with 63 additions and 20 deletions

View File

@@ -14,6 +14,15 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libpango-1.0-0 \
libpangoft2-1.0-0 \
libharfbuzz0b \
libfontconfig1 \
libcairo2 \
fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --upgrade --no-cache-dir pip RUN pip install --upgrade --no-cache-dir pip
COPY requirements.txt ./ COPY requirements.txt ./

15
app.py
View File

@@ -1,6 +1,8 @@
from flask import Flask, render_template, jsonify from flask import Flask, render_template, jsonify, send_file, request
from weasyprint import HTML
from datetime import datetime, timezone from datetime import datetime, timezone
import os import os
import io
app = Flask(__name__) app = Flask(__name__)
@@ -11,6 +13,17 @@ GIT_COMMIT = os.getenv('GIT_COMMIT', 'unknown')
def index(): def index():
return render_template('index.html') return render_template('index.html')
@app.route('/download')
def download_pdf():
html = render_template('index.html', pdf_mode=True)
pdf = HTML(string=html, base_url=request.host_url).write_pdf()
return send_file(
io.BytesIO(pdf),
mimetype='application/pdf',
as_attachment=True,
download_name='Erika_Nielsen_CV.pdf'
)
@app.route('/health') @app.route('/health')
def health(): def health():
return jsonify({ return jsonify({

View File

@@ -1,2 +1,3 @@
flask>=3.0.0 flask>=3.0.0
gunicorn>=22.0.0 gunicorn>=22.0.0
weasyprint>=62.0

View File

@@ -379,26 +379,44 @@
} }
.pdf-btn:hover { background: #2f72d0; transform: scale(1.04); } .pdf-btn:hover { background: #2f72d0; transform: scale(1.04); }
/* Print */ /* Print / PDF via WeasyPrint */
@media print { @media print {
@page { size: A4; margin: 0; } @page { size: A4; margin: 8mm; }
html, body { background: white; padding: 0; margin: 0; display: block; } html, body { background: white !important; padding: 0; margin: 0; }
body { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
-webkit-print-color-adjust: exact; .cv-wrapper { box-shadow: none; border-radius: 0; width: 194mm; max-width: 194mm; }
print-color-adjust: exact;
}
.cv-wrapper {
box-shadow: none;
border-radius: 0;
width: 100%;
max-width: 100%;
transform-origin: top left;
transform: scale(0.78);
height: 128vh;
overflow: hidden;
}
.sidebar { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .sidebar { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.pdf-btn { display: none; } .pdf-btn { display: none !important; }
/* Kompakt til 1 side */
body { padding: 0; }
.hero { padding: 18px 22px 12px; }
.hero-name { font-size: 22px; }
.hero-tagline { font-size: 11px; margin-bottom: 6px; }
.hero-summary { font-size: 11px; line-height: 1.5; }
.main { padding: 0 18px 10px; }
section { margin-bottom: 10px; }
.section-title { font-size: 10px; padding: 5px 0 4px; margin-bottom: 8px; }
.timeline-item { padding: 7px 0; }
.timeline-period { font-size: 9px; }
.timeline-title { font-size: 12px; }
.timeline-subtitle { font-size: 10px; }
.timeline-bullets { font-size: 10px; margin-top: 2px; }
.timeline-bullets li { margin-bottom: 0; }
.edu-card { padding: 7px 10px; }
.edu-period { font-size: 9px; }
.edu-title { font-size: 11px; }
.edu-place { font-size: 10px; }
.lang-item label { font-size: 10px; margin-bottom: 3px; }
.lang-bar-track { height: 4px; }
.tag { font-size: 9px; padding: 2px 6px; }
.sidebar-name h1 { font-size: 17px; }
.sidebar-name p { font-size: 10px; }
.sidebar-section { margin-bottom: 16px; }
.sidebar-section-title { font-size: 8px; }
.sidebar-item { font-size: 11px; }
.sidebar-item small { font-size: 9px; }
.avatar-wrap { padding: 18px 0 10px; }
.avatar { width: 70px; height: 70px; font-size: 22px; }
} }
/* Mobile */ /* Mobile */
@@ -604,7 +622,9 @@
</main> </main>
</div> </div>
<button class="pdf-btn" onclick="window.print()">⬇ Download CV som PDF</button> {% if not pdf_mode %}
<button class="pdf-btn" onclick="window.location='/download'">⬇ Download CV som PDF</button>
{% endif %}
</body> </body>
</html> </html>