Files
kennethreitz.org/tuftecms/app.py
T
kennethreitz 8267a8e646 Performance: add GZip compression, externalize CSS, bump cache, reduce workers
- Add Starlette GZipMiddleware for response compression (~60% smaller HTML)
- Extract 290 lines of inline CSS to /static/site.css (browser-cacheable)
- Bump HTML Cache-Control from 5min to 1hr
- Reduce Granian workers from 4 to 2 (matches shared-cpu-2x)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:43:17 -04:00

223 lines
7.7 KiB
Python

"""Application factory for TufteCMS."""
import os
from pathlib import Path
from flask import Flask
from .blueprints import api_bp, content_bp, feeds_bp, main_bp
from .config import Config
def warm_caches():
"""Warm up all caches at application startup."""
print("🔥 Starting background cache warming...")
try:
from .core.cache import (
get_blog_cache,
get_connections_cache,
get_outlines_cache,
get_quotes_cache,
get_sidenotes_cache,
get_terms_cache,
)
# Load all caches
blog_data = get_blog_cache()
sidenotes_data = get_sidenotes_cache()
outlines_data = get_outlines_cache()
quotes_data = get_quotes_cache()
connections_data = get_connections_cache()
terms_data = get_terms_cache()
# Print cache statistics
print(f"✅ Blog cache: {blog_data['stats']['total_posts']} posts loaded")
print(
f"✅ Sidenotes cache: {sidenotes_data['stats']['total_sidenotes']} sidenotes from {sidenotes_data['stats']['total_articles']} articles"
)
print(
f"✅ Outlines cache: {outlines_data['stats']['total_headings']} headings from {outlines_data['stats']['total_articles']} articles"
)
print(
f"✅ Quotes cache: {quotes_data['stats']['total_quotes']} quotes from {quotes_data['stats']['total_articles']} articles"
)
print(
f"✅ Connections cache: {connections_data['stats']['total_outgoing']} outgoing, {connections_data['stats']['total_incoming']} incoming connections"
)
print(
f"✅ Terms cache: {terms_data['stats']['total_terms']} terms with {terms_data['stats']['total_references']} references"
)
print("🚀 All caches warmed up successfully!")
except Exception as e:
print(f"❌ Error warming caches: {e}")
# Don't fail startup if cache warming fails
import traceback
traceback.print_exc()
def warm_caches_background(app):
"""Warm up caches in background thread."""
import threading
def cache_worker():
with app.app_context():
warm_caches()
# Start cache warming in background thread
cache_thread = threading.Thread(target=cache_worker, daemon=True)
cache_thread.start()
print("🚀 Server starting... (caches warming in background)")
def create_app(config_class=Config):
"""Create and configure the Flask application."""
# Get absolute paths for directories
base_dir = Path(__file__).parent.parent
template_dir = Path(__file__).parent / "templates"
static_dir = Path(__file__).parent / "static"
app = Flask(
__name__,
template_folder=str(template_dir),
static_folder=str(static_dir),
static_url_path="/static",
)
app.config.from_object(config_class)
# Register template filters
register_template_filters(app)
# Register context processors
register_context_processors(app)
# Register blueprints
app.register_blueprint(main_bp)
app.register_blueprint(api_bp, url_prefix="/api")
app.register_blueprint(feeds_bp)
app.register_blueprint(content_bp)
# Warm up caches in background
warm_caches_background(app)
return app
def register_template_filters(app):
"""Register custom Jinja2 template filters."""
@app.template_filter("strftime")
def strftime_filter(date, fmt="%Y-%m-%d"):
"""Format a datetime object using strftime."""
from datetime import datetime
if date is None:
return ""
if isinstance(date, str) and date.lower() == "now":
date = datetime.now()
return date.strftime(fmt)
@app.template_filter("unescape")
def unescape_filter(text):
"""Unescape HTML entities in text."""
import html
if text is None:
return ""
return html.unescape(text)
def register_context_processors(app):
"""Register context processors to inject data into all templates."""
@app.context_processor
def inject_index_counts():
"""Make index counts available to all templates."""
try:
# Import here to avoid circular imports
from .core.cache import (
get_connections_cache,
get_outlines_cache,
get_quotes_cache,
get_sidenotes_cache,
get_terms_cache,
)
# Get actual cache data
sidenotes_data = get_sidenotes_cache()
outlines_data = get_outlines_cache()
quotes_data = get_quotes_cache()
connections_data = get_connections_cache()
terms_data = get_terms_cache()
return {
"index_counts": {
"sidenotes": sidenotes_data["stats"].get("total_sidenotes", 0),
"outlines": outlines_data["stats"].get("total_headings", 0),
"quotes": quotes_data["stats"].get("total_quotes", 0),
"connections_outgoing": connections_data["stats"].get(
"total_outgoing", 0
),
"connections_incoming": connections_data["stats"].get(
"total_incoming", 0
),
"terms": terms_data["stats"].get("total_terms", 0),
"terms_total_refs": terms_data["stats"].get("total_references", 0),
}
}
except Exception as e:
print(f"Error getting index counts: {e}")
# Fallback to prevent template errors
return {
"index_counts": {
"sidenotes": 0,
"outlines": 0,
"quotes": 0,
"connections_outgoing": 0,
"connections_incoming": 0,
"terms": 0,
"terms_total_refs": 0,
}
}
@app.context_processor
def inject_pdf_availability():
"""Check if PDF generation is available."""
try:
from weasyprint import HTML
from weasyprint.text.fonts import FontConfiguration
return {"pdf_available": True}
except (ImportError, OSError):
return {"pdf_available": False}
@app.after_request
def add_security_headers(response):
"""Add security and content headers to all responses."""
# Security headers
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
response.headers['Permissions-Policy'] = 'interest-cohort=()' # Disable FLoC
# Content headers for HTML responses
if response.content_type and "text/html" in response.content_type:
response.headers['Content-Language'] = 'en'
# Add cache control for HTML (short cache, must revalidate)
if not response.headers.get('Cache-Control'):
response.headers['Cache-Control'] = 'public, max-age=3600, must-revalidate'
# Inject a script right after <head> to immediately show content
content = response.get_data(as_text=True)
if "<head>" in content and "visibility: hidden" in content:
# Add script to immediately show content
immediate_script = """<script>
// Immediately show content to prevent invisible page
document.documentElement.classList.add('loaded');
</script>"""
content = content.replace("<head>", "<head>" + immediate_script)
response.set_data(content)
return response