Mengembalikan potongan HTML dari dalam fungsi Python — seperti yang kita lakukan pada chapter Pengenalan Flask — cepat atau lambat menjadi tidak terkelola: bayangkan menulis seluruh halaman katalog dengan f-string bersarang. Solusi Flask adalah template engine Jinja2: HTML disimpan di berkas terpisah, dan bagian yang dinamis ditandai dengan sintaks khusus. Pada chapter ini kita juga mengatur berkas statis seperti CSS dan gambar yang tidak berubah tiap request.
render_template dan Variabel #
Fungsi render_template() memuat berkas dari folder templates/, mengisinya dengan data, lalu mengembalikan HTML jadi sebagai response. Sebuah rute katalog akan tampak seperti ini:
from flask import Flask, render_template
app = Flask(__name__)
PRODUK_LIST = [
{"nama": "Buku Python", "harga": 95000, "stok": 12},
{"nama": "Mouse Wireless", "harga": 150000, "stok": 3},
{"nama": "Keyboard Mekanik", "harga": 750000, "stok": 0},
]
@app.route("/produk")
def daftar_produk():
return render_template("produk.html", produk_list=PRODUK_LIST)Argument produk_list=PRODUK_LIST membuat list produk tersedia sebagai variabel produk_list di dalam template. Sintaks Jinja2 hanya mengenal dua bentuk: {{ ... }} mencetak nilai, sedangkan {% ... %} menjalankan logika seperti loop dan kondisi. Berikut isi templates/produk.html:
{% extends "base.html" %}
{% block judul %}Daftar Produk{% endblock %}
{% block konten %}
<h1>Daftar Produk</h1>
<ul>
{% for produk in produk_list %}
<li>{{ produk.nama }} — Rp{{ produk.harga }}
{% if produk.stok == 0 %}<em>(habis)</em>
{% elif produk.stok < 5 %}<em>(hampir habis)</em>
{% endif %}
</li>
{% else %}
<li>Belum ada produk.</li>
{% endfor %}
</ul>
{% endblock %}Mari kita telaah baris-baris pentingnya:
- Baris 1 (
{% extends "base.html" %}): template ini mewarisi kerangka daribase.html, bukan menulis ulang seluruh halaman. - Baris 2 (
{% block judul %}): isi blockjuduldari kerangka diganti menjadi teks halaman ini. - Baris 6 (
{% for produk in produk_list %}): loop Jinja2 bekerja persis sepertiforpada Python, termasuk akses atributproduk.namapada dictionary. - Baris 8-10 (
{% if produk.stok == 0 %}): kondisi memilih teks stok yang sesuai — operator perbandingannya sama dengan Python. - Baris 12-13 (
{% else %}): cabangelsemilikfordijalankan hanya bila list kosong — fitur yang juga ada di Python.
Kerangka templates/base.html mendefinisikan lubang-lubang yang bisa diisi halaman anak:
<!DOCTYPE html>
<html lang="id">
<head>
<title>{% block judul %}Toko Buku{% endblock %}</title>
</head>
<body>
<nav>Beranda | Katalog</nav>
{% block konten %}{% endblock %}
<footer>Contoh Flask</footer>
</body>
</html>Semua halaman anak kini berbagi navigasi dan footer yang sama; mengubah base.html satu kali mengubah seluruh halaman.
Saat rute di atas diminta melalui test_client(), hasil render sebenarnya adalah:
client = app.test_client()
print(client.get("/produk").text)Output yang Dihasilkan:
<!DOCTYPE html>
<html lang="id">
<head>
<title>Daftar Produk</title>
</head>
<body>
<nav>Beranda | Katalog</nav>
<h1>Daftar Produk</h1>
<ul>
<li>Buku Python — Rp95000
</li>
<li>Mouse Wireless — Rp150000
<em>(hampir habis)</em>
</li>
<li>Keyboard Mekanik — Rp750000
<em>(habis)</em>
</li>
</ul>
<footer>Contoh Flask</footer>
</body>
</html>Perhatikan baris-baris kosong di hasil render: tag {% ... %} dieksekusi Jinja2 tetapi baris tempat tag itu berada tetap meninggalkan baris baru. Jinja2 menyediakan kontrol whitespace seperti {%- ... -%} dan opsi trim_blocks untuk membersihkannya saat halaman mulai kompleks.
Static Files #
CSS, JavaScript, dan gambar tidak diproses Python — cukup dilayani apa adanya. Flask menyediakan folder static/ dan endpoint static untuk itu. URL-nya dibangun dengan url_for():
from flask import url_for
with app.test_request_context():
print(url_for("static", filename="style.css"))
print(url_for("static", filename="img/logo.png"))Output yang Dihasilkan:
/static/style.css
/static/img/logo.pngDi dalam template, tautan stylesheet ditulis dengan {{ url_for('static', filename='style.css') }} di dalam atribut href. Sama seperti rute biasa, penggunaan url_for() membuat struktur folder static bebas diubah tanpa menyentuh templates.
Sekilas Message Flashing #
Poling pesan sukses atau gagal lewat query string itu rapuh — pesan hilang saat halaman di-refresh. Flask menyediakan mekanisme flash: pesan disimpan sekali di session, lalu diambil sekali di request berikutnya:
from flask import flash, get_flashed_messages
app.secret_key = "kunci-rahasia-contoh"
@app.route("/simpan")
def simpan():
flash("Data berhasil disimpan!")
return "redirect ke halaman berikutnya..."
@app.route("/pesan")
def pesan():
return str(get_flashed_messages())
client = app.test_client()
client.get("/simpan")
print(client.get("/pesan").text)Output yang Dihasilkan:
['Data berhasil disimpan!']Request /pesan kedua akan mendapat list kosong: pesan flash sekali pakai, persis perilaku notifikasi pada aplikasi nyata. Flash membutuhkan secret_key karena session dienkripsi — topik yang berlanjut di chapter Forms dan Validation.