- Use browserless over gotenberg

- Implement functionality to move items between sections or pages
- Enhance custom sections to have a `type` property
- Update the v4 importer to account for custom sections
- Update healthcheck to be a simple curl command
- Update dependencies to latest
and a lot more changes
This commit is contained in:
Amruth Pillai
2026-01-21 18:49:54 +01:00
parent b3c342b029
commit 70064be7de
54 changed files with 2153 additions and 822 deletions
+4 -11
View File
@@ -15,13 +15,8 @@ export const printerRouter = {
.input(z.object({ id: z.string() }))
.output(z.object({ url: z.string() }))
.handler(async ({ input, context }) => {
// Get resume to find the owner's userId for storage key
const resume = await resumeService.getByIdForPrinter({ id: input.id });
const url = await printerService.printResumeAsPDF({
id: input.id,
userId: resume.userId,
});
const url = await printerService.printResumeAsPDF(resume);
if (!context.user) {
await resumeService.statistics.increment({ id: input.id, downloads: true });
@@ -40,11 +35,9 @@ export const printerRouter = {
})
.input(z.object({ id: z.string() }))
.output(z.object({ url: z.string() }))
.handler(async ({ input, context }) => {
const url = await printerService.getResumeScreenshot({
id: input.id,
userId: context.user.id,
});
.handler(async ({ input }) => {
const resume = await resumeService.getByIdForPrinter({ id: input.id });
const url = await printerService.getResumeScreenshot(resume);
return { url };
}),
+126 -84
View File
@@ -1,9 +1,25 @@
import { ORPCError } from "@orpc/server";
import type { InferSelectModel } from "drizzle-orm";
import puppeteer, { type Browser, type Page } from "puppeteer-core";
import type { schema } from "@/integrations/drizzle";
import { printMarginTemplates } from "@/schema/templates";
import { env } from "@/utils/env";
import { generatePrinterToken } from "@/utils/printer-token";
import { resumeService } from "./resume";
import { getStorageService, uploadFile } from "./storage";
type PressureResponse = {
cpu: number;
date: number;
isAvailable: boolean;
maxConcurrent: number;
maxQueued: number;
memory: number;
message: string;
queued: number;
reason: string;
recentlyRejected: number;
running: number;
};
const pageDimensions = {
a4: {
width: "210mm",
@@ -17,65 +33,102 @@ const pageDimensions = {
const SCREENSHOT_TTL = 1000 * 60 * 60; // 1 hour
let pdfBrowser: Browser | null = null;
let screenshotBrowser: Browser | null = null;
async function interceptLocalhostRequests(page: Page) {
await page.setRequestInterception(true);
page.on("request", (request) => {
const url = request.url();
if (url.includes(env.APP_URL) && env.PRINTER_APP_URL) {
const newUrl = url.replace(env.APP_URL, env.PRINTER_APP_URL);
request.continue({ url: newUrl });
return;
}
request.continue();
});
}
export const printerService = {
printResumeAsPDF: async (input: { id: string; userId: string }): Promise<string> => {
healthcheck: async (): Promise<PressureResponse> => {
const printerEndpoint = env.PRINTER_ENDPOINT;
const headers = new Headers({ Accept: "application/json" });
const endpoint = new URL(printerEndpoint);
endpoint.protocol = endpoint.protocol === "wss:" ? "https:" : "http:";
endpoint.pathname = "/pressure";
const response = await fetch(endpoint, { headers });
const data = (await response.json()) as { pressure: PressureResponse };
return data.pressure;
},
printResumeAsPDF: async (
input: Pick<InferSelectModel<typeof schema.resume>, "userId" | "id" | "data">,
): Promise<string> => {
const { id, userId, data } = input;
const storageService = getStorageService();
const pdfPrefix = `uploads/${userId}/pdfs/${id}`;
// Delete any existing PDFs for this resume
const pdfPrefix = `uploads/${input.userId}/pdfs/${input.id}`;
await storageService.delete(pdfPrefix);
const resume = await resumeService.getByIdForPrinter({ id: input.id });
const format = resume.data.metadata.page.format;
const locale = resume.data.metadata.page.locale;
const baseUrl = env.PRINTER_APP_URL ?? env.APP_URL;
const domain = new URL(baseUrl).hostname;
const token = generatePrinterToken(input.id);
const url = `${baseUrl}/printer/${input.id}?token=${token}`;
const format = data.metadata.page.format;
const locale = data.metadata.page.locale;
const template = data.metadata.template;
const formData = new FormData();
const cookies = [{ name: "locale", value: locale, domain }];
const token = generatePrinterToken(id);
const url = `${baseUrl}/printer/${id}?token=${token}`;
formData.append("url", url);
formData.append("marginTop", "0");
formData.append("marginLeft", "0");
formData.append("marginRight", "0");
formData.append("marginBottom", "0");
formData.append("printBackground", "true");
formData.append("skipNetworkIdleEvent", "false");
formData.append("cookies", JSON.stringify(cookies));
formData.append("paperWidth", pageDimensions[format].width);
formData.append("paperHeight", pageDimensions[format].height);
let marginX = 0;
let marginY = 0;
const headers = new Headers();
if (env.GOTENBERG_USERNAME && env.GOTENBERG_PASSWORD) {
const credentials = `${env.GOTENBERG_USERNAME}:${env.GOTENBERG_PASSWORD}`;
const encodedCredentials = btoa(credentials);
headers.set("Authorization", `Basic ${encodedCredentials}`);
if (printMarginTemplates.includes(template)) {
marginX = Math.round(data.metadata.page.marginX / 0.75);
marginY = Math.round(data.metadata.page.marginY / 0.75);
}
const response = await fetch(`${env.GOTENBERG_ENDPOINT}/forms/chromium/convert/url`, {
headers,
method: "POST",
body: formData,
});
if (!response.ok) {
throw new ORPCError("UNAUTHORIZED", {
status: response.status,
message: response.statusText,
if (!pdfBrowser || !pdfBrowser.connected) {
pdfBrowser = await puppeteer.connect({
acceptInsecureCerts: true,
browserWSEndpoint: env.PRINTER_ENDPOINT,
});
}
const pdfBuffer = await response.arrayBuffer();
await pdfBrowser.setCookie({ name: "locale", value: locale, domain });
const page = await pdfBrowser.newPage();
if (env.APP_URL.includes("localhost")) await interceptLocalhostRequests(page);
await page.goto(url, { waitUntil: "networkidle0" });
const pdfBuffer = await page.pdf({
width: pageDimensions[format].width,
height: pageDimensions[format].height,
tagged: true,
waitForFonts: true,
printBackground: true,
margin: {
top: marginY,
right: marginX,
bottom: marginY,
left: marginX,
},
});
await page.close();
// Store PDF and return URL
const result = await uploadFile({
userId: input.userId,
resumeId: input.id,
userId,
resumeId: id,
data: new Uint8Array(pdfBuffer),
contentType: "application/pdf",
type: "pdf",
@@ -84,9 +137,13 @@ export const printerService = {
return result.url;
},
getResumeScreenshot: async (input: { id: string; userId: string }): Promise<string> => {
getResumeScreenshot: async (
input: Pick<InferSelectModel<typeof schema.resume>, "userId" | "id" | "data">,
): Promise<string> => {
const { id, userId, data } = input;
const storageService = getStorageService();
const screenshotPrefix = `uploads/${input.userId}/screenshots/${input.id}`;
const screenshotPrefix = `uploads/${userId}/screenshots/${id}`;
const existingScreenshots = await storageService.list(screenshotPrefix);
const now = Date.now();
@@ -105,59 +162,44 @@ export const printerService = {
const latest = sortedFiles[0];
const age = now - latest.timestamp;
if (age < SCREENSHOT_TTL) {
// Return URL of cached screenshot
return new URL(latest.path, env.APP_URL).toString();
}
if (age < SCREENSHOT_TTL) return new URL(latest.path, env.APP_URL).toString();
// Delete old screenshots
await Promise.all(sortedFiles.map((file) => storageService.delete(file.path)));
}
}
const baseUrl = env.PRINTER_APP_URL ?? env.APP_URL;
const domain = new URL(baseUrl).hostname;
const token = generatePrinterToken(input.id);
const url = `${baseUrl}/printer/${input.id}?token=${token}`;
const locale = data.metadata.page.locale;
const formData = new FormData();
const token = generatePrinterToken(id);
const url = `${baseUrl}/printer/${id}?token=${token}`;
formData.append("url", url);
formData.append("clip", "true");
formData.append("width", "794");
formData.append("height", "1123");
formData.append("format", "webp");
formData.append("optimizeForSpeed", "true");
formData.append("skipNetworkIdleEvent", "false");
const headers = new Headers();
if (env.GOTENBERG_USERNAME && env.GOTENBERG_PASSWORD) {
const credentials = `${env.GOTENBERG_USERNAME}:${env.GOTENBERG_PASSWORD}`;
const encodedCredentials = btoa(credentials);
headers.set("Authorization", `Basic ${encodedCredentials}`);
}
const response = await fetch(`${env.GOTENBERG_ENDPOINT}/forms/chromium/screenshot/url`, {
headers,
method: "POST",
body: formData,
});
if (!response.ok) {
throw new ORPCError("UNAUTHORIZED", {
status: response.status,
message: response.statusText,
if (!screenshotBrowser || !screenshotBrowser.connected) {
screenshotBrowser = await puppeteer.connect({
acceptInsecureCerts: true,
defaultViewport: { width: 794, height: 1123 },
browserWSEndpoint: env.PRINTER_ENDPOINT,
});
}
const imageBuffer = await response.arrayBuffer();
await screenshotBrowser.setCookie({ name: "locale", value: locale, domain });
const page = await screenshotBrowser.newPage();
if (env.APP_URL.includes("localhost")) await interceptLocalhostRequests(page);
await page.goto(url, { waitUntil: "networkidle0" });
const screenshotBuffer = await page.screenshot({ type: "webp", quality: 80 });
await page.close();
// Store screenshot and return URL
const result = await uploadFile({
userId: input.userId,
resumeId: input.id,
data: new Uint8Array(imageBuffer),
userId,
resumeId: id,
data: new Uint8Array(screenshotBuffer),
contentType: "image/webp",
type: "screenshot",
});