Changeset d18c60e in Klonkt for src/routes


Ignore:
Timestamp:
07/01/2026 06:55:46 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
0688b5f
Parents:
43273c9
Message:

feat(fediverse): alt text for images (accessibility → AS2 attachment name)

Media federated to the fediverse now carries a description (AS2 name on the
attachment), which Mastodon shows and screen readers read. The cover gets an alt
field in the editor; inline images keep their own <img alt="…">. Also used as the
cover's on-site alt.

  • src/config/database.js — posts.cover_alt column.
  • src/services/ActivityPubService.js — buildNote carries alt per media URL (cover_alt + inline <img alt>) and emits it as the attachment name (and on the image fallback).
  • src/routes/posts.js — capture/store cover_alt on create/save and pass it to the federation hooks.
  • src/services/Scheduler.js — carry cover_alt when a scheduled post goes live.
  • src/views/pages/post-edit.ejs — "Alt text (description)" field under the cover URL.
  • src/views/pages/post.ejs — the on-page cover <img> uses cover_alt.
  • src/services/i18n.js — pedit.f_cover_alt + pedit.cover_alt_placeholder (nl/en/de).
  • test/media-alt.test.js — cover alt, inline alt, and no-alt = no name.
  • CHANGELOG(.nl/.de).md — "Alt text for images" under Unreleased.

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/posts.js

    r43273c9 rd18c60e  
    246246  const nsfw = req.body.nsfw ? 1 : 0;
    247247  const cw = (req.body.content_warning || '').trim().slice(0, 200);
     248  const coverAlt = (req.body.cover_alt || '').trim().slice(0, 1500) || null; // cover alt text (a11y)
    248249
    249250  // Content arrives as user-authored HTML from the WYSIWYG editor — sanitize
     
    284285    INSERT INTO posts (
    285286      id, site_id, slug, author_id, title, content, excerpt,
    286       status, cover_image_url, cover_video_url, pinned, tags, type, noindex, fan_only, nsfw, content_warning, poll_json, publish_at,
     287      status, cover_image_url, cover_video_url, cover_alt, pinned, tags, type, noindex, fan_only, nsfw, content_warning, poll_json, publish_at,
    287288      created_at, updated_at, published_at
    288     ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
     289    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    289290  `).run(
    290291    postId, site.id, finalSlug, req.session.user.id,
    291292    title || finalSlug, cleanContent, excerpt || '',
    292     finalStatus, cover_image_url || null, (req.body.cover_video_url || null), parsePinnedRank(pinned),
     293    finalStatus, cover_image_url || null, (req.body.cover_video_url || null), coverAlt, parsePinnedRank(pinned),
    293294    JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
    294295    finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, pollJson, publishAt,
     
    312313      ActivityPubService.deliverCreate(site, {
    313314        id: postId, slug: finalSlug, title: title || finalSlug,
    314         content: cleanContent, cover_image_url: cover_image_url || null, cover_video_url: req.body.cover_video_url || null,
     315        content: cleanContent, cover_image_url: cover_image_url || null, cover_video_url: req.body.cover_video_url || null, cover_alt: coverAlt,
    315316        published_at: publishedAt, created_at: now, fan_only: fanOnly, nsfw, content_warning: cw, poll_json: pollJson,
    316317      }).catch(() => { /* best-effort */ });
     
    379380  const nsfw = req.body.nsfw ? 1 : 0;
    380381  const cw = (req.body.content_warning || '').trim().slice(0, 200);
     382  const coverAlt = (req.body.cover_alt || '').trim().slice(0, 1500) || null; // cover alt text (a11y)
    381383  const newSlug = req.body.slug;
    382384  const action = req.body.action || 'save';
     
    422424    UPDATE posts SET
    423425      title = ?, content = ?, excerpt = ?, status = ?,
    424       cover_image_url = ?, cover_video_url = ?, pinned = ?, tags = ?,
     426      cover_image_url = ?, cover_video_url = ?, cover_alt = ?, pinned = ?, tags = ?,
    425427      type = ?, noindex = ?, fan_only = ?, nsfw = ?, content_warning = ?, poll_json = ?, publish_at = ?,
    426428      slug = ?, published_at = ?, updated_at = ?
     
    428430  `).run(
    429431    title, cleanContent, excerpt, finalStatus,
    430     cover_image_url || null, (req.body.cover_video_url || null), parsePinnedRank(pinned),
     432    cover_image_url || null, (req.body.cover_video_url || null), coverAlt, parsePinnedRank(pinned),
    431433    JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
    432434    finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, pollJson, publishAt,
     
    454456    const apPost = {
    455457      id: post.id, slug: finalSlug, title: title || finalSlug,
    456       content: cleanContent, cover_image_url: cover_image_url || null, cover_video_url: req.body.cover_video_url || null,
     458      content: cleanContent, cover_image_url: cover_image_url || null, cover_video_url: req.body.cover_video_url || null, cover_alt: coverAlt,
    457459      published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, nsfw, content_warning: cw, poll_json: pollJson,
    458460    };
Note: See TracChangeset for help on using the changeset viewer.