JSON Resume Themes
JSON Resume Themes
A practical example showing how Blocks validates resume themes for semantic HTML, accessibility (WCAG), and responsive design. This example demonstrates how domain rules eliminate duplication across multiple themes.
Overview
This example implements a system for rendering professional resume themes using Handlebars templates, with multi-layer validation ensuring:
- Semantic HTML - Proper use of header, main, section, article tags
- Accessibility - ARIA labels, semantic structure, keyboard navigation
- Responsive Design - Mobile-first approach with media queries
- Visual Hierarchy - Clear typography and spacing
Domain Model
Entities
domain:
entities:
resume:
fields:
- basics # Name, email, phone, summary
- work # Work experience history
- education # Educational background
- skills # Technical and soft skills
- projects # Personal/professional projects
- languages # Language proficiencyPhilosophy
philosophy:
- "Resume themes must prioritize readability and professionalism"
- "All themes must be responsive and accessible"
- "Semantic HTML and proper structure are required"
- "Visual hierarchy guides the reader through content"Domain Rules (DRY)
Instead of repeating validation rules for each theme, define them once:
blocks:
domain_rules:
- id: semantic_html
description: "Must use semantic HTML tags (header, main, section, article)"
- id: accessibility
description: "Must include proper ARIA labels and semantic structure"
- id: responsive_design
description: "Must include responsive meta tags and mobile-first CSS"
- id: visual_hierarchy
description: "Must have clear typography scale and spacing rhythm"All theme blocks inherit these rules automatically.
Example Blocks
Modern Professional Theme
blocks:
theme.modern_professional:
description: "Clean, modern resume theme with professional styling"
inputs:
- name: resume
type: entity.resume
outputs:
- name: html
type: string
measures: [valid_html]
test_data: "test-data/sample-resume.json"Implementation (blocks/modern-professional/block.ts):
import Handlebars from 'handlebars';
import { readFileSync } from 'fs';
import { join } from 'path';
export interface Resume {
basics: {
name: string;
label: string;
email: string;
phone?: string;
summary?: string;
location?: {
city: string;
countryCode: string;
};
};
work?: Array<{
company: string;
position: string;
startDate: string;
endDate?: string;
summary?: string;
highlights?: string[];
}>;
education?: Array<{
institution: string;
area: string;
studyType: string;
startDate: string;
endDate?: string;
}>;
skills?: Array<{
name: string;
level?: string;
keywords?: string[];
}>;
}
const templateSource = readFileSync(
join(__dirname, 'template.hbs'),
'utf-8'
);
const template = Handlebars.compile(templateSource);
export function modernProfessionalTheme(resume: Resume) {
// Input validation only - domain rules validated at dev time
if (!resume.basics?.name || !resume.basics?.label) {
throw new Error("Resume must include basics.name and basics.label");
}
return { html: template(resume) };
}Template (blocks/modern-professional/template.hbs):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{basics.name}} - {{basics.label}}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
header {
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 2px solid #2563eb;
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: #1e3a8a;
}
h2 {
font-size: 1.5rem;
margin: 2rem 0 1rem;
color: #2563eb;
text-transform: uppercase;
letter-spacing: 0.05em;
}
section { margin-bottom: 2rem; }
@media (max-width: 640px) {
body { padding: 1rem; }
h1 { font-size: 2rem; }
h2 { font-size: 1.25rem; }
}
</style>
</head>
<body>
<header role="banner">
<h1>{{basics.name}}</h1>
<p class="subtitle" aria-label="Professional title">{{basics.label}}</p>
{{#if basics.email}}
<p><a href="mailto:{{basics.email}}" aria-label="Email contact">{{basics.email}}</a></p>
{{/if}}
</header>
<main role="main">
{{#if basics.summary}}
<section aria-labelledby="summary-heading">
<h2 id="summary-heading">Summary</h2>
<p>{{basics.summary}}</p>
</section>
{{/if}}
{{#if work}}
<section aria-labelledby="experience-heading">
<h2 id="experience-heading">Experience</h2>
{{#each work}}
<article class="work-item">
<h3>{{position}} at {{company}}</h3>
<p class="dates">{{startDate}} - {{#if endDate}}{{endDate}}{{else}}Present{{/if}}</p>
{{#if summary}}<p>{{summary}}</p>{{/if}}
{{#if highlights}}
<ul aria-label="Key achievements">
{{#each highlights}}<li>{{this}}</li>{{/each}}
</ul>
{{/if}}
</article>
{{/each}}
</section>
{{/if}}
{{#if education}}
<section aria-labelledby="education-heading">
<h2 id="education-heading">Education</h2>
{{#each education}}
<article class="education-item">
<h3>{{studyType}} in {{area}}</h3>
<p>{{institution}}</p>
<p class="dates">{{startDate}} - {{endDate}}</p>
</article>
{{/each}}
</section>
{{/if}}
{{#if skills}}
<section aria-labelledby="skills-heading">
<h2 id="skills-heading">Skills</h2>
<ul aria-label="Technical skills">
{{#each skills}}
<li><strong>{{name}}</strong>{{#if level}} ({{level}}){{/if}}</li>
{{/each}}
</ul>
</section>
{{/if}}
</main>
</body>
</html>Validation
Run validation during development:
blocks run theme.modern_professionalMulti-Layer Feedback
Schema Validator (deterministic):
- ✓ Input type matches
entity.resume - ✓ Output type matches
string
Shape Validator (deterministic):
- ✓
index.tsexports theme - ✓
block.tsexists - ✓ Template file present
Domain Validator (AI-powered):
- ✓ Template uses semantic HTML (header, main, section, article)
- ✓ ARIA labels present for all sections
- ✓ Responsive meta tag included
- ✓ Media queries for mobile devices
- ✓ Clear visual hierarchy with typography scale
Benefits of This Approach
1. DRY Domain Rules
All themes inherit the same semantic requirements:
blocks:
domain_rules:
- id: semantic_html
- id: accessibility
- id: responsive_design
theme.modern_professional: {} # Inherits all domain_rules
theme.creative: {} # Inherits all domain_rules
theme.minimal: {} # Inherits all domain_rules2. Development-Time Validation
The domain validator reads template SOURCE during development:
- Analyzes
template.hbsfor semantic HTML - Checks CSS for responsive patterns
- Validates ARIA structure
- No runtime HTML parsing needed
3. Simple Block Implementation
Blocks stay focused (20-30 lines):
export function theme(resume: Resume) {
// Input validation only
if (!resume.basics?.name) {
throw new Error("Resume must include name");
}
// Render and return
return { html: template(resume) };
}Domain compliance is enforced by validators, not runtime code.
4. Consistent Quality
All themes must pass the same standards:
- Semantic HTML
- WCAG accessibility
- Mobile-responsive
- Clear hierarchy
Adding Custom Theme Rules
Override domain rules for specific themes:
blocks:
domain_rules:
- id: semantic_html
- id: accessibility
theme.creative:
domain_rules:
# Override: creative theme has different requirements
- id: semantic_html
- id: accessibility
- id: bold_typography
description: "Must use large, bold headings for visual impact"
- id: color_contrast
description: "Must maintain WCAG AAA contrast ratios"Real-World Usage
import { modernProfessionalTheme } from './blocks/modern-professional';
const resume = {
basics: {
name: "Jane Smith",
label: "Senior Software Engineer",
email: "jane@example.com",
summary: "Full-stack engineer with 8 years of experience..."
},
work: [
{
company: "Tech Corp",
position: "Senior Engineer",
startDate: "2020-01",
highlights: [
"Led team of 5 engineers",
"Reduced API latency by 40%"
]
}
],
skills: [
{ name: "TypeScript", level: "Expert" },
{ name: "React", level: "Advanced" }
]
};
const { html } = modernProfessionalTheme(resume);
// Serve or save HTMLNext Steps
- Configure AI providers for domain validation
- Understand validation architecture
- Explore HR Recommendation Engine for multi-block composition