Files
cesnimda 18c556bd54
CI / quality (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / lighthouse (push) Has been cancelled
CI / relay (push) Has been cancelled
CI / images (push) Has been cancelled
feat: ATS-safe CV rework (EN + NO), replacing scrambled two-column PDFs
- single-column, real-text PDFs: standard headings (Summary, Skills, Experience,
  Projects, Education), consistent dates, no letter-spacing
- 'Systems Developer, eight years experience' (drops 'mid-level'); apprentice->developer
  progression shown; Projects section added; Norwegian written natively (errors fixed)
- one page each, ~8 kB (was 132/172 kB); verified clean ATS text extraction + reading order
- generator committed to tools/cv (content.js + pdfkit/docx builders) for reproducibility
- editable .docx kept alongside; site file-size labels updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 07:22:57 +02:00

108 lines
3.5 KiB
JavaScript

const fs = require('fs');
const PDFDocument = require('pdfkit');
// Shared content (mirrors build-cv.js).
const { EN, NO } = require('./content.js');
const PAGE_W = 595.28;
const MARGIN = 54;
const CW = PAGE_W - MARGIN * 2; // content width
const INK = '#1a1a1a';
const GRAY = '#555555';
const RULE = '#b0b0b0';
function build(c, outfile) {
const doc = new PDFDocument({ size: 'A4', margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN } });
doc.pipe(fs.createWriteStream(outfile));
const rule = (gap = 4) => {
const y = doc.y + gap;
doc.moveTo(MARGIN, y).lineTo(MARGIN + CW, y).lineWidth(0.75).strokeColor(RULE).stroke();
doc.y = y + gap + 2;
};
const heading = (text) => {
doc.moveDown(0.5);
doc.fillColor(INK).font('Helvetica-Bold').fontSize(12).text(text, MARGIN, doc.y);
rule(2);
};
const bullet = (text, prefix) => {
const y = doc.y;
doc.fillColor(INK).font('Helvetica').fontSize(10).text('•', MARGIN + 2, y, { width: 10, lineBreak: false });
const tx = MARGIN + 16;
const tw = CW - 16;
if (prefix) {
doc.font('Helvetica-Bold').text(prefix, tx, y, { width: tw, continued: true });
doc.font('Helvetica').text(text, { width: tw });
} else {
doc.font('Helvetica').text(text, tx, y, { width: tw });
}
doc.moveDown(0.15);
};
const roleLine = (left, dates) => {
doc.moveDown(0.3);
const y = doc.y;
doc.font('Helvetica').fontSize(9.5).fillColor(GRAY).text(dates, MARGIN, y, { width: CW, align: 'right', lineBreak: false });
doc.font('Helvetica-Bold').fontSize(10.5).fillColor(INK).text(left, MARGIN, y, { width: CW - 90, lineBreak: false });
doc.y = y + 14;
};
// Header
doc.fillColor(INK).font('Helvetica-Bold').fontSize(20).text(c.name, MARGIN, MARGIN);
doc.font('Helvetica').fontSize(12).fillColor('#333').text(c.title, { paragraphGap: 2 });
doc.font('Helvetica').fontSize(9.5).fillColor(GRAY)
.text(`${c.location} · ${c.email} · ${c.phone}`);
doc.text('cesnimda.co.uk · cesnimda.co.uk/Linkedin');
rule(4);
// Summary
heading(c.t.summary);
doc.font('Helvetica').fontSize(10).fillColor(INK).text(c.summary, MARGIN, doc.y, { width: CW, align: 'justify' });
// Skills
heading(c.t.skills);
for (const s of c.skills) {
const y = doc.y;
doc.font('Helvetica-Bold').fontSize(10).fillColor(INK).text(`${s.group}: `, MARGIN, y, { width: CW, continued: true });
doc.font('Helvetica').text(s.items, { width: CW });
doc.moveDown(0.15);
}
// Experience
heading(c.t.experience);
for (const e of c.experience) {
roleLine(`${e.role}${e.org}`, e.dates);
if (e.note) {
doc.font('Helvetica-Oblique').fontSize(9.5).fillColor(GRAY).text(e.note, MARGIN, doc.y, { width: CW });
doc.moveDown(0.1);
}
for (const b of e.bullets) bullet(b);
}
// Earlier roles
doc.moveDown(0.4);
doc.font('Helvetica-Bold').fontSize(10).fillColor(INK).text(c.t.earlier, MARGIN, doc.y, { width: CW });
doc.moveDown(0.1);
for (const r of c.earlier) bullet(r);
// Projects
heading(c.t.projects);
for (const p of c.projects) bullet(p.desc, `${p.name}`);
// Education
heading(c.t.education);
roleLine(c.education.qual, c.education.dates);
doc.font('Helvetica').fontSize(10).fillColor(INK).text(c.education.org, MARGIN, doc.y, { width: CW });
doc.end();
return new Promise((res) => doc.on('end', res));
}
(async () => {
await build(EN, 'connor-babbington-cv-en.pdf');
await build(NO, 'connor-babbington-cv-no.pdf');
console.log('wrote EN + NO pdf');
})();