Mempelajari Jinja2 sebagai templating engine Flask: syntax dasar, template inheritance dengan blocks, macros untuk komponen reusable, filters, dan autoescaping untuk mencegah XSS, serta menyusun layout dan partials untuk web app.

Sejauh ini kita mengembalikan string dan JSON. Saat membangun web app dengan halaman HTML — bukan sekadar API — kalian butuh cara merender halaman secara efisien. Itulah tugas Jinja2, templating engine bawaan Flask. Episode 6 membahas syntax dasar Jinja2, template inheritance (yang menghemat puluhan jam pekerjaan), macros, filters, dan autoescaping — mekanisme keamanan bawaan yang mencegah XSS. Kuasai episode ini, dan kalian bisa membangun UI web utuh dengan DRY yang sehat.
from flask import Flask, render_template
app = Flask(__name__)
@app.get("/")
def index() -> str:
return render_template("index.html", title="Home", user="devnull")<!doctype html>
<html lang="id">
<head><title>{{ title }}</title></head>
<body>
<h1>Selamat datang, {{ user }}</h1>
</body>
</html>{{ title }} dan {{ user }} adalah expression yang diganti nilai dari argumen render_template — context yang kalian kirim adalah satu-satunya jembatan data ke halaman.
Jinja2 punya tiga konstruksi utama:
| Konstruksi | Syntax | Fungsi |
|---|---|---|
| Expression | {{ nilai }} | Menampilkan nilai |
| Statement | {% if %} / {% for %} | Logika & kontrol alur |
| Comment | {# catatan #} | Tidak dirender |
{% if user.is_admin %}
<a href="/admin">Panel Admin</a>
{% else %}
<span>Mode pengguna biasa</span>
{% endif %}
<ul>
{% for item in items %}
<li>{{ loop.index }}. {{ item.name }}</li>
{% else %}
<li>Belum ada item</li>
{% endfor %}
</ul>loop.index adalah variabel khusus Jinja2 (indeks perulangan), dan klausa {% else %} pada for berjalan saat list kosong — pola rapi untuk empty state.
Masalah terbesar UI multi-halaman adalah duplikasi: navbar, footer, dan head HTML berulang di setiap halaman. Jinja2 menyelesaikannya dengan inheritance: satu layout induk dengan {% block %}, dan setiap halaman hanya mengisi bloknya.
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>{% block title %}My App{% endblock %}</title>
</head>
<body>
<nav>
<a href="/">Home</a> | <a href="/about">About</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>{% extends "base.html" %}
{% block title %}Tentang Kami{% endblock %}
{% block content %}
<h1>Tentang Kami</h1>
<p>Kami belajar Flask bersama.</p>
{% endblock %}{% extends "base.html" %} menyatakan bahwa halaman ini mewarisi layout; setiap {% block %} yang didefinisikan akan menggantikan blok bersangkutan di induk. Tambah satu baris navbar di base.html dan semua halaman berubah sekaligus — ini kekuatan DRY yang paling terasa.
Untuk potongan UI yang dipakai ulang tanpa inheritance (misal kartu item), gunakan {% include %}:
<article class="card">
<h2>{{ item.name }}</h2>
<p>Harga: {{ item.price }}</p>
</article>Dipanggil dalam loop: {% for item in items %} {% include "_card.html" %} {% endfor %}. Nama file partial yang diawali underscore (_card.html) adalah konvensi umum untuk menandai "file ini bukan halaman, hanya komponen".
Saat partial butuh parameter, macro lebih cocok — ibarat fungsi di dalam template:
{% macro card(item) %}
<article class="card">
<h2>{{ item.name }}</h2>
<p>Harga: {{ item.price | currency }}</p>
</article>
{% endmacro %}Lalu dipanggil lewat {% import "_macros.html" as ui %} dan {{ ui.card(item) }} di dalam loop. Perhatikan pipe (|) pada {{ item.price | currency }} — itu syntax filter.
Filters memodifikasi nilai di dalam expression. Beberapa yang paling berguna:
<p>{{ user.name | upper }}</p>
<p>{{ item.price | round(2) }}</p>
<p>{{ long_text | truncate(80) }}</p>
<p>{{ created_at | default("belum ada") }}</p>Kalian juga bisa mendefinisikan filter custom:
def rupiah(value: float) -> str:
return f"Rp {value:,.0f}"
app.jinja_env.filters["rupiah"] = rupiahLalu pakai di template: {{ item.price | rupiah }}. Filters menjaga logika presentasi tetap di template, bukan bercampur dengan view.
Sekarang bagian paling penting untuk keamanan: autoescaping. Jinja2 secara default meng-escape semua output {{ ... }} — karakter seperti <, >, &, " diubah menjadi entitas HTML.
<p>{{ user_input }}</p>Jika user_input berisi <script>alert("xss")</script>, Jinja2 merendernya sebagai teks biasa, bukan kode — karena < diubah menjadi <. Tanpa ini, input pengguna bisa dieksekusi sebagai JavaScript oleh browser pengunjung lain: itulah serangan XSS.
Warning
Jangan mengesampingkan autoescaping tanpa alasan kuat. Memakai |safe atau Markup(...) berarti kalian menyatakan "string ini aman" — dan jika sumbernya input pengguna, kalian baru saja membuka celah XSS. Untuk konten markdown atau rich-text, sanitisasi di level server (episode 17), bukan sekadar |safe.
Kapan perlu |safe? Hanya untuk konten yang kalian kendalikan penuh dan memang harus HTML — misal artikel yang ditulis admin lewat editor markdown yang sudah di-sanitize.
{% extends %} di awal: harus baris pertama template anak.templates/; template di subfolder dirujuk dengan slash ("auth/login.html").Pada episode 6 ini, kalian telah menguasai Jinja2 — tulang punggung UI Flask.
Inti yang harus dibawa pulang:
render_template() merender file di templates/ dengan context yang kalian kirim.{{ }} untuk output, {% %} untuk logika, {# #} untuk komentar.extends + block) menghilangkan duplikasi layout; include dan macro untuk komponen reusable.upper, default, truncate) dan filter custom menjaga logika presentasi tetap di template.Di episode 7 selanjutnya, kita melengkapi sisi aset web app: static files & serving — folder /static, custom static folder, cache headers, dan praktik serve CSS/JS/images dengan benar. Pastikan lab kalian siap!