Posts
Hashtags and locations
How a post gets its hashtags and its locations, how many it can have, and the hashtags offered to somebody while they write.
How a post gets its hashtags
A hashtag is a # and the letters after it, at the start of the text or
after a space: letters of any language, digits, _ and common emoji
(entities.HashtagRegexPattern). a#b is not one.
hashtag-service reads the text of a post each time it is added or changed,
and files the post under each hashtag in it. A hashtag that does not exist
yet is made then. Capitals don't matter: #Anahita and #anahita are one
hashtag, found at /hashtags/anahita, and shown as the first post to use it
wrote it.
A post from another server gets its hashtags the same way, from its text; see Federation.
At most ten hashtags
A post is filed under its first ten different hashtags, counted in the order they are written, whatever their capitals. The rest stay in the text as plain words: not links, not filed, not sent as tags.
| Where | What happens |
|---|---|
| hashtag-service | Files the post under the first ten |
| The web app | Links the first ten in the text, and says so while writing past them |
| ActivityPub | Sends the first ten as links and as the note's tags |
| Bluesky | Marks the first ten as tags in the post's facets |
Nothing is refused. A post with more is saved whole, so an old post with fifteen hashtags can still be edited. A post from another server with more arrives whole too, and is found under its first ten only.
The number is constants.MaxHashtagsOfAPost in anahita-libs, and
Medium.MAX_HASHTAGS in the web app. Ten is also how many hashtags a custom
feed can filter on (feeds.MaxHashtags). For comparison: Mastodon, Pixelfed
and the other fediverse servers set no limit in code; Bluesky allows eight
extra tags outside a post's text, and no limit in it.
At most five locations
A post can be tied to at most five locations. Locations are added one at a
time (POST /locations/:id/inbounds/), so a sixth is refused with
422 and {"error": "too_many_locations"}. Tying a post to a location it
already has is not a sixth.
The number is constants.MaxLocationsOfAPost in anahita-libs, and
Medium.MAX_LOCATIONS in the web app. Five is also how many locations a custom
feed can filter on (feeds.MaxLocations).
A post's locations on other servers
A location chosen for a post in the composer is tied to it as it is made
(location_ids on adding it, up to five) and goes with it to other servers,
when the post goes: the first as its location, all as Places in its
tag. Adding or taking off a location on its page sends it again. See
Federation.
Who may add a location
A hashtag is made by writing it. A location is added by somebody, with a name and a point, and who may add one is the installation's choice:
LOCATIONS_FROM |
Who may add a location |
|---|---|
administrators (the default) |
Administrators and super administrators |
registered |
Anybody signed in |
super-administrators |
Super administrators |
location-service enforces it, and auth-service publishes it in NodeInfo as
locationsFrom, from which the web app offers Add only to those it lets
add one; give both services the same value. Editing and deleting a location
stay with administrators whatever it says. Anybody signed in may tie a post
of theirs to a location that exists. See
Configuration.
Trending
GET /hashtags/_browse/trending and GET /locations/_browse/trending list
what the most profiles used lately, and how many (profiles, shown as
"3 profiles").
- Each profile once, of every kind. A profile uses a tag by writing a post that carries it, or by being tagged with it: a person, a group, an event or a website, here or on another server. One profile posting a hashtag twenty times counts as one, so nobody can make something trend alone. Ties go to the one on more posts and profiles.
- By when a post was written, not when it arrived, and a profile by when it was tagged, not when it was made. A profile's older posts fetched when it is followed don't trend that day.
- Public only: posts anybody may see, on profiles anybody may see, and public profiles, so what trends says nothing about what is kept from view.
- Over a window the installation chooses (
TRENDING_WINDOW, see Configuration): daily where it federates, monthly where only its own members post. A window with fewer than ten is widened, to a week, a month, then a year, and the answer'swindowsays which it is of. The web app shows it under the tabs.
Suggestions while writing
hashtag-service offers hashtags already in use to somebody signed in who is writing. There are two ways to ask:
| Request | For | Offers |
|---|---|---|
GET /hashtags/_suggest?q=ana |
Typing a hashtag | Those that begin with q (what follows the #) |
POST /hashtags/_suggest with {"title": "…", "content": "…"} |
Long content | Those that are words of the title and content |
Both take limit, 1 to 20, and offer 8 when it is not given. Both answer
with the best first:
{
"data": [
{ "id": 138587, "alias": "anahita", "name": "anahita", "uses": 1031, "followed": false }
]
}
aliasis the hashtag in lower case;nameis how it is shown.usesis how many posts it is on that anybody may see, plus the person's own.followedis true when it is in one of the person's custom feeds, which is what following a hashtag is.
The order. Hashtags the person follows come first. For a typed start, the hashtag that is exactly what was typed comes next, then the most used. For a text, the words the text uses most come next, then the most used.
What is counted. Only posts anybody may see (public, published, switched on, on a profile anybody may see) and the person's own. So a hashtag used only where the person can't look is never offered to them, and what is offered says nothing about what is kept from view.
Words of a text. The title and body are split into words of three
letters or more, and each two words next to each other are also tried run
together, so "open science" finds #OpenScience. Addresses, code,
mentions and hashtags already written are not words. A word is offered only
when it is a hashtag on more than one post, and hashtags the text already
has are left out. The first 100,000 characters are read.
Matching ignores capitals and accents: cafe finds #café. % and _
are matched as themselves.
A hashtag's and a location's posts
GET /hashtags/:id/inbounds and GET /locations/:id/inbounds list what is
tied to a hashtag or a location, in one of two orders, by sort:
sort |
Order |
|---|---|
recent |
Newest first. The web app's first tab, and its default |
relevant |
What people have found useful lately, as search's Most relevant ranks it, without the words: likes, saves, replies and quotes, each fading by the operator's half-life, citations, and a bonus for what the viewer follows, with the viewer's own leanings. Newest first among equals |
popular, an all-time sum of a post's counts, is still taken, and the web
app no longer asks for it: an old post that did well stayed on top for good.
What the web app does
- While typing a hashtag, in the short box, a reply and long content,
hashtags in use that begin with what was typed are offered under the line
being written, or above it when there is no room (on a phone, the
keyboard takes the bottom of the screen). They float over the page, so
nothing under them moves.
- On a phone: a row of chips; one tap puts one in.
- On a desktop: a short list with how many posts each is on; the arrow keys move through it, Enter or Tab puts one in, Escape puts it away.
- Under the long content editor, a row of suggested hashtags made from the title and content, asked for a moment after writing stops once there are 80 characters or more. One tap adds a hashtag at the end, on the line of hashtags the text ends with, or on a line of its own.
- At ten hashtags, both say "Only the first 10 hashtags count" instead of offering more.
- At five locations, a post's Locations card says a post can have up to 5 locations in place of its add button.
Where the code is
anahita-libs/constants/tags.go: the two caps.hashtag-service/cmd/api/handlers/hashtag_events.go: filing a post under its hashtags.hashtag-service/cmd/api/handlers/suggest.go: the two suggestion requests, and the words of a text (wordsOf).graph-grpc-service/data/repositories/hashtag_suggestions.go: which hashtags are offered, and in what order (HashtagService.Suggestinproto/graphpb/hashtag.proto).location-service/cmd/api/handlers/inbound.go: the cap on locations.anahita-libs/federation/content.goandatproto-service/bsky/outgoing.go: the cap in what is sent to other servers.
From anahitasocial/anahita-services, 2026-10-27.
Written by Claude, reviewed by Rastin Mehr · About this