Skip to content

feat: add optional profile photo support for DACH CV market#291

Open
andruwa13 wants to merge 25 commits intosantifer:mainfrom
andruwa13:feat/dach-cv-photo
Open

feat: add optional profile photo support for DACH CV market#291
andruwa13 wants to merge 25 commits intosantifer:mainfrom
andruwa13:feat/dach-cv-photo

Conversation

@andruwa13
Copy link
Copy Markdown

@andruwa13 andruwa13 commented Apr 13, 2026

German-speaking market (DACH) expects a Bewerbungsfoto in the top-right corner of the CV. This change adds opt-in photo support:

  • templates/cv-template.html: wrap header in .header-inner flex container, add .cv-photo styles, inject {{PHOTO_BLOCK}} placeholder on the right
  • config/profile.example.yml: add commented-out 'photo:' field with DACH context and instructions
  • modes/pdf.md: add instructions for Claude to embed photo as base64 data URI

Closes #264

Behavior:

  • photo field absent/empty → {{PHOTO_BLOCK}} replaced with '' (no change)
  • photo field set → image embedded as base64, displayed top-right (90×115px)

References:

Testing

  • Created a comprehensive test suite handlePhotoSubstitution.test.mjs that covers:
    • Successful photo embedding (JPG/PNG).
    • Handling of missing configuration or missing photo files.
    • Validation of absolute paths and path traversal security (prevents escaping the project root).
    • File size limits (rejects files > 2MB).
    • Unsupported file formats.
    • Graceful fallback (empty string) when no photo is provided.
  • Verified all tests pass using the main test runner: node test-all.mjs --quick.

Technical Details

The photo is embedded as a base64 Data URI directly into the HTML before it is sent to Playwright. This ensures the PDF is self-contained and avoids issues with local file access in different environments.

User Review Required

None. The feature is opt-in and does not change the layout for users who don't provide a photo.

What does this PR do?

Related issue

Type of change

  • Bug fix
  • New feature
  • Documentation / translation
  • Refactor (no behavior change)

Checklist

  • I have read CONTRIBUTING.md
  • I linked a related issue above (required for features and architecture changes)
  • My PR does not include personal data (CV, email, real names)
  • I ran node test-all.mjs and all tests pass
  • My changes respect the Data Contract (no modifications to user-layer files)
  • My changes align with the project roadmap

Questions? Join the Discord for faster feedback.

Summary by CodeRabbit

  • New Features

    • Optional profile photo in header and PDF exports; a new CLI script can generate localized CV HTML/PDF using it.
  • Documentation

    • Guidance added for enabling the photo, accepted formats, path rules, size limits, embedding, and fallback behavior.
  • Style

    • Header changed to two-column layout; contact-row wrapping improved for better responsiveness.
  • Chores

    • Project version bumped to 1.6.0 and changelog updated.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds optional resume photo support across config example, documentation, template, PDF generation, and a temporary CV generator: introduces {{PHOTO_BLOCK}}, template/CSS for a photo, validation/embedding rules, and logic to base64-embed project-relative images into PDFs or clear the placeholder with warnings.

Changes

Cohort / File(s) Summary
Configuration Example
config/profile.example.yml
Inserted a commented example photo: assets/photo.jpg and guidance for the optional top-level photo field.
PDF Mode Documentation
modes/pdf.md
Added {{PHOTO_BLOCK}} placeholder spec and photo handling rules: project-relative only, no absolute/.. escapes, resolve symlinks within project, allow .jpg/.jpeg/.png, ignore >2 MiB (log + clear), base64-embed accepted images into data: URIs; failures clear placeholder and emit warnings.
Template Markup & Styling
templates/cv-template.html
Reworked header into .header-inner with .header-text and {{PHOTO_BLOCK}}; added flex layout and .cv-photo sizing/cropping (object-fit: cover), updated contact-row markup and overflow/wrapping CSS.
PDF Generation Logic
generate-pdf.mjs
Added handlePhotoSubstitution(html, projectRoot) to read config/profile.yml, validate project-relative path, enforce allowed extensions and ≤2 MiB size, base64-encode image and replace {{PHOTO_BLOCK}} with <img class="cv-photo" src="data:...">; on errors remove placeholder and console.warn; invoked early in generatePDF(); added path/fs/js-yaml imports.
Temporary Generator Script
temp_cv_generator.mjs
New CLI helper that builds localized HTML from cv.md/template/profile, attempts to embed photo (base64) into {{PHOTO_BLOCK}}, writes timestamped temp HTML, and spawns generate-pdf.mjs to produce the PDF.
Release & Changelog
.release-please-manifest.json, CHANGELOG.md
Bumped manifest from 1.5.01.6.0 and added a 1.6.0 changelog entry describing new features and fixes.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as Generator/CLI
    participant Config as Config Loader
    participant FS as File System
    participant Encoder as Base64 Encoder
    participant Renderer as Template Renderer
    participant PDF as PDF Generator

    CLI->>Config: load `config/profile.yml`
    Config-->>CLI: return config (photo path?)
    alt photo present and valid
        CLI->>FS: resolve & read photo (project-relative, resolve symlinks, ensure inside project)
        FS-->>CLI: binary image (if ≤2MiB and allowed ext)
        CLI->>Encoder: base64-encode → data URI
        Encoder-->>Renderer: data URI
        Renderer->>Renderer: replace {{PHOTO_BLOCK}} with <img class="cv-photo" src="data:...">
    else photo absent/invalid/oversize
        Renderer->>Renderer: replace {{PHOTO_BLOCK}} with ""
        CLI->>CLI: console.warn (reason)
    end
    CLI->>Renderer: continue template processing (font-path rewriting, ATS normalization)
    Renderer-->>PDF: rendered HTML
    PDF-->>CLI: output PDF
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically summarizes the main change: adding optional profile photo support for the DACH CV market, which aligns with the core feature across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Welcome to career-ops, @andruwa13! Thanks for your first PR.

