Changeset 221a209 in Klonkt


Ignore:
Timestamp:
06/16/2026 08:52:32 PM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
2248cd2
Parents:
fb02cc0
Message:

Circle: carry over + display tags from the original post

Publisher puts tags as AS Hashtag array in the outbox (href pointing to the
source tag page). Consumer parses + caches them (remote_posts.tags). Feed
cards display them (post-card), and the reader shows tag chips that link to
the /tag page of the source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@…>

Location:
src
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    rfb02cc0 r221a209  
    172172    );
    173173  `);
     174
     175  // Tags van de originele post — getoond in de cirkel (comma-separated string).
     176  ensureColumn('remote_posts', 'tags', 'TEXT');
    174177}
    175178
  • src/routes/circle.js

    rfb02cc0 r221a209  
    3030
    3131  const rows = db.prepare(`
    32     SELECT p.id, p.published, p.title, p.summary, p.media_json,
     32    SELECT p.id, p.published, p.title, p.summary, p.media_json, p.tags,
    3333           a.name AS actor_name
    3434    FROM remote_posts p
     
    5151      created_at: r.published,
    5252      type: 'post',
    53       tags: '',
     53      tags: r.tags || '',
    5454      pinned: 0,
    5555      status: 'published',
     
    8181
    8282  const row = db.prepare(`
    83     SELECT p.id, p.published, p.title, p.summary, p.url, p.media_json,
     83    SELECT p.id, p.published, p.title, p.summary, p.url, p.media_json, p.tags,
    8484           a.name AS actor_name, a.url AS actor_url, a.avatar AS actor_avatar
    8585    FROM remote_posts p
     
    100100    sourceAvatar: safeUrl(row.actor_avatar),
    101101    originalUrl: safeUrl(row.url),
     102    tags: (row.tags || '').split(',').map((t) => t.trim()).filter(Boolean),
     103    sourceTagBase: safeUrl(row.actor_url) ? safeUrl(row.actor_url).replace(/\/+$/, '') : null,
    102104  };
    103105
  • src/services/CircleFederation.js

    rfb02cc0 r221a209  
    8585}
    8686
     87// Tags-kolom (JSON-array of comma-separated) -> nette string-array.
     88function parseTags(raw) {
     89  if (!raw) return [];
     90  if (Array.isArray(raw)) return raw.map((t) => String(t).trim()).filter(Boolean);
     91  try { const j = JSON.parse(raw); if (Array.isArray(j)) return j.map((t) => String(t).trim()).filter(Boolean); } catch { /* geen JSON */ }
     92  return String(raw).split(',').map((t) => t.trim()).filter(Boolean);
     93}
     94
    8795function iso(d) {
    8896  const t = d ? new Date(d) : new Date();
     
    136144
    137145  const rows = db.prepare(`
    138     SELECT slug, title, excerpt, content, cover_image_url, published_at, created_at, type
     146    SELECT slug, title, excerpt, content, cover_image_url, published_at, created_at, type, tags
    139147    FROM posts
    140148    WHERE site_id = ? AND status = 'published'
     
    148156    const published = iso(p.published_at || p.created_at);
    149157    const summary = (p.excerpt || stripHtml(p.content)).slice(0, 500);
     158    const tags = parseTags(p.tags).slice(0, 12);
    150159    return {
    151160      type: 'Create',
     
    161170        published,
    162171        ...(p.cover_image_url ? { image: { type: 'Image', url: abs(base, p.cover_image_url) } } : {}),
     172        // ActivityStreams: tags als Hashtag-objecten (href naar de bron-tagpagina).
     173        ...(tags.length ? { tag: tags.map((t) => ({ type: 'Hashtag', name: '#' + String(t).replace(/^#/, ''), href: `${base}/tag/${encodeURIComponent(t)}` })) } : {}),
    163174      },
    164175    };
  • src/services/CircleService.js

    rfb02cc0 r221a209  
    2929function baseOf(remoteUrl) {
    3030  return String(remoteUrl).replace(/\/+$/, '');
     31}
     32
     33// AS Hashtag-array -> comma-separated tagnamen (zonder #), gesanitized.
     34function extractTags(tag) {
     35  if (!Array.isArray(tag)) return null;
     36  const names = tag
     37    .map((t) => String((t && t.name) || '').replace(/^#/, '').trim())
     38    .filter(Boolean)
     39    .slice(0, 12);
     40  return names.length ? names.join(', ') : null;
    3141}
    3242
     
    8393    `),
    8494    upsertPost: db.prepare(`
    85       INSERT INTO remote_posts (id, actor_id, published, title, summary, url, media_json, raw_json, fetched_at)
    86       VALUES (@id, @actor_id, @published, @title, @summary, @url, @media_json, @raw_json, CURRENT_TIMESTAMP)
     95      INSERT INTO remote_posts (id, actor_id, published, title, summary, url, media_json, tags, raw_json, fetched_at)
     96      VALUES (@id, @actor_id, @published, @title, @summary, @url, @media_json, @tags, @raw_json, CURRENT_TIMESTAMP)
    8797      ON CONFLICT(id) DO UPDATE SET
    8898        published=excluded.published, title=excluded.title, summary=excluded.summary,
    89         url=excluded.url, media_json=excluded.media_json, raw_json=excluded.raw_json, fetched_at=CURRENT_TIMESTAMP
     99        url=excluded.url, media_json=excluded.media_json, tags=excluded.tags,
     100        raw_json=excluded.raw_json, fetched_at=CURRENT_TIMESTAMP
    90101    `),
    91102  };
     
    169180      url: obj.url || obj.id,
    170181      media_json: media.length ? JSON.stringify(media) : null,
     182      tags: extractTags(obj.tag),
    171183      raw_json: JSON.stringify(obj).slice(0, 20000),
    172184    });
  • src/views/pages/circle-post.ejs

    rfb02cc0 r221a209  
    2323  <div class="cirkel-post-body"><%= post.body %></div>
    2424
     25  <% if (post.tags && post.tags.length) { %>
     26    <div class="cirkel-post-tags">
     27      <% post.tags.forEach(function(t){ %>
     28        <% if (post.sourceTagBase) { %>
     29          <a class="cirkel-tag" href="<%= post.sourceTagBase %>/tag/<%= encodeURIComponent(t) %>" target="_blank" rel="noopener">#<%= t %></a>
     30        <% } else { %>
     31          <span class="cirkel-tag">#<%= t %></span>
     32        <% } %>
     33      <% }); %>
     34    </div>
     35  <% } %>
     36
    2537  <% if (post.originalUrl) { %>
    2638    <p class="cirkel-post-readmore">
     
    4254.cirkel-post-cover { width: 100%; border-radius: var(--radius); margin: 0 0 1.5rem; display: block; }
    4355.cirkel-post-body { font-size: 1.05rem; line-height: 1.7; white-space: pre-wrap; color: var(--ink); }
     56.cirkel-post-tags { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: 1.25rem; }
     57.cirkel-tag { font-size: .8rem; color: var(--ink-muted); background: var(--paper-2); border: 1px solid var(--rule); border-radius: 999px; padding: .15rem .6rem; text-decoration: none; }
     58.cirkel-tag:hover { border-color: var(--accent); color: var(--accent); }
    4459.cirkel-post-readmore { margin-top: 2rem; padding-top: 1.25rem; border-top: 1px solid var(--rule); }
    4560.cirkel-post-readmore a { color: var(--accent); text-decoration: none; font-weight: 600; }
Note: See TracChangeset for help on using the changeset viewer.