Index: CHANGELOG.de.md
===================================================================
--- CHANGELOG.de.md	(revision d11e30842b5f999ca26d0395008660a22503322b)
+++ CHANGELOG.de.md	(revision e00c0e9397ee0864f97459b36a39ff5ad86a9e41)
@@ -35,4 +35,8 @@
   ist die Datei verbreitet — erneutes "Schließen" wäre Scheinsicherheit. Der Editor sperrt die Wahl
   nach dem Öffnen und warnt dich, bevor du sie ankreuzt.
+
+### Behoben
+- **Nackte Webadressen werden im Fediverse zu Links.** Eine einfache URL in einem Beitrag oder einer
+  Antwort föderiert jetzt als klickbarer Link statt als reiner Text.
 
 ## [1.2.0] — 2026-07-01
Index: CHANGELOG.md
===================================================================
--- CHANGELOG.md	(revision d11e30842b5f999ca26d0395008660a22503322b)
+++ CHANGELOG.md	(revision e00c0e9397ee0864f97459b36a39ff5ad86a9e41)
@@ -30,4 +30,8 @@
   has spread, so "closing" it again would be false security — the editor now locks the choice after
   opening and warns you before you tick it.
+
+### Fixed
+- **Plain web addresses become links on the fediverse.** A bare URL typed in a post or reply now
+  federates as a clickable link instead of plain text.
 
 ## [1.2.0] — 2026-07-01
Index: CHANGELOG.nl.md
===================================================================
--- CHANGELOG.nl.md	(revision d11e30842b5f999ca26d0395008660a22503322b)
+++ CHANGELOG.nl.md	(revision e00c0e9397ee0864f97459b36a39ff5ad86a9e41)
@@ -33,4 +33,8 @@
   is het bestand verspreid — weer "sluiten" zou schijnveiligheid zijn. De editor vergrendelt de keuze
   na het openen en waarschuwt je voordat je 'm aanvinkt.
+
+### Opgelost
+- **Kale webadressen worden links op de fediverse.** Een losse URL in een post of reactie federeert
+  nu als klikbare link in plaats van platte tekst.
 
 ## [1.2.0] — 2026-07-01
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision d11e30842b5f999ca26d0395008660a22503322b)
+++ src/services/ActivityPubService.js	(revision e00c0e9397ee0864f97459b36a39ff5ad86a9e41)
@@ -343,4 +343,5 @@
   body = body.replace(/\r?\n/g, '<br>');
   body = linkHashtags(base, body); // link inline #hashtags in the post body too
+  body = linkUrls(body);           // bare URLs → clickable links on the federated copy
   // Append the tags-field hashtags to the content so Mastodon renders them as clickable
   // hashtags (a Hashtag that's only in the `tag` array isn't shown inline). CamelCase
@@ -1371,4 +1372,17 @@
     `${pre}<a href="${base}/tag/${encodeURIComponent(tag.toLowerCase())}" class="mention hashtag" rel="tag">#${tag}</a>`);
 }