A few things to know:

  • Tests will run automatically — check the status below
  • Make sure you've linked a related issue (required for features)
  • Read CONTRIBUTING.md if you haven't

We'll review your PR soon. Join our Discord if you have questions.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modes/pdf.md`:
- Around line 186-190: The fenced HTML code block for the photo example around
the replacement for {{PHOTO_BLOCK}} is missing blank lines before and after
which violates MD031; update the markdown in the "Replace `{{PHOTO_BLOCK}}` in
the template with:" section so there is an empty line directly before the
opening ```html fence and an empty line directly after the closing ``` fence
(i.e., ensure the fenced block for the <img class="cv-photo"...> example is
separated by blank lines from surrounding list items and the subsequent step
about `photo:` being absent).

In `@templates/cv-template.html`:
- Around line 115-136: The header CSS is unused because the header markup
doesn't include the new classes or the photo placeholder; update the header
template to wrap the existing title/contact markup in a container with class
"header-inner", add a photo container element with class "cv-photo" that renders
the template variable "{{PHOTO_BLOCK}}" (or falls back to nothing when empty),
and move the textual parts into an element with class "header-text" so the
styles (.header-inner, .header-text, .cv-photo, and {{PHOTO_BLOCK}}) can apply
and allow photo injection to work.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bd09c424-5e3b-482f-9058-6fa710a6ffb0

📥 Commits

Reviewing files that changed from the base of the PR and between 4b5093a and 8de370b.

📒 Files selected for processing (3)
  • config/profile.example.yml
  • modes/pdf.md
  • templates/cv-template.html

Comment thread modes/pdf.md
Comment thread templates/cv-template.html Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modes/pdf.md`:
- Around line 180-192: The Template HTML placeholder matrix is missing the new
PHOTO_BLOCK introduced for DACH profile photos; update the canonical placeholder
table (the Template HTML section) to add a row documenting {{PHOTO_BLOCK}} with
its semantics: when config/profile.yml has a photo: path it becomes an <img>
data URI (data:image/jpeg|png;base64,...) and when absent it is replaced with an
empty string. Ensure the table entry references {{PHOTO_BLOCK}} and mentions
config/profile.yml as the source and the data URI / empty-string behavior so
documentation and implementation remain in sync.
- Around line 182-192: Update the photo embedding logic that processes
config/profile.yml's photo field and the template placeholder {{PHOTO_BLOCK}} to
explicitly handle unsupported formats: detect the image MIME type (based on file
extension or inspecting bytes), allow only image/jpeg and image/png for base64
data URI embedding, and for any other format (e.g., HEIC, WebP, SVG) log a
deterministic warning and replace {{PHOTO_BLOCK}} with an empty string "";
ensure the code path that builds the data URI (the JPEG/PNG branch) and the
fallback branch are clearly named/located around the photo-processing routine
that populates {{PHOTO_BLOCK}} so reviewers can find the change easily.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c85710d8-1781-4058-9ab3-256519eec6cc

📥 Commits

Reviewing files that changed from the base of the PR and between 8de370b and b7e2ff1.

📒 Files selected for processing (2)
  • modes/pdf.md
  • templates/cv-template.html

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modes/pdf.md`:
- Around line 183-197: The docs describe substituting {{PHOTO_BLOCK}} from
config/profile.yml.photo but no code implements it: update the HTML generation
pipeline (e.g., in the module that reads templates before invoking
generate-pdf.mjs or inside generate-pdf.mjs) to read config/profile.yml,
validate the photo file extension (allow .jpg, .jpeg, .png), and if valid read
and base64-encode the image into a data URI (image/jpeg or image/png); then
replace the {{PHOTO_BLOCK}} placeholder in templates/cv-template.html with <img
class="cv-photo" src="data:..."> before PDF conversion; if the extension is
unsupported or the file is missing/empty log a warning and replace
{{PHOTO_BLOCK}} with an empty string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8853eed6-0868-469b-bec3-55fa8e67ba0e

📥 Commits

Reviewing files that changed from the base of the PR and between b7e2ff1 and bf7fe6c.

📒 Files selected for processing (1)
  • modes/pdf.md

