vuejs/vue · error · Error

Content placeholder not found in template.

Error message

Content placeholder not found in template.

What it means

Thrown by parseTemplate when the template HTML does not contain the content placeholder (default: <!--vue-ssr-outlet-->). The placeholder marks where rendered app content is injected between neck and tail. Without it the template cannot be split into head/neck/tail sections. parseTemplate is called once during TemplateRenderer construction when a string template is provided.

Source

Thrown at packages/server-renderer/src/template-renderer/parse-template.ts:25

export type ParsedTemplate = {
  head: (data: any) => string
  neck: (data: any) => string
  tail: (data: any) => string
}

export function parseTemplate(
  template: string,
  contentPlaceholder: string = '<!--vue-ssr-outlet-->'
): ParsedTemplate {
  if (typeof template === 'object') {
    return template
  }

  let i = template.indexOf('</head>')
  const j = template.indexOf(contentPlaceholder)

  if (j < 0) {
    throw new Error(`Content placeholder not found in template.`)
  }

  if (i < 0) {
    i = template.indexOf('<body>')
    if (i < 0) {
      i = j
    }
  }

  return {
    head: compile(template.slice(0, i), compileOptions),
    neck: compile(template.slice(i, j), compileOptions),
    tail: compile(template.slice(j + contentPlaceholder.length), compileOptions)
  }
}

View on GitHub (pinned to 9e88707940)

Solutions

  1. Add <!--vue-ssr-outlet--> inside the <body> of the template HTML where app content should appear.
  2. If using a custom placeholder, pass it as the contentPlaceholder argument to parseTemplate (note: createRenderer does not expose this, so the default marker is effectively required).
  3. Ensure HTML minifiers preserve comments or run after the template is parsed.
  4. If the template is an object/function (programmatic), parseTemplate returns it as-is and this check is skipped.

Example fix

<!-- before -->
<!DOCTYPE html>
<html>
  <body>
    <div id="app"></div>
  </body>
</html>

<!-- after -->
<!DOCTYPE html>
<html>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>
Defensive patterns

Strategy: validation

Validate before calling

const PLACEHOLDER = '<!--vue-ssr-outlet-->'

function templateHasPlaceholder(template: string, placeholder = PLACEHOLDER): boolean {
  return template.indexOf(placeholder) >= 0
}

if (typeof options.template === 'string' && !templateHasPlaceholder(options.template)) {
  throw new Error(
    `SSR template must contain ${PLACEHOLDER} where app content should be injected.`
  )
}
createRenderer(options)

Type guard

function isValidSsrTemplate(t: unknown): boolean {
  return typeof t === 'string' && t.includes('<!--vue-ssr-outlet-->')
}

Try / catch

try {
  createRenderer({ template })
} catch (e) {
  if (e.message.includes('Content placeholder not found')) {
    template = template.replace('<div id="app"></div>', '<!--vue-ssr-outlet-->')
    createRenderer({ template })
  } else {
    throw e
  }
}

Prevention

When it happens

Trigger: Passing createRenderer a template string that lacks the <!--vue-ssr-outlet--> marker (or whatever custom contentPlaceholder was configured). The indexOf check returns -1 and the error fires immediately at renderer creation.

Common situations: Copy-pasting an HTML shell from a non-Vue project that omits the placeholder. Renaming or removing the placeholder by mistake. Using {{{app}}} or {{content}} (other frameworks' syntax) instead of the Vue SSR marker. A minifier or HTML processor stripped HTML comments including the placeholder.

Related errors


AI-assisted analysis of vuejs/vue@9e88707940 (2026-08-11). Data as JSON: /api/errors/68c864ba2d940cae. Report an issue: GitHub.