Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 7 из 20 Средний 40 мин 120 XP

Структура проекта и статические файлы во 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
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
Так раскладывают проекты официальная документация Flask и большинство туториалов. Папки templates и static создаются рядом с app.py - Flask найдёт их сам, без единой строчки настроек.

Две папки фиксированы. templates мы освоили в уроке 3 про шаблоны — там живёт разметка. static — герой сегодняшнего урока: всё, что сервер отдаёт как есть, без рендеринга — стили, скрипты, картинки, шрифты. Остальные файлы (config, requirements) — не требование Flask, а договорённость, которая экономит часы.

Папка или файлЧто кладёмПримеры
templates/HTML-шаблоны Jinja2base.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-ом:

Отдача CSS-файла
$ 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; }
Файл отдаётся как есть, Python его не обрабатывает. Content-Type Flask определил по расширению: картинка получит image/png, скрипт - application/javascript.

Встроенный dev-сервер справляется со статикой для учёбы и внутренних инструментов. На продакшне статику обычно отдаёт nginx, а Flask о ней забывает — приёмы деплоя разберём в уроке 10. Но структура папок от этого не меняется, поэтому приучаемся к ней сейчас.

Подключаем стиль в шаблоне

templates/index.html — первая попытка
<!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>
Работает, но адрес /static/css/style.css вписан руками - сейчас объясню, почему так оставлять нельзя.

url_for: ссылки, которые не ломаются

Функция url_for строит URL по имени, а не по вашей памяти: url_for('static', filename='css/style.css') вернёт ровно /static/css/style.css — но соберёт адрес из настроек приложения. Сменили префикс статики, переехали на поддомен — все ссылки починились сами. Ниже упрощённая модель настоящей url_for, чтобы показать механику. Пример запускаемый: внутри Flask это обычный Jinja2 из урока 3.

Механика url_for (запустите)
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>
Мы вручную добавили url_for в глобальные функции Jinja2-окружения - ровно так же поступает Flask при создании приложения, только его url_for знает все маршруты и умеет подставлять параметры вида int:post_id.

Во 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")
Теперь CSS лежит в assets/css/style.css, а в браузере доступен по /files/css/style.css. url_for('static', ...) вернёт именно /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 из логов

Запустили приложение, открыли страницу — а в консоли сервера лишняя строка:

Лог dev-сервера
127.0.0.1 - - [14/Mar/2025 09:41:03] "GET /favicon.ico HTTP/1.1" 404 -
Это лог werkzeug. Браузер сам запрашивает /favicon.ico - иконку вкладки - даже если вы её нигде не подключали.

Лечится в два движения: положите 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подпись сессий и flashNone — session не работает
PERMANENT_SESSION_LIFETIMEсрок жизни сессии с permanent31 день
MAX_CONTENT_LENGTHмаксимальный размер запросабез ограничения
SESSION_COOKIE_HTTPONLYкука session недоступна JSTrue
TEMPLATES_AUTO_RELOADперезагрузка шаблонов без debugпо значению DEBUG
app.py — свои настройки в config
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 Кофейня Зерно
app.config - обычный словарь с заглавными ключами. В view-функциях настройки читают через current_app.config - увидим это при знакомстве с blueprint'ами.

Выносим секреты из кода

SECRET_KEY прямо в app.py — норм для учебного проекта и плохая привычка для настоящего: репозиторий уедет на GitHub вместе с ключом. Правильная схема: безобидные настройки — в config.py, секреты — в переменные окружения:

config.py и app.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")
from_object читает переменные модуля config.py, записанные ЗАГЛАВНЫМИ буквами. os.environ.get берёт значение переменной окружения: на хостинге её задают в панели или в .env-файле. Запасное 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 поверх:

templates/base.html с Bootstrap
<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>
Знакомый каркас base.html из урока 3, обросший Bootstrap-классами. Скрипт bootstrap.bundle.min.js подключается перед </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/
Классическая ловушка такого разделения - циклический импорт: routes.py нужен app, а app.py нужны маршруты. Решения: импортировать маршруты в самом конце app.py или вынести создание приложения в фабрику create_app().

Это уже почти архитектура. Следующая ступень — 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))
Проверь себя
0 / 5

1. Где Flask по умолчанию ищет статические файлы и по какому URL они доступны?

2. Что вернёт url_for('static', filename='css/style.css')?

3. Зачем писать в шаблонах url_for вместо ручных путей вроде /static/style.css?

4. Что хранят в app.config?

5. Почему debug=True нельзя включать на продакшн-сервере?

Карточки терминов
Запомнено: 0 / 6
Практика

Реализуйте упрощённую url_for: для endpoint "static" возвращайте "/static/" + filename, для остальных берите путь из словаря routes, а дополнительные именованные аргументы приклеивайте как query-строку: перед первым параметром "?", между остальными "&". Проверьте тремя вызовами из print-ов.

practice.py
Вопросы и ответы по уроку

Почему не подключается 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.

Проверьте знания по Flask

В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.

Тест по Flask: 20 задач