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>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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');
|
||||
})();
|
||||
Reference in New Issue
Block a user