Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.claude/settings.local.json
.DS_Store
.playwright-mcp/
.pnpm-store/
.vocab-codegen.lock/
dist/
Expand All @@ -12,6 +13,7 @@ test/smoke/mastodon/.certs/
test/smoke/mastodon/mastodon.env
test/smoke/sharkey/.certs/
test/smoke/sharkey/sharkey.env
tmp/
smoke.log
t.ts
t2.ts
Expand Down
314 changes: 314 additions & 0 deletions docs/manual/pragmatics.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,3 +418,317 @@ Mastodon](pragmatics/mastodon-followers.png)
> [!NOTE]
> Mastodon does not display the followers collection of a remote actor,
> but other ActivityPub implementations may display it.


Objects
-------

The following types of objects are commonly used to represent posts and other
public-facing content in the fediverse:

- `Article` represents a multi-paragraph written work.
- `Note` is a short post.
- `Question` is a poll.

Link-like objects such as `Mention`, `Hashtag`, and `Emoji` are usually attached
to these objects through their `tags` property. The exact way ActivityPub
implementations render these objects differs, but Mastodon and Misskey already
share a number of de facto conventions.

### `Note`: Short posts

The `Note` type is the most common object type for short posts. In Mastodon,
the `content` property becomes the post body, and `attachments` are rendered
below the body.

~~~~ typescript twoslash
import { Hashtag, Image, Mention, Note } from "@fedify/vocab";
// ---cut-before---
new Note({
content: // [!code highlight]
'<p>Hello <a class="mention u-url" href="https://example.com/users/friend">' +
'@friend@example.com</a>! This note demonstrates ' +
'<a class="mention hashtag" rel="tag" ' +
'href="https://example.com/tags/fedify">#fedify</a>.</p>',
attachments: [ // [!code highlight]
new Image({
url: new URL("https://picsum.photos/id/237/1200/800"),
mediaType: "image/jpeg",
name: "A placeholder dog photo",
}),
],
tags: [
new Mention({
href: new URL("https://example.com/users/friend"),
name: "@friend@example.com",
}),
new Hashtag({
href: new URL("https://example.com/tags/fedify"),
name: "#fedify",
}),
],
})
~~~~

> [!NOTE]
> The `content` property expects an HTML string. If it contains characters
> like `<`, `>`, and `&`, you should escape HTML entities.

For example, the above `Note` object is displayed like the following in
Mastodon:

![Screenshot: A note with an attached image in
Mastodon](pragmatics/mastodon-note-body.png)

### `summary`: Content warnings

On `Note` objects, the `summary` property is commonly used as a content
warning. In Mastodon, it becomes the warning text shown above the collapsed
post body.

~~~~ typescript twoslash
import { Note } from "@fedify/vocab";
// ---cut-before---
new Note({
summary: "CW: Rendering pragmatics demo", // [!code highlight]
content: "<p>Hello @friend@example.com! This note demonstrates #fedify.</p>",
})
~~~~

> [!NOTE]
> The `summary` property also expects an HTML string.

For example, the above `summary` is displayed like the following in Mastodon:

![Screenshot: A note with a content warning in
Mastodon](pragmatics/mastodon-note.png)

### `Article`: Long-form posts

The `Article` type is commonly used for long-form posts such as blog entries.
In Mastodon, the `name` property is displayed as the title, the `url` property
is shown as the canonical link, and `tags` can still surface hashtags below the
post body.

~~~~ typescript twoslash
import { Article, Hashtag } from "@fedify/vocab";
// ---cut-before---
new Article({
name: "Pragmatics of `Article` objects", // [!code highlight]
url: new URL("https://example.com/blog/pragmatics-article"), // [!code highlight]
content: // [!code highlight]
"<p>This article demonstrates how a long-form object can expose a title " +
"and a canonical permalink.</p>",
tags: [ // [!code highlight]
new Hashtag({
href: new URL("https://example.com/tags/activitypub"),
name: "#activitypub",
}),
],
})
~~~~

> [!NOTE]
> Many ActivityPub implementations render `Article` objects more compactly than
> `Note` objects. If you want a title-like presentation, populate the `name`
> property instead of relying on the body alone.

For example, the above `Article` object is displayed like the following in
Mastodon:

![Screenshot: An article object with a title, canonical link, and hashtag in
Mastodon](pragmatics/mastodon-article.png)

### `Question`: Polls

The `Question` type is used for polls. In Mastodon, the question body comes
from `content`, the poll choices come from `exclusiveOptions` or
`inclusiveOptions`, and metadata such as `voters` and `closed` are displayed
below the choices.

### `exclusiveOptions`: Single-choice polls

~~~~ typescript twoslash
import { Collection, Note, Question } from "@fedify/vocab";
import { Temporal } from "@js-temporal/polyfill";
// ---cut-before---
new Question({
content: "<p>Which pragmatics example should the manual explain first?</p>", // [!code highlight]
exclusiveOptions: [ // [!code highlight]
new Note({ name: "A short note", replies: new Collection({ totalItems: 4 }) }),
new Note({ name: "A long article", replies: new Collection({ totalItems: 2 }) }),
new Note({
name: "A poll question",
replies: new Collection({ totalItems: 7 }),
}),
],
voters: 13, // [!code highlight]
closed: Temporal.Instant.from("2026-04-28T12:00:00Z"), // [!code highlight]
})
~~~~

> [!NOTE]
> Use `exclusiveOptions` for single-choice polls. A `Question` object should
> not contain both `exclusiveOptions` and `inclusiveOptions`.

When Mastodon shows poll results, each option's `replies.totalItems` value is
used as that option's vote count. In the above example, the values 4, 2, and 7
add up to 13, which matches `voters` and yields the percentages shown in the
results view.

