Uptoolkit
GitHub

Schema.org JSON-LD

Maps ActivityPub actors, objects, and activities to Schema.org JSON-LD.

{
  "@context": "https://schema.org",
  "@type": "Person",
  "@id": "https://social.example/users/ada",
  "name": "Ada Okoye",
  "alternateName": "@ada@social.example",
  "identifier": "acct:ada@social.example",
  "description": "Front-end engineer. Building small tools for large groups of people.",
  "url": "https://social.example/users/ada",
  "image": {
    "@type": "ImageObject",
    "contentUrl": "https://api.dicebear.com/9.x/thumbs/svg?seed=ada",
    "url": "https://api.dicebear.com/9.x/thumbs/svg?seed=ada",
    "caption": "Ada Okoye avatar",
    "description": "Ada Okoye avatar",
    "encodingFormat": "image/svg+xml"
  },
  "sameAs": [
    "https://ada.example"
  ],
  "interactionStatistic": [
    {
      "@type": "InteractionCounter",
      "interactionType": {
        "@type": "FollowAction"
      },
      "userInteractionCount": 3187
    },
    {
      "@type": "InteractionCounter",
      "interactionType": {
        "@type": "SubscribeAction"
      },
      "userInteractionCount": 291
    },
    {
      "@type": "InteractionCounter",
      "interactionType": {
        "@type": "WriteAction"
      },
      "userInteractionCount": 412
    }
  ],
  "additionalProperty": [
    {
      "@type": "PropertyValue",
      "name": "Website",
      "value": "https://ada.example"
    },
    {
      "@type": "PropertyValue",
      "name": "Location",
      "value": "Lagos, Nigeria"
    },
    {
      "@type": "PropertyValue",
      "name": "Pronouns",
      "value": "she/her"
    }
  ]
}
{
  "@context": "https://schema.org",
  "@type": "SocialMediaPosting",
  "@id": "https://fediverse.example/users/mira/posts/coastline-drift",
  "url": "https://fediverse.example/users/mira/posts/coastline-drift",
  "articleBody": "Spent the week redrawing a 1:25000 coastline by hand. The shoreline has moved 40 metres inland since the 1984 survey.\n\nEvery map is a snapshot of a place that has already changed. #cartography #openstreetmap",
  "text": "Spent the week redrawing a 1:25000 coastline by hand. The shoreline has moved 40 metres inland since the 1984 survey.\n\nEvery map is a snapshot of a place that has already changed. #cartography #openstreetmap",
  "datePublished": "2025-06-12T14:26:00.000Z",
  "inLanguage": "en",
  "author": {
    "@type": "Person",
    "@id": "https://fediverse.example/users/mira",
    "name": "Mira Halvorsen",
    "alternateName": "@mira@fediverse.example",
    "identifier": "acct:mira@fediverse.example",
    "description": "Cartographer. I make maps of things that are hard to see.",
    "url": "https://fediverse.example/users/mira",
    "image": {
      "@type": "ImageObject",
      "contentUrl": "https://api.dicebear.com/9.x/thumbs/svg?seed=mira",
      "url": "https://api.dicebear.com/9.x/thumbs/svg?seed=mira",
      "caption": "Mira Halvorsen avatar",
      "description": "Mira Halvorsen avatar",
      "encodingFormat": "image/svg+xml"
    },
    "sameAs": [
      "https://maps.example/mira"
    ],
    "additionalProperty": [
      {
        "@type": "PropertyValue",
        "name": "Atlas",
        "value": "https://maps.example/mira"
      },
      {
        "@type": "PropertyValue",
        "name": "Location",
        "value": "Bergen, Norway"
      }
    ]
  },
  "image": [
    {
      "@type": "ImageObject",
      "contentUrl": "https://api.dicebear.com/9.x/shapes/svg?seed=coastline",
      "url": "https://api.dicebear.com/9.x/shapes/svg?seed=coastline",
      "caption": "Hand-drawn coastline contour map with annotated survey markers",
      "description": "Hand-drawn coastline contour map with annotated survey markers",
      "width": 1200,
      "height": 800
    },
    {
      "@type": "ImageObject",
      "contentUrl": "https://api.dicebear.com/9.x/shapes/svg?seed=survey",
      "url": "https://api.dicebear.com/9.x/shapes/svg?seed=survey",
      "caption": "Comparison overlay of the 1984 and current shoreline",
      "description": "Comparison overlay of the 1984 and current shoreline",
      "width": 1200,
      "height": 800
    }
  ],
  "keywords": [
    "cartography",
    "openstreetmap"
  ],
  "contentLocation": {
    "@type": "Place",
    "name": "Bergen, Norway",
    "geo": {
      "@type": "GeoCoordinates",
      "latitude": 60.39,
      "longitude": 5.32
    }
  },
  "commentCount": 2,
  "interactionStatistic": [
    {
      "@type": "InteractionCounter",
      "interactionType": {
        "@type": "LikeAction"
      },
      "userInteractionCount": 214
    },
    {
      "@type": "InteractionCounter",
      "interactionType": {
        "@type": "ShareAction"
      },
      "userInteractionCount": 38
    },
    {
      "@type": "InteractionCounter",
      "interactionType": {
        "@type": "CommentAction"
      },
      "userInteractionCount": 2
    }
  ]
}

Installation

npx shadcn@latest add https://ui.uptoolkit.com/r/schema-org.json

Usage

ActivityPub gets your content to other servers; Schema.org gets it into search results and AI answers. Both are JSON-LD, so the same source object projects into either shape without a second source of truth.

Every function is pure, takes an ActivityPub document, and returns a plain JSON-LD node. undefined, null, and empty arrays are dropped, so output stays small enough to inline.

ActivityPubSchema.org
PersonPerson
Group, OrganizationOrganization
NoteSocialMediaPosting
ArticleBlogPosting
reply NoteComment with parentItem
actor profileProfilePage with mainEntity
OrderedCollectionCollectionPage wrapping an ItemList
Like, Announce, FollowLikeAction, ShareAction, FollowAction
import { JsonLd } from "@/components/json-ld";
import { toProfilePageJsonLd, toSocialMediaPostingJsonLd } from "@/lib/schema-org";

<JsonLd data={toSocialMediaPostingJsonLd(note, { includeComments: true })} />
<JsonLd data={toProfilePageJsonLd(actor, { counts: { followers: 3187 }, posts })} />

Counts become interactionStatistic entries with the right InteractionCounter action type, which is what Google reads for social engagement signals:

toInteractionStatisticJsonLd({ likes: 214, shares: 38, replies: 12, followers: 3187 });

Set withContext: false on any call to omit @context when nesting a node inside a larger graph. toPersonJsonLd emits the WebFinger acct: URI as identifier, so a Schema.org consumer can join the node back to the fediverse identity.