## 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>
311 lines
8.4 KiB
Markdown
311 lines
8.4 KiB
Markdown
# 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. |