Files
portfolio/docs/architecture.md
T
mahdiarghyani 1586bb7f80 chore: add BMAD agent workflows and configuration system
- Add comprehensive agent workflow definitions for 8 specialized roles (analyst, architect, developer, product manager, scrum master, technical writer, UX designer, QA engineer)
- Add 35+ workflow definitions covering analysis, planning, solutioning, and implementation phases
- Add BMAD configuration system with agent, task, tool, workflow, and file manifests
- Add BMM (Business Model Methodology) documentation including quick-start guides, architecture references, and workflow analysis
- Add test architecture knowledge base with 20+ testing patterns and best practices
- Add team configuration templates and party mode setup for collaborative development
- Establish foundation for enterprise agentic development framework with adaptive scaling capabilities
2025-11-30 14:36:05 +03:30

577 lines
16 KiB
Markdown

# Resume Export Feature - Architecture Document
## Executive Summary
This architecture defines the Resume Export feature for nuxt-portfolio - a server-side PDF generation system using Puppeteer that produces ATS-compatible, pixel-perfect PDF resumes from Vue components. The WYSIWYG approach ensures web preview matches PDF output exactly.
---
## Decision Summary
| Category | Decision | Version | Affects FRs | Rationale |
|----------|----------|---------|-------------|-----------|
| PDF Engine | Puppeteer (server-side) | ^23.x | FR8-14 | Best quality, font embedding, full control |
| Data Schema | JSON Resume (customized) | - | FR1-4 | Industry standard, extensible |
| Font (EN) | Inter | via @nuxt/fonts | FR22-23 | Modern, ATS-safe, excellent readability |
| Font (FA) | Vazirmatn | via @nuxt/fonts | FR22-23 | Best Persian web font, RTL support |
| Template System | Vue Components | - | FR15-21 | Full control, reusable, type-safe |
| API Pattern | Nuxt Server Route | - | FR10-14 | Native Nuxt, no extra server |
| Layout | Two-column (sidebar + main) | - | FR6, FR9 | Matches design template |
| Storage | File-based (TypeScript) | - | FR1, FR3 | Simple, version controlled |
---
## Project Structure
```
app/
├── components/
│ └── resume/
│ ├── ResumePreview.vue # Main container (A4 layout)
│ ├── ResumeHeader.vue # Name, job title
│ ├── ResumeContact.vue # Phone, email, location, links
│ ├── ResumeSidebar.vue # Skills, education, languages
│ ├── ResumeExperience.vue # Work history with highlights
│ ├── ResumeSummary.vue # Professional summary
│ └── ResumeDownloadButton.vue # Floating action button
├── data/
│ └── resume.en.ts # English resume data
├── pages/
│ └── resume.vue # /resume route (standalone page)
├── types/
│ └── resume.ts # TypeScript interfaces
└── composables/
└── useResumePdf.ts # PDF download logic
server/
└── api/
└── resume/
└── pdf.get.ts # GET /api/resume/pdf → PDF buffer
```
---
## FR Category to Architecture Mapping
| FR Category | FRs | Components/Files | Notes |
|-------------|-----|------------------|-------|
| Resume Data Management | FR1-4 | `data/resume.en.ts`, `types/resume.ts` | TypeScript for type safety |
| Resume Preview Page | FR5-9 | `pages/resume.vue`, `components/resume/*` | Standalone, no nav |
| PDF Export | FR10-14 | `server/api/resume/pdf.get.ts`, `useResumePdf.ts` | Puppeteer server-side |
| Template Sections | FR15-21 | Individual resume components | Modular design |
| Styling & Theming | FR22-25 | Tailwind classes, CSS variables | Blue/white theme |
---
## Technology Stack Details
### Core Stack (Existing)
- **Framework:** Nuxt 4.1.3
- **Language:** TypeScript 5.9.x
- **Styling:** Tailwind CSS 4.1.x
- **UI Components:** Nuxt UI 4.0.x
- **Fonts:** @nuxt/fonts 0.11.x
### New Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| puppeteer | ^23.x | Server-side PDF generation |
### Installation
```bash
pnpm add puppeteer
```
---
## Data Architecture
### Resume Interface
```typescript
// app/types/resume.ts
export interface ResumeBasics {
name: string
label: string // Job title
email: string
phone: string
url?: string
location: {
city: string
country: string
}
profiles: Array<{
network: string // LinkedIn, GitHub, etc.
url: string
icon?: string // Iconify icon name
}>
summary: string
}
export interface WorkExperience {
company: string
position: string
startDate: string // YYYY-MM format
endDate?: string // YYYY-MM or undefined for "Present"
highlights: string[] // Bullet points
}
export interface Education {
institution: string
area: string // Field of study
studyType: string // Degree type
startDate: string
endDate?: string
}
export interface Skill {
name: string // Category name
keywords: string[] // Individual skills
}
export interface Language {
language: string
fluency: 'Native' | 'Fluent' | 'Intermediate' | 'Basic'
}
export interface Certification {
name: string
issuer: string
date: string
}
export interface Resume {
basics: ResumeBasics
work: WorkExperience[]
education: Education[]
skills: Skill[]
languages?: Language[]
certifications?: Certification[]
}
```
### Sample Data Structure
```typescript
// app/data/resume.en.ts
import type { Resume } from '~/types/resume'
export const resumeData: Resume = {
basics: {
name: 'Ali Arghyani',
label: 'Senior Frontend Developer',
email: 'ali@example.com',
phone: '+98 912 345 6789',
location: { city: 'Tehran', country: 'Iran' },
profiles: [
{ network: 'LinkedIn', url: 'https://linkedin.com/in/ali', icon: 'i-mdi-linkedin' },
{ network: 'GitHub', url: 'https://github.com/ali', icon: 'i-mdi-github' }
],
summary: 'Experienced frontend developer with 5+ years...'
},
work: [
{
company: 'Tech Company',
position: 'Senior Frontend Developer',
startDate: '2022-01',
highlights: [
'Led development of customer-facing dashboard',
'Improved performance by 40%'
]
}
],
education: [...],
skills: [
{ name: 'Frontend', keywords: ['Vue.js', 'Nuxt', 'TypeScript', 'Tailwind'] },
{ name: 'Tools', keywords: ['Git', 'Docker', 'CI/CD'] }
],
languages: [
{ language: 'Persian', fluency: 'Native' },
{ language: 'English', fluency: 'Fluent' }
]
}
```
---
## API Contracts
### GET /api/resume/pdf
**Request:**
```
GET /api/resume/pdf
```
**Response (Success):**
```
Status: 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="Ali_Arghyani_Resume.pdf"
Body: <PDF binary>
```
**Response (Error):**
```
Status: 500 Internal Server Error
Content-Type: application/json
Body: { "error": "PDF generation failed", "message": "..." }
```
---
## Novel Pattern: WYSIWYG PDF Export
### Problem
Web preview must match PDF output exactly (pixel-perfect).
### Solution
Single component renders both web preview and PDF source.
### Flow
```
┌─────────────────────────────────────────────────────┐
│ User visits /resume │
│ ┌───────────────────────────────────────────────┐ │
│ │ ResumePreview.vue (A4 aspect ratio) │ │
│ │ - Same component for web AND PDF │ │
│ │ - Tailwind + print styles │ │
│ └───────────────────────────────────────────────┘ │
│ [Download Button - FAB bottom-right] │
└─────────────────────────────────────────────────────┘
│ User clicks download
┌─────────────────────────────────────────────────────┐
│ useResumePdf.ts │
│ 1. Set loading state │
│ 2. Fetch /api/resume/pdf │
│ 3. Create blob URL │
│ 4. Trigger download │
│ 5. Clear loading state │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ server/api/resume/pdf.get.ts │
│ 1. Launch Puppeteer (headless) │
│ 2. Navigate to /resume?print=true │
│ 3. Wait for networkidle0 (fonts loaded) │
│ 4. page.pdf({ format: 'A4', printBackground: true })│
│ 5. Close browser │
│ 6. Return PDF buffer with headers │
└─────────────────────────────────────────────────────┘
```
### Key Implementation Details
**Query Parameter:** `?print=true`
- Hides download button in PDF
- Removes any interactive elements
**CSS Print Styles:**
```css
@media print {
.no-print { display: none; }
.resume-container {
width: 210mm;
min-height: 297mm;
}
}
```
---
## Implementation Patterns
### Naming Conventions
| Type | Convention | Example |
|------|------------|---------|
| Vue Components | PascalCase | `ResumeHeader.vue` |
| Composables | camelCase + `use` prefix | `useResumePdf.ts` |
| Data files | kebab-case + locale suffix | `resume.en.ts` |
| API routes | kebab-case + method suffix | `pdf.get.ts` |
| TypeScript types | PascalCase | `WorkExperience` |
| CSS classes | Tailwind utilities | `text-blue-600` |
### Component Organization
```
components/resume/
├── ResumePreview.vue # Container - imports all others
├── ResumeHeader.vue # Standalone section
├── ResumeContact.vue # Standalone section
├── ResumeSidebar.vue # Standalone section
├── ResumeExperience.vue # Standalone section
├── ResumeSummary.vue # Standalone section
└── ResumeDownloadButton.vue # FAB - hidden in print
```
### Error Handling
```typescript
// Composable pattern
export function useResumePdf() {
const isGenerating = ref(false)
const toast = useToast()
async function downloadPdf() {
isGenerating.value = true
try {
const response = await $fetch('/api/resume/pdf', {
responseType: 'blob'
})
// Create download link
const url = URL.createObjectURL(response)
const a = document.createElement('a')
a.href = url
a.download = 'Ali_Arghyani_Resume.pdf'
a.click()
URL.revokeObjectURL(url)
} catch (error) {
toast.add({
title: 'Error generating PDF',
description: 'Please try again',
color: 'error'
})
} finally {
isGenerating.value = false
}
}
return { isGenerating, downloadPdf }
}
```
### Loading State
```vue
<template>
<UButton
:loading="isGenerating"
:disabled="isGenerating"
icon="i-heroicons-arrow-down-tray"
size="lg"
class="fixed bottom-6 right-6 no-print"
@click="downloadPdf"
>
Download PDF
</UButton>
</template>
```
---
## Consistency Rules
### Color Scheme
| Element | Color | Tailwind Class |
|---------|-------|----------------|
| Primary (headers, icons) | Blue | `text-blue-600`, `bg-blue-600` |
| Background | White | `bg-white` |
| Text | Dark gray | `text-gray-800` |
| Secondary text | Medium gray | `text-gray-600` |
| Borders/dividers | Light gray | `border-gray-200` |
### Typography
| Element | Font | Size | Weight |
|---------|------|------|--------|
| Name | Inter | 2rem | Bold |
| Job Title | Inter | 1.25rem | Medium |
| Section Headers | Inter | 1rem | Semibold |
| Body Text | Inter | 0.875rem | Normal |
| Persian Text | Vazirmatn | Same sizes | Same weights |
### Spacing
| Element | Spacing |
|---------|---------|
| Page margins | 1.5rem (24px) |
| Section gap | 1.5rem |
| Item gap | 0.75rem |
| Sidebar width | 35% |
| Main content width | 65% |
---
## Security Architecture
### PDF Generation Security
- Puppeteer runs in sandboxed mode
- No user input in PDF generation (data is server-controlled)
- Timeout limit: 10 seconds max
- No external resource loading in PDF
### Data Security
- Resume data stored in source code (no database)
- No PII exposed in API responses (only PDF binary)
- HTTPS required for production
---
## Performance Considerations
### PDF Generation
| Metric | Target | Strategy |
|--------|--------|----------|
| Generation time | < 3 seconds | Puppeteer reuse, minimal page |
| PDF file size | < 500KB | No images, optimized fonts |
| Memory usage | < 512MB | Single browser instance |
### Page Load
| Metric | Target | Strategy |
|--------|--------|----------|
| Preview load | < 1 second | Static data, no API calls |
| LCP | < 1.5s | Inline critical CSS |
| CLS | 0 | Fixed dimensions |
### Optimization Strategies
1. **Font subsetting:** Only include used characters
2. **No images in resume:** Text-only for ATS
3. **Lazy browser launch:** Only when PDF requested
4. **Browser instance reuse:** For multiple requests
---
## Deployment Architecture
### Vercel Deployment
```json
// vercel.json (existing)
{
"functions": {
"server/api/resume/pdf.get.ts": {
"memory": 1024,
"maxDuration": 10
}
}
}
```
### Puppeteer on Vercel
Use `@sparticuz/chromium` for serverless:
```typescript
// server/api/resume/pdf.get.ts
import puppeteer from 'puppeteer-core'
import chromium from '@sparticuz/chromium'
export default defineEventHandler(async (event) => {
const browser = await puppeteer.launch({
args: chromium.args,
executablePath: await chromium.executablePath(),
headless: chromium.headless
})
// ... generate PDF
})
```
---
## Development Environment
### Prerequisites
- Node.js >= 18.20.0
- pnpm 9.x
- Chrome/Chromium (for local Puppeteer)
### Setup Commands
```bash
# Install dependencies
pnpm install
# Run development server
pnpm dev
# Test PDF generation locally
# Visit http://localhost:3000/resume
# Click download button
```
### Environment Variables
```env
# .env (optional - for production Puppeteer)
PUPPETEER_EXECUTABLE_PATH=/path/to/chrome
```
---
## Architecture Decision Records (ADRs)
### ADR-001: Server-side PDF Generation
**Decision:** Use Puppeteer server-side instead of client-side libraries
**Context:** Need pixel-perfect PDF with proper font embedding
**Alternatives Considered:**
- html2pdf.js (client) - Poor quality, converts to image
- jsPDF (client) - Manual layout, no CSS support
- Print to PDF (browser) - Inconsistent across browsers
**Consequences:**
- (+) Perfect quality, font embedding works
- (+) Consistent output across all clients
- (-) Requires server resources
- (-) Slightly slower than client-side
---
### ADR-002: Vue Components as Templates
**Decision:** Each resume section is a Vue component
**Context:** Need maintainable, type-safe template system
**Alternatives Considered:**
- JSON config + dynamic renderer - Too complex for MVP
- HTML templates - No type safety
**Consequences:**
- (+) Full TypeScript support
- (+) Easy to modify individual sections
- (+) Reusable across web and PDF
- (-) Adding new templates requires code
---
### ADR-003: JSON Resume Schema (Modified)
**Decision:** Use JSON Resume standard with customizations
**Context:** Need structured, extensible data format
**Modifications from standard:**
- Simplified location (city + country only)
- Added icon field to profiles
- Persian language support
**Consequences:**
- (+) Industry standard, well-documented
- (+) Easy to export/import
- (+) Future-proof for integrations
---
_Generated by BMAD Decision Architecture Workflow_
_Date: 2025-11-30_
_For: mahdi_