mirror of
https://github.com/mmahdium/portfolio.git
synced 2026-08-14 12:12:48 +03:30
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
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
# Story 3.1: Create PDF Generation API Route
|
||||
|
||||
Status: drafted
|
||||
|
||||
## Story
|
||||
|
||||
As a system,
|
||||
I want a server endpoint that generates PDF from the resume page,
|
||||
so that users get consistent, high-quality PDF output.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **AC1:** Given a GET request to `/api/resume/pdf`, when the server processes the request, then it returns a PDF binary with Content-Type `application/pdf`
|
||||
2. **AC2:** Response includes `Content-Disposition: attachment; filename="Ali_Arghyani_Resume.pdf"`
|
||||
3. **AC3:** PDF matches the web preview exactly (WYSIWYG)
|
||||
4. **AC4:** PDF text is selectable and copy-able (ATS-compatible)
|
||||
5. **AC5:** PDF is A4 format (210mm × 297mm)
|
||||
6. **AC6:** PDF generation completes in under 3 seconds
|
||||
7. **AC7:** Given an error occurs, when caught, then it returns status 500 with JSON error message
|
||||
8. **AC8:** Timeout is set to 10 seconds max
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Create API route file (AC: #1, #2)
|
||||
- [ ] Create `server/api/resume/pdf.get.ts`
|
||||
- [ ] Set up Nuxt event handler with `defineEventHandler`
|
||||
- [ ] Configure response headers (Content-Type, Content-Disposition)
|
||||
|
||||
- [ ] Implement Puppeteer PDF generation (AC: #3-#6)
|
||||
- [ ] Import puppeteer (dev) or puppeteer-core + chromium (prod)
|
||||
- [ ] Get base URL from request headers
|
||||
- [ ] Launch browser in headless mode
|
||||
- [ ] Navigate to `/resume?print=true`
|
||||
- [ ] Wait for `networkidle0` (fonts loaded)
|
||||
- [ ] Generate PDF with options: format A4, printBackground true
|
||||
- [ ] Close browser in finally block
|
||||
- [ ] Return PDF buffer
|
||||
|
||||
- [ ] Add error handling (AC: #7, #8)
|
||||
- [ ] Wrap in try-catch block
|
||||
- [ ] Set timeout to 10 seconds
|
||||
- [ ] Return 500 status with error JSON on failure
|
||||
- [ ] Log errors to console
|
||||
|
||||
- [ ] Configure for Vercel deployment
|
||||
- [ ] Use environment detection for puppeteer vs puppeteer-core
|
||||
- [ ] Import @sparticuz/chromium for production
|
||||
- [ ] Update vercel.json with function config (memory: 1024, maxDuration: 10)
|
||||
|
||||
- [ ] Test API endpoint
|
||||
- [ ] Test locally with `curl` or browser
|
||||
- [ ] Verify PDF opens correctly
|
||||
- [ ] Check text is selectable
|
||||
- [ ] Measure generation time
|
||||
- [ ] Test error handling
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Architecture Alignment
|
||||
|
||||
**From Architecture Doc:**
|
||||
- File location: `server/api/resume/pdf.get.ts`
|
||||
- Uses Puppeteer server-side (ADR-001)
|
||||
- Navigates to `/resume?print=true` for WYSIWYG
|
||||
- Returns PDF buffer with proper headers
|
||||
|
||||
**From Tech Spec Epic 3:**
|
||||
- AC1-AC4 map to this story
|
||||
- Performance target: < 3 seconds
|
||||
- Memory limit: 1024MB on Vercel
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
**Local Development (puppeteer with bundled Chromium):**
|
||||
```typescript
|
||||
import puppeteer from 'puppeteer'
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true
|
||||
})
|
||||
```
|
||||
|
||||
**Production (Vercel with @sparticuz/chromium):**
|
||||
```typescript
|
||||
import puppeteer from 'puppeteer-core'
|
||||
import chromium from '@sparticuz/chromium'
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
args: chromium.args,
|
||||
executablePath: await chromium.executablePath(),
|
||||
headless: chromium.headless
|
||||
})
|
||||
```
|
||||
|
||||
**PDF Generation Options:**
|
||||
```typescript
|
||||
const pdf = await page.pdf({
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: { top: 0, right: 0, bottom: 0, left: 0 }
|
||||
})
|
||||
```
|
||||
|
||||
**Response Headers:**
|
||||
```typescript
|
||||
setResponseHeaders(event, {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': 'attachment; filename="Ali_Arghyani_Resume.pdf"'
|
||||
})
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
**Install:**
|
||||
```bash
|
||||
pnpm add puppeteer puppeteer-core @sparticuz/chromium
|
||||
```
|
||||
|
||||
**vercel.json:**
|
||||
```json
|
||||
{
|
||||
"functions": {
|
||||
"server/api/resume/pdf.get.ts": {
|
||||
"memory": 1024,
|
||||
"maxDuration": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
- [ ] API returns PDF binary
|
||||
- [ ] Content-Type is application/pdf
|
||||
- [ ] Content-Disposition has correct filename
|
||||
- [ ] PDF opens in viewer
|
||||
- [ ] Text is selectable in PDF
|
||||
- [ ] Colors are correct (blue headers)
|
||||
- [ ] Generation time < 3 seconds
|
||||
- [ ] Error returns 500 with JSON
|
||||
- [ ] Works on Vercel deployment
|
||||
|
||||
### References
|
||||
|
||||
- [Source: docs/architecture.md#API-Contracts]
|
||||
- [Source: docs/architecture.md#Novel-Pattern-WYSIWYG-PDF-Export]
|
||||
- [Source: docs/architecture.md#Deployment-Architecture]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-3.md#AC1-AC4]
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Context Reference
|
||||
|
||||
<!-- Will be filled by SM agent -->
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
<!-- Will be filled by dev agent -->
|
||||
|
||||
### Debug Log References
|
||||
|
||||
<!-- Will be filled by dev agent during implementation -->
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
<!-- Will be filled by dev agent after completion -->
|
||||
|
||||
### File List
|
||||
|
||||
<!-- Will be filled by dev agent with created/modified files -->
|
||||
|
||||
---
|
||||
|
||||
**Change Log:**
|
||||
- 2025-12-01: Story drafted by SM agent (Bob)
|
||||
@@ -0,0 +1,171 @@
|
||||
# Story 3.2: Create PDF Download Composable
|
||||
|
||||
Status: drafted
|
||||
|
||||
## Story
|
||||
|
||||
As a developer,
|
||||
I want a composable that handles PDF download logic,
|
||||
so that the download button can trigger downloads easily.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **AC1:** Given I call `downloadPdf()` from the composable, when the function executes, then it fetches `/api/resume/pdf` as blob
|
||||
2. **AC2:** The composable creates object URL from blob and triggers browser download
|
||||
3. **AC3:** Download filename is from `getPdfFilename()` (e.g., "Ali_Arghyani_Resume.pdf")
|
||||
4. **AC4:** The composable returns `isGenerating: Ref<boolean>` for loading state
|
||||
5. **AC5:** The composable returns `downloadPdf: () => Promise<void>` function
|
||||
6. **AC6:** Given an error occurs, when caught, then it shows toast notification with error message
|
||||
7. **AC7:** After error, `isGenerating` is set back to false
|
||||
8. **AC8:** Object URL is revoked after download to prevent memory leaks
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Create composable file (AC: #4, #5)
|
||||
- [ ] Create `app/composables/useResumePdf.ts`
|
||||
- [ ] Define `isGenerating` ref with initial value false
|
||||
- [ ] Define `downloadPdf` async function
|
||||
- [ ] Return both from composable
|
||||
|
||||
- [ ] Implement download logic (AC: #1-#3, #8)
|
||||
- [ ] Set `isGenerating = true` at start
|
||||
- [ ] Fetch `/api/resume/pdf` with `responseType: 'blob'`
|
||||
- [ ] Create object URL from blob: `URL.createObjectURL(response)`
|
||||
- [ ] Create temporary `<a>` element
|
||||
- [ ] Set `href` to object URL
|
||||
- [ ] Set `download` attribute to filename from `getPdfFilename()`
|
||||
- [ ] Trigger click on element
|
||||
- [ ] Revoke object URL: `URL.revokeObjectURL(url)`
|
||||
- [ ] Set `isGenerating = false` in finally block
|
||||
|
||||
- [ ] Add error handling (AC: #6, #7)
|
||||
- [ ] Wrap in try-catch block
|
||||
- [ ] Import `useToast()` from Nuxt UI
|
||||
- [ ] Show error toast on catch
|
||||
- [ ] Log error to console
|
||||
- [ ] Ensure `isGenerating = false` in finally
|
||||
|
||||
- [ ] Test composable
|
||||
- [ ] Import in component and call `downloadPdf()`
|
||||
- [ ] Verify loading state changes
|
||||
- [ ] Verify file downloads with correct name
|
||||
- [ ] Test error handling (disconnect network)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Architecture Alignment
|
||||
|
||||
**From Architecture Doc:**
|
||||
- File location: `app/composables/useResumePdf.ts`
|
||||
- Uses `$fetch` with blob response type
|
||||
- Uses Nuxt UI `useToast()` for notifications
|
||||
- Gets filename from `useResumeData().getPdfFilename()`
|
||||
|
||||
**From Tech Spec Epic 3:**
|
||||
- AC5-AC7 map to this story
|
||||
- Composable interface defined in spec
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
**Composable Structure:**
|
||||
```typescript
|
||||
export function useResumePdf() {
|
||||
const isGenerating = ref(false)
|
||||
const toast = useToast()
|
||||
const { getPdfFilename } = useResumeData()
|
||||
|
||||
async function downloadPdf() {
|
||||
isGenerating.value = true
|
||||
try {
|
||||
const response = await $fetch('/api/resume/pdf', {
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
const url = URL.createObjectURL(response)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = getPdfFilename()
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('PDF generation failed:', error)
|
||||
toast.add({
|
||||
title: 'Error generating PDF',
|
||||
description: 'Please try again',
|
||||
color: 'error'
|
||||
})
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { isGenerating, downloadPdf }
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in Component:**
|
||||
```vue
|
||||
<script setup>
|
||||
const { isGenerating, downloadPdf } = useResumePdf()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton
|
||||
:loading="isGenerating"
|
||||
:disabled="isGenerating"
|
||||
@click="downloadPdf"
|
||||
>
|
||||
Download PDF
|
||||
</UButton>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `useResumeData()` composable (Epic 1) - for `getPdfFilename()`
|
||||
- `useToast()` from Nuxt UI - for error notifications
|
||||
- `$fetch` from Nuxt - for API calls
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
- [ ] Composable exports `isGenerating` and `downloadPdf`
|
||||
- [ ] `isGenerating` starts as false
|
||||
- [ ] `isGenerating` becomes true during download
|
||||
- [ ] `isGenerating` returns to false after completion
|
||||
- [ ] PDF downloads with correct filename
|
||||
- [ ] Error shows toast notification
|
||||
- [ ] Error logs to console
|
||||
- [ ] `isGenerating` returns to false after error
|
||||
|
||||
### References
|
||||
|
||||
- [Source: docs/architecture.md#Error-Handling]
|
||||
- [Source: docs/architecture.md#Implementation-Patterns]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-3.md#AC5-AC7]
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Context Reference
|
||||
|
||||
<!-- Will be filled by SM agent -->
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
<!-- Will be filled by dev agent -->
|
||||
|
||||
### Debug Log References
|
||||
|
||||
<!-- Will be filled by dev agent during implementation -->
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
<!-- Will be filled by dev agent after completion -->
|
||||
|
||||
### File List
|
||||
|
||||
<!-- Will be filled by dev agent with created/modified files -->
|
||||
|
||||
---
|
||||
|
||||
**Change Log:**
|
||||
- 2025-12-01: Story drafted by SM agent (Bob)
|
||||
@@ -0,0 +1,166 @@
|
||||
# Story 3.3: Connect Download Button to PDF Generation
|
||||
|
||||
Status: drafted
|
||||
|
||||
## Story
|
||||
|
||||
As a user,
|
||||
I want to click the download button and get my PDF,
|
||||
so that I can use my resume for job applications.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **AC1:** Given I click the download button, when PDF generation starts, then the button shows loading spinner
|
||||
2. **AC2:** The button is disabled during PDF generation
|
||||
3. **AC3:** Given PDF generation succeeds, when the PDF is ready, then the browser downloads the file
|
||||
4. **AC4:** Downloaded filename is "Ali_Arghyani_Resume.pdf"
|
||||
5. **AC5:** Button returns to normal state after download completes
|
||||
6. **AC6:** Given PDF generation fails, when the error occurs, then a toast notification appears
|
||||
7. **AC7:** Button returns to normal state after error
|
||||
8. **AC8:** Button works correctly after error (can retry)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Update ResumeDownloadButton component (AC: #1-#8)
|
||||
- [ ] Import `useResumePdf()` composable
|
||||
- [ ] Destructure `isGenerating` and `downloadPdf`
|
||||
- [ ] Bind `:loading="isGenerating"` to UButton
|
||||
- [ ] Bind `:disabled="isGenerating"` to UButton
|
||||
- [ ] Bind `@click="downloadPdf"` to UButton
|
||||
- [ ] Remove placeholder click handler
|
||||
|
||||
- [ ] Test full download flow
|
||||
- [ ] Click button, verify spinner appears
|
||||
- [ ] Verify button is disabled during generation
|
||||
- [ ] Verify PDF downloads with correct filename
|
||||
- [ ] Verify button returns to normal after download
|
||||
- [ ] Test error handling (disconnect network)
|
||||
- [ ] Verify toast appears on error
|
||||
- [ ] Verify button works after error (retry)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Architecture Alignment
|
||||
|
||||
**From Architecture Doc:**
|
||||
- Component: `app/components/resume/ResumeDownloadButton.vue`
|
||||
- Uses `useResumePdf()` composable
|
||||
- Uses Nuxt UI `UButton` with loading state
|
||||
|
||||
**From Tech Spec Epic 3:**
|
||||
- AC6, AC8 map to this story
|
||||
- Final integration story for Epic 3
|
||||
|
||||
### Learnings from Previous Stories
|
||||
|
||||
**From Story 2.5 (Status: done)**
|
||||
- Download button already created with placeholder handler
|
||||
- Print mode detection implemented via `isPrintMode` prop
|
||||
- Button has correct styling and position
|
||||
|
||||
**From Story 3.2 (Status: drafted)**
|
||||
- `useResumePdf()` composable provides `isGenerating` and `downloadPdf`
|
||||
- Error handling with toast notifications
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
**Current ResumeDownloadButton.vue (from Story 2.5):**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
isPrintMode?: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
function handleDownload() {
|
||||
console.log('Download PDF clicked')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton
|
||||
v-if="!isPrintMode"
|
||||
icon="i-heroicons-arrow-down-tray"
|
||||
size="lg"
|
||||
color="primary"
|
||||
class="fixed bottom-6 right-6 shadow-lg no-print z-50"
|
||||
@click="handleDownload"
|
||||
>
|
||||
<span class="hidden sm:inline">Download PDF</span>
|
||||
</UButton>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Updated ResumeDownloadButton.vue:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
isPrintMode?: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const { isGenerating, downloadPdf } = useResumePdf()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton
|
||||
v-if="!isPrintMode"
|
||||
icon="i-heroicons-arrow-down-tray"
|
||||
size="lg"
|
||||
color="primary"
|
||||
:loading="isGenerating"
|
||||
:disabled="isGenerating"
|
||||
class="fixed bottom-6 right-6 shadow-lg no-print z-50"
|
||||
@click="downloadPdf"
|
||||
>
|
||||
<span class="hidden sm:inline">Download PDF</span>
|
||||
</UButton>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
- [ ] Click button shows loading spinner
|
||||
- [ ] Button is disabled during generation
|
||||
- [ ] PDF downloads automatically
|
||||
- [ ] Filename is "Ali_Arghyani_Resume.pdf"
|
||||
- [ ] Button returns to normal after success
|
||||
- [ ] Error shows toast notification
|
||||
- [ ] Button returns to normal after error
|
||||
- [ ] Can retry after error
|
||||
- [ ] Print mode still hides button
|
||||
|
||||
### References
|
||||
|
||||
- [Source: docs/architecture.md#Loading-State]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-3.md#AC6-AC8]
|
||||
- [Source: docs/sprint-artifacts/2-5-create-download-button-component.md]
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Context Reference
|
||||
|
||||
<!-- Will be filled by SM agent -->
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
<!-- Will be filled by dev agent -->
|
||||
|
||||
### Debug Log References
|
||||
|
||||
<!-- Will be filled by dev agent during implementation -->
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
<!-- Will be filled by dev agent after completion -->
|
||||
|
||||
### File List
|
||||
|
||||
<!-- Will be filled by dev agent with created/modified files -->
|
||||
|
||||
---
|
||||
|
||||
**Change Log:**
|
||||
- 2025-12-01: Story drafted by SM agent (Bob)
|
||||
@@ -60,8 +60,8 @@ development_status:
|
||||
# Goal: Enable high-quality PDF generation matching web preview
|
||||
# FRs: FR10-14
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
epic-3: backlog
|
||||
3-1-create-pdf-generation-api-route: backlog
|
||||
3-2-create-pdf-download-composable: backlog
|
||||
3-3-connect-download-button-to-pdf-generation: backlog
|
||||
epic-3: contexted
|
||||
3-1-create-pdf-generation-api-route: drafted
|
||||
3-2-create-pdf-download-composable: drafted
|
||||
3-3-connect-download-button-to-pdf-generation: drafted
|
||||
epic-3-retrospective: optional
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
# 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_
|
||||
Reference in New Issue
Block a user