repos

isaacbythewood.com-nextjs

mirror archived upstream

Animation-heavy personal portfolio on Next.js: a custom cursor, a page-transition loader, and canvas backgrounds.

canvas-animationscss-modulesdark-themehandcodednextjspersonal-websiteportfolioreact

3.7 KB · 132 lines · JavaScript Raw History
  1const fs = require("fs");
  2const path = require("path");
  3const { chromium } = require("playwright");
  4const { marked } = require("marked");
  5const matter = require("gray-matter");
  6
  7const RESUME_DIR = __dirname;
  8const CONTENT_PATH = path.join(RESUME_DIR, "content.md");
  9const TEMPLATE_PATH = path.join(RESUME_DIR, "template.html");
 10const OUTPUT_PATH = path.join(
 11  RESUME_DIR,
 12  "..",
 13  "public",
 14  "static",
 15  "pdfs",
 16  "resume-isaac-bythewood.pdf"
 17);
 18
 19function renderMarkdownBody(markdown) {
 20  const renderer = new marked.Renderer();
 21
 22  renderer.heading = function ({ tokens, depth }) {
 23    const text = this.parser.parseInline(tokens);
 24    if (depth === 2) {
 25      const note = text.includes("Experience") || text.includes("Projects")
 26        ? ' <span class="section-note">— See LinkedIn for more</span>'
 27        : "";
 28      return `<h2>${text}${note}</h2>`;
 29    }
 30    if (depth === 3) {
 31      return `<h3>${text}</h3>`;
 32    }
 33    return `<h${depth}>${text}</h${depth}>`;
 34  };
 35
 36  renderer.paragraph = function ({ tokens }) {
 37    const text = this.parser.parseInline(tokens);
 38    if (
 39      text.match(
 40        /^[A-Z].*\u2014\s*(JANUARY|FEBRUARY|MARCH|APRIL|MAY|JUNE|JULY|AUGUST|SEPTEMBER|OCTOBER|NOVEMBER|DECEMBER)/i
 41      )
 42    ) {
 43      return `<div class="meta">${text}</div>`;
 44    }
 45    return `<p>${text}</p>`;
 46  };
 47
 48  marked.setOptions({ renderer });
 49
 50  // Parse markdown, then wrap each h3 + meta + ul group in an entry div
 51  let html = marked.parse(markdown);
 52  html = html.replace(
 53    /(<h3>[\s\S]*?)(?=<h3>|<h2>|$)/g,
 54    '<div class="entry">$1</div>'
 55  );
 56  return html;
 57}
 58
 59function buildHtml(frontmatter, body) {
 60  let template = fs.readFileSync(TEMPLATE_PATH, "utf-8");
 61
 62  const markdownHtml = renderMarkdownBody(body);
 63
 64  const linksHtml = frontmatter.links
 65    .map((l) => `<a href="${l.url}">${l.label}</a>`)
 66    .join("\n");
 67
 68  const skillsHtml = frontmatter.skills
 69    .map((s) => `<li>${s}</li>`)
 70    .join("\n");
 71
 72  const techHtml = frontmatter.technologies
 73    .map((t) => `<li>${t}</li>`)
 74    .join("\n");
 75
 76  const eduHtml = frontmatter.education
 77    .map(
 78      (e) =>
 79        `<div class="education-entry"><span class="school">${e.institution},</span> ${e.years}<br>${e.degree}</div>`
 80    )
 81    .join("\n");
 82
 83  template = template
 84    .replace(/\{\{name\}\}/g, frontmatter.name)
 85    .replace(/\{\{title\}\}/g, frontmatter.title)
 86    .replace(/\{\{summary\}\}/g, frontmatter.summary)
 87    .replace(/\{\{location\}\}/g, frontmatter.location)
 88    .replace(/\{\{phone\}\}/g, frontmatter.phone)
 89    .replace(/\{\{email\}\}/g, frontmatter.email)
 90    .replace(/\{\{links\}\}/g, linksHtml)
 91    .replace(/\{\{skills\}\}/g, skillsHtml)
 92    .replace(/\{\{technologies\}\}/g, techHtml)
 93    .replace(/\{\{education\}\}/g, eduHtml)
 94    .replace(/\{\{body\}\}/g, markdownHtml);
 95
 96  return template;
 97}
 98
 99async function generatePdf() {
100  const raw = fs.readFileSync(CONTENT_PATH, "utf-8");
101  const { data: frontmatter, content: body } = matter(raw);
102  const html = buildHtml(frontmatter, body);
103
104  const launchOptions = {};
105
106  // Use system Chromium if PLAYWRIGHT_CHROMIUM_PATH is set (e.g. in Alpine Docker)
107  if (process.env.PLAYWRIGHT_CHROMIUM_PATH) {
108    launchOptions.executablePath = process.env.PLAYWRIGHT_CHROMIUM_PATH;
109  }
110
111  const browser = await chromium.launch(launchOptions);
112  const page = await browser.newPage({
113    viewport: { width: 816, height: 1056 },
114  });
115  await page.setContent(html, { waitUntil: "networkidle" });
116
117  await page.pdf({
118    path: OUTPUT_PATH,
119    format: "Letter",
120    printBackground: true,
121    margin: { top: 0, right: 0, bottom: 0, left: 0 },
122  });
123
124  await browser.close();
125  console.log(`Resume PDF generated: ${OUTPUT_PATH}`);
126}
127
128generatePdf().catch((err) => {
129  console.error("Failed to generate resume:", err);
130  process.exit(1);
131});