For example, the above `Question` object is displayed like the following in
Mastodon after opening the results view:

![Screenshot: A single-choice question object rendered as poll results in
Mastodon](pragmatics/mastodon-question.png)

### `inclusiveOptions`: Multiple-choice polls

Use `inclusiveOptions` for polls where a voter may choose more than one option.

~~~~ typescript twoslash
import { Collection, Note, Question } from "@fedify/vocab";
import { Temporal } from "@js-temporal/polyfill";
// ---cut-before---
new Question({
content: "<p>Which pragmatics details should the manual cover next?</p>", // [!code highlight]
inclusiveOptions: [ // [!code highlight]
new Note({
name: "Content warnings",
replies: new Collection({ totalItems: 6 }),
}),
new Note({
name: "Custom emoji",
replies: new Collection({ totalItems: 8 }),
}),
new Note({
name: "Poll results",
replies: new Collection({ totalItems: 3 }),
}),
],
voters: 13, // [!code highlight]
closed: Temporal.Instant.from("2026-04-20T12:00:00Z"), // [!code highlight]
})
~~~~

In Mastodon, the `replies.totalItems` values on `inclusiveOptions` still become
the per-option counts in the results view, but unlike a single-choice poll they
do not need to add up to `voters`, because each voter may select more than one
option.

For example, the above multiple-choice `Question` object is initially displayed
like the following in Mastodon:

![Screenshot: A multiple-choice question object before opening poll results in
Mastodon](pragmatics/mastodon-question-multi-choices.png)

After opening the results view, Mastodon shows the per-option counts derived
from `replies.totalItems`:

![Screenshot: A multiple-choice question object rendered as poll results in
Mastodon](pragmatics/mastodon-question-multi.png)

### `Mention`: Mentioned accounts

The `Mention` type is usually placed in an object's `tags` property to indicate
that a link in `content` points to another actor. In Mastodon, this makes the
linked handle render as a mention of the remote account.

~~~~ typescript twoslash
import { Mention, Note } from "@fedify/vocab";
// ---cut-before---
new Note({
content: // [!code highlight]
'<p>Hello <a class="mention u-url" href="https://example.com/users/friend">' +
'@friend@example.com</a>!</p>',
tags: [ // [!code highlight]
new Mention({
href: new URL("https://example.com/users/friend"), // [!code highlight]
name: "@friend@example.com", // [!code highlight]
}),
],
})
~~~~

> [!NOTE]
> In practice, you should keep the anchor in `content` and the `Mention` object
> in `tags` aligned with each other. If they point to different actors,
> ActivityPub implementations may render or notify inconsistently.
> For Mastodon-compatible mention rendering, the anchor in `content` should use
> `class="mention u-url"` so the link is treated as a mention instead of a
> generic profile URL.

For example, the above `Mention` object is displayed like the following in
Mastodon:

![Screenshot: A rendered mention in Mastodon](pragmatics/mastodon-mention.png)

### `Hashtag`: Clickable hashtags

The `Hashtag` type is also commonly placed in `tags`. In Mastodon, it turns a
matching hashtag in `content` into a clickable tag link.

~~~~ typescript twoslash
import { Hashtag, Note } from "@fedify/vocab";
// ---cut-before---
new Note({
content: // [!code highlight]
'<p>This note demonstrates <a class="mention hashtag" rel="tag" ' + // [!code highlight]
'href="https://example.com/tags/fedify">' +
'#fedify</a>.</p>',
tags: [ // [!code highlight]
new Hashtag({
href: new URL("https://example.com/tags/fedify"), // [!code highlight]
name: "#fedify", // [!code highlight]
}),
],
})
~~~~

> [!NOTE]
> For Mastodon-compatible hashtag rendering, the anchor in `content` should use
> `class="mention hashtag"` and `rel="tag"`. Without those attributes,
> Mastodon is more likely to treat it as a plain link instead of a hashtag.

For example, the above `Hashtag` object is displayed like the following in
Mastodon:

![Screenshot: A rendered hashtag in Mastodon](pragmatics/mastodon-hashtag.png)

If you click the hashtag in Mastodon, it opens a hashtag-specific menu instead
of behaving like a plain external link:

![Screenshot: A Mastodon hashtag menu opened from a rendered hashtag](pragmatics/mastodon-hashtag-dropdown.png)

### `Emoji`: Custom emoji

The `Emoji` type represents a custom emoji. In Mastodon, the shortcode in
`content` is replaced with the image from `icon` when a matching `Emoji`
object is present in `tags`.

~~~~ typescript twoslash
import { Emoji, Image, Note } from "@fedify/vocab";
// ---cut-before---
new Note({
content: "<p>Let us celebrate with :fedify_party:.</p>", // [!code highlight]
tags: [ // [!code highlight]
new Emoji({
name: ":fedify_party:", // [!code highlight]
icon: new Image({
url: new URL("https://example.com/emojis/fedify-party.png"), // [!code highlight]
mediaType: "image/png", // [!code highlight]
}),
}),
],
})
~~~~

> [!NOTE]
> The shortcode in `content` should match `Emoji.name` exactly, usually in the
> `:shortcode:` form. If there is no matching `Emoji` object in `tags`, the
> shortcode is displayed as plain text.

For example, the above `Emoji` object is displayed like the following in
Mastodon:

![Screenshot: A rendered custom emoji in Mastodon](pragmatics/mastodon-emoji.png)
Binary file added docs/manual/pragmatics/mastodon-article.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/manual/pragmatics/mastodon-emoji.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/manual/pragmatics/mastodon-hashtag.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/manual/pragmatics/mastodon-mention.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/manual/pragmatics/mastodon-note-body.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/manual/pragmatics/mastodon-note.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/manual/pragmatics/mastodon-question.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading