Static file management

#Static file management: elegantly load CSS, JavaScript and local images

📂 Stage: Stage 1 - Breaking the ice and setting sail (Basics) 🔗 Related chapters: Jinja2 模板引擎(下) · environment-setup

In the previous articles, we learned to use Jinja2 templates to render dynamic content, but text and variables alone are too simple! To make the page look beautiful and interactive, you need styles, scripts, pictures, and user-uploaded files — these are all static files. Today I will explain the routine of Flask static file management in one go, from getting started to avoiding pitfalls👇


1. Static file directory: Flask gives you the green light by default

Flask has built-in static file service, no additional routing is needed, just put the file into the agreedstatic/Just a folder** will do.

1.1 Standard directory structure

项目根目录/
└── app/                     # 你的主应用包
    ├── static/              ← 静态文件专属目录(默认名称,不要随意改名)
    │   ├── css/             # 所有样式文件
    │   │   └── style.css
    │   ├── js/              # 交互脚本
    │   │   └── main.js
    │   ├── images/          # 网站自带的图片资源
    │   │   ├── logo.png
    │   │   └── banner.jpg
    │   └── uploads/         # 用户上传的公开文件(如头像、附件)
    │       ├── avatars/
    │       └── attachments/
    └── templates/           # Jinja2 模板文件(上节课的主场)

⚠️ Note: All files that need to be accessed directly through the browser (such as CSS, JS, images, public uploaded files) must be placedstatic/down, otherwise it will be 404 when accessed.

1.2 Register additional static folders (optional scenario)

If you have a "media library" that comes with the project and don't want to plug it intostatic/, such as located in the project root directorymedia/A folder can expose its access path in the following two ways.

Method 1: Flask native routing (suitable for temporary, fewer files)

# app/__init__.py
from flask import Flask, send_from_directory
import os

def create_app():
    app = Flask(__name__)
    MEDIA_FOLDER = os.path.join(os.path.dirname(app.root_path), "media")

    # 自定义路由:访问 /media/xxx.jpg 时去 MEDIA_FOLDER 找文件
    @app.route("/media/<path:filename>")
    def media(filename):
        return send_from_directory(MEDIA_FOLDER, filename)

    return app

Method 2: Werkzeug middleware (suitable for stability, high traffic, and large files)

# app/__init__.py
from flask import Flask
from werkzeug.middleware.shared_data import SharedDataMiddleware
import os

def create_app():
    app = Flask(__name__)
    MEDIA_FOLDER = os.path.join(os.path.dirname(app.root_path), "media")

    # 直接在 WSGI 层挂载静态目录,性能比原生路由稍好
    app.wsgi_app = SharedDataMiddleware(
        app.wsgi_app,
        {"/media": MEDIA_FOLDER}
    )

    return app

💡 Tips: In most scenarios, use the defaultstatic/The directory is enough. Custom static folders are mainly used for some special needs, such as sharing folders with existing systems.


2. Elegantly reference static files in templates

**Never write hard/static/css/style.css! ** useurl_for('static', filename=...)The generated URL is more flexible - even if the routing prefix of the static file is changed later, or the project is deployed to a subdirectory, there is no need to change the link everywhere.

2.1 Basic reference gestures

<!-- 样式表放在 <head> 里 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">

<!-- 脚本文件放在 </body> 前,避免阻塞页面渲染 -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>

<!-- 图片直接用 -->
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="网站 Logo" width="120">

2.2 Must do in production environment: add version number to static files

Browsers cache static files by default. If you update CSS or JS, users may still see the old version. The solution is to append a version parameter to the resource address. There are two common methods.

🟢 Manually add version number (temporary use for small projects)

<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}?v=1.0.2">

Disadvantages: Every time you modify a file, you have to manually change the version number, which is easy to forget.

Use MD5 to generate an 8-bit hash based on the file content. The hash will only change when the file content changes, and does not affect the cache of unchanged files.

# app/utils.py
from flask import current_app, url_for
import hashlib
import os

def static_hash(filename):
    """为静态文件生成带内容哈希的 URL,自动处理缓存更新"""
    filepath = os.path.join(
        current_app.root_path, "static", filename
    )
    if os.path.exists(filepath):
        with open(filepath, "rb") as f:
            # 取前 8 位足够区分不同版本
            version_hash = hashlib.md5(f.read()).hexdigest()[:8]
        return f"{url_for('static', filename=filename)}?v={version_hash}"
    # 文件不存在时返回普通 URL,避免报错
    return url_for('static', filename=filename)

