mirror of
https://github.com/mmahdium/portfolio.git
synced 2026-08-16 21:14:31 +03:30
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
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
# 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_
|
||||
Reference in New Issue
Block a user