339 lines
8.8 KiB
TypeScript
339 lines
8.8 KiB
TypeScript
/**
|
||
* Postiz platform specifications.
|
||
*
|
||
* Character limits, required fields, media rules and postType defaults per
|
||
* Postiz provider identifier. Drives the planner palette and inline editor:
|
||
* dropping a platform onto the canvas creates a `post` node pre-tagged with
|
||
* that platform's constraints.
|
||
*
|
||
* Limits reflect Postiz 5.x provider handlers as of 2026-04. Numbers match
|
||
* what Postiz's `settings.__type` handlers enforce server-side.
|
||
*/
|
||
|
||
export type PostizPlatformId =
|
||
| 'x'
|
||
| 'linkedin'
|
||
| 'facebook'
|
||
| 'instagram'
|
||
| 'threads'
|
||
| 'bluesky'
|
||
| 'mastodon'
|
||
| 'youtube'
|
||
| 'tiktok'
|
||
| 'reddit'
|
||
| 'pinterest'
|
||
| 'discord'
|
||
| 'slack';
|
||
|
||
export type PostizMediaKind = 'image' | 'video' | 'gif';
|
||
export type PostizPostType =
|
||
| 'text'
|
||
| 'thread'
|
||
| 'image'
|
||
| 'carousel'
|
||
| 'video'
|
||
| 'short'
|
||
| 'story'
|
||
| 'reel'
|
||
| 'link';
|
||
|
||
export interface PlatformSpec {
|
||
id: PostizPlatformId;
|
||
label: string;
|
||
icon: string;
|
||
color: string;
|
||
/** Main content character limit (per segment for threads). */
|
||
charLimit: number;
|
||
/** Optional title/subject limit — rendered as a separate input when set. */
|
||
titleLimit?: number;
|
||
titleRequired?: boolean;
|
||
/** Supports a native multi-segment thread via Postiz `value[]`. */
|
||
supportsThreads: boolean;
|
||
/** Media types the platform accepts (drives the inline editor hint). */
|
||
mediaTypes: PostizMediaKind[];
|
||
/** Maximum media items per post. 0 = media not allowed. */
|
||
maxMedia: number;
|
||
/** Media is required (cannot post text-only). */
|
||
mediaRequired?: boolean;
|
||
/** Default postType when dropped on the canvas. */
|
||
defaultPostType: PostizPostType;
|
||
/** Extra postTypes selectable for this platform. */
|
||
postTypes: PostizPostType[];
|
||
/** Hashtag count limit (soft warning when exceeded). 0 = no hashtags. */
|
||
hashtagLimit: number;
|
||
/** Human-readable note displayed under the content editor. */
|
||
note: string;
|
||
/** Extra per-platform input fields rendered in the inline editor. */
|
||
extraFields?: PlatformExtraField[];
|
||
}
|
||
|
||
export interface PlatformExtraField {
|
||
key: string;
|
||
label: string;
|
||
type: 'text' | 'select' | 'textarea' | 'checkbox';
|
||
placeholder?: string;
|
||
options?: string[];
|
||
required?: boolean;
|
||
maxLength?: number;
|
||
}
|
||
|
||
export const PLATFORM_SPECS: Record<PostizPlatformId, PlatformSpec> = {
|
||
x: {
|
||
id: 'x',
|
||
label: 'X (Twitter)',
|
||
icon: '𝕏',
|
||
color: '#000000',
|
||
charLimit: 280,
|
||
supportsThreads: true,
|
||
mediaTypes: ['image', 'video', 'gif'],
|
||
maxMedia: 4,
|
||
defaultPostType: 'text',
|
||
postTypes: ['text', 'thread', 'image', 'video'],
|
||
hashtagLimit: 3,
|
||
note: '280 chars/tweet. Threads supported. Up to 4 media per tweet.',
|
||
extraFields: [
|
||
{
|
||
key: 'who_can_reply_post', label: 'Who can reply',
|
||
type: 'select', options: ['everyone', 'following', 'mentionedUsers'],
|
||
},
|
||
],
|
||
},
|
||
linkedin: {
|
||
id: 'linkedin',
|
||
label: 'LinkedIn',
|
||
icon: 'in',
|
||
color: '#0A66C2',
|
||
charLimit: 3000,
|
||
supportsThreads: false,
|
||
mediaTypes: ['image', 'video'],
|
||
maxMedia: 9,
|
||
defaultPostType: 'text',
|
||
postTypes: ['text', 'image', 'carousel', 'video'],
|
||
hashtagLimit: 5,
|
||
note: '3000 chars. Up to 9 images or 1 video. 3–5 hashtags recommended.',
|
||
extraFields: [
|
||
{ key: 'post_to_company', label: 'Post as company page', type: 'checkbox' },
|
||
],
|
||
},
|
||
facebook: {
|
||
id: 'facebook',
|
||
label: 'Facebook',
|
||
icon: 'f',
|
||
color: '#1877F2',
|
||
charLimit: 63206,
|
||
supportsThreads: false,
|
||
mediaTypes: ['image', 'video'],
|
||
maxMedia: 10,
|
||
defaultPostType: 'text',
|
||
postTypes: ['text', 'image', 'video', 'link'],
|
||
hashtagLimit: 6,
|
||
note: 'Effectively unlimited text. First 250 chars show in preview.',
|
||
},
|
||
instagram: {
|
||
id: 'instagram',
|
||
label: 'Instagram',
|
||
icon: '📷',
|
||
color: '#E4405F',
|
||
charLimit: 2200,
|
||
supportsThreads: false,
|
||
mediaTypes: ['image', 'video'],
|
||
maxMedia: 10,
|
||
mediaRequired: true,
|
||
defaultPostType: 'image',
|
||
postTypes: ['image', 'carousel', 'reel', 'story'],
|
||
hashtagLimit: 30,
|
||
note: '2200-char caption. Media required. Up to 10 items in a carousel.',
|
||
extraFields: [
|
||
{
|
||
key: 'post_type', label: 'Post type',
|
||
type: 'select', options: ['post', 'reel', 'story'],
|
||
},
|
||
],
|
||
},
|
||
threads: {
|
||
id: 'threads',
|
||
label: 'Threads',
|
||
icon: '@',
|
||
color: '#000000',
|
||
charLimit: 500,
|
||
supportsThreads: true,
|
||
mediaTypes: ['image', 'video'],
|
||
maxMedia: 10,
|
||
defaultPostType: 'text',
|
||
postTypes: ['text', 'thread', 'image'],
|
||
hashtagLimit: 1,
|
||
note: '500 chars/post. Threads supported. One hashtag per post.',
|
||
},
|
||
bluesky: {
|
||
id: 'bluesky',
|
||
label: 'Bluesky',
|
||
icon: '🦋',
|
||
color: '#0085FF',
|
||
charLimit: 300,
|
||
supportsThreads: true,
|
||
mediaTypes: ['image', 'gif'],
|
||
maxMedia: 4,
|
||
defaultPostType: 'text',
|
||
postTypes: ['text', 'thread', 'image'],
|
||
hashtagLimit: 3,
|
||
note: '300 chars/post. Threads supported. Up to 4 images.',
|
||
},
|
||
mastodon: {
|
||
id: 'mastodon',
|
||
label: 'Mastodon',
|
||
icon: '🐘',
|
||
color: '#6364FF',
|
||
charLimit: 500,
|
||
supportsThreads: true,
|
||
mediaTypes: ['image', 'video', 'gif'],
|
||
maxMedia: 4,
|
||
defaultPostType: 'text',
|
||
postTypes: ['text', 'thread', 'image'],
|
||
hashtagLimit: 10,
|
||
note: '500 chars/toot (instance-dependent). Threads via replies.',
|
||
extraFields: [
|
||
{
|
||
key: 'visibility', label: 'Visibility',
|
||
type: 'select', options: ['public', 'unlisted', 'private', 'direct'],
|
||
},
|
||
{ key: 'spoiler_text', label: 'Content warning', type: 'text', placeholder: 'Optional CW' },
|
||
],
|
||
},
|
||
youtube: {
|
||
id: 'youtube',
|
||
label: 'YouTube',
|
||
icon: '▶️',
|
||
color: '#FF0000',
|
||
charLimit: 5000,
|
||
titleLimit: 100,
|
||
titleRequired: true,
|
||
supportsThreads: false,
|
||
mediaTypes: ['video'],
|
||
maxMedia: 1,
|
||
mediaRequired: true,
|
||
defaultPostType: 'video',
|
||
postTypes: ['video', 'short'],
|
||
hashtagLimit: 15,
|
||
note: 'Title ≤100 chars. Description ≤5000. Video required.',
|
||
extraFields: [
|
||
{
|
||
key: 'type', label: 'Visibility',
|
||
type: 'select', options: ['public', 'unlisted', 'private'],
|
||
},
|
||
{
|
||
key: 'category', label: 'Category ID',
|
||
type: 'select',
|
||
options: ['1', '2', '10', '15', '17', '19', '20', '22', '23', '24', '25', '26', '27', '28'],
|
||
},
|
||
],
|
||
},
|
||
tiktok: {
|
||
id: 'tiktok',
|
||
label: 'TikTok',
|
||
icon: '🎵',
|
||
color: '#000000',
|
||
charLimit: 2200,
|
||
titleLimit: 150,
|
||
supportsThreads: false,
|
||
mediaTypes: ['video'],
|
||
maxMedia: 1,
|
||
mediaRequired: true,
|
||
defaultPostType: 'video',
|
||
postTypes: ['video'],
|
||
hashtagLimit: 10,
|
||
note: 'Caption ≤2200 chars. Video required. Short-form vertical.',
|
||
extraFields: [
|
||
{
|
||
key: 'privacy_level', label: 'Privacy',
|
||
type: 'select', options: ['PUBLIC_TO_EVERYONE', 'MUTUAL_FOLLOW_FRIENDS', 'SELF_ONLY'],
|
||
},
|
||
{ key: 'disable_duet', label: 'Disable duets', type: 'checkbox' },
|
||
{ key: 'disable_comment', label: 'Disable comments', type: 'checkbox' },
|
||
],
|
||
},
|
||
reddit: {
|
||
id: 'reddit',
|
||
label: 'Reddit',
|
||
icon: '🔶',
|
||
color: '#FF4500',
|
||
charLimit: 40000,
|
||
titleLimit: 300,
|
||
titleRequired: true,
|
||
supportsThreads: false,
|
||
mediaTypes: ['image', 'video'],
|
||
maxMedia: 1,
|
||
defaultPostType: 'text',
|
||
postTypes: ['text', 'link', 'image', 'video'],
|
||
hashtagLimit: 0,
|
||
note: 'Title ≤300 chars. Hashtags unused. Subreddit required.',
|
||
extraFields: [
|
||
{ key: 'subreddit', label: 'Subreddit', type: 'text', required: true, placeholder: 'r/example' },
|
||
{
|
||
key: 'flair', label: 'Flair (optional)',
|
||
type: 'text', placeholder: 'Flair text',
|
||
},
|
||
],
|
||
},
|
||
pinterest: {
|
||
id: 'pinterest',
|
||
label: 'Pinterest',
|
||
icon: '📌',
|
||
color: '#E60023',
|
||
charLimit: 500,
|
||
titleLimit: 100,
|
||
supportsThreads: false,
|
||
mediaTypes: ['image', 'video'],
|
||
maxMedia: 1,
|
||
mediaRequired: true,
|
||
defaultPostType: 'image',
|
||
postTypes: ['image', 'video'],
|
||
hashtagLimit: 20,
|
||
note: 'Pin description ≤500. Title ≤100. Image or video required.',
|
||
extraFields: [
|
||
{ key: 'link', label: 'Destination URL', type: 'text', placeholder: 'https://' },
|
||
{ key: 'board', label: 'Board', type: 'text', placeholder: 'Board name' },
|
||
],
|
||
},
|
||
discord: {
|
||
id: 'discord',
|
||
label: 'Discord',
|
||
icon: '🎮',
|
||
color: '#5865F2',
|
||
charLimit: 2000,
|
||
supportsThreads: false,
|
||
mediaTypes: ['image', 'video', 'gif'],
|
||
maxMedia: 10,
|
||
defaultPostType: 'text',
|
||
postTypes: ['text', 'image'],
|
||
hashtagLimit: 0,
|
||
note: '2000 chars per message. Posts into a configured channel.',
|
||
},
|
||
slack: {
|
||
id: 'slack',
|
||
label: 'Slack',
|
||
icon: '#',
|
||
color: '#4A154B',
|
||
charLimit: 40000,
|
||
supportsThreads: false,
|
||
mediaTypes: ['image'],
|
||
maxMedia: 10,
|
||
defaultPostType: 'text',
|
||
postTypes: ['text'],
|
||
hashtagLimit: 0,
|
||
note: 'Long-form message into a configured channel.',
|
||
},
|
||
};
|
||
|
||
export const ALL_PLATFORM_IDS: PostizPlatformId[] = [
|
||
'x', 'linkedin', 'facebook', 'instagram', 'threads', 'bluesky',
|
||
'mastodon', 'youtube', 'tiktok', 'reddit', 'pinterest', 'discord', 'slack',
|
||
];
|
||
|
||
export function getPlatformSpec(platform: string): PlatformSpec | null {
|
||
return (PLATFORM_SPECS as Record<string, PlatformSpec>)[platform] || null;
|
||
}
|
||
|
||
export function charLimitFor(platform: string): number {
|
||
return getPlatformSpec(platform)?.charLimit ?? 2200;
|
||
}
|