Comment thread modes/pdf.md
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@generate-pdf.mjs`:
- Around line 40-44: The current resolve(projectRoot, photoPath) allows absolute
or ../ traversal to escape the repo; before reading the file (using
existsSync/fullPath), validate the canonical path is contained in projectRoot by
resolving and then checking that path.relative(projectRoot, fullPath) does not
start with '..' and that fullPath is not absolute outside projectRoot (also
guard against null bytes); if the check fails, log a warning and return the
empty PHOTO_BLOCK replacement as you do now. Use the same variables
(projectRoot, photoPath, fullPath, existsSync) and perform this containment
check immediately after computing fullPath and before accessing the filesystem.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4052f488-c1e2-4fc0-b81a-55974aefcb05

📥 Commits

Reviewing files that changed from the base of the PR and between bf7fe6c and a5a63d4.

📒 Files selected for processing (1)
  • generate-pdf.mjs

Comment thread generate-pdf.mjs Outdated
github-actions Bot and others added 8 commits April 14, 2026 19:29
German-speaking market (DACH) expects a Bewerbungsfoto in the top-right
corner of the CV. This change adds opt-in photo support:

- templates/cv-template.html: wrap header in .header-inner flex container,
  add .cv-photo styles, inject {{PHOTO_BLOCK}} placeholder on the right
- config/profile.example.yml: add commented-out 'photo:' field with DACH
  context and instructions
- modes/pdf.md: add instructions for Claude to embed photo as base64 data URI

Behavior:
- photo field absent/empty → {{PHOTO_BLOCK}} replaced with '' (no change)
- photo field set → image embedded as base64, displayed top-right (90×115px)

References:
- https://www.lebenslauf.de/ratgeber/lebenslauf/bewerbungsfoto/
- https://karrierebibel.de/lebenslauf-ohne-foto/
- https://www.experteer.de/magazin/gehoert-ein-foto-auf-den-lebenslauf/
Reads photo from config/profile.yml, validates format (.jpg/.jpeg/.png),
base64-encodes and replaces {{PHOTO_BLOCK}} in the HTML template.
Unsupported formats produce a warning and clean fallback.
Reject absolute paths and ../ traversal sequences before reading photo
from disk. Prevents arbitrary local file disclosure via config/profile.yml.
German-speaking market (DACH) expects a Bewerbungsfoto in the top-right
corner of the CV. This change adds opt-in photo support:

- templates/cv-template.html: wrap header in .header-inner flex container,
  add .cv-photo styles, inject {{PHOTO_BLOCK}} placeholder on the right
- config/profile.example.yml: add commented-out 'photo:' field with DACH
  context and instructions
- modes/pdf.md: add instructions for Claude to embed photo as base64 data URI

Behavior:
- photo field absent/empty → {{PHOTO_BLOCK}} replaced with '' (no change)
- photo field set → image embedded as base64, displayed top-right (90×115px)

References:
- https://www.lebenslauf.de/ratgeber/lebenslauf/bewerbungsfoto/
- https://karrierebibel.de/lebenslauf-ohne-foto/
- https://www.experteer.de/magazin/gehoert-ein-foto-auf-den-lebenslauf/
Reads photo from config/profile.yml, validates format (.jpg/.jpeg/.png),
base64-encodes and replaces {{PHOTO_BLOCK}} in the HTML template.
Unsupported formats produce a warning and clean fallback.
@andruwa13 andruwa13 force-pushed the feat/dach-cv-photo branch from 2f90166 to 11e09ac Compare April 14, 2026 20:11
@andruwa13
Copy link
Copy Markdown
Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@generate-pdf.mjs`:
- Around line 47-52: The containment check using resolve(projectRoot,
trimmedPhotoPath) and relPath currently can be bypassed by symlinks; before
computing relPath or doing the isAbsolute check, call fs.realpathSync on the
resolved fullPath (and optionally on projectRoot) to get canonical paths, then
compute relative(realProjectRoot, realFullPath) (using the same symbols
fullPath, relPath, resolve, relative, isAbsolute, projectRoot, trimmedPhotoPath)
and perform the startsWith('..') / isAbsolute checks against the real paths so
symlinked files outside the project are correctly rejected.

In `@modes/pdf.md`:
- Around line 185-187: The file contains a duplicated sentence "`photo:` must be
a top-level key in `config/profile.yml` (not under `candidate:`)." — remove one
of the duplicates so the statement appears only once; search for that exact
sentence in modes/pdf.md and delete the redundant occurrence, leaving a single
instance.

In `@templates/cv-template.html`:
- Around line 124-146: The CSS rules for .header-inner, .header-text, and
.cv-photo are not indented consistently with the rest of the style block; update
the indentation to use 2-space indentation for each selector block and their
properties (aligning with existing file style) so .header-inner { ... },
.header-text { ... }, and .cv-photo { ... } are reformatted with two spaces
before each property and the opening/closing braces to match the file's CSS
indentation standard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2cbb6276-57a3-49d8-be8c-4ad6f9de83ff

📥 Commits