Then register this function as a global template function, so that all templates can use it directly.

# app/__init__.py
from app.utils import static_hash

def create_app():
    app = Flask(__name__)
    # 添加为全局模板函数,名称就用 static_hash
    app.add_template_global(static_hash, 'static_hash')
    return app

Use it like this in the template:

<link rel="stylesheet" href="{{ static_hash('css/style.css') }}">

✨ Just modify it in the futurestyle.cssAnd redeploy, the user's browser will automatically load the latest version, no additional operations are required.


3. CSS organization and coverage skills

3.1 Design a common master style sheet

It is recommended that variables, reset styles, and layout containers that are common to the entire site should be written instatic/css/style.cssinside. The exclusive style of the subpage uses Jinja2'sblockInsert to avoid cluttering all your CSS together.

/* static/css/style.css */

/* 1. CSS 自定义属性(变量),方便一键换主题 */
:root {
    --primary-color: #10b981;  /* 绿色主题 */
    --text-dark: #1f2937;
    --bg-light: #f9fafb;
    --border-gray: #e5e7eb;
}

/* 2. 轻量重置(清除浏览器默认外边距、内边距、盒模型) */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
    color: var(--text-dark);
    background: var(--bg-light);
}

/* 3. 通用布局容器 */
.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 24px;
}

3.2 Quick integration of Bootstrap

Don't want to write the layout from scratch? Bootstrap is a mature choice, and the two introduction methods can be used as needed.

Method 1: CDN (preferred for development stage or small projects, no need to download files)

<!-- base.html 的 <head> 中 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

<!-- base.html 的 </body> 之前(提升加载速度) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

Method 2: Local file (no external network environment or production environment optimization)

Go to Bootstrap official website to download the compiled compressed package, andbootstrap.min.cssput instatic/css/bootstrap.bundle.min.jsput instatic/js/, then useurl_fororstatic_hashJust quote.

3.3 Subtemplate append/override styles

existbase.htmlReserve one inextra_headBlocks, sub-templates can add their own here<style>or<link>

<!-- base.html -->
<head>
    ...
    {% block extra_head %}{% endblock %}
</head>
<!-- 子模板,比如 articles.html -->
{% extends "base.html" %}

{% block extra_head %}
    <!-- 引入文章专属样式 -->
    <link rel="stylesheet" href="{{ static_hash('css/article.css') }}">
    <!-- 或者直接小范围修改内联样式 -->
    <style>
        .navbar {
            background-color: var(--primary-color) !important;
        }
    </style>
{% endblock %}

4. File upload: public files are handled like this

This section only covers uploaded files that require public access (such as user avatars and public images). Private attachments should existstatic/In addition, use a special view to authenticate before outputting, which will be discussed in the advanced chapter later.

4.1 Security configuration and auxiliary functions

existapp/__init__.py(or a separate configuration module).

# app/__init__.py
import os

def create_app():
    app = Flask(__name__)

    # 基础安全配置
    app.config["SECRET_KEY"] = "your-secret-key-here"          # 必须设,flash 需要
    app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024        # 限制最大 16MB
    app.config["ALLOWED_EXTENSIONS"] = {"png", "jpg", "jpeg", "gif", "webp"}  # 只允许图片
    app.config["UPLOAD_FOLDER"] = os.path.join(
        app.root_path, "static", "uploads"
    )

    return app

Then write a general extension checking function (usually defined in the blueprint file where the view is located):

# app/utils.py 或者在蓝图文件中
from flask import current_app

def allowed_file(filename):
    """检查文件名后缀是否在白名单里"""
    return "." in filename and \
           filename.rsplit(".", 1)[1].lower() in current_app.config["ALLOWED_EXTENSIONS"]

⚠️ allowed_fileUsed internallycurrent_app.config, so it must be called within the request context (that is, used temporarily in the view function, or as an auxiliary function of the view function).

4.2 Write upload route (take avatar as an example)

# app/routes/upload.py
from flask import Blueprint, request, redirect, url_for, flash, current_app
from werkzeug.utils import secure_filename
from flask_login import current_user, login_required
from app import db          # 假设你的 db 已初始化
from app.utils import allowed_file
import os
import uuid

