- 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
+18
View File
@@ -166,6 +166,12 @@ export const resumeRouter = {
}),
)
.output(z.string().describe("The ID of the created resume."))
.errors({
RESUME_SLUG_ALREADY_EXISTS: {
message: "A resume with this slug already exists.",
status: 400,
},
})
.handler(async ({ context, input }) => {
return await resumeService.create({
name: input.name,
@@ -187,6 +193,12 @@ export const resumeRouter = {
})
.input(z.object({ data: resumeDataSchema }))
.output(z.string().describe("The ID of the imported resume."))
.errors({
RESUME_SLUG_ALREADY_EXISTS: {
message: "A resume with this slug already exists.",
status: 400,
},
})
.handler(async ({ context, input }) => {
const name = generateRandomName();
const slug = slugify(name);
@@ -220,6 +232,12 @@ export const resumeRouter = {
}),
)
.output(z.void())
.errors({
RESUME_SLUG_ALREADY_EXISTS: {
message: "A resume with this slug already exists.",
status: 400,
},
})
.handler(async ({ context, input }) => {
return await resumeService.update({
id: input.id,
+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,
+38 -15
View File
@@ -1,5 +1,6 @@
import { ORPCError } from "@orpc/client";
import { and, arrayContains, asc, desc, eq, sql } from "drizzle-orm";
import { get } from "es-toolkit/compat";
import { match } from "ts-pattern";
import { schema } from "@/integrations/drizzle";
import { db } from "@/integrations/drizzle/client";
@@ -238,16 +239,26 @@ export const resumeService = {
input.data = input.data ?? defaultResumeData;
input.data.metadata.page.locale = input.locale;
await db.insert(schema.resume).values({
id,
name: input.name,
slug: input.slug,
tags: input.tags,
userId: input.userId,
data: input.data,
});
try {
await db.insert(schema.resume).values({
id,
name: input.name,
slug: input.slug,
tags: input.tags,
userId: input.userId,
data: input.data,
});
return id;
return id;
} catch (error) {
const constraint = get(error, "cause.constraint") as string | undefined;
if (constraint === "resume_slug_user_id_unique") {
throw new ORPCError("RESUME_SLUG_ALREADY_EXISTS", { status: 400 });
}
throw error;
}
},
update: async (input: {
@@ -274,12 +285,24 @@ export const resumeService = {
isPublic: input.isPublic,
};
await db
.update(schema.resume)
.set(updateData)
.where(
and(eq(schema.resume.id, input.id), eq(schema.resume.isLocked, false), eq(schema.resume.userId, input.userId)),
);
try {
await db
.update(schema.resume)
.set(updateData)
.where(
and(
eq(schema.resume.id, input.id),
eq(schema.resume.isLocked, false),
eq(schema.resume.userId, input.userId),
),
);
} catch (error) {
if (get(error, "cause.constraint") === "resume_slug_user_id_unique") {
throw new ORPCError("RESUME_SLUG_ALREADY_EXISTS", { status: 400 });
}
throw error;
}
},
setLocked: async (input: { id: string; userId: string; isLocked: boolean }): Promise<void> => {
+2 -1
View File
@@ -27,7 +27,8 @@ export const getQueryClient = () => {
},
},
mutationCache: new MutationCache({
onSettled: () => {
onSettled: (_1, _2, _3, _4, _5, context) => {
if (context?.meta?.noInvalidate) return;
queryClient.invalidateQueries();
},
}),