diff --git a/docs/sprint-artifacts/3-1-create-pdf-generation-api-route.context.xml b/docs/sprint-artifacts/3-1-create-pdf-generation-api-route.context.xml
new file mode 100644
index 0000000..d46ea0a
--- /dev/null
+++ b/docs/sprint-artifacts/3-1-create-pdf-generation-api-route.context.xml
@@ -0,0 +1,142 @@
+
+
+ 3
+ 3.1
+ Create PDF Generation API Route
+ ready-for-dev
+ 2025-12-01
+ BMAD Story Context Workflow
+ docs/sprint-artifacts/3-1-create-pdf-generation-api-route.md
+
+
+
+ system
+ a server endpoint that generates PDF from the resume page
+ users get consistent, high-quality PDF output
+
+ - Create API route file at server/api/resume/pdf.get.ts
+ - Implement Puppeteer PDF generation
+ - Add error handling with timeout
+ - Configure for Vercel deployment
+ - Test API endpoint
+
+
+
+
+ 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
+ Response includes Content-Disposition: attachment; filename="Ali_Arghyani_Resume.pdf"
+ PDF matches the web preview exactly (WYSIWYG)
+ PDF text is selectable and copy-able (ATS-compatible)
+ PDF is A4 format (210mm × 297mm)
+ PDF generation completes in under 3 seconds
+ Given an error occurs, when caught, then it returns status 500 with JSON error message
+ Timeout is set to 10 seconds max
+
+
+
+
+
+ docs/architecture.md
+ Resume Export Feature - Architecture Document
+
+ GET /api/resume/pdf returns PDF binary with Content-Type: application/pdf and Content-Disposition header
+
+
+ docs/architecture.md
+ Resume Export Feature - Architecture Document
+ Novel Pattern: WYSIWYG PDF Export
+ Puppeteer navigates to /resume?print=true, waits for networkidle0, generates PDF with format A4 and printBackground true
+
+
+ docs/architecture.md
+ Resume Export Feature - Architecture Document
+
+ Use puppeteer-core + @sparticuz/chromium for Vercel serverless. Memory: 1024MB, maxDuration: 10s
+
+
+ docs/sprint-artifacts/tech-spec-epic-3.md
+ Epic Technical Specification: PDF Export
+
+ GET /api/resume/pdf - Success returns PDF buffer, Error returns 500 with JSON { error, message }
+
+
+
+
+ app/pages/resume.vue
+ Resume page that will be captured by Puppeteer. Supports ?print=true query param.
+ isPrintMode computed property
+
+
+ app/composables/useResumeData.ts
+ Provides getPdfFilename() for generating filename
+ getPdfFilename()
+
+
+
+
+
+
+
+
+
+
+
+
+ - File location must be server/api/resume/pdf.get.ts (Nuxt server route convention)
+ - Must use defineEventHandler from Nuxt
+ - Must detect environment for puppeteer vs puppeteer-core selection
+ - Must navigate to /resume?print=true (not /resume)
+ - Must wait for networkidle0 before PDF generation
+ - Must close browser in finally block to prevent memory leaks
+ - Timeout must be 10 seconds max
+ - Memory limit 1024MB on Vercel
+
+
+
+
+ defineEventHandler
+ Nuxt server utility
+ defineEventHandler(async (event) => { ... })
+ nitro/runtime
+
+
+ setResponseHeaders
+ Nuxt server utility
+ setResponseHeaders(event, { 'Content-Type': string, 'Content-Disposition': string })
+ h3
+
+
+ getRequestURL
+ Nuxt server utility
+ getRequestURL(event): URL
+ h3
+
+
+ puppeteer.launch
+ Puppeteer API
+ puppeteer.launch({ headless: boolean, args?: string[], executablePath?: string }): Promise<Browser>
+ puppeteer or puppeteer-core
+
+
+ page.pdf
+ Puppeteer API
+ page.pdf({ format: 'A4', printBackground: boolean, margin?: object }): Promise<Buffer>
+ puppeteer
+
+
+
+
+ Nuxt server route testing. Test API response headers and PDF content.
+ server/api/**/*.spec.ts, tests/
+
+ Request /api/resume/pdf, verify Content-Type is application/pdf
+ Verify Content-Disposition header contains correct filename
+ Open generated PDF, compare visually to web preview
+ Open PDF in reader, try to select and copy text
+ Check PDF page dimensions are A4 (210mm × 297mm)
+ Measure time from request to response, verify under 3 seconds
+ Simulate error, verify 500 status and JSON response
+ Verify timeout configuration in code
+
+
+
diff --git a/docs/sprint-artifacts/3-1-create-pdf-generation-api-route.md b/docs/sprint-artifacts/3-1-create-pdf-generation-api-route.md
index 8ce9e8b..d368cef 100644
--- a/docs/sprint-artifacts/3-1-create-pdf-generation-api-route.md
+++ b/docs/sprint-artifacts/3-1-create-pdf-generation-api-route.md
@@ -1,6 +1,6 @@
# Story 3.1: Create PDF Generation API Route
-Status: drafted
+Status: ready-for-dev
## Story
diff --git a/docs/sprint-artifacts/3-2-create-pdf-download-composable.context.xml b/docs/sprint-artifacts/3-2-create-pdf-download-composable.context.xml
new file mode 100644
index 0000000..93fa3a5
--- /dev/null
+++ b/docs/sprint-artifacts/3-2-create-pdf-download-composable.context.xml
@@ -0,0 +1,134 @@
+
+
+ 3
+ 3.2
+ Create PDF Download Composable
+ ready-for-dev
+ 2025-12-01
+ BMAD Story Context Workflow
+ docs/sprint-artifacts/3-2-create-pdf-download-composable.md
+
+
+
+ developer
+ a composable that handles PDF download logic
+ the download button can trigger downloads easily
+
+ - Create composable file at app/composables/useResumePdf.ts
+ - Implement download logic with blob handling
+ - Add error handling with toast notifications
+ - Test composable functionality
+
+
+
+
+ Given I call downloadPdf() from the composable, when the function executes, then it fetches /api/resume/pdf as blob
+ The composable creates object URL from blob and triggers browser download
+ Download filename is from getPdfFilename() (e.g., "Ali_Arghyani_Resume.pdf")
+ The composable returns isGenerating: Ref<boolean> for loading state
+ The composable returns downloadPdf: () => Promise<void> function
+ Given an error occurs, when caught, then it shows toast notification with error message
+ After error, isGenerating is set back to false
+ Object URL is revoked after download to prevent memory leaks
+
+
+
+
+
+ docs/architecture.md
+ Resume Export Feature - Architecture Document
+
+ Composable pattern with isGenerating ref, try-catch, toast notifications, and finally block for cleanup
+
+
+ docs/sprint-artifacts/tech-spec-epic-3.md
+ Epic Technical Specification: PDF Export
+
+ useResumePdf.ts: Set isGenerating true, fetch blob, create object URL, trigger download, revoke URL, set isGenerating false
+
+
+
+
+ app/composables/useResumeData.ts
+ Existing composable that provides getPdfFilename() helper
+ getPdfFilename(): string
+
+function getPdfFilename(): string {
+ const name = resumeData.basics.name
+ const filename = name.replace(/\s+/g, '_')
+ return `${filename}_Resume.pdf`
+}
+
+
+
+ server/api/resume/pdf.get.ts
+ API endpoint that returns PDF blob (Story 3.1)
+ GET /api/resume/pdf
+
+
+
+
+
+
+
+
+
+
+
+ - File location must be app/composables/useResumePdf.ts
+ - Must use $fetch with responseType: 'blob'
+ - Must use useToast() from Nuxt UI for error notifications
+ - Must use getPdfFilename() from useResumeData() for filename
+ - Must revoke object URL after download to prevent memory leaks
+ - Must set isGenerating = false in finally block (not just catch)
+ - Must handle both success and error cases
+
+
+
+
+ useResumePdf
+ Vue composable
+ function useResumePdf(): { isGenerating: Ref<boolean>, downloadPdf: () => Promise<void> }
+ app/composables/useResumePdf.ts
+
+
+ $fetch
+ Nuxt utility
+ $fetch<T>(url: string, options?: { responseType: 'blob' }): Promise<T>
+ nuxt/app
+
+
+ useToast
+ Nuxt UI composable
+ useToast(): { add: (options: ToastOptions) => void }
+ @nuxt/ui
+
+
+ URL.createObjectURL
+ Web API
+ URL.createObjectURL(blob: Blob): string
+ global
+
+
+ URL.revokeObjectURL
+ Web API
+ URL.revokeObjectURL(url: string): void
+ global
+
+
+
+
+ Vue composable testing with Vitest. Mock $fetch and useToast.
+ app/composables/**/*.spec.ts, tests/
+
+ Mock $fetch, call downloadPdf(), verify fetch called with correct URL and responseType
+ Verify URL.createObjectURL called with blob, anchor element created and clicked
+ Verify anchor download attribute matches getPdfFilename() output
+ Verify isGenerating.value is Ref<boolean> and starts as false
+ Verify downloadPdf is async function returning Promise<void>
+ Mock $fetch to throw, verify toast.add called with error message
+ Mock $fetch to throw, verify isGenerating.value is false after error
+ Verify URL.revokeObjectURL called after download
+
+
+
diff --git a/docs/sprint-artifacts/3-2-create-pdf-download-composable.md b/docs/sprint-artifacts/3-2-create-pdf-download-composable.md
index 62ff5ba..92d1aef 100644
--- a/docs/sprint-artifacts/3-2-create-pdf-download-composable.md
+++ b/docs/sprint-artifacts/3-2-create-pdf-download-composable.md
@@ -1,6 +1,6 @@
# Story 3.2: Create PDF Download Composable
-Status: drafted
+Status: ready-for-dev
## Story
diff --git a/docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.context.xml b/docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.context.xml
new file mode 100644
index 0000000..5c614ca
--- /dev/null
+++ b/docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.context.xml
@@ -0,0 +1,135 @@
+
+
+ 3
+ 3.3
+ Connect Download Button to PDF Generation
+ ready-for-dev
+ 2025-12-01
+ BMAD Story Context Workflow
+ docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.md
+
+
+
+ user
+ to click the download button and get my PDF
+ I can use my resume for job applications
+
+ - Update ResumeDownloadButton component to use useResumePdf composable
+ - Bind loading and disabled states to button
+ - Connect click handler to downloadPdf function
+ - Test full download flow
+
+
+
+
+ Given I click the download button, when PDF generation starts, then the button shows loading spinner
+ The button is disabled during PDF generation
+ Given PDF generation succeeds, when the PDF is ready, then the browser downloads the file
+ Downloaded filename is "Ali_Arghyani_Resume.pdf"
+ Button returns to normal state after download completes
+ Given PDF generation fails, when the error occurs, then a toast notification appears
+ Button returns to normal state after error
+ Button works correctly after error (can retry)
+
+
+
+
+
+ docs/architecture.md
+ Resume Export Feature - Architecture Document
+
+ UButton with :loading="isGenerating" :disabled="isGenerating" @click="downloadPdf"
+
+
+ docs/sprint-artifacts/tech-spec-epic-3.md
+ Epic Technical Specification: PDF Export
+
+ Button shows loading spinner, button is disabled, loading clears when complete
+
+
+
+
+ app/components/resume/ResumeDownloadButton.vue
+ Existing download button component with placeholder handler (from Story 2.5)
+ handleDownload (to be replaced), isPrintMode prop
+
+<script setup lang="ts">
+interface Props {
+ isPrintMode?: boolean
+}
+
+defineProps<Props>()
+
+function handleDownload() {
+ // Placeholder - will be replaced in Epic 3 Story 3.3
+ 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>
+
+
+
+ app/composables/useResumePdf.ts
+ Composable providing isGenerating and downloadPdf (Story 3.2)
+ useResumePdf(), isGenerating, downloadPdf
+
+
+
+
+
+
+
+
+
+
+ - Must preserve existing isPrintMode prop functionality
+ - Must preserve existing styling (fixed position, shadow, z-index)
+ - Must preserve responsive text (hidden on mobile)
+ - Must remove placeholder handleDownload function
+ - Must use useResumePdf() composable
+ - Must bind :loading and :disabled to isGenerating
+ - Must bind @click to downloadPdf
+
+
+
+
+ useResumePdf
+ Vue composable
+ function useResumePdf(): { isGenerating: Ref<boolean>, downloadPdf: () => Promise<void> }
+ app/composables/useResumePdf.ts
+
+
+ UButton
+ Nuxt UI component
+ <UButton :loading="boolean" :disabled="boolean" @click="handler" />
+ @nuxt/ui
+
+
+
+
+ Vue component testing with Vitest and @vue/test-utils. Test user interactions.
+ app/components/**/*.spec.ts, tests/
+
+ Click button, verify loading spinner appears (check loading prop or spinner element)
+ During generation, verify button has disabled attribute
+ Mock successful API, verify file download triggered
+ Verify downloaded file has correct filename
+ After success, verify loading spinner gone and button enabled
+ Mock API error, verify toast notification appears
+ After error, verify button returns to normal state
+ After error, click button again, verify it works (retry)
+
+
+
diff --git a/docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.md b/docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.md
index 42a90dc..c860cf9 100644
--- a/docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.md
+++ b/docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.md
@@ -1,6 +1,6 @@
# Story 3.3: Connect Download Button to PDF Generation
-Status: drafted
+Status: ready-for-dev
## Story
diff --git a/docs/sprint-artifacts/sprint-status.yaml b/docs/sprint-artifacts/sprint-status.yaml
index 2ad2b18..4b45dcc 100644
--- a/docs/sprint-artifacts/sprint-status.yaml
+++ b/docs/sprint-artifacts/sprint-status.yaml
@@ -61,7 +61,7 @@ development_status:
# FRs: FR10-14
# ═══════════════════════════════════════════════════════════════
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
+ 3-1-create-pdf-generation-api-route: ready-for-dev
+ 3-2-create-pdf-download-composable: ready-for-dev
+ 3-3-connect-download-button-to-pdf-generation: ready-for-dev
epic-3-retrospective: optional