# Phase 5: RSS Feed & Production Container - Implementation Design **Date**: 2025-11-18 **Phase**: 5 **Status**: Design Complete, Ready for Implementation **Dependencies**: Phase 4 (Web Interface) complete **Version**: 0.6.0 target ## Implementation Guidelines ### Version Management - **Current Version**: 0.5.1 (in `starpunk/__init__.py`) - **Target Version**: 0.6.0 (skip intermediate versions) - **Rationale**: Phase 5 introduces significant new features warranting minor version bump ### Git Workflow - **Branch Strategy**: Use feature branch `feature/phase-5-rss-container` - **Workflow**: 1. Create feature branch from main 2. Implement all Phase 5 features 3. Create PR for review 4. Merge to main when complete - **Rationale**: Cleaner history, easier rollback, follows project standards for larger features ## Executive Summary Phase 5 implements RSS feed generation with full IndieWeb compliance and creates a production-ready container for deployment testing. This phase completes the core V1 feature set for content syndication and enables testing of IndieAuth authentication in a production-like environment. **Key Deliverables**: - RSS 2.0 feed generation with proper microformats - Feed route and caching strategy - Production-ready container (Podman/Docker compatible) - Container configuration for public-facing deployment - IndieAuth testing capability with HTTPS - Container orchestration for development and production ## Scope ### In Scope for Phase 5 **RSS Feed Generation**: - RSS 2.0 compliant XML feed - Recent published notes (configurable limit) - Proper RFC-822 date formatting - HTML content with CDATA wrapping - Unique GUID for each entry - Channel metadata (title, link, description) - Feed caching (5-minute default) - Auto-discovery link in templates **Production Container**: - Containerfile (Podman/Docker compatible) - Multi-stage build for optimization - Gunicorn WSGI server configuration - Health check endpoint - Volume mounts for persistent data - Environment variable configuration - Production-ready security settings - Container compose configuration **Deployment Features**: - HTTPS support via reverse proxy - IndieAuth callback handling - Production logging configuration - Graceful shutdown handling - Data persistence strategy - Container networking setup ### Out of Scope **Deferred to Phase 6+**: - Micropub endpoint implementation - Feed pagination - Feed filtering/categories - Atom feed format - JSON Feed format - Feed validation UI - Auto-update notification - Feed analytics - Multiple feed types - Advanced caching strategies ## Architecture Overview ### Component Diagram ``` ┌────────────────────────────────────────────────────────┐ │ Client (Browser/RSS Reader) │ └────────────┬───────────────────────────────────────────┘ │ │ HTTPS (port 443) ↓ ┌────────────────────────────────────────────────────────┐ │ Reverse Proxy (Caddy/Nginx) - Host │ │ - SSL Termination │ │ - Proxy to container │ └────────────┬───────────────────────────────────────────┘ │ │ HTTP (port 8000) ↓ ┌────────────────────────────────────────────────────────┐ │ Container (starpunk:0.6.0) │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Gunicorn WSGI Server (4 workers) │ │ │ └─────────────┬──────────────────────────────────────┘ │ │ │ │ │ ↓ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Flask Application │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ Web Routes │ │ Feed Route │ │ │ │ │ │ (existing) │ │ /feed.xml │ │ │ │ │ └──────────────┘ └──────┬───────┘ │ │ │ │ │ │ │ │ │ ↓ │ │ │ │ ┌──────────────────┐ │ │ │ │ │ Feed Generator │ │ │ │ │ │ (new module) │ │ │ │ │ └──────┬───────────┘ │ │ │ └───────────────────────────┼────────────────────────┘ │ │ │ │ │ ↓ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Business Logic (Notes Module) │ │ │ └─────────────┬──────────────────────────────────────┘ │ │ │ │ │ ↓ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Data Layer (Volume Mount) │ │ │ │ ┌──────────────┐ ┌────────────────┐ │ │ │ │ │ Markdown │ │ SQLite DB │ │ │ │ │ │ Files │ │ starpunk.db │ │ │ │ │ └──────────────┘ └────────────────┘ │ │ │ └────────────────────────────────────────────────────┘ │ │ /data (bind mount to host) │ └────────────────────────────────────────────────────────┘ │ ↓ ┌────────────────────────────────────────────────────────┐ │ Host Filesystem (Persistent Data) │ │ ./container-data/ │ │ ├── notes/ (markdown files) │ │ └── starpunk.db (database) │ └────────────────────────────────────────────────────────┘ ``` ### Feed Generation Flow ``` RSS Reader → GET /feed.xml ↓ Route Handler → Check cache (5 min) ↓ Cache Hit? → Yes → Return cached XML ↓ No Feed Generator → list_notes(published=True, limit=50) ↓ Notes Module → Query database + read files ↓ Feed Generator → Build RSS XML: - Channel metadata - For each note: - Title (first line or timestamp) - Link (permalink) - Description (HTML content) - PubDate (RFC-822 format) - GUID (permalink) ↓ Cache XML (5 minutes) ↓ RSS Reader ← Return application/rss+xml ``` ## RSS Feed Specification ### Module: `starpunk/feed.py` **Purpose**: Generate RSS 2.0 compliant feed from published notes **Functions**: ```python def generate_feed(site_url: str, site_name: str, site_description: str, notes: list[Note], limit: int = 50) -> str: """ Generate RSS 2.0 XML feed Args: site_url: Base URL of the site site_name: Site title for the feed site_description: Site description notes: List of Note objects to include limit: Maximum number of entries (default 50) Returns: RSS 2.0 XML string Raises: ValueError: If site_url or site_name is empty """ def format_rfc822_date(dt: datetime) -> str: """ Format datetime to RFC-822 format for RSS Args: dt: Datetime object to format Returns: RFC-822 formatted date string Example: "Mon, 18 Nov 2024 12:00:00 +0000" """ def get_note_title(note: Note) -> str: """ Extract title from note content (first line or timestamp) Args: note: Note object Returns: Title string (max 100 chars) """ def clean_html_for_rss(html: str) -> str: """ Ensure HTML is safe for RSS CDATA wrapping Args: html: Rendered HTML content Returns: Cleaned HTML safe for CDATA """ ``` ### RSS Feed Structure **Channel Elements**: ```xml Site Name https://example.com/ Site description en-us Mon, 18 Nov 2024 12:00:00 +0000 Note Title or Timestamp https://example.com/note/my-note-slug https://example.com/note/my-note-slug Mon, 18 Nov 2024 10:30:00 +0000 Rendered HTML content goes here

]]>
``` **Standards Compliance**: - RSS 2.0 specification - RFC-822 date format - CDATA wrapping for HTML content - Atom self-link for feed discovery - Proper GUID (permalink as GUID) - XML declaration with UTF-8 encoding ### Route: `GET /feed.xml` **Handler**: `public.feed()` **Location**: Add to `starpunk/routes/public.py` **Implementation**: ```python from flask import Response, current_app from starpunk.feed import generate_feed from starpunk.notes import list_notes import hashlib # Simple in-memory cache _feed_cache = {'xml': None, 'timestamp': None, 'etag': None} @bp.route("/feed.xml") def feed(): """ RSS 2.0 feed of published notes Returns: XML response with RSS feed Headers: Content-Type: application/rss+xml; charset=utf-8 Cache-Control: public, max-age=300 ETag: hash of feed content Caching: - Server-side: 5 minute in-memory cache - Client-side: 5 minute cache via Cache-Control - Conditional requests: ETag support """ from datetime import datetime, timedelta # Check if cache is valid (5 minutes) cache_duration = timedelta(minutes=5) now = datetime.utcnow() if _feed_cache['xml'] and _feed_cache['timestamp']: if now - _feed_cache['timestamp'] < cache_duration: # Return cached feed with ETag response = Response( _feed_cache['xml'], mimetype='application/rss+xml; charset=utf-8' ) response.headers['Cache-Control'] = 'public, max-age=300' response.headers['ETag'] = _feed_cache['etag'] return response # Generate fresh feed notes = list_notes(published_only=True, limit=50) feed_xml = generate_feed( site_url=current_app.config['SITE_URL'], site_name=current_app.config['SITE_NAME'], site_description=current_app.config.get('SITE_DESCRIPTION', ''), notes=notes ) # Calculate ETag etag = hashlib.md5(feed_xml.encode('utf-8')).hexdigest() # Update cache _feed_cache['xml'] = feed_xml _feed_cache['timestamp'] = now _feed_cache['etag'] = etag # Return response response = Response(feed_xml, mimetype='application/rss+xml; charset=utf-8') response.headers['Cache-Control'] = 'public, max-age=300' response.headers['ETag'] = etag return response ``` ### Template Updates **Add to `templates/base.html`**: ```html ``` **Add feed link to homepage** (`templates/index.html`): ```html ``` ## Production Container Specification ### Containerfile (Podman/Docker Compatible) **Location**: `/home/phil/Projects/starpunk/Containerfile` **Purpose**: Multi-stage production-optimized container **Contents**: ```dockerfile # syntax=docker/dockerfile:1 # Build stage - minimal Python build environment FROM python:3.11-slim AS builder # Install uv for fast dependency installation COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv WORKDIR /build # Copy dependency files COPY requirements.txt . # Create virtual environment and install dependencies RUN uv venv /opt/venv && \ . /opt/venv/bin/activate && \ uv pip install --no-cache -r requirements.txt # Runtime stage - minimal runtime image FROM python:3.11-slim # Create non-root user for security RUN useradd --create-home --shell /bin/bash starpunk && \ mkdir -p /app /data/notes && \ chown -R starpunk:starpunk /app /data # Copy virtual environment from builder COPY --from=builder /opt/venv /opt/venv # Set PATH to use venv ENV PATH="/opt/venv/bin:$PATH" \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 WORKDIR /app # Copy application code COPY --chown=starpunk:starpunk . . # Switch to non-root user USER starpunk # Expose application port EXPOSE 8000 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD python3 -c "import httpx; httpx.get('http://localhost:8000/health', timeout=2.0)" || exit 1 # Run gunicorn CMD ["gunicorn", \ "--bind", "0.0.0.0:8000", \ "--workers", "4", \ "--worker-class", "sync", \ "--worker-tmp-dir", "/dev/shm", \ "--max-requests", "1000", \ "--max-requests-jitter", "50", \ "--timeout", "30", \ "--graceful-timeout", "30", \ "--access-logfile", "-", \ "--error-logfile", "-", \ "--log-level", "info", \ "app:app"] ``` ### Container Compose Configuration **Location**: `/home/phil/Projects/starpunk/compose.yaml` **Purpose**: Production deployment orchestration **Contents**: ```yaml version: '3.8' services: starpunk: image: starpunk:0.6.0 container_name: starpunk build: context: . dockerfile: Containerfile # Restart policy restart: unless-stopped # Ports - only expose to localhost ports: - "127.0.0.1:8000:8000" # Environment variables env_file: - .env environment: # Override .env for container environment - FLASK_ENV=production - FLASK_DEBUG=0 - DATA_PATH=/data - NOTES_PATH=/data/notes - DATABASE_PATH=/data/starpunk.db # Volume mounts for persistent data volumes: - ./container-data:/data:rw # Health check healthcheck: test: ["CMD", "python3", "-c", "import httpx; httpx.get('http://localhost:8000/health', timeout=2.0)"] interval: 30s timeout: 3s retries: 3 start_period: 10s # Resource limits deploy: resources: limits: cpus: '1.0' memory: 512M reservations: cpus: '0.25' memory: 128M # Logging logging: driver: "json-file" options: max-size: "10m" max-file: "3" # Network networks: - starpunk-net networks: starpunk-net: driver: bridge ``` ### Health Check Endpoint **Add to `starpunk/__init__.py`**: ```python @app.route('/health') def health_check(): """ Health check endpoint for containers and monitoring Returns: JSON with status and basic info Response codes: 200: Application healthy 500: Application unhealthy Checks: - Database connectivity - File system access - Basic application state """ from flask import jsonify import os try: # Check database from starpunk.database import get_db with app.app_context(): db = get_db(app) db.execute("SELECT 1").fetchone() db.close() # Check file system data_path = app.config['DATA_PATH'] if not os.path.exists(data_path): raise Exception("Data path not accessible") return jsonify({ 'status': 'healthy', 'version': app.config.get('VERSION', '0.6.0'), 'environment': app.config.get('ENV', 'unknown') }), 200 except Exception as e: return jsonify({ 'status': 'unhealthy', 'error': str(e) }), 500 ``` ### Container Build & Run Instructions **Build container**: ```bash # Using Docker docker build -t starpunk:0.6.0 -f Containerfile . # Using Podman podman build -t starpunk:0.6.0 -f Containerfile . ``` **Run container (development)**: ```bash # Create data directory mkdir -p container-data/notes # Run with docker docker run -d \ --name starpunk \ -p 127.0.0.1:8000:8000 \ -v $(pwd)/container-data:/data:rw \ --env-file .env \ starpunk:0.6.0 # Run with podman podman run -d \ --name starpunk \ -p 127.0.0.1:8000:8000 \ -v $(pwd)/container-data:/data:rw,Z \ --env-file .env \ starpunk:0.6.0 ``` **Run with compose**: ```bash # Docker Compose docker compose up -d # Podman Compose podman-compose up -d ``` ### Reverse Proxy Configuration **Caddy** (recommended for auto-HTTPS): **Location**: `/home/phil/Projects/starpunk/Caddyfile.example` ```caddy # Caddyfile for StarPunk reverse proxy # Rename to Caddyfile and update domain your-domain.com { # Reverse proxy to container reverse_proxy localhost:8000 # Logging log { output file /var/log/caddy/starpunk.log } # Security headers header { # Remove server header -Server # Security headers Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" X-XSS-Protection "1; mode=block" Referrer-Policy "strict-origin-when-cross-origin" } # Compress responses encode gzip zstd # Static file caching @static { path /static/* } header @static Cache-Control "public, max-age=31536000, immutable" } ``` **Nginx** (alternative): **Location**: `/home/phil/Projects/starpunk/nginx.conf.example` ```nginx # Nginx configuration for StarPunk # Save to /etc/nginx/sites-available/starpunk upstream starpunk { server localhost:8000; } server { listen 80; server_name your-domain.com; # Redirect to HTTPS return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name your-domain.com; # SSL certificates (use certbot) ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; # SSL configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Logging access_log /var/log/nginx/starpunk-access.log; error_log /var/log/nginx/starpunk-error.log; # Max upload size client_max_body_size 10M; # Proxy to container location / { proxy_pass http://starpunk; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Timeouts proxy_connect_timeout 30s; proxy_send_timeout 30s; proxy_read_timeout 30s; } # Static files caching location /static/ { proxy_pass http://starpunk; proxy_cache_valid 200 1y; add_header Cache-Control "public, max-age=31536000, immutable"; } # RSS feed caching location /feed.xml { proxy_pass http://starpunk; proxy_cache_valid 200 5m; add_header Cache-Control "public, max-age=300"; } } ``` ## Configuration Updates ### Environment Variables **Add to `.env.example`**: ```bash # ============================================================================= # RSS FEED CONFIGURATION # ============================================================================= # Maximum number of items in RSS feed (default: 50) FEED_MAX_ITEMS=50 # Feed cache duration in seconds (default: 300 = 5 minutes) FEED_CACHE_SECONDS=300 # ============================================================================= # CONTAINER CONFIGURATION # ============================================================================= # Application version VERSION=0.6.0 # Environment: development or production ENVIRONMENT=production # Number of Gunicorn workers (default: 4) # Recommendation: (2 x CPU cores) + 1 WORKERS=4 # Worker timeout in seconds (default: 30) WORKER_TIMEOUT=30 # Max requests per worker before restart (prevents memory leaks) MAX_REQUESTS=1000 ``` ### Config Module Updates **Update `starpunk/config.py`**: ```python # RSS feed configuration app.config['FEED_MAX_ITEMS'] = int(os.getenv('FEED_MAX_ITEMS', '50')) app.config['FEED_CACHE_SECONDS'] = int(os.getenv('FEED_CACHE_SECONDS', '300')) # Application version app.config['VERSION'] = os.getenv('VERSION', '0.6.0') # Container/deployment configuration app.config['ENVIRONMENT'] = os.getenv('ENVIRONMENT', 'development') app.config['WORKERS'] = int(os.getenv('WORKERS', '4')) ``` ## Testing Strategy ### Unit Tests **File**: `tests/test_feed.py` **Tests**: ```python def test_generate_feed_basic(): """Test feed generation with minimal notes""" def test_generate_feed_empty(): """Test feed generation with no notes""" def test_generate_feed_limit(): """Test feed respects item limit""" def test_rfc822_date_format(): """Test RFC-822 date formatting""" def test_note_title_extraction(): """Test title extraction from note content""" def test_html_cleaning_for_cdata(): """Test HTML cleaning for CDATA wrapping""" def test_feed_xml_structure(): """Test XML structure is valid""" def test_feed_includes_all_required_elements(): """Test all required RSS elements present""" ``` **File**: `tests/test_routes_feed.py` **Tests**: ```python def test_feed_route_returns_xml(): """Test /feed.xml returns XML""" def test_feed_route_content_type(): """Test correct Content-Type header""" def test_feed_route_caching(): """Test feed caching behavior""" def test_feed_route_etag(): """Test ETag generation and validation""" def test_feed_route_only_published(): """Test feed only includes published notes""" def test_feed_route_limit(): """Test feed respects limit configuration""" ``` ### Integration Tests **Container Testing**: ```bash #!/bin/bash # Test container build and basic functionality # Build container podman build -t starpunk:0.6.0-test -f Containerfile . # Run container podman run -d \ --name starpunk-test \ -p 8000:8000 \ --env-file .env.test \ starpunk:0.6.0-test # Wait for startup sleep 5 # Test health endpoint curl -f http://localhost:8000/health || exit 1 # Test feed endpoint curl -f http://localhost:8000/feed.xml || exit 1 # Check feed is valid XML curl -s http://localhost:8000/feed.xml | xmllint --noout - || exit 1 # Cleanup podman stop starpunk-test podman rm starpunk-test echo "Container tests passed!" ``` ### Manual Validation **RSS Validation**: 1. Visit https://validator.w3.org/feed/ 2. Enter feed URL 3. Verify no errors 4. Check all required elements present **IndieAuth Testing**: 1. Deploy container with public HTTPS URL 2. Configure ADMIN_ME with your domain 3. Test IndieLogin authentication flow 4. Verify callback works correctly 5. Test session creation and validation **Feed Reader Testing**: 1. Add feed to RSS reader (Feedly, NewsBlur, etc.) 2. Verify notes appear correctly 3. Check formatting and links 4. Test update behavior ## File Organization ``` starpunk/ ├── feed.py # RSS feed generation (new) ├── routes/ │ └── public.py # Add feed route (update) ├── __init__.py # Add health check (update) └── config.py # Add feed config (update) /home/phil/Projects/starpunk/ (root) ├── Containerfile # Container build (new) ├── compose.yaml # Container orchestration (new) ├── Caddyfile.example # Caddy config (new) ├── nginx.conf.example # Nginx config (new) ├── .containerignore # Container build ignore (new) └── .env.example # Update with feed/container config templates/ ├── base.html # Add RSS discovery link (update) └── index.html # Add RSS link in nav (update) tests/ ├── test_feed.py # Feed generation tests (new) └── test_routes_feed.py # Feed route tests (new) docs/ ├── designs/ │ ├── phase-5-rss-and-container.md # This file │ └── phase-5-quick-reference.md # Implementation guide └── decisions/ └── ADR-014-rss-feed-implementation.md # RSS decision record ``` ## Security Considerations ### Feed Security - No authentication required (public feed) - Only published notes included - HTML content sanitized via markdown rendering - CDATA wrapping prevents XSS - No user input in feed generation - Rate limiting via reverse proxy ### Container Security - Non-root user (starpunk:starpunk) - Minimal base image (python:3.11-slim) - No unnecessary packages - Read-only root filesystem (optional) - Resource limits (CPU, memory) - Health checks for failure detection - Secrets via environment variables (not in image) - Volume permissions (UID/GID mapping) ### Production Deployment - HTTPS required (via reverse proxy) - Security headers enforced - Session cookies: Secure, HttpOnly, SameSite - CSRF protection maintained - No debug mode in production - Logging without sensitive data - Regular security updates (base image) ## Performance Considerations ### RSS Feed - Server-side caching (5 minutes) - Client-side caching (Cache-Control) - Conditional requests (ETag) - Limit feed items (default 50) - Efficient database query - Pre-rendered HTML (no render on feed generation) ### Container Optimization - Multi-stage build (smaller image) - Gunicorn with multiple workers - Worker recycling (max-requests) - Shared memory for worker tmp - Minimal logging overhead - Resource limits prevent resource exhaustion ### Expected Performance - Feed generation: < 100ms (cached: < 10ms) - Container startup: < 5 seconds - Memory usage: < 256MB (typical) - CPU usage: < 10% (idle), < 50% (load) - Container image size: < 200MB ## Migration from Phase 4 ### Code Changes - Add `starpunk/feed.py` module - Update `starpunk/routes/public.py` (add feed route) - Update `starpunk/__init__.py` (add health check) - Update `starpunk/config.py` (add feed config) - Update templates (RSS discovery links) ### New Files - `Containerfile` - `compose.yaml` - `Caddyfile.example` - `nginx.conf.example` - `.containerignore` ### Configuration - Update `.env.example` with feed/container variables - No breaking changes to existing configuration - Backward compatible ### Database - No schema changes required - No migrations needed ## Acceptance Criteria Phase 5 is complete when: ### Functional Requirements - [ ] RSS feed generates valid RSS 2.0 XML - [ ] Feed includes recent published notes - [ ] Feed respects configured item limit - [ ] Feed has proper RFC-822 dates - [ ] Feed includes HTML content in CDATA - [ ] Feed route is accessible at /feed.xml - [ ] Feed caching works (5 minutes) - [ ] Feed discovery link in templates - [ ] Container builds successfully - [ ] Container runs application correctly - [ ] Health check endpoint works - [ ] Data persists across container restarts - [ ] Container works with both Podman and Docker - [ ] Compose configuration works ### Quality Requirements - [ ] Feed validates with W3C validator - [ ] Test coverage > 90% - [ ] All tests pass - [ ] No linting errors (flake8) - [ ] Code formatted (black) - [ ] Container image < 250MB - [ ] Container startup < 10 seconds - [ ] Memory usage < 512MB under load ### Security Requirements - [ ] Feed only shows published notes - [ ] Container runs as non-root user - [ ] No secrets in container image - [ ] Security headers configured - [ ] HTTPS enforced in production config - [ ] Resource limits configured ### Documentation Requirements - [ ] RSS implementation documented - [ ] Container build documented - [ ] Deployment guide complete - [ ] Reverse proxy configs provided - [ ] IndieAuth testing documented - [ ] CHANGELOG updated - [ ] Version incremented to 0.6.0 ### Production Testing Requirements - [ ] Container deployed with public URL - [ ] IndieAuth tested with real IndieLogin - [ ] RSS feed accessible via HTTPS - [ ] Feed appears in RSS readers - [ ] Session persistence works - [ ] Container restarts gracefully ## Implementation Checklist ### Phase 5.1: RSS Feed Implementation - [ ] Create `starpunk/feed.py` module - [ ] Implement `generate_feed()` function - [ ] Implement `format_rfc822_date()` function - [ ] Implement `get_note_title()` function - [ ] Implement `clean_html_for_rss()` function - [ ] Add feed route to `public.py` - [ ] Implement feed caching - [ ] Add ETag support - [ ] Update templates with RSS discovery - [ ] Test feed generation ### Phase 5.2: Feed Testing - [ ] Write unit tests for feed.py - [ ] Write route tests for feed endpoint - [ ] Test caching behavior - [ ] Test ETag validation - [ ] Validate with W3C Feed Validator - [ ] Test with RSS readers - [ ] Verify date formatting - [ ] Check HTML rendering ### Phase 5.3: Container Implementation - [ ] Create Containerfile - [ ] Create compose.yaml - [ ] Create .containerignore - [ ] Add health check endpoint - [ ] Configure Gunicorn - [ ] Test container build - [ ] Test container run - [ ] Test data persistence - [ ] Test health checks ### Phase 5.4: Production Configuration - [ ] Create Caddyfile.example - [ ] Create nginx.conf.example - [ ] Update .env.example - [ ] Document environment variables - [ ] Create deployment guide - [ ] Test reverse proxy configs - [ ] Document SSL setup - [ ] Document IndieAuth testing ### Phase 5.5: Integration Testing - [ ] Build and run container - [ ] Test with Podman - [ ] Test with Docker - [ ] Test compose orchestration - [ ] Test health endpoint - [ ] Test feed accessibility - [ ] Test data persistence - [ ] Test graceful shutdown ### Phase 5.6: Production Deployment Testing - [ ] Deploy to public-facing server - [ ] Configure reverse proxy with HTTPS - [ ] Test IndieAuth authentication - [ ] Verify callback URLs work - [ ] Test session creation - [ ] Verify feed in RSS reader - [ ] Check security headers - [ ] Monitor resource usage ### Phase 5.7: Documentation - [ ] Complete ADR-014 (RSS implementation) - [ ] Document feed generation - [ ] Document container setup - [ ] Document deployment process - [ ] Document IndieAuth testing - [ ] Update CHANGELOG - [ ] Increment version to 0.6.0 - [ ] Create quick reference guide ## Success Metrics Phase 5 is successful if: 1. **RSS feed is valid** and passes W3C validation 2. **Feed appears in RSS readers** correctly 3. **Container builds** in under 2 minutes 4. **Container runs** reliably with no crashes 5. **IndieAuth works** with public HTTPS URL 6. **Data persists** across container restarts 7. **Memory usage** stays under 512MB 8. **Feed caching** reduces load 9. **All tests pass** with >90% coverage 10. **Documentation** enables easy deployment ## Dependencies ### Python Packages (Existing) - feedgen (already in requirements.txt) - Flask - markdown - httpx ### No New Python Dependencies Required ### External Dependencies - Podman or Docker (user provides) - Reverse proxy: Caddy or Nginx (user provides) - Public domain with HTTPS (user provides) ### Build Tools - Container runtime (podman/docker) - Python 3.11+ - uv (for fast dependency installation) ## Risk Assessment ### Technical Risks **Risk: Feed caching causes stale content** - Likelihood: Low - Impact: Low - Mitigation: 5-minute cache is acceptable delay, ETag support, clear cache on note creation **Risk: Container fails to start** - Likelihood: Low - Impact: High - Mitigation: Health checks, startup probes, extensive testing, fallback to direct deployment **Risk: IndieAuth callback fails with HTTPS** - Likelihood: Medium - Impact: High - Mitigation: Clear documentation, example configs, testing guide, troubleshooting section **Risk: Data loss on container restart** - Likelihood: Low - Impact: Critical - Mitigation: Volume mounts, data persistence testing, backup documentation ### Operational Risks **Risk: Incorrect reverse proxy configuration** - Likelihood: Medium - Impact: High - Mitigation: Example configs, testing guide, common issues documentation **Risk: Resource exhaustion in container** - Likelihood: Low - Impact: Medium - Mitigation: Resource limits, health checks, monitoring guidance **Risk: RSS feed validation failures** - Likelihood: Low - Impact: Low - Mitigation: Extensive testing, W3C validation, feed reader testing ## Future Enhancements (V2+) Deferred features: - Feed pagination (older entries) - Multiple feed formats (Atom, JSON Feed) - Feed categories/tags - Feed discovery for individual tags - Feed analytics - Podcast RSS support - Media enclosures - Container orchestration (Kubernetes) - Container registry publishing - Auto-update mechanisms - Feed notification (WebSub) ## References ### Internal Documentation - [Phase 4: Web Interface](/home/phil/Projects/starpunk/docs/design/phase-4-web-interface.md) - [Architecture Overview](/home/phil/Projects/starpunk/docs/architecture/overview.md) - [ADR-002: Flask Extensions](/home/phil/Projects/starpunk/docs/decisions/ADR-002-flask-extensions.md) - [Versioning Strategy](/home/phil/Projects/starpunk/docs/standards/versioning-strategy.md) ### External Standards - [RSS 2.0 Specification](https://www.rssboard.org/rss-specification) - [RFC-822 Date Format](https://www.rfc-editor.org/rfc/rfc822) - [W3C Feed Validator](https://validator.w3.org/feed/) - [Dockerfile Best Practices](https://docs.docker.com/develop/dev-best-practices/) - [Podman Documentation](https://docs.podman.io/) - [Gunicorn Configuration](https://docs.gunicorn.org/en/stable/settings.html) --- **Phase**: 5 **Version Target**: 0.6.0 **Status**: Design Complete, Ready for Implementation **Next Phase**: Phase 6 (Micropub Implementation)