Index: src/routes/admin-sites.js
===================================================================
--- src/routes/admin-sites.js	(revision e2c3d09c54bd06c4a04e4c72e4ff59aa9374b705)
+++ src/routes/admin-sites.js	(revision ccaa5301c212451f2e6b7c28c49282055278e961)
@@ -26,4 +26,5 @@
 import { toWebp } from '../services/ImageWebpService.js';
 import { mediaDir } from '../config/paths.js';
+import AP from '../services/ActivityPubService.js';
 
 
@@ -71,4 +72,25 @@
   }
   return arr.length ? JSON.stringify(arr) : null;
+}
+
+/**
+ * FEP-7628 aliases (alsoKnownAs): one former identity per line, as an actor
+ * URL or an @user@host handle. Handles resolve via WebFinger AT SAVE TIME on
+ * purpose — a typo'd alias that silently lands on the actor would make a later
+ * Move fail at the old server with no hint why. Throws the offending line.
+ */
+async function parseApAliases(raw, ownActorUri) {
+  const lines = String(raw || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
+  if (lines.length > 5) throw new Error(lines[5] + ' (max 5)');
+  const out = [];
+  for (const line of lines) {
+    let uri = null;
+    if (/^https?:\/\//i.test(line)) uri = line;
+    else if (line.includes('@')) uri = await AP.webfingerResolve(line).catch(() => null);
+    if (!uri) throw new Error(line);
+    if (uri === ownActorUri) continue; // claiming yourself adds nothing
+    if (!out.includes(uri)) out.push(uri);
+  }
+  return out;
 }
 
@@ -256,4 +278,7 @@
   }
 
+  let apAliases = '';
+  try { apAliases = (JSON.parse(site.ap_aliases || '[]') || []).join('\n'); } catch { /* show empty on malformed */ }
+
   renderPage(req, res, 'pages/admin-site-edit', {
     pageTitleKey: 'admin.t_editsite', pageTitleVars: { title: site.title },
@@ -266,4 +291,5 @@
     platforms: listPlatforms(),
     parsedLinks,
+    apAliases,
     success: req.query.success || null,
     error: req.query.error || null,
@@ -272,6 +298,6 @@
 
 // ==================== SAVE ====================
-router.post('/:slug/save', requireSiteManagerBySlug, (req, res) => {
-  const site = db.prepare('SELECT id FROM sites WHERE slug = ?').get(req.params.slug);
+router.post('/:slug/save', requireSiteManagerBySlug, async (req, res) => {
+  const site = db.prepare('SELECT id, ap_aliases FROM sites WHERE slug = ?').get(req.params.slug);
   if (!site) return res.redirect('/admin/sites?error=Not+found');
 
@@ -279,4 +305,14 @@
   const feedViewDef = f.feed_view_default === 'grid' ? 'grid' : 'timeline';
   const profileLinksJson = buildProfileLinks(f);
+
+  // FEP-7628 aliases — validated/resolved before anything is written.
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  let apAliasesJson = null;
+  try {
+    const arr = await parseApAliases(f.ap_aliases, AP.actorId(base, req.params.slug));
+    apAliasesJson = arr.length ? JSON.stringify(arr) : null;
+  } catch (e) {
+    return res.redirect(`/admin/sites/${req.params.slug}/edit?error=` + encodeURIComponent(`Alias niet herkend of niet vindbaar: ${e.message}`));
+  }
 
   // theme_override: only accept the three legal values. Empty string means
@@ -294,4 +330,5 @@
       profile_enabled = ?,
       profile_links = ?,
+      ap_aliases = ?,
       is_public = ?, robots_index = ?, require_login_to_comment = ?,
       enable_audio_player = ?,
@@ -312,4 +349,5 @@
     f.profile_enabled ? 1 : 0,
     profileLinksJson,
+    apAliasesJson,
     f.is_public ? 1 : 0,
     f.robots_index ? 1 : 0,
@@ -336,4 +374,14 @@
   }
 
+  // Alias change → broadcast an actor Update so remote caches refresh. The old
+  // server re-fetches the actor live during a Move anyway; this is freshness,
+  // not correctness, hence best-effort.
+  if ((site.ap_aliases || null) !== apAliasesJson) {
+    try {
+      const fresh = db.prepare('SELECT * FROM sites WHERE id = ?').get(site.id);
+      AP.deliverActorUpdate(fresh).catch(() => {});
+    } catch { /* never blocks the save */ }
+  }
+
   res.redirect(`/admin/sites/${req.params.slug}/edit?success=` + encodeURIComponent('Opgeslagen'));
 });