+// Auto-link bare http(s) URLs in already-safe HTML (federated copies). Splits on existing
+// <a>…</a> so a linked URL is never wrapped twice; requires start/whitespace/'>' before the
+// URL so attribute values (src="https://…") never match. Trailing sentence punctuation stays
+// outside the link (Mastodon-style).
+function linkUrls(html) {
+  const parts = String(html || '').split(/(<a\b[^>]*>[\s\S]*?<\/a>)/gi);
+  for (let i = 0; i < parts.length; i++) {
+    if (/^<a\b/i.test(parts[i])) continue; // already a link → leave as-is
+    parts[i] = parts[i].replace(/(^|[\s>])(https?:\/\/[^\s<]+?)([.,;:!?)\]»]*)(?=$|[\s<])/g,
+      (m, pre, url, trail) => `${pre}<a href="${url.replace(/"/g, '%22')}" rel="nofollow noopener" target="_blank">${url}</a>${trail}`);
+  }
+  return parts.join('');
+}
 // Extract the AP Hashtag tag objects from already-linked reply content.
 function hashtagTags(base, content) {
@@ -1497,5 +1511,5 @@
   const mention = parent.actor_uri
     ? `<a href="${escHtml(parent.actor_url || parent.actor_uri)}" class="u-url mention" data-actor="${escHtml(parent.actor_uri)}">${escHtml(dispHandle)}</a> ` : '';
-  const content = `<p>${mention}${linkHashtags(base, mres.html)}</p>`;
+  const content = `<p>${mention}${linkUrls(linkHashtags(base, mres.html))}</p>`;
   // Dedup: skip if the exact same reply was already sent (double-submit guard).
   const dup = db.prepare('SELECT 1 FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? LIMIT 1')
@@ -1657,5 +1671,5 @@
     ? `<a href="${escHtml(toProfile)}" class="u-url mention" data-actor="${escHtml(row.to_actor)}">${escHtml(toHandle)}</a> ` : '';
   const mres = await resolveMentionsInText(base, escHtml(text).replace(/\r?\n/g, '<br>'));
-  const content = `<p>${mention}${linkHashtags(base, mres.html)}</p>`;
+  const content = `<p>${mention}${linkUrls(linkHashtags(base, mres.html))}</p>`;
   db.prepare('UPDATE ap_outbox SET content = ? WHERE id = ?').run(content, outboxId);
   const note = buildReplyNote(base, site, iStmts().getO.get(outboxId));
Index: test/url-linkify.test.js
===================================================================
--- test/url-linkify.test.js	(revision e00c0e9397ee0864f97459b36a39ff5ad86a9e41)
+++ test/url-linkify.test.js	(revision e00c0e9397ee0864f97459b36a39ff5ad86a9e41)
@@ -0,0 +1,47 @@
+// Auto-linking bare URLs — a plain http(s) URL in a post's federated copy becomes a
+// clickable link; already-linked URLs and attribute values are left alone; trailing
+// sentence punctuation stays outside the link. In-memory SQLite. Run: npm test
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const AP = (await import('../src/services/ActivityPubService.js')).default;
+
+const BASE = 'https://test.example';
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'demo', 'Demo', 'u1', 1);
+const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
+site.primary_slug = 'demo';
+
+const note = (content, extra = {}) => AP.buildNote(BASE, site, {
+  id: 'u' + Math.abs(content.length * 7919 % 100000), slug: 'x', title: '', content, tags: '[]',
+  created_at: '2026-01-01T00:00:00Z', ...extra,
+});
+
+test('a bare URL becomes a link; trailing punctuation stays outside', () => {
+  const n = note('<p>check https://example.com/x?a=1&amp;b=2, ok</p>');
+  assert.match(n.content, /<a href="https:\/\/example\.com\/x\?a=1&amp;b=2" rel="nofollow noopener" target="_blank">https:\/\/example\.com\/x\?a=1&amp;b=2<\/a>,/);
+});
+
+test('a URL with a fragment does not get hashtag-ified', () => {
+  const n = note('<p>see https://example.com/page#section</p>');
+  assert.match(n.content, /<a href="https:\/\/example\.com\/page#section"/);
+  assert.ok(!/class="mention hashtag"[^>]*>#section/.test(n.content), 'no hashtag link out of the fragment');
+});
+
+test('an already-linked URL is not wrapped twice', () => {
+  const n = note('<p><a href="https://example.com/">https://example.com/</a></p>');
+  assert.equal((n.content.match(/<a /g) || []).length, 1);
+});
+
+test('attribute values are untouched, standalone URLs still link (image stripped from content)', () => {
+  const n = note('<p><img src="https://cdn.example.com/pic.png" alt=""> and https://example.org</p>');
+  // <img> is stripped from federated content (travels as attachment) — no anchor made for its src.
+  assert.ok(!n.content.includes('cdn.example.com'), 'img src not present in content');
+  assert.match(n.content, /<a href="https:\/\/example\.org"/);
+});
