source: Klonkt/src/routes/circle.js@ 2520dcc

main
Last change on this file since 2520dcc was 221a209, checked in by roboburr <roboburr@…>, 3 months ago

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@…>

  • Property mode set to 100644
File size: 3.8 KB
Line 
1/**
2 * Cirkel-feed + lokale lees-pagina.
3 * GET /cirkel -> overzicht (zelfde timeline/grid-view als de home)
4 * GET /cirkel/:id -> losse remote-post in eigen chrome (blijf op je site)
5 * Alleen actief als tenancy === 'circle' (anders next() -> postsRoutes/404).
6 * Zie docs/cirkels-v1-spec.md §5c.
7 */
8
9import express from 'express';
10import { renderPage } from '../middleware/render.js';
11import db from '../config/database.js';
12import { getTenancy } from '../services/SettingsService.js';
13
14const router = express.Router();
15
16function safeUrl(u) {
17 return typeof u === 'string' && /^https?:\/\//i.test(u) ? u : null;
18}
19function safeJson(s) {
20 try { return s ? JSON.parse(s) : []; } catch { return []; }
21}
22function mediaImage(media_json) {
23 const media = safeJson(media_json).map((m) => ({ ...m, url: safeUrl(m.url) })).filter((m) => m.url);
24 return media.find((m) => m.type === 'image') || null;
25}
26
27// ── Overzicht ────────────────────────────────────────────────
28router.get('/cirkel', (req, res, next) => {
29 if (getTenancy() !== 'circle') return next();
30
31 const rows = db.prepare(`
32 SELECT p.id, p.published, p.title, p.summary, p.media_json, p.tags,
33 a.name AS actor_name
34 FROM remote_posts p
35 JOIN remote_actors a ON a.id = p.actor_id
36 ORDER BY COALESCE(p.published, p.fetched_at) DESC
37 LIMIT 100
38 `).all();
39
40 const posts = rows.map((r) => {
41 const image = mediaImage(r.media_json);
42 return {
43 id: r.id,
44 // Lokale lees-pagina -> de kaart blijft op de eigen site (post-card linkt
45 // lokaal + htmx, GEEN external_url).
46 slug: 'cirkel/' + encodeURIComponent(r.id),
47 title: r.title || '(zonder titel)',
48 excerpt: r.summary || '',
49 cover_image_url: image ? image.url : null,
50 published_at: r.published,
51 created_at: r.published,
52 type: 'post',
53 tags: r.tags || '',
54 pinned: 0,
55 status: 'published',
56 source_name: r.actor_name || 'Onbekend',
57 };
58 });
59
60 // Sites in de cirkel — alleen actieve links (outdated/error vallen weg, net als
61 // hun posts). Voor de grafische header met avatars.
62 const sites = db.prepare(`
63 SELECT a.name, a.url, a.avatar
64 FROM remote_actors a
65 JOIN circle_links l ON l.remote_actor_id = a.id
66 WHERE l.status = 'active'
67 ORDER BY a.name
68 `).all()
69 .map((s) => ({
70 name: s.name || 'Onbekend',
71 url: safeUrl(s.url),
72 avatar: safeUrl(s.avatar),
73 }));
74
75 renderPage(req, res, 'pages/circle-feed', { pageTitle: 'Cirkel', bodyClass: 'on-cirkel', posts, sites });
76});
77
78// ── Losse remote-post (lokaal lezen) ─────────────────────────
79router.get('/cirkel/:id', (req, res, next) => {
80 if (getTenancy() !== 'circle') return next();
81
82 const row = db.prepare(`
83 SELECT p.id, p.published, p.title, p.summary, p.url, p.media_json, p.tags,
84 a.name AS actor_name, a.url AS actor_url, a.avatar AS actor_avatar
85 FROM remote_posts p
86 JOIN remote_actors a ON a.id = p.actor_id
87 WHERE p.id = ?
88 `).get(req.params.id);
89
90 if (!row) return next();
91
92 const image = mediaImage(row.media_json);
93 const post = {
94 title: row.title || '(zonder titel)',
95 body: row.summary || '', // platte tekst (gesanitized bij ingest)
96 published: row.published,
97 image: image ? image.url : null,
98 sourceName: row.actor_name || 'Onbekend',
99 sourceUrl: safeUrl(row.actor_url),
100 sourceAvatar: safeUrl(row.actor_avatar),
101 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,
104 };
105
106 renderPage(req, res, 'pages/circle-post', {
107 pageTitle: post.title,
108 bodyClass: 'on-cirkel-post',
109 post,
110 });
111});
112
113export default router;
Note: See TracBrowser for help on using the repository browser.