From d74ce32cd0ea68d86cce84a51b1d1ab1cf4c6f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 7 Jun 2026 08:43:03 +0200 Subject: [PATCH 1/3] fix(journal): actor profile fields must serialize as a JSON array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mastodon's PropertyValue parser requires the actor's attachment to be a JSON *array* and silently ignores a bare object — and Fedify compacts single-element arrays to bare objects, so the 🥾 trails.cool field shipped in #464 never rendered (verified: Mastodon stored fields = {} after a forced actor refresh on the soak instance). Ship two fields so the array survives serialization: the 🥾 profile link (rel=me) plus an Instance link — the latter is genuinely useful for self-hosted instances anyway. Test now asserts the array shape explicitly. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/journal/app/lib/federation.server.test.ts | 18 ++++++++++++------ apps/journal/app/lib/federation.server.ts | 8 ++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/journal/app/lib/federation.server.test.ts b/apps/journal/app/lib/federation.server.test.ts index f00aa39..294ab08 100644 --- a/apps/journal/app/lib/federation.server.test.ts +++ b/apps/journal/app/lib/federation.server.test.ts @@ -95,12 +95,18 @@ describe("actor object", () => { expect(actor.preferredUsername).toBe("bruno"); expect(actor.name).toBe("Bruno"); expect(actor.summary).toBe("Riding bikes"); - // Profile-metadata field Mastodon renders ("this is a trails profile") - const attachments = Array.isArray(actor.attachment) ? actor.attachment : [actor.attachment]; - const field = attachments.find((a: { type: string }) => a?.type === "PropertyValue"); - expect(field?.name).toBe("🥾 trails.cool"); - expect(field?.value).toContain('href="http://localhost:3000/users/bruno"'); - expect(field?.value).toContain('rel="me"'); + // Profile-metadata fields Mastodon renders ("this is a trails + // profile"). MUST serialize as a JSON array — Mastodon's parser + // silently ignores a bare attachment object, which is what Fedify + // compacts single-element arrays into (hence two fields). + expect(Array.isArray(actor.attachment)).toBe(true); + const fields = actor.attachment.filter((a: { type: string }) => a?.type === "PropertyValue"); + expect(fields.length).toBeGreaterThanOrEqual(2); + const trails = fields.find((f: { name: string }) => f.name === "🥾 trails.cool"); + expect(trails?.value).toContain('href="http://localhost:3000/users/bruno"'); + expect(trails?.value).toContain('rel="me"'); + const instance = fields.find((f: { name: string }) => f.name === "Instance"); + expect(instance?.value).toContain("localhost:3000"); }); it("404s the actor for a private user", async () => { diff --git a/apps/journal/app/lib/federation.server.ts b/apps/journal/app/lib/federation.server.ts index 6cb971e..8622653 100644 --- a/apps/journal/app/lib/federation.server.ts +++ b/apps/journal/app/lib/federation.server.ts @@ -180,11 +180,19 @@ function buildFederation(): Federation { // metadata table — a human-visible "this is a trails profile" // marker. (The machine-readable marker is NodeInfo; this is // flair.) Mastodon strips most HTML in values but keeps links. + // NOTE: Mastodon's parser requires `attachment` to be a JSON + // *array* and silently ignores a bare object — and Fedify + // compacts single-element arrays to bare objects. Keep at least + // two fields here so the array survives serialization. attachments: [ new PropertyValue({ name: "🥾 trails.cool", value: `${getOrigin().replace(/^https?:\/\//, "")}/users/${identifier}`, }), + new PropertyValue({ + name: "Instance", + value: `${getOrigin().replace(/^https?:\/\//, "")}`, + }), ], }); }) From 4e63f7631a75d3ccf8808c803b73a9bb1a7d3ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 7 Jun 2026 09:13:44 +0200 Subject: [PATCH 2/3] fix(journal): show remote followers in follower/following lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit countFollowers counts every accepted follow row, but listFollowers inner-joined users on follower_id — so a federated follower (NULL follower_id, follower_actor_iri set) bumped the count while never appearing in the list. Observed live: profile said '1 follower', list below was empty. listFollowing had the same latent bug for outbound trails-to-trails follows. Both lists now left-join users + remote_actors: local entries link to the local profile as before; remote entries display cached actor data (IRI parsing as fallback) and link out to the remote profile. Integration test pins count/list consistency for a remote follower. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../journal/app/components/CollectionPage.tsx | 8 +- .../lib/federation-inbox.integration.test.ts | 17 ++++ apps/journal/app/lib/follow.server.ts | 99 ++++++++++++++++--- 3 files changed, 109 insertions(+), 15 deletions(-) diff --git a/apps/journal/app/components/CollectionPage.tsx b/apps/journal/app/components/CollectionPage.tsx index 08c8bab..2632b44 100644 --- a/apps/journal/app/components/CollectionPage.tsx +++ b/apps/journal/app/components/CollectionPage.tsx @@ -4,6 +4,9 @@ interface Entry { username: string; displayName: string | null; domain: string; + /** Local path (`/users/x`) or, for federated entries, the remote profile URL. */ + profileUrl: string; + remote: boolean; } interface Props { @@ -42,9 +45,10 @@ export function CollectionPage({ kind, user, entries, page, total }: Props) { ) : (