.claude/skills/export-to-pdf/SKILL.md
Export a markdown file to PDF using Jinja templates and WeasyPrint. Renders markdown through the template system and converts to PDF. Supports metadata like title, author, client, date via YAML frontmatter or arguments. Triggers: "export to pdf", "render pdf", "convert to pdf", "make pdf", "export document as pdf", "pdf export", "generate pdf".
npx skillsauth add johncarpenter/knowledge-base export-to-pdfInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Export markdown files to professionally styled PDF documents using Jinja2 templates and WeasyPrint.
/export-to-pdf <markdown-file> [template] [options]
| Argument | Required | Default | Description |
|----------|----------|---------|-------------|
| markdown-file | Yes | - | Path to the source markdown file |
| template | No | 2lines-external | Template directory name |
| Option | Description |
|--------|-------------|
| --title | Document title (overrides frontmatter) |
| --subtitle | Document subtitle or one-line description |
| --author | Author name (default: "John Carpenter, 2Lines Software") |
| --client | Client name (for external docs) |
| --date | Document date (default: today, e.g., "February 2026") |
| --version | Document version (default: "1.0") |
| --classification | Classification level (default: "Confidential") |
| --output | Output PDF file path (default: same name with .pdf) |
/export-to-pdf clients/Circuit/proposal.md
/export-to-pdf research/technical-spike.md --title "Technical Analysis" --client "Acme Corp"
/export-to-pdf pipeline/rfp-response.md 2lines-external --output exports/proposal.pdf
Unlike export-to-html which creates a folder, export-to-pdf outputs a single PDF file:
clients/Circuit/proposal.md → clients/Circuit/proposal.pdf
Requires WeasyPrint for PDF rendering:
pip install weasyprint
On macOS, you may also need:
brew install pango
| Template | Directory | Purpose |
|----------|-----------|---------|
| 2lines-external | _templates/2lines-external/ | External documents (proposals, reports, deliverables) |
The PDF export follows the same workflow as HTML export, with an additional PDF rendering step:
Parse arguments, read markdown, parse frontmatter, convert to HTML, build context.
from jinja2 import Environment, FileSystemLoader
templates_dir = Path("/Users/john/Documents/Workspace/2Lines/knowledge-base/_templates")
template_path = templates_dir / template_name
env = Environment(loader=FileSystemLoader(str(template_path)))
template = env.get_template('base.html')
html_string = template.render(**context)
from weasyprint import HTML, CSS
# Create HTML document with base_url for resolving relative paths (logo, css)
html_doc = HTML(string=html_string, base_url=str(template_path))
# Render to PDF
html_doc.write_pdf(output_path)
#!/usr/bin/env python3
"""Render markdown to PDF using templates and WeasyPrint."""
import re
from pathlib import Path
from datetime import date
import markdown
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML
BASE_DIR = Path("/Users/john/Documents/Workspace/2Lines/knowledge-base")
TEMPLATES_DIR = BASE_DIR / "_templates"
def export_to_pdf(
markdown_file: str,
template_name: str = "2lines-external",
output_path: str = None,
**options
) -> Path:
"""
Export a markdown file to PDF.
Args:
markdown_file: Path to markdown file (relative to BASE_DIR or absolute)
template_name: Template directory name
output_path: Output PDF file path (optional)
**options: Metadata options (title, author, client, etc.)
Returns:
Path to the generated PDF file
"""
# Resolve markdown path
md_path = Path(markdown_file)
if not md_path.is_absolute():
md_path = BASE_DIR / md_path
if not md_path.exists():
raise FileNotFoundError(f"Markdown file not found: {md_path}")
# Read markdown content
md_content = md_path.read_text()
# Parse YAML frontmatter if present
frontmatter = {}
content = md_content
if md_content.startswith('---'):
match = re.match(r'^---\n(.*?)\n---\n', md_content, re.DOTALL)
if match:
try:
import yaml
frontmatter = yaml.safe_load(match.group(1)) or {}
except:
pass
content = md_content[match.end():]
# Convert markdown to HTML
md = markdown.Markdown(extensions=[
'tables',
'fenced_code',
'codehilite',
'toc',
])
html_content = md.convert(content)
# Extract table of contents
toc_html = md.toc
# Build context (frontmatter, then options override)
context = {
**frontmatter,
**{k: v for k, v in options.items() if v is not None},
'content': html_content,
'toc': toc_html,
}
# Set defaults
if 'title' not in context:
context['title'] = md_path.stem.replace('-', ' ').title()
if 'date' not in context:
context['date'] = date.today().isoformat()
# Load and render template
template_path = TEMPLATES_DIR / template_name
if not template_path.exists():
raise FileNotFoundError(f"Template not found: {template_path}")
env = Environment(loader=FileSystemLoader(str(template_path)))
template = env.get_template('base.html')
html_string = template.render(**context)
# Determine output path
if output_path:
out_path = Path(output_path)
if not out_path.is_absolute():
out_path = BASE_DIR / out_path
else:
out_path = md_path.with_suffix('.pdf')
out_path.parent.mkdir(parents=True, exist_ok=True)
# Convert to PDF using WeasyPrint
# base_url allows WeasyPrint to resolve relative paths (logo.svg, style.css)
html_doc = HTML(string=html_string, base_url=str(template_path) + '/')
html_doc.write_pdf(out_path)
return out_path
When the user invokes /export-to-pdf, execute:
markdown, jinja2, and weasyprint are installed| Error | Response |
|-------|----------|
| File not found | Report error with suggestions for correct path |
| Template not found | List available templates |
| Missing dependencies | Show pip install markdown jinja2 weasyprint |
| WeasyPrint error | Show error message (often related to missing system fonts) |
After successful export:
Export Complete
Source: clients/Circuit/proposal.md
Template: 2lines-external
Output: clients/Circuit/proposal.pdf
Metadata:
Title: Project Proposal
Client: Circuit
Date: 2026-02-12
Classification: Confidential
Open PDF: open clients/Circuit/proposal.pdf
@media print and @page rules optimized for PDF--- in markdown or add <div class="page-break"></div> for manual breaksmacOS:
brew install pango libffi
pip install weasyprint
Ubuntu/Debian:
sudo apt-get install libpango-1.0-0 libpangocairo-1.0-0
pip install weasyprint
If fonts don't render correctly:
For very large documents, WeasyPrint may be slow. Consider:
--optimize-images flag (if supported)tools
Generate FinOps cost comparison report using cloud-doctor MCP for Suncorp Azure subscriptions
tools
Query, retrieve, and save Granola meeting notes and summaries locally using the proofgeist Granola MCP server (local cache-based). Use this skill whenever the user wants to: search or find past meetings, get meeting summaries or transcripts, copy meeting notes to local markdown files, review what was discussed in a meeting, look up action items or decisions from meetings, export meeting data, or analyze meeting patterns. Triggers: "meeting notes", "meeting summary", "what was discussed", "find the meeting", "copy meeting notes", "export meeting", "meeting transcript", "action items from meeting", "last meeting", "recent meetings", "meeting with [person]", "granola".
tools
Backup today's Granola meeting notes to local markdown files, organized by client folder. Uses Granola folder metadata to route meetings to appropriate client directories. Run this as part of an evening workflow to archive meeting notes, summaries, and action items. Triggers: "backup meetings", "save today's meetings", "archive meetings", "evening backup", "download meeting notes", "sync meetings", "meeting backup".
tools
Search, retrieve, and index local markdown knowledgebase files using the QMD CLI. Use this skill whenever the user wants to: search a local knowledgebase of markdown files, retrieve specific documents or passages from indexed collections, index or re-index a directory of .md files, check the status of the knowledge index, or perform any knowledge retrieval task against local documentation. Triggers: "search the knowledgebase", "find in docs", "index my files", "look up", "search notes", "retrieve document", "knowledge search", "what does the docs say about", "find information about", "re-index", "update the index".