Files
portfolio/docs/sprint-artifacts/tech-spec-epic-3.md
T
mahdiarghyani 9d05829503 docs(epic-3): add tech spec and story drafts for PDF Export
Epic 3: PDF Export - Planning Phase Complete

Tech Spec:
- Create tech-spec-epic-3.md with 10 acceptance criteria
- Define API contracts, workflows, and NFRs
- Document dependencies (puppeteer, @sparticuz/chromium)
- Add Vercel deployment configuration
- Include traceability mapping and test strategy

Story Drafts:
- 3-1: Create PDF Generation API Route (8 ACs, Puppeteer integration)
- 3-2: Create PDF Download Composable (8 ACs, download logic)
- 3-3: Connect Download Button to PDF Generation (8 ACs, integration)

Sprint Status Updates:
- epic-3: backlog → contexted
- All 3 stories: backlog → drafted

Files Created:
- docs/sprint-artifacts/tech-spec-epic-3.md
- docs/sprint-artifacts/3-1-create-pdf-generation-api-route.md
- docs/sprint-artifacts/3-2-create-pdf-download-composable.md
- docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.md

Files Modified:
- docs/sprint-artifacts/sprint-status.yaml

Ready for: Story context generation or implementation
2025-12-01 12:04:37 +03:30

390 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Epic Technical Specification: PDF Export
Date: 2025-12-01
Author: mahdi
Epic ID: 3
Status: Draft
---
## Overview
Epic 3 delivers the PDF Export functionality for the Resume Export feature. This epic implements server-side PDF generation using Puppeteer, enabling users to download their resume as a high-quality, ATS-compatible PDF that matches the web preview exactly (WYSIWYG approach).
The implementation leverages the existing `/resume` page (completed in Epic 2) as the source for PDF generation. Puppeteer navigates to `/resume?print=true`, captures the page, and returns a PDF buffer. A client-side composable handles the download flow with proper loading states and error handling.
**Key Value:** One-click PDF download that produces pixel-perfect, ATS-friendly resumes in under 3 seconds.
## Objectives and Scope
**In Scope:**
- Server-side PDF generation API route (`/api/resume/pdf`)
- Puppeteer integration for headless Chrome rendering
- Client-side composable for download logic (`useResumePdf`)
- Download button integration with loading states
- Error handling with toast notifications
- Vercel serverless deployment configuration
**Out of Scope:**
- Multiple export formats (DOCX, plain text) - future
- Custom filename input - uses fixed format
- Resume customization UI - future
- Persian language PDF - future epic
- Multiple templates - future
## System Architecture Alignment
**Architecture Decisions Referenced:**
- ADR-001: Server-side PDF Generation (Puppeteer chosen over client-side libraries)
- Novel Pattern: WYSIWYG PDF Export (same component for web and PDF)
- API Pattern: Nuxt Server Route
**Components Created:**
- `server/api/resume/pdf.get.ts` - API endpoint
- `app/composables/useResumePdf.ts` - Download logic
**Components Modified:**
- `app/components/resume/ResumeDownloadButton.vue` - Connect to composable
**Dependencies on Epic 2:**
- `/resume` page must exist and render correctly
- `?print=true` query parameter must hide download button
- Print styles must be properly configured
## Detailed Design
### Services and Modules
| Component | Responsibility | Inputs | Outputs | Owner |
|-----------|---------------|--------|---------|-------|
| `server/api/resume/pdf.get.ts` | Generate PDF from resume page | GET request | PDF binary buffer | Story 3.1 |
| `app/composables/useResumePdf.ts` | Handle download flow | User click | File download | Story 3.2 |
| `ResumeDownloadButton.vue` | Trigger download, show loading | isPrintMode prop | Click event | Story 3.3 |
### Data Models and Contracts
**No new data models required.** Epic 3 uses existing:
- `Resume` interface from `app/types/resume.ts`
- `resumeData` from `app/data/resume.en.ts`
- `useResumeData()` composable for `getPdfFilename()`
**PDF Output Contract:**
- Format: A4 (210mm × 297mm)
- Content-Type: `application/pdf`
- Filename: `Ali_Arghyani_Resume.pdf` (from `getPdfFilename()`)
- File size: < 500KB target
### APIs and Interfaces
**GET /api/resume/pdf**
```typescript
// Request
GET /api/resume/pdf
// Response (Success - 200)
Headers:
Content-Type: application/pdf
Content-Disposition: attachment; filename="Ali_Arghyani_Resume.pdf"
Body: <PDF binary buffer>
// Response (Error - 500)
Headers:
Content-Type: application/json
Body: {
"error": "PDF generation failed",
"message": "<error details>"
}
```
**useResumePdf Composable Interface:**
```typescript
interface UseResumePdf {
isGenerating: Ref<boolean>
downloadPdf: () => Promise<void>
}
function useResumePdf(): UseResumePdf
```
### Workflows and Sequencing
**PDF Download Flow:**
```
User clicks "Download PDF" button
┌─────────────────────────────────┐
│ ResumeDownloadButton.vue │
│ - Calls downloadPdf() │
│ - Shows loading spinner │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ useResumePdf.ts │
│ 1. Set isGenerating = true │
│ 2. Fetch /api/resume/pdf (blob) │
│ 3. Create object URL │
│ 4. Create <a> element │
│ 5. Set download filename │
│ 6. Trigger click │
│ 7. Revoke object URL │
│ 8. Set isGenerating = false │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ server/api/resume/pdf.get.ts │
│ 1. Get base URL from request │
│ 2. Launch Puppeteer browser │
│ 3. Navigate to /resume?print=true│
│ 4. Wait for networkidle0 │
│ 5. Generate PDF (A4, background)│
│ 6. Close browser │
│ 7. Return PDF with headers │
└─────────────────────────────────┘
Browser downloads PDF file
```
**Error Flow:**
```
API returns error (500)
┌─────────────────────────────────┐
│ useResumePdf.ts │
│ 1. Catch error │
│ 2. Show toast notification │
│ 3. Log error to console │
│ 4. Set isGenerating = false │
└─────────────────────────────────┘
```
## Non-Functional Requirements
### Performance
| Metric | Target | Strategy |
|--------|--------|----------|
| PDF generation time | < 3 seconds | Puppeteer with networkidle0 |
| PDF file size | < 500KB | Text-only, no images, optimized fonts |
| Memory usage | < 512MB | Single browser instance, close after use |
| API timeout | 10 seconds max | Vercel function config |
**Optimization Strategies:**
- Use `networkidle0` to ensure fonts are loaded
- Close browser immediately after PDF generation
- No external resource loading in PDF
- Minimal page content (print mode hides extras)
### Security
- **Sandboxed execution:** Puppeteer runs in sandboxed mode
- **No user input:** PDF generation uses server-controlled data only
- **Timeout protection:** 10 second max duration prevents resource exhaustion
- **No external resources:** PDF doesn't load external URLs
- **HTTPS required:** Production deployment uses HTTPS only
### Reliability/Availability
- **Graceful degradation:** If PDF fails, user sees error toast and can retry
- **Timeout handling:** Long-running requests are terminated at 10 seconds
- **Browser cleanup:** Browser instance always closed in finally block
- **Retry capability:** User can click download again after error
### Observability
- **Console logging:** Errors logged to console for debugging
- **Toast notifications:** User-facing error messages
- **Loading state:** Visual feedback during generation
- **Error details:** API returns error message for debugging
## Dependencies and Integrations
**New Dependencies:**
| Package | Version | Purpose | Environment |
|---------|---------|---------|-------------|
| puppeteer | ^23.x | Local PDF generation | Development |
| puppeteer-core | ^23.x | Serverless PDF generation | Production |
| @sparticuz/chromium | ^131.x | Chromium for Vercel | Production |
**Installation:**
```bash
# Development (full Puppeteer with bundled Chromium)
pnpm add puppeteer
# Production (Vercel serverless)
pnpm add puppeteer-core @sparticuz/chromium
```
**Internal Dependencies:**
- `app/pages/resume.vue` - Source page for PDF
- `app/composables/useResumeData.ts` - `getPdfFilename()` helper
- `@nuxt/ui` - `useToast()` for notifications, `UButton` for loading state
**Configuration:**
```json
// vercel.json
{
"functions": {
"server/api/resume/pdf.get.ts": {
"memory": 1024,
"maxDuration": 10
}
}
}
```
## Acceptance Criteria (Authoritative)
### AC1: PDF Generation API
**Given** a GET request to `/api/resume/pdf`
**When** the server processes the request
**Then** it returns a PDF binary with correct headers
**And** Content-Type is `application/pdf`
**And** Content-Disposition includes filename `Ali_Arghyani_Resume.pdf`
### AC2: PDF Content Quality
**Given** the PDF is generated
**When** I open the PDF
**Then** it matches the web preview exactly (WYSIWYG)
**And** text is selectable and copy-able (ATS-compatible)
**And** fonts are embedded correctly (Inter)
**And** colors are preserved (blue headers, white background)
### AC3: PDF Dimensions
**Given** the PDF is generated
**When** I check the page size
**Then** it is A4 format (210mm × 297mm)
**And** margins are consistent with web preview
### AC4: Generation Performance
**Given** I request PDF generation
**When** the process completes
**Then** it takes less than 3 seconds
**And** PDF file size is less than 500KB
### AC5: Download Composable
**Given** I call `downloadPdf()` from the composable
**When** the function executes
**Then** it fetches the PDF as blob
**And** triggers browser download
**And** uses correct filename from `getPdfFilename()`
### AC6: Loading State
**Given** I click the download button
**When** PDF generation is in progress
**Then** the button shows loading spinner
**And** the button is disabled
**And** loading clears when complete
### AC7: Error Handling
**Given** PDF generation fails
**When** the error is caught
**Then** a toast notification appears with error message
**And** the button returns to normal state
**And** error is logged to console
### AC8: Button Integration
**Given** the download button is connected to the composable
**When** I click it
**Then** `downloadPdf()` is called
**And** loading state is bound to button
**And** button works correctly after error
### AC9: Print Mode Compatibility
**Given** Puppeteer navigates to `/resume?print=true`
**When** the page renders
**Then** download button is hidden
**And** all content renders correctly
**And** print styles are applied
### AC10: Vercel Deployment
**Given** the API is deployed to Vercel
**When** I request PDF generation
**Then** it works with serverless Chromium
**And** respects memory and timeout limits
## Traceability Mapping
| AC | Spec Section | Components | FR | Test Idea |
|----|--------------|------------|-----|-----------|
| AC1 | APIs and Interfaces | `pdf.get.ts` | FR10, FR12 | Request API, verify headers |
| AC2 | Data Models | `pdf.get.ts` | FR11, FR12 | Open PDF, check text selection |
| AC3 | Data Models | `pdf.get.ts` | FR11 | Check PDF page dimensions |
| AC4 | Performance | `pdf.get.ts` | FR14 | Measure generation time |
| AC5 | Workflows | `useResumePdf.ts` | FR10 | Call composable, verify download |
| AC6 | Workflows | `ResumeDownloadButton.vue` | FR10 | Click button, check spinner |
| AC7 | Workflows | `useResumePdf.ts` | FR10 | Simulate error, check toast |
| AC8 | Services | `ResumeDownloadButton.vue` | FR10 | Click button, verify flow |
| AC9 | Workflows | `pdf.get.ts` | FR11 | Check print mode rendering |
| AC10 | Dependencies | `pdf.get.ts` | FR14 | Deploy and test on Vercel |
## Risks, Assumptions, Open Questions
**Risks:**
| Risk | Severity | Mitigation |
|------|----------|------------|
| Puppeteer timeout on Vercel | Medium | Set 10s timeout, optimize page load |
| Font loading issues in headless | Medium | Use `networkidle0`, test thoroughly |
| Memory limits on serverless | Low | Use 1024MB, single browser instance |
| Chromium binary size | Low | Use `@sparticuz/chromium` (optimized) |
**Assumptions:**
- Vercel supports Puppeteer with `@sparticuz/chromium`
- Inter font loads correctly in headless Chrome
- 10 second timeout is sufficient for PDF generation
- Single PDF generation at a time (no concurrency needed)
**Open Questions:**
- **Q:** Should we cache generated PDFs?
**A:** No for MVP - data is static, regeneration is fast enough
- **Q:** Should we support custom filenames?
**A:** No for MVP - use fixed format from `getPdfFilename()`
## Test Strategy Summary
**Unit Tests:**
- `useResumePdf` composable: mock fetch, verify state changes
- `getPdfFilename()` returns correct format
**Integration Tests:**
- API route returns PDF with correct headers
- Full download flow works end-to-end
**Manual Tests:**
- Click download button, verify PDF downloads
- Open PDF, verify content matches preview
- Test on Vercel deployment
- Test error handling (disconnect network)
- Verify ATS compatibility (copy text from PDF)
**Performance Tests:**
- Measure generation time (< 3s target)
- Check PDF file size (< 500KB target)
- Test under load (optional)
**Acceptance Tests:**
- Run through all 10 ACs
- Verify on multiple browsers
- Test on mobile (download should work)
---
_Generated by BMAD Epic Tech Context Workflow_
_Date: 2025-12-01_
_For: mahdi_