diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b7f2b2..7336c30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.2.0-rc.1] - 2025-11-28
+
+### Added
+- **Custom Slug Input Field** - Web UI now supports custom slugs (v1.2.0 Phase 1)
+ - Added optional custom slug field to note creation form
+ - Slugs are read-only after creation to preserve permalinks
+ - Auto-validates and sanitizes slug format (lowercase, numbers, hyphens only)
+ - Shows helpful placeholder text and validation guidance
+ - Matches Micropub `mp-slug` behavior for consistency
+ - Falls back to auto-generation when field is left blank
+
+- **Author Profile Discovery** - Automatic h-card discovery from IndieAuth identity (v1.2.0 Phase 2)
+ - Discovers author information from user's IndieAuth profile URL on login
+ - Caches author h-card data (name, photo, bio, rel-me links) for 24 hours
+ - Uses mf2py library for reliable Microformats2 parsing
+ - Graceful fallback to domain name if discovery fails
+ - Never blocks login functionality (per ADR-061)
+ - Eliminates need for manual author configuration
+
+- **Complete Microformats2 Support** - Full IndieWeb h-entry, h-card, h-feed markup (v1.2.0 Phase 2)
+ - All notes display as proper h-entry with required properties (u-url, dt-published, e-content, p-author)
+ - Author h-card nested within each h-entry (not standalone)
+ - p-name property only added when note has explicit title (starts with # heading)
+ - u-uid and u-url match for notes (permalink stability)
+ - Homepage displays as h-feed with proper structure
+ - rel-me links from discovered profile added to HTML head
+ - dt-updated property shown when note is modified
+ - Passes Microformats2 validation (indiewebify.me compatible)
+
+- **Media Upload Support** - Image upload and display for notes (v1.2.0 Phase 3)
+ - Upload up to 4 images per note via web UI (JPEG, PNG, GIF, WebP)
+ - Automatic image optimization with Pillow library
+ - Rejects files over 10MB or dimensions over 4096x4096 pixels
+ - Auto-resizes images over 2048px (longest edge) to improve performance
+ - EXIF orientation correction ensures proper display
+ - Social media style layout: media displays at top, text content below
+ - Optional captions for accessibility (used as alt text)
+ - Media stored in date-organized folders (data/media/YYYY/MM/)
+ - UUID-based filenames prevent collisions
+ - Media included in all syndication feeds (RSS, ATOM, JSON Feed)
+ - RSS: HTML embedding in description
+ - ATOM: Both enclosures and HTML content
+ - JSON Feed: Native attachments array
+ - Multiple u-photo properties in Microformats2 markup
+ - Media files cached immutably (1 year) for performance
+
## [1.1.2] - 2025-11-28
### Fixed
diff --git a/docs/decisions/ADR-056-no-selfhosted-indieauth.md b/docs/decisions/ADR-056-no-selfhosted-indieauth.md
new file mode 100644
index 0000000..4038ece
--- /dev/null
+++ b/docs/decisions/ADR-056-no-selfhosted-indieauth.md
@@ -0,0 +1,110 @@
+# ADR-056: Use External IndieAuth Provider (Never Self-Host)
+
+## Status
+**ACCEPTED** - This is a permanent, non-negotiable decision.
+
+## Context
+StarPunk is a minimal IndieWeb CMS focused on **content creation and syndication**, not identity infrastructure. The project philosophy demands that every line of code must justify its existence.
+
+The question of whether to implement self-hosted IndieAuth has been raised multiple times. This ADR documents the final, permanent decision on this matter.
+
+## Decision
+**StarPunk will NEVER implement self-hosted IndieAuth.**
+
+We will always rely on external IndieAuth providers such as:
+- indielogin.com (primary recommendation)
+- Other established IndieAuth providers
+
+This decision is **permanent and non-negotiable**.
+
+## Rationale
+
+### 1. Project Focus
+StarPunk's mission is to be a minimal CMS for publishing IndieWeb content. Our core competencies are:
+- Publishing notes with proper microformats
+- Generating RSS/Atom/JSON feeds
+- Implementing Micropub for content creation
+- Media management for content
+
+Identity infrastructure is explicitly **NOT** our focus.
+
+### 2. Complexity vs Value
+Implementing IndieAuth would require:
+- OAuth 2.0 implementation
+- Token management
+- Security considerations
+- Key storage and rotation
+- User profile management
+- Authorization code flows
+
+This represents hundreds or thousands of lines of code that don't serve our core mission of content publishing.
+
+### 3. Existing Solutions Work
+External IndieAuth providers like indielogin.com:
+- Are battle-tested
+- Handle security updates
+- Support multiple authentication methods
+- Are free to use
+- Align with IndieWeb principles of building on existing infrastructure
+
+### 4. Philosophy Alignment
+Our core philosophy states: "Every line of code must justify its existence. When in doubt, leave it out."
+
+Self-hosted IndieAuth cannot justify its existence in a minimal content-focused CMS.
+
+## Consequences
+
+### Positive
+- Dramatically reduced codebase complexity
+- No security burden for identity management
+- Faster development of content features
+- Clear project boundaries
+- User authentication "just works" via proven providers
+
+### Negative
+- Dependency on external service (indielogin.com)
+- Cannot function without internet connection to auth provider
+- No control over authentication user experience
+
+### Mitigations
+- Document clear setup instructions for using indielogin.com
+- Support multiple external providers for redundancy
+- Cache authentication tokens appropriately
+
+## Alternatives Considered
+
+### 1. Self-Hosted IndieAuth (REJECTED)
+**Why considered:** Full control over authentication
+**Why rejected:** Massive scope creep, violates project philosophy
+
+### 2. No Authentication (REJECTED)
+**Why considered:** Ultimate simplicity
+**Why rejected:** Single-user system still needs access control
+
+### 3. Basic Auth or Simple Password (REJECTED)
+**Why considered:** Very simple to implement
+**Why rejected:** Not IndieWeb compliant, poor user experience
+
+### 4. Hybrid Approach (REJECTED)
+**Why considered:** Optional self-hosted with external fallback
+**Why rejected:** Maintains complexity we're trying to avoid
+
+## Implementation Notes
+
+All authentication code should:
+1. Assume an external IndieAuth provider
+2. Never include hooks or abstractions for self-hosting
+3. Document indielogin.com as the recommended provider
+4. Include clear error messages when auth provider is unavailable
+
+## References
+- Project Philosophy: "Every line of code must justify its existence"
+- IndieAuth Specification: https://indieauth.spec.indieweb.org/
+- indielogin.com: https://indielogin.com/
+
+## Final Note
+This decision has been made after extensive consideration and multiple discussions. It is final.
+
+**Do not propose self-hosted IndieAuth in future architectural discussions.**
+
+The goal of StarPunk is **content**, not **identity**.
\ No newline at end of file
diff --git a/docs/decisions/ADR-057-media-attachment-model.md b/docs/decisions/ADR-057-media-attachment-model.md
new file mode 100644
index 0000000..e4f6841
--- /dev/null
+++ b/docs/decisions/ADR-057-media-attachment-model.md
@@ -0,0 +1,110 @@
+# ADR-057: Media Attachment Model
+
+## Status
+Accepted
+
+## Context
+The v1.2.0 media upload feature needed a clear model for how media relates to notes. Initial design assumed inline markdown image insertion (like a blog editor), but user feedback clarified that notes are more like social media posts (tweets, Mastodon toots) where media is attached rather than inline.
+
+Key insights from user:
+- "Notes are more like tweets, thread posts, mastodon posts etc. where the media is inserted is kind of irrelevant"
+- Media should appear at the TOP of notes when displayed
+- Text content should appear BELOW media
+- Multiple images per note should be supported
+
+## Decision
+We will implement a social media-style attachment model for media:
+
+1. **Database Design**: Use a junction table (`note_media`) to associate media files with notes, allowing:
+ - Multiple media per note (max 4)
+ - Explicit ordering via `display_order` column
+ - Per-attachment metadata (captions)
+ - Future reuse of media across notes
+
+2. **Display Model**: Media attachments appear at the TOP of notes:
+ - 1 image: Full width display
+ - 2 images: Side-by-side layout
+ - 3-4 images: Grid layout
+ - Text content always appears below media
+
+3. **Syndication Strategy**:
+ - RSS: Embed media as HTML in description (universal support)
+ - ATOM: Use both `` and HTML content
+ - JSON Feed: Use native `attachments` array (cleanest)
+
+4. **Microformats2**: Multiple `u-photo` properties for multi-photo posts
+
+## Rationale
+**Why attachment model over inline markdown?**
+- Matches user mental model (social media posts)
+- Simplifies UI/UX (no cursor tracking needed)
+- Better syndication support (especially JSON Feed)
+- Cleaner Microformats2 markup
+- Consistent display across all contexts
+
+**Why junction table over array column?**
+- Better query performance for feeds
+- Supports future media reuse
+- Per-attachment metadata
+- Explicit ordering control
+- Standard relational design
+
+**Why limit to 4 images?**
+- Twitter limit is 4 images
+- Mastodon limit is 4 images
+- Prevents performance issues
+- Maintains clean grid layouts
+- Sufficient for microblogging use case
+
+## Consequences
+
+### Positive
+- Clean separation of media and text content
+- Familiar social media UX pattern
+- Excellent syndication feed support
+- Future-proof for media galleries
+- Supports accessibility via captions
+- Efficient database queries
+
+### Negative
+- No inline images in markdown content
+- All media must appear at top
+- Cannot mix text and images
+- More complex database schema
+- Additional JOIN queries needed
+
+### Neutral
+- Different from traditional blog CMSs
+- Requires grid layout CSS
+- Media upload is separate from text editing
+
+## Alternatives Considered
+
+### Alternative 1: Inline Markdown Images
+Store media URLs in markdown content as ``.
+- **Pros**: Traditional blog approach, flexible positioning
+- **Cons**: Poor syndication, complex editing UX, inconsistent display
+
+### Alternative 2: JSON Array in Notes Table
+Store media IDs as JSON array column in notes table.
+- **Pros**: Simpler schema, fewer tables
+- **Cons**: Poor query performance, no per-media metadata, violates 1NF
+
+### Alternative 3: Single Media Per Note
+Restrict to one image per note.
+- **Pros**: Simplest implementation
+- **Cons**: Too limiting, doesn't match social media patterns
+
+## Implementation Notes
+
+1. Migration will create both `media` and `note_media` tables
+2. Feed generators must query media via JOIN
+3. Template must render media before content
+4. Upload UI shows thumbnails, not markdown insertion
+5. Consider lazy loading for performance
+
+## References
+- [IndieWeb multi-photo posts](https://indieweb.org/multi-photo)
+- [Microformats2 u-photo property](https://microformats.org/wiki/h-entry#u-photo)
+- [JSON Feed attachments](https://jsonfeed.org/version/1.1#attachments)
+- [Twitter photo upload limits](https://help.twitter.com/en/using-twitter/tweeting-gifs-and-pictures)
\ No newline at end of file
diff --git a/docs/decisions/ADR-058-image-optimization-strategy.md b/docs/decisions/ADR-058-image-optimization-strategy.md
new file mode 100644
index 0000000..155b55e
--- /dev/null
+++ b/docs/decisions/ADR-058-image-optimization-strategy.md
@@ -0,0 +1,183 @@
+# ADR-058: Image Optimization Strategy
+
+## Status
+Accepted
+
+## Context
+The v1.2.0 media upload feature requires decisions about image size limits, optimization, and validation. Based on user requirements:
+- 4 images maximum per note (confirmed)
+- No drag-and-drop reordering needed (display order is upload order)
+- Image optimization desired
+- Optional caption field for each image (accessibility)
+
+Research was conducted on:
+- Web image best practices (2024)
+- IndieWeb implementation patterns
+- Python image processing libraries
+- Storage implications for single-user CMS
+
+## Decision
+
+### Image Limits
+We will enforce the following limits:
+
+1. **Count**: Maximum 4 images per note
+2. **File Size**: Maximum 10MB per image
+3. **Dimensions**: Maximum 4096x4096 pixels
+4. **Formats**: JPEG, PNG, GIF, WebP only
+
+### Optimization Strategy
+We will implement **automatic resizing on upload**:
+
+1. **Resize Policy**:
+ - Images larger than 2048 pixels (longest edge) will be resized
+ - Aspect ratio will be preserved
+ - Original quality will be maintained (no aggressive compression)
+ - EXIF orientation will be corrected
+
+2. **Rejection Policy**:
+ - Files over 10MB will be rejected (before optimization)
+ - Dimensions over 4096x4096 will be rejected
+ - Invalid formats will be rejected
+ - Corrupted files will be rejected
+
+3. **Processing Library**: Use **Pillow** for image processing
+
+### Database Schema Updates
+Add caption field to `note_media` table:
+```sql
+CREATE TABLE note_media (
+ id INTEGER PRIMARY KEY,
+ note_id INTEGER NOT NULL,
+ media_id INTEGER NOT NULL,
+ display_order INTEGER NOT NULL DEFAULT 0,
+ caption TEXT, -- Optional caption for accessibility
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE,
+ FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE,
+ UNIQUE(note_id, media_id)
+);
+```
+
+## Rationale
+
+### Why 10MB file size limit?
+- Generous for high-quality photos from modern phones
+- Prevents storage abuse on single-user instance
+- Reasonable upload time even on slower connections
+- Matches or exceeds most social platforms
+
+### Why 4096x4096 max dimensions?
+- Covers 16-megapixel images (4000x4000)
+- Sufficient for 4K displays (3840x2160)
+- Prevents memory issues during processing
+- Larger than needed for web display
+
+### Why resize to 2048px?
+- Optimal balance between quality and performance
+- Retina-ready (2x scaling on 1024px display)
+- Significant file size reduction
+- Matches common social media limits
+- Preserves quality for most use cases
+
+### Why Pillow over alternatives?
+- De-facto standard for Python image processing
+- Fastest for basic resize operations
+- Minimal dependencies
+- Well-documented and stable
+- Sufficient for our needs (resize, format conversion, EXIF)
+
+### Why automatic optimization?
+- Better user experience (no manual intervention)
+- Consistent output quality
+- Storage efficiency
+- Faster page loads
+- Users still get good quality
+
+### Why no thumbnail generation?
+- Adds complexity for minimal benefit
+- Modern browsers handle image scaling well
+- Single-user CMS doesn't need CDN optimization
+- Can be added later if needed
+
+## Consequences
+
+### Positive
+- Automatic optimization improves performance
+- Generous limits support high-quality photography
+- Captions improve accessibility
+- Storage usage remains reasonable
+- Fast processing with Pillow
+
+### Negative
+- Users cannot upload raw/unprocessed images
+- Some quality loss for images over 2048px
+- No manual control over optimization
+- Additional processing time on upload
+
+### Neutral
+- Requires Pillow dependency
+- Images stored at single resolution
+- No progressive enhancement (thumbnails)
+
+## Alternatives Considered
+
+### Alternative 1: No Optimization
+Accept images as-is, no processing.
+- **Pros**: Simpler, preserves originals
+- **Cons**: Storage bloat, slow page loads, memory issues
+
+### Alternative 2: Strict Limits (1MB, 1920x1080)
+Match typical web recommendations.
+- **Pros**: Optimal performance, minimal storage
+- **Cons**: Too restrictive for photography, poor UX
+
+### Alternative 3: Generate Multiple Sizes
+Create thumbnail, medium, and full sizes.
+- **Pros**: Optimal delivery, responsive images
+- **Cons**: Complex implementation, 3x storage, overkill for single-user
+
+### Alternative 4: Client-side Resizing
+Resize in browser before upload.
+- **Pros**: Reduces server load
+- **Cons**: Inconsistent quality, browser limitations, poor UX
+
+## Implementation Notes
+
+1. **Validation Order**:
+ - Check file size (reject if >10MB)
+ - Check MIME type (accept only allowed formats)
+ - Load with Pillow (validates file integrity)
+ - Check dimensions (reject if >4096px)
+ - Resize if needed (>2048px)
+ - Save optimized version
+
+2. **Error Messages**:
+ - "File too large. Maximum size is 10MB"
+ - "Invalid image format. Accepted: JPEG, PNG, GIF, WebP"
+ - "Image dimensions too large. Maximum is 4096x4096"
+ - "Image appears to be corrupted"
+
+3. **Pillow Configuration**:
+ ```python
+ # Preserve quality during resize
+ image.thumbnail((2048, 2048), Image.Resampling.LANCZOS)
+
+ # Correct EXIF orientation
+ ImageOps.exif_transpose(image)
+
+ # Save with original quality
+ image.save(output, quality=95, optimize=True)
+ ```
+
+4. **Caption Implementation**:
+ - Add caption field to upload form
+ - Store in `note_media.caption`
+ - Use as alt text in HTML
+ - Include in Microformats markup
+
+## References
+- [MDN Web Performance: Images](https://developer.mozilla.org/en-US/docs/Web/Performance/images)
+- [Pillow Documentation](https://pillow.readthedocs.io/)
+- [Web.dev Image Optimization](https://web.dev/fast/#optimize-your-images)
+- [Twitter Image Specifications](https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/uploading-media/media-best-practices)
\ No newline at end of file
diff --git a/docs/decisions/ADR-061-author-discovery.md b/docs/decisions/ADR-061-author-discovery.md
new file mode 100644
index 0000000..b00b188
--- /dev/null
+++ b/docs/decisions/ADR-061-author-discovery.md
@@ -0,0 +1,111 @@
+# ADR-061: Author Profile Discovery from IndieAuth
+
+## Status
+Accepted
+
+## Context
+StarPunk v1.2.0 requires Microformats2 compliance, including proper h-card author information in h-entries. The original design assumed author information would be configured via environment variables (AUTHOR_NAME, AUTHOR_PHOTO, etc.).
+
+However, since StarPunk uses IndieAuth for authentication, and users authenticate with their domain/profile URL, we have an opportunity to discover author information directly from their IndieWeb profile rather than requiring manual configuration.
+
+The user explicitly stated: "These should be retrieved from the logged in profile domain (rel me etc.)" when asked about author configuration.
+
+## Decision
+Implement automatic author profile discovery from the IndieAuth 'me' URL:
+
+1. When a user logs in via IndieAuth, fetch their profile page
+2. Parse h-card microformats and rel-me links from the profile
+3. Cache this information in a new `author_profile` database table
+4. Use discovered information in templates for Microformats2 markup
+5. Provide fallback behavior when discovery fails
+
+## Rationale
+1. **IndieWeb Native**: Discovery from profile URLs is a core IndieWeb pattern
+2. **DRY Principle**: Author already maintains their profile; no need to duplicate
+3. **Dynamic Updates**: Profile changes are reflected on next login
+4. **Standards-Based**: Uses existing h-card and rel-me specifications
+5. **User Experience**: Zero configuration for author information
+6. **Consistency**: Author info always matches their IndieWeb identity
+
+## Consequences
+
+### Positive
+- No manual configuration of author information required
+- Automatically stays in sync with user's profile
+- Supports full IndieWeb identity model
+- Works with any IndieAuth provider
+- Discoverable rel-me links for identity verification
+
+### Negative
+- Requires network request during login (mitigated by caching)
+- Depends on proper markup on user's profile page
+- Additional database table required
+- More complex than static configuration
+- Parsing complexity for microformats
+
+### Implementation Details
+
+#### Database Schema
+```sql
+CREATE TABLE author_profile (
+ id INTEGER PRIMARY KEY,
+ me_url TEXT NOT NULL UNIQUE,
+ name TEXT,
+ photo TEXT,
+ bio TEXT,
+ rel_me_links TEXT, -- JSON array
+ discovered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+#### Discovery Flow
+1. User authenticates with IndieAuth
+2. On successful login, trigger discovery
+3. Fetch user's profile page (with timeout)
+4. Parse h-card for: name, photo, bio
+5. Parse rel-me links
+6. Store in database with timestamp
+7. Use cache for 7 days, refresh on login
+
+#### Fallback Strategy
+- If discovery fails during login, use cached data if available
+- If no cache exists, use minimal defaults (domain as name)
+- Never block login due to discovery failure
+- Log failures for monitoring
+
+## Alternatives Considered
+
+### 1. Environment Variables (Original Design)
+Static configuration via .env file
+- ✅ Simple, no network requests
+- ❌ Requires manual configuration
+- ❌ Duplicates information already on profile
+- ❌ Can become out of sync
+
+### 2. Hybrid Approach
+Environment variables with optional discovery
+- ✅ Flexibility for both approaches
+- ❌ More complex configuration
+- ❌ Unclear which takes precedence
+
+### 3. Discovery Only, No Cache
+Fetch profile on every request
+- ✅ Always up to date
+- ❌ Performance impact
+- ❌ Reliability issues
+
+### 4. Static Import Tool
+CLI command to import profile once
+- ✅ No runtime discovery needed
+- ❌ Manual process
+- ❌ Can become stale
+
+## Implementation Priority
+High - Required for v1.2.0 Microformats2 compliance
+
+## References
+- https://microformats.org/wiki/h-card
+- https://indieweb.org/rel-me
+- https://indieweb.org/discovery
+- W3C IndieAuth specification
\ No newline at end of file
diff --git a/docs/design/v1.2.0/developer-qa.md b/docs/design/v1.2.0/developer-qa.md
new file mode 100644
index 0000000..4ca5816
--- /dev/null
+++ b/docs/design/v1.2.0/developer-qa.md
@@ -0,0 +1,303 @@
+# v1.2.0 Developer Q&A
+
+**Date**: 2025-11-28
+**Architect**: StarPunk Architect Subagent
+**Purpose**: Answer critical implementation questions for v1.2.0
+
+## Custom Slugs Answers
+
+**Q1: Validation pattern conflict - should we apply new lowercase validation to existing slugs?**
+- **Answer:** Validate only new custom slugs, don't migrate existing slugs
+- **Rationale:** Existing slugs work, no need to change them retroactively
+- **Implementation:** In `validate_and_sanitize_custom_slug()`, apply lowercase enforcement only to new/edited slugs
+
+**Q2: Form field readonly behavior - how should the slug field behave on edit forms?**
+- **Answer:** Display as readonly input field with current value visible
+- **Rationale:** Users need to see the current slug but understand it cannot be changed
+- **Implementation:** Use `readonly` attribute, not `disabled` (disabled fields don't submit with form)
+
+**Q3: Slug uniqueness validation - where should this happen?**
+- **Answer:** Both client-side (for UX) and server-side (for security)
+- **Rationale:** Client-side prevents unnecessary submissions, server-side is authoritative
+- **Implementation:** Database unique constraint + Python validation in `validate_and_sanitize_custom_slug()`
+
+## Media Upload Answers
+
+**Q4: Media upload flow - how should upload and note association work?**
+- **Answer:** Upload during note creation, associate via note_id after creation
+- **Rationale:** Simpler than pre-upload with temporary IDs
+- **Implementation:** Upload files in `create_note_submit()` after note is created, store associations in media table
+
+**Q5: Storage directory structure - exact path format?**
+- **Answer:** `data/media/YYYY/MM/filename-uuid.ext`
+- **Rationale:** Date organization helps with backups and management
+- **Implementation:** Use `os.makedirs(path, exist_ok=True)` to create directories as needed
+
+**Q6: File naming convention - how to ensure uniqueness?**
+- **Answer:** `{original_name_slug}-{uuid4()[:8]}.{extension}`
+- **Rationale:** Preserves original name for SEO while ensuring uniqueness
+- **Implementation:** Slugify original filename, append 8-char UUID, preserve extension
+
+**Q7: MIME type validation - which types exactly?**
+- **Answer:** Allow: image/jpeg, image/png, image/gif, image/webp. Reject all others
+- **Rationale:** Common web formats only, no SVG (XSS risk)
+- **Implementation:** Use python-magic for reliable MIME detection, not just file extension
+
+**Q8: Upload size limits - what's reasonable?**
+- **Answer:** 10MB per file, 40MB total per note (4 files × 10MB)
+- **Rationale:** Sufficient for high-quality images without overwhelming storage
+- **Implementation:** Check in both client-side JavaScript and server-side validation
+
+**Q9: Database schema for media table - exact columns?**
+- **Answer:** id, note_id, filename, mime_type, size_bytes, width, height, uploaded_at
+- **Rationale:** Minimal but sufficient metadata for display and management
+- **Implementation:** Use Pillow to extract image dimensions on upload
+
+**Q10: Orphaned file cleanup - how to handle?**
+- **Answer:** Keep orphaned files, add admin cleanup tool in future version
+- **Rationale:** Data preservation is priority, cleanup can be manual for v1.2.0
+- **Implementation:** Log orphaned files but don't auto-delete
+
+**Q11: Upload progress indication - required for v1.2.0?**
+- **Answer:** No, simple form submission is sufficient for v1.2.0
+- **Rationale:** Keep it simple, can enhance in future version
+- **Implementation:** Standard HTML form with enctype="multipart/form-data"
+
+**Q12: Image display order - how to maintain?**
+- **Answer:** Use upload sequence, store display_order in media table
+- **Rationale:** Predictable and simple
+- **Implementation:** Auto-increment display_order starting at 0
+
+**Q13: Thumbnail generation - needed for v1.2.0?**
+- **Answer:** No, use CSS for responsive sizing
+- **Rationale:** Simplicity over optimization for v1
+- **Implementation:** Use `max-width: 100%` and lazy loading
+
+**Q14: Edit form media handling - can users remove media?**
+- **Answer:** Yes, checkbox to mark for deletion
+- **Rationale:** Essential editing capability
+- **Implementation:** "Remove" checkboxes next to each image in edit form
+
+**Q15: Media URL structure - exact format?**
+- **Answer:** `/media/YYYY/MM/filename.ext` (matches storage path)
+- **Rationale:** Clean URLs, date organization visible
+- **Implementation:** Route in `starpunk/routes/public.py` using send_from_directory
+
+## Author Discovery Answers
+
+**Q16: Discovery failure handling - what if profile URL is unreachable?**
+- **Answer:** Use defaults: name from IndieAuth me URL domain, no photo
+- **Rationale:** Always provide something, never break
+- **Implementation:** Try discovery, catch all exceptions, use defaults
+
+**Q17: h-card parsing library - which one?**
+- **Answer:** Use mf2py (already in requirements for Micropub)
+- **Rationale:** Already a dependency, well-maintained
+- **Implementation:** `import mf2py; result = mf2py.parse(url=profile_url)`
+
+**Q18: Multiple h-cards on profile - which to use?**
+- **Answer:** First h-card with url property matching the profile URL
+- **Rationale:** Most specific match per IndieWeb convention
+- **Implementation:** Loop through h-cards, check url property
+
+**Q19: Discovery caching duration - how long?**
+- **Answer:** 24 hours, with manual refresh button in admin
+- **Rationale:** Balance between freshness and performance
+- **Implementation:** Store discovered_at timestamp, check age
+
+**Q20: Profile update mechanism - when to refresh?**
+- **Answer:** On login + manual refresh button + 24hr expiry
+- **Rationale:** Login is natural refresh point
+- **Implementation:** Call discovery in auth callback
+
+**Q21: Missing properties handling - what if no name/photo?**
+- **Answer:** name = domain from URL, photo = None (no image)
+- **Rationale:** Graceful degradation
+- **Implementation:** Use get() with defaults on parsed properties
+
+**Q22: Database schema for author_profile - exact columns?**
+- **Answer:** me_url (PK), name, photo, url, discovered_at, raw_data (JSON)
+- **Rationale:** Cache parsed data + raw for debugging
+- **Implementation:** Single row table, upsert on discovery
+
+## Microformats2 Answers
+
+**Q23: h-card placement - where exactly in templates?**
+- **Answer:** Only within h-entry author property (p-author h-card)
+- **Rationale:** Correct semantic placement per spec
+- **Implementation:** In note partial template, not standalone
+
+**Q24: h-feed container - which pages need it?**
+- **Answer:** Homepage (/) and any paginated list pages
+- **Rationale:** Feed pages only, not single note pages
+- **Implementation:** Wrap note list in div.h-feed with h1.p-name
+
+**Q25: Optional properties - which to include?**
+- **Answer:** Only what we have: author, name, url, published, content
+- **Rationale:** Don't add empty properties
+- **Implementation:** Use conditional template blocks
+
+**Q26: Micropub compatibility - any changes needed?**
+- **Answer:** No, Micropub already handles microformats correctly
+- **Rationale:** Micropub creates data, templates display it
+- **Implementation:** Ensure templates match Micropub's data model
+
+## Feed Integration Answers
+
+**Q27: RSS/Atom changes for media - how to include images?**
+- **Answer:** Add as enclosures (RSS) and link rel="enclosure" (Atom)
+- **Rationale:** Standard podcast/media pattern
+- **Implementation:** Loop through note.media, add enclosure elements
+
+**Q28: JSON Feed media handling - which property?**
+- **Answer:** Use "attachments" array per JSON Feed 1.1 spec
+- **Rationale:** Designed for exactly this use case
+- **Implementation:** Create attachment objects with url, mime_type
+
+**Q29: Feed caching - any changes needed?**
+- **Answer:** No, existing cache logic is sufficient
+- **Rationale:** Media URLs are stable once uploaded
+- **Implementation:** No changes required
+
+**Q30: Author in feeds - use discovered data?**
+- **Answer:** Yes, use discovered name and photo in feed metadata
+- **Rationale:** Consistency across all outputs
+- **Implementation:** Pass author_profile to feed templates
+
+## Database Migration Answers
+
+**Q31: Migration naming convention - what number?**
+- **Answer:** Use next sequential: 005_add_media_support.sql
+- **Rationale:** Continue existing pattern
+- **Implementation:** Check latest migration, increment
+
+**Q32: Migration rollback - needed?**
+- **Answer:** No, forward-only migrations per project convention
+- **Rationale:** Simplicity, follows existing pattern
+- **Implementation:** CREATE IF NOT EXISTS, never DROP
+
+**Q33: Migration testing - how to verify?**
+- **Answer:** Test on copy of production database
+- **Rationale:** Real-world data is best test
+- **Implementation:** Copy data/starpunk.db, run migration, verify
+
+## Testing Strategy Answers
+
+**Q34: Test data for media - what to use?**
+- **Answer:** Generate 1x1 pixel PNG in tests, don't use real files
+- **Rationale:** Minimal, fast, no binary files in repo
+- **Implementation:** Use Pillow to generate test images in memory
+
+**Q35: Author discovery mocking - how to test?**
+- **Answer:** Mock HTTP responses with test h-card HTML
+- **Rationale:** Deterministic, no external dependencies
+- **Implementation:** Use responses library or unittest.mock
+
+**Q36: Integration test priority - which are critical?**
+- **Answer:** Upload → Display → Edit → Delete flow
+- **Rationale:** Core user journey must work
+- **Implementation:** Single test that exercises full lifecycle
+
+## Error Handling Answers
+
+**Q37: Upload failure recovery - how to handle?**
+- **Answer:** Show error, preserve form data, allow retry
+- **Rationale:** Don't lose user's work
+- **Implementation:** Flash error, return to form with content preserved
+
+**Q38: Discovery network timeout - how long to wait?**
+- **Answer:** 5 second timeout for profile fetch
+- **Rationale:** Balance between patience and responsiveness
+- **Implementation:** Use requests timeout parameter
+
+## Deployment Answers
+
+**Q39: Media directory permissions - what's needed?**
+- **Answer:** data/media/ needs write permission for app user
+- **Rationale:** Same as existing data/ directory
+- **Implementation:** Document in deployment guide, create in setup
+
+**Q40: Upgrade path from v1.1.2 - any special steps?**
+- **Answer:** Run migration, create media directory, restart app
+- **Rationale:** Minimal disruption
+- **Implementation:** Add to CHANGELOG upgrade notes
+
+**Q41: Configuration changes - any new env vars?**
+- **Answer:** No, all settings have sensible defaults
+- **Rationale:** Maintain zero-config philosophy
+- **Implementation:** Hardcode limits in code with constants
+
+## Critical Path Decisions Summary
+
+These are the key decisions to unblock implementation:
+
+1. **Media upload flow**: Upload after note creation, associate via note_id
+2. **Author discovery**: Use mf2py, cache for 24hrs, graceful fallbacks
+3. **h-card parsing**: First h-card with matching URL property
+4. **h-card placement**: Only within h-entry as p-author
+5. **Migration strategy**: Sequential numbering (005), forward-only
+
+## Implementation Order
+
+Based on dependencies and complexity:
+
+### Phase 1: Custom Slugs (2 hours)
+- Simplest feature
+- No database changes
+- Template and validation only
+
+### Phase 2: Author Discovery (4 hours)
+- Build discovery module
+- Add author_profile table
+- Integrate with auth flow
+- Update templates
+
+### Phase 3: Media Upload (6 hours)
+- Most complex feature
+- Media table and migration
+- Upload handling
+- Template updates
+- Storage management
+
+## File Structure
+
+Key files to create/modify:
+
+### New Files
+- `starpunk/discovery.py` - Author discovery module
+- `starpunk/media.py` - Media handling module
+- `migrations/005_add_media_support.sql` - Database changes
+- `static/js/media-upload.js` - Optional enhancement
+
+### Modified Files
+- `templates/admin/new.html` - Add slug and media fields
+- `templates/admin/edit.html` - Add slug (readonly) and media
+- `templates/partials/note.html` - Add microformats markup
+- `templates/public/index.html` - Add h-feed container
+- `starpunk/routes/admin.py` - Handle slugs and uploads
+- `starpunk/routes/auth.py` - Trigger discovery on login
+- `starpunk/models/note.py` - Add media relationship
+
+## Success Metrics
+
+Implementation is complete when:
+
+1. ✅ Custom slug can be specified on creation
+2. ✅ Images can be uploaded and displayed
+3. ✅ Author info is discovered from IndieAuth profile
+4. ✅ IndieWebify.me validates h-feed and h-entry
+5. ✅ All tests pass
+6. ✅ No regressions in existing functionality
+7. ✅ Media files are tracked in database
+8. ✅ Errors are handled gracefully
+
+## Final Notes
+
+- Keep it simple - this is v1.2.0, not v2.0.0
+- Data preservation over premature optimization
+- When uncertain, choose the more explicit option
+- Document any deviations from this guidance
+
+---
+
+This Q&A document serves as the authoritative implementation guide for v1.2.0. Any questions not covered here should follow the principle of maximum simplicity.
\ No newline at end of file
diff --git a/docs/design/v1.2.0/feature-specification.md b/docs/design/v1.2.0/feature-specification.md
new file mode 100644
index 0000000..200e964
--- /dev/null
+++ b/docs/design/v1.2.0/feature-specification.md
@@ -0,0 +1,872 @@
+# v1.2.0 Feature Specification
+
+## Overview
+
+Version 1.2.0 focuses on three essential improvements to the StarPunk web interface:
+1. Custom slug support in the web UI
+2. Media upload capability (web UI only, not Micropub)
+3. Complete Microformats2 implementation
+
+## Feature 1: Custom Slugs in Web UI
+
+### Current State
+- Slugs are auto-generated from the first line of content
+- Custom slugs only possible via Micropub API (mp-slug property)
+- Web UI has no option to specify custom slugs
+
+### Requirements
+- Add optional "Slug" field to note creation form
+- Validate slug format (URL-safe, unique)
+- If empty, fall back to auto-generation
+- Support custom slugs in edit form as well
+
+### Design Specification
+
+#### Form Updates
+Location: `templates/admin/new.html` and `templates/admin/edit.html`
+
+Add new form field:
+```html
+
+
+
+ URL-safe characters only (lowercase letters, numbers, hyphens)
+ {% if editing %}
+ Slugs cannot be changed after creation to preserve permalinks
+ {% endif %}
+
+```
+
+#### Backend Changes
+Location: `starpunk/routes/admin.py`
+
+Modify `create_note_submit()`:
+- Extract slug from form data
+- Pass to `create_note()` as `custom_slug` parameter
+- Handle validation errors
+
+Modify `edit_note_submit()`:
+- Display current slug as read-only
+- Do NOT allow slug updates (prevent broken permalinks)
+
+#### Validation Rules
+- Must be URL-safe: `^[a-z0-9-]+$`
+- Maximum length: 200 characters
+- Must be unique (database constraint)
+- Empty string = auto-generate
+- **Read-only after creation** (no editing allowed)
+
+### Acceptance Criteria
+- [ ] Slug field appears in create note form
+- [ ] Slug field appears in edit note form
+- [ ] Custom slugs are validated for format
+- [ ] Custom slugs are validated for uniqueness
+- [ ] Empty field triggers auto-generation
+- [ ] Error messages are user-friendly
+
+---
+
+## Feature 2: Media Upload (Web UI Only)
+
+### Current State
+- No media upload capability
+- Notes are text/markdown only
+- No file storage infrastructure
+
+### Requirements
+- Upload images when creating/editing notes
+- Store uploaded files locally
+- Display media at top of note (social media style)
+- Support multiple media per note
+- Basic file validation
+- NOT implementing Micropub media endpoint (future version)
+
+### Design Specification
+
+#### Conceptual Model
+Media attachments work like social media posts (Twitter, Mastodon, etc.):
+- Media is displayed at the TOP of the note when published
+- Text content appears BELOW the media
+- Multiple images can be attached to a single note (maximum 4)
+- Media is stored as attachments, not inline markdown
+- Display order is upload order (no reordering interface)
+- Each image can have an optional caption for accessibility
+
+#### Storage Architecture
+```
+data/
+ media/
+ 2025/
+ 01/
+ image-slug-12345.jpg
+ another-image-67890.png
+```
+
+URL Structure: `/media/2025/01/filename.jpg` (date-organized paths)
+
+#### Database Schema
+
+**Option A: Junction Table (RECOMMENDED)**
+```sql
+-- Media files table
+CREATE TABLE media (
+ id INTEGER PRIMARY KEY,
+ filename TEXT NOT NULL,
+ original_name TEXT NOT NULL,
+ path TEXT NOT NULL UNIQUE,
+ mime_type TEXT NOT NULL,
+ size INTEGER NOT NULL,
+ width INTEGER, -- Image dimensions for responsive display
+ height INTEGER,
+ uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Note-media relationship table
+CREATE TABLE note_media (
+ id INTEGER PRIMARY KEY,
+ note_id INTEGER NOT NULL,
+ media_id INTEGER NOT NULL,
+ display_order INTEGER NOT NULL DEFAULT 0,
+ caption TEXT, -- Optional alt text/caption
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE,
+ FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE,
+ UNIQUE(note_id, media_id)
+);
+
+CREATE INDEX idx_note_media_note ON note_media(note_id);
+CREATE INDEX idx_note_media_order ON note_media(note_id, display_order);
+```
+
+**Rationale**: Junction table provides flexibility for:
+- Multiple media per note with ordering
+- Reusing media across notes (future)
+- Per-attachment metadata (captions)
+- Efficient queries for syndication feeds
+
+#### Display Strategy
+
+**Note Rendering**:
+```html
+
+
+ {% if note.media %}
+
+ {% for media in note.media %}
+
+ {% endfor %}
+
+ {% else %}
+
+
+ {% for media in note.media[:4] %}
+
+ {% endfor %}
+
+ {% endif %}
+
+ {% endif %}
+
+
+
+ {{ note.html|safe }}
+
+
+```
+
+#### Upload Flow
+1. User selects multiple files via HTML file input
+2. Files validated (type, size)
+3. Files saved to `data/media/YYYY/MM/` with generated names
+4. Database records created in `media` table
+5. Associations created in `note_media` table
+6. Media displayed as thumbnails below textarea
+7. User can remove or reorder attachments
+
+#### Form Updates
+Location: `templates/admin/new.html` and `templates/admin/edit.html`
+
+```html
+
+
+
+```
+
+#### Backend Implementation
+Location: New module `starpunk/media.py`
+
+Key functions:
+- `validate_media_file(file)` - Check type, size (max 10MB), dimensions (max 4096x4096)
+- `optimize_image(file)` - Resize if >2048px, correct EXIF orientation (using Pillow)
+- `save_media_file(file)` - Store optimized version to disk with date-based path
+- `generate_media_url(filename)` - Create public URL
+- `track_media_upload(metadata)` - Save to database
+- `attach_media_to_note(note_id, media_ids, captions)` - Create note-media associations with captions
+- `get_media_by_note(note_id)` - List media for a note ordered by display_order
+- `extract_image_dimensions(file)` - Get width/height for storage
+
+Image Processing with Pillow:
+```python
+from PIL import Image, ImageOps
+
+def optimize_image(file_obj):
+ """Optimize image for web display."""
+ img = Image.open(file_obj)
+
+ # Correct EXIF orientation
+ img = ImageOps.exif_transpose(img)
+
+ # Check dimensions
+ if max(img.size) > 4096:
+ raise ValueError("Image dimensions exceed 4096x4096")
+
+ # Resize if needed (preserve aspect ratio)
+ if max(img.size) > 2048:
+ img.thumbnail((2048, 2048), Image.Resampling.LANCZOS)
+
+ return img
+```
+
+#### Routes
+Location: `starpunk/routes/public.py`
+
+Add route to serve media:
+```python
+@bp.route('/media///')
+def serve_media(year, month, filename):
+ # Serve file from data/media/YYYY/MM/
+ # Set appropriate cache headers
+```
+
+Location: `starpunk/routes/admin.py`
+
+Add upload endpoint:
+```python
+@bp.route('/admin/upload', methods=['POST'])
+@require_auth
+def upload_media():
+ # Handle AJAX upload, return JSON with URL and media_id
+ # Store in media table, return metadata
+```
+
+#### Syndication Feed Support
+
+**RSS 2.0 Strategy**:
+```xml
+
+
+ Note Title
+
+
+
+
+
+
Note text content here...
+
+ ]]>
+ ...
+
+```
+Rationale: RSS `` only supports single items and is meant for podcasts/downloads. HTML in description is standard for blog posts with images.
+
+**ATOM 1.0 Strategy**:
+```xml
+
+
+ Note Title
+
+
+
+ <div class="media">
+ <img src="https://site.com/media/2025/01/image1.jpg" />
+ <img src="https://site.com/media/2025/01/image2.jpg" />
+ </div>
+ <div>Note text content...</div>
+
+
+```
+Rationale: ATOM supports multiple `` elements. We include both enclosures (for feed readers that understand them) AND HTML content (for universal display).
+
+**JSON Feed 1.1 Strategy**:
+```json
+{
+ "id": "...",
+ "title": "Note Title",
+ "content_html": "
...
Note text...
",
+ "attachments": [
+ {
+ "url": "https://site.com/media/2025/01/image1.jpg",
+ "mime_type": "image/jpeg",
+ "size_in_bytes": 123456
+ },
+ {
+ "url": "https://site.com/media/2025/01/image2.jpg",
+ "mime_type": "image/jpeg",
+ "size_in_bytes": 234567
+ }
+ ]
+}
+```
+Rationale: JSON Feed has native support for multiple attachments! This is the cleanest implementation.
+
+**Feed Generation Updates**:
+- Modify `generate_rss()` to prepend media HTML to content
+- Modify `generate_atom()` to add `` elements
+- Modify `generate_json_feed()` to populate `attachments` array
+- Query `note_media` JOIN `media` when generating feeds
+
+#### Security Considerations
+- Validate MIME types server-side (JPEG, PNG, GIF, WebP only)
+- Reject files over 10MB (before processing)
+- Limit total uploads (4 images max per note)
+- Sanitize filenames (remove special characters, use slugify)
+- Prevent directory traversal attacks
+- Add rate limiting to upload endpoint
+- Validate image dimensions (max 4096x4096, reject if larger)
+- Use Pillow to verify file integrity (corrupted files will fail to open)
+- Resize images over 2048px to prevent memory issues
+- Strip potentially harmful EXIF data during optimization
+
+### Acceptance Criteria
+- [ ] Multiple file upload field in create/edit forms
+- [ ] Images saved to data/media/ directory after optimization
+- [ ] Media-note associations tracked in database with captions
+- [ ] Media displayed at TOP of notes
+- [ ] Text content displayed BELOW media
+- [ ] Media served at /media/YYYY/MM/filename
+- [ ] File type validation (JPEG, PNG, GIF, WebP only)
+- [ ] File size validation (10MB max, checked before processing)
+- [ ] Image dimension validation (4096x4096 max)
+- [ ] Automatic resize for images over 2048px
+- [ ] EXIF orientation correction during processing
+- [ ] Max 4 images per note enforced
+- [ ] Caption field for each uploaded image
+- [ ] Captions used as alt text in HTML
+- [ ] Media appears in RSS feeds (HTML in description)
+- [ ] Media appears in ATOM feeds (enclosures + HTML)
+- [ ] Media appears in JSON feeds (attachments array)
+- [ ] User can remove attached images
+- [ ] Display order matches upload order (no reordering UI)
+- [ ] Error handling for invalid/oversized/corrupted files
+
+---
+
+## Feature 3: Complete Microformats2 Support
+
+### Current State
+- Basic h-entry on note pages
+- Basic h-feed on index
+- Missing h-card (author info)
+- Missing many microformats properties
+- No rel=me links
+
+### Requirements
+Full compliance with Microformats2 specification:
+- Complete h-entry implementation
+- Author h-card on all pages
+- Proper h-feed structure
+- rel=me for identity verification
+- All relevant properties marked up
+
+### Design Specification
+
+#### Author Discovery System
+When a user authenticates via IndieAuth, we discover their author information from their profile URL:
+
+1. **Discovery Process** (runs during login):
+ - User logs in with IndieAuth using their domain (e.g., https://user.example.com)
+ - System fetches the user's profile page
+ - Parses h-card microformats from the profile
+ - Extracts: name, photo, bio/note, rel-me links
+ - Caches author info in database (new `author_profile` table)
+
+2. **Database Schema** for Author Profile:
+```sql
+CREATE TABLE author_profile (
+ id INTEGER PRIMARY KEY,
+ me_url TEXT NOT NULL UNIQUE, -- The IndieAuth 'me' URL
+ name TEXT, -- From h-card p-name
+ photo TEXT, -- From h-card u-photo
+ bio TEXT, -- From h-card p-note
+ rel_me_links TEXT, -- JSON array of rel-me URLs
+ discovered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+3. **Caching Strategy**:
+ - Cache on first login
+ - Refresh on each login (but use cache if discovery fails)
+ - Manual refresh button in admin settings
+ - Cache expires after 7 days (configurable)
+
+4. **Fallback Behavior**:
+ - If discovery fails, use cached data if available
+ - If no cache and discovery fails, use minimal defaults:
+ - Name: Domain name (e.g., "user.example.com")
+ - Photo: None (gracefully degrade)
+ - Bio: None
+ - Log discovery failures for debugging
+
+#### h-card (Author Information)
+Location: `templates/partials/author.html` (new)
+
+Required properties from discovered profile:
+- p-name (author name from discovery)
+- u-url (author URL from ADMIN_ME)
+- u-photo (avatar from discovery, optional)
+
+```html
+