PDF Template Components - Reusable Blocks
Component library for building professional PDF templates with reusable blocks.
Overview
PDF template components are reusable HTML/CSS snippets that can be inserted into multiple templates. They ensure:
- ✅ Consistency - Same letterhead across all documents
- ✅ Maintainability - Update once, applies to all templates
- ✅ Organization branding - Each org has custom components
- ✅ Time savings - Drag-drop instead of writing HTML
Component Categories
| Category | Purpose | Examples |
|---|---|---|
letterhead | Organization header | Logo + name + address |
footer | Page footer | Organization info, page numbers, legal notice |
signature | Specialist signature | Signature image, name, title, license |
header | Document header | Document title, metadata, date |
table | Reusable table layouts | Service line items, medication tables |
section | Content sections | Patient info, findings, recommendations |
Default Components
1. Standard Letterhead
Category: letterhead
HTML:
<div class="letterhead">
<div class="letterhead-logo">
{{if .Organization.LogoURL}}
<img src="{{.Organization.LogoURL}}" alt="{{.Organization.Name}}" class="logo" />
{{end}}
</div>
<div class="letterhead-info">
<h1 class="org-name">{{.Organization.Name}}</h1>
<p class="org-contact">
{{.Organization.Address}}<br>
Tel: {{.Organization.Phone}} | Email: {{.Organization.Email}}
{{if .Organization.Website}}
<br>{{.Organization.Website}}
{{end}}
</p>
</div>
</div>CSS:
.letterhead {
text-align: center;
border-bottom: 2px solid #003366;
padding-bottom: 10mm;
margin-bottom: 10mm;
}
.letterhead-logo {
margin-bottom: 5mm;
}
.letterhead .logo {
max-width: 150px;
height: auto;
}
.letterhead .org-name {
font-size: 18pt;
color: #003366;
margin: 0 0 5px 0;
font-weight: bold;
}
.letterhead .org-contact {
font-size: 10pt;
color: #666;
margin: 0;
line-height: 1.4;
}Variables Used:
Organization.LogoURLOrganization.NameOrganization.AddressOrganization.PhoneOrganization.EmailOrganization.Website
2. Specialist Signature Block
Category: signature
HTML:
<div class="signature-block">
<div class="signature-line">
{{if .Specialist.SignatureURL}}
<img src="{{.Specialist.SignatureURL}}" alt="Signature" class="signature-image" />
{{else}}
<div class="signature-placeholder">
_______________________________
</div>
{{end}}
</div>
<div class="signature-info">
<p class="specialist-name"><strong>{{.Specialist.Name}}</strong></p>
<p class="specialist-title">{{.Specialist.Title}}</p>
{{if .Specialist.LicenseNumber}}
<p class="specialist-license">License No: {{.Specialist.LicenseNumber}}</p>
{{end}}
</div>
</div>CSS:
.signature-block {
margin-top: 30mm;
text-align: right;
page-break-inside: avoid;
}
.signature-line {
margin-bottom: 5mm;
}
.signature-image {
max-width: 200px;
height: auto;
}
.signature-placeholder {
font-size: 12pt;
color: #999;
}
.signature-info {
font-size: 11pt;
line-height: 1.4;
}
.signature-info p {
margin: 2px 0;
}
.specialist-name {
font-size: 12pt;
}
.specialist-title {
color: #666;
font-style: italic;
}
.specialist-license {
font-size: 10pt;
color: #666;
}Variables Used:
Specialist.SignatureURLSpecialist.NameSpecialist.TitleSpecialist.LicenseNumber
3. Page Footer
Category: footer
HTML:
<div class="page-footer">
<div class="footer-content">
<p class="footer-text">
{{.Organization.Name}} | Confidential Medical Document
{{if .GeneratedAt}}
| Generated: {{.GeneratedAt.Format "2006-01-02 15:04"}}
{{end}}
</p>
<p class="footer-legal">
This document contains confidential medical information. Unauthorized disclosure is prohibited.
</p>
</div>
</div>CSS:
.page-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 9pt;
color: #666;
border-top: 1px solid #ccc;
padding-top: 3mm;
background: white;
}
.footer-content {
max-width: 170mm;
margin: 0 auto;
}
.footer-text {
margin: 0 0 2mm 0;
font-size: 9pt;
}
.footer-legal {
margin: 0;
font-size: 8pt;
color: #999;
font-style: italic;
}Variables Used:
Organization.NameGeneratedAt
4. Patient Information Section
Category: section
HTML:
<div class="patient-info-section">
<h3 class="section-title">Patient Information</h3>
<div class="info-grid">
<div class="info-row">
<span class="info-label">Name:</span>
<span class="info-value">{{.Patient.Name}}</span>
</div>
<div class="info-row">
<span class="info-label">Date of Birth:</span>
<span class="info-value">{{.Patient.Birthdate.Format "January 2, 2006"}}</span>
</div>
<div class="info-row">
<span class="info-label">Age:</span>
<span class="info-value">{{.Patient.Age}} years</span>
</div>
{{if .Patient.Phone}}
<div class="info-row">
<span class="info-label">Phone:</span>
<span class="info-value">{{.Patient.Phone}}</span>
</div>
{{end}}
{{if .Patient.Email}}
<div class="info-row">
<span class="info-label">Email:</span>
<span class="info-value">{{.Patient.Email}}</span>
</div>
{{end}}
</div>
</div>CSS:
.patient-info-section {
margin-bottom: 10mm;
page-break-inside: avoid;
}
.section-title {
font-size: 14pt;
color: #003366;
border-bottom: 1px solid #003366;
padding-bottom: 2mm;
margin-bottom: 5mm;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3mm;
}
.info-row {
display: flex;
}
.info-label {
font-weight: bold;
min-width: 120px;
flex-shrink: 0;
}
.info-value {
flex: 1;
}Variables Used:
Patient.NamePatient.BirthdatePatient.AgePatient.PhonePatient.Email
5. Form Fields List
Category: section
HTML:
<div class="form-fields-section">
<h3 class="section-title">Consultation Details</h3>
<div class="fields-list">
{{range .FormFields}}
{{if not .IsPrivate}}
<div class="field-row">
<span class="field-label">{{.Label}}:</span>
<span class="field-value">{{.Value}}</span>
</div>
{{end}}
{{end}}
</div>
</div>CSS:
.form-fields-section {
margin-bottom: 10mm;
}
.fields-list {
background: #f9f9f9;
padding: 5mm;
border-radius: 2mm;
}
.field-row {
display: flex;
padding: 2mm 0;
border-bottom: 1px solid #eee;
}
.field-row:last-child {
border-bottom: none;
}
.field-label {
font-weight: bold;
min-width: 150px;
flex-shrink: 0;
color: #003366;
}
.field-value {
flex: 1;
}Variables Used:
FormFields(array)FormFields[].LabelFormFields[].ValueFormFields[].IsPrivate
6. Appointment Details Header
Category: header
HTML:
<div class="appointment-header">
<div class="header-left">
<h2 class="document-title">Consultation Report</h2>
<p class="document-subtitle">{{.Appointment.ServiceName}}</p>
</div>
<div class="header-right">
<p class="appointment-date">
<strong>Date:</strong> {{.Appointment.StartedAt.Format "January 2, 2006"}}
</p>
<p class="appointment-time">
<strong>Time:</strong> {{.Appointment.StartedAt.Format "15:04"}} - {{.Appointment.EndedAt.Format "15:04"}}
</p>
</div>
</div>CSS:
.appointment-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10mm;
padding-bottom: 5mm;
border-bottom: 2px solid #003366;
}
.header-left {
flex: 1;
}
.document-title {
font-size: 20pt;
color: #003366;
margin: 0 0 3mm 0;
}
.document-subtitle {
font-size: 12pt;
color: #666;
margin: 0;
}
.header-right {
text-align: right;
font-size: 11pt;
}
.header-right p {
margin: 2mm 0;
}Variables Used:
Appointment.ServiceNameAppointment.StartedAtAppointment.EndedAt
7. Medication Table
Category: table
HTML:
<div class="medication-table">
<h3 class="section-title">Prescribed Medications</h3>
<table class="data-table">
<thead>
<tr>
<th>Medication</th>
<th>Dosage</th>
<th>Frequency</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{{range .Medications}}
<tr>
<td>{{.Name}}</td>
<td>{{.Dosage}}</td>
<td>{{.Frequency}}</td>
<td>{{.Duration}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>CSS:
.medication-table {
margin-bottom: 10mm;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 11pt;
}
.data-table thead {
background: #003366;
color: white;
}
.data-table th {
padding: 3mm;
text-align: left;
font-weight: bold;
}
.data-table td {
padding: 3mm;
border-bottom: 1px solid #ddd;
}
.data-table tbody tr:nth-child(even) {
background: #f9f9f9;
}
.data-table tbody tr:hover {
background: #f0f0f0;
}Variables Used:
Medications(array)Medications[].NameMedications[].DosageMedications[].FrequencyMedications[].Duration
Creating Custom Components
Via UI (Recommended)
- Navigate to Settings → PDF Templates → Components
- Click "Create Component"
- Fill in:
- Name: "Custom Letterhead"
- Category:
letterhead - Description: "Letterhead with custom branding"
- Write HTML in editor (Monaco editor with syntax highlighting)
- Write CSS in editor
- Click "Preview" to see rendered component
- Click "Save"
Via API
curl -X POST https://api.example.com/v1/pdf-template-components \
-H "Content-Type: application/json" \
-d '{
"name": "Custom Letterhead",
"description": "Letterhead with custom branding",
"category": "letterhead",
"component_html": "<div class=\"letterhead\">...</div>",
"component_css": ".letterhead { text-align: center; }",
"variables_used": ["Organization.LogoURL", "Organization.Name"]
}'Using Components in Templates
Via Block Editor
- Drag "Letterhead" block from palette
- In properties panel:
- Select "Standard Letterhead" from dropdown
- Or click "Customize" to edit inline
- Component HTML/CSS is inserted into template
Manually in HTML
<!DOCTYPE html>
<html>
<head>
<style>
{{template "letterhead_css"}}
</style>
</head>
<body>
{{template "letterhead"}}
<h2>Consultation Report</h2>
<!-- Your content here -->
{{template "signature"}}
{{template "footer"}}
</body>
</html>Component Versioning
Components are not versioned (unlike templates). Changes to components affect all templates that use them immediately.
Best practices:
- Don't break existing templates - If making breaking changes, create a new component
- Test before updating - Preview all templates using the component
- Communicate changes - Notify admins when updating shared components
Component Library Management
Searching Components
const SearchComponents: React.FC = () => {
const [components, setComponents] = useState<PDFTemplateComponent[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
const filteredComponents = useMemo(() => {
return components.filter(c => {
const matchesSearch = c.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = !categoryFilter || c.category === categoryFilter;
return matchesSearch && matchesCategory;
});
}, [components, searchTerm, categoryFilter]);
return (
<div className="component-library">
<SearchBar value={searchTerm} onChange={setSearchTerm} />
<CategoryFilter value={categoryFilter} onChange={setCategoryFilter} />
<div className="component-grid">
{filteredComponents.map(component => (
<ComponentCard
key={component.id}
component={component}
onInsert={() => insertComponent(component)}
/>
))}
</div>
</div>
);
};Component Preview
const ComponentPreview: React.FC<{ component: PDFTemplateComponent }> = ({ component }) => {
const [previewHTML, setPreviewHTML] = useState('');
useEffect(() => {
// Merge component with sample data
const sampleData = {
Organization: {
Name: 'Sample Clinic',
LogoURL: 'https://via.placeholder.com/150',
Address: '123 Main St',
Phone: '+1 234 567 8900',
},
};
// Render component HTML with sample data
const html = renderTemplate(component.component_html, sampleData);
// Wrap in HTML document with CSS
const fullHTML = `
<!DOCTYPE html>
<html>
<head>
<style>${component.component_css}</style>
</head>
<body>
${html}
</body>
</html>
`;
setPreviewHTML(fullHTML);
}, [component]);
return (
<div className="component-preview">
<iframe srcDoc={previewHTML} sandbox="allow-same-origin" />
</div>
);
};Advanced: Nested Components
Components can reference other components:
<!-- "Full Report Header" component -->
<div class="report-header">
{{template "letterhead"}}
<div class="appointment-details">
{{template "appointment_header"}}
</div>
</div>Implementation:
- Store component dependencies in
components_usedJSONB - When rendering, resolve nested components recursively
- Prevent circular dependencies (max depth: 3)
Component Analytics
Track component usage to understand which are most popular:
-- Find most-used components
SELECT
c.id,
c.name,
c.category,
COUNT(DISTINCT t.id) AS templates_using
FROM pdf_template_components c
LEFT JOIN pdf_templates t ON t.components_used @> jsonb_build_array(c.name)
GROUP BY c.id, c.name, c.category
ORDER BY templates_using DESC;Component Marketplace (Future)
Allow sharing components across organizations:
ALTER TABLE pdf_template_components
ADD COLUMN is_public BOOLEAN DEFAULT FALSE,
ADD COLUMN downloads INT DEFAULT 0;
-- Public components (shared across orgs)
SELECT * FROM pdf_template_components
WHERE is_public = true
ORDER BY downloads DESC;Summary
✅ 7 default components - Letterhead, footer, signature, patient info, form fields, appointment header, medication table ✅ Organization-scoped - Each org can customize components ✅ Reusable - Insert into multiple templates ✅ Category-based - Organized by purpose (header, footer, signature, etc.) ✅ Live preview - See component before inserting ✅ Template variables - Access all template data (Organization, Patient, Specialist, etc.) ✅ No versioning - Changes affect all templates immediately
Components are the building blocks of professional PDF templates, ensuring consistency and saving time.