Changeset fd8faf5 in Klonkt for src/routes/search.js
- Timestamp:
- 09/30/2026 07:27:05 AM (2 hours ago)
- Branches:
- main
- Children:
- 19e28ce
- Parents:
- 86cfa1a
- File:
-
- 1 edited
-
src/routes/search.js (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/search.js
r86cfa1a rfd8faf5 21 21 import express from 'express'; 22 22 import db from '../config/database.js'; 23 import { renderPage } from '../middleware/render.js';23 import { renderPage, formatDate } from '../middleware/render.js'; 24 24 import { audioUrl } from '../services/AudioStreamService.js'; 25 25 import { getSetting } from '../services/SettingsService.js'; … … 86 86 87 87 /** 88 * Is dit een HANDLE (@naam@server) en geen zoekterm? Dan de genormaliseerde 89 * handle, anders null. 90 * 91 * Met of zonder @ vooraan, zoals Mastodon het ook aanneemt. De server moet een 92 * echte domeinnaam zijn (met een punt en een extensie): "@robin" is een naam om 93 * naar te zoeken, geen adres. Een spatie maakt er een zoekopdracht van. 94 * 95 * WebFinger zelf is openbaar en gaat niet ondertekend; het profiel erachter 96 * wel, via resolveRemoteActor, en alleen namens een ingelogde beheerder. 97 */ 98 export function lookupHandle(q) { 99 const m = /^@?([a-z0-9_.-]+)@([a-z0-9-]+(?:\.[a-z0-9-]+)*\.[a-z]{2,})$/i.exec(String(q || '').trim()); 100 return m ? `@${m[1]}@${m[2].toLowerCase()}` : null; 101 } 102 103 /** 88 104 * Mag deze bezoeker een adres laten OPHALEN? 89 105 * … … 114 130 * een anonieme GET, en dan lijkt een bestaande post te ontbreken. 115 131 */ 116 async function lookUpRemote(remoteUri, site ) {132 async function lookUpRemote(remoteUri, site, { handle = false } = {}) { 117 133 let remote = null; 118 134 let remoteKind = null; 119 try { remote = await ActivityPubService.resolveRemoteNote(remoteUri, { asSlug: site.slug }); } catch { /* onbereikbaar */ } 135 // Een handle wijst altijd naar een persoon: geen post-poging, die zou alleen 136 // een verzoek kosten dat niets kan opleveren. 137 if (!handle) { 138 try { remote = await ActivityPubService.resolveRemoteNote(remoteUri, { asSlug: site.slug }); } catch { /* onbereikbaar */ } 139 } 120 140 if (remote) remoteKind = 'note'; 121 141 if (!remote) { … … 226 246 } 227 247 228 // ── Full results page ──────────────────────────────────────────────────────── 229 router.get('/', async (req, res) => { 230 const site = res.locals.site; 231 if (!site) return res.status(404).send('No site'); 232 const rawQ = (req.query.q || '').toString().trim(); 233 234 if (!rawQ) { 235 return renderPage(req, res, 'pages/search', { 236 pageTitle: 'Zoeken', bodyClass: 'on-special', query: '', 237 results: [], tracks: [], events: [], pages: [], total: 0, 238 }); 239 } 240 241 // Een adres wordt OPGEHAALD, niet doorzocht (shaer-utpi). Eerst als post, 242 // dan als profiel: een URL die geen van beide oplevert is gewoon een 243 // zoekterm die toevallig op een link lijkt, en dan blijft de rest staan. 244 const remoteUri = lookupUri(rawQ); 248 // ── Wat er bij een zoekopdracht hoort ──────────────────────────────────────── 249 // 250 // EEN verzameling voor beide ingangen: de pagina /search?q=... en het live vak 251 // onder de zoekbalk (/search/results). Ze tonen sinds Robins ontwerp van 30-9 252 // hetzelfde, dus ze horen ook hetzelfde op te halen. 253 // 254 // Een adres wordt OPGEHAALD, niet doorzocht (shaer-utpi). Eerst als post, dan 255 // als profiel; ondertekend, en alleen namens een ingelogde beheerder. 256 async function gather(req, res, rawQ) { 257 const site = res.locals.site; 258 // Een adres (https://...) of een handle (@naam@server): allebei iets dat van 259 // een andere server moet komen, dus allebei achter dezelfde rechtengrens. 260 const uri = lookupUri(rawQ); 261 const handle = uri ? null : lookupHandle(rawQ); 262 const remoteUri = uri || handle; 245 263 const mayLookup = remoteUri ? mayLookUp(req, res) : false; 246 264 const { remote, remoteKind, reacted: remoteReacted } = (remoteUri && mayLookup) 247 ? await lookUpRemote(remoteUri, site )265 ? await lookUpRemote(remoteUri, site, { handle: !!handle }) 248 266 : { remote: null, remoteKind: null, reacted: { liked: false, boosted: false } }; 249 250 267 const r = searchSite(req, res, rawQ, { posts: 50, tracks: 25, events: 25, pages: 8 }); 251 const total = r.results.length + r.tracks.length + r.events.length + r.pages.length; 268 return { 269 query: rawQ, 270 results: r.results, tracks: r.tracks, events: r.events, pages: r.pages, 271 total: r.results.length + r.tracks.length + r.events.length + r.pages.length, 272 queryError: r.queryError, 273 remoteUri, remote, remoteKind, mayLookup, remoteReacted, 274 siteTitle: site.title || '', 275 }; 276 } 277 278 // ── De pagina: het zoekvlak, open, met de resultaten erin ──────────────────── 279 router.get('/', async (req, res) => { 280 const site = res.locals.site; 281 if (!site) return res.status(404).send('No site'); 282 const rawQ = (req.query.q || '').toString().trim().slice(0, 2048); 283 const data = rawQ ? await gather(req, res, rawQ) : { query: '' }; 252 284 renderPage(req, res, 'pages/search', { 253 pageTitle: `Zoeken: ${rawQ}`, bodyClass: 'on-special', query: rawQ, 254 results: r.results, tracks: r.tracks, events: r.events, pages: r.pages, 255 total, queryError: r.queryError, 256 remoteUri, remote, remoteKind, mayLookup, 257 remoteReacted, 258 siteTitle: site.title || '', 285 pageTitle: rawQ ? `Zoeken: ${rawQ}` : 'Zoeken', bodyClass: 'on-special', 286 ...data, 259 287 }); 260 288 }); 261 289 262 // ── De live-preview van een adres (HTML-fragment) ─────────────────────────────290 // ── Het live vak onder de zoekbalk (HTML-fragment) ─────────────────────────── 263 291 // 264 // Plak je een link in de zoekbalk, dan haalt de uitklaplijst de post zelf op 265 // in plaats van te zoeken naar de tekst van de link (shaer-utpi). 292 // Dezelfde render als de pagina (partials/search-results.ejs), zodat het vak 293 // en de pagina er per constructie hetzelfde uitzien. Dit verving twee routes: 294 // /suggest (JSON, dat de browser zelf tot een lijstje bouwde, anders dan de 295 // pagina) en /remote (alleen het adres). 266 296 // 267 // Dezelfde grens als de volle pagina, en hier weegt hij zwaarder: deze route 268 // wordt bij elke toetsaanslag geraakt, dus zonder die grens is hij een 269 // haalservice die iedereen deze server op adressen naar keuze laat afsturen. 270 // Mag je niet ophalen, of is het geen adres, dan 204: de zoekbalk valt dan 271 // terug op de gewone suggesties. Geen 403, want voor een bezoeker is een 272 // geplakte link gewoon een zoekterm en geen geweigerde handeling. 297 // De rechtengrens voor het ophalen zit in gather en is dezelfde als voor de 298 // pagina; hier weegt hij zwaarder, want dit wordt bij elke toetsaanslag 299 // geraakt. Een bezoeker die een link plakt krijgt de gewone resultaten en de 300 // zin waarom er verder niets staat, en er gaat geen verzoek uit. 273 301 const VIEWS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'views'); 274 302 275 router.get('/re mote', async (req, res) => {276 const site = res.locals.site; 277 const r emoteUri = lookupUri(String(req.query.q || '').slice(0, 2048));278 if (!site || !remoteUri || !mayLookUp(req, res)) return res.status(204).end();279 const { remote, remoteKind, reacted } = await lookUpRemote(remoteUri, site);280 // Dezelfde taal als de pagina eromheen, anders staat er een Engelse kaart303 router.get('/results', async (req, res) => { 304 const site = res.locals.site; 305 const rawQ = String(req.query.q || '').trim().slice(0, 2048); 306 if (!site || rawQ.length < 2) return res.status(204).end(); 307 const data = await gather(req, res, rawQ); 308 // Dezelfde taal als de pagina eromheen, anders staat er een Engelse lijst 281 309 // in een Nederlandse zoekbalk. 282 310 const lang = resolveLang(req, { userLang: req.session?.user?.lang, defaultLang: getSetting('default_lang') }); 283 const html = await ejs.renderFile(path.join(VIEWS_DIR, 'partials', 'remote-preview.ejs'), { 311 const html = await ejs.renderFile(path.join(VIEWS_DIR, 'partials', 'search-results.ejs'), { 312 ...data, 284 313 t: (k, vars) => i18nT(lang, k, vars), 285 remote, remoteKind, remoteUri, reacted,286 site Title: site.title || '',314 formatDate, 315 siteUrlBase: res.locals.siteUrlBase || '', 287 316 }, { async: false }); 288 317 // Wat deze bezoeker ziet hangt van zijn rechten en zijn reacties af: niet … … 292 321 }); 293 322 294 // ── Live suggestions (JSON) ──────────────────────────────────────────────────295 router.get('/suggest', (req, res) => {296 const site = res.locals.site;297 if (!site) return res.json({ posts: [], tracks: [], events: [], pages: [] });298 const rawQ = (req.query.q || '').toString().trim().slice(0, 100);299 if (rawQ.length < 2) return res.json({ posts: [], tracks: [], events: [], pages: [] });300 301 const urlFor = (slug) => `/${slug}`;302 const r = searchSite(req, res, rawQ, { posts: 5, tracks: 4, events: 3, pages: 4 });303 res.json({304 posts: r.results.map((p) => ({ title: p.title || '(zonder titel)', url: urlFor(p.slug) })),305 tracks: r.tracks.map((tr) => ({ title: tr.title, artist: tr.artist, url: tr.postUrl })),306 events: r.events.map((e) => ({ when: [e.date, e.time].filter(Boolean).join(' '), where: e.where, url: e.url })),307 pages: r.pages,308 });309 });310 311 323 export default router;
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)