import type { APIRoute } from "astro"; import { chromium } from "playwright"; import { config } from "../../../config"; import * as TOML from "@iarna/toml"; // Helper function to fetch and return SVG icon from Simple Icons CDN async function getSimpleIcon(iconName: string): Promise { try { const response = await fetch( `https://cdn.jsdelivr.net/npm/simple-icons@v10/icons/${iconName.toLowerCase()}.svg`, ); if (!response.ok) { console.warn(`Failed to fetch icon: ${iconName}`); return ""; } const svgContent = await response.text(); return svgContent.replace( "', "mdi:download": '', "mdi:link": '', }; return iconMap[iconName] || ""; } interface ResumeData { basics: { name: string; email: string; website?: string; profiles: { network: string; username: string; url: string; }[]; }; layout?: { left_column?: string[]; right_column?: string[]; }; summary: { content: string; }; experience: { company: string; position: string; location: string; date: string; description: string[]; url?: string; }[]; education: { institution: string; degree: string; field: string; date: string; details?: string[]; }[]; skills: { name: string; level: number; }[]; volunteer: { organization: string; position: string; date: string; }[]; awards: { title: string; organization: string; date: string; description?: string; }[]; } // Template helper functions const createSection = ( title: string, content: string, spacing = "space-y-3", ) => `

${title}

${content}
`; const createExperienceItem = (exp: any) => `

${exp.position}

${exp.company} ${exp.date} ${exp.location}
    ${exp.description.map((item: string) => `
  • ${item}
  • `).join("")}