upload_bp = Blueprint("upload", __name__)

@upload_bp.route("/upload/avatar", methods=["POST"])
@login_required
def upload_avatar():
    # 1. 基础校验
    if "file" not in request.files:
        flash("请先选择文件!", "danger")
        return redirect(url_for("profile.index"))
    file = request.files["file"]
    if file.filename == "":
        flash("文件名不能为空!", "danger")
        return redirect(url_for("profile.index"))

    # 2. 文件类型白名单检查
    if not allowed_file(file.filename):
        flash("仅支持 PNG/JPG/JPEG/GIF/WEBP 格式!", "danger")
        return redirect(url_for("profile.index"))

    # 3. 生成安全的唯一文件名
    secure_name = secure_filename(file.filename)          # 去除危险字符
    ext = secure_name.rsplit(".", 1)[1].lower()           # 拿扩展名
    unique_name = f"{uuid.uuid4().hex}.{ext}"             # UUID 杜绝重名

    avatar_dir = os.path.join(current_app.config["UPLOAD_FOLDER"], "avatars")
    os.makedirs(avatar_dir, exist_ok=True)                # 确保目录存在
    file.save(os.path.join(avatar_dir, unique_name))

    # 4. 更新用户头像路径(假设有 User 模型)
    current_user.avatar_url = f"/static/uploads/avatars/{unique_name}"
    db.session.commit()

    flash("头像上传成功!", "success")
    return redirect(url_for("profile.index"))

4.3 Upload form with preview

The form must be addedenctype="multipart/form-data", otherwise the file cannot be submitted.

<!-- templates/profile/index.html -->
<form method="POST" action="{{ url_for('upload.upload_avatar') }}" enctype="multipart/form-data">
    <div class="mb-3">
        <label for="avatar" class="form-label">更换头像</label>
        <input type="file" class="form-control" id="avatar" name="file" accept="image/*" required>
    </div>
    <!-- 图片预览区域 -->
    <div class="mb-3">
        <img id="avatar_preview" class="img-thumbnail" style="max-width: 200px; display: none;" alt="预览">
    </div>
    <button type="submit" class="btn btn-primary">上传</button>
</form>

<!-- 简单预览脚本 -->
<script>
document.getElementById('avatar').addEventListener('change', function() {
    const preview = document.getElementById('avatar_preview');
    const file = this.files[0];
    if (file) {
        preview.src = URL.createObjectURL(file);
        preview.style.display = 'block';
    } else {
        preview.style.display = 'none';
    }
});
</script>

5. Let the browser find the favicon obediently

By default, the browser will request the root directory of the website.favicon.ico, but the safest way is to put the icon instatic/down, then onbase.htmlof<head>explicitly specified.

<!-- base.html <head> 顶部 -->
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- 显式指定 favicon,避免 404 -->
    <link rel="icon" type="image/x-icon" href="{{ static_hash('favicon.ico') }}">
    <title>{% block title %}我的 Flask 博客{% endblock %}</title>
    ...
</head>

✅ Even if the browser has smart search for multiple tabs, it is recommended to add this line to reduce unnecessary requests and error logs.


6. Summary & Best Practices

A picture is worth a thousand words

RequirementsKey operations
Place static filesBy defaultapp/static/And classify and create subdirectories
Referenced in templatePriority usedurl_for('static', filename=...)or customizestatic_hash
Control browser cacheAutomatically generate content hashes and automatically refresh when files change
Upload files securelyUsesecure_filenamePrevent path crossing,uuidPrevent duplicate names, limit size and type
show site iconwillfavicon.icoputstatic/and in<head>Explicit import
  1. Categorized storage, no confusion - Don’t throw CSS, JS, images, and uploaded files directly instatic/Root directory, classified by folders.
  2. The development version relies on Flask, and the online version relies on Nginx/Apache - Flask's own static file service efficiency is low, and the production environment must be handed over to the reverse proxy.
  3. Keep private files awaystatic/ - User private attachments, ID photos, etc. must not be placed in a directory that can be directly accessed through URL, and must be output using a view with permission control.
  4. Production environment compression and merging - You can use tools (such as Flask-Assets, Webpack) to package and compress multiple CSS/JS, greatly reducing the number of requests.

🔗 Extended reading