feat(workspace): link previews above bullets with a web address (M5)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m52s

- GET /link-preview?url= fetches the page on the server (https only,
  SSRF-guarded like webhooks), reads Open Graph/Twitter tags, title,
  description and icon from the head, loads picture and icon (no SVG) and
  keeps them in link_previews (migration 6) for 7 days; readers' browsers
  never contact the page. Off in the demo; 300 fetches per address in
  15 minutes.
- Compact card above the bullet (icon, site, title, one line, small
  picture); the duplicate link badge is left out.
- ✕ turns the address into [title](url), so hiding syncs with the note;
  "Link-Vorschauen anzeigen" under appearance switches all off per device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-30 00:06:05 +01:00
1 parent 83408d120d
commit 4c26eb2960
16 files changed
+688 -6

No files matched your search

+9 -4
View File
@@ -27,19 +27,24 @@ export function mediaContent(kind: 'image' | 'audio', id: string, caption = ''):
const loaded = new Map<string, Promise<{ blob: Blob; url: string }>>();
function loadMedia(id: string): Promise<{ blob: Blob; url: string }> {
let p = loaded.get(id);
return loadAuthorized(`/api/v1/media/${encodeURIComponent(id)}`);
}
/** Any picture of the API (needs the login, so no plain <img src>), as Blob plus blob: URL. */
export function loadAuthorized(path: string): Promise<{ blob: Blob; url: string }> {
let p = loaded.get(path);
if (!p) {
p = (async () => {
const token = getToken();
const res = await fetch(`/api/v1/media/${encodeURIComponent(id)}`, {
const res = await fetch(path, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (!res.ok) throw new Error(res.status === 404 ? 'Datei nicht gefunden' : `HTTP ${res.status}`);
const blob = await res.blob();
return { blob, url: URL.createObjectURL(blob) };
})();
p.catch(() => loaded.delete(id)); // try again next time (e.g. was offline)
loaded.set(id, p);
p.catch(() => loaded.delete(path)); // try again next time (e.g. was offline)
loaded.set(path, p);
}
return p;
}