Skip to content

Content Model

Slingshot uses a shared content model across chat messages, community threads, and community replies. Every content entity carries the same field vocabulary so front-end clients can use a single rendering pipeline.

A content entity renders in this order:

  1. Quote — if quotedMessageId / quotedReplyId is present, render the quotePreview block above the body.
  2. Body — the main text, interpreted according to format.
  3. Attachments — file/media previews from the attachments array.
  4. Embeds — link previews from the embeds array.
  5. Poll — if pollId is present, render the embedded poll widget.
ValueBehavior
'markdown' (default)Parse body as GitHub-flavored Markdown, then parse inline tokens in text runs.
'plain'Render body as literal text — no Markdown parsing. Inline tokens (<@userId>, <@&roleId>, etc.) are still parsed.

Tokens are inline markers embedded in the body text. They are parsed by parseContentTokens() from @lastshotlabs/slingshot-core/content.

TokenSyntaxExamplePurpose
User mention<@userId><@user-123>Mention a specific user.
Broadcast mention<@everyone> or <@here><@everyone>Mention all members or online members.
Role mention<@&roleId><@&moderator>Mention all users with a role.
Context reference<#contextId><#channel-abc>Link to a room/container/thread.
Emoji shortcode:shortcode::wave:Render an emoji by name.

Tokens inside code spans (`...`) and fenced code blocks are not parsed.

FieldTypeNotes
bodystringRequired. Max 8,000 chars.
typeenum'text' | 'image' | 'file' | 'gif' | 'sticker' | 'voice' | 'location' | 'contact' | 'system'
formatenum'plain' | 'markdown' (default)
mentionsstring[]User IDs mentioned. Client-provided or parsed from body.
broadcastMentions('everyone'|'here')[]Broadcast mention tokens.
mentionedRoleIdsstring[]Role IDs mentioned.
attachmentsAssetRef[]Uploaded files/media. Max 10.
embedsEmbedData[]Server-resolved link previews.
quotedMessageIdstringID of the quoted message (immutable).
quotePreviewQuotePreviewSnapshot of quoted message body + author.
pollIdstringAttached poll entity ID (immutable).
stickerIdstringSticker asset ID (immutable).
locationLocationDataGeolocation for type: 'location'.
contactContactDataContact card for type: 'contact'.
systemEventSystemEventDataEvent payload for type: 'system'.
appMetadataRecord<string, unknown>Opaque application metadata.
FieldTypeNotes
bodystring?Optional.
formatenum'plain' | 'markdown' (default)
mentionsstring[]User IDs mentioned.
broadcastMentions('everyone'|'here')[]Broadcast mention tokens.
mentionedRoleIdsstring[]Role IDs mentioned.
attachmentsAssetRef[]Uploaded files/media.
embedsEmbedData[]Link previews.
pollIdstringAttached poll entity ID (immutable).
FieldTypeNotes
bodystringRequired.
formatenum'plain' | 'markdown' (default)
mentionsstring[]User IDs mentioned.
broadcastMentions('everyone'|'here')[]Broadcast mention tokens.
mentionedRoleIdsstring[]Role IDs mentioned.
attachmentsAssetRef[]Uploaded files/media.
embedsEmbedData[]Link previews.
quotedReplyIdstringID of the quoted reply (immutable).
quotePreviewQuotePreviewSnapshot of quoted reply body + author.

All content-model types are exported from @lastshotlabs/slingshot-core:

import type {
AssetRef,
ContactData,
ContentFormat,
EmbedData,
LocationData,
QuotePreview,
SystemEventData,
VoiceMetadata,
} from '@lastshotlabs/slingshot-core';

Validation schemas (Zod) are on the /content subpath:

import {
assetRefSchema,
contactDataSchema,
embedDataSchema,
locationDataSchema,
quotePreviewSchema,
systemEventDataSchema,
} from '@lastshotlabs/slingshot-core/content';

Constants:

import {
// 10
MAX_CONTENT_ATTACHMENTS,
// 8,000
MAX_CONTENT_BODY_LENGTH,
// 50
MAX_CONTENT_MENTIONS,
} from '@lastshotlabs/slingshot-core';
import { parseContentTokens } from '@lastshotlabs/slingshot-core/content';
const message = { body: 'Hello <@user-123>' };
const parsed = parseContentTokens(message.body);
// parsed.segments — array of ContentSegment
// parsed.mentionedUserIds — string[]
// parsed.broadcastMentions — ('everyone' | 'here')[]

Walk the segments array. Each segment has a type discriminator:

Segment typeRendering
textRender as plain text (or Markdown if format === 'markdown').
mentionRender as a user mention chip. Look up display name by userId.
broadcastMentionRender as @everyone or @here badge.
roleMentionRender as a role mention chip.
contextRefRender as a clickable link to the referenced room/container.
emojiRender the shortcode as an emoji image or native character.
codeSpanRender as inline code.
codeBlockRender as a fenced code block with optional language tag.

After the body, render in order: attachments → embeds → poll.

When content is indexed by slingshot-search, the body is stripped of tokens via stripContentTokens() before indexing. This ensures that <@user-123> does not pollute full-text search results. The raw token data is preserved in the mentions, broadcastMentions, and mentionedRoleIds fields for filtered search.