Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision bfd9c7325cd36c5b855d62c6a23001f6733ef4f8)
+++ src/services/ActivityPubService.js	(revision 8179fdb41a15e9de99e4eb136db707d7ff3c6781)
@@ -29,53 +29,8 @@
 import Blocklist from './BlocklistService.js';
 import * as Guardianship from './guardianship/index.js';
-
-const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
-// Full JSON-LD context for every AP object we emit: AS2 core + security (publicKey) + the
-// extension terms we actually use (Mastodon/toot + schema.org), each with a term definition
-// so a strict JSON-LD processor resolves them instead of dropping them → valid AS2/JSON-LD.
-// This is the same context shape Mastodon publishes, so Mastodon sees no change.
-const AP_CONTEXT = [
-  'https://www.w3.org/ns/activitystreams',
-  'https://w3id.org/security/v1',
-  {
-    toot: 'http://joinmastodon.org/ns#',
-    schema: 'http://schema.org#',
-    sensitive: 'as:sensitive',
-    Hashtag: 'as:Hashtag',
-    manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
-    discoverable: 'toot:discoverable',
-    // FEP-7628 (account moves): same term declaration Mastodon ships.
-    alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' },
-    movedTo: { '@id': 'as:movedTo', '@type': '@id' },
-    featured: { '@id': 'toot:featured', '@type': '@id' },
-    PropertyValue: 'schema:PropertyValue',
-    value: 'schema:value',
-    embedUrl: { '@id': 'schema:embedUrl', '@type': '@id' },
-    // Wat een track beschrijft en AS2 niet kent (shaer-0nh). Funkwhale zet deze
-    // vier op zijn Audio; het bleken geen eigen verzinsels maar termen die
-    // schema.org gewoon heeft -- en schema.org stond hier al. De SLEUTELS zijn
-    // die van Funkwhale, want daar leest hij op; de BETEKENIS komt van
-    // schema.org, dus we hoeven geen vreemd vocabulaire binnen te halen.
-    license: { '@id': 'schema:license', '@type': '@id' },
-    position: 'schema:position',
-    bitrate: 'schema:bitrate',
-    size: 'schema:contentSize',
-    // Poll (Question) extension: Question/oneOf/anyOf/endTime/closed are AS2 core, but the
-    // per-poll unique-voter count is a Mastodon (toot) term — declare it so the emitted
-    // Question stays valid JSON-LD (a strict processor would otherwise drop votersCount).
-    votersCount: 'toot:votersCount',
-    // Kanaal-vocabulaire (shaer-0nh). Funkwhale declareert `category` niet
-    // inline maar via zijn eigen remote context https://funkwhale.audio/ns, en
-    // die host is vanaf hier onbereikbaar -- de IRI hieronder is dus AFGELEID
-    // en niet geverifieerd. Wat vandaag telt voor interop is de JSON-sleutel,
-    // want daar matchen lezers op; de declaratie zorgt alleen dat een strikte
-    // JSON-LD-processor hem niet laat vallen. Nakijken zodra die host weer
-    // antwoordt.
-    category: { '@id': 'https://funkwhale.audio/ns#category' },
-    // FEP-633c (Guardians): the shaer namespace, owned by the guardianship
-    // module (src/services/guardianship/).
-    ...Guardianship.SHAER_CONTEXT,
-  },
-];
+import { PUBLIC, AP_CONTEXT, safeUrl, actorId, noteId, guessMediaType } from './ap-core.js';
+// Doorgeven wat hier altijd vandaan kwam, zodat elke bestaande aanroep blijft werken.
+export { AP_CONTEXT, actorId, noteId, guessMediaType };
+
 
 // Short random suffix so two activity ids minted in the same millisecond (e.g.
@@ -85,5 +40,4 @@
 // Keep only http(s) URLs — drops javascript:/data:/etc so a remote actor can't
 // smuggle a dangerous scheme into a stored href/src (rendered in owner-only views).
-const safeUrl = (u) => { const s = String(u == null ? '' : u).trim(); return /^https?:\/\//i.test(s) ? s : ''; };
 
 // ── SSRF guard for outbound fetches ───────────────────────────────
@@ -259,22 +213,5 @@
 
 // ── document builders ─────────────────────────────────────────────
-export function actorId(base, slug) { return `${base}/ap/users/${encodeURIComponent(slug)}`; }
-
-/**
- * mediaType raden uit een bestandsnaam. Stond twee keer functie-lokaal in dit
- * bestand, met een commentaar dat ze "dezelfde afleiding" waren -- en dat was
- * niet zo: de ene kende video, de andere alleen beeld. Nu een kaart, hier.
- * De terugval is image/jpeg omdat dit alleen op omslagen en bijlagen wordt
- * losgelaten, nooit op geluid: dat draagt zijn eigen mime_type uit de database.
- */
-export function guessMediaType(u) {
-  const e = ((u || '').split('?')[0].match(/\.(\w+)$/) || [])[1];
-  return ({
-    jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
-    webp: 'image/webp', avif: 'image/avif',
-    mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime',
-  })[(e || '').toLowerCase()] || 'image/jpeg';
-}
-export function noteId(base, postId) { return `${base}/ap/notes/${encodeURIComponent(postId)}`; }
+
 
 /** Eén Link uit een AS2 `url` kiezen op mediaType. Een `url` mag een string,
