Add alt text to photo uploads
CI / Lint (pull_request) Successful in 53s
CI / Test (pull_request) Successful in 1m6s
Release Drafter / Update release notes draft (pull_request) Successful in 5s

- Form field for optionally adding a description
- Publish as `imeta` `alt`
- Render as `<img>` alt when set

Fixes a placeholder being published instead. Also strips the current
placeholder if set as alt text (for existing uploads).
This commit is contained in:
2026-08-07 07:25:53 -06:00
parent ac9870240f
commit 9807a2b822
7 changed files with 140 additions and 28 deletions
+4 -4
View File
@@ -237,7 +237,7 @@ export default class PhotoCarousel extends Component {
data-src={{photo.url}}
class="place-header-photo
{{if photo.isLandscape 'landscape' 'portrait'}}"
alt={{@name}}
alt={{photo.alt}}
{{fadeInImage photo.url}}
/>
{{else if this.isGalleryThumbnails}}
@@ -245,7 +245,7 @@ export default class PhotoCarousel extends Component {
data-src={{if photo.thumbUrl photo.thumbUrl photo.url}}
class="place-header-photo
{{if photo.isLandscape 'landscape' 'portrait'}}"
alt={{@name}}
alt={{photo.alt}}
{{fadeInImage (if photo.thumbUrl photo.thumbUrl photo.url)}}
/>
{{else}}
@@ -260,7 +260,7 @@ export default class PhotoCarousel extends Component {
<img
data-src={{photo.url}}
class="place-header-photo landscape"
alt={{@name}}
alt={{photo.alt}}
{{fadeInImage photo.url}}
/>
</picture>
@@ -269,7 +269,7 @@ export default class PhotoCarousel extends Component {
<img
data-src={{if photo.thumbUrl photo.thumbUrl photo.url}}
class="place-header-photo portrait"
alt={{@name}}
alt={{photo.alt}}
{{fadeInImage (if photo.thumbUrl photo.thumbUrl photo.url)}}
/>
{{/if}}
+30 -8
View File
@@ -27,6 +27,7 @@ export default class PlacePhotoUpload extends Component {
@tracked isPublishing = false;
@tracked isDragging = false;
@tracked selectedTags = [];
@tracked altText = '';
get place() {
return this.args.place || {};
@@ -103,6 +104,7 @@ export default class PlacePhotoUpload extends Component {
this.file = null;
this.uploadedPhoto = null;
this.selectedTags = [];
this.altText = '';
if (this.args.onUploadStateChange) {
this.args.onUploadStateChange(false);
}
@@ -118,6 +120,11 @@ export default class PlacePhotoUpload extends Component {
this.selectedTags = [tag];
}
@action
updateAltText(event) {
this.altText = event.target.value;
}
deletePhotoTask = task(async (photoData) => {
try {
if (photoData.hash) {
@@ -177,7 +184,10 @@ export default class PlacePhotoUpload extends Component {
imeta.push(`dim ${photo.dim}`);
}
imeta.push('alt A photo of a place');
const alt = this.altText.trim();
if (alt) {
imeta.push(`alt ${alt}`);
}
if (photo.fallbackUrls && photo.fallbackUrls.length > 0) {
for (const fallbackUrl of photo.fallbackUrls) {
@@ -209,6 +219,7 @@ export default class PlacePhotoUpload extends Component {
// Clear out the file so user can upload more or be done
this.file = null;
this.uploadedPhoto = null;
this.altText = '';
if (this.args.onUploadStateChange) {
this.args.onUploadStateChange(false);
@@ -235,13 +246,11 @@ export default class PlacePhotoUpload extends Component {
{{/if}}
{{#if this.file}}
<div class="photo-grid">
<PlacePhotoUploadItem
@file={{this.file}}
@onSuccess={{this.handleUploadSuccess}}
@onRemove={{this.removeFile}}
/>
</div>
<PlacePhotoUploadItem
@file={{this.file}}
@onSuccess={{this.handleUploadSuccess}}
@onRemove={{this.removeFile}}
/>
{{#if this.suggestedTags.length}}
<div class="photo-tag-suggestions">
@@ -263,6 +272,19 @@ export default class PlacePhotoUpload extends Component {
</div>
{{/if}}
<div class="form-group">
<label for="photo-alt-input">Description (optional):</label>
<input
id="photo-alt-input"
type="text"
class="form-control"
placeholder="Describe this photo"
value={{this.altText}}
disabled={{this.isPublishing}}
{{on "input" this.updateAltText}}
/>
</div>
<button
type="button"
class="btn btn-primary btn-publish"
+6 -8
View File
@@ -245,13 +245,6 @@ body {
display: none;
}
.photo-grid {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
}
.photo-upload-item {
position: relative;
aspect-ratio: 4 / 3;
@@ -259,6 +252,7 @@ body {
overflow: hidden;
background: #1e262e;
width: 100%;
margin-bottom: 1.25rem;
}
.photo-upload-item img {
@@ -1866,8 +1860,12 @@ button.create-place {
font-size: 1.2rem;
}
.place-photo-upload .form-group {
margin-bottom: 1.5rem;
}
.photo-tag-suggestions {
margin: 1rem 0 1.5rem;
margin-bottom: 1.25rem;
}
.photo-tag-suggestions-title {
+6
View File
@@ -67,6 +67,7 @@ export function parsePlacePhotos(events) {
let blurhash = null;
let isLandscape = false;
let aspectRatio = 16 / 9; // default
let altText = null;
let placeIdentifier = event.tags.find((t) => t[0] === 'i')?.[1];
for (const tag of imeta.slice(1)) {
@@ -85,6 +86,10 @@ export function parsePlacePhotos(events) {
isLandscape = true;
}
}
} else if (tag.startsWith('alt ')) {
const alt = tag.substring(4).trim();
// Strip the legacy placeholder we used to write on every event
altText = alt === 'A photo of a place' ? null : alt || null;
}
}
@@ -100,6 +105,7 @@ export function parsePlacePhotos(events) {
aspectRatio,
placeIdentifier,
tags: eventTagValues,
alt: altText,
});
}
}