- 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:
Amruth Pillai
2026-01-23 23:31:24 +01:00
parent ed74fb67f2
commit 4e73a81d4b
96 changed files with 3996 additions and 3418 deletions
+72 -11
View File
@@ -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,