Reviewing files that changed from the base of the PR and between a5a63d4 and 11e09ac.

📒 Files selected for processing (6)
  • .release-please-manifest.json
  • CHANGELOG.md
  • config/profile.example.yml
  • generate-pdf.mjs
  • modes/pdf.md
  • templates/cv-template.html

Comment thread generate-pdf.mjs
Comment thread modes/pdf.md Outdated
Comment thread templates/cv-template.html Outdated
… and add symlink‑resolution safety for photo handling
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@generate-pdf.mjs`:
- Around line 47-55: The code currently resolves a symlink to realPath but then
continues validation and embedding using trimmedPhotoPath/fullPath, which allows
a symlinked file (e.g., secrets.txt) to bypass the .jpg whitelist; change all
subsequent path-based checks and reads to use realPath (and relPath derived from
realpathSync(projectRoot) as already computed) — specifically use realPath when
extracting the extension, checking whitelist, calling fs.stat/size, reading
bytes, and embedding the file instead of trimmedPhotoPath/fullPath so the
canonical target is validated and read.

In `@modes/pdf.md`:
- Around line 182-199: The doc for "Profile Photo (DACH market)" only covers
file type validation but must also document the runtime constraints enforced by
generate-pdf.mjs: state that photo: must be a relative in-repo path (no absolute
paths or paths that escape the repo) and that files >2 MB are rejected/skipped;
explain that when a file is rejected for being out-of-repo, absolute, or too
large the renderer logs a warning and replaces {{PHOTO_BLOCK}} with an empty
string, and update the bullets under `photo:` handling to include these checks
and the exact data-URI behavior (use image/jpeg or image/png) so the prompt
contract matches generate-pdf.mjs.

In `@templates/cv-template.html`:
- Around line 103-117: The separators (`|`) must not be separate flex children
and should be attached to each contact item; update the markup/templating to
stop emitting standalone separator nodes and instead render the separator as
part of each contact element (e.g. append the separator string inside each
contact link/span except the last), and adjust CSS selectors in .contact-row so
the separator is part of the item and cannot wrap (for example use .contact-row
a:not(:last-child)::after { content: " |"; white-space: nowrap; } or add
.contact-item { white-space: nowrap; display: inline-flex; } and remove any
standalone separator elements), replacing usages that relied on span.separator
and the rule .contact-row span:not(.separator) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a54887b0-ff26-4a86-957c-b36d00cd4ede

📥 Commits

Reviewing files that changed from the base of the PR and between 11e09ac and ed2f79e.

📒 Files selected for processing (3)
  • generate-pdf.mjs
  • modes/pdf.md
  • templates/cv-template.html

Comment thread modes/pdf.md
Comment thread templates/cv-template.html
… and add symlink‑resolution safety for photo handling
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@generate-pdf.mjs`:
- Around line 61-64: Redundant existsSync check: remove the unreachable if
(!existsSync(fullPath)) { ... } block around
html.replace(/\{\{PHOTO_BLOCK\}\}/g, '') since realpathSync(fullPath) already
throws and the earlier error-handling path (the return at lines handling
realpathSync) covers the "file not found" case; also remove any now-unused
existsSync import/require and keep the replacement logic solely in the
realpathSync error branch, referencing fullPath, realpathSync, and the
html.replace('{{PHOTO_BLOCK}}', ...) usage to locate the code to change.

In `@templates/cv-template.html`:
- Line 398: Remove the stray literal "generate-pdf.mjs" text from the template
so it does not render in the output; specifically edit the line that contains
the separator span with class "separator" and the LinkedIn anchor (the anchor
using {{LINKEDIN_URL}} and {{LINKEDIN_DISPLAY}}) to eliminate the extraneous
text and ensure only the separator span and the anchor remain, preserving proper
spacing and markup.
- Line 147: The comment "/* === SECTIONS === */" is not indented consistently
with other CSS section comments; update the template in
templates/cv-template.html so that the comment string /* === SECTIONS === */ is
prefixed with two space characters to match the existing 2-space indentation
pattern used throughout the file (search for other section comments to copy
their indentation style).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ea1f3d3f-abf0-4b49-9c17-a167b91d14c0

📥 Commits

Reviewing files that changed from the base of the PR and between ed2f79e and a9ee538.

📒 Files selected for processing (3)
  • generate-pdf.mjs
  • modes/pdf.md
  • templates/cv-template.html

Comment thread generate-pdf.mjs Outdated
Comment thread templates/cv-template.html Outdated
Comment thread templates/cv-template.html Outdated
…plate markup, remove redundant checks and stray text
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
templates/cv-template.html (2)

396-400: ⚠️ Potential issue | 🟡 Minor

Standalone separator spans can wrap independently, producing orphaned | characters.

When the header width is constrained by the presence of {{PHOTO_BLOCK}}, the flex-wrap behavior on .contact-row may cause a <span class="separator">|</span> to wrap onto its own line, resulting in a line starting or ending with just |.

Consider attaching the separator to each contact item via CSS pseudo-elements instead:

