Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
+++ src/config/database.js	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -379,4 +379,14 @@
     );
     CREATE INDEX IF NOT EXISTS idx_ap_timeline_slug ON ap_timeline(slug, published);
+    CREATE TABLE IF NOT EXISTS ap_blocks (
+      id INTEGER PRIMARY KEY AUTOINCREMENT,
+      slug TEXT NOT NULL,          -- our site that set the block
+      target TEXT NOT NULL,        -- actor URI (actor block) or domain (domain block)
+      kind TEXT NOT NULL,          -- 'actor' | 'domain'
+      label TEXT,                  -- display (@handle or domain)
+      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+      UNIQUE(slug, target)
+    );
+    CREATE INDEX IF NOT EXISTS idx_ap_blocks_target ON ap_blocks(target);
   `);
 }
Index: src/middleware/render.js
===================================================================
--- src/middleware/render.js	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
+++ src/middleware/render.js	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -100,4 +100,7 @@
   const _role = _u ? _u.role : null;
   const canSeeBeheer = !!(_u && (_role === 'god' || _role === 'admin' || _role === 'kijker' || userOwnsSite));
+  // Who may use the fediverse client (timeline/notifications/blocking) — actual
+  // site managers only (these routes are requireSiteManager; viewers are excluded).
+  const canManageFedi = !!(_u && (_role === 'god' || _role === 'admin' || userOwnsSite));
 
   // Interface language: session choice (this session) → logged-in user's own preference
@@ -118,4 +121,5 @@
     userOwnsSite,
     canSeeBeheer,
+    canManageFedi,
     isViewer: _isViewer,
     canMutate: !_isViewer,
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
+++ src/routes/posts.js	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -86,5 +86,5 @@
   'tag', 'type', 'user', 'users', 'artiesten', 'leden', 'favorieten', 'feed.xml', 'atom.xml', 'sitemap.xml',
   'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets',
-  'authorize_interaction', 'fediverse', 'tijdlijn', 'meldingen',
+  'authorize_interaction', 'fediverse', 'tijdlijn', 'meldingen', 'blokkeren',
 ]);
 
@@ -613,4 +613,31 @@
   const items = site ? ActivityPubService.getNotifications(site.slug, 80) : [];
   renderPage(req, res, 'pages/fedi-notifications', { pageTitle: 'Meldingen', bodyClass: 'on-special', items });
+});
+
+// Blocking / defederation (owner-only).
+router.get('/blokkeren', requireSiteManager, (req, res) => {
+  const site = res.locals.site;
+  const blocks = site ? ActivityPubService.listBlocks(site.slug) : [];
+  renderPage(req, res, 'pages/blocks', { pageTitle: 'Blokkeren', bodyClass: 'on-special', blocks, success: req.query.success || null, error: req.query.error || null });
+});
+
+router.post('/blokkeren/add', requireSiteManager, async (req, res) => {
+  const site = res.locals.site;
+  let q = 'success=' + encodeURIComponent('Geblokkeerd');
+  if (site) {
+    try {
+      const r = await ActivityPubService.blockTarget(site, (req.body.target || '').toString());
+      if (r && r.error) q = 'error=' + encodeURIComponent(r.error === 'not_found' ? 'Account niet gevonden' : 'Voer een @handle of domein in');
+      else q = 'success=' + encodeURIComponent(((r && r.label) || '') + ' geblokkeerd');
+    } catch (e) { q = 'error=' + encodeURIComponent('Blokkeren mislukt'); }
+  }
+  const ref = req.get('Referer') || '';
+  res.redirect((ref.includes('/tijdlijn') ? '/tijdlijn?' : '/blokkeren?') + q);
+});
+
+router.post('/blokkeren/remove', requireSiteManager, (req, res) => {
+  const site = res.locals.site;
+  if (site) { try { ActivityPubService.unblock(site, (req.body.target || '').toString()); } catch (e) { /* ignore */ } }
+  res.redirect('/blokkeren?success=' + encodeURIComponent('Deblokkeerd'));
 });
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
+++ src/services/ActivityPubService.js	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -364,4 +364,6 @@
   // forged replies/likes/follows/timeline posts). GET/discovery stays open.
   const claimedActor = typeof act.actor === 'string' ? act.actor : (act.actor && act.actor.id);
+  // Blocked actor/domain → silently drop (202, don't reveal the block).
+  if (claimedActor && isBlockedAny(claimedActor)) { console.log('[AP] inbox dropped (blocked)', claimedActor); return 202; }
   const GATED = ['Create', 'Like', 'Announce', 'Follow', 'Delete', 'Undo', 'Accept', 'Reject'];
   if (GATED.includes(type)) {
@@ -774,4 +776,58 @@
 }
 
+// ── Blocking / defederation ───────────────────────────────────────
+let _insBl, _delBl, _listBl;
+function blStmts() {
+  if (!_insBl) {
+    _insBl = db.prepare('INSERT OR IGNORE INTO ap_blocks (slug, target, kind, label, created_at) VALUES (?,?,?,?,CURRENT_TIMESTAMP)');
+    _delBl = db.prepare('DELETE FROM ap_blocks WHERE slug = ? AND target = ?');
+    _listBl = db.prepare('SELECT * FROM ap_blocks WHERE slug = ? ORDER BY created_at DESC');
+  }
+  return { ins: _insBl, del: _delBl, list: _listBl };
+}
+export function listBlocks(slug) { return blStmts().list.all(slug); }
+
+// True if an actor (or its whole domain) is blocked anywhere on this instance.
+export function isBlockedAny(actorUri) {
+  if (!actorUri) return false;
+  let domain = ''; try { domain = new URL(actorUri).host; } catch { /* ignore */ }
+  try { return !!db.prepare("SELECT 1 FROM ap_blocks WHERE (kind='actor' AND target=?) OR (kind='domain' AND target=?) LIMIT 1").get(actorUri, domain); }
+  catch { return false; }
+}
+
+function purgeBlocked(kind, target) {
+  try {
+    if (kind === 'domain') {
+      const like = `%//${target}/%`;
+      db.prepare('DELETE FROM ap_interactions WHERE actor_uri LIKE ?').run(like);
+      db.prepare('DELETE FROM ap_timeline WHERE author_uri LIKE ?').run(like);
+      db.prepare('DELETE FROM ap_followers WHERE actor_uri LIKE ?').run(like);
+    } else {
+      db.prepare('DELETE FROM ap_interactions WHERE actor_uri = ?').run(target);
+      db.prepare('DELETE FROM ap_timeline WHERE author_uri = ?').run(target);
+      db.prepare('DELETE FROM ap_followers WHERE actor_uri = ?').run(target);
+    }
+  } catch { /* best-effort */ }
+}
+
+// Block an actor (@handle or actor URL) or a whole domain; purges their content.
+export async function blockTarget(site, input) {
+  const raw = String(input || '').trim();
+  if (!site || !site.slug || !raw) return { error: 'empty' };
+  let kind, target, label;
+  if (/^https?:\/\//i.test(raw)) { kind = 'actor'; target = raw; label = raw; }
+  else if (raw.includes('@')) {
+    const actorUrl = await webfingerResolve(raw);
+    if (!actorUrl) return { error: 'not_found' };
+    kind = 'actor'; target = actorUrl; label = raw.startsWith('@') ? raw : ('@' + raw);
+  } else { kind = 'domain'; target = raw.toLowerCase(); label = raw.toLowerCase(); }
+  blStmts().ins.run(site.slug, target, kind, label);
+  purgeBlocked(kind, target);
+  console.log('[AP] block', site.slug, kind, target);
+  return { ok: true, label };
+}
+
+export function unblock(site, target) { blStmts().del.run(site.slug, target); return { ok: true }; }
+
 export default {
   getOrCreateKeys, apWants, sendAP, actorId, noteId,
@@ -781,4 +837,4 @@
   listOutbox, deliverOutboxDelete,
   webfingerResolve, followActor, unfollowActor, listFollowing, getTimeline, sendInteraction,
-  getNotifications,
+  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
 };
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
+++ src/services/i18n.js	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -26,5 +26,5 @@
     'nav.language': 'Taal',
     'nav.notifications': 'Meldingen',
-    'notif.title': 'Meldingen', 'notif.empty': 'Nog geen meldingen.', 'notif.someone': 'Iemand', 'notif.followed': 'volgt je nu', 'notif.liked': 'likete je post', 'notif.boosted': 'boostte je post', 'notif.replied': 'reageerde op',
+    'notif.title': 'Meldingen', 'notif.empty': 'Nog geen meldingen.', 'notif.someone': 'Iemand', 'notif.followed': 'volgt je nu', 'notif.liked': 'likete je post', 'notif.boosted': 'boostte je post', 'notif.replied': 'reageerde op', 'blk.title': 'Blokkeren', 'blk.lead': 'Blokkeer een account of een heel domein — hun reacties, likes en posts verdwijnen en nieuwe worden geweigerd.', 'blk.block_btn': 'Blokkeren', 'blk.empty': 'Niks geblokkeerd.', 'blk.unblock': 'Deblokkeren', 'tl.block': 'Blokkeer',
     'notif.reply': '{actor} reageerde op je reactie', 'notif.comment': '{actor} reageerde op je post', 'notif.like': '{actor} vindt je post leuk',
     'switch.agenda': 'Agenda',
@@ -1029,5 +1029,5 @@
     'nav.language': 'Language',
     'nav.notifications': 'Notifications',
-    'notif.title': 'Notifications', 'notif.empty': 'No notifications yet.', 'notif.someone': 'Someone', 'notif.followed': 'followed you', 'notif.liked': 'liked your post', 'notif.boosted': 'boosted your post', 'notif.replied': 'replied to',
+    'notif.title': 'Notifications', 'notif.empty': 'No notifications yet.', 'notif.someone': 'Someone', 'notif.followed': 'followed you', 'notif.liked': 'liked your post', 'notif.boosted': 'boosted your post', 'notif.replied': 'replied to', 'blk.title': 'Blocking', 'blk.lead': 'Block an account or a whole domain — their replies, likes and posts disappear and new ones are refused.', 'blk.block_btn': 'Block', 'blk.empty': 'Nothing blocked.', 'blk.unblock': 'Unblock', 'tl.block': 'Block',
     'notif.reply': '{actor} replied to your comment', 'notif.comment': '{actor} commented on your post', 'notif.like': '{actor} liked your post',
     'switch.agenda': 'Agenda',
@@ -2023,5 +2023,5 @@
     'nav.language': 'Sprache',
     'nav.notifications': 'Benachrichtigungen',
-    'notif.title': 'Benachrichtigungen', 'notif.empty': 'Noch keine Benachrichtigungen.', 'notif.someone': 'Jemand', 'notif.followed': 'folgt dir jetzt', 'notif.liked': 'gefällt dein Beitrag', 'notif.boosted': 'teilte deinen Beitrag', 'notif.replied': 'antwortete auf',
+    'notif.title': 'Benachrichtigungen', 'notif.empty': 'Noch keine Benachrichtigungen.', 'notif.someone': 'Jemand', 'notif.followed': 'folgt dir jetzt', 'notif.liked': 'gefällt dein Beitrag', 'notif.boosted': 'teilte deinen Beitrag', 'notif.replied': 'antwortete auf', 'blk.title': 'Blockieren', 'blk.lead': 'Blockiere ein Konto oder eine ganze Domain — ihre Antworten, Likes und Beiträge verschwinden und neue werden abgelehnt.', 'blk.block_btn': 'Blockieren', 'blk.empty': 'Nichts blockiert.', 'blk.unblock': 'Entsperren', 'tl.block': 'Blockieren',
     'notif.reply': '{actor} hat auf deinen Kommentar geantwortet', 'notif.comment': '{actor} hat deinen Beitrag kommentiert', 'notif.like': '{actor} gefällt dein Beitrag',
     'switch.agenda': 'Termine',
Index: src/views/pages/admin.ejs
===================================================================
--- src/views/pages/admin.ejs	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
+++ src/views/pages/admin.ejs	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -59,4 +59,5 @@
     <a href="/tijdlijn" class="btn">🌐 <%= t('tl.title') %></a>
     <a href="/meldingen" class="btn">🔔 <%= t('notif.title') %></a>
+    <a href="/blokkeren" class="btn">🚫 <%= t('blk.title') %></a>
     <a href="/admin/updates" class="btn"><%= t('admin.b_updates') %></a>
     <a href="/admin/handleiding" class="btn"><%= t('admin.b_help') %></a>
Index: src/views/pages/blocks.ejs
===================================================================
--- src/views/pages/blocks.ejs	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
+++ src/views/pages/blocks.ejs	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -0,0 +1,41 @@
+<div class="bl-wrap">
+  <h1 class="bl-title"><%= t('blk.title') %></h1>
+  <p class="bl-lead"><%= t('blk.lead') %></p>
+  <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
+  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
+
+  <form method="post" action="/blokkeren/add" class="bl-form">
+    <input type="text" name="target" placeholder="@naam@server.social  •  server.social" autocomplete="off" spellcheck="false" required>
+    <button type="submit" class="btn btn-primary"><%= t('blk.block_btn') %></button>
+  </form>
+
+  <% if (!blocks || !blocks.length) { %>
+    <p class="bl-empty"><%= t('blk.empty') %></p>
+  <% } else { %>
+    <ul class="bl-list">
+      <% blocks.forEach(function(b){ %>
+        <li class="bl-item">
+          <span class="bl-kind"><%= b.kind === 'domain' ? '🌐' : '👤' %></span>
+          <span class="bl-label"><%= b.label || b.target %></span>
+          <form method="post" action="/blokkeren/remove"><input type="hidden" name="target" value="<%= b.target %>"><button type="submit" class="bl-unblock"><%= t('blk.unblock') %></button></form>
+        </li>
+      <% }); %>
+    </ul>
+  <% } %>
+</div>
+
+<style>
+  .bl-wrap { max-width: 560px; margin: 2rem auto; padding: 0 1rem; }
+  .bl-title { margin: 0 0 .25rem; }
+  .bl-lead { color: var(--ink-soft, #888); margin: 0 0 1.25rem; }
+  .bl-form { display: flex; gap: .5rem; margin: 0 0 1.25rem; flex-wrap: wrap; }
+  .bl-form input { flex: 1; min-width: 220px; height: 2.4rem; padding: 0 .9rem; border-radius: 999px; font: inherit;
+    border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, #000); }
+  .bl-empty { color: var(--ink-soft, #888); }
+  .bl-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .5rem; }
+  .bl-item { display: flex; align-items: center; gap: .6rem; padding: .55rem .8rem; border-radius: 12px; background: color-mix(in srgb, var(--ink, #000) 4%, transparent); border: 1px solid color-mix(in srgb, var(--ink, #000) 8%, transparent); }
+  .bl-kind { font-size: 1.05rem; }
+  .bl-label { flex: 1; min-width: 0; overflow-wrap: anywhere; }
+  .bl-unblock { background: none; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); border-radius: 8px; padding: .25rem .6rem; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; }
+  .bl-unblock:hover { color: var(--ink, #000); }
+</style>
Index: src/views/pages/timeline.ejs
===================================================================
--- src/views/pages/timeline.ejs	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
+++ src/views/pages/timeline.ejs	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -49,4 +49,5 @@
               <form method="post" action="/tijdlijn/boost" class="tl-act-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" class="tl-act" title="<%= t('fedi.boosts') %>">🔁</button></form>
               <button type="button" class="tl-act fedi-remote-reply-btn" data-fedi-uri="<%= p.id %>" data-fedi-ph="<%= t('fedi.remote_ph') %>" title="<%= t('fedi.remote_reply') %>">↩</button>
+              <form method="post" action="/blokkeren/add" class="tl-act-form" onsubmit="return confirm('<%= t('tl.block') %>?');"><input type="hidden" name="target" value="<%= p.author_uri %>"><button type="submit" class="tl-act" title="<%= t('tl.block') %>">🚫</button></form>
             </div>
           </div>
Index: src/views/partials/topnav.ejs
===================================================================
--- src/views/partials/topnav.ejs	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
+++ src/views/partials/topnav.ejs	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -99,9 +99,8 @@
       <% } %>
 
-      <% if (user) { %>
-        <a class="nav-btn nav-notif" href="/notifications" aria-label="<%= t('nav.notifications') %>" title="<%= t('nav.notifications') %>"
-           hx-get="/notifications?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="/notifications" hx-indicator="#pcms-loading">
+      <% if (user && typeof canManageFedi !== 'undefined' && canManageFedi) { %>
+        <a class="nav-btn nav-notif" href="/meldingen" aria-label="<%= t('notif.title') %>" title="<%= t('notif.title') %>"
+           hx-get="/meldingen?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="/meldingen" hx-indicator="#pcms-loading">
           <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
-          <% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %><span class="notif-badge"><%= notifUnread > 9 ? '9+' : notifUnread %></span><% } %>
         </a>
       <% } %>
@@ -139,10 +138,12 @@
                 <span><%= t('nav.account') %></span>
               </a>
-              <a href="/notifications" role="menuitem" class="udi"
-                 hx-get="/notifications?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
-                 hx-push-url="/notifications" hx-indicator="#pcms-loading">
+              <% if (typeof canManageFedi !== 'undefined' && canManageFedi) { %>
+              <a href="/meldingen" role="menuitem" class="udi"
+                 hx-get="/meldingen?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
+                 hx-push-url="/meldingen" hx-indicator="#pcms-loading">
                 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
-                <span><%= t('nav.notifications') %><% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %> (<%= notifUnread %>)<% } %></span>
+                <span><%= t('notif.title') %></span>
               </a>
+              <% } %>
               <a href="/favorieten" role="menuitem" class="udi"
                  hx-get="/favorieten?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
