Зачем нужна статика

Статические файлы — это файлы, которые не меняются в процессе работы приложения:

  • CSS — стили оформления сайта
  • JavaScript — интерактивность на страницах
  • Изображения — логотипы, иконки, фоны

FastAPI не умеет раздавать статику по умолчанию — это нужно настраивать отдельно через StaticFiles.


Структура проекта

В проекте есть следующая структура файлов:

                pytexpert.ru/
├── app/
│   ├── core/
│   │   ├── base/
│   │   │   ├── base.css
│   │   │   ├── base.js
│   │   │   └── images/
│   │   │       └── logo.png
│   │   └── (другие модули)
│   ├── static/
│   │   └── (синхронизируется из core/)
│   ├── utils/
│   │   ├── cors.py
│   │   ├── log.py
│   │   ├── routes.py
│   │   └── static.py  ← создаём сейчас
│   └── main.py
├── .env
├── .env_example
├── requirements.txt
└── .gitignore

Статические файлы хранятся в app/core/ и синхронизируются в app/static/ при запуске приложения. Синхронизация копирует файлы из app/core/ в app/static/, сохраняя структуру папок.

Например:

  • app/core/base/base.cssapp/static/core/base/base.css
  • app/core/base/images/logo.pngapp/static/core/base/images/logo.png

Создание утилиты static.py

Создадим файл app/utils/static.py:

                import shutil
import time
from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse

STATIC_VERSION = str(int(time.time()))

APP_DIR = Path(__file__).parent.parent
CORE_DIR = APP_DIR / "core"
STATIC_DIR = APP_DIR / "static"
DOWNLOAD_DIR = APP_DIR.parent / "download"


def get_static_version() -> str:
    return STATIC_VERSION


def sync_static():
    """Синхронизация статических файлов из core/ в static/"""
    extensions = {'.css', '.js', '.woff2', '.woff', '.ttf', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.map'}
    
    if not CORE_DIR.exists():
        return 0, 0
    
    copied = 0
    skipped = 0
    
    for filepath in CORE_DIR.rglob('*'):
        if not filepath.is_file():
            continue
        
        if filepath.suffix not in extensions:
            continue
        
        rel_path = filepath.relative_to(APP_DIR)
        dest_path = STATIC_DIR / rel_path
        
        if dest_path.exists() and filepath.stat().st_mtime <= dest_path.stat().st_mtime:
            skipped += 1
            continue
        
        dest_path.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(filepath, dest_path)
        copied += 1
    
    return copied, skipped


def setup_static(app: FastAPI) -> None:
    """Настройка раздачи статических файлов"""
    
    STATIC_DIR.mkdir(parents=True, exist_ok=True)
    DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
    
    sync_static()
    
    app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
    app.mount("/download", StaticFiles(directory=str(DOWNLOAD_DIR)), name="download")

    @app.get("/favicon.ico", include_in_schema=False)
    async def favicon_redirect():
        return RedirectResponse(url="/static/core/base/images/favicon.ico")

Разбор кода static.py

  • sync_static() — копирует файлы из app/core/ в app/static/. Копируются только файлы с нужными расширениями.
  • STATIC_VERSION — версия статики на основе времени запуска. Используется для обновления кеша браузера.
  • app.mount() — монтирует директорию по указанному URL. Теперь файлы из app/static/ доступны по адресу /static/...
  • /favicon.ico — редирект на реальный файл фавикона.

Подключение к main.py

Обновим app/main.py, чтобы подключить статику:

                from fastapi import FastAPI
from .config import settings
from .utils.cors import setup_cors
from .utils.lifespan import get_lifespan
from .utils.routes import setup_routes
from .utils.static import setup_static

# Создаём приложение
app = FastAPI(
    title=settings.APP_NAME,
    description=settings.APP_DESCRIPTION,
    version=settings.APP_VERSION,
    lifespan=get_lifespan()
)

# Настройки
setup_cors(app)      # из статьи про CORS
setup_static(app)    # статика
setup_routes(app)    # из статьи про роутинг

# Запуск
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "app.main:app",
        host=settings.APP_HOST,
        port=settings.APP_PORT
    )

Разбор обновлённого main.py

  • setup_cors(app) — настройка CORS
  • setup_static(app) — подключаем раздачу статических файлов
  • setup_routes(app) — подключаем роутеры
  • lifespan — управление жизненным циклом с логированием

Фильтр static в шаблонах

В шаблонах мы используем фильтр |static для генерации путей к статическим файлам.

Пример из базового шаблона app/core/base/base.html:

                <link rel="stylesheet" href="/static/core/base/base.css?v=1789232230">
<script src="/static/core/base/base.js?v=1789232230"></script>

Фильтр static преобразует путь core/base/base.css в полный URL /static/core/base/base.css.

Фильтр определён в app/core/base/route.py:

                def static_url(path: str) -> str:
    return f"/static/{path}"

templates.env.globals["static"] = static_url

Проверка работы

Запустите сервер:

uvicorn app.main:app --reload

Теперь проверьте доступность статических файлов в браузере:

  • http://127.0.0.1:8000/static/core/base/base.css — должен открыться CSS-файл
  • http://127.0.0.1:8000/static/core/base/images/logo.png — должно отобразиться изображение

Итог

Сегодня мы сделали:

  • ✅ Создали app/utils/static.py — настройку раздачи статики с синхронизацией
  • ✅ Подключили статику в app/main.py
  • ✅ Узнали, как работает фильтр |static в шаблонах
  • ✅ Проверили работу статики в браузере

Теперь ваше приложение умеет отдавать статические файлы. Это основа для стилизации сайта — в следующих статьях мы создадим базовый шаблон и главную страницу.

После проверки закоммитьте и запушьте изменения в GitHub:

git add .
git commit -m "Добавлена раздача статических файлов"
git push

Ищете хостинг для размещения вашего проекта в сети Интернет?
Посмотрите здесь →

Реклама. ООО «Бегет» ИНН 7801451618 erid: 2VtzqxQSRkE