Proposed fix using ::after pseudo-elements
   .contact-row a {
     color: `#555`;
     text-decoration: none;
     white-space: normal;
     overflow-wrap: anywhere;
     word-break: break-word;
   }
+
+  .contact-row > *:not(:last-child)::after {
+    content: " | ";
+    color: `#ccc`;
+  }

-  .contact-row .separator {
-    color: `#ccc`;
-  }

Then remove the <span class="separator">|</span> elements from the markup:

   <div class="contact-row">
-    {{PHONE}}<span class="separator">|</span>
-    <span>{{EMAIL}}</span><span class="separator">|</span>
-    <a href="{{LINKEDIN_URL}}">{{LINKEDIN_DISPLAY}}</a><span class="separator">|</span>
-    <a href="{{PORTFOLIO_URL}}">{{PORTFOLIO_DISPLAY}}</a><span class="separator">|</span>
+    {{PHONE}}
+    <span>{{EMAIL}}</span>
+    <a href="{{LINKEDIN_URL}}">{{LINKEDIN_DISPLAY}}</a>
+    <a href="{{PORTFOLIO_URL}}">{{PORTFOLIO_DISPLAY}}</a>
     <span>{{LOCATION}}</span>
   </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/cv-template.html` around lines 396 - 400, The standalone separator
spans (class "separator") can wrap alone causing orphaned "|" characters; remove
the explicit <span class="separator">|</span> elements from the contact markup
(the lines around {{PHONE}}, {{EMAIL}}, {{LINKEDIN_DISPLAY}},
{{PORTFOLIO_DISPLAY}}, {{LOCATION}} in templates/cv-template.html) and instead
add a CSS-based separator on the contact container (e.g., use the
.contact-row/.contact-item element and apply a ::after pseudo-element to render
the "|" for all but the last item); ensure the pseudo-element uses
display/spacing that stays attached to its item and add a last-child rule to
suppress the trailing separator.

124-146: 🧹 Nitpick | 🔵 Trivial

CSS indentation is inconsistent with the rest of the file.

The new CSS rules for .header-inner, .header-text, and .cv-photo lack the 2-space indentation used throughout the rest of the <style> block.

Proposed fix for consistent indentation
-  
-/* === PROFILE PHOTO (DACH market — optional) === */
-.header-inner {
-  display: flex;
-  align-items: flex-start;
-  gap: 20px;
-}
-
-.header-text {
-  flex: 1;
-  min-width: 0;
-}
-
-.cv-photo {
-  width: 90px;
-  height: 115px;
-  object-fit: cover;
-  object-position: center top;
-  border-radius: 6px;
-  flex-shrink: 0;
-  display: block;
-}
+
+  /* === PROFILE PHOTO (DACH market — optional) === */
+  .header-inner {
+    display: flex;
+    align-items: flex-start;
+    gap: 20px;
+  }
+
+  .header-text {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .cv-photo {
+    width: 90px;
+    height: 115px;
+    object-fit: cover;
+    object-position: center top;
+    border-radius: 6px;
+    flex-shrink: 0;
+    display: block;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/cv-template.html` around lines 124 - 146, The CSS rules for
.header-inner, .header-text, and .cv-photo use no leading spaces and are
inconsistent with the rest of the <style> block; update these selectors and
their rule blocks to use the standard 2-space indentation (match surrounding
rules), ensuring each selector line and each property line is indented by two
spaces so the block for .header-inner, .header-text, and .cv-photo conforms to
the file’s existing styling convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@generate-pdf.mjs`:
- Line 55: The call realpathSync(projectRoot) used to compute relPath can throw
if projectRoot becomes inaccessible; follow the defensive pattern used for
realPath by resolving projectRoot once (e.g., into realProjectRoot) inside a
try-catch and use that value in the relative(realProjectRoot, realPath) call (or
wrap the single realpathSync(projectRoot) call in a try-catch and fall back to
projectRoot or handle the error), updating the relPath assignment to reference
the safe, pre-resolved value; adjust any error logging to include context so
callers know the fallback occurred.

---

Duplicate comments:
In `@templates/cv-template.html`:
- Around line 396-400: The standalone separator spans (class "separator") can
wrap alone causing orphaned "|" characters; remove the explicit <span
class="separator">|</span> elements from the contact markup (the lines around
{{PHONE}}, {{EMAIL}}, {{LINKEDIN_DISPLAY}}, {{PORTFOLIO_DISPLAY}}, {{LOCATION}}
in templates/cv-template.html) and instead add a CSS-based separator on the
contact container (e.g., use the .contact-row/.contact-item element and apply a
::after pseudo-element to render the "|" for all but the last item); ensure the
pseudo-element uses display/spacing that stays attached to its item and add a
last-child rule to suppress the trailing separator.
- Around line 124-146: The CSS rules for .header-inner, .header-text, and
.cv-photo use no leading spaces and are inconsistent with the rest of the
<style> block; update these selectors and their rule blocks to use the standard
2-space indentation (match surrounding rules), ensuring each selector line and
each property line is indented by two spaces so the block for .header-inner,
.header-text, and .cv-photo conforms to the file’s existing styling convention.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8320162a-491c-47ec-a6fe-736ed196b92e

📥 Commits

Reviewing files that changed from the base of the PR and between a9ee538 and a0b47b8.

📒 Files selected for processing (3)
  • generate-pdf.mjs
  • modes/pdf.md
  • templates/cv-template.html

Comment thread generate-pdf.mjs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@temp_cv_generator.mjs`:
- Around line 133-138: There is duplicated photo embedding/security logic
between temp_cv_generator.mjs (the photoBlock replacement using the photoBlock
variable) and generate-pdf.mjs (handlePhotoSubstitution), causing inconsistent
YAML path handling and duplicate validation; remove the photo substitution from
temp_cv_generator.mjs (the conditional that replaces '{{PHOTO_BLOCK}}' using
photoBlock) and let handlePhotoSubstitution in generate-pdf.mjs own all photo
validation/embedding, or alternatively modify the call site in
temp_cv_generator.mjs to pass an explicit flag (e.g., skipPhotoSubstitution)
into the generate-pdf.mjs entry point so handlePhotoSubstitution can skip work
when already done—ensure the YAML photo path used by handlePhotoSubstitution is
the single source of truth and delete the photoBlock replacement code and any
related validation in temp_cv_generator.mjs if you choose the first option.
- Around line 477-480: The try/catch around await import('fs').then(fs =>
fs.promises.mkdir('output', { recursive: true })) currently swallows all errors;
change it to only ignore benign "already exists" cases and surface others by
checking the caught error (e.g., if (!e || e.code !== 'EEXIST') {
console.error('Failed to create output dir:', e); throw e; }) or simply rethrow
non-EEXIST errors; ensure you reference the same import('fs')...mkdir call and
the empty catch block when making this change.
- Around line 54-56: The language detection incorrectly treats Ukrainian ('ua')
as German by including profileData.language?.primary === 'ua' in the isGerman
check; update the logic in the isGerman/lang determination (symbols: isGerman,
lang, profileData.language?.primary) so 'ua' is not mapped to German — either
remove 'ua' from the German branch and let it fall back to English, or add a
separate mapping for Ukrainian (e.g., treat 'ua' as 'uk' or introduce a new
isUkrainian flag and set lang accordingly) so section titles are chosen
correctly.
- Around line 30-52: The photo loading block using profileData.candidate.photo
(photoPath -> pathModule.resolve -> fs.readFile -> base64Photo) must enforce the
same protections as generate-pdf.mjs: reject absolute/outside-project paths by
resolving photoFullPath against a safe base (e.g., process.cwd() or a configured
assets dir) and ensure photoFullPath.startsWith(baseDir) to prevent ../
traversal, validate the file extension against a whitelist (e.g., .png, .jpg,
.jpeg) before reading, check the file size via fs.stat and refuse files larger
than a safe limit (e.g., a few MB) to avoid memory exhaustion, and ensure
fs.readFile is only called after these checks; on any validation failure set
photoBlock = '' and log a clear error.
- Around line 153-194: The custom parseYAML function is brittle; replace it with
js-yaml's loader: add an ES import like "import { load as yamlLoad } from
'js-yaml';" at the top of temp_cv_generator.mjs, remove the parseYAML
implementation, and wherever parseYAML(...) is called return yamlLoad(yamlStr)
(or rename callers to use yamlLoad). Ensure the returned object matches existing
usage (pass { schema: ... } only if needed) and remove any local parseYAML
references to avoid duplication.
- Around line 14-22: The file paths passed to readFile (for
'templates/cv-template.html', 'cv.md', and 'config/profile.yml') are
CWD-relative and should be resolved relative to the script; update the readFile
calls to build absolute paths using the already-defined __dirname (e.g., use
path.join or path.resolve with __dirname) so readFile reads __dirname +
'/templates/cv-template.html', __dirname + '/cv.md', and __dirname +
'/config/profile.yml' when assigning template, cvMarkdown, and profileYaml
respectively.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c83b98bf-3ed2-415e-8c8f-3fa6c3fc9889

