feat(tags): Add tag archive route and admin interface integration
Implement Phase 3 of v1.3.0 tags feature per microformats-tags-design.md: Routes (starpunk/routes/public.py): - Add /tag/<tag> archive route with normalization and 404 handling - Pre-load tags in index route for all notes - Pre-load tags in note route for individual notes Admin (starpunk/routes/admin.py): - Parse comma-separated tag input in create route - Parse tag input in update route - Pre-load tags when displaying edit form - Empty tag field removes all tags Templates: - Add tag input field to templates/admin/edit.html - Add tag input field to templates/admin/new.html - Use Jinja2 map filter to display existing tags Implementation details: - Tag URL parameter normalized to lowercase before lookup - Tags pre-loaded using object.__setattr__ pattern (like media) - parse_tag_input() handles trim, dedupe, normalization - All existing tests pass (micropub categories, admin routes) Per architect design: - No pagination on tag archives (acceptable for v1.3.0) - No autocomplete in admin (out of scope) - Follows existing media loading patterns Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
258
docs/design/v1.2.0/2025-12-09-v1.2.0-release.md
Normal file
258
docs/design/v1.2.0/2025-12-09-v1.2.0-release.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# v1.2.0 Release Report
|
||||
|
||||
**Date**: 2025-12-09
|
||||
**Version**: 1.2.0
|
||||
**Release Type**: Stable Minor Release
|
||||
**Previous Version**: 1.1.2
|
||||
|
||||
## Overview
|
||||
|
||||
Successfully promoted v1.2.0-rc.2 to stable v1.2.0 release. This is a major feature release adding comprehensive media support, author discovery, custom slugs, and enhanced syndication feeds.
|
||||
|
||||
## Release Process
|
||||
|
||||
### 1. Version Updates
|
||||
|
||||
**File**: `starpunk/__init__.py`
|
||||
- Updated `__version__` from `"1.2.0-rc.2"` to `"1.2.0"`
|
||||
- Updated `__version_info__` from `(1, 2, 0, "dev")` to `(1, 2, 0)`
|
||||
|
||||
### 2. CHANGELOG Updates
|
||||
|
||||
**File**: `CHANGELOG.md`
|
||||
- Merged rc.1 and rc.2 entries into single `[1.2.0]` section
|
||||
- Added release date: 2025-12-09
|
||||
- Consolidated all features and fixes from both release candidates
|
||||
- Maintained chronological order of changes
|
||||
|
||||
### 3. Git Operations
|
||||
|
||||
**Commit**: `927db4a`
|
||||
```
|
||||
release: Bump version to 1.2.0
|
||||
|
||||
Promote v1.2.0-rc.2 to stable v1.2.0 release
|
||||
|
||||
- Merged rc.1 and rc.2 changelog entries
|
||||
- Updated version in starpunk/__init__.py
|
||||
- All features tested in production
|
||||
```
|
||||
|
||||
**Tag**: `v1.2.0` (annotated)
|
||||
- Comprehensive release notes included
|
||||
- Documents all major features
|
||||
- Notes standards compliance
|
||||
- Includes upgrade instructions
|
||||
|
||||
### 4. Container Images
|
||||
|
||||
Built and pushed container images:
|
||||
- `git.thesatelliteoflove.com/phil/starpunk:v1.2.0`
|
||||
- `git.thesatelliteoflove.com/phil/starpunk:latest`
|
||||
|
||||
**Image Size**: 190 MB
|
||||
**Base**: Python 3.11-slim
|
||||
**Build**: Multi-stage with uv package manager
|
||||
|
||||
### 5. Registry Push
|
||||
|
||||
Successfully pushed to remote:
|
||||
- Git commit pushed to `origin/main`
|
||||
- Git tag `v1.2.0` pushed to remote
|
||||
- Container images pushed to `git.thesatelliteoflove.com` registry
|
||||
|
||||
## Release Contents
|
||||
|
||||
### Major Features
|
||||
|
||||
#### Media Upload & Display
|
||||
- Upload up to 4 images per note (JPEG, PNG, GIF, WebP)
|
||||
- Automatic image optimization with Pillow library
|
||||
- File size limit: 10MB per image
|
||||
- Dimension limit: 4096x4096 pixels
|
||||
- Auto-resize images over 2048px
|
||||
- EXIF orientation correction
|
||||
- Social media style layout (media first, then text)
|
||||
- Optional captions for accessibility
|
||||
- Responsive image sizing with proper CSS
|
||||
|
||||
#### Feed Media Enhancement
|
||||
- Media RSS namespace (xmlns:media) for structured metadata
|
||||
- RSS enclosure element for first image (per RSS 2.0 spec)
|
||||
- Media RSS media:content elements for all images
|
||||
- Media RSS media:thumbnail element for preview
|
||||
- JSON Feed image field (per JSON Feed 1.1 spec)
|
||||
- Enhanced display in modern feed readers (Feedly, Inoreader, NetNewsWire)
|
||||
|
||||
#### Author Profile Discovery
|
||||
- Automatic h-card discovery from IndieAuth identity
|
||||
- Caches author information (name, photo, bio, rel-me links)
|
||||
- 24-hour cache TTL
|
||||
- Graceful fallback to domain name
|
||||
- Never blocks login functionality
|
||||
- Eliminates need for manual author configuration
|
||||
|
||||
#### Complete Microformats2 Support
|
||||
- Full h-entry markup with required properties
|
||||
- Author h-card nested within each h-entry
|
||||
- Proper p-name handling (only when explicit title)
|
||||
- u-uid and u-url match for permalink stability
|
||||
- Homepage as h-feed with proper structure
|
||||
- rel-me links from discovered profile
|
||||
- dt-updated property when note modified
|
||||
- Passes Microformats2 validation
|
||||
|
||||
#### Custom Slugs
|
||||
- Web UI custom slug input field
|
||||
- Optional field with auto-generation fallback
|
||||
- Read-only after creation (preserves permalinks)
|
||||
- Automatic validation and sanitization
|
||||
- Helpful placeholder text and guidance
|
||||
- Matches Micropub mp-slug behavior
|
||||
|
||||
### Fixes from RC Releases
|
||||
|
||||
#### RC.2 Fixes
|
||||
- Media display on homepage (not just individual note pages)
|
||||
- Responsive image sizing with container constraints
|
||||
- Caption display (alt text only, not visible text)
|
||||
- Logging correlation ID crash in non-request contexts
|
||||
|
||||
#### RC.1 Fixes
|
||||
- All features tested and validated in production
|
||||
|
||||
## Standards Compliance
|
||||
|
||||
- W3C Micropub Specification
|
||||
- Microformats2 h-entry, h-card, h-feed
|
||||
- RSS 2.0 with Media RSS extension
|
||||
- JSON Feed 1.1 specification
|
||||
- IndieWeb best practices
|
||||
|
||||
## Testing
|
||||
|
||||
- 600+ tests passing
|
||||
- All features tested in production (rc.1 and rc.2)
|
||||
- Enhanced feed reader compatibility verified
|
||||
- Media upload and display validated
|
||||
- Author discovery tested with multiple profiles
|
||||
|
||||
## Upgrade Instructions
|
||||
|
||||
### From v1.1.2
|
||||
|
||||
No breaking changes. Simple upgrade process:
|
||||
|
||||
1. Pull latest code: `git pull origin main`
|
||||
2. Checkout tag: `git checkout v1.2.0`
|
||||
3. Restart application
|
||||
|
||||
### Configuration
|
||||
|
||||
No configuration changes required. All new features work automatically.
|
||||
|
||||
Optional configuration for media:
|
||||
- `MEDIA_MAX_SIZE` - Max file size in bytes (default: 10MB)
|
||||
- `MEDIA_MAX_DIMENSION` - Max dimension in pixels (default: 4096)
|
||||
- `MEDIA_RESIZE_THRESHOLD` - Auto-resize threshold (default: 2048)
|
||||
|
||||
## Verification
|
||||
|
||||
### Version Check
|
||||
```bash
|
||||
$ uv run python -c "from starpunk import __version__; print(__version__)"
|
||||
1.2.0
|
||||
```
|
||||
|
||||
### Git Tag
|
||||
```bash
|
||||
$ git tag -l v1.2.0
|
||||
v1.2.0
|
||||
|
||||
$ git log -1 --oneline
|
||||
927db4a release: Bump version to 1.2.0
|
||||
```
|
||||
|
||||
### Container Images
|
||||
```bash
|
||||
$ podman images | grep starpunk | grep v1.2.0
|
||||
git.thesatelliteoflove.com/phil/starpunk v1.2.0 20853617ebf1 190 MB
|
||||
git.thesatelliteoflove.com/phil/starpunk latest 20853617ebf1 190 MB
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Updated Files
|
||||
- `/home/phil/Projects/starpunk/starpunk/__init__.py`
|
||||
- `/home/phil/Projects/starpunk/CHANGELOG.md`
|
||||
|
||||
### Release Documentation
|
||||
- Git tag annotation with full release notes
|
||||
- This implementation report
|
||||
- CHANGELOG.md with complete details
|
||||
|
||||
### Existing Documentation (Unchanged)
|
||||
- `/home/phil/Projects/starpunk/docs/design/v1.2.0-media-css-design.md`
|
||||
- `/home/phil/Projects/starpunk/docs/design/v1.1.2-caption-alttext-update.md`
|
||||
- `/home/phil/Projects/starpunk/docs/design/media-display-fixes.md`
|
||||
- `/home/phil/Projects/starpunk/docs/reports/2025-11-28-media-display-fixes.md`
|
||||
|
||||
## Release Timeline
|
||||
|
||||
- **2025-11-28**: v1.2.0-rc.1 released (initial feature complete)
|
||||
- **2025-12-09**: v1.2.0-rc.2 released (media display fixes)
|
||||
- **2025-12-09**: v1.2.0 stable released (production validated)
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
Fully backward compatible with v1.1.2. No breaking changes.
|
||||
|
||||
- Existing notes display correctly
|
||||
- Existing feeds continue working
|
||||
- Existing configuration valid
|
||||
- Existing clients unaffected
|
||||
|
||||
## Known Issues
|
||||
|
||||
None identified. All features tested and stable in production.
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Post-Release
|
||||
1. Monitor production deployment
|
||||
2. Update any documentation references to version numbers
|
||||
3. Announce release to users
|
||||
|
||||
### Future Development (v1.3.0 or v2.0.0)
|
||||
- Additional IndieWeb features (Webmentions, etc.)
|
||||
- Enhanced search capabilities
|
||||
- Performance optimizations
|
||||
- User-requested features
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- `/home/phil/Projects/starpunk/docs/standards/versioning-strategy.md`
|
||||
- `/home/phil/Projects/starpunk/docs/standards/git-branching-strategy.md`
|
||||
- `/home/phil/Projects/starpunk/CHANGELOG.md`
|
||||
|
||||
## Compliance
|
||||
|
||||
This release follows:
|
||||
- Semantic Versioning 2.0.0
|
||||
- Keep a Changelog format
|
||||
- Git workflow from versioning-strategy.md
|
||||
- Developer protocol from CLAUDE.md
|
||||
|
||||
## Summary
|
||||
|
||||
Successfully promoted v1.2.0-rc.2 to stable v1.2.0 release. All steps completed:
|
||||
|
||||
- Version updated in `starpunk/__init__.py`
|
||||
- CHANGELOG.md updated with merged entries
|
||||
- Git commit created and pushed
|
||||
- Annotated tag `v1.2.0` created and pushed
|
||||
- Container images built (v1.2.0 and latest)
|
||||
- Container images pushed to registry
|
||||
- All verification checks passed
|
||||
|
||||
The release is now available for production deployment.
|
||||
300
docs/design/v1.2.0/2025-12-10-documentation-audit-v1.2.0.md
Normal file
300
docs/design/v1.2.0/2025-12-10-documentation-audit-v1.2.0.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# Documentation Audit Report - Post v1.2.0 Release
|
||||
|
||||
**Date**: 2025-12-10
|
||||
**Agent**: Documentation Manager
|
||||
**Scope**: Comprehensive documentation audit and cleanup after v1.2.0 release
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Performed a comprehensive documentation audit of the StarPunk project following the v1.2.0 release. The audit focused on repository structure compliance, design document organization, ADR integrity, and README currency. All issues identified have been resolved, resulting in a well-organized and maintainable documentation system.
|
||||
|
||||
**Overall Documentation Health**: Excellent
|
||||
|
||||
## Audit Findings and Actions
|
||||
|
||||
### 1. Repository Root Compliance
|
||||
|
||||
**Status**: PASS
|
||||
|
||||
**Finding**: Repository root contains only the three approved documentation files:
|
||||
- README.md
|
||||
- CLAUDE.md
|
||||
- CHANGELOG.md
|
||||
|
||||
**Action**: No action required. Root structure is compliant with documentation standards.
|
||||
|
||||
---
|
||||
|
||||
### 2. Misplaced Design Documents
|
||||
|
||||
**Status**: RESOLVED
|
||||
|
||||
**Finding**: Three design documents were located in `/docs/design/` root instead of version-specific subdirectories:
|
||||
- `media-display-fixes.md` (v1.2.0 content)
|
||||
- `v1.1.2-caption-alttext-update.md` (v1.1.2 content, marked as superseded)
|
||||
- `v1.2.0-media-css-design.md` (v1.2.0 content, marked as superseded)
|
||||
|
||||
**Actions Taken**:
|
||||
1. Moved `media-display-fixes.md` → `v1.2.0/media-display-fixes.md`
|
||||
2. Moved `v1.1.2-caption-alttext-update.md` → `v1.1.2/caption-alttext-update.md`
|
||||
3. Moved `v1.2.0-media-css-design.md` → `v1.2.0/media-css-design.md`
|
||||
|
||||
**Rationale**: Version-based organization improves discoverability and maintains clear historical record of design evolution.
|
||||
|
||||
---
|
||||
|
||||
### 3. Legacy Design Documents Organization
|
||||
|
||||
**Status**: RESOLVED
|
||||
|
||||
**Finding**: 31 design documents from v1.0.0 and v1.1.x development remained in `/docs/design/` root, including:
|
||||
- 19 phase-based documents from initial implementation (v1.0.0)
|
||||
- 9 hotfix and diagnostic documents (v1.1.1)
|
||||
- 3 feed enhancement documents (v1.1.2)
|
||||
|
||||
**Actions Taken**:
|
||||
|
||||
**Created Version Folders**:
|
||||
- `/docs/design/v1.0.0/` - For initial implementation (phase-based)
|
||||
- `/docs/design/v1.1.1/` - For authentication hotfix documents
|
||||
|
||||
**Moved v1.0.0 Documents** (19 files):
|
||||
- All `phase-*.md` files (phase 1.1 through 5)
|
||||
- `initial-files.md`, `initial-schema-*.md`
|
||||
- `project-structure.md`
|
||||
- `micropub-endpoint-design.md`
|
||||
|
||||
**Moved v1.1.1 Documents** (9 files):
|
||||
- `auth-redirect-loop-*.md` (diagnosis and fix)
|
||||
- `hotfix-v1.1.1-*.md`
|
||||
- `indieauth-pkce-authentication.md`
|
||||
- `token-security-migration.md`
|
||||
|
||||
**Moved v1.1.2 Documents** (3 files):
|
||||
- `feed-media-handling-options.md`
|
||||
- `feed-media-option2-design.md`
|
||||
- `caption-alttext-update.md`
|
||||
|
||||
**Result**: Only `INDEX.md` remains in `/docs/design/` root, which is correct and expected.
|
||||
|
||||
---
|
||||
|
||||
### 4. Design Documentation INDEX Update
|
||||
|
||||
**Status**: COMPLETED
|
||||
|
||||
**Finding**: The `/docs/design/INDEX.md` file referenced the old flat structure with phase documents in the root.
|
||||
|
||||
**Actions Taken**:
|
||||
1. Rewrote INDEX.md to reflect version-based organization
|
||||
2. Added clear organization section listing all version folders
|
||||
3. Documented key design documents for each version
|
||||
4. Updated "How to Use" section for version-based navigation
|
||||
5. Updated "Document Types" to reflect current patterns
|
||||
6. Updated last-modified date to 2025-12-10
|
||||
|
||||
**New Structure**:
|
||||
- Organization section with version folder listing
|
||||
- Version-specific sections (v1.0.0, v1.1.1, v1.1.2, v1.2.0)
|
||||
- Key documents highlighted for each version
|
||||
- Updated usage guidance for developers
|
||||
|
||||
---
|
||||
|
||||
### 5. ADR Numbering Sequence
|
||||
|
||||
**Status**: VERIFIED - No Issues
|
||||
|
||||
**Finding**: ADR sequence shows a gap: jumps from ADR-059 to ADR-061, missing ADR-060.
|
||||
|
||||
**Investigation**:
|
||||
- ADR-059 references "Option 2 (ADR-060)" for Media RSS implementation
|
||||
- Media RSS was implemented in v1.2.0 (confirmed in CHANGELOG)
|
||||
- No separate ADR-060 document was created
|
||||
|
||||
**Conclusion**: ADR-060 was planned but implementation happened without creating a separate ADR. The decision is adequately documented in ADR-059 itself, which describes both Option 2 (basic Media RSS) and Option 3 (full standardization). This is acceptable - not every decision requires a separate ADR when well-documented in a related ADR.
|
||||
|
||||
**Recommendation**: If future work requires more detailed Media RSS decisions, create ADR-060 at that time. For now, ADR-059 provides sufficient documentation.
|
||||
|
||||
---
|
||||
|
||||
### 6. README.md Currency
|
||||
|
||||
**Status**: UPDATED
|
||||
|
||||
**Finding**: README.md showed version 1.1.0 but current version is 1.2.0. Major v1.2.0 features were not documented.
|
||||
|
||||
**Actions Taken**:
|
||||
|
||||
**Version Updates**:
|
||||
- Changed "Current Version: 1.1.0" → "1.2.0"
|
||||
- Updated versioning section to reflect current 1.2.0 stable release
|
||||
|
||||
**Features Section Enhancements**:
|
||||
Added new v1.2.0 features:
|
||||
- Media attachments (image upload and display)
|
||||
- Microformats2 (full h-entry, h-card, h-feed markup)
|
||||
- Author discovery (automatic profile discovery)
|
||||
- RSS, ATOM, JSON Feed (multiple syndication formats)
|
||||
- Custom slugs (permalink control)
|
||||
|
||||
Updated from "6 core dependencies" to "Core dependencies" (more accurate, less maintenance burden).
|
||||
|
||||
**IndieWeb Compliance Section**:
|
||||
Enhanced to include:
|
||||
- "RSS 2.0 with Media RSS extensions" (was just "RSS 2.0")
|
||||
- Added "ATOM 1.0 - Syndication format"
|
||||
- Added "JSON Feed 1.1 - Modern feed format"
|
||||
- Enhanced Microformats2 description to mention h-entry, h-card, h-feed
|
||||
|
||||
**Result**: README now accurately reflects v1.2.0 capabilities and feature set.
|
||||
|
||||
---
|
||||
|
||||
### 7. Reports Folder Organization
|
||||
|
||||
**Status**: EXCELLENT - No Action Required
|
||||
|
||||
**Finding**: The `/docs/reports/` folder is well-organized with 76 implementation reports.
|
||||
|
||||
**Observations**:
|
||||
- All reports follow naming convention: `YYYY-MM-DD-description.md`
|
||||
- Clear chronological ordering (oldest: 2025-11-18, newest: 2025-12-09)
|
||||
- Version-tagged reports (e.g., `v1.2.0-phase1-custom-slugs.md`)
|
||||
- One untracked file: `2025-12-09-v1.2.0-release.md` (appropriate for v1.2.0)
|
||||
|
||||
**Action**: Renamed one report for consistency:
|
||||
- `2025-11-28-media-display-fixes.md` → `2025-11-28-v1.2.0-media-display-fixes.md`
|
||||
|
||||
**Assessment**: Reports folder follows best practices and needs no further cleanup.
|
||||
|
||||
---
|
||||
|
||||
### 8. Superseded Documents
|
||||
|
||||
**Status**: VERIFIED
|
||||
|
||||
**Finding**: Several documents marked as "Superseded" were found:
|
||||
- `v1.1.2/caption-alttext-update.md` - Superseded by `media-display-fixes.md`
|
||||
- `v1.2.0/media-css-design.md` - Superseded by `media-display-fixes.md`
|
||||
- Various ADRs with superseded status headers
|
||||
|
||||
**Assessment**:
|
||||
- Superseded documents are properly marked with status headers
|
||||
- They are retained for historical context (correct approach)
|
||||
- They are now organized in version folders (improves discoverability)
|
||||
- Cross-references to superseding documents are present
|
||||
|
||||
**Action**: No action required. Superseded documents are properly handled.
|
||||
|
||||
---
|
||||
|
||||
## Documentation Organization Summary
|
||||
|
||||
### Repository Root
|
||||
```
|
||||
/
|
||||
├── README.md ✓ Updated to v1.2.0
|
||||
├── CLAUDE.md ✓ Current
|
||||
├── CHANGELOG.md ✓ Current
|
||||
```
|
||||
|
||||
### Design Documentation Structure
|
||||
```
|
||||
docs/design/
|
||||
├── INDEX.md ✓ Updated for version-based structure
|
||||
├── v1.0.0/ ✓ 19 documents (initial implementation)
|
||||
├── v1.1.1/ ✓ 9 documents (hotfix)
|
||||
├── v1.1.2/ ✓ 10 documents (feed enhancements)
|
||||
└── v1.2.0/ ✓ 6 documents (media and IndieWeb)
|
||||
```
|
||||
|
||||
### ADR Status
|
||||
- Total ADRs: 56 (ADR-001 through ADR-061, excluding ADR-060)
|
||||
- Gap at ADR-060: Acceptable (documented in ADR-059)
|
||||
- All ADRs properly numbered and sequenced
|
||||
- Superseded ADRs have status headers
|
||||
|
||||
### Reports Status
|
||||
- Total reports: 76 implementation reports
|
||||
- All follow naming convention: `YYYY-MM-DD-description.md`
|
||||
- Date range: 2025-11-18 to 2025-12-10
|
||||
- Well-organized, chronologically ordered
|
||||
|
||||
---
|
||||
|
||||
## Git Changes Summary
|
||||
|
||||
The following files were moved/renamed using `git mv` to preserve history:
|
||||
|
||||
**Design Document Relocations** (34 files):
|
||||
- 19 files → `docs/design/v1.0.0/`
|
||||
- 9 files → `docs/design/v1.1.1/`
|
||||
- 3 files → `docs/design/v1.1.2/`
|
||||
- 3 files → `docs/design/v1.2.0/`
|
||||
|
||||
**Report Rename** (1 file):
|
||||
- `2025-11-28-media-display-fixes.md` → `2025-11-28-v1.2.0-media-display-fixes.md`
|
||||
|
||||
**Documentation Updates** (2 files):
|
||||
- `README.md` - Version and features updated
|
||||
- `docs/design/INDEX.md` - Complete restructure for version-based organization
|
||||
|
||||
**Total Changes**: 37 file operations + 2 content updates
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate Actions
|
||||
None required. All issues have been resolved.
|
||||
|
||||
### Future Maintenance
|
||||
|
||||
1. **Design Document Discipline**
|
||||
- Always create new design docs in appropriate version folder
|
||||
- Use version prefixes in filenames for cross-version documents
|
||||
- Update INDEX.md when adding new version folders
|
||||
|
||||
2. **ADR Management**
|
||||
- Continue sequential numbering (next: ADR-062)
|
||||
- Consider creating ADR-060 if Media RSS needs detailed decision doc
|
||||
- Always mark superseded ADRs with status headers
|
||||
|
||||
3. **README Maintenance**
|
||||
- Update version number on each release
|
||||
- Add new features to features section
|
||||
- Keep IndieWeb compliance section current
|
||||
|
||||
4. **Reports Best Practices**
|
||||
- Continue using `YYYY-MM-DD-description.md` format
|
||||
- Include version prefix for version-specific work
|
||||
- Create reports for all significant implementations
|
||||
|
||||
### Documentation Health Indicators
|
||||
|
||||
Monitor these metrics to maintain documentation quality:
|
||||
|
||||
- **Root Cleanliness**: Only README.md, CLAUDE.md, CHANGELOG.md in root
|
||||
- **Design Organization**: All design docs in version folders (except INDEX.md)
|
||||
- **ADR Sequence**: Sequential numbering with documented gaps
|
||||
- **Report Consistency**: All reports follow naming convention
|
||||
- **README Currency**: Version and features match current release
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The StarPunk documentation is now in excellent health following the v1.2.0 release. All structural issues have been resolved, historical documents are properly organized by version, and the README accurately reflects current capabilities.
|
||||
|
||||
The version-based organization of design documents provides a clear historical record and improves discoverability. The reports folder demonstrates excellent discipline with consistent naming and comprehensive coverage of implementation work.
|
||||
|
||||
**Documentation Health Score**: A+ (Excellent)
|
||||
|
||||
**Ready for v1.3.0 Development**: Yes
|
||||
|
||||
---
|
||||
|
||||
**Audit Completed**: 2025-12-10
|
||||
**Maintained By**: Documentation Manager Agent
|
||||
**Next Audit Recommended**: After v1.3.0 release
|
||||
@@ -1,303 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,872 +0,0 @@
|
||||
# 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
|
||||
<div class="form-group">
|
||||
<label for="slug">Custom Slug (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="slug"
|
||||
name="slug"
|
||||
pattern="[a-z0-9-]+"
|
||||
maxlength="200"
|
||||
placeholder="leave-blank-for-auto-generation"
|
||||
{% if editing %}readonly{% endif %}
|
||||
>
|
||||
<small>URL-safe characters only (lowercase letters, numbers, hyphens)</small>
|
||||
{% if editing %}
|
||||
<small class="text-warning">Slugs cannot be changed after creation to preserve permalinks</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 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
|
||||
<article class="note">
|
||||
<!-- Media displayed first -->
|
||||
{% if note.media %}
|
||||
<div class="media-attachments">
|
||||
{% if note.media|length == 1 %}
|
||||
<!-- Single image: full width -->
|
||||
<img src="{{ media.url }}" alt="{{ media.caption or '' }}" class="single-image">
|
||||
{% elif note.media|length == 2 %}
|
||||
<!-- Two images: side by side -->
|
||||
<div class="media-grid media-grid-2">
|
||||
{% for media in note.media %}
|
||||
<img src="{{ media.url }}" alt="{{ media.caption or '' }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- 3-4 images: grid layout -->
|
||||
<div class="media-grid media-grid-{{ note.media|length }}">
|
||||
{% for media in note.media[:4] %}
|
||||
<img src="{{ media.url }}" alt="{{ media.caption or '' }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Text content displayed below media -->
|
||||
<div class="content">
|
||||
{{ note.html|safe }}
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
#### 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
|
||||
<div class="form-group">
|
||||
<label for="media">Attach Images</label>
|
||||
<input
|
||||
type="file"
|
||||
id="media"
|
||||
name="media"
|
||||
accept="image/*"
|
||||
multiple
|
||||
class="media-upload"
|
||||
>
|
||||
<small>Accepted formats: JPG, PNG, GIF, WebP (max 10MB each, max 4 images)</small>
|
||||
|
||||
<!-- Preview attached media with captions -->
|
||||
<div id="media-preview" class="media-preview">
|
||||
<!-- Thumbnails appear here after upload with caption fields -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Handle media as attachments, not inline insertion
|
||||
document.getElementById('media').addEventListener('change', async (e) => {
|
||||
const preview = document.getElementById('media-preview');
|
||||
const files = Array.from(e.target.files).slice(0, 4); // Max 4
|
||||
|
||||
for (const file of files) {
|
||||
// Upload and show thumbnail
|
||||
const url = await uploadMedia(file);
|
||||
addMediaThumbnail(preview, url, file.name);
|
||||
}
|
||||
});
|
||||
|
||||
function addMediaThumbnail(container, url, filename) {
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = 'media-thumb';
|
||||
thumb.innerHTML = `
|
||||
<img src="${url}" alt="${filename}">
|
||||
<input type="text" name="caption[]" placeholder="Caption (optional)" class="media-caption">
|
||||
<button type="button" class="remove-media" data-url="${url}">×</button>
|
||||
<input type="hidden" name="attached_media[]" value="${url}">
|
||||
`;
|
||||
container.appendChild(thumb);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 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/<year>/<month>/<filename>')
|
||||
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
|
||||
<!-- Embed media as HTML in description with CDATA -->
|
||||
<item>
|
||||
<title>Note Title</title>
|
||||
<description><![CDATA[
|
||||
<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 class="content">
|
||||
<p>Note text content here...</p>
|
||||
</div>
|
||||
]]></description>
|
||||
<pubDate>...</pubDate>
|
||||
</item>
|
||||
```
|
||||
Rationale: RSS `<enclosure>` 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
|
||||
<!-- Multiple link elements with rel="enclosure" for each media item -->
|
||||
<entry>
|
||||
<title>Note Title</title>
|
||||
<link rel="enclosure"
|
||||
type="image/jpeg"
|
||||
href="https://site.com/media/2025/01/image1.jpg"
|
||||
length="123456" />
|
||||
<link rel="enclosure"
|
||||
type="image/jpeg"
|
||||
href="https://site.com/media/2025/01/image2.jpg"
|
||||
length="234567" />
|
||||
<content type="html">
|
||||
<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>
|
||||
</content>
|
||||
</entry>
|
||||
```
|
||||
Rationale: ATOM supports multiple `<link rel="enclosure">` 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": "<div class='media'>...</div><div>Note text...</div>",
|
||||
"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 `<link rel="enclosure">` 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
|
||||
<div class="h-card">
|
||||
<a class="p-name u-url" href="{{ author.me_url }}">
|
||||
{{ author.name or author.me_url }}
|
||||
</a>
|
||||
{% if author.photo %}
|
||||
<img class="u-photo" src="{{ author.photo }}" alt="{{ author.name }}">
|
||||
{% endif %}
|
||||
{% if author.bio %}
|
||||
<p class="p-note">{{ author.bio }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Enhanced h-entry
|
||||
Location: `templates/note.html`
|
||||
|
||||
Complete properties with discovered author and media support:
|
||||
- p-name (note title, if exists)
|
||||
- e-content (note content)
|
||||
- dt-published (creation date)
|
||||
- dt-updated (modification date)
|
||||
- u-url (permalink)
|
||||
- p-author (nested h-card with discovered info)
|
||||
- u-uid (unique identifier)
|
||||
- u-photo (multiple for multi-photo posts)
|
||||
- p-category (tags, future)
|
||||
|
||||
```html
|
||||
<article class="h-entry">
|
||||
<!-- Multiple u-photo for multi-photo posts (social media style) -->
|
||||
{% if note.media %}
|
||||
{% for media in note.media %}
|
||||
<img class="u-photo" src="{{ media.url }}" alt="{{ media.caption or '' }}">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Text content -->
|
||||
<div class="e-content">
|
||||
{{ note.html|safe }}
|
||||
</div>
|
||||
|
||||
<!-- Title only if exists (most notes won't have titles) -->
|
||||
{% if note.has_explicit_title %}
|
||||
<h1 class="p-name">{{ note.title }}</h1>
|
||||
{% endif %}
|
||||
|
||||
<footer>
|
||||
<a class="u-url u-uid" href="{{ url }}">
|
||||
<time class="dt-published" datetime="{{ iso_date }}">
|
||||
{{ formatted_date }}
|
||||
</time>
|
||||
</a>
|
||||
|
||||
{% if note.updated_at %}
|
||||
<time class="dt-updated" datetime="{{ updated_iso }}">
|
||||
Updated: {{ updated_formatted }}
|
||||
</time>
|
||||
{% endif %}
|
||||
|
||||
<!-- Author h-card only within h-entry -->
|
||||
<div class="p-author h-card">
|
||||
<a class="p-name u-url" href="{{ author.me_url }}">
|
||||
{{ author.name or author.me_url }}
|
||||
</a>
|
||||
{% if author.photo %}
|
||||
<img class="u-photo" src="{{ author.photo }}" alt="{{ author.name }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
```
|
||||
|
||||
**Multi-photo Implementation Notes**:
|
||||
- Multiple `u-photo` elements indicate a multi-photo post (like Instagram, Twitter)
|
||||
- Photos are considered primary content when present
|
||||
- Consuming applications (like Bridgy) will respect platform limits (e.g., Twitter's 4-photo max)
|
||||
- Photos appear BEFORE text content, matching social media conventions
|
||||
|
||||
#### Enhanced h-feed
|
||||
Location: `templates/index.html`
|
||||
|
||||
Required structure:
|
||||
- h-feed container
|
||||
- p-name (feed title)
|
||||
- p-author (feed author)
|
||||
- Multiple h-entry children
|
||||
|
||||
#### rel=me Links
|
||||
Location: `templates/base.html`
|
||||
|
||||
Add to <head> using discovered rel-me links:
|
||||
```html
|
||||
{% if author.rel_me_links %}
|
||||
{% for profile in author.rel_me_links %}
|
||||
<link rel="me" href="{{ profile }}">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
#### Discovery Module
|
||||
Location: New module `starpunk/author_discovery.py`
|
||||
|
||||
Key functions:
|
||||
- `discover_author_info(me_url)` - Fetch and parse h-card from profile
|
||||
- `parse_hcard(html, url)` - Extract h-card properties
|
||||
- `parse_rel_me(html, url)` - Extract rel-me links
|
||||
- `cache_author_profile(profile_data)` - Store in database
|
||||
- `get_cached_author(me_url)` - Retrieve from cache
|
||||
- `refresh_author_profile(me_url)` - Force refresh
|
||||
|
||||
Integration points:
|
||||
- Called during IndieAuth login success in `auth_external.py`
|
||||
- Admin settings page for manual refresh (`/admin/settings`)
|
||||
- Template context processor to inject author data globally
|
||||
|
||||
#### Microformats Parsing
|
||||
Use existing library for parsing:
|
||||
- Option 1: `mf2py` - Python microformats2 parser
|
||||
- Option 2: Custom minimal parser (lighter weight)
|
||||
|
||||
Parse these specific properties:
|
||||
- h-card properties: name, photo, url, note, email
|
||||
- rel-me links for identity verification
|
||||
- Store as JSON in database for flexibility
|
||||
|
||||
### Testing & Validation
|
||||
|
||||
Use these tools to validate:
|
||||
1. https://indiewebify.me/ - Complete IndieWeb validation
|
||||
2. https://microformats.io/ - Microformats parser
|
||||
3. https://search.google.com/test/rich-results - Google's structured data test
|
||||
|
||||
### Acceptance Criteria
|
||||
- [ ] Author info discovered from IndieAuth profile URL
|
||||
- [ ] h-card present within h-entries only (not standalone)
|
||||
- [ ] h-entry has all required properties
|
||||
- [ ] h-feed properly structures the homepage
|
||||
- [ ] rel=me links in HTML head (from discovery)
|
||||
- [ ] Passes indiewebify.me Level 2 tests
|
||||
- [ ] Parsed correctly by microformats.io
|
||||
- [ ] Graceful fallback when discovery fails
|
||||
- [ ] Author profile cached in database
|
||||
- [ ] Manual refresh option in admin
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
Recommended implementation sequence:
|
||||
|
||||
1. **Custom Slugs** (simplest, least dependencies)
|
||||
- Modify forms
|
||||
- Update backend
|
||||
- Test uniqueness
|
||||
|
||||
2. **Microformats2** (template-only changes)
|
||||
- Add h-card partial
|
||||
- Enhance h-entry
|
||||
- Add rel=me links
|
||||
- Validate with tools
|
||||
|
||||
3. **Media Upload** (most complex)
|
||||
- Create media module
|
||||
- Add upload forms
|
||||
- Implement storage
|
||||
- Add serving route
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following are explicitly NOT included in v1.2.0:
|
||||
|
||||
- Micropub media endpoint
|
||||
- Video upload support
|
||||
- Thumbnail generation (separate from main image)
|
||||
- CDN integration
|
||||
- Media gallery interface
|
||||
- Webmention support
|
||||
- Multi-user support
|
||||
- Self-hosted IndieAuth (see ADR-056)
|
||||
|
||||
---
|
||||
|
||||
## Database Schema Changes
|
||||
|
||||
Required schema changes for v1.2.0:
|
||||
|
||||
### 1. Media Tables
|
||||
```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
|
||||
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);
|
||||
```
|
||||
|
||||
### 2. Author Profile Table
|
||||
```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
|
||||
);
|
||||
```
|
||||
|
||||
### 3. No Changes Required For:
|
||||
- Custom slugs: Already supported via existing `slug` column
|
||||
|
||||
---
|
||||
|
||||
## Configuration Changes
|
||||
|
||||
New configuration variables:
|
||||
```
|
||||
# Media settings
|
||||
MAX_UPLOAD_SIZE=10485760 # 10MB in bytes
|
||||
ALLOWED_MEDIA_TYPES=image/jpeg,image/png,image/gif,image/webp
|
||||
MEDIA_PATH=data/media # Storage location
|
||||
|
||||
# Author discovery settings
|
||||
AUTHOR_CACHE_TTL=604800 # 7 days in seconds
|
||||
AUTHOR_DISCOVERY_TIMEOUT=5.0 # HTTP timeout for profile fetch
|
||||
```
|
||||
|
||||
Note: Author information is NOT configured via environment variables. It is discovered from the authenticated user's IndieAuth profile URL.
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **File Upload Security**
|
||||
- Validate MIME types
|
||||
- Check file extensions
|
||||
- Limit file sizes
|
||||
- Sanitize filenames
|
||||
- Store outside web root if possible
|
||||
|
||||
2. **Slug Validation**
|
||||
- Prevent directory traversal
|
||||
- Enforce URL-safe characters
|
||||
- Check uniqueness
|
||||
|
||||
3. **Microformats**
|
||||
- No security implications
|
||||
- Ensure proper HTML escaping continues
|
||||
|
||||
---
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests
|
||||
- Slug validation logic
|
||||
- Media file validation
|
||||
- Unique filename generation
|
||||
|
||||
### Integration Tests
|
||||
- Custom slug creation flow
|
||||
- Media upload and serving
|
||||
- Microformats parsing
|
||||
|
||||
### Manual Testing
|
||||
- Upload various image formats
|
||||
- Try invalid slugs
|
||||
- Validate microformats output
|
||||
- Test with screen readers
|
||||
|
||||
---
|
||||
|
||||
## Additional Design Considerations
|
||||
|
||||
### Media Upload Details
|
||||
1. **Social Media Model**: Media works like Twitter/Mastodon posts
|
||||
- Media displays at TOP of note
|
||||
- Text appears BELOW media
|
||||
- Multiple images supported (max 4)
|
||||
- No inline markdown images (attachments only)
|
||||
- Display order is upload order (no reordering)
|
||||
|
||||
2. **File Type Restrictions**:
|
||||
- Accept: image/jpeg, image/png, image/gif, image/webp
|
||||
- Reject: SVG (security), video formats (v1.2.0 scope)
|
||||
- Validate MIME type server-side, not just extension
|
||||
|
||||
3. **Image Processing** (using Pillow):
|
||||
- Automatic resize if >2048px (longest edge)
|
||||
- EXIF orientation correction
|
||||
- File integrity validation
|
||||
- Preserve aspect ratio
|
||||
- Quality setting: 95 (high quality)
|
||||
- No separate thumbnail generation
|
||||
|
||||
4. **Display Layout**:
|
||||
- 1 image: Full width
|
||||
- 2 images: Side by side (50% each)
|
||||
- 3 images: Grid (1 large + 2 small, or equal grid)
|
||||
- 4 images: 2x2 grid
|
||||
|
||||
5. **Image Limits** (per ADR-058):
|
||||
- Max file size: 10MB per image
|
||||
- Max dimensions: 4096x4096 pixels
|
||||
- Auto-resize threshold: 2048 pixels (longest edge)
|
||||
- Max images per note: 4
|
||||
|
||||
6. **Accessibility Features**:
|
||||
- Optional caption field for each image
|
||||
- Captions stored in `note_media.caption`
|
||||
- Used as alt text in HTML output
|
||||
- Included in syndication feeds
|
||||
|
||||
7. **Database Design Rationale**:
|
||||
- Junction table allows flexible ordering
|
||||
- Supports future media reuse across notes
|
||||
- Per-attachment captions for accessibility
|
||||
- Efficient queries for feed generation
|
||||
|
||||
8. **Feed Syndication Strategy**:
|
||||
- RSS: HTML with images in description (universal support)
|
||||
- ATOM: Both enclosures AND HTML content (best compatibility)
|
||||
- JSON Feed: Native attachments array (cleanest implementation)
|
||||
|
||||
### Slug Handling
|
||||
1. **Absolute No-Edit Policy**: Once created, slugs are immutable
|
||||
- No admin override
|
||||
- No database updates allowed
|
||||
- Prevents broken permalinks completely
|
||||
|
||||
2. **Validation Pattern**: `^[a-z0-9-]+$`
|
||||
- Lowercase only for consistency
|
||||
- No underscores (hyphens preferred)
|
||||
- No special characters
|
||||
|
||||
### Author Discovery Edge Cases
|
||||
1. **Multiple h-cards on Profile**:
|
||||
- Use first representative h-card (class="h-card" on body or first found)
|
||||
- Log if multiple found for debugging
|
||||
|
||||
2. **Missing Properties**:
|
||||
- Name: Falls back to domain
|
||||
- Photo: Omit if not found
|
||||
- Bio: Omit if not found
|
||||
- All properties are optional except URL
|
||||
|
||||
3. **Network Failures**:
|
||||
- Use cached data even if expired
|
||||
- Log failure for monitoring
|
||||
- Never block login due to discovery failure
|
||||
|
||||
4. **Invalid Markup**:
|
||||
- Best-effort parsing
|
||||
- Log parsing errors
|
||||
- Use whatever can be extracted
|
||||
|
||||
## Success Metrics
|
||||
|
||||
v1.2.0 is successful when:
|
||||
1. Users can specify custom slugs via web UI (immutable after creation)
|
||||
2. Users can upload images via web UI with auto-insertion
|
||||
3. Author info discovered from IndieAuth profile
|
||||
4. Site passes IndieWebify.me Level 2
|
||||
5. All existing tests continue to pass
|
||||
6. No regression in existing functionality
|
||||
7. Media tracked in database with metadata
|
||||
8. Graceful handling of discovery failures
|
||||
@@ -1,114 +0,0 @@
|
||||
# CSS Design for Media Display (v1.2.0)
|
||||
|
||||
## Status
|
||||
**Superseded by media-display-fixes.md**
|
||||
|
||||
This document contains an earlier design iteration. The authoritative specification is now in `media-display-fixes.md` which provides a more comprehensive solution including template refactoring and consistent media display across all pages.
|
||||
|
||||
## Problem Statement
|
||||
Images uploaded via the media upload feature display at full resolution, breaking layout bounds and creating poor user experience. Need CSS rules to constrain and style images appropriately.
|
||||
|
||||
## Design Decision
|
||||
|
||||
### CSS Rules to Add
|
||||
|
||||
Add the following CSS rules after line 49 (after `.empty-state` rules) in `/home/phil/Projects/starpunk/static/css/style.css`:
|
||||
|
||||
```css
|
||||
/* Media Display Styles (v1.2.0) */
|
||||
.note-media { margin-bottom: var(--spacing-md); }
|
||||
.note-media figure, .e-content figure { margin: 0 0 var(--spacing-md) 0; }
|
||||
.note-media img, .e-content img, .u-photo { max-width: 100%; height: auto; display: block; border-radius: var(--border-radius); }
|
||||
.note-media figcaption, .e-content figcaption { margin-top: var(--spacing-sm); font-size: 0.875rem; color: var(--color-text-light); font-style: italic; }
|
||||
|
||||
/* Multiple media items grid */
|
||||
.note-media { display: flex; flex-wrap: wrap; gap: var(--spacing-md); }
|
||||
.note-media .media-item { flex: 1 1 100%; }
|
||||
|
||||
/* Desktop: side-by-side for multiple images */
|
||||
@media (min-width: 768px) {
|
||||
.note-media .media-item:only-child { flex: 1 1 100%; }
|
||||
.note-media .media-item:not(:only-child) { flex: 1 1 calc(50% - var(--spacing-sm)); }
|
||||
}
|
||||
```
|
||||
|
||||
## Rationale
|
||||
|
||||
### 1. Responsive Image Constraints
|
||||
- `max-width: 100%` ensures images never exceed container width
|
||||
- `height: auto` maintains aspect ratio
|
||||
- `display: block` removes inline spacing issues
|
||||
- Works with existing HTML `width` and `height` attributes for proper aspect ratio hints
|
||||
|
||||
### 2. Consistent Visual Design
|
||||
- `border-radius: var(--border-radius)` matches existing design system (4px)
|
||||
- Uses existing spacing variables for consistent margins
|
||||
- Caption styling matches `.note-meta` text style (0.875rem, light gray)
|
||||
|
||||
### 3. Flexible Layout
|
||||
- Single images take full width
|
||||
- Multiple images display in a responsive grid
|
||||
- Mobile: stacked vertically (100% width each)
|
||||
- Desktop: two columns for multiple images (50% width each)
|
||||
- Flexbox with gap provides clean spacing
|
||||
|
||||
### 4. Scope Coverage
|
||||
- `.note-media img` - images in the media section
|
||||
- `.e-content img` - images in markdown content
|
||||
- `.u-photo` - microformats photo class (covers both media and author photos)
|
||||
- Applies to both `figure` and standalone `img` elements
|
||||
|
||||
### 5. Performance Considerations
|
||||
- No complex calculations or transforms
|
||||
- Leverages browser native image sizing
|
||||
- Uses existing CSS variables (no new computations)
|
||||
- Respects HTML width/height attributes for layout stability
|
||||
|
||||
## Alternative Approaches Considered
|
||||
|
||||
### Object-fit Approach (Rejected)
|
||||
```css
|
||||
img { object-fit: cover; width: 100%; height: 400px; }
|
||||
```
|
||||
- Rejected: Crops images, losing content
|
||||
- Rejected: Fixed height doesn't work for varied aspect ratios
|
||||
|
||||
### Container Query Approach (Rejected)
|
||||
```css
|
||||
@container (min-width: 600px) { ... }
|
||||
```
|
||||
- Rejected: Limited browser support
|
||||
- Rejected: Unnecessary complexity for this use case
|
||||
|
||||
### CSS Grid Approach (Rejected)
|
||||
```css
|
||||
.note-media { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
|
||||
```
|
||||
- Rejected: More complex than needed
|
||||
- Rejected: Less flexible for single vs multiple images
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. **Location in style.css**: Insert after line 49, before `.form-group` rules
|
||||
2. **Testing Required**:
|
||||
- Single image display
|
||||
- Multiple images (2, 3, 4 images)
|
||||
- Portrait and landscape orientations
|
||||
- Mobile and desktop viewports
|
||||
- Images in markdown content
|
||||
- Author avatar photos
|
||||
|
||||
3. **Browser Compatibility**: All rules use widely supported CSS features (flexbox, max-width, CSS variables)
|
||||
|
||||
4. **Future Enhancements** (not for v1.2.0):
|
||||
- Lightbox/modal for full-size viewing
|
||||
- Lazy loading optimization
|
||||
- WebP format support
|
||||
- Image galleries with thumbnails
|
||||
|
||||
## Standards Compliance
|
||||
|
||||
- **IndieWeb**: Preserves `.u-photo` microformat class
|
||||
- **Accessibility**: Maintains alt text display, proper figure/figcaption semantics
|
||||
- **Performance**: No JavaScript required, pure CSS solution
|
||||
- **Progressive Enhancement**: Images remain functional without CSS
|
||||
@@ -1,269 +0,0 @@
|
||||
# Media Upload Implementation Guide
|
||||
|
||||
## Overview
|
||||
This guide provides implementation details for the v1.2.0 media upload feature based on the finalized design.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Image Limits (per ADR-058)
|
||||
- **Max file size**: 10MB per image (reject before processing)
|
||||
- **Max dimensions**: 4096x4096 pixels (reject if larger)
|
||||
- **Auto-resize threshold**: 2048 pixels on longest edge
|
||||
- **Max images per note**: 4
|
||||
- **Accepted formats**: JPEG, PNG, GIF, WebP only
|
||||
|
||||
### Features
|
||||
- **Caption support**: Each image has optional caption field
|
||||
- **No reordering**: Display order matches upload order
|
||||
- **Auto-optimization**: Images >2048px automatically resized
|
||||
- **EXIF correction**: Orientation fixed during processing
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
### 1. Dependencies
|
||||
Add to `pyproject.toml`:
|
||||
```toml
|
||||
dependencies = [
|
||||
# ... existing dependencies
|
||||
"Pillow>=10.0.0", # Image processing
|
||||
]
|
||||
```
|
||||
|
||||
### 2. Image Processing Module Structure
|
||||
Create `starpunk/media.py`:
|
||||
|
||||
```python
|
||||
from PIL import Image, ImageOps
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
class MediaProcessor:
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
MAX_DIMENSIONS = 4096
|
||||
RESIZE_THRESHOLD = 2048
|
||||
ALLOWED_MIMES = {
|
||||
'image/jpeg': '.jpg',
|
||||
'image/png': '.png',
|
||||
'image/gif': '.gif',
|
||||
'image/webp': '.webp'
|
||||
}
|
||||
|
||||
def validate_file_size(self, file_obj):
|
||||
"""Check file size before processing."""
|
||||
file_obj.seek(0, os.SEEK_END)
|
||||
size = file_obj.tell()
|
||||
file_obj.seek(0)
|
||||
|
||||
if size > self.MAX_FILE_SIZE:
|
||||
raise ValueError(f"File too large: {size} bytes (max {self.MAX_FILE_SIZE})")
|
||||
|
||||
return size
|
||||
|
||||
def optimize_image(self, file_obj):
|
||||
"""Optimize image for web display."""
|
||||
# Open and validate
|
||||
try:
|
||||
img = Image.open(file_obj)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid or corrupted image: {e}")
|
||||
|
||||
# Correct EXIF orientation
|
||||
img = ImageOps.exif_transpose(img)
|
||||
|
||||
# Check dimensions
|
||||
width, height = img.size
|
||||
if max(width, height) > self.MAX_DIMENSIONS:
|
||||
raise ValueError(f"Image too large: {width}x{height} (max {self.MAX_DIMENSIONS})")
|
||||
|
||||
# Resize if needed
|
||||
if max(width, height) > self.RESIZE_THRESHOLD:
|
||||
img.thumbnail((self.RESIZE_THRESHOLD, self.RESIZE_THRESHOLD),
|
||||
Image.Resampling.LANCZOS)
|
||||
|
||||
return img
|
||||
|
||||
def generate_filename(self, original_name, content):
|
||||
"""Generate unique filename with date path."""
|
||||
# Create hash for uniqueness
|
||||
hash_obj = hashlib.sha256(content)
|
||||
hash_hex = hash_obj.hexdigest()[:8]
|
||||
|
||||
# Get extension
|
||||
_, ext = os.path.splitext(original_name)
|
||||
|
||||
# Generate date-based path
|
||||
now = datetime.now()
|
||||
year = now.strftime('%Y')
|
||||
month = now.strftime('%m')
|
||||
|
||||
# Create filename
|
||||
filename = f"{now.strftime('%Y%m%d')}-{hash_hex}{ext}"
|
||||
|
||||
return f"{year}/{month}/{filename}"
|
||||
```
|
||||
|
||||
### 3. Database Migration
|
||||
Create migration for media tables:
|
||||
|
||||
```sql
|
||||
-- Create media table
|
||||
CREATE TABLE IF NOT EXISTS media (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
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,
|
||||
height INTEGER,
|
||||
uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create note_media junction table with caption support
|
||||
CREATE TABLE IF NOT EXISTS note_media (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
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)
|
||||
);
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX idx_note_media_note ON note_media(note_id);
|
||||
CREATE INDEX idx_note_media_order ON note_media(note_id, display_order);
|
||||
```
|
||||
|
||||
### 4. Upload Endpoint
|
||||
In `starpunk/routes/admin.py`:
|
||||
|
||||
```python
|
||||
@bp.route('/admin/upload', methods=['POST'])
|
||||
@require_auth
|
||||
def upload_media():
|
||||
"""Handle AJAX media upload."""
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'error': 'No file provided'}), 400
|
||||
|
||||
file = request.files['file']
|
||||
|
||||
try:
|
||||
# Process with MediaProcessor
|
||||
processor = MediaProcessor()
|
||||
|
||||
# Validate size first (before loading image)
|
||||
size = processor.validate_file_size(file.file)
|
||||
|
||||
# Optimize image
|
||||
optimized = processor.optimize_image(file.file)
|
||||
|
||||
# Generate path
|
||||
path = processor.generate_filename(file.filename, file.read())
|
||||
|
||||
# Save to disk
|
||||
save_path = Path(app.config['MEDIA_PATH']) / path
|
||||
save_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
optimized.save(save_path, quality=95, optimize=True)
|
||||
|
||||
# Save to database
|
||||
media_id = save_media_metadata(
|
||||
filename=path.name,
|
||||
original_name=file.filename,
|
||||
path=path,
|
||||
mime_type=file.content_type,
|
||||
size=save_path.stat().st_size,
|
||||
width=optimized.width,
|
||||
height=optimized.height
|
||||
)
|
||||
|
||||
# Return success
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'media_id': media_id,
|
||||
'url': f'/media/{path}'
|
||||
})
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
app.logger.error(f"Upload failed: {e}")
|
||||
return jsonify({'error': 'Upload failed'}), 500
|
||||
```
|
||||
|
||||
### 5. Template Updates
|
||||
Update note creation/edit forms to include:
|
||||
- Multiple file input with accept attribute
|
||||
- Caption fields for each uploaded image
|
||||
- Client-side preview with caption inputs
|
||||
- Remove button for each image
|
||||
- Hidden fields to track attached media IDs
|
||||
|
||||
### 6. Display Implementation
|
||||
When rendering notes:
|
||||
1. Query `note_media` JOIN `media` ordered by `display_order`
|
||||
2. Display images at top of note
|
||||
3. Use captions as alt text
|
||||
4. Apply responsive grid layout CSS
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Unit Tests
|
||||
- [ ] File size validation (reject >10MB)
|
||||
- [ ] Dimension validation (reject >4096px)
|
||||
- [ ] MIME type validation (accept only JPEG/PNG/GIF/WebP)
|
||||
- [ ] Image resize logic (>2048px gets resized)
|
||||
- [ ] Filename generation (unique, date-based)
|
||||
- [ ] EXIF orientation correction
|
||||
|
||||
### Integration Tests
|
||||
- [ ] Upload single image
|
||||
- [ ] Upload multiple images (up to 4)
|
||||
- [ ] Reject 5th image
|
||||
- [ ] Upload with captions
|
||||
- [ ] Delete uploaded image
|
||||
- [ ] Edit note with existing media
|
||||
- [ ] Corrupted file handling
|
||||
- [ ] Oversized file handling
|
||||
|
||||
### Manual Testing
|
||||
- [ ] Upload from phone camera
|
||||
- [ ] Upload screenshots
|
||||
- [ ] Test all supported formats
|
||||
- [ ] Verify captions appear as alt text
|
||||
- [ ] Check responsive layouts (1-4 images)
|
||||
- [ ] Verify images in RSS/ATOM/JSON feeds
|
||||
|
||||
## Error Messages
|
||||
Provide clear, actionable error messages:
|
||||
|
||||
- "File too large. Maximum size is 10MB"
|
||||
- "Image dimensions too large. Maximum is 4096x4096 pixels"
|
||||
- "Invalid image format. Accepted: JPEG, PNG, GIF, WebP"
|
||||
- "Maximum 4 images per note"
|
||||
- "Image appears to be corrupted"
|
||||
|
||||
## Performance Considerations
|
||||
- Process images synchronously (single-user CMS)
|
||||
- Use quality=95 for good balance of size/quality
|
||||
- Consider lazy loading for feed pages
|
||||
- Cache resized images (future enhancement)
|
||||
|
||||
## Security Notes
|
||||
- Always validate MIME type server-side
|
||||
- Use Pillow to verify file integrity
|
||||
- Sanitize filenames before saving
|
||||
- Prevent directory traversal in media paths
|
||||
- Strip EXIF data that might contain GPS/personal info
|
||||
|
||||
## Future Enhancements (NOT in v1.2.0)
|
||||
- Micropub media endpoint support
|
||||
- Video upload support
|
||||
- Separate thumbnail generation
|
||||
- CDN integration
|
||||
- Bulk upload interface
|
||||
- Image editing tools (crop, rotate)
|
||||
@@ -1,143 +0,0 @@
|
||||
# V1.2.0 Media Upload - Final Design Summary
|
||||
|
||||
## Design Status: COMPLETE ✓
|
||||
|
||||
This document summarizes the finalized design for v1.2.0 media upload feature based on user requirements and architectural decisions.
|
||||
|
||||
## User Requirements (Confirmed)
|
||||
1. **Image limit**: 4 images per note
|
||||
2. **Reordering**: Not needed (display order = upload order)
|
||||
3. **Image optimization**: Yes, automatic resize for large images
|
||||
4. **Captions**: Yes, optional caption field for each image
|
||||
|
||||
## Architectural Decisions
|
||||
|
||||
### ADR-057: Media Attachment Model
|
||||
- Social media style attachments (not inline markdown)
|
||||
- Media displays at TOP of notes
|
||||
- Text content appears BELOW media
|
||||
- Junction table for flexible associations
|
||||
|
||||
### ADR-058: Image Optimization Strategy
|
||||
- **Max file size**: 10MB per image
|
||||
- **Max dimensions**: 4096x4096 pixels
|
||||
- **Auto-resize**: Images >2048px resized automatically
|
||||
- **Processing library**: Pillow
|
||||
- **Formats**: JPEG, PNG, GIF, WebP only
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Image Processing
|
||||
- **Validation**: Size, dimensions, format, integrity
|
||||
- **Optimization**: Resize to 2048px max, EXIF correction
|
||||
- **Quality**: 95% JPEG quality (high quality)
|
||||
- **Storage**: data/media/YYYY/MM/ structure
|
||||
|
||||
### Database Schema
|
||||
```sql
|
||||
-- Media table with dimensions
|
||||
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,
|
||||
height INTEGER,
|
||||
uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Junction table with captions
|
||||
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, -- 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)
|
||||
);
|
||||
```
|
||||
|
||||
### User Interface
|
||||
- Multiple file input (accept images only)
|
||||
- Caption field for each uploaded image
|
||||
- Preview thumbnails during upload
|
||||
- Remove button per image
|
||||
- No drag-and-drop reordering
|
||||
- Maximum 4 images enforced
|
||||
|
||||
### Display Layout
|
||||
- 1 image: Full width
|
||||
- 2 images: Side by side (50% each)
|
||||
- 3 images: Grid layout
|
||||
- 4 images: 2x2 grid
|
||||
|
||||
### Syndication Support
|
||||
- **RSS**: HTML with images in description
|
||||
- **ATOM**: Both enclosures and HTML content
|
||||
- **JSON Feed**: Native attachments array
|
||||
- **Microformats2**: Multiple u-photo properties
|
||||
|
||||
## Implementation Guidance
|
||||
|
||||
### Dependencies
|
||||
- **Pillow**: For image processing and optimization
|
||||
|
||||
### Processing Pipeline
|
||||
1. Check file size (<10MB)
|
||||
2. Validate MIME type
|
||||
3. Load with Pillow (validates integrity)
|
||||
4. Check dimensions (<4096px)
|
||||
5. Correct EXIF orientation
|
||||
6. Resize if needed (>2048px)
|
||||
7. Save optimized version
|
||||
8. Store metadata in database
|
||||
|
||||
### Error Handling
|
||||
Clear user-facing messages for:
|
||||
- File too large
|
||||
- Invalid format
|
||||
- Dimensions too large
|
||||
- Corrupted file
|
||||
- Maximum images reached
|
||||
|
||||
## Acceptance Criteria
|
||||
- ✓ 4 image maximum per note
|
||||
- ✓ No reordering interface
|
||||
- ✓ Automatic optimization for large images
|
||||
- ✓ Caption support for accessibility
|
||||
- ✓ JPEG, PNG, GIF, WebP support
|
||||
- ✓ 10MB file size limit
|
||||
- ✓ 4096x4096 dimension limit
|
||||
- ✓ Auto-resize at 2048px
|
||||
- ✓ EXIF orientation correction
|
||||
- ✓ Display order = upload order
|
||||
|
||||
## Related Documents
|
||||
- `/docs/decisions/ADR-057-media-attachment-model.md`
|
||||
- `/docs/decisions/ADR-058-image-optimization-strategy.md`
|
||||
- `/docs/design/v1.2.0/feature-specification.md`
|
||||
- `/docs/design/v1.2.0/media-implementation-guide.md`
|
||||
|
||||
## Design Sign-off
|
||||
The v1.2.0 media upload feature design is now complete and ready for implementation. All user requirements have been addressed, technical decisions documented, and implementation guidance provided.
|
||||
|
||||
### Key Highlights
|
||||
- **Simple and elegant**: Automatic optimization, no complex UI
|
||||
- **Accessible**: Caption support for all images
|
||||
- **Standards-compliant**: Full syndication feed support
|
||||
- **Performant**: Optimized images, reasonable limits
|
||||
- **Secure**: Multiple validation layers, Pillow verification
|
||||
|
||||
## Next Steps
|
||||
1. Implement database migrations
|
||||
2. Create MediaProcessor class with Pillow
|
||||
3. Add upload endpoint to admin routes
|
||||
4. Update note creation/edit forms
|
||||
5. Implement media display in templates
|
||||
6. Update feed generators for media
|
||||
7. Write comprehensive tests
|
||||
Reference in New Issue
Block a user