Структура проекта и статические файлы во Flask: static и url_for
Седьмой урок курса Flask: каноническая структура проекта, папка static, url_for, подключение CSS и Bootstrap, favicon и настройки в app.config.
Редакция Питоники
Открываете свой app.py через месяц после старта. Восемьсот строк: маршруты перемешаны с SQL, стили инлайном в HTML-строках, шаблоны собраны из конкатенаций. Работает — но править страшно, а место для новой функции приходится искать скроллом. Это не признак плохого кода, это признак отсутствия структуры проекта. Сегодня разложим Flask-приложение по полочкам: шаблоны в templates, стили и картинки в static, настройки в app.config.
К концу урока у приложения будет канонический скелет, который вы мгновенно узнаёте в чужих репозиториях. Одинаковая раскладка — не эстетика ради эстетики: по ней коллега (и вы сами через полгода) находит нужный файл за секунды, а инструменты деплоя работают без сюрпризов.
Структура проекта Flask: канонический скелет
Вот раскладка, к которой рано или поздно приходит любой Flask-проект среднего размера:
my-site/
app.py # маршруты и запуск приложения
config.py # настройки: режимы и секреты
requirements.txt # список зависимостей для pip install
templates/
base.html # общий каркас всех страниц
index.html
posts/
list.html
static/
css/
style.css
js/
app.js
img/
logo.png
Две папки фиксированы. templates мы освоили в уроке 3 про шаблоны — там живёт разметка. static — герой сегодняшнего урока: всё, что сервер отдаёт как есть, без рендеринга — стили, скрипты, картинки, шрифты. Остальные файлы (config, requirements) — не требование Flask, а договорённость, которая экономит часы.
| Папка или файл | Что кладём | Примеры |
|---|---|---|
| templates/ | HTML-шаблоны Jinja2 | base.html, posts/list.html |
| static/ | файлы без обработки | style.css, app.js, logo.png |
| app.py | маршруты и запуск | view-функции, @app.route |
| config.py | настройки | SECRET_KEY, DEBUG |
| requirements.txt | зависимости | Flask==3.1.0 |
Папка static: CSS, JS и картинки
Flask создаёт статический маршрут автоматически: любой файл из папки static доступен по URL /static/путь/от/папки. Никаких @app.route для стилей писать не нужно. Проверим curl-ом:
$ curl -i http://127.0.0.1:5000/static/css/style.css
HTTP/1.1 200 OK
Content-Type: text/css; charset=utf-8
Last-Modified: Fri, 14 Mar 2025 09:12:44 GMT
body { font-family: Georgia, serif; color: #2b2b2b; }Встроенный dev-сервер справляется со статикой для учёбы и внутренних инструментов. На продакшне статику обычно отдаёт nginx, а Flask о ней забывает — приёмы деплоя разберём в уроке 10. Но структура папок от этого не меняется, поэтому приучаемся к ней сейчас.
Подключаем стиль в шаблоне
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/static/css/style.css">
<title>Кофейня Зерно</title>
</head>
<body>
<h1>Меню на сегодня</h1>
</body>
</html>
url_for: ссылки, которые не ломаются
Функция url_for строит URL по имени, а не по вашей памяти: url_for('static', filename='css/style.css') вернёт ровно /static/css/style.css — но соберёт адрес из настроек приложения. Сменили префикс статики, переехали на поддомен — все ссылки починились сами. Ниже упрощённая модель настоящей url_for, чтобы показать механику. Пример запускаемый: внутри Flask это обычный Jinja2 из урока 3.
from jinja2 import Environment
routes = {"index": "/", "posts": "/posts/", "subscribe": "/subscribe/"}
def url_for(endpoint, **values):
if endpoint == "static":
return "/static/" + values["filename"]
url = routes.get(endpoint, "/" + endpoint)
if values:
query = "&".join(f"{k}={v}" for k, v in values.items())
url = url + "?" + query
return url
env = Environment(trim_blocks=True, lstrip_blocks=True)
env.globals["url_for"] = url_for
head = env.from_string(
'<link rel="stylesheet" href="{{ url_for("static", filename="css/style.css") }}">
'
'<a class="navbar-brand" href="{{ url_for("index") }}">Кофейня Зерно</a>'
)
print(head.render())
page = env.from_string('<a href="{{ url_for("posts", page=2) }}">Страница 2</a>')
print(page.render())
<link rel="stylesheet" href="/static/css/style.css"> <a class="navbar-brand" href="/">Кофейня Зерно</a> <a href="/posts/?page=2">Страница 2</a>
Во Flask url_for доступна в шаблонах из коробки, а в Python-коде её импортируют: from flask import url_for. Написали все ссылки через url_for — и переименование путей перестало быть игрой «найди все ссылки в шаблонах». Лишние аргументы, которых нет в правиле маршрута, превращаются в query-строку: url_for('posts', page=2) даёт /posts/?page=2 — готовая пагинация.
url_for для маршрутов с параметрами
Если маршрут объявлен как /post/<int:post_id>, то url_for('post', post_id=7) соберёт /post/7/. Плюсы налицо: имя функции меняется редко, путь — часто; параметры проверяются на месте; значения экранируются для URL автоматически. Именно поэтому в base.html ссылки на разделы пишут через url_for, а не руками.
Два флага пригодятся позже. _external=True строит абсолютный адрес с доменом — http://127.0.0.1:5000/static/css/style.css — он нужен в письмах и RSS, где относительная ссылка бесполезна. А _anchor='comments' добавит хвост #comments для прыжка к секции. В повседневных шаблонах оба почти не встречаются, но знать о них стоит, чтобы не клеить домен к строке руками.
Свои имена папок: static_folder и static_url_path
Имя static — соглашение, а не приговор. Если папка называется иначе или нужен свой URL-префикс, это задаётся при создании приложения:
app = Flask(__name__, static_folder="assets", static_url_path="/files")
Зачем это нужно? Первый случай — наследованный проект, где папка исторически зовётся assets. Второй — когда статику раздают с отдельного домена статики или CDN: префикс меняется в одном месте, url_for перестраивает все ссылки, а шаблоны даже не замечают переезда.
Кэш браузера и версия статики
Статику браузер кэширует агрессивно и по праву: файлы большие, меняются редко. Но однажды вы обновите style.css, а у пользователя останется старая версия — кэш не знает, что файл изменился. Простое лекарство — версионный параметр: url_for('static', filename='css/style.css', v='2') вернёт /static/css/style.css?v=2, и браузер сочтёт это новым файлом. Крупные проекты автоматизируют это хэшем содержимого в имени файла, для учебного проекта хватит и рук.
В ответе из curl выше мелькал заголовок Last-Modified — по нему браузер спрашивает: файл изменился с того марта? Если нет, сервер отвечает 304 Not Modified без тела, и страница грузится заметно быстрее. Это работает из коробки; версионный параметр нужен для случая, когда вы не хотите ждать вопрос — просто говорите браузеру: новый адрес, качай заново.
Favicon: убираем 404 из логов
Запустили приложение, открыли страницу — а в консоли сервера лишняя строка:
127.0.0.1 - - [14/Mar/2025 09:41:03] "GET /favicon.ico HTTP/1.1" 404 -
Лечится в два движения: положите favicon.ico в static/img и добавьте в base.html строку link с rel="icon": {{ url_for('static', filename='img/favicon.ico') }}. 404 исчезнет, вкладка получит вашу иконку, а логи перестанут путать при отладке.
app.config: настройки приложения
У каждого Flask-приложения есть словарь настроек app.config. Там живут служебные ключи — SECRET_KEY из урока 6 про сессии, DEBUG — и ваши собственные, какие угодно:
Служебных ключей у Flask десятки, но запоминать их все не нужно — большинство имеет разумные значения по умолчанию. Вот те, что встречаются почти в каждом проекте:
| Ключ | Зачем нужен | Значение по умолчанию |
|---|---|---|
| DEBUG | отладчик и авто-перезагрузка | False |
| SECRET_KEY | подпись сессий и flash | None — session не работает |
| PERMANENT_SESSION_LIFETIME | срок жизни сессии с permanent | 31 день |
| MAX_CONTENT_LENGTH | максимальный размер запроса | без ограничения |
| SESSION_COOKIE_HTTPONLY | кука session недоступна JS | True |
| TEMPLATES_AUTO_RELOAD | перезагрузка шаблонов без debug | по значению DEBUG |
from flask import Flask
app = Flask(__name__)
app.config["SECRET_KEY"] = "2f764333a5baf6e5d5890970e9db117f"
app.config["DEBUG"] = True
# свои настройки - просто новые ключи
app.config["POSTS_PER_PAGE"] = 12
app.config["SHOP_NAME"] = "Кофейня Зерно"
print(app.config["POSTS_PER_PAGE"], app.config["SHOP_NAME"])
12 Кофейня Зерно
Выносим секреты из кода
SECRET_KEY прямо в app.py — норм для учебного проекта и плохая привычка для настоящего: репозиторий уедет на GitHub вместе с ключом. Правильная схема: безобидные настройки — в config.py, секреты — в переменные окружения:
# config.py
DEBUG = True
POSTS_PER_PAGE = 12
# app.py
import os
from flask import Flask
app = Flask(__name__)
app.config.from_object("config")
app.config["SECRET_KEY"] = os.environ.get("SP_SECRET_KEY", "dev-only")
Тот же приём работает для DEBUG: app.config["DEBUG"] = os.environ.get("FLASK_DEBUG") == "1" — режим включается флагом запуска, без правки кода. Разделение «в коде — логика, в окружении — настройки» — первый шаг к спокойному деплою.
Переменные окружения удобно держать в файле .env рядом с проектом: по строке на переменную, вида SP_SECRET_KEY=2f764333. Расширение python-dotenv подхватывает его при запуске, а сам файл добавляют в .gitignore — в репозиторий уезжает только образец .env.example. Ещё одна переменная, которую знает сам Flask, — FLASK_APP: она подсказывает команде flask run, где лежит приложение, если файл называется не app.py.
Подключаем Bootstrap руками
Писать CSS с нуля каждый раз скучно. Самый простой путь без сборщиков и npm — подключить Bootstrap с CDN, а свои правки класть в static/css/style.css поверх:
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav class="navbar bg-dark" data-bs-theme="dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">Кофейня Зерно</a>
</div>
</nav>
<main class="container mt-3">
{% block content %}{% endblock %}
</main>
</body>
Порядок строк важен: ваш style.css подключается после bootstrap.min.css, чтобы перекрывать его правила при конфликте. CDN-вариант не требует сборки и кэшируется браузерами; альтернатива — скачать dist-файлы в static/vendor, тогда сайт перестанет зависеть от чужого сервера. Для учебного проекта CDN — разумный минимум.
Режимы: разработка и продакшн
Dev-сервер Flask (flask run или app.run) удобен, но однопоточен и не предназначен для боя. Главное правило режимов: debug=True — только на своей машине.
Включается отладка тремя способами, и все пишут в один конфиг: app.run(debug=True), переменная окружения FLASK_DEBUG=1 или флаг запуска flask --app app run --debug. В разработке это спасает часы: сервер сам перезагружается при правке кода и показывает трейсбек прямо в браузере вместо белой страницы.
Крупный проект: раскладываем по ролям
Когда маршрутов становится десяток, файл app.py делят по ролям — не по страницам, а по типу работы:
coffee-site/
app.py # создание приложения и запуск
config.py # настройки
routes.py # view-функции: главная, посты, подписка
models.py # модели SQLAlchemy из урока 5
forms.py # описания форм
requirements.txt
templates/
static/
Это уже почти архитектура. Следующая ступень — Blueprint: маршруты группируются в независимые модули auth, blog, api со своими шаблонами и префиксами URL, а приложение собирается из блоков, как конструктор. Этим займёмся в уроке 8.
Чек-лист структуры Flask-проекта
- templates — для шаблонов, static — для файлов как есть: CSS, JS, картинки
- ссылки на статику только через url_for('static', filename=...)
- настройки — в app.config, секреты — в переменных окружения
- debug=True не покидает локальную машину
- favicon.ico в static избавляет логи от лишних 404
Итоги: проект, в котором приятно работать
Скелет собран: разметка в templates, стили и картинки в static, ссылки через url_for, настройки в app.config, секреты в окружении, отладка — только в разработке. На первый взгляд это бюрократия, но именно по такому скелету вы через месяц найдёте нужный файл за десять секунд — вместо воспоминаний, в какой строке 800-строчного файла живёт корзина. Дальше самоучитель Flask ведёт к blueprint'ам, аутентификации и финальному проекту.
Практика на сейчас: откройте учебный проект из уроков 1-5 и переложите его по скелету из начала урока — пятнадцать минут работы, а все дальнейшие правки станут заметно приятнее. В следующем уроке масштабируем эту структуру на большие приложения с помощью Blueprint'ов.
Сначала предскажи ответ в голове — это главный навык программиста.
config = {"DEBUG": False}
print(config.get("SECRET_KEY", "не задан"))
parts = ["css", "style.css"]
print("/static/" + "/".join(parts))
1. Где Flask по умолчанию ищет статические файлы и по какому URL они доступны?
2. Что вернёт url_for('static', filename='css/style.css')?
3. Зачем писать в шаблонах url_for вместо ручных путей вроде /static/style.css?
4. Что хранят в app.config?
5. Почему debug=True нельзя включать на продакшн-сервере?
Реализуйте упрощённую url_for: для endpoint "static" возвращайте "/static/" + filename, для остальных берите путь из словаря routes, а дополнительные именованные аргументы приклеивайте как query-строку: перед первым параметром "?", между остальными "&". Проверьте тремя вызовами из print-ов.
Почему не подключается CSS-файл во Flask?
Откройте вкладку Network в консоли браузера: почти всегда там висит GET /static/css/style.css со статусом 404. Проверьте, что файл лежит в папке static рядом с app.py (не styles), а ссылка в шаблоне собрана через {{ url_for('static', filename='css/style.css') }}. Быстрая проверка без браузера: curl -I http://127.0.0.1:5000/static/css/style.css — ждите 200, а не 404.
Где Flask ищет статические файлы и как это изменить?
По умолчанию — в папке static рядом с файлом приложения, по URL /static/<имя>. Папку и префикс можно поменять при создании: Flask(__name__, static_folder="assets", static_url_path="/files") — файлы берутся из assets, а в URL они доступны по /files/....
Как подключить CSS-файл к шаблону Flask?
Положите файл в static/css/ и добавьте в шаблон link с href="{{ url_for('static', filename='css/style.css') }}". url_for соберёт правильный адрес из настроек и не сломается при смене префикса статики или переезде сайта.
Куда положить SECRET_KEY, чтобы он не попал в git?
В переменную окружения или отдельный config-файл, добавленный в .gitignore. В коде его читают через os.environ.get("SP_SECRET_KEY") или app.config.from_prefixed_env(). В репозитории держат только образец config.example.py без настоящих секретов.
Нужен ли отдельный веб-сервер для статики на продакшне?
Встроенный сервер Flask отдаёт статику, но связка gunicorn + nginx быстрее и безопаснее: nginx отдаёт static напрямую, а Flask занимается только динамикой. Для учебного или внутреннего проекта встроенного сервера вполне достаточно.
Понравился урок? Сошлитесь на него
«Написали все ссылки через url_for — и переименование путей перестало быть игрой «найди все ссылки в шаблонах».»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Flask · Урок 3
Шаблоны Jinja2: HTML-страницы с данными из Python
Третий урок курса Flask: шаблоны Jinja2 — переменные, циклы, фильтры и наследование base.html. Отличие этого урока: все примеры Jinja2 можно отрендерить прямо на странице.
Flask · Урок 6
Сессии, куки и flash-сообщения во Flask: память о пользователе
Шестой урок курса Flask: session как словарь, куки set_cookie, секретный ключ и flash-сообщения. Учим приложение помнить пользователя между запросами.
Flask · Урок 8
Blueprint: организуем большое приложение на Flask
Восьмой урок курса Flask: Blueprint — мини-приложения внутри одного проекта. Регистрируем блюпринты, задаём префиксы адресов, разносим шаблоны по папкам и рефакторим блог из одного файла в модули.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
requests · Урок 1
Библиотека requests Python с нуля: первый GET-запрос
Первый GET-запрос на requests: что улетает по сети, что возвращается и как разобрать ответ на статус, заголовки и тело — механику проверяем прямо в браузере.
requests для начинающихhttp ответ структура
openpyxl · Урок 1
Excel на Python: первая книга xlsx через openpyxl
Первая книга xlsx из кода: Workbook, запись в ячейки и wb.save — настоящий Excel-файл создаётся прямо в браузере, без установленного Excel.
python excel файлыopenpyxl для начинающих
Flask · Урок 12
Конфигурация Flask: config-классы и переменные окружения
Двенадцатый урок курса Flask: настройки живут не в коде, а в конфигурации. Собираем класс Config, читаем SECRET_KEY из переменных окружения и переключаем приложение в тестовый режим одной строкой app.config.update(TESTING=True).
flask конфигурация configflask app.config
Проверьте знания по Flask
В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по Flask: 20 задач