santifer/career-ops · error
Template missing required placeholders: }}`).join(', ')}
Error message
Template ${fileFor(chosen)} missing required placeholders: ${v.missing.map((m) => `{{${m}}}`).join(', ')} What it means
For HTML templates, resolveTemplate() runs validateTemplate(), which reads the file and checks it literally contains each required placeholder token: {{NAME}}, {{EXPERIENCE}}, {{EDUCATION}} for kind 'cv'; {{NAME}}, {{ROLE_TITLE}}, {{OPENING}} for kind 'cover'. The check is a plain substring match for the exact uppercase `{{TOKEN}}` spelling — any missing token is listed in the error. TeX templates (format: 'tex') skip this validation entirely, so the throw only happens on the html path.
Solutions
- Add each missing token listed in the message verbatim, uppercase, with double braces (e.g. {{EXPERIENCE}}) at the right spot in the template body
- Copy templates/cv-template.html / cover-letter-template base file and restyle around its existing placeholders instead of starting from a foreign layout
- Pre-check custom templates with validateTemplate(path, kind) — it returns { ok, missing } without throwing
- If the token set genuinely changed for your fork, update the `required` array for that kind in KINDS (system-layer change)
Example fix
<!-- before -->
<h1>Jane Doe</h1>
<section class="jobs">…hard-coded…</section>
<!-- after -->
<h1>{{NAME}}</h1>
<section class="jobs">{{EXPERIENCE}}</section>
<section class="edu">{{EDUCATION}}</section> Defensive patterns
Strategy: validation
Validate before calling
import { validateTemplate, KINDS } from './cv-templates.mjs';
const v = validateTemplate('templates/cv-template.fancy.html', 'cv');
if (!v.ok) {
throw new Error(`custom template lacks placeholders: ${v.missing.join(', ')}`);
} Type guard
import { validateTemplate } from './cv-templates.mjs';
const templateIsValid = (path, kind) => validateTemplate(path, kind).ok; Try / catch
try {
resolveTemplate('cv', 'fancy');
} catch (err) {
if (err.message.includes('missing required placeholders')) {
// fall back to standard (which always has them) while the custom one is fixed
resolveTemplate('cv', 'standard');
} else throw err;
} Prevention
- Start custom HTML from the shipped template so required tokens survive the restyle
- Run validateTemplate() in a repo lint/test step over every templates/*.html so drift is caught at commit time, not render time
- Keep tokens exactly uppercase with double braces — the check is literal substring matching, not a template engine
When it happens
Trigger: A custom cv-template.custom.html that renders experience with a different token like {{JOBS}} or {{experience}} (lowercase); a cover-letter template missing {{ROLE_TITLE}}; placeholders renamed when a designer handed over the HTML; template edited through a tool that HTML-escaped the double braces ({{ → {{).
Common situations: Bringing in a NiceCV/Canva-exported HTML layout that never had career-ops tokens; casing drift after hand-editing; someone 'cleaning up unused variables' from the template; copying a cover template as the base for a cv (each kind requires a different token 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
- Template missing required placeholders: }}`).join(', ')}
- 4dayweek: invalid URL
- 4dayweek: URL must use HTTPS
- a 40-hex commit --sha is required
- apify: entry has invalid field_map. Each of title, url…
AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20).
Data as JSON: /api/errors/bb331937ea8dfce5.
Report an issue: GitHub.
Appendix: source
Thrown at cv-templates.mjs:154
const explicit = Boolean(name && String(name).trim());
let chosen = kebab(explicit ? name : loadProfileDefault(kind, { profilePath }) || 'standard');
const fileFor = (n) => (n === 'standard' ? `${cfg.prefix}.${format}` : `${cfg.prefix}.${n}.${format}`);
let path = resolve(dir, fileFor(chosen));
if (!existsSync(path)) {
if (fallback && chosen !== 'standard') {
chosen = 'standard';
path = resolve(dir, fileFor(chosen));
}
if (!existsSync(path)) {
throw new Error(`Template not found for kind=${kind} name=${chosen} (${fileFor(chosen)})`);
}
}
if (format === 'html') {
const v = validateTemplate(path, kind);
if (!v.ok) {
throw new Error(
`Template ${fileFor(chosen)} missing required placeholders: ${v.missing.map((m) => `{{${m}}}`).join(', ')}`
);
}
}
return path;
}
// ---- CLI ----
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(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 60398d6549)