- Add ADR-014: RSS Feed Implementation - Add ADR-015: Phase 5 Implementation Approach - Add Phase 5 design documents (RSS and container) - Add pre-implementation review - Add RSS and container validation reports - Add architectural approval for v0.6.0 release Architecture reviews confirm 98/100 (RSS) and 96/100 (container) scores. Phase 5 approved for production deployment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
36 KiB
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:
- Create feature branch from main
- Implement all Phase 5 features
- Create PR for review
- 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:
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 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:
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:
<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):
<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:
# 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:
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:
@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:
# 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):
# 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:
# 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
# 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 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:
# =============================================================================
# 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:
# 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:
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:
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:
#!/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:
- Visit https://validator.w3.org/feed/
- Enter feed URL
- Verify no errors
- Check all required elements present
IndieAuth Testing:
- Deploy container with public HTTPS URL
- Configure ADMIN_ME with your domain
- Test IndieLogin authentication flow
- Verify callback works correctly
- Test session creation and validation
Feed Reader Testing:
- Add feed to RSS reader (Feedly, NewsBlur, etc.)
- Verify notes appear correctly
- Check formatting and links
- 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.pymodule - 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
Containerfilecompose.yamlCaddyfile.examplenginx.conf.example.containerignore
Configuration
- Update
.env.examplewith 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.pymodule - 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:
- RSS feed is valid and passes W3C validation
- Feed appears in RSS readers correctly
- Container builds in under 2 minutes
- Container runs reliably with no crashes
- IndieAuth works with public HTTPS URL
- Data persists across container restarts
- Memory usage stays under 512MB
- Feed caching reduces load
- All tests pass with >90% coverage
- 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
External Standards
- RSS 2.0 Specification
- RFC-822 Date Format
- W3C Feed Validator
- Dockerfile Best Practices
- Podman Documentation
- Gunicorn Configuration
Phase: 5 Version Target: 0.6.0 Status: Design Complete, Ready for Implementation Next Phase: Phase 6 (Micropub Implementation)