📥 Commits

Reviewing files that changed from the base of the PR and between a0b47b8 and 8c4a624.

📒 Files selected for processing (2)
  • generate-pdf.mjs
  • temp_cv_generator.mjs

Comment thread temp_cv_generator.mjs Outdated
Comment thread temp_cv_generator.mjs
Comment thread temp_cv_generator.mjs Outdated
Comment thread temp_cv_generator.mjs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
temp_cv_generator.mjs (3)

54-57: ⚠️ Potential issue | 🟡 Minor

Ukrainian ('ua') incorrectly mapped to German.

Line 55 treats Ukrainian as German, resulting in German section titles (e.g., "Zusammenfassung", "Berufserfahrung") for Ukrainian CVs. This appears unintentional.

Proposed fix
-    const isGerman = profileData.language?.primary === 'de' || profileData.language?.primary === 'ua';
+    const isGerman = profileData.language?.primary === 'de';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@temp_cv_generator.mjs` around lines 54 - 57, The language detection is
mapping Ukrainian ('ua') to German because isGerman checks for 'de' OR 'ua';
update the logic so only 'de' maps to German (change isGerman to check
profileData.language?.primary === 'de' only) and handle Ukrainian explicitly
(e.g., set lang = 'uk' when profileData.language?.primary === 'ua', otherwise
default to 'en'); update the variables isGerman and lang (and any downstream
uses) accordingly to prevent Ukrainian CVs from getting German section titles.

477-480: 🧹 Nitpick | 🔵 Trivial

Empty catch block swallows mkdir errors.

If mkdir fails for reasons other than "already exists" (e.g., permissions), the error is silently swallowed, and the subsequent PDF write will fail with a confusing error.

Proposed fix
     try {
       await import('fs').then(fs => fs.promises.mkdir('output', { recursive: true }));
-    } catch (e) {}
+    } catch (e) {
+      console.warn(`⚠️ Could not create output directory: ${e.message}`);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@temp_cv_generator.mjs` around lines 477 - 480, The empty catch on the output
directory creation swallows real failures; update the try/catch around the
dynamic fs import and the mkdir call (the import('fs').then(fs =>
fs.promises.mkdir('output', { recursive: true })) block) to handle errors
explicitly: in the catch inspect the thrown error and if error.code indicates
"EEXIST" allow it, otherwise log the error (or use console.error/processLogger)
and rethrow so permission or other failures surface instead of being silently
ignored.

