Skip to main content

Hashtags & Trending — Technical Reference

← Back to Hashtags & Trending

Where this lives

Backend

  • apps/backend/graphql/resolvers/hashtag.resolver.js — resolves hashtag, hashtagByName, searchHashtags, trendingHashtags, hashtagStats
  • apps/backend/graphql/types/hashtag.type.jsHashtag SDL type and query schema
  • apps/backend/managers/post-managers/hashtag.manager.js — trending score calculation, search, and lookup logic
  • apps/backend/data-access-services/post/hashtag.access-service.js — hashtag DB access (moved under data-access-services/post/ along with the other post-related access services)
  • apps/backend/graphql/resolvers/user-mentions-tags.resolver.js — implements usersMentionedBy, usersIMentioned, removeTag, removeMention (the monolithic user.resolver.js this doc previously pointed at no longer exists — the user resolvers were split into many files)
  • Verified but worth flagging: post-hashtag.access-service.js (now data-access-services/post/post-hashtag.access-service.js) and conversation-hashtag.access-service.js (now data-access-services/message/conversation-hashtag.access-service.js) are wired — post.manager.js#createPost links each extracted hashtag via the former (this doc previously said neither was called by any manager — corrected). Separately, removeTag's underlying mentions-tags.manager.js methods getUsersMentionedBy/getUsersIMentioned are still TODO/mock stubs that return empty data regardless of input (removeTag itself was fixed this session — see checklist below). myMentions, mentionsInContent, mentionUser, and tagUser no longer exist in the schema at all (removed — see checklist below), so ignore any references to them below as historical.

Frontend

Technical implementation checklist

  • trendingHashtags — wired in hashtag.resolver.js, calls hashtag.manager.js#getTrendingHashtags; consumed by HashtagPage.tsx's "Trending" chips row (this doc previously said there was no frontend caller — corrected)
  • searchHashtags — wired in hashtag.resolver.js; consumed by HomePage.tsx's mobile search overlay
  • Hashtag auto-extraction on post creation — post.manager.js:217 calls hashtagManager.extractAndCreateHashtags; combined with getByHashtag/postsByHashtag, this backs the real /hashtag/[tag] feed (HashtagPage.tsx) and the clickable hashtag links PostCard.tsx renders via linkifyText — see Posts & Feed
  • hashtagByName / hashtagStats — wired in hashtag.resolver.js; consumed by HashtagPage.tsx's header (total posts / distinct people) — the hashtag feed's post grid itself still uses postsByHashtag, not these two (this doc previously said neither had a frontend caller — corrected)
  • PostHashtag (posts) and ConversationHashtag (in-chat) are both wired — post.manager.js#createPost links each extracted hashtag via post-hashtag.access-service.js; message.manager.js#createMessage extracts #hashtags from message text, links them to the conversation via conversation-hashtag.access-service.js (de-duplicated) and bumps usage; hashtag.access-service.js#getTrending counts recent conversation-hashtag links alongside post links, so in-chat hashtags surface in trending too.
  • usersMentionedBy / usersIMentioned — wired in user-mentions-tags.resolver.js; consumed by Settings → Mention history (MentionHistoryPage.tsx, at /settings/mentions) — but the underlying mentions-tags.manager.js methods are still TODO/mock stubs that always return an empty result, so the tabs render and query correctly but never show real mention data
  • removeTagFixed this session, two bugs: (1) the resolver used to destructure a non-existent tagId argument, so every call threw regardless of input — it now reads the real userId/contentType/contentId args the SDL declares and returns the real boolean from the manager. (2) mentions-tags.manager.js#removeTag was a stub (console.log, no DB write) — it now does a real PostMention deletion, gated to POST content only, and authorized to either the tagged user or the post's owner (looked up via postAccessService.findById); anyone else gets permissions.cannot_remove_tag. removeMention(mentionId) remains a separate, simpler mutation for the same underlying PostMention row, backed by post-mention.manager.js#deleteMention.
  • myMentions / mentionsInContent / mentionUser / tagUserremoved from the mentions-tags.type.js SDL. They were dead fields with no resolver and no frontend consumer, duplicating concepts already covered by the real PostMention-based tagging system (tap-to-tag + caption @mentions, recorded via post-mention.access-service.js), so they were deleted rather than implemented to keep the schema honest.

