feat: v1.2.0-rc.2 - Media display fixes and feed enhancements
## Added - Feed Media Enhancement with Media RSS namespace support - RSS enclosure, media:content, media:thumbnail elements - JSON Feed image field for first image - ADR-059: Full feed media standardization roadmap ## Fixed - Media display on homepage (was only showing on note pages) - Responsive image sizing with CSS constraints - Caption display (now alt text only, not visible) - Logging correlation ID crash in non-request contexts ## Documentation - Feed media design documents and implementation reports - Media display fixes design and validation reports - Updated ROADMAP with v1.3.0/v1.4.0 media plans 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
311
docs/design/media-display-fixes.md
Normal file
311
docs/design/media-display-fixes.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# Media Display Fixes - Architectural Design
|
||||
|
||||
## Status
|
||||
Active
|
||||
|
||||
## Problem Statement
|
||||
Three issues with current media display implementation:
|
||||
1. **Images too large** - No CSS constraints on image dimensions
|
||||
2. **Captions visible** - Currently showing figcaption, should use alt text only
|
||||
3. **Images missing on homepage** - Media not fetched or displayed in index.html
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### Issue 1: Images Too Large
|
||||
The current CSS (`/static/css/style.css`) has NO styles for:
|
||||
- `.note-media` container
|
||||
- `.media-item` figure elements
|
||||
- `.u-photo` images
|
||||
- Responsive image constraints
|
||||
|
||||
Images display at their native dimensions, which can break layouts.
|
||||
|
||||
### Issue 2: Captions Visible
|
||||
Template (`note.html` lines 25-27) explicitly renders figcaption:
|
||||
```html
|
||||
{% if item.caption %}
|
||||
<figcaption>{{ item.caption }}</figcaption>
|
||||
{% endif %}
|
||||
```
|
||||
This violates the social media pattern where captions are for accessibility (alt text) only.
|
||||
|
||||
### Issue 3: Missing Homepage Media
|
||||
The index route (`public.py` line 231) doesn't fetch media:
|
||||
```python
|
||||
notes = list_notes(published_only=True, limit=20)
|
||||
```
|
||||
Compare to the note route (lines 263-267) which DOES fetch media.
|
||||
|
||||
## Architectural Solution
|
||||
|
||||
### Design Principles
|
||||
1. **Consistency**: Same media display logic on all pages
|
||||
2. **Responsive**: Images adapt to viewport and container
|
||||
3. **Accessible**: Alt text for screen readers, no visible captions
|
||||
4. **Performance**: Lazy loading for below-fold images
|
||||
5. **Standards**: Proper Microformats2 markup maintained
|
||||
|
||||
### Component Architecture
|
||||
|
||||
#### 1. CSS Media Display System
|
||||
Create responsive, constrained image display with grid layouts:
|
||||
|
||||
```css
|
||||
/* Media container styles */
|
||||
.note-media {
|
||||
margin-bottom: var(--spacing-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Single image - full width */
|
||||
.note-media:has(.media-item:only-child) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.note-media:has(.media-item:only-child) .media-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Two images - side by side */
|
||||
.note-media:has(.media-item:nth-child(2):last-child) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Three or four images - grid */
|
||||
.note-media:has(.media-item:nth-child(3)),
|
||||
.note-media:has(.media-item:nth-child(4)) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Media item wrapper */
|
||||
.media-item {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--color-bg-alt);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
aspect-ratio: 1 / 1; /* Instagram-style square crop */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Image constraints */
|
||||
.media-item img,
|
||||
.u-photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; /* Crop to fill container */
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* For single images, allow natural aspect ratio */
|
||||
.note-media:has(.media-item:only-child) .media-item {
|
||||
aspect-ratio: auto;
|
||||
max-height: 500px; /* Prevent extremely tall images */
|
||||
}
|
||||
|
||||
.note-media:has(.media-item:only-child) .media-item img {
|
||||
object-fit: contain; /* Show full image for singles */
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* Remove figcaption from display */
|
||||
.media-item figcaption {
|
||||
display: none; /* Captions are for alt text only */
|
||||
}
|
||||
|
||||
/* Mobile responsive adjustments */
|
||||
@media (max-width: 767px) {
|
||||
/* Stack images vertically on small screens */
|
||||
.note-media:has(.media-item:nth-child(2):last-child) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.media-item {
|
||||
aspect-ratio: 16 / 9; /* Wider aspect on mobile */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Template Refactoring
|
||||
|
||||
Create a reusable macro for media display to ensure consistency:
|
||||
|
||||
**New template partial: `templates/partials/media.html`**
|
||||
```jinja2
|
||||
{# Reusable media display macro #}
|
||||
{% macro display_media(media_items) %}
|
||||
{% if media_items %}
|
||||
<div class="note-media">
|
||||
{% for item in media_items %}
|
||||
<figure class="media-item">
|
||||
<img src="{{ url_for('public.media_file', path=item.path) }}"
|
||||
alt="{{ item.caption or 'Image' }}"
|
||||
class="u-photo"
|
||||
loading="lazy">
|
||||
{# No figcaption - caption is for alt text only #}
|
||||
</figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
```
|
||||
|
||||
**Updated `note.html`** (lines 16-31):
|
||||
```jinja2
|
||||
{# Import media macro #}
|
||||
{% from "partials/media.html" import display_media %}
|
||||
|
||||
{# Media display at TOP (v1.2.0 Phase 3, per ADR-057) #}
|
||||
{{ display_media(note.media) }}
|
||||
```
|
||||
|
||||
**Updated `index.html`** (after line 26, before e-content):
|
||||
```jinja2
|
||||
{# Import media macro at top of file #}
|
||||
{% from "partials/media.html" import display_media %}
|
||||
|
||||
{# In the note loop, after the title check #}
|
||||
{% if has_explicit_title %}
|
||||
<h3 class="p-name">{{ note.title }}</h3>
|
||||
{% endif %}
|
||||
|
||||
{# Media preview (if available) #}
|
||||
{{ display_media(note.media) }}
|
||||
|
||||
{# e-content: note content (preview) #}
|
||||
<div class="e-content">
|
||||
```
|
||||
|
||||
#### 3. Route Handler Updates
|
||||
|
||||
Update the index route to fetch media for each note:
|
||||
|
||||
**`starpunk/routes/public.py`** (lines 219-233):
|
||||
```python
|
||||
@bp.route("/")
|
||||
def index():
|
||||
"""
|
||||
Homepage displaying recent published notes with media
|
||||
|
||||
Returns:
|
||||
Rendered homepage template with note list including media
|
||||
|
||||
Template: templates/index.html
|
||||
Microformats: h-feed containing h-entry items with u-photo
|
||||
"""
|
||||
from starpunk.media import get_note_media
|
||||
|
||||
# Get recent published notes (limit 20)
|
||||
notes = list_notes(published_only=True, limit=20)
|
||||
|
||||
# Attach media to each note for display
|
||||
for note in notes:
|
||||
media = get_note_media(note.id)
|
||||
# Use object.__setattr__ since Note is frozen dataclass
|
||||
object.__setattr__(note, 'media', media)
|
||||
|
||||
return render_template("index.html", notes=notes)
|
||||
```
|
||||
|
||||
### Implementation Guidelines
|
||||
|
||||
#### Phase 1: CSS Foundation
|
||||
1. Add media display styles to `/static/css/style.css`
|
||||
2. Test with 1, 2, 3, and 4 image layouts
|
||||
3. Verify responsive behavior on mobile/tablet/desktop
|
||||
4. Ensure images don't overflow containers
|
||||
|
||||
#### Phase 2: Template Refactoring
|
||||
1. Create `templates/partials/` directory if not exists
|
||||
2. Create `media.html` with display macro
|
||||
3. Update `note.html` to use macro
|
||||
4. Update `index.html` to import and use macro
|
||||
5. Remove figcaption rendering completely
|
||||
|
||||
#### Phase 3: Route Updates
|
||||
1. Import `get_note_media` in index route
|
||||
2. Fetch media for each note in loop
|
||||
3. Attach media using `object.__setattr__`
|
||||
4. Verify media passes to template
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
#### Visual Tests
|
||||
- [ ] Single image displays at reasonable size
|
||||
- [ ] Two images display side-by-side
|
||||
- [ ] Three images display in 2x2 grid (one empty)
|
||||
- [ ] Four images display in 2x2 grid
|
||||
- [ ] Images maintain aspect ratio appropriately
|
||||
- [ ] No layout overflow on any screen size
|
||||
- [ ] Captions not visible (alt text only)
|
||||
|
||||
#### Functional Tests
|
||||
- [ ] Homepage shows media for notes
|
||||
- [ ] Individual note page shows media
|
||||
- [ ] Media lazy loads below fold
|
||||
- [ ] Alt text present for accessibility
|
||||
- [ ] Microformats2 u-photo preserved
|
||||
|
||||
#### Performance Tests
|
||||
- [ ] Page load time acceptable with media
|
||||
- [ ] Images don't block initial render
|
||||
- [ ] Lazy loading works correctly
|
||||
|
||||
### Security Considerations
|
||||
- Media paths already sanitized in media_file route
|
||||
- Alt text must be HTML-escaped in templates
|
||||
- No user-controlled CSS injection points
|
||||
|
||||
### Accessibility Requirements
|
||||
- Alt text MUST be present (fallback to "Image")
|
||||
- Images must not convey information not in text
|
||||
- Focus indicators for keyboard navigation
|
||||
- Proper semantic HTML (figure elements)
|
||||
|
||||
### Future Enhancements (Not for V1)
|
||||
- Image optimization/resizing on upload
|
||||
- WebP format support with fallbacks
|
||||
- Lightbox for full-size viewing
|
||||
- Video/audio media support
|
||||
- CDN integration for media serving
|
||||
|
||||
## Decision Rationale
|
||||
|
||||
### Why Grid Layout?
|
||||
- Native CSS, no JavaScript required
|
||||
- Excellent responsive support
|
||||
- Handles variable image counts elegantly
|
||||
- Familiar social media pattern
|
||||
|
||||
### Why Hide Captions?
|
||||
- Follows Twitter/Mastodon pattern
|
||||
- Captions are for accessibility (alt text)
|
||||
- Cleaner visual presentation
|
||||
- Text content provides context
|
||||
|
||||
### Why Lazy Loading?
|
||||
- Improves initial page load
|
||||
- Reduces bandwidth for visitors
|
||||
- Native browser support
|
||||
- Progressive enhancement
|
||||
|
||||
### Why Aspect Ratio Control?
|
||||
- Prevents layout shift during load
|
||||
- Creates consistent grid appearance
|
||||
- Matches social media expectations
|
||||
- Improves visual harmony
|
||||
|
||||
## Implementation Priority
|
||||
1. **Critical**: Fix homepage media display (functionality gap)
|
||||
2. **High**: Add CSS constraints (UX/visual issue)
|
||||
3. **Medium**: Hide captions (visual polish)
|
||||
|
||||
All three fixes should be implemented together for consistency.
|
||||
Reference in New Issue
Block a user