Skip to content

Static Files

Starlette-Templates provides static file handling with

  • Pre-compressed gzip file support
  • Automatic HTTP caching headers
  • Multi-directory fallback support
  • ETag and Last-Modified headers

StaticFiles

StaticFiles serves static files with support for pre-compressed .gz files that are automatically decompressed in browsers and multi-directory fallback support.

Basic Usage

from starlette.applications import Starlette
from starlette.routing import Mount
from starlette_templates.staticfiles import StaticFiles

app = Starlette(
    routes=[
        Mount("/static", StaticFiles(directories=["static"]), name="static"),
    ]
)

Pre-compressed Files

Create pre-compressed versions of your static files for better performance:

# Create pre-compressed CSS files
gzip -k static/css/bootstrap.css
gzip -k static/css/style.css

# Create pre-compressed JavaScript files
gzip -k static/js/app.js
gzip -k static/js/vendor.js

This creates .gz versions alongside the original files:

static/
├── css/
│   ├── bootstrap.css
│   ├── bootstrap.css.gz
│   ├── style.css
│   └── style.css.gz
└── js/
    ├── app.js
    ├── app.js.gz
    ├── vendor.js
    └── vendor.js.gz

Using Pre-compressed Files

Reference the .gz files in your templates:

<!DOCTYPE html>
<html>
<head>
    <!-- Browser receives decompressed CSS with caching headers -->
    <link rel="stylesheet" href="{{ url_for('static', path='/css/bootstrap.css.gz') }}">
    <link rel="stylesheet" href="{{ url_for('static', path='/css/style.css.gz') }}">
</head>
<body>
    <script src="{{ url_for('static', path='/js/vendor.js.gz') }}"></script>
    <script src="{{ url_for('static', path='/js/app.js.gz') }}"></script>
</body>
</html>

The browser automatically decompresses the files and applies proper caching headers.

How It Works

When serving a .gz file, StaticFiles:

  1. Serves the pre-compressed content
  2. Sets Content-Encoding: gzip header
  3. Sets appropriate Content-Type based on the original file extension
  4. Adds ETag and Last-Modified headers for caching
  5. Returns 304 Not Modified for cached requests

Configuration

from pathlib import Path

static_files = StaticFiles(
    directories=[Path("static")],
    packages=None,           # Optional: list of package names
    html=False,              # Don't serve HTML files
    check_dir=True,          # Verify directory exists
)

Multi-Directory Support

StaticFiles serves static files from multiple directories with fallback priority, useful for:

  • Theme overrides
  • Plugin systems
  • Framework + application static files
  • Development vs production assets

Basic Usage

from pathlib import Path
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette_templates.staticfiles import StaticFiles

static_files = StaticFiles(
    directories=[
        Path("myapp/static"),        # Check here first
        Path("framework/static"),    # Fallback to here
        Path("vendor/static"),       # Then here
    ]
)

app = Starlette(
    routes=[
        Mount("/static", static_files, name="static"),
    ]
)

Fallback Priority

Files are resolved in order:

  1. If myapp/static/css/style.css exists, serve it
  2. Otherwise, if framework/static/css/style.css exists, serve it
  3. Otherwise, if vendor/static/css/style.css exists, serve it
  4. Otherwise, return 404

This allows you to override framework files with your application files.

Example Directory Structure

myapp/
└── static/
    └── css/
        └── custom.css          # Application-specific styles

framework/
└── static/
    ├── css/
    │   ├── base.css           # Framework base styles
    │   └── components.css     # Framework components
    └── js/
        └── framework.js       # Framework JavaScript

vendor/
└── static/
    └── js/
        ├── jquery.js          # Third-party libraries
        └── bootstrap.js

Usage in Templates

<link rel="stylesheet" href="{{ url_for('static', path='/css/base.css') }}">
<!-- Served from framework/static/css/base.css -->

<link rel="stylesheet" href="{{ url_for('static', path='/css/custom.css') }}">
<!-- Served from myapp/static/css/custom.css -->

<script src="{{ url_for('static', path='/js/jquery.js') }}"></script>
<!-- Served from vendor/static/js/jquery.js -->