Hashtag model

FieldDescription
nameOriginal hashtag (mixed case)
normalizedNameLowercase normalized version
usageCountTotal usage count
trendingScoreCalculated trending score
lastUsedAtMost recent usage timestamp

Queries

trendingHashtags returns the top-N hashtags sorted by trendingScore, which combines recent velocity with overall usage. Use it for the Explore page's trending row.

searchHashtags autocompletes a partial hashtag string — call it on every keystroke as the user types # in the post composer. Results are sorted by usageCount.

hashtagByName fetches a single hashtag by its exact name — useful when navigating to a hashtag page (e.g. /hashtag/photography).

hashtagStats returns the total number of posts and unique users that have used a hashtag — displayed on the hashtag detail page.

query TrendingHashtags($limit: Int) {
trendingHashtags(limit: $limit) {
id name usageCount trendingScore lastUsedAt
}
}

query SearchHashtags($query: String!, $limit: Int) {
searchHashtags(query: $query, limit: $limit) { id name usageCount }
}

query HashtagByName($name: String!) {
hashtagByName(name: $name) { id name usageCount trendingScore }
}

query HashtagStats($hashtagId: ID!) {
hashtagStats(hashtagId: $hashtagId) { totalPosts totalUsers }
}

Hashtags in conversations

Messages can also carry hashtags. The ConversationHashtag table indexes them for in-chat search.

User mentions

myMentions, mentionsInContent, mentionUser, and tagUser do not exist in the schema — they were removed (see checklist above); ignore any older references to them.

removeMention deletes a specific PostMention row and is the mutation that actually works end-to-end — either the post's owner or the mentioned user may call it (post-mention.manager.js#deleteMention). removeTag (userId/contentType/contentId args — contentType must be POST, the only content type tags are modeled for today) also now works end-to-end: it deletes the matching PostMention row, restricted to the tagged user or the post's owner — see the checklist above for the two bugs fixed this session.

# Remove a specific mention (post owner or the mentioned user can call this)
mutation RemoveMention($mentionId: ID!) {
removeMention(mentionId: $mentionId)
}

# Tagged user or the post's owner can call this
mutation RemoveTag($userId: ID!, $contentType: String!, $contentId: ID!) {
removeTag(userId: $userId, contentType: $contentType, contentId: $contentId)
}

Content tags

ContentTag is a system-managed label applied automatically by AI classification — distinct from user-authored hashtags. Tags describe the topic category of posts and conversations.

FieldDescription
nameTag label (e.g. photography, sports)
categoryTag category group
usageCountNumber of items tagged
confidenceAvgAverage AI confidence score (0–1)

Tags are applied to posts via PostContentTag and to conversations via ConversationContentTag. They power the content interest engine — see Posts & Feed for how user affinity is tracked per tag.

Mention history

usersMentionedBy shows which users a given account tends to mention — useful for an analytics or relationship view. usersIMentioned shows who the current user has mentioned recently. Both are consumed by Settings → Mention history (MentionHistoryPage.tsx), but their backing mentions-tags.manager.js implementation is still a TODO/mock stub that always returns an empty list — the tabs are fully wired end-to-end but won't display real mention data until that manager is implemented against PostMention/MessageMention.

query UsersMentionedBy($userId: ID!, $limit: Int) {
usersMentionedBy(userId: $userId, limit: $limit) { id username }
}

query UsersIMentioned($limit: Int) {
usersIMentioned(limit: $limit) { id username }
}