Templating Compared: Django Templates/Jinja2, ERB, Blade, EJS/Pug & JSX
Web Framework Internals
Chapter 5 ยท Templating Compared: Django Templates/Jinja2, ERB, Blade, EJS/Pug & JSX
Chapter 4 built one template engine by hand and ran directly into two real design questions: what should happen to a value the moment it's inserted into HTML, and how should a parsed template actually get reused across renders. This chapter takes those exact same questions to seven real, independently-built systems — Django Templates, Jinja2, ERB, Blade, EJS, Pug, and JSX — and checks, directly against each project's own documentation, how each one really answers them.
The Same Value, Seven Real Escaping Answers
Every system below faces the identical decision Chapter 4 built SafeString to solve: escape a value by default, and require something visually distinct to opt out. The syntax differs completely from system to system — the underlying decision, verified directly against each one's own documentation, does not.
safe filter… Think of safe as shorthand for safe from further escaping."
dangerouslySetInnerHTML with a warning built into the prop's own name: "This is dangerous… you must exercise extreme caution!" — then demonstrate exactly why, using a real, worked example: a blog post's own stored content field containing <img src="" onerror='alert("you were hacked")'>, passed straight into dangerouslySetInnerHTML. Per React's own docs: "The code embedded in the HTML will run. A hacker could use this security hole to steal user information or to perform actions on their behalf" — the exact same real vulnerability class Chapter 4 built and closed by hand, independently confirmed here in a completely different language and framework.
| System | Escaped by default | Explicit opt-out | Real mechanism underneath |
|---|---|---|---|
| Django | {{ var }} | {{ var|safe }} | Replaces 5 HTML-significant characters, per Django's own docs |
| Jinja2 | {{ var }} | {{ var|safe }} | Same filter name and surface syntax as Django, autoescaping toggled per environment |
| ERB / Rails | <%= var %> | raw(var) / var.html_safe | A Rails/ActionView addition — see the section below |
| Blade | {{ $var }} | {!! $var !!} | PHP's own htmlspecialchars(), per Laravel's own docs |
| EJS | <%= var %> | <%- var %> | "HTML escaped" vs. "unescaped", per EJS's own docs, verbatim |
| Pug | #{var} | !{var} | A pound sign ("safe") vs. a bang ("danger"), per Pug's own docs |
| JSX | {var} | dangerouslySetInnerHTML | React's compiler escapes every {} expression by default |
Compile-to-Code, Two More Real Ways: Blade→PHP, ERB→Ruby
Chapter 4 verified one real system that compiles a parsed template into actual, executable host-language source code — Jinja2, into Python. Checking Blade and ERB's own real documentation directly turns up two more:
Ruby's own standard-library ERB class goes further still — its real, public .src method hands back the actual generated Ruby source a given template compiles to, letting this exact claim be checked directly rather than taken on faith. For the template 'The time is <%= Time.now %>.', Ruby's own documentation shows the real compiled result:
_erbout) — the exact same accumulator pattern Chapter 4's own compile_to_python() used for its __out list, now confirmed in a second, completely different host language. But the interpolated value itself is only ever passed through .to_s — never through anything resembling html.escape(). Plain ERB, verified from its own real compiled output, does not escape by default at all. The "ERB / Rails" row in this chapter's own comparison table above is real, but it isn't a property of ERB itself — it's something Rails' own ActionView layer adds around ERB's output tag, specifically for use inside a Rails application. Reach for plain ERB outside of Rails and the default behavior is exactly Chapter 4's original, vulnerable render() function from the start of that chapter, not the safe one it was rebuilt into.
Django's Deliberate Choice Not to Compile
Three real systems verified so far — Jinja2, Blade, ERB — all compile a parsed template into real, executable host-language code. Django, verified in Chapter 4 as walking a real Node tree instead, is the outlier among the four. Django's own documentation gives a real, direct reason, and it isn't about speed:
Jinja2's own documentation describes the real, direct consequence of not sharing that restriction: "Since Jinja2 supports passing arguments to callables in templates, many features that require a template tag or filter in Django templates can be achieved by calling a function in Jinja2 templates." Compiling a template into real Python code is a low-friction choice once a template language already permits calling arbitrary functions with arguments — the generated code is just a small, ordinary snippet of the same language the templates can already reach into directly. Django's decision to interpret a restricted node tree instead of compiling isn't only the performance tradeoff Chapter 4 measured — it's the exact same underlying decision as its own restricted expression language, seen from the other side: a template language deliberately kept unable to "invent a programming language" has no real need for a compile step that would hand it one.
pug.compile('string of pug', options) hands back a real, callable function, fn, invoked afterward as fn(locals) — a fifth. Of the seven systems checked in this chapter, five verifiably compile a parsed template into real, callable host-language code (Jinja2, Blade, ERB, EJS, Pug); Django alone walks an interpreted tree instead, and its own documentation gives the reason directly, in its own words, above.
Template Inheritance, Compared Across Five Real Systems
Chapter 4 built extends/block from scratch — a parent template names overridable regions, each carrying its own default; a child extends the parent and overrides only the regions it wants to change. Checking how four more real systems handle the identical, common problem — sharing a header/footer/nav layout across pages — turns up a genuine, documented split.
yield identifies a section where content from the view should be inserted." A separate mechanism, content_for, "allows you to insert content into a named yield block in your layout" — a genuinely different shape from Django/Jinja2's own uniform block, where every region, main or extra, is named and overridable through the identical mechanism.
@yield alone can't carry a default the way Django/Jinja2's single block tag can — a layout author reaches for @yield when a child is expected to always supply content, and for @section/@show together specifically when the layout itself needs a fallback. Rails draws a similar line in a different place: one truly universal slot (yield, standing in for "the entire page"), with every additional named region needing its own explicit content_for call. Both are real, working answers to the identical problem Chapter 4 solved with a single, uniform mechanism — neither is simply Django/Jinja2's own design with different keywords.
JSX breaks from all four of the above in a genuinely different way — not with a different spelling for the same idea, but by rejecting the idea itself. React's own documentation states this directly:
children prop — composition, not inheritance, is the real, documented mechanism for the identical page-sharing problem every other system in this chapter solves with extends/block-shaped syntax.
One Feature, Seven Real Answers
| System | Reuse strategy | Inheritance mechanism |
|---|---|---|
| Django | Parse once, walk a tree | extends / block (uniform, one mechanism) |
| Jinja2 | Parse once, compile to Python | extends / block (same syntax as Django) |
| ERB / Rails | Compile to Ruby (ERB itself); Rails adds escaping on top | yield (main slot) + content_for (named extras) |
| Blade | Compile to PHP, cached until modified | @extends / @section / @yield / @show (two real directive pairs) |
| EJS | Compiles to a cached JavaScript function, per EJS's own docs | No built-in inheritance directive — layouts are typically composed via includes |
| Pug | Compiles to a real, callable JavaScript function (pug.compile() returns it directly) | Real extends / block directives — the identical named-region-with-an-optional-default model as Django/Jinja2 |
| JSX | Compiled by Babel into React.createElement() calls | None — real, documented composition (children props) instead |
Where This Course Is Headed
Chapter 6 moves from what a framework sends back to the browser to where the data it renders actually comes from — how a real ORM layer turns a row in a database into an object a template (or a JSX component) can simply read a property from, built and verified from scratch the same way this chapter's own template engine was in Chapter 4.
Hands-On Exercises
Using this chapter's own real ERB-compiled-source example as a model, write out by hand what the compiled Ruby source for the template "Hello, <%= name %>!" would look like, following the identical _erbout accumulator pattern, and explain what the absence of any escaping call in that generated code means for a plain ERB template used outside of Rails.
๐ View solutionPick any two of the seven real "opt out of escaping" mechanisms from this chapter's own big table (e.g. Blade's {!! !!} and Pug's !{}) and explain, in your own words, why every one of them chooses a visually distinct, unusual-looking symbol rather than reusing the exact same syntax as the safe/default case with some hidden flag.
๐ View solutionUsing Rails' own yield/content_for mechanism and Django/Jinja2's own extends/block mechanism, explain the real structural difference between a layout with one main content area (Rails' plain yield) and one with several independently named, overridable regions (Django/Jinja2's block, or Rails' own content_for) โ and why a real page layout with a header, a sidebar, and a footer needs the multi-region kind, not yield alone.
๐ View solutionChapter 5 Quick Reference
- Escaping, seven real answers โ Django/Jinja2's |safe, ERB/Rails' raw()/.html_safe, Blade's {!! !!}, EJS's <%- %>, Pug's !{}, React's dangerouslySetInnerHTML โ different spelling, identical design decision
- React's own real XSS example โ a stored blog-post field containing an onerror handler, rendered live via dangerouslySetInnerHTML, per React's own documentation
- Five of seven compile to real code โ Jinja2 (Python, Ch.4), Blade (PHP), ERB (Ruby, verified via its own real .src output using the same _erbout accumulator pattern as Chapter 4's own __out list), EJS and Pug (both JavaScript) โ only Django walks an interpreted tree instead, for a documented, deliberate reason
- Plain ERB doesn't escape by default โ verified directly from its own compiled Ruby source; Rails' own ActionView layer adds that behavior, not ERB itself
- Django's own reason for not compiling โ a documented design-philosophy choice ("the goal is not to invent a programming language"), not only a performance tradeoff
- Inheritance, five real shapes โ Django/Jinja2's uniform block, Blade's two-directive-pair split, Rails' single-slot-plus-named-extras, and JSX's real, documented rejection of inheritance in favor of composition
- Next chapter: Data access & the ORM layer โ how it actually works