153-194: 🛠️ Refactor suggestion | 🟠 Major

Prefer js-yaml over custom parser.

This custom YAML parser won't handle edge cases like multi-line strings, arrays, anchors, or escaped characters. Since generate-pdf.mjs already uses js-yaml, reuse it for consistency and correctness.

♻️ Proposed refactor

Add import at top of file:

import yaml from 'js-yaml';

Replace the custom parser:

-// Helper function to parse simple YAML
-function parseYAML(yamlStr) {
-  const lines = yamlStr.split('\n');
-  const result = {};
-  let currentObj = result;
-  let stack = [result];
-  // ... (40 lines of custom parsing)
-  return result;
-}
+// Use js-yaml for robust parsing
+function parseYAML(yamlStr) {
+  return yaml.load(yamlStr) || {};
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@temp_cv_generator.mjs` around lines 153 - 194, Replace the fragile custom
parseYAML implementation with js-yaml: add "import yaml from 'js-yaml';" at the
top of the file, remove the parseYAML function, and where parseYAML(...) is
called use yaml.load(yamlString) (or yaml.safeLoad if you prefer) to get a JS
object; ensure callers still expect a plain object and handle thrown parse
errors (wrap yaml.load in try/catch or validate before use) so behavior matches
the previous API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@temp_cv_generator.mjs`:
- Around line 133-138: Remove the duplicate PHOTO_BLOCK substitution from
temp_cv_generator.mjs: delete the conditional that replaces '{{PHOTO_BLOCK}}'
using the local photoBlock variable (the block around the photoBlock
replacement) so that generate-pdf.mjs's handlePhotoSubstitution is the single
source of truth for photo handling; ensure no other code in
temp_cv_generator.mjs expects the placeholder to be expanded (if so, adjust
callers to pass the raw placeholder to generate-pdf.mjs) and keep
generate-pdf.mjs's path traversal and size validations intact.
- Around line 30-52: profileData is using the wrong YAML key and the file read
is vulnerable to path traversal; change references from
profileData.candidate.photo to profileData.photo (so the photo value is actually
read), and before resolving/reading photoPath validate and restrict it to a safe
project directory: normalize the input, reject absolute paths or paths
containing ../, resolve against a known base (e.g. project asset dir) and ensure
the resolved photoFullPath starts with that base directory; keep the existing
try/catch and on validation failure set photoBlock = '' and log a clear message.

---

Duplicate comments:
In `@temp_cv_generator.mjs`:
- Around line 54-57: The language detection is mapping Ukrainian ('ua') to
German because isGerman checks for 'de' OR 'ua'; update the logic so only 'de'
maps to German (change isGerman to check profileData.language?.primary === 'de'
only) and handle Ukrainian explicitly (e.g., set lang = 'uk' when
profileData.language?.primary === 'ua', otherwise default to 'en'); update the
variables isGerman and lang (and any downstream uses) accordingly to prevent
Ukrainian CVs from getting German section titles.
- Around line 477-480: The empty catch on the output directory creation swallows
real failures; update the try/catch around the dynamic fs import and the mkdir
call (the import('fs').then(fs => fs.promises.mkdir('output', { recursive: true
})) block) to handle errors explicitly: in the catch inspect the thrown error
and if error.code indicates "EEXIST" allow it, otherwise log the error (or use
console.error/processLogger) and rethrow so permission or other failures surface
instead of being silently ignored.
- Around line 153-194: Replace the fragile custom parseYAML implementation with
js-yaml: add "import yaml from 'js-yaml';" at the top of the file, remove the
parseYAML function, and where parseYAML(...) is called use yaml.load(yamlString)
(or yaml.safeLoad if you prefer) to get a JS object; ensure callers still expect
a plain object and handle thrown parse errors (wrap yaml.load in try/catch or
validate before use) so behavior matches the previous API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 46ec9537-03d6-4152-b3b4-a9efd59a9fbe

📥 Commits

Reviewing files that changed from the base of the PR and between 8c4a624 and 998ef7d.

📒 Files selected for processing (2)
  • temp_cv_generator.mjs
  • templates/cv-template.html

Comment thread temp_cv_generator.mjs
Comment thread temp_cv_generator.mjs Outdated
  - Load photo from top‑level `profileData.photo` with full path validation.
  - Prevent path‑traversal, disallow absolute paths, enforce project‑relative paths, size & format limits.
  - Remove duplicate {{PHOTO_BLOCK}} substitution (delegated to generate-pdf.mjs).
  - Replace custom YAML parser with robust `js-yaml` loader.
  - Add safety warning for output‑directory creation.
  - Adjust language detection (German only for `de`).

  BREAKING CHANGE: Photo field moved from `candidate.photo` to top‑level `photo` in `config/profile.yml`. Existing configs must be updated accordingly.
clmoon2 pushed a commit to clmoon2/career-ops that referenced this pull request Apr 18, 2026
…ensorWave/Ema security interns)

