# ATOM Feed Specification - v1.1.2
## Overview
This specification defines the implementation of ATOM 1.0 feed generation for StarPunk, providing an alternative syndication format to RSS with enhanced metadata support and standardized content handling.
## Requirements
### Functional Requirements
1. **ATOM 1.0 Compliance**
- Full conformance to RFC 4287
- Valid XML namespace declarations
- Required elements present
- Proper content type handling
2. **Content Support**
- Text content (escaped)
- HTML content (escaped or CDATA)
- XHTML content (inline XML)
- Base64 for binary (future)
3. **Metadata Richness**
- Author information
- Category/tag support
- Updated vs published dates
- Link relationships
4. **Streaming Generation**
- Memory-efficient output
- Chunked response support
- No full document in memory
### Non-Functional Requirements
1. **Performance**
- Generation time <100ms for 50 entries
- Streaming chunks of ~4KB
- Minimal memory footprint
2. **Compatibility**
- Works with major feed readers
- Valid per W3C Feed Validator
- Proper content negotiation
## ATOM Feed Structure
### Namespace and Root Element
```xml
```
### Feed-Level Elements
#### Required Elements
| Element | Description | Example |
|---------|-------------|---------|
| `id` | Permanent, unique identifier | `https://example.com/` |
| `title` | Human-readable title | `
StarPunk Notes` |
| `updated` | Last significant update | `2024-11-25T12:00:00Z` |
#### Recommended Elements
| Element | Description | Example |
|---------|-------------|---------|
| `author` | Feed author | `John Doe` |
| `link` | Feed relationships | `` |
| `subtitle` | Feed description | `Personal notes` |
#### Optional Elements
| Element | Description |
|---------|-------------|
| `category` | Categorization scheme |
| `contributor` | Secondary contributors |
| `generator` | Software that generated feed |
| `icon` | Small visual identification |
| `logo` | Larger visual identification |
| `rights` | Copyright/license info |
### Entry-Level Elements
#### Required Elements
| Element | Description | Example |
|---------|-------------|---------|
| `id` | Permanent, unique identifier | `https://example.com/note/123` |
| `title` | Entry title | `My Note Title` |
| `updated` | Last modification | `2024-11-25T12:00:00Z` |
#### Recommended Elements
| Element | Description |
|---------|-------------|
| `author` | Entry author (if different from feed) |
| `content` | Full content |
| `link` | Entry URL |
| `summary` | Short summary |
#### Optional Elements
| Element | Description |
|---------|-------------|
| `category` | Entry categories/tags |
| `contributor` | Secondary contributors |
| `published` | Initial publication time |
| `rights` | Entry-specific rights |
| `source` | If republished from elsewhere |
## Implementation Design
### ATOM Generator Class
```python
class AtomGenerator:
"""ATOM 1.0 feed generator with streaming support"""
def __init__(self, site_url: str, site_name: str, site_description: str):
self.site_url = site_url.rstrip('/')
self.site_name = site_name
self.site_description = site_description
def generate(self, notes: List[Note], limit: int = 50) -> Iterator[str]:
"""Generate ATOM feed as stream of chunks
IMPORTANT: Notes are expected to be in DESC order (newest first)
from the database. This order MUST be preserved in the feed.
"""
# Yield XML declaration
yield '\n'
# Yield feed opening with namespace
yield '\n'
# Yield feed metadata
yield from self._generate_feed_metadata()
# Yield entries - maintain DESC order (newest first)
# DO NOT reverse! Database order is correct
for note in notes[:limit]:
yield from self._generate_entry(note)
# Yield closing tag
yield '\n'
def _generate_feed_metadata(self) -> Iterator[str]:
"""Generate feed-level metadata"""
# Required elements
yield f' {self._escape_xml(self.site_url)}/\n'
yield f' {self._escape_xml(self.site_name)}\n'
yield f' {self._format_atom_date(datetime.now(timezone.utc))}\n'
# Links
yield f' \n'
yield f' \n'
# Optional elements
if self.site_description:
yield f' {self._escape_xml(self.site_description)}\n'
# Generator
yield ' StarPunk\n'
def _generate_entry(self, note: Note) -> Iterator[str]:
"""Generate a single entry"""
permalink = f"{self.site_url}{note.permalink}"
yield ' \n'
# Required elements
yield f' {self._escape_xml(permalink)}\n'
yield f' {self._escape_xml(note.title)}\n'
yield f' {self._format_atom_date(note.updated_at or note.created_at)}\n'
# Link to entry
yield f' \n'
# Published date (if different from updated)
if note.created_at != note.updated_at:
yield f' {self._format_atom_date(note.created_at)}\n'
# Author (if available)
if hasattr(note, 'author'):
yield ' \n'
yield f' {self._escape_xml(note.author.name)}\n'
if note.author.email:
yield f' {self._escape_xml(note.author.email)}\n'
if note.author.uri:
yield f' {self._escape_xml(note.author.uri)}\n'
yield ' \n'
# Content
yield from self._generate_content(note)
# Categories/tags
if hasattr(note, 'tags') and note.tags:
for tag in note.tags:
yield f' \n'
yield ' \n'
def _generate_content(self, note: Note) -> Iterator[str]:
"""Generate content element with proper type"""
# Determine content type based on note format
if note.html:
# HTML content - use escaped HTML
yield ' '
yield self._escape_xml(note.html)
yield '\n'
else:
# Plain text content
yield ' '
yield self._escape_xml(note.content)
yield '\n'
# Add summary if available
if hasattr(note, 'summary') and note.summary:
yield ' '
yield self._escape_xml(note.summary)
yield '\n'
```
### Date Formatting
ATOM uses RFC 3339 date format, which is a profile of ISO 8601.
```python
def _format_atom_date(self, dt: datetime) -> str:
"""Format datetime to RFC 3339 for ATOM
Format: 2024-11-25T12:00:00Z or 2024-11-25T12:00:00-05:00
Args:
dt: Datetime object (naive assumed UTC)
Returns:
RFC 3339 formatted string
"""
# Ensure timezone aware
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
# Format to RFC 3339
# Use 'Z' for UTC, otherwise offset
if dt.tzinfo == timezone.utc:
return dt.strftime('%Y-%m-%dT%H:%M:%SZ')
else:
return dt.strftime('%Y-%m-%dT%H:%M:%S%z')
```
### XML Escaping
```python
def _escape_xml(self, text: str) -> str:
"""Escape special XML characters
Escapes: & < > " '
Args:
text: Text to escape
Returns:
XML-safe escaped text
"""
if not text:
return ''
# Order matters: & must be first
text = text.replace('&', '&')
text = text.replace('<', '<')
text = text.replace('>', '>')
text = text.replace('"', '"')
text = text.replace("'", ''')
return text
```
## Content Type Handling
### Text Content
Plain text, must be escaped:
```xml
This is plain text with <escaped> characters
```
### HTML Content
HTML as escaped text:
```xml
<p>This is <strong>HTML</strong> content</p>
```
### XHTML Content (Future)
Well-formed XML inline:
```xml