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:
- Serves the pre-compressed content
- Sets
Content-Encoding: gzipheader - Sets appropriate
Content-Typebased on the original file extension - Adds
ETagandLast-Modifiedheaders for caching - Returns
304 Not Modifiedfor 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:
- If
myapp/static/css/style.cssexists, serve it - Otherwise, if
framework/static/css/style.cssexists, serve it - Otherwise, if
vendor/static/css/style.cssexists, serve it - 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:
- Verify the directory path is correct
- Check file permissions
- Ensure the mount path is correct
- 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:
- Verify
.gzfiles exist - Check file naming (must end in
.gz) - Ensure you're using starlette-templates
StaticFiles(not Starlette's built-inStaticFiles) - Check browser accepts gzip encoding
Caching Issues
If files aren't being cached:
- Check browser caching is enabled
- Verify ETag headers are being sent
- Clear browser cache for testing
- Check proxy/CDN configuration doesn't strip cache headers