""" Public routes for StarPunk Handles public-facing pages including homepage and note permalinks. No authentication required for these routes. """ import hashlib from datetime import datetime, timedelta from flask import Blueprint, abort, render_template, Response, current_app from starpunk.notes import list_notes, get_note from starpunk.feed import generate_feed_streaming # Create blueprint bp = Blueprint("public", __name__) # Simple in-memory cache for RSS feed note list # Caches the database query results to avoid repeated DB hits # XML is streamed, not cached (memory optimization for large feeds) # Structure: {'notes': list[Note], 'timestamp': datetime} _feed_cache = {"notes": None, "timestamp": None} @bp.route("/") def index(): """ Homepage displaying recent published notes Returns: Rendered homepage template with note list Template: templates/index.html Microformats: h-feed containing h-entry items """ # Get recent published notes (limit 20) notes = list_notes(published_only=True, limit=20) return render_template("index.html", notes=notes) @bp.route("/note/") def note(slug: str): """ Individual note permalink page Args: slug: URL-safe note identifier Returns: Rendered note template with full content Raises: 404: If note not found or not published Template: templates/note.html Microformats: h-entry """ # Get note by slug note_obj = get_note(slug=slug) # Return 404 if note doesn't exist or isn't published if not note_obj or not note_obj.published: abort(404) return render_template("note.html", note=note_obj) @bp.route("/feed.xml") def feed(): """ RSS 2.0 feed of published notes Generates standards-compliant RSS 2.0 feed using memory-efficient streaming. Instead of building the entire feed in memory, yields XML chunks directly to the client for optimal memory usage with large feeds. Cache duration is configurable via FEED_CACHE_SECONDS (default: 300 seconds = 5 minutes). Cache stores note list to avoid repeated database queries, but streaming prevents holding full XML in memory. Returns: Streaming XML response with RSS feed Headers: Content-Type: application/rss+xml; charset=utf-8 Cache-Control: public, max-age={FEED_CACHE_SECONDS} Streaming Strategy: - Database query cached (avoid repeated DB hits) - XML generation streamed (avoid full XML in memory) - Client-side: Cache-Control header with max-age Performance: - Memory usage: O(1) instead of O(n) for feed size - Latency: Lower time-to-first-byte (TTFB) - Recommended for feeds with 100+ items Examples: >>> # Request streams XML directly to client >>> response = client.get('/feed.xml') >>> response.status_code 200 >>> response.headers['Content-Type'] 'application/rss+xml; charset=utf-8' """ # Get cache duration from config (in seconds) cache_seconds = current_app.config.get("FEED_CACHE_SECONDS", 300) cache_duration = timedelta(seconds=cache_seconds) now = datetime.utcnow() # Check if note list cache is valid # We cache the note list to avoid repeated DB queries, but still stream the XML if _feed_cache["notes"] and _feed_cache["timestamp"]: cache_age = now - _feed_cache["timestamp"] if cache_age < cache_duration: # Use cached note list notes = _feed_cache["notes"] else: # Cache expired, fetch fresh notes max_items = current_app.config.get("FEED_MAX_ITEMS", 50) notes = list_notes(published_only=True, limit=max_items) _feed_cache["notes"] = notes _feed_cache["timestamp"] = now else: # No cache, fetch notes max_items = current_app.config.get("FEED_MAX_ITEMS", 50) notes = list_notes(published_only=True, limit=max_items) _feed_cache["notes"] = notes _feed_cache["timestamp"] = now # Generate streaming response # This avoids holding the full XML in memory - chunks are yielded directly max_items = current_app.config.get("FEED_MAX_ITEMS", 50) generator = generate_feed_streaming( site_url=current_app.config["SITE_URL"], site_name=current_app.config["SITE_NAME"], site_description=current_app.config.get("SITE_DESCRIPTION", ""), notes=notes, limit=max_items, ) # Return streaming response with appropriate headers response = Response(generator, mimetype="application/rss+xml; charset=utf-8") response.headers["Cache-Control"] = f"public, max-age={cache_seconds}" return response