santifer/career-ops · error

Template missing required placeholders: }}`).join(', ')}

Error message

Template ${where} missing required placeholders: ${v.missing.map((m) => `{{${m}}}`).join(', ')}

What it means

resolveTemplate validates that HTML CV templates contain all required placeholders (e.g. {{name}}, {{experience}}) before returning the template path. If a template file passes initial resolution but lacks any placeholder the CV generator will substitute, validation fails and this error is thrown, naming the actual file (pack-qualified when the template comes from a pack). It exists to fail fast at template-resolution time instead of producing a silently broken CV with unsubstituted tokens.

Solutions

  1. Open the file named in the error message and re-add the listed {{placeholder}} tokens exactly as shown (they are rendered verbatim in the message).
  2. If you don't need a custom design, restore the stock templates/cv-template.html (git checkout templates/cv-template.html or re-copy from templates/).
  3. If the template comes from a pack, update the pack or fix the pack-qualified file `<pack>/<fileFor(chosen)>` named in the error.
  4. If the placeholder list itself is wrong for your use case, update the required-placeholder definition in validateTemplate rather than the template.
  5. As a last resort, set format to 'latex'/'tex' path if your template is a .tex file, since validation only runs for format === 'html'.

Example fix

// before: template missing the contact block after a redesign edit
<div class="experience">{{experience}}</div>

// after: restore the required placeholders the validator expects
<div class="header">
  <h1>{{name}}</h1>
  <p>{{contact}}</p>
</div>
<div class="experience">{{experience}}</div>
Defensive patterns

Strategy: validation

Validate before calling

import { validateTemplate } from './cv-templates.mjs';
const v = validateTemplate(templatePath, kind);
if (!v.ok) {
  console.error(`Fix placeholders before generating: ${v.missing.map(m => `{{${m}}}`).join(', ')}`);
  process.exit(1);
}

Type guard

function templateHasPlaceholders(html, required) {
  return required.every(p => html.includes(`{{${p}}}`));
}

Try / catch

try {
  const path = await resolveTemplate({ format: 'html', kind });
} catch (err) {
  if (err.message.includes('missing required placeholders')) {
    console.error('Template is missing tokens; restore stock template or re-add:', err.message);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling resolveTemplate with format === 'html' (directly or via CV generation) where validateTemplate(path, kind) returns {ok:false, missing:[...]} for the chosen template — i.e. a hand-edited, custom, or pack-shipped HTML template that dropped one or more {{placeholder}} tokens, or renamed one to something the generator does not recognize.

Common situations: Users customize templates/cv-template.html and accidentally delete a section wrapper containing a placeholder; a third-party CV pack's HTML file was edited or is from an older template contract version; a template was converted from the LaTeX variant (different placeholder names) and used as HTML; a rename inside the template didn't match the generator's expected placeholder set.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/dae413dd6f2d6547. Report an issue: GitHub.

Appendix: source

Thrown at cv-templates.mjs:272

  // Every by-name caller lands here — build-cv-latex.mjs, generate-cover-letter.mjs.
  const found = discover(kind, { dir, format });

  let entry = found.get(chosen);
  if (!entry && fallback && chosen !== 'standard') {
    chosen = 'standard';
    entry = found.get(chosen);
  }
  if (!entry) {
    throw new Error(`Template not found for kind=${kind} name=${chosen} (${fileFor(chosen)})`);
  }
  const path = entry.path;
  if (format === 'html') {
    const v = validateTemplate(path, kind);
    if (!v.ok) {
      // Name the file that is actually short, not the flat filename it would
      // have had. For a pack these differ, and the flat name points at nothing.
      const where = entry.pack ? `${entry.pack}/${fileFor(chosen)}` : fileFor(chosen);
      throw new Error(
        `Template ${where} missing required placeholders: ${v.missing.map((m) => `{{${m}}}`).join(', ')}`
      );
    }
  }
  return path;
}

// ---- CLI ----
const isMain = isMainModule(import.meta.url);
if (isMain) {
  const argv = process.argv.slice(2);
  const cmd = argv[0];
  const kind = argv[1];
  const flags = Object.fromEntries(
    argv.filter((a) => a.startsWith('--')).map((a) => {
      const [k, v] = a.replace(/^--/, '').split('=');
      return [k, v ?? true];
    })

View on GitHub (pinned to aac998c7ed)