Vibefed kb-fediverse-hashtags
git clone https://github.com/reiver/vibefed
T=$(mktemp -d) && git clone --depth=1 https://github.com/reiver/vibefed "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/kb-fediverse-hashtags" ~/.claude/skills/reiver-vibefed-kb-fediverse-hashtags && rm -rf "$T"
skills/kb-fediverse-hashtags/SKILL.mdFediverse Hashtags — Complete Reference
Overview
Hashtags on the Fediverse work differently from centralised platforms. Because there is no global index, what a user sees under a hashtag is determined by what their particular server has observed — not a complete picture of the whole network.
1. The ActivityPub Wire Format
The Hashtag
type
HashtagHashtag is not a standard ActivityStreams 2.0 type. It was introduced by
Mastodon and placed in the as: namespace (https://www.w3.org/ns/activitystreams#Hashtag),
even though it was never formally added to the ActivityStreams context document.
It has since been formalised in the ActivityPub Miscellaneous Terms extension
(https://purl.archive.org/miscellany), but most implementations still declare
it via the as: namespace for compatibility.
The
Hashtag type is a subtype of Link, not of Object. This matters:
it should not have an id property in strict JSON-LD terms, though some
implementations add one anyway.
Declaring Hashtag
in @context
Hashtag@contextThere are two common patterns in the wild:
Pattern A — inline in
(Mastodon, Pixelfed style):@context
"@context": [ "https://www.w3.org/ns/activitystreams", { "Hashtag": "as:Hashtag" } ]
Pattern B — via the Miscellany extension:
"@context": [ "https://www.w3.org/ns/activitystreams", "https://purl.archive.org/miscellany" ]
Both resolve to the same URI. Pattern A is more common in existing implementations. Pattern B is the more formally correct approach.
Attaching a hashtag to a Note
NoteHashtags are expressed in the
tag array of an object (usually a Note).
Each entry is a Hashtag object with:
:type
(required)"Hashtag"
: URL of a page listing content with this tag (required — this is what makes it ahref
subtype)Link
: the hashtag string, conventionally including thename
prefix#
{ "@context": [ "https://www.w3.org/ns/activitystreams", { "Hashtag": "as:Hashtag" } ], "id": "https://example.com/notes/1", "type": "Note", "attributedTo": "https://example.com/users/alice", "content": "<p>Watching the eclipse! <a href='https://example.com/tags/eclipse2024' class='mention hashtag' rel='tag'>#<span>eclipse2024</span></a></p>", "tag": [ { "type": "Hashtag", "href": "https://example.com/tags/eclipse2024", "name": "#eclipse2024" } ] }
The tag
array can hold multiple types
tagThe
tag array is shared by Hashtag, Mention, and Emoji entries.
Implementations must check type to differentiate them:
"tag": [ { "type": "Mention", "href": "https://other.example/users/bob", "name": "@bob@other.example" }, { "type": "Hashtag", "href": "https://example.com/tags/cats", "name": "#cats" }, { "type": "Emoji", "id": "https://example.com/emoji/42", "name": ":blobcat:" } ]
The dual representation problem
Hashtags appear in two places in an ActivityPub object, and both must be consistent:
array — the structured metadata, used by receiving servers to index the post under that tagtag
field — the HTML body, where hashtags appear ascontent
links with<a>
andrel="tag"class="mention hashtag"
If the
tag array entry is missing, receiving servers may not index the post
under that hashtag at all, even if the #word appears visibly in the content.
If the content link is missing or malformed, the hashtag may not be
clickable or display correctly.
2. Case Sensitivity
Case handling is one of the most inconsistent areas of Fediverse hashtag implementation.
The
field vs. the name
field:href
- The
(and the path used in URLs likehref
) is typically lowercased/tags/eclipse2024 - The
field may preserve the original capitalisation the author typed (name
) or may be lowercased"#Eclipse2024"
Mastodon's behaviour:
- Stores tags lowercased internally
- The
field in the outgoing ActivityPub object reflects the database canonical form, not necessarily what the author typedname
is always lowercasehref
CamelCase for accessibility: Users are encouraged to write hashtags in CamelCase (
#DogsAtPollingStations
rather than #dogsatpollingstations) because screen readers can parse
individual words from CamelCase. This is a widely-promoted Fediverse
convention, not a technical requirement.
Interoperability implication: When processing incoming
Hashtag objects, implementations should normalise
the tag name to lowercase before indexing or comparing, since #Cats, #cats,
and #CATS all refer to the same tag.
3. Featured Hashtags
Mastodon allows users to pin hashtags to their profile. This is implemented as an extra
featuredTags property on the actor object pointing to a
Collection of Hashtag objects:
{ "id": "https://mastodon.example/@alice", "type": "Person", "featuredTags": "https://mastodon.example/@alice/collections/tags" }
The
featuredTags term is defined in the Mastodon namespace:
http://joinmastodon.org/ns#featuredTags.
4. How Hashtag Federation Works (and Why It's Incomplete)
This is the most important and most misunderstood aspect of Fediverse hashtags.
There is no global hashtag index
Unlike centralised platforms, no server has a complete view of all posts under a hashtag across the entire network. A server only indexes posts it has received.
A server receives a post if:
- A local user follows the author
- A local user has boosted/replied to the post (pulling it into a thread)
- The server subscribes to a relay that delivered the post
- The post was explicitly fetched (e.g., by pasting a URL into the search bar)
Following a hashtag is a local filter
When a user on Mastodon "follows" a hashtag, it is not a network-level subscription. It is a client-side filter on the server's federated timeline. The server does not send any ActivityPub messages to announce this follow. Posts under that hashtag only appear in the user's timeline if the server would have received them anyway.
Following hashtags does not cause the server to go out and fetch posts from other servers that use that hashtag. This is a common point of confusion.
Relays expand the picture
Relays are ActivityPub actors that re-broadcast public posts from many servers to subscribers. A server admin can subscribe to a relay to increase the volume of posts the server receives. Some relays (like FediBuzz) allow filtering by hashtag, so a server can subscribe to receive only posts tagged with specific hashtags. This is an admin-level operation, not something individual users can do.
The RSS workaround
Most Fediverse servers expose a public RSS feed for each hashtag timeline:
https://{server}/tags/{tagname}.rss
This can be used to subscribe to a hashtag's timeline on a specific server via any feed reader.
Some Fediverse servers instead expose a public Atom feed for each hashtag timeline:
https://{server}/tags/{tagname}.atom
5. Hashtag URLs and Tag Pages
The
href in a Hashtag object points to a human-readable page showing
posts with that tag. The URL convention varies by platform:
| Platform | Tag page URL pattern |
|---|---|
| Mastodon | |
| Pixelfed | |
| Misskey / Calckey | |
| PeerTube | |
| Pleroma / Akkoma | |
The
href should always link to the originating server's tag page, not to
any other server's.
6. Hashtag as an Actor (Experimental)
The W3C ActivityPub Primer describes an experimental approach where a
Hashtag object is given inbox and outbox properties, making it a
followable actor. This would allow true network-level hashtag subscription
via ActivityPub's follow mechanism:
{ "@context": [ "https://www.w3.org/ns/activitystreams", "https://purl.archive.org/miscellany" ], "type": "Hashtag", "id": "https://example.org/hashtag/cats", "href": "https://example.org/tags/cats", "name": "#cats", "inbox": "https://example.org/hashtag/cats/inbox", "outbox": "https://example.org/hashtag/cats/outbox" }
This is not widely implemented and has known problems:
Hashtag is a
Link subtype, making id and inbox/outbox properties unusual; and
many implementations only allow standard AS2 actor types (Person,
Organization, Service, etc.) to be followed.
7. Common Implementation Mistakes
- Missing
array entry: includingtag
in#hashtag
HTML but omitting the structuredcontent
entry inHashtag
— receiving servers may not index ittag - Wrong
host: pointinghref
at a different server's tag page rather than the originating server'shref - Including
in#
: thehref
URL path should not containhref
, which is a URL fragment character — use#
not/tags/cats/tags/#cats - Omitting
prefix: some implementations sendname
without the"name": "cats"
— conventions vary, but including#
is more common and matches Mastodon's behaviour# - Case mismatch:
is lowercase buthref
has mixed case — this is acceptable but can cause confusion; be consistentname - Not including
inHashtag
: sending@context
without declaring it in"type": "Hashtag"
means strict JSON-LD parsers will reject or ignore it@context
8. Summary: What a Correct Implementation Looks Like
A server that correctly implements hashtags should:
- Parse incoming
arrays, identify entries withtag
, and index the post under the normalised (lowercased) tag nametype: "Hashtag" - When creating a post with hashtags, populate both the
array (structured metadata) and thetag
HTML (linkedcontent
anchors)#tag - Declare
in theHashtag
of outgoing objects@context - Expose a tag timeline page at a stable URL (typically
)/tags/{name} - Set the
of eachhref
entry to point to that tag timeline pageHashtag - Optionally expose an RSS feed at
/tags/{name}.rss - Optionally expose an Atom feed at
/tags/{name}.atom - Accept incoming posts and index them under their hashtags so local users can search and follow those tags