diff --git a/packages/web/relay.config.json b/packages/web/relay.config.json
index 89f9e7e..1f531fa 100644
--- a/packages/web/relay.config.json
+++ b/packages/web/relay.config.json
@@ -2,5 +2,10 @@
"src": "./src",
"schema": "./schema.graphql",
"language": "typescript",
+ "customScalarTypes": {
+ "DateTime": "string",
+ "URL": "string",
+ "UUID": "string"
+ },
"eagerEsModules": true
}
diff --git a/packages/web/src/components/ActorCard.tsx b/packages/web/src/components/ActorCard.tsx
new file mode 100644
index 0000000..c7913a0
--- /dev/null
+++ b/packages/web/src/components/ActorCard.tsx
@@ -0,0 +1,49 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { A } from "@solidjs/router";
+import { graphql } from "relay-runtime";
+import { Show } from "solid-js";
+import { createFragment } from "solid-relay";
+
+import type { ActorCard_actor$key } from "./__generated__/ActorCard_actor.graphql.ts";
+
+import styles from "~/styles/instance.module.css";
+
+export const ActorCard = (props: { $actor: ActorCard_actor$key }) => {
+ const actorData = createFragment(
+ graphql`
+ fragment ActorCard_actor on Actor {
+ handle
+ id
+ }
+ `,
+ () => props.$actor,
+ );
+
+ return (
+
+ {(actor) => (
+
+
+
+ {actor().handle}
+
+
+ )}
+
+ );
+};
diff --git a/packages/web/src/components/ActorDetail.tsx b/packages/web/src/components/ActorDetail.tsx
index af8be37..1a87990 100644
--- a/packages/web/src/components/ActorDetail.tsx
+++ b/packages/web/src/components/ActorDetail.tsx
@@ -14,32 +14,148 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+import { Title } from "@solidjs/meta";
import { graphql } from "relay-runtime";
-import { Show } from "solid-js";
+import { Index, Show } from "solid-js";
import { createFragment } from "solid-relay";
import type { ActorDetail_actor$key } from "./__generated__/ActorDetail_actor.graphql.ts";
+import { CopyButton } from "./CopyButton.tsx";
-import styles from "~/styles/instance.module.css";
+import styles from "~/styles/actor.module.css";
-export const ActorDetail = (props: { $actor: ActorDetail_actor$key }) => {
- const actorData = createFragment(
+/**
+ * Read-only identity and federation information for an actor.
+ * @returns The actor identity and endpoint sections.
+ */
+export function ActorDetail(props: { $actor: ActorDetail_actor$key }) {
+ const data = createFragment(
graphql`
- fragment ActorDetail_actor on Actor {
+ fragment ActorDetail_actor on Actor @throwOnFieldError {
handle
+ username
+ uuid
+ type
+ created
+ iri
+ avatarUrl
+ inboxUrl
+ outbox {
+ iri
+ }
+ followers {
+ iri
+ }
+ following {
+ iri
+ }
+ featured {
+ iri
+ }
+ profileUrl
+ instance {
+ host
+ }
}
`,
() => props.$actor,
);
-
return (
-
- {(actor) => (
-
-
-
+
+ >
+ );
+ }}
);
-};
+}
diff --git a/packages/web/src/components/CopyButton.tsx b/packages/web/src/components/CopyButton.tsx
new file mode 100644
index 0000000..0ebc806
--- /dev/null
+++ b/packages/web/src/components/CopyButton.tsx
@@ -0,0 +1,89 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { Show, createSignal, onCleanup } from "solid-js";
+
+import styles from "~/styles/buttons.module.css";
+
+/**
+ * Copy a value to the clipboard and announce the result.
+ * @returns A copy button with accessible feedback.
+ */
+export function CopyButton(props: { value: string; label: string }) {
+ const [message, setMessage] = createSignal("");
+ let resetTimer: ReturnType | undefined;
+ let request = 0;
+ const feedbackDuration = 2000;
+ onCleanup(() => {
+ request += 1;
+ clearTimeout(resetTimer);
+ });
+ async function copy() {
+ request += 1;
+ const currentRequest = request;
+ clearTimeout(resetTimer);
+ setMessage("");
+ try {
+ await navigator.clipboard.writeText(props.value);
+ if (currentRequest !== request) return;
+ setMessage("Copied.");
+ resetTimer = setTimeout(() => setMessage(""), feedbackDuration);
+ } catch {
+ if (currentRequest !== request) return;
+ setMessage("Could not copy. Select and copy the text manually.");
+ }
+ }
+ return (
+
+
+
+
+ );
+}
diff --git a/packages/web/src/routes/actor/[id].tsx b/packages/web/src/routes/actor/[id].tsx
new file mode 100644
index 0000000..3c1949a
--- /dev/null
+++ b/packages/web/src/routes/actor/[id].tsx
@@ -0,0 +1,105 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { Title } from "@solidjs/meta";
+import {
+ A,
+ type RouteDefinition,
+ type RouteSectionProps,
+ query,
+} from "@solidjs/router";
+import { graphql } from "relay-runtime";
+import { ErrorBoundary, Show, Suspense } from "solid-js";
+import {
+ createPreloadedQuery,
+ loadQuery,
+ useRelayEnvironment,
+} from "solid-relay";
+
+import { ActorDetail } from "~/components/ActorDetail.tsx";
+
+import type { ActorDetailQuery } from "./__generated__/ActorDetailQuery.graphql.ts";
+
+import styles from "~/styles/actor.module.css";
+
+const actorDetailQuery = graphql`
+ query ActorDetailQuery($id: ID!) @throwOnFieldError {
+ node(id: $id) {
+ ... on Actor @alias(as: "actor") {
+ ...ActorDetail_actor
+ }
+ }
+ }
+`;
+
+const loadActorDetailQuery = query(
+ (id: string) =>
+ loadQuery(useRelayEnvironment()(), actorDetailQuery, {
+ id,
+ }),
+ "ActorDetailQuery",
+);
+
+export const route = {
+ preload({ params }) {
+ if (params.id === undefined) {
+ throw new Error("Missing actor ID route parameter.");
+ }
+ return loadActorDetailQuery(params.id);
+ },
+} satisfies RouteDefinition;
+
+type RouteData = ReturnType;
+
+export default function ActorDetailPage(props: RouteSectionProps) {
+ return (
+
+ Actor — DrFed
+
+ ← All instances
+
+
+ Unable to load this actor. Please reload the page to try again.
+