PDF Templates - Visual Document Designer
Design custom PDF layouts for reports, prescriptions, and consent forms without code.
What this enables
Branded documents: Create professional PDFs with your clinic's logo, colors, and branding—not generic templates.
Form → PDF conversion: Auto-generate consultation reports from form data—patient answers flow directly into your letterhead.
Reusable components: Build once, use everywhere—letterhead, signature blocks, disclaimers that appear on all documents.
Non-technical design: Drag blocks, insert form fields, preview in real-time—no HTML/CSS required (but available if needed).
Multi-document types: Different layouts for reports, prescriptions, invoices, consent forms—all with shared branding.
How it works
- Admin opens template designer: Creates "Consultation Report" template
- Drag blocks: Letterhead, patient name block, findings section, signature line
- Insert form fields: "Add patient city" → drag in form field → preview shows real data
- Preview: See exactly how the PDF will look (live refresh as you edit)
- Generate PDF: When form is completed, system auto-generates beautiful PDF with form answers
- Download/email: Patient gets PDF report emailed automatically
Technical Reference
Overview
PDF Templates enable organizations to create custom visual layouts for their documents (reports, prescriptions, disclaimers). This feature provides a minimal block-based editor for designing professional PDFs without writing HTML/CSS, while still allowing advanced users to customize the underlying template code.
Core Concept
PDF templates are visual layouts that transform form data into branded, professional documents. They bridge the gap between:
- Forms (structured data collection)
- Documents (PDF outputs for patients/specialists)
Form Data (JSONB) + PDF Template (HTML/CSS) = Professional PDF
{"field_10": "Amsterdam"} + {{.FormValues.field_10}} = City: AmsterdamWhy This Design?
| Problem | Solution |
|---|---|
| "Every org needs different PDF layouts" | Per-organization template customization |
| "Non-technical users can't write HTML" | Block-based visual editor with drag-drop |
| "Need to insert form field values dynamically" | Go template syntax with autocomplete |
| "Header/footer must be consistent" | Reusable components (letterhead, signature blocks) |
| "Multiple document types share layout" | Template inheritance (base template + overrides) |
| "Preview must match final PDF" | Live preview using same chromedp renderer |
Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ PDF Template Feature │
└─────────────────────────────────────────────────────────┘
1. Template Designer (Admin UI)
├─ Block-based editor (drag sections, text, tables, images)
├─ Field picker (insert form field placeholders)
├─ Component library (letterhead, signature, disclaimers)
├─ Live preview (real-time render with sample data)
└─ HTML/CSS code view (for advanced customization)
2. Template Storage (Backend)
├─ pdf_templates table (HTML, CSS, metadata)
├─ pdf_template_versions (versioning like custom fields)
├─ pdf_template_components (reusable blocks)
└─ Organization-scoped (multi-tenant)
3. Rendering Engine (chromedp)
├─ Merge template + form data → HTML
├─ Apply CSS styling
├─ Convert HTML → PDF (headless Chrome)
└─ Cache signed documents to S3Template Types
| Type | Document Type | Use Case |
|---|---|---|
report | Consultation Report | Patient visit summaries, specialist observations |
prescription | Prescription | Medication orders, treatment plans |
disclaimer | Consent/Legal | GDPR consent, HIPAA notice, procedure disclaimers |
certificate | Certificates | Completion certificates, medical clearance |
invoice | Billing | Invoice for services rendered |
Template Structure
Database Schema
CREATE TABLE pdf_templates (
id BIGSERIAL PRIMARY KEY,
organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
-- Metadata
name TEXT NOT NULL,
description TEXT,
template_type TEXT NOT NULL, -- 'report', 'prescription', 'disclaimer', 'certificate', 'invoice'
-- Template content
template_html TEXT NOT NULL, -- HTML template with Go template syntax
template_css TEXT, -- Custom CSS styling
-- Layout config (JSONB)
layout_config JSONB NOT NULL DEFAULT '{}',
-- {
-- "pageSize": "A4",
-- "orientation": "portrait",
-- "margins": {"top": 20, "right": 20, "bottom": 20, "left": 20},
-- "header": true,
-- "footer": true
-- }
-- Versioning
version INT NOT NULL DEFAULT 1,
published BOOLEAN NOT NULL DEFAULT FALSE,
-- Component references (reusable blocks)
components_used JSONB, -- ["letterhead", "signature_block", "footer"]
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (organization_id, name)
);
CREATE TABLE pdf_template_versions (
id BIGSERIAL PRIMARY KEY,
template_id BIGINT NOT NULL REFERENCES pdf_templates(id) ON DELETE CASCADE,
organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
version INT NOT NULL,
template_html TEXT NOT NULL,
template_css TEXT,
layout_config JSONB NOT NULL,
changed_by BIGINT REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (template_id, version)
);
CREATE TABLE pdf_template_components (
id BIGSERIAL PRIMARY KEY,
organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
component_html TEXT NOT NULL,
component_css TEXT,
-- Component category
category TEXT NOT NULL, -- 'header', 'footer', 'signature', 'letterhead', 'table', 'section'
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (organization_id, name)
);Template Variables (Go Template Syntax)
Templates use Go's text/template package for dynamic content:
Available Variables
type TemplateData struct {
// Document metadata
DocumentID int64
DocumentType string
GeneratedAt time.Time
// Organization
Organization Organization
// {
// "name": "RestartiX Clinic Bucharest",
// "address": "Strada Victoriei 123",
// "phone": "+40 21 123 4567",
// "logo_url": "https://s3.../logo.png"
// }
// Patient
Patient Patient
// {
// "name": "Ion Popescu",
// "email": "[email protected]",
// "phone": "+40 700 123 456",
// "birthdate": "1985-05-15"
// }
// Specialist
Specialist Specialist
// {
// "name": "Dr. Maria Ionescu",
// "title": "MD, Physiotherapist",
// "license_number": "12345",
// "signature_url": "https://s3.../signature.png"
// }
// Appointment
Appointment Appointment
// {
// "started_at": "2025-02-15T10:00:00Z",
// "ended_at": "2025-02-15T11:00:00Z",
// "service_name": "Spine Consultation"
// }
// Form data (from signed form)
FormValues map[string]interface{}
// {
// "field_10": "Amsterdam",
// "field_11": "Big pain",
// "field_15": "2025-02-15"
// }
FormFields []FormField
// [
// {"custom_field_id": 10, "label": "City", "value": "Amsterdam"},
// {"custom_field_id": 11, "label": "Pain Level", "value": "Big pain"}
// ]
}Template Syntax Examples
<!-- Organization letterhead -->
<div class="letterhead">
<img src="{{.Organization.LogoURL}}" alt="Logo" />
<h1>{{.Organization.Name}}</h1>
<p>{{.Organization.Address}} | {{.Organization.Phone}}</p>
</div>
<!-- Patient info -->
<div class="patient-info">
<h2>Patient Information</h2>
<p><strong>Name:</strong> {{.Patient.Name}}</p>
<p><strong>Date of Birth:</strong> {{.Patient.Birthdate}}</p>
</div>
<!-- Form fields (dynamic loop) -->
<div class="form-data">
<h2>Consultation Details</h2>
{{range .FormFields}}
<div class="field">
<strong>{{.Label}}:</strong> {{.Value}}
</div>
{{end}}
</div>
<!-- Specific field by ID -->
<p>Chief Complaint: {{index .FormValues "field_10"}}</p>
<!-- Conditional rendering -->
{{if .Specialist.SignatureURL}}
<div class="signature">
<img src="{{.Specialist.SignatureURL}}" alt="Signature" />
<p>{{.Specialist.Name}}, {{.Specialist.Title}}</p>
</div>
{{end}}
<!-- Date formatting -->
<p>Generated on: {{.GeneratedAt.Format "January 2, 2006"}}</p>Minimal Block-Based Editor
Instead of external editors, build a simple custom editor with these core blocks:
Block Types
// Block definitions
type Block = {
id: string;
type: BlockType;
config: BlockConfig;
children?: Block[];
};
enum BlockType {
// Layout blocks
SECTION = 'section', // Container for other blocks
COLUMNS = 'columns', // 2-3 column layout
// Content blocks
TEXT = 'text', // Rich text (bold, italic, underline)
HEADING = 'heading', // H1-H4
PARAGRAPH = 'paragraph', // Body text
IMAGE = 'image', // Logo, signature, uploaded images
TABLE = 'table', // Data tables
// Dynamic blocks
FIELD = 'field', // Single form field value
FIELD_LIST = 'field_list', // All form fields as list
FIELD_TABLE = 'field_table', // All form fields as table
// Components
LETTERHEAD = 'letterhead', // Org header
SIGNATURE = 'signature', // Specialist signature
FOOTER = 'footer', // Page footer
}
// Example block config
interface TextBlockConfig {
content: string;
fontSize: number;
fontWeight: 'normal' | 'bold';
textAlign: 'left' | 'center' | 'right';
color: string;
}
interface FieldBlockConfig {
fieldId: number; // custom_field_id
label: string;
showLabel: boolean;
format?: 'text' | 'date' | 'currency';
}Editor UI Structure
┌──────────────────────────────────────────────────────────┐
│ PDF Template Editor │
├──────────────────────────────────────────────────────────┤
│ │
│ [Block Palette] [Canvas] [Preview] │
│ │
│ 📝 Text ┌─────────────┐ ┌──────────┐ │
│ 📋 Heading │ LETTERHEAD │ │ PDF │ │
│ 📊 Table ├─────────────┤ │ Preview │ │
│ 🖼️ Image │ Patient Info│ │ │ │
│ 📄 Field ├─────────────┤ │ [Live] │ │
│ 📑 Field List │ Form Fields │ │ │ │
│ 🏢 Letterhead ├─────────────┤ │ │ │
│ ✍️ Signature │ SIGNATURE │ │ │ │
│ 📌 Footer └─────────────┘ └──────────┘ │
│ │
│ [Properties Panel] │
│ ┌────────────────────────────────────┐ │
│ │ Selected: Heading Block │ │
│ │ • Font Size: [24px] │ │
│ │ • Font Weight: [Bold ▼] │ │
│ │ • Alignment: [Left ▼] │ │
│ │ • Color: [#000000] │ │
│ └────────────────────────────────────┘ │
│ │
│ [< Back] [Save Draft] [Preview] [Publish Template] │
└──────────────────────────────────────────────────────────┘Editor Features (Minimal)
- Drag & Drop Blocks - Add blocks from palette to canvas
- Inline Editing - Click text to edit content
- Property Panel - Configure selected block (font size, color, alignment)
- Field Picker - Select form fields to insert (autocomplete)
- Live Preview - Render PDF in iframe using sample data
- Component Library - Save/reuse common blocks (letterhead, footer)
- HTML/CSS View - Switch to code editor for advanced users
- Sample Data - Test template with mock form values
How It Works: End-to-End Flow
┌─────────────────────────────────────────────────────────┐
│ 1. TEMPLATE CREATION (Admin) │
└─────────────────────────────────────────────────────────┘
1. Admin navigates to PDF Templates
2. Clicks "Create New Template"
3. Selects type: "Report"
4. Uses block editor to design layout:
├─ Add Letterhead component
├─ Add Heading: "Consultation Report"
├─ Add Patient Info section
├─ Add Field List (all form fields)
├─ Add Signature component
└─ Add Footer component
5. Clicks "Preview" → Sees PDF with sample data
6. Adjusts styling in properties panel
7. Clicks "Publish Template" → Version 1 created
┌─────────────────────────────────────────────────────────┐
│ 2. PDF GENERATION (Runtime) │
└─────────────────────────────────────────────────────────┘
1. Specialist fills and signs form
2. Form status: completed → signed
3. Document service:
├─ Reads pdf_templates WHERE type = 'report'
├─ Reads form.values and form.fields
├─ Merges template + data using Go templates
├─ Applies CSS styling
├─ Renders HTML → PDF with chromedp
└─ Caches PDF to S3
4. Patient downloads PDF
┌─────────────────────────────────────────────────────────┐
│ 3. TEMPLATE UPDATES (Versioning) │
└─────────────────────────────────────────────────────────┘
1. Admin edits template
2. Changes are saved as draft (published = false)
3. Admin clicks "Publish"
├─ Current version archived to pdf_template_versions
├─ version incremented (1 → 2)
├─ published = true
4. New documents use version 2
5. Old documents still reference version 1 (immutable)Integration Points
With Forms Feature
-- Forms store which PDF template to use
ALTER TABLE form_templates ADD COLUMN pdf_template_id BIGINT
REFERENCES pdf_templates(id);
-- Example: Intake survey form → uses "Report" PDF template
UPDATE form_templates
SET pdf_template_id = (SELECT id FROM pdf_templates WHERE name = 'Standard Report')
WHERE type = 'survey';With Documents Feature
-- Documents reference the PDF template version used
ALTER TABLE appointment_documents ADD COLUMN pdf_template_version INT;
-- When generating PDF, store which version was used
UPDATE appointment_documents
SET pdf_template_version = 2
WHERE id = 123;API Endpoints
See api.md for complete API documentation.
Quick reference:
POST /v1/pdf-templates Create template
GET /v1/pdf-templates List templates
GET /v1/pdf-templates/{id} Get template
PUT /v1/pdf-templates/{id} Update template (draft)
POST /v1/pdf-templates/{id}/publish Publish new version
DELETE /v1/pdf-templates/{id} Delete template
GET /v1/pdf-templates/{id}/preview Preview with sample data
GET /v1/pdf-templates/{id}/render Render with real form data
POST /v1/pdf-template-components Create reusable component
GET /v1/pdf-template-components List componentsSample Template (HTML Output)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page {
size: A4;
margin: 20mm;
}
body {
font-family: 'Helvetica', 'Arial', sans-serif;
font-size: 12pt;
line-height: 1.6;
color: #333;
}
.letterhead {
text-align: center;
border-bottom: 2px solid #003366;
padding-bottom: 10mm;
margin-bottom: 10mm;
}
.letterhead img {
max-width: 150px;
margin-bottom: 5mm;
}
.section {
margin-bottom: 10mm;
}
.field {
margin-bottom: 3mm;
}
.field strong {
min-width: 150px;
display: inline-block;
}
.signature {
margin-top: 20mm;
text-align: right;
}
.signature img {
max-width: 200px;
}
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 10pt;
color: #666;
border-top: 1px solid #ccc;
padding-top: 3mm;
}
</style>
</head>
<body>
<!-- Letterhead -->
<div class="letterhead">
<img src="{{.Organization.LogoURL}}" alt="{{.Organization.Name}}" />
<h1>{{.Organization.Name}}</h1>
<p>{{.Organization.Address}} | {{.Organization.Phone}}</p>
</div>
<!-- Document Title -->
<h2 style="text-align: center; color: #003366;">Consultation Report</h2>
<!-- Patient Information -->
<div class="section">
<h3>Patient Information</h3>
<div class="field"><strong>Name:</strong> {{.Patient.Name}}</div>
<div class="field"><strong>Date of Birth:</strong> {{.Patient.Birthdate}}</div>
<div class="field"><strong>Date:</strong> {{.Appointment.StartedAt.Format "January 2, 2006"}}</div>
</div>
<!-- Consultation Details -->
<div class="section">
<h3>Consultation Details</h3>
{{range .FormFields}}
{{if not .IsPrivate}}
<div class="field">
<strong>{{.Label}}:</strong> {{.Value}}
</div>
{{end}}
{{end}}
</div>
<!-- Specialist Signature -->
<div class="signature">
{{if .Specialist.SignatureURL}}
<img src="{{.Specialist.SignatureURL}}" alt="Signature" />
{{end}}
<p><strong>{{.Specialist.Name}}</strong><br>
{{.Specialist.Title}}<br>
License: {{.Specialist.LicenseNumber}}</p>
</div>
<!-- Footer -->
<div class="footer">
<p>{{.Organization.Name}} | Confidential Medical Document | Generated: {{.GeneratedAt.Format "2006-01-02 15:04"}}</p>
</div>
</body>
</html>Benefits Summary
✅ No external dependencies - Custom editor built in-house ✅ Organization branding - Each org customizes their layouts ✅ Non-technical users - Drag-drop blocks, no HTML needed ✅ Advanced users - Can edit HTML/CSS directly ✅ Consistent output - Same renderer for preview and final PDF ✅ Versioning - Old documents remain unchanged when templates update ✅ Reusable components - Letterhead, signature blocks shared across templates ✅ Multi-document types - One feature handles reports, prescriptions, disclaimers, etc.
Next Steps
- Read schema.md for complete database design
- Read api.md for API endpoint specifications
- Read editor.md for visual editor implementation guide
- Read rendering.md for chromedp rendering details
- Read components.md for reusable component library