Markdown Print Styles and PDF Formatting: Complete Guide for Professional Document Output and Print-Ready Publications
Master print styles and PDF formatting for Markdown documents. Learn CSS print media queries, page layout control, typography optimization, and automated PDF generation for professional document output.
Print styles and PDF formatting for Markdown documents enable professional document output that maintains design consistency, typography excellence, and layout precision across digital screens and physical publications. While Markdown provides semantic structure ideal for web display, specialized CSS print media queries and PDF generation techniques ensure optimal presentation when documents transition from screen to paper or professional PDF distribution.
Why Master Print Styles for Markdown?
Professional print formatting provides essential benefits for document publication:
- Professional Publications: Create print-ready documents that maintain brand standards and typography excellence
- Consistent Output: Ensure reliable formatting across different PDF generators and printing systems
- Cost-Effective Publishing: Optimize layouts for efficient paper usage and reduced printing costs
- Accessibility Compliance: Implement print styles that support high contrast and readable typography
- Cross-Platform Reliability: Generate consistent results across browsers and PDF conversion tools
Understanding Print CSS Fundamentals
Print Media Queries and Page Control
CSS print media queries provide comprehensive control over document appearance in print format:
/* comprehensive-print-styles.css - Professional print formatting */
/* Print-specific CSS variables */
@media print {
:root {
--print-font-family: Georgia, 'Times New Roman', serif;
--print-font-size: 12pt;
--print-line-height: 1.4;
--print-margin: 1in;
--print-header-height: 0.5in;
--print-footer-height: 0.5in;
--print-primary-color: #000000;
--print-secondary-color: #333333;
--print-accent-color: #666666;
--print-border-color: #000000;
--print-heading-size-h1: 18pt;
--print-heading-size-h2: 16pt;
--print-heading-size-h3: 14pt;
--print-heading-size-h4: 12pt;
}
/* Page setup and margins */
@page {
size: 8.5in 11in; /* US Letter size */
margin: var(--print-margin);
/* Headers and footers */
@top-left {
content: "Document Title";
font: 10pt var(--print-font-family);
color: var(--print-secondary-color);
}
@top-right {
content: "Page " counter(page);
font: 10pt var(--print-font-family);
color: var(--print-secondary-color);
}
@bottom-center {
content: "© 2025 Company Name - Confidential";
font: 8pt var(--print-font-family);
color: var(--print-accent-color);
}
}
/* A4 page size alternative */
@page :first {
size: A4;
margin-top: 2in; /* Extra space for letterhead */
}
/* Chapter pages */
@page chapter {
@bottom-center {
content: none;
}
@top-left {
content: attr(data-chapter-title);
}
}
/* Base document styles */
* {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
print-color-adjust: exact !important;
}
body {
font-family: var(--print-font-family);
font-size: var(--print-font-size);
line-height: var(--print-line-height);
color: var(--print-primary-color);
background: white;
margin: 0;
padding: 0;
}
/* Typography optimization for print */
h1, h2, h3, h4, h5, h6 {
font-family: var(--print-font-family);
font-weight: bold;
color: var(--print-primary-color);
page-break-after: avoid;
page-break-inside: avoid;
margin-top: 24pt;
margin-bottom: 12pt;
}
h1 {
font-size: var(--print-heading-size-h1);
page-break-before: always;
counter-reset: section;
}
h1:first-child {
page-break-before: avoid;
margin-top: 0;
}
h2 {
font-size: var(--print-heading-size-h2);
page-break-before: avoid;
counter-increment: section;
counter-reset: subsection;
}
h2::before {
content: counter(section) ". ";
}
h3 {
font-size: var(--print-heading-size-h3);
counter-increment: subsection;
}
h3::before {
content: counter(section) "." counter(subsection) " ";
}
h4 {
font-size: var(--print-heading-size-h4);
}
/* Paragraph and text formatting */
p {
margin: 0 0 12pt 0;
text-align: justify;
text-justify: inter-word;
hyphens: auto;
-webkit-hyphens: auto;
-ms-hyphens: auto;
orphans: 3;
widows: 3;
}
p:last-child {
margin-bottom: 0;
}
/* List formatting */
ul, ol {
margin: 12pt 0;
padding-left: 24pt;
page-break-inside: avoid;
}
li {
margin-bottom: 6pt;
page-break-inside: avoid;
orphans: 2;
widows: 2;
}
/* Table formatting */
table {
width: 100%;
border-collapse: collapse;
margin: 12pt 0;
page-break-inside: avoid;
font-size: 10pt;
}
table, th, td {
border: 1pt solid var(--print-border-color);
}
th {
background: #f0f0f0 !important;
font-weight: bold;
padding: 6pt;
text-align: left;
}
td {
padding: 4pt 6pt;
vertical-align: top;
}
/* Table headers on new pages */
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
/* Code formatting */
code {
font-family: 'Courier New', Courier, monospace;
font-size: 10pt;
background: #f5f5f5;
padding: 1pt 2pt;
border: 0.5pt solid #cccccc;
}
pre {
background: #f8f8f8;
border: 1pt solid #cccccc;
padding: 8pt;
margin: 12pt 0;
white-space: pre-wrap;
word-wrap: break-word;
page-break-inside: avoid;
font-family: 'Courier New', Courier, monospace;
font-size: 9pt;
line-height: 1.2;
}
pre code {
background: none;
border: none;
padding: 0;
}
/* Blockquote formatting */
blockquote {
border-left: 3pt solid var(--print-border-color);
margin: 12pt 0 12pt 12pt;
padding-left: 12pt;
font-style: italic;
page-break-inside: avoid;
}
/* Image handling */
img {
max-width: 100%;
height: auto;
page-break-inside: avoid;
page-break-after: avoid;
}
figure {
margin: 12pt 0;
page-break-inside: avoid;
text-align: center;
}
figcaption {
font-size: 10pt;
font-style: italic;
margin-top: 6pt;
text-align: center;
}
/* Hide web-only elements */
.no-print,
.screen-only,
nav,
.navigation,
.sidebar,
.advertisement,
.social-share,
button:not(.print-button),
input[type="search"],
.search-form {
display: none !important;
}
/* Show print-only elements */
.print-only {
display: block !important;
}
/* URL display for links */
a:after {
content: " (" attr(href) ")";
font-size: 9pt;
color: var(--print-secondary-color);
text-decoration: none;
}
a[href^="#"]:after,
a[href^="javascript:"]:after,
a[href=""]:after {
content: "";
}
/* Page break controls */
.page-break-before {
page-break-before: always;
}
.page-break-after {
page-break-after: always;
}
.page-break-inside-avoid {
page-break-inside: avoid;
}
.keep-together {
page-break-inside: avoid;
display: block;
}
/* Chapter and section breaks */
.chapter {
page: chapter;
page-break-before: always;
}
.section {
page-break-before: avoid;
}
/* Table of contents styling */
.toc {
page-break-after: always;
}
.toc-entry {
display: flex;
justify-content: space-between;
margin-bottom: 6pt;
border-bottom: 1pt dotted var(--print-accent-color);
padding-bottom: 2pt;
}
.toc-title {
flex-grow: 1;
margin-right: 12pt;
}
.toc-page {
flex-shrink: 0;
font-weight: bold;
}
/* Print optimization for specific content types */
.callout,
.alert,
.note {
border: 2pt solid var(--print-border-color);
padding: 8pt;
margin: 12pt 0;
background: #f9f9f9 !important;
page-break-inside: avoid;
}
.callout-title {
font-weight: bold;
margin-bottom: 6pt;
}
/* Footnote styling */
.footnote {
font-size: 9pt;
margin-top: 24pt;
border-top: 1pt solid var(--print-border-color);
padding-top: 6pt;
}
.footnote-ref {
vertical-align: super;
font-size: 8pt;
line-height: 0;
}
/* Index styling */
.index {
column-count: 2;
column-gap: 24pt;
column-rule: 1pt solid var(--print-accent-color);
}
.index-entry {
margin-bottom: 3pt;
text-indent: -12pt;
margin-left: 12pt;
}
.index-subentry {
margin-left: 12pt;
font-size: 10pt;
}
}
/* High-resolution print styles */
@media print and (min-resolution: 300dpi) {
body {
font-size: 11pt;
}
h1 { font-size: 17pt; }
h2 { font-size: 15pt; }
h3 { font-size: 13pt; }
code, pre {
font-size: 8pt;
}
}
/* Print styles for different paper sizes */
@media print and (width: 210mm) { /* A4 width */
:root {
--print-margin: 25mm;
}
@page {
size: A4;
margin: var(--print-margin);
}
}
@media print and (width: 8.5in) { /* US Letter width */
:root {
--print-margin: 1in;
}
@page {
size: letter;
margin: var(--print-margin);
}
}
Advanced Page Layout and Typography
Professional document formatting with sophisticated layout control:
/* advanced-print-layout.css - Enterprise document formatting */
@media print {
/* Document structure variables */
:root {
--document-title: "Technical Documentation";
--document-version: "v2.1";
--document-date: "September 2025";
--company-name: "Professional Publishing";
--page-number-format: decimal;
--chapter-number-format: upper-roman;
--section-number-format: decimal;
}
/* Multi-page document structure */
@page cover {
margin: 0;
@top-left { content: none; }
@top-right { content: none; }
@bottom-center { content: none; }
}
@page toc {
@top-left {
content: "Table of Contents";
}
@bottom-center {
content: "Page " counter(page, lower-roman);
}
}
@page chapter:first {
margin-top: 2in;
@top-left { content: none; }
}
@page chapter:left {
margin-left: 1.5in;
margin-right: 1in;
@top-left {
content: var(--document-title);
}
@top-right { content: none; }
}
@page chapter:right {
margin-left: 1in;
margin-right: 1.5in;
@top-left { content: none; }
@top-right {
content: "Chapter " counter(chapter, var(--chapter-number-format));
}
}
/* Advanced counters */
body {
counter-reset: chapter 0 page 1;
}
.cover-page {
page: cover;
page-break-after: always;
}
.table-of-contents {
page: toc;
counter-reset: page 1;
page-break-after: always;
}
.chapter {
page: chapter;
page-break-before: always;
counter-increment: chapter;
counter-reset: section 0 subsection 0;
}
/* Chapter title pages */
.chapter-title {
font-size: 24pt;
font-weight: bold;
text-align: center;
margin-top: 2in;
margin-bottom: 1in;
page-break-after: avoid;
}
.chapter-title::before {
content: "Chapter " counter(chapter, var(--chapter-number-format));
display: block;
font-size: 18pt;
font-weight: normal;
margin-bottom: 0.5in;
color: var(--print-secondary-color);
}
.chapter-summary {
font-style: italic;
text-align: center;
margin: 1in 2in;
page-break-after: always;
}
/* Advanced section numbering */
.section-header {
counter-increment: section;
counter-reset: subsection 0;
}
.section-header::before {
content: counter(chapter) "." counter(section) " ";
font-weight: bold;
}
.subsection-header {
counter-increment: subsection;
}
.subsection-header::before {
content: counter(chapter) "." counter(section) "." counter(subsection) " ";
}
/* Professional table formatting */
.data-table {
font-size: 9pt;
margin: 18pt 0;
width: 100%;
}
.data-table th {
background: #e0e0e0 !important;
font-weight: bold;
text-align: center;
padding: 8pt 6pt;
border: 1pt solid var(--print-border-color);
}
.data-table td {
padding: 4pt 6pt;
border: 0.5pt solid var(--print-accent-color);
text-align: left;
}
.data-table .numeric {
text-align: right;
font-family: 'Courier New', monospace;
}
.data-table .centered {
text-align: center;
}
/* Table captions and numbering */
.table-caption {
font-weight: bold;
margin-bottom: 6pt;
text-align: center;
counter-increment: table;
}
.table-caption::before {
content: "Table " counter(chapter) "-" counter(table) ": ";
}
/* Figure numbering and captions */
.figure {
counter-increment: figure;
text-align: center;
page-break-inside: avoid;
margin: 18pt 0;
}
.figure-caption {
font-style: italic;
font-size: 10pt;
margin-top: 6pt;
counter-increment: figure;
}
.figure-caption::before {
content: "Figure " counter(chapter) "-" counter(figure) ": ";
font-weight: bold;
}
/* Code listing formatting */
.code-listing {
page-break-inside: avoid;
counter-increment: listing;
}
.code-listing-title {
font-weight: bold;
margin-bottom: 6pt;
border-bottom: 1pt solid var(--print-accent-color);
padding-bottom: 3pt;
}
.code-listing-title::before {
content: "Listing " counter(chapter) "-" counter(listing) ": ";
}
.code-listing pre {
margin-top: 0;
border: 1pt solid var(--print-border-color);
background: #f8f8f8 !important;
}
/* Cross-references */
.ref-chapter::before {
content: "Chapter " target-counter(attr(href), chapter);
}
.ref-section::before {
content: "Section " target-counter(attr(href), chapter) "." target-counter(attr(href), section);
}
.ref-page::after {
content: " (page " target-counter(attr(href), page) ")";
font-style: italic;
color: var(--print-secondary-color);
}
/* Appendix formatting */
.appendix {
page-break-before: always;
counter-reset: section 0;
counter-increment: appendix;
}
.appendix-title::before {
content: "Appendix " counter(appendix, upper-alpha) ": ";
}
/* Glossary and index */
.glossary-term {
font-weight: bold;
text-indent: -18pt;
margin-left: 18pt;
margin-bottom: 6pt;
}
.glossary-definition {
margin-bottom: 12pt;
margin-left: 18pt;
}
/* Bibliography and references */
.bibliography {
page-break-before: always;
}
.reference {
text-indent: -24pt;
margin-left: 24pt;
margin-bottom: 12pt;
counter-increment: reference;
}
.reference::before {
content: "[" counter(reference) "] ";
font-weight: bold;
}
/* Mathematical equations */
.equation {
text-align: center;
margin: 18pt 0;
counter-increment: equation;
}
.equation::after {
content: " (" counter(chapter) "." counter(equation) ")";
float: right;
margin-right: 12pt;
}
/* Warning and note boxes */
.warning-box {
border: 2pt solid #cc0000 !important;
background: #ffe6e6 !important;
padding: 12pt;
margin: 18pt 0;
page-break-inside: avoid;
}
.warning-box::before {
content: "⚠ WARNING: ";
font-weight: bold;
color: #cc0000;
}
.note-box {
border: 1pt solid var(--print-accent-color);
background: #f0f0f0 !important;
padding: 8pt;
margin: 12pt 0;
font-size: 10pt;
}
.note-box::before {
content: "Note: ";
font-weight: bold;
}
/* Signature lines */
.signature-line {
border-bottom: 1pt solid var(--print-border-color);
width: 3in;
height: 24pt;
margin: 24pt 0 6pt 0;
display: inline-block;
}
.signature-label {
font-size: 10pt;
color: var(--print-secondary-color);
}
/* Legal document formatting */
.legal-section {
margin-left: 0.5in;
text-indent: -0.5in;
}
.legal-subsection {
margin-left: 1in;
text-indent: -0.5in;
}
.legal-paragraph {
margin-left: 1.5in;
text-indent: -0.5in;
}
}
PDF Generation and Automation
Command-Line PDF Generation
Automated PDF generation from Markdown using various tools:
#!/bin/bash
# markdown-to-pdf-generator.sh - Professional PDF generation script
# Configuration variables
INPUT_DIR="./markdown-docs"
OUTPUT_DIR="./pdf-output"
STYLES_DIR="./print-styles"
TEMPLATE_DIR="./pdf-templates"
# Pandoc-based PDF generation
generate_pdf_pandoc() {
local input_file="$1"
local output_file="$2"
local template="${3:-default}"
echo "Generating PDF with Pandoc: $input_file -> $output_file"
pandoc "$input_file" \
--pdf-engine=xelatex \
--template="$TEMPLATE_DIR/${template}.latex" \
--css="$STYLES_DIR/print-styles.css" \
--metadata-file="$TEMPLATE_DIR/metadata.yaml" \
--table-of-contents \
--toc-depth=3 \
--number-sections \
--highlight-style=tango \
--variable geometry:margin=1in \
--variable fontsize:12pt \
--variable documentclass:article \
--variable papersize:letter \
--filter pandoc-crossref \
--filter pandoc-citeproc \
--output "$output_file"
}
# Puppeteer-based PDF generation
generate_pdf_puppeteer() {
local input_file="$1"
local output_file="$2"
echo "Generating PDF with Puppeteer: $input_file -> $output_file"
node -e "
const puppeteer = require('puppeteer');
const markdown = require('markdown-it')();
const fs = require('fs');
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
// Read and convert Markdown
const markdownContent = fs.readFileSync('$input_file', 'utf8');
const htmlContent = markdown.render(markdownContent);
// Read CSS styles
const printStyles = fs.readFileSync('$STYLES_DIR/comprehensive-print-styles.css', 'utf8');
const layoutStyles = fs.readFileSync('$STYLES_DIR/advanced-print-layout.css', 'utf8');
// Create complete HTML document
const fullHtml = \`
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Document</title>
<style>
\${printStyles}
\${layoutStyles}
@media screen {
body {
max-width: 8.5in;
margin: 0 auto;
padding: 1in;
background: white;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
}
</style>
</head>
<body class='typography-enhanced'>
\${htmlContent}
</body>
</html>\`;
await page.setContent(fullHtml, { waitUntil: 'networkidle2' });
await page.pdf({
path: '$output_file',
format: 'Letter',
margin: {
top: '1in',
right: '1in',
bottom: '1in',
left: '1in'
},
printBackground: true,
preferCSSPageSize: true,
displayHeaderFooter: true,
headerTemplate: \`
<div style='font-size: 10px; width: 100%; text-align: center; color: #666;'>
<span class='title'></span>
</div>\`,
footerTemplate: \`
<div style='font-size: 10px; width: 100%; text-align: center; color: #666;'>
Page <span class='pageNumber'></span> of <span class='totalPages'></span>
</div>\`
});
await browser.close();
console.log('PDF generated successfully');
})();
" 2>/dev/null
}
# WeasyPrint-based PDF generation
generate_pdf_weasyprint() {
local input_file="$1"
local output_file="$2"
echo "Generating PDF with WeasyPrint: $input_file -> $output_file"
# Convert Markdown to HTML first
local html_file="${input_file%.*}.html"
python3 -c "
import markdown
from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration
# Read Markdown file
with open('$input_file', 'r', encoding='utf-8') as f:
md_content = f.read()
# Convert to HTML
md = markdown.Markdown(extensions=[
'toc',
'tables',
'fenced_code',
'codehilite',
'attr_list'
])
html_content = md.convert(md_content)
# Create complete HTML document
full_html = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Document</title>
<link rel='stylesheet' href='$STYLES_DIR/comprehensive-print-styles.css'>
<link rel='stylesheet' href='$STYLES_DIR/advanced-print-layout.css'>
</head>
<body>
{html_content}
</body>
</html>
'''
# Generate PDF
font_config = FontConfiguration()
html_doc = HTML(string=full_html, base_url='$PWD/')
css_files = [
CSS('$STYLES_DIR/comprehensive-print-styles.css'),
CSS('$STYLES_DIR/advanced-print-layout.css')
]
html_doc.write_pdf(
'$output_file',
stylesheets=css_files,
font_config=font_config
)
print('PDF generated successfully with WeasyPrint')
"
}
# Prince XML-based PDF generation (commercial)
generate_pdf_prince() {
local input_file="$1"
local output_file="$2"
echo "Generating PDF with Prince XML: $input_file -> $output_file"
# Convert Markdown to HTML
local html_file="${input_file%.*}.html"
markdown "$input_file" > "$html_file"
# Generate PDF with Prince
prince \
--css="$STYLES_DIR/comprehensive-print-styles.css" \
--css="$STYLES_DIR/advanced-print-layout.css" \
--media=print \
--pdf-profile="PDF/A-1a" \
--pdf-title="Professional Document" \
--pdf-author="Document Generator" \
--pdf-keywords="Markdown, PDF, Professional" \
--pdf-creator="Prince XML" \
"$html_file" \
--output="$output_file"
# Clean up temporary HTML
rm "$html_file"
}
# Batch processing function
process_markdown_files() {
local method="${1:-pandoc}"
local template="${2:-default}"
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Process all Markdown files
find "$INPUT_DIR" -name "*.md" -type f | while read -r input_file; do
local filename=$(basename "$input_file" .md)
local output_file="$OUTPUT_DIR/${filename}.pdf"
case "$method" in
"pandoc")
generate_pdf_pandoc "$input_file" "$output_file" "$template"
;;
"puppeteer")
generate_pdf_puppeteer "$input_file" "$output_file"
;;
"weasyprint")
generate_pdf_weasyprint "$input_file" "$output_file"
;;
"prince")
generate_pdf_prince "$input_file" "$output_file"
;;
*)
echo "Unknown method: $method"
echo "Available methods: pandoc, puppeteer, weasyprint, prince"
return 1
;;
esac
# Validate PDF generation
if [ -f "$output_file" ]; then
local file_size=$(stat -f%z "$output_file" 2>/dev/null || stat -c%s "$output_file" 2>/dev/null)
if [ "$file_size" -gt 1024 ]; then
echo "✓ Successfully generated: $output_file ($file_size bytes)"
else
echo "✗ Failed to generate valid PDF: $output_file"
fi
else
echo "✗ PDF generation failed: $output_file"
fi
done
}
# Quality assurance checks
validate_pdf_output() {
local pdf_file="$1"
echo "Validating PDF: $pdf_file"
# Check file exists and has content
if [ ! -f "$pdf_file" ]; then
echo "✗ PDF file does not exist"
return 1
fi
local file_size=$(stat -f%z "$pdf_file" 2>/dev/null || stat -c%s "$pdf_file" 2>/dev/null)
if [ "$file_size" -lt 1024 ]; then
echo "✗ PDF file too small ($file_size bytes)"
return 1
fi
# Validate PDF structure
if command -v pdfinfo >/dev/null 2>&1; then
local page_count=$(pdfinfo "$pdf_file" 2>/dev/null | grep "Pages:" | awk '{print $2}')
if [ "$page_count" -gt 0 ]; then
echo "✓ PDF contains $page_count pages"
else
echo "✗ PDF appears to be corrupted"
return 1
fi
fi
# Check for text content
if command -v pdftotext >/dev/null 2>&1; then
local text_length=$(pdftotext "$pdf_file" - 2>/dev/null | wc -c)
if [ "$text_length" -gt 100 ]; then
echo "✓ PDF contains text content ($text_length characters)"
else
echo "⚠ PDF may not contain extractable text"
fi
fi
echo "✓ PDF validation completed"
}
# Main execution
main() {
local method="${1:-pandoc}"
local template="${2:-default}"
local validate="${3:-false}"
echo "=== Markdown to PDF Generator ==="
echo "Method: $method"
echo "Template: $template"
echo "Input Directory: $INPUT_DIR"
echo "Output Directory: $OUTPUT_DIR"
echo "=================================="
# Check dependencies
case "$method" in
"pandoc")
if ! command -v pandoc >/dev/null 2>&1; then
echo "Error: Pandoc is not installed"
exit 1
fi
;;
"puppeteer")
if ! command -v node >/dev/null 2>&1; then
echo "Error: Node.js is not installed"
exit 1
fi
;;
"weasyprint")
if ! command -v python3 >/dev/null 2>&1; then
echo "Error: Python 3 is not installed"
exit 1
fi
;;
"prince")
if ! command -v prince >/dev/null 2>&1; then
echo "Error: Prince XML is not installed"
exit 1
fi
;;
esac
# Process files
process_markdown_files "$method" "$template"
# Validate output if requested
if [ "$validate" = "true" ]; then
echo "=== Validating Generated PDFs ==="
find "$OUTPUT_DIR" -name "*.pdf" -type f | while read -r pdf_file; do
validate_pdf_output "$pdf_file"
done
fi
echo "=== Generation Complete ==="
}
# Execute if script is run directly
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
main "$@"
fi
GitHub Actions Workflow for PDF Generation
Automated PDF generation in CI/CD pipelines:
# .github/workflows/generate-pdfs.yml
name: Generate PDF Documentation
on:
push:
branches: [main]
paths: ['docs/**/*.md']
pull_request:
branches: [main]
paths: ['docs/**/*.md']
workflow_dispatch:
inputs:
pdf_method:
description: 'PDF generation method'
required: true
default: 'puppeteer'
type: choice
options:
- 'puppeteer'
- 'pandoc'
- 'weasyprint'
jobs:
generate-pdfs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install Node.js dependencies
run: |
npm install -g puppeteer markdown-it
npm install puppeteer markdown-it
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
- name: Install Python dependencies
run: |
pip install weasyprint markdown beautifulsoup4
- name: Install Pandoc
uses: r-lib/actions/setup-pandoc@v2
with:
pandoc-version: 'latest'
- name: Install LaTeX for Pandoc
run: |
sudo apt-get update
sudo apt-get install -y texlive-latex-recommended \
texlive-latex-extra \
texlive-fonts-recommended \
texlive-xetex
- name: Create output directories
run: |
mkdir -p pdf-output
mkdir -p pdf-reports
- name: Generate PDFs with Puppeteer
if: github.event.inputs.pdf_method == 'puppeteer' || github.event.inputs.pdf_method == ''
run: |
node generate-pdfs.js
- name: Generate PDFs with Pandoc
if: github.event.inputs.pdf_method == 'pandoc'
run: |
for file in docs/**/*.md; do
filename=$(basename "$file" .md)
pandoc "$file" \
--pdf-engine=xelatex \
--css=styles/print-styles.css \
--table-of-contents \
--number-sections \
--variable geometry:margin=1in \
--output "pdf-output/${filename}.pdf"
done
- name: Generate PDFs with WeasyPrint
if: github.event.inputs.pdf_method == 'weasyprint'
run: |
python generate-pdfs-weasyprint.py
- name: Validate PDF files
run: |
echo "=== PDF Generation Report ===" > pdf-reports/generation-report.txt
echo "Generated at: $(date)" >> pdf-reports/generation-report.txt
echo "Method: ${{ github.event.inputs.pdf_method || 'puppeteer' }}" >> pdf-reports/generation-report.txt
echo "" >> pdf-reports/generation-report.txt
total_files=0
successful_files=0
for pdf in pdf-output/*.pdf; do
if [ -f "$pdf" ]; then
total_files=$((total_files + 1))
file_size=$(stat -c%s "$pdf")
if [ "$file_size" -gt 1024 ]; then
successful_files=$((successful_files + 1))
echo "✓ $(basename "$pdf"): $file_size bytes" >> pdf-reports/generation-report.txt
else
echo "✗ $(basename "$pdf"): Failed ($file_size bytes)" >> pdf-reports/generation-report.txt
fi
fi
done
echo "" >> pdf-reports/generation-report.txt
echo "Summary: $successful_files/$total_files files generated successfully" >> pdf-reports/generation-report.txt
# Fail if no PDFs were generated successfully
if [ "$successful_files" -eq 0 ]; then
echo "Error: No PDFs were generated successfully"
exit 1
fi
- name: Upload PDF artifacts
uses: actions/upload-artifact@v3
with:
name: generated-pdfs-${{ github.event.inputs.pdf_method || 'puppeteer' }}
path: pdf-output/
retention-days: 30
- name: Upload generation report
uses: actions/upload-artifact@v3
with:
name: pdf-generation-report
path: pdf-reports/
retention-days: 7
- name: Comment on PR with results
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('pdf-reports/generation-report.txt', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## PDF Generation Report
\`\`\`
${report}
\`\`\`
Download the generated PDFs from the workflow artifacts.`
});
Platform-Specific Implementation
Jekyll PDF Generation Plugin
Jekyll plugin for automated PDF generation:
# _plugins/pdf_generator.rb
require 'puppeteer-ruby'
require 'kramdown'
require 'liquid'
module Jekyll
class PDFGenerator < Generator
safe true
priority :low
def generate(site)
@site = site
@config = site.config['pdf_generator'] || {}
# Skip if PDF generation is disabled
return unless @config['enabled']
# Create PDF output directory
pdf_dir = File.join(site.dest, 'pdfs')
FileUtils.mkdir_p(pdf_dir)
# Generate PDFs for specified collections
collections_to_process = @config['collections'] || ['posts']
collections_to_process.each do |collection_name|
collection = site.collections[collection_name]
next unless collection
collection.docs.each do |doc|
generate_pdf_for_document(doc, pdf_dir)
end
end
# Generate combined PDFs if configured
generate_combined_pdfs(pdf_dir) if @config['combined_pdfs']
end
private
def generate_pdf_for_document(doc, output_dir)
return unless should_generate_pdf?(doc)
begin
Jekyll.logger.info "PDF Generator:", "Processing #{doc.relative_path}"
# Prepare HTML content
html_content = prepare_html_content(doc)
# Generate PDF filename
pdf_filename = "#{doc.basename_without_ext}.pdf"
pdf_path = File.join(output_dir, pdf_filename)
# Generate PDF using Puppeteer
generate_pdf_with_puppeteer(html_content, pdf_path, doc)
Jekyll.logger.info "PDF Generator:", "Generated #{pdf_filename}"
rescue StandardError => e
Jekyll.logger.error "PDF Generator:", "Failed to generate PDF for #{doc.relative_path}: #{e.message}"
end
end
def should_generate_pdf?(doc)
# Check if document should be converted to PDF
return false if doc.data['pdf'] == false
return true if doc.data['pdf'] == true
# Default behavior based on configuration
@config['auto_generate'] != false
end
def prepare_html_content(doc)
# Get the rendered content
content = doc.content
# Apply PDF-specific transformations
content = transform_content_for_pdf(content, doc)
# Create complete HTML document
template = get_pdf_template
# Prepare template variables
template_vars = {
'title' => doc.data['title'] || doc.basename_without_ext,
'author' => doc.data['author'] || @site.config['author'],
'date' => doc.data['date']&.strftime('%B %d, %Y') || '',
'description' => doc.data['description'] || '',
'content' => content,
'print_styles' => get_print_styles,
'site' => @site.config
}
# Render template
liquid_template = Liquid::Template.parse(template)
liquid_template.render(template_vars)
end
def transform_content_for_pdf(content, doc)
# Convert relative URLs to absolute
base_url = @site.config['url'] || ''
content = content.gsub(/href="\//, "href=\"#{base_url}/")
content = content.gsub(/src="\//, "src=\"#{base_url}/")
# Add print-specific classes
content = content.gsub(/<h([1-6])/, '<h\1 class="pdf-heading"')
content = content.gsub(/<table/, '<table class="pdf-table"')
content = content.gsub(/<pre/, '<pre class="pdf-code"')
# Handle page breaks
content = content.gsub(/<!-- page-break -->/, '<div class="page-break-before"></div>')
content
end
def get_pdf_template
template_path = @config['template'] || File.join(@site.source, '_layouts', 'pdf.html')
if File.exist?(template_path)
File.read(template_path)
else
default_pdf_template
end
end
def default_pdf_template
<<~HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<meta name="author" content="{{ author }}">
<meta name="description" content="{{ description }}">
<style>
{{ print_styles }}
</style>
</head>
<body>
<header class="pdf-header">
<h1>{{ title }}</h1>
{% if author %}
<p class="author">By {{ author }}</p>
{% endif %}
{% if date %}
<p class="date">{{ date }}</p>
{% endif %}
</header>
<main class="pdf-content">
{{ content }}
</main>
<footer class="pdf-footer">
<p>Generated from {{ site.title | default: "Jekyll Site" }}</p>
</footer>
</body>
</html>
HTML
end
def get_print_styles
styles_path = File.join(@site.source, 'assets', 'css', 'print.css')
if File.exist?(styles_path)
File.read(styles_path)
else
# Return basic print styles
basic_print_styles
end
end
def basic_print_styles
<<~CSS
@media print {
body {
font-family: Georgia, serif;
font-size: 12pt;
line-height: 1.4;
color: #000;
background: white;
}
.pdf-header {
text-align: center;
margin-bottom: 2em;
page-break-after: avoid;
}
.pdf-header h1 {
font-size: 18pt;
margin-bottom: 0.5em;
}
.author, .date {
font-size: 10pt;
color: #666;
}
.pdf-heading {
page-break-after: avoid;
margin-top: 1.5em;
}
.pdf-table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
.pdf-table th, .pdf-table td {
border: 1px solid #000;
padding: 6pt;
}
.pdf-code {
font-family: Courier, monospace;
font-size: 10pt;
background: #f5f5f5;
border: 1px solid #ccc;
padding: 8pt;
page-break-inside: avoid;
}
.page-break-before {
page-break-before: always;
}
.pdf-footer {
margin-top: 2em;
padding-top: 1em;
border-top: 1px solid #ccc;
font-size: 10pt;
text-align: center;
color: #666;
}
}
CSS
end
def generate_pdf_with_puppeteer(html_content, pdf_path, doc)
# PDF generation options from configuration
pdf_options = @config['pdf_options'] || {}
default_options = {
format: 'Letter',
margin: {
top: '1in',
right: '1in',
bottom: '1in',
left: '1in'
},
print_background: true,
prefer_css_page_size: true
}
options = default_options.merge(pdf_options)
# Use Puppeteer to generate PDF
Puppeteer.launch(headless: true) do |browser|
page = browser.new_page
page.set_content(html_content, wait_until: 'networkidle2')
page.pdf(path: pdf_path, **options)
end
end
def generate_combined_pdfs(output_dir)
combined_configs = @config['combined_pdfs']
combined_configs.each do |config|
next unless config['name'] && config['collections']
Jekyll.logger.info "PDF Generator:", "Generating combined PDF: #{config['name']}"
# Collect documents
documents = []
config['collections'].each do |collection_name|
collection = @site.collections[collection_name]
next unless collection
collection_docs = collection.docs.select { |doc| should_generate_pdf?(doc) }
collection_docs = collection_docs.sort_by { |doc| doc.data['date'] || Time.now } if config['sort_by_date']
documents.concat(collection_docs)
end
# Generate combined HTML
combined_content = generate_combined_content(documents, config)
# Generate combined PDF
pdf_filename = "#{config['name']}.pdf"
pdf_path = File.join(output_dir, pdf_filename)
generate_pdf_with_puppeteer(combined_content, pdf_path, nil)
Jekyll.logger.info "PDF Generator:", "Generated combined PDF: #{pdf_filename}"
end
end
def generate_combined_content(documents, config)
content_parts = []
# Add title page if configured
if config['title_page']
content_parts << generate_title_page(config['title_page'])
end
# Add table of contents if configured
if config['table_of_contents']
content_parts << generate_table_of_contents(documents)
end
# Add documents
documents.each_with_index do |doc, index|
# Add page break between documents
content_parts << '<div class="page-break-before"></div>' if index > 0
# Add document content
doc_content = prepare_html_content(doc)
content_parts << extract_body_content(doc_content)
end
# Wrap in complete HTML document
template_vars = {
'title' => config['title'] || config['name'],
'author' => config['author'] || @site.config['author'],
'date' => Time.now.strftime('%B %d, %Y'),
'content' => content_parts.join("\n"),
'print_styles' => get_print_styles,
'site' => @site.config
}
liquid_template = Liquid::Template.parse(get_pdf_template)
liquid_template.render(template_vars)
end
def generate_title_page(title_config)
<<~HTML
<div class="title-page">
<h1>#{title_config['title']}</h1>
#{title_config['subtitle'] ? "<h2>#{title_config['subtitle']}</h2>" : ''}
#{title_config['author'] ? "<p class='author'>#{title_config['author']}</p>" : ''}
#{title_config['date'] ? "<p class='date'>#{title_config['date']}</p>" : ''}
</div>
<div class="page-break-after"></div>
HTML
end
def generate_table_of_contents(documents)
toc_entries = documents.map.with_index do |doc, index|
title = doc.data['title'] || doc.basename_without_ext
"<div class='toc-entry'><span class='toc-title'>#{title}</span><span class='toc-page'>#{index + 1}</span></div>"
end.join("\n")
<<~HTML
<div class="table-of-contents">
<h2>Table of Contents</h2>
#{toc_entries}
</div>
<div class="page-break-after"></div>
HTML
end
def extract_body_content(html)
# Extract content from <main> or <body> tags
match = html.match(/<main[^>]*>(.*?)<\/main>/m) || html.match(/<body[^>]*>(.*?)<\/body>/m)
match ? match[1] : html
end
end
end
Hugo PDF Shortcodes
Hugo shortcodes for PDF-optimized content:
<!-- layouts/shortcodes/pdf-chapter.html -->
{{ $title := .Get "title" | default .Page.Title }}
{{ $number := .Get "number" }}
<div class="chapter" data-chapter-title="{{ $title }}">
{{ if $number }}
<h1 class="chapter-title" data-chapter-number="{{ $number }}">{{ $title }}</h1>
{{ else }}
<h1 class="chapter-title">{{ $title }}</h1>
{{ end }}
{{ if .Get "summary" }}
<div class="chapter-summary">{{ .Get "summary" | markdownify }}</div>
{{ end }}
<div class="chapter-content">
{{ .Inner | markdownify }}
</div>
</div>
<!-- layouts/shortcodes/pdf-table.html -->
{{ $caption := .Get "caption" }}
{{ $class := .Get "class" | default "data-table" }}
<div class="table-container">
{{ if $caption }}
<div class="table-caption">{{ $caption }}</div>
{{ end }}
<table class="{{ $class }}">
{{ .Inner }}
</table>
</div>
<!-- layouts/shortcodes/pdf-figure.html -->
{{ $src := .Get "src" }}
{{ $caption := .Get "caption" }}
{{ $alt := .Get "alt" | default $caption }}
<figure class="figure">
<img src="{{ $src | relURL }}" alt="{{ $alt }}" class="figure-image">
{{ if $caption }}
<figcaption class="figure-caption">{{ $caption | markdownify }}</figcaption>
{{ end }}
</figure>
<!-- layouts/shortcodes/pdf-code.html -->
{{ $language := .Get "lang" | default "text" }}
{{ $title := .Get "title" }}
{{ $file := .Get "file" }}
<div class="code-listing">
{{ if $title }}
<div class="code-listing-title">{{ $title }}</div>
{{ end }}
<pre class="code-listing-content" data-language="{{ $language }}"><code class="language-{{ $language }}">{{- if $file -}}{{ readFile $file }}{{- else -}}{{ .Inner }}{{- end -}}</code></pre>
</div>
<!-- layouts/shortcodes/pdf-pagebreak.html -->
<div class="page-break-before"></div>
<!-- layouts/shortcodes/pdf-note.html -->
{{ $type := .Get "type" | default "note" }}
{{ $title := .Get "title" }}
<div class="{{ $type }}-box">
{{ if $title }}
<div class="note-title">{{ $title }}</div>
{{ end }}
{{ .Inner | markdownify }}
</div>
Hugo Content Usage:
{{< pdf-chapter title="Introduction" number="1" summary="Overview of the system architecture and design principles." >}}
This chapter provides a comprehensive introduction to the system design.
## System Overview
The architecture follows a microservices pattern with the following components:
{{< pdf-table caption="System Components Overview" >}}
| Component | Purpose | Technology |
|-----------|---------|------------|
| API Gateway | Request routing | Node.js |
| User Service | Authentication | Python |
| Data Service | Storage | PostgreSQL |
{{< /pdf-table >}}
{{< pdf-figure src="/images/architecture-diagram.png" caption="System Architecture Diagram showing the relationship between components" >}}
{{< pdf-code lang="javascript" title="API Gateway Configuration" >}}
const express = require('express');
const app = express();
app.use('/api/users', userRoutes);
app.use('/api/data', dataRoutes);
app.listen(3000, () => {
console.log('API Gateway running on port 3000');
});
{{< /pdf-code >}}
{{< pdf-note type="warning" title="Security Consideration" >}}
Always validate and sanitize input data before processing to prevent security vulnerabilities.
{{< /pdf-note >}}
{{< /pdf-chapter >}}
{{< pdf-pagebreak >}}
{{< pdf-chapter title="Implementation Details" number="2" >}}
This chapter covers the detailed implementation of each component.
{{< /pdf-chapter >}}
Integration with Modern Workflows
Print styles and PDF formatting integrate seamlessly with comprehensive Markdown workflows. When combined with document versioning and change tracking, professional PDF output ensures version-controlled document distribution that maintains formatting consistency across revisions while enabling collaborative development of complex publications.
For comprehensive documentation systems requiring both digital and print delivery, PDF formatting works effectively with custom CSS classes and styling to create design systems that translate seamlessly from web presentation to professional print output while maintaining brand identity and visual hierarchy.
When managing enterprise documentation workflows, print styles complement blockquote advanced styling to create professional publications that effectively communicate complex information through both textual content and sophisticated visual presentation that works across digital screens and physical print media.
Troubleshooting Common PDF Issues
Font and Typography Problems
Problem: Fonts not rendering correctly in PDF output or missing special characters
Solutions:
/* Font fallback and embedding strategies */
@media print {
/* Specify complete font stacks */
body {
font-family: Georgia, 'Times New Roman', Times, serif;
}
code, pre {
font-family: 'Courier New', Courier, 'Lucida Console', monospace;
}
/* Ensure Unicode support */
* {
font-variant-numeric: normal;
font-feature-settings: normal;
}
/* Fix for missing characters */
.unicode-fix {
font-family: 'Arial Unicode MS', Arial, sans-serif;
}
/* Web font fallbacks */
@font-face {
font-family: 'CustomFont';
src: local('Georgia'), local('Times New Roman');
font-display: block;
}
}
PDF Generation Configuration:
// Puppeteer font handling
const pdfOptions = {
format: 'Letter',
margin: { top: '1in', right: '1in', bottom: '1in', left: '1in' },
printBackground: true,
preferCSSPageSize: true,
// Font loading settings
waitUntil: ['networkidle2', 'load'],
timeout: 60000,
// Ensure fonts are loaded
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-font-subpixel-positioning',
'--enable-font-antialiasing'
]
};
Page Break and Layout Issues
Problem: Content breaking awkwardly across pages or overlapping page margins
Solutions:
@media print {
/* Better page break control */
.keep-together {
page-break-inside: avoid;
break-inside: avoid;
}
.force-page-break {
page-break-before: always;
break-before: page;
}
.avoid-page-break-after {
page-break-after: avoid;
break-after: avoid;
}
/* Fix for widows and orphans */
p, li, dd {
orphans: 3;
widows: 3;
}
/* Heading page break control */
h1, h2, h3, h4 {
page-break-after: avoid;
break-after: avoid;
page-break-inside: avoid;
break-inside: avoid;
}
/* List handling */
ul, ol {
page-break-inside: avoid;
break-inside: avoid;
}
/* Table handling */
table {
page-break-inside: avoid;
break-inside: avoid;
}
thead {
display: table-header-group;
}
tr {
page-break-inside: avoid;
break-inside: avoid;
}
/* Image handling */
img {
page-break-inside: avoid;
break-inside: avoid;
page-break-after: avoid;
break-after: avoid;
max-width: 100%;
height: auto;
}
/* Code block handling */
pre {
page-break-inside: avoid;
break-inside: avoid;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
}
Color and Background Issues
Problem: Colors or backgrounds not appearing in PDF output
Solutions:
@media print {
/* Force color and background printing */
* {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* Ensure backgrounds are printed */
.highlight-box {
background: #f0f8ff !important;
-webkit-print-color-adjust: exact;
}
/* Convert colors to print-friendly versions */
.screen-blue {
color: #003366 !important; /* Darker for print */
background: #e6f3ff !important; /* Lighter for print */
}
/* High contrast alternatives */
@media print and (prefers-contrast: high) {
.color-element {
background: white !important;
color: black !important;
border: 2px solid black !important;
}
}
}
Puppeteer Configuration:
const page = await browser.newPage();
// Enable background graphics
await page.emulateMediaType('print');
await page.evaluateOnNewDocument(() => {
document.documentElement.style.setProperty('-webkit-print-color-adjust', 'exact');
});
await page.pdf({
path: outputPath,
format: 'Letter',
printBackground: true, // Critical for backgrounds
preferCSSPageSize: true,
margin: { top: '1in', right: '1in', bottom: '1in', left: '1in' }
});
Conclusion
Print styles and PDF formatting for Markdown documents transform digital content into professional publications that maintain design excellence, typography precision, and layout consistency across different output formats and distribution channels. By mastering CSS print media queries, automated PDF generation techniques, and platform-specific implementations, content creators can produce high-quality documents that serve both digital and physical publication requirements.
The key to successful print formatting lies in understanding the fundamental differences between screen and print media, implementing robust CSS that handles page breaks gracefully, and choosing appropriate PDF generation tools that meet specific project requirements. Whether you're creating technical documentation, academic papers, or business reports, the techniques covered in this guide provide the foundation for professional document output that maintains quality across all distribution formats.
Remember to test your print styles across different PDF generators and browsers, validate output quality with various paper sizes and printer settings, and implement automated workflows that ensure consistent results across large document sets. With proper print style implementation, your Markdown content becomes a powerful platform for professional publication that bridges the gap between digital creation and physical distribution while maintaining the simplicity and portability that makes Markdown ideal for modern content workflows.