WebSearch sweep across Ashby/Greenhouse/Lever + PolyAI API. Found 4 new security-domain internship
listings added to pipeline. Together AI Security Intern URL rediscovered — already evaluated santifer#334
(4.5/5, April 10); supplemental report santifer#378 written since original santifer#291 is missing from disk.
SpeedyApply + SimplifyJobs: 0 new qualifying today.

Pipeline additions: Obsidian Security SWE Intern (~3.7), Symmetry Systems Full Stack (~3.6),
TensorWave Security Intern (~3.6), Ema Security Intern (~3.5).

URGENT: Anthropic Fellows deadline April 26 (8 days). Together AI Security Intern apply if pending.

https://claude.ai/code/session_01HP9N7L3xyLXiaJqCNscfKw
@andruwa13 andruwa13 force-pushed the feat/dach-cv-photo branch 2 times, most recently from c911e42 to e2bec30 Compare April 29, 2026 18:09
deepak-glitch pushed a commit to deepak-glitch/career-ops that referenced this pull request Apr 29, 2026
…Fs, archived 7 below-threshold)

- Scan: scan.mjs Level 1/2 + Level 3 WebSearch (Greenhouse FDE/AI Engineer,
  Ashby Applied AI/FDE, Workable FDE, Himalayas Junior); +10 new URLs
  appended to data/pipeline.md and data/scan-history.tsv.
- Pipeline (santifer#286-santifer#294): 9 evaluations across Sezzle II (LATAM-only),
  Canopy Connect FDE (3.2/5 PDF), Caylent FDE (Canada Senior no-spons),
  Prelude FDE (Sr endpoint security), Silent Eight Jr FDE (Poland-only),
  InnovationTeam Jr CV (India-only Senior), Innovaccer FDE AI (3.4/5 PDF
  - direct healthcare archetype + domain fit), Anthropic Paris FDE
  (native French), Sayari CE FDE (Senior). Zapier closed (substituted IDs
  for Prelude/Sayari where original Greenhouse/Ashby IDs returned 404/null).
- 2 PDFs generated for score >= 3.0 (Canopy Connect, Innovaccer);
  others marked 'Not generated (score < 3.0)' per PDF policy.
- merge-tracker.mjs: +7 added (skipped Sezzle/Anthropic — existing higher-
  score entries already tracked).
- verify-pipeline.mjs: 0 errors / 0 warnings.
- cleanup-low-scores.mjs: archived 5 reports under reports/below-threshold/
  (santifer#288 Caylent, santifer#289 Prelude, santifer#290 Silent Eight, santifer#291 InnovationTeam,
  santifer#294 Sayari) + 2 orphan reports (santifer#286 Sezzle, santifer#293 Anthropic Paris).

https://claude.ai/code/session_overnight_2026-04-29_T2026-04-29T20:39Z
@Qodo-Free-For-OSS
Copy link
Copy Markdown

Hi, temp_cv_generator.mjs writes generated HTML to /tmp/..., which fails on non-Unix environments and some restricted runtimes. This makes the script non-portable and can crash even with valid input data.

Severity: remediation recommended | Category: reliability

How to fix: Use os.tmpdir() or output/

Agent prompt to fix - you can give this to your LLM of choice:

Issue description

Hardcoding /tmp breaks portability and can cause failures.

Issue Context

temp_cv_generator.mjs currently writes HTML to /tmp/cv-...html.

Fix Focus Areas

  • temp_cv_generator.mjs[154-160]

Suggested change

  • Use os.tmpdir() for temp files, or write to output/ under the project root.
  • Ensure the destination directory exists before writeFile.

Spotted by Qodo code review - free for open-source projects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Photo support in CV template for DACH / European job market

2 participants