Combining Multi-Directory and Gzip Support

StaticFiles automatically supports both multi-directory fallback and pre-compressed files:

from pathlib import Path
from starlette_templates.staticfiles import StaticFiles

# StaticFiles automatically supports .gz files and multiple directories
static_files = StaticFiles(
    directories=[
        Path("myapp/static"),
        Path("framework/static"),
    ]
)

Then create compressed versions in any directory:

gzip -k myapp/static/css/custom.css
gzip -k framework/static/css/base.css

HTTP Caching

StaticFiles supports HTTP caching:

ETag Headers

Automatically generated based on file modification time and size:

ETag: "abc123-1234567890"

Last-Modified Headers

Set from file modification time:

Last-Modified: Wed, 20 Dec 2023 12:00:00 GMT

304 Not Modified

When client sends If-None-Match or If-Modified-Since headers that match:

HTTP/1.1 304 Not Modified
ETag: "abc123-1234567890"

Cache-Control Headers

Set appropriate cache headers for static files:

from starlette.responses import Response

# In your static files configuration
static_files = StaticFiles(
    directories=["static"],
    # Files are served with cache headers
)

Response includes:

Cache-Control: public, max-age=3600

Best Practices

Pre-compression

Pre-compress large static files for production:

# Find and compress all CSS and JS files
find static -type f \( -name "*.css" -o -name "*.js" \) -exec gzip -k {} \;

# Or use a more selective approach
gzip -k static/css/bootstrap.css
gzip -k static/js/app.bundle.js

Build Process Integration

Integrate compression into your build process:

# build.py
import gzip
import shutil
from pathlib import Path

def compress_static_files():
    static_dir = Path("static")

    for file in static_dir.rglob("*.css"):
        with open(file, 'rb') as f_in:
            with gzip.open(f"{file}.gz", 'wb') as f_out:
                shutil.copyfileobj(f_in, f_out)

    for file in static_dir.rglob("*.js"):
        with open(file, 'rb') as f_in:
            with gzip.open(f"{file}.gz", 'wb') as f_out:
                shutil.copyfileobj(f_in, f_out)

if __name__ == "__main__":
    compress_static_files()

Directory Organization

Organize static files by type and priority:

static/
├── css/
│   ├── vendor/      # Third-party CSS
│   ├── base/        # Base styles
│   └── components/  # Component styles
├── js/
│   ├── vendor/      # Third-party JS
│   └── app/         # Application JS
├── img/
│   ├── icons/
│   └── photos/
└── fonts/

Development vs Production

Use different configurations for development and production:

import os
from pathlib import Path
from starlette_templates.staticfiles import StaticFiles

# StaticFiles works for both development and production
static_files = StaticFiles(directories=["static"])

# In production, ensure you have .gz files created for better performance

CDN Integration

For production, consider using a CDN with fallback:

# In your context processor
async def add_static_context(request: Request) -> dict:
    cdn_url = os.getenv("CDN_URL", "")

    return {
        "static_url": cdn_url if cdn_url else request.url_for("static", path="/"),
    }

In templates:

<link rel="stylesheet" href="{{ static_url }}css/style.css">

Troubleshooting

File Not Found

If static files aren't loading:

  1. Verify the directory path is correct
  2. Check file permissions
  3. Ensure the mount path is correct
  4. Verify the static files route comes before catch-all routes
app = Starlette(
    routes=[
        # Static files BEFORE template files
        Mount("/static", StaticFiles(directories=["static"]), name="static"),
        # Template files last
        Mount("/", TemplateRouter()),
    ]
)

Gzip Not Working

If pre-compressed files aren't being served:

  1. Verify .gz files exist
  2. Check file naming (must end in .gz)
  3. Ensure you're using starlette-templates StaticFiles (not Starlette's built-in StaticFiles)
  4. Check browser accepts gzip encoding

Caching Issues

If files aren't being cached:

  1. Check browser caching is enabled
  2. Verify ETag headers are being sent
  3. Clear browser cache for testing
  4. Check proxy/CDN configuration doesn't strip cache headers