getgrav/grav · error · Twig\Error\SyntaxError
Expected endblock for block "%s" (but "%s" given).
Error message
Expected endblock for block "%s" (but "%s" given).
What it means
Grav's bundled Twig deferred extension supplies the block token parser used here. After parsing a block body, it accepts an optional name after {% endblock %} and verifies that it matches the opening block name; a mismatch raises Twig SyntaxError with both names and the source line.
Source
Thrown at system/src/Twig/DeferredExtension/DeferredTokenParser.php:52
$deferred = $stream->nextIf(Token::NAME_TYPE, 'deferred');
if ($deferred) {
$block = new DeferredBlockNode($name, new EmptyNode(), $lineno);
} else {
$block = new BlockNode($name, new EmptyNode(), $lineno);
}
$this->parser->setBlock($name, $block);
$this->parser->pushLocalScope();
$this->parser->pushBlockStack($name);
if ($stream->nextIf(Token::BLOCK_END_TYPE)) {
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
if ($token = $stream->nextIf(Token::NAME_TYPE)) {
$value = $token->getValue();
if ($value != $name) {
throw new SyntaxError(\sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
} else {
$body = new Nodes([
new PrintNode($this->parser->parseExpression(), $lineno),
]);
}
$stream->expect(Token::BLOCK_END_TYPE);
$block->setNode('body', $body);
$this->parser->popBlockStack();
$this->parser->popLocalScope();
return new BlockReferenceNode($name, $lineno, $this->getTag());
}
public function decideBlockEnd(Token $token): bool
{View on GitHub (pinned to 6040efed04)
Solutions
- Change the name after {% endblock %} to exactly match the opening block.
- Remove the optional name and use plain {% endblock %}, which this parser accepts.
- Search the whole template chain, includes, and embedded strings for both names reported in the message.
- Run twig lint or compile templates in CI after large template refactors.
Example fix
// before
{% block hero %}
<h1>{{ page.title }}</h1>
{% endblock intro %}
// after
{% block hero %}
<h1>{{ page.title }}</h1>
{% endblock hero %}
{# or simply: {% endblock %} #} Defensive patterns
Strategy: validation
Validate before calling
$source = file_get_contents($templateFile);
preg_match_all('/{%\s*(block|endblock)\b([^%}]*)%}/', $source, $matches, PREG_SET_ORDER);
$stack = [];
foreach ($matches as $tag) {
if ($tag[1] === 'block') {
preg_match('/([A-Za-z_]\w*)/', $tag[2], $name);
$stack[] = $name[1] ?? null;
} elseif ($stack) {
preg_match('/([A-Za-z_]\w*)/', $tag[2], $name);
$expected = array_pop($stack);
if (($name[1] ?? $expected) !== $expected) {
throw new InvalidArgumentException("endblock name does not match block {$expected}");
}
}
} Try / catch
try {
$twig->load($templateName);
} catch (SyntaxError $e) {
return ['error' => 'Template syntax error: ' . $e->getMessage(), 'line' => $e->getLine()];
} Prevention
- Run twig lint on templates in CI.
- Rename opening and closing tags together.
- Prefer unnamed {% endblock %} when names add no value.
- Review template merges for changed block names.
- Compile page templates after bulk find-and-replace operations.
When it happens
Trigger: Render a template containing {% block hero %}...{% endblock old_hero %}. The parser reaches DeferredTokenParser.php:48-53, sees endblock's name differs from hero, and throws before the template can compile.
Common situations: Copying and renaming a block but forgetting its endblock, merge conflicts that pair mismatched tags, nested block editing, and generated templates whose names are interpolated incorrectly.
Related errors
- Not Implemented
- No backups defined...
- Backup location: {$backup_root} does not exist...
- Invalid backup location: {$backup_root}
- Backup location not allowed (outside site root): {$backup_ro
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/851ee6a06fcde7db.
Report an issue: GitHub.