mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-17 08:39:47 +10:00
- fixes #2562, add better error messages for duplicate resume slugs
- improvements made to ditgar template - general improvements to all templates with backgrounds - update dependencies and translations - improved print function that handles single page and multi page resumes
This commit is contained in:
@@ -8,12 +8,12 @@ import { getStorageService, uploadFile } from "./storage";
|
||||
|
||||
const pageDimensions = {
|
||||
a4: {
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
width: 794,
|
||||
height: 1123,
|
||||
},
|
||||
letter: {
|
||||
width: "8.5in",
|
||||
height: "11in",
|
||||
width: 816,
|
||||
height: 1056,
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -27,7 +27,7 @@ async function getBrowser(): Promise<Browser> {
|
||||
|
||||
const connectOptions: ConnectOptions = {
|
||||
acceptInsecureCerts: true,
|
||||
defaultViewport: { width: 794, height: 1123 },
|
||||
defaultViewport: pageDimensions.a4,
|
||||
};
|
||||
|
||||
if (isWebSocket) connectOptions.browserWSEndpoint = env.PRINTER_ENDPOINT;
|
||||
@@ -64,16 +64,30 @@ export const printerService = {
|
||||
await browser.disconnect();
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a PDF from a resume and uploads it to storage.
|
||||
*
|
||||
* The process:
|
||||
* 1. Clean up any existing PDF for this resume
|
||||
* 2. Navigate to the printer route which renders the resume
|
||||
* 3. Calculate PDF margins (some templates require margins to be applied via PDF)
|
||||
* 4. Adjust CSS variables so content fits within printable area (accounting for margins)
|
||||
* 5. Add page break CSS to ensure each visual resume page becomes a PDF page
|
||||
* 6. Generate the PDF with proper dimensions and margins
|
||||
* 7. Upload to storage and return the URL
|
||||
*/
|
||||
printResumeAsPDF: async (
|
||||
input: Pick<InferSelectModel<typeof schema.resume>, "userId" | "id" | "data">,
|
||||
): Promise<string> => {
|
||||
const { id, userId, data } = input;
|
||||
|
||||
// Step 1: Delete any existing PDF for this resume to ensure fresh generation
|
||||
const storageService = getStorageService();
|
||||
const pdfPrefix = `uploads/${userId}/pdfs/${id}`;
|
||||
|
||||
await storageService.delete(pdfPrefix);
|
||||
|
||||
// Step 2: Prepare the URL and authentication for the printer route
|
||||
// The printer route renders the resume in a format optimized for PDF generation
|
||||
const baseUrl = env.PRINTER_APP_URL ?? env.APP_URL;
|
||||
const domain = new URL(baseUrl).hostname;
|
||||
|
||||
@@ -81,9 +95,13 @@ export const printerService = {
|
||||
const locale = data.metadata.page.locale;
|
||||
const template = data.metadata.template;
|
||||
|
||||
// Generate a secure token to authenticate the printer request
|
||||
const token = generatePrinterToken(id);
|
||||
const url = `${baseUrl}/printer/${id}?token=${token}`;
|
||||
|
||||
// Step 3: Calculate PDF margins
|
||||
// Some templates require margins to be applied via PDF (they use print:p-0 to remove CSS padding)
|
||||
// Convert from CSS pixels to PDF points (divide by 0.75 since 1pt = 0.75px at 72dpi)
|
||||
let marginX = 0;
|
||||
let marginY = 0;
|
||||
|
||||
@@ -92,21 +110,63 @@ export const printerService = {
|
||||
marginY = Math.round(data.metadata.page.marginY / 0.75);
|
||||
}
|
||||
|
||||
// Step 4: Connect to the browser and navigate to the printer route
|
||||
const browser = await getBrowser();
|
||||
|
||||
// Set locale cookie so the resume renders in the correct language
|
||||
await browser.setCookie({ name: "locale", value: locale, domain });
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Wait for the page to fully load (network idle + custom loaded attribute)
|
||||
await page.goto(url, { waitUntil: "networkidle0" });
|
||||
await page.waitForFunction(() => document.body.getAttribute("data-wf-loaded") === "true", { timeout: 5_000 });
|
||||
|
||||
// Step 5: Adjust the DOM for proper PDF pagination
|
||||
// This runs in the browser context to modify CSS before PDF generation
|
||||
await page.evaluate((marginY: number) => {
|
||||
const root = document.documentElement;
|
||||
const container = document.querySelector(".resume-preview-container") as HTMLElement | null;
|
||||
|
||||
// The --page-height CSS variable controls the height of each resume page.
|
||||
// We need to reduce it by the PDF margins so content fits within the printable area.
|
||||
// Without this, content would overflow and create empty pages.
|
||||
const containerHeight = container ? getComputedStyle(container).getPropertyValue("--page-height").trim() : null;
|
||||
const rootHeight = getComputedStyle(root).getPropertyValue("--page-height").trim();
|
||||
const currentHeight = containerHeight || rootHeight;
|
||||
const heightValue = Number.parseFloat(currentHeight);
|
||||
|
||||
if (!Number.isNaN(heightValue)) {
|
||||
// Subtract top + bottom margins from page height
|
||||
const newHeight = `${heightValue - marginY * 2}px`;
|
||||
if (container) container.style.setProperty("--page-height", newHeight);
|
||||
root.style.setProperty("--page-height", newHeight);
|
||||
}
|
||||
|
||||
// Add page break CSS to each resume page element (identified by data-page-index attribute)
|
||||
// This ensures each visual resume page starts a new PDF page
|
||||
const pageElements = document.querySelectorAll("[data-page-index]");
|
||||
|
||||
for (const el of pageElements) {
|
||||
const element = el as HTMLElement;
|
||||
const index = Number.parseInt(element.getAttribute("data-page-index") ?? "0", 10);
|
||||
|
||||
// Force a page break before each page except the first
|
||||
if (index > 0) element.style.breakBefore = "page";
|
||||
|
||||
// Allow content within a page to break naturally if it overflows
|
||||
// (e.g., if a single page has more content than fits on one PDF page)
|
||||
element.style.breakInside = "auto";
|
||||
}
|
||||
}, marginY);
|
||||
|
||||
// Step 6: Generate the PDF with the specified dimensions and margins
|
||||
const pdfBuffer = await page.pdf({
|
||||
width: pageDimensions[format].width,
|
||||
height: pageDimensions[format].height,
|
||||
tagged: true,
|
||||
waitForFonts: true,
|
||||
printBackground: true,
|
||||
width: `${pageDimensions[format].width}px`,
|
||||
height: `${pageDimensions[format].height}px`,
|
||||
tagged: true, // Adds accessibility tags to the PDF
|
||||
waitForFonts: true, // Ensures all fonts are loaded before rendering
|
||||
printBackground: true, // Includes background colors and images
|
||||
margin: {
|
||||
top: marginY,
|
||||
right: marginX,
|
||||
@@ -117,6 +177,7 @@ export const printerService = {
|
||||
|
||||
await page.close();
|
||||
|
||||
// Step 7: Upload the generated PDF to storage
|
||||
const result = await uploadFile({
|
||||
userId,
|
||||
resumeId: id,
|
||||
|
||||
Reference in New Issue
Block a user