DataLens

Menyajikan file statis

Sajikan CSS, gambar, JavaScript, dan file unduhan dari folder static, tautkan dengan url_for, dan kirim file yang dibuat secara langsung.

Level: Menengah · 8 menit baca

Latihan modul ini

Soal-soal untuk pelajaran ini berjalan di browser-mu dan langsung mengecek jawabanmu.

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

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

Latihan modul ini

Soal-soal untuk pelajaran ini berjalan di browser-mu dan langsung mengecek jawabanmu.

Berikutnya: Menangani data formulir dan query string →