# 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
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Site Name</title>
    <link>https://example.com/</link>
    <description>Site description</description>
    <language>en-us</language>
    <lastBuildDate>Mon, 18 Nov 2024 12:00:00 +0000</lastBuildDate>
    <atom:link href="https://example.com/feed.xml" rel="self" type="application/rss+xml"/>

    <!-- Items -->
    <item>
      <title>Note Title or Timestamp</title>
      <link>https://example.com/note/my-note-slug</link>
      <guid isPermaLink="true">https://example.com/note/my-note-slug</guid>
      <pubDate>Mon, 18 Nov 2024 10:30:00 +0000</pubDate>
      <description><![CDATA[
        <p>Rendered HTML content goes here</p>
      ]]></description>
    </item>

    <!-- More items... -->
  </channel>
</rss>
```

**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
<head>
  <!-- Existing head content -->

  <!-- RSS Feed Discovery -->
  <link rel="alternate" type="application/rss+xml"
        title="{{ config.SITE_NAME }} RSS Feed"
        href="{{ url_for('public.feed', _external=True) }}">
</head>
```

**Add feed link to homepage** (`templates/index.html`):
```html
<nav>
  <a href="/">Home</a>
  <a href="{{ url_for('public.feed') }}">RSS Feed</a>
</nav>
```

## 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)
