Idenya
Sebuah halaman lebih dari sekadar HTML: ia butuh stylesheet, logo, mungkin juga script. File-file ini tidak berubah per request, jadi disebut file statis. Flask menyajikannya dari folder bernama static/ di samping aplikasimu, tanpa perlu menulis route.
shop/
├── app.py
├── templates/
│ └── base.html
└── static/
├── style.css
└── logo.svg
Route static
Setiap aplikasi Flask otomatis punya route /static/<path:filename> yang menyajikan file dari static/:
from flask import Flask
app = Flask(__name__)
client = app.test_client()
r = client.get("/static/style.css")
print(r.status_code, r.mimetype)
print(r.get_data(as_text=True))
print(client.get("/static/missing.css").status_code)
200 text/css
body { font-family: system-ui, sans-serif; color: #1A1D3A; }
h1 { color: #5669FD; }
404
Flask mengatur Content-Type yang tepat dari ekstensi file (text/css untuk .css, image/svg+xml untuk .svg), dan dari situlah browser tahu apa yang diterimanya. File yang tidak ada menghasilkan 404 biasa.
Menautkan file statis: url_for("static")
Di template, tautkan file statis dengan url_for("static", filename=...), jangan pernah dengan path yang di-hard-code:
from flask import Flask, render_template_string
app = Flask(__name__)
PAGE = """<!doctype html>
<html>
<head><link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"></head>
<body><img src="{{ url_for('static', filename='logo.svg') }}" alt="Toko Kita"><h1>Toko Kita</h1></body>
</html>"""
@app.route("/")
def home():
return render_template_string(PAGE)
print(app.test_client().get("/").get_data(as_text=True))
<!doctype html>
<html>
<head><link rel="stylesheet" href="/static/style.css"></head>
<body><img src="/static/logo.svg" alt="Toko Kita"><h1>Toko Kita</h1></body>
</html>
Kenapa url_for lagi? Karena di production folder static sering dipindahkan: disajikan dari /assets/, dari CDN di domain lain, atau dengan versi di namanya untuk caching. Ubah satu pengaturan, dan setiap link url_for("static", ...) ikut menyesuaikan.
Caching dan cache busting
Browser menyimpan cache file statis supaya halaman cepat dimuat. Masalahnya: setelah kamu mengubah style.css, pengunjung mungkin tetap memakai salinan lama. Solusi umumnya adalah cache busting: tambahkan versi ke URL, sehingga file yang berubah punya URL baru:
from flask import Flask, render_template_string
app = Flask(__name__)
VERSION = "2024.07.1"
with app.test_request_context():
print(render_template_string("{{ url_for('static', filename='style.css', v=version) }}", version=VERSION))
/static/style.css?v=2024.07.1
Keyword argument tambahan untuk url_for menjadi query parameter. Naikkan VERSION di setiap rilis, dan setiap browser akan mengambil stylesheet yang baru.
Mengirim file dari tempat lain
File statis bersifat publik: siapa pun yang menebak URL-nya bisa mendapatkannya. Untuk file yang perlu pengecekan (invoice untuk pelanggan yang sedang login) atau tersimpan di tempat lain, gunakan send_from_directory di route buatanmu sendiri:
from flask import Flask, send_from_directory, abort
app = Flask(__name__)
ALLOWED = {"price-list.pdf", "catalog-2024.pdf"}
@app.route("/downloads/<name>")
def download(name):
if name not in ALLOWED:
abort(404)
return send_from_directory("downloads", name, as_attachment=True)
client = app.test_client()
r = client.get("/downloads/price-list.pdf")
print(r.status_code, r.headers["Content-Disposition"])
print(client.get("/downloads/../app.py").status_code)
200 attachment; filename=price-list.pdf
404
send_from_directory(folder, name)menolak path yang mencoba keluar dari folder (../app.py), serangan klasik yang disebut path traversal.as_attachment=TruemengaturContent-Disposition: attachment, sehingga browser mengunduh file-nya alih-alih membukanya.- Allow-list (
ALLOWED) adalah pengecekan tambahan buatanmu sendiri.
File yang dibuat langsung: laporan CSV
Sebuah route juga bisa mengembalikan file yang dibuatnya saat itu juga, misalnya ekspor CSV. Atur header-nya sendiri:
import csv
import io
from flask import Flask, Response
app = Flask(__name__)
ROWS = [("category", "revenue"), ("Electronics", 5113000), ("Kitchen", 2610000)]
@app.route("/export.csv")
def export():
buffer = io.StringIO()
csv.writer(buffer).writerows(ROWS)
return Response(buffer.getvalue(), mimetype="text/csv",
headers={"Content-Disposition": "attachment; filename=revenue.csv"})
r = app.test_client().get("/export.csv")
print(r.mimetype, r.headers["Content-Disposition"])
print(r.get_data(as_text=True))
text/csv attachment; filename=revenue.csv
category,revenue
Electronics,5113000
Kitchen,2610000
Response(body, mimetype=..., headers=...) memberimu kendali penuh atas response, saat mengembalikan string saja tidak cukup.
Mengapa ini penting untuk kode yang bersih
Perlakukan lokasi file seperti konfigurasi lainnya: jangan pernah di-hard-code di template. url_for("static", ...) menjaga link tetap berfungsi saat infrastruktur berubah, send_from_directory menjaga unduhan tetap aman, dan file yang dibuat langsung melewati satu route kecil dengan header yang eksplisit. Setiap bagian mengerjakan satu tugas, di satu tempat.
Rangkuman
- File di
static/otomatis disajikan di/static/<filename>, dengan Content-Type yang tepat. - Tautkan dengan
url_for("static", filename="..."); argumen tambahan menjadi query parameter (cache busting). - File statis bersifat publik; untuk file yang perlu dicek atau privat, tulis route dengan
send_from_directory(folder, name, as_attachment=True). send_from_directorymemblokir path traversal; tambahkan juga allow-list buatanmu sendiri.- Kembalikan file yang dibuat langsung dengan
Response(body, mimetype=..., headers={"Content-Disposition": ...}).