`; const createSkillItem = (skill: any) => { const progressValue = skill.level * 20; return `
${skill.name} ${skill.level}/5
`; }; const createEducationItem = (edu: any) => { const detailsList = edu.details ? edu.details .map((detail: string) => `
  • ${detail}
  • `) .join("") : ""; return `

    ${edu.institution}

    ${edu.degree} in ${edu.field} ${edu.date}
    ${detailsList ? `
      ${detailsList}
    ` : ""}
    `; }; const createVolunteerItem = (vol: any) => `

    ${vol.organization}

    ${vol.position} ${vol.date}
    `; const createAwardItem = (award: any) => `

    ${award.title}

    ${award.organization} ${award.date}
    ${award.description ? `
    ${award.description}
    ` : ""}
    `; const createHead = (name: string) => ` ${name} - Resume `; const createHeader = ( basics: any, emailIcon: string, profileIcons: { [key: string]: string }, ) => `

    ${basics.name}

    ${basics.email ? `
    ${emailIcon} ${basics.email}
    ` : ""} ${ basics.profiles ?.map((profile: any) => { const icon = profileIcons[profile.network] || ""; const displayUrl = profile.url .replace(/^https?:\/\//, "") .replace(/\/$/, ""); return `
    ${icon} ${displayUrl}
    `; }) .join("") || "" }
    `; const createSummarySection = (summary: any, resumeConfig: any) => { if ( !summary || !summary.content || resumeConfig.sections.summary?.enabled === false ) return ""; return `

    ${resumeConfig.sections.summary?.title || "Summary"}

    ${summary.content}
    `; }; const createColumnSections = ( sectionNames: string[], sections: { [key: string]: string }, resumeConfig: any, ) => { const sectionConfig = { experience: { title: resumeConfig.sections.experience?.title || "Experience", enabled: resumeConfig.sections.experience?.enabled !== false, spacing: "space-y-3", }, skills: { title: resumeConfig.sections.skills?.title || "Skills", enabled: resumeConfig.sections.skills?.enabled !== false, spacing: "space-y-1", }, education: { title: resumeConfig.sections.education?.title || "Education", enabled: resumeConfig.sections.education?.enabled !== false, spacing: "space-y-3", }, volunteer: { title: resumeConfig.sections.volunteer?.title || "Volunteer Work", enabled: resumeConfig.sections.volunteer?.enabled !== false, spacing: "space-y-2", }, awards: { title: resumeConfig.sections.awards?.title || "Awards & Recognition", enabled: resumeConfig.sections.awards?.enabled !== false, spacing: "space-y-2", }, }; return sectionNames .map((sectionName) => { const config = sectionConfig[sectionName as keyof typeof sectionConfig]; const content = sections[sectionName]; // Skip if section doesn't exist in config, has no content, or is disabled if (!config || !content || content.trim() === "" || !config.enabled) return ""; return createSection(config.title, content, config.spacing); }) .filter((section) => section !== "") .join(""); }; const fetchProfileIcons = async (profiles: any[]) => { const profileIcons: { [key: string]: string } = {}; if (profiles) { for (const profile of profiles) { const iconName = profile.network.toLowerCase(); profileIcons[profile.network] = await getSimpleIcon(iconName); } } return profileIcons; }; const generateResumeHTML = async (data: ResumeData): Promise => { const resumeConfig = config.resumeConfig; // Use layout from TOML data, fallback to site config, then to default const layout = data.layout ? { leftColumn: data.layout.left_column || [ "experience", "volunteer", "awards", ], rightColumn: data.layout.right_column || ["skills", "education"], } : resumeConfig.layout || { leftColumn: ["experience", "volunteer", "awards"], rightColumn: ["skills", "education"], }; // Pre-fetch icons const profileIcons = await fetchProfileIcons(data.basics.profiles); const emailIcon = getMdiIcon("mdi:email"); // Generate section content const sections = { experience: Array.isArray(data.experience) ? data.experience.map(createExperienceItem).join("") : "", skills: Array.isArray(data.skills) ? data.skills.map(createSkillItem).join("") : "", education: Array.isArray(data.education) ? data.education.map(createEducationItem).join("") : "", volunteer: Array.isArray(data.volunteer) ? data.volunteer.map(createVolunteerItem).join("") : "", awards: Array.isArray(data.awards) ? data.awards.map(createAwardItem).join("") : "", }; return ` ${createHead(data.basics.name)}
    ${createHeader(data.basics, emailIcon, profileIcons)} ${createSummarySection(data.summary, resumeConfig)}
    ${createColumnSections(layout.leftColumn ?? [], sections, resumeConfig)}
    ${createColumnSections(layout.rightColumn ?? [], sections, resumeConfig)}
    `; }; async function generatePDFFromToml(tomlContent: string): Promise { const resumeData: ResumeData = TOML.parse( tomlContent, ) as unknown as ResumeData; const htmlContent = await generateResumeHTML(resumeData); const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || (process.env.NODE_ENV === "production" ? "/usr/bin/chromium-browser" : undefined), args: [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--disable-web-security", "--disable-features=VizDisplayCompositor", ], }); const page = await browser.newPage(); await page.setContent(htmlContent, { waitUntil: "networkidle" }); const pdfBuffer = await page.pdf({ format: "A4", margin: { top: "0.2in", bottom: "0.2in", left: "0.2in", right: "0.2in", }, printBackground: true, scale: 0.9, }); await browser.close(); return pdfBuffer; } export const GET: APIRoute = async ({ request }) => { try { if (!config.resumeConfig.tomlFile || !config.resumeConfig.tomlFile.trim()) { return new Response("Resume not configured", { status: 404 }); } let tomlContent: string; // Check if tomlFile is a path (starts with /) or raw content if (config.resumeConfig.tomlFile.startsWith("/")) { // It's a file path - fetch it const url = new URL(request.url); const baseUrl = `${url.protocol}//${url.host}`; const response = await fetch(`${baseUrl}${config.resumeConfig.tomlFile}`); if (!response.ok) { throw new Error( `Failed to fetch resume: ${response.status} ${response.statusText}`, ); } tomlContent = await response.text(); } else { // It's raw TOML content tomlContent = config.resumeConfig.tomlFile; } const pdfBuffer = await generatePDFFromToml(tomlContent); return new Response(pdfBuffer, { headers: { "Content-Type": "application/pdf", "Content-Disposition": `attachment; filename="Atridad_Lahiji_Resume.pdf"`, "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0", }, }); } catch (error) { console.error("Error generating PDF:", error); return new Response("Error generating PDF", { status: 500 }); } }; export const POST: APIRoute = async ({ request }) => { try { const tomlContent = await request.text(); if (!tomlContent.trim()) { return new Response("TOML content is required", { status: 400 }); } // Validate TOML content let resumeData: ResumeData; try { resumeData = TOML.parse(tomlContent) as unknown as ResumeData; } catch (parseError) { return new Response( `Invalid TOML format: ${parseError instanceof Error ? parseError.message : "Unknown error"}`, { status: 400 }, ); } // Basic validation if (!resumeData.basics?.name) { return new Response("Resume must include basics.name", { status: 400 }); } const pdfBuffer = await generatePDFFromToml(tomlContent); const filename = `${resumeData.basics.name.replace(/[^a-zA-Z0-9]/g, "_")}_Resume.pdf`; return new Response(pdfBuffer, { headers: { "Content-Type": "application/pdf", "Content-Disposition": `attachment; filename="${filename}"`, "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0", }, }); } catch (error) { console.error("Error generating PDF:", error); return new Response("Error generating PDF", { status: 500 }); } };