generated from fahricansecer/boilerplate-be
289 lines
8.8 KiB
TypeScript
289 lines
8.8 KiB
TypeScript
// Platform Adapters Service - Transform content for different platforms
|
|
// Path: src/modules/content/services/platform-adapters.service.ts
|
|
|
|
import { Injectable, Logger } from '@nestjs/common';
|
|
import { SocialPlatform } from '@prisma/client';
|
|
|
|
export interface PlatformConfig {
|
|
name: string;
|
|
maxLength: number;
|
|
supportsMedia: boolean;
|
|
supportsLinks: boolean;
|
|
supportsHashtags: boolean;
|
|
supportsMentions: boolean;
|
|
supportsEmoji: boolean;
|
|
mediaFormats?: string[];
|
|
optimalLength?: number;
|
|
bestPostingTimes?: string[];
|
|
engagementTips?: string[];
|
|
}
|
|
|
|
export const PLATFORM_CONFIGS: Record<SocialPlatform, PlatformConfig> = {
|
|
TWITTER: {
|
|
name: 'X (Twitter)',
|
|
maxLength: 280,
|
|
optimalLength: 240,
|
|
supportsMedia: true,
|
|
supportsLinks: true,
|
|
supportsHashtags: true,
|
|
supportsMentions: true,
|
|
supportsEmoji: true,
|
|
mediaFormats: ['image', 'video', 'gif'],
|
|
bestPostingTimes: ['9:00', '12:00', '17:00', '21:00'],
|
|
engagementTips: [
|
|
'Use threads for longer content',
|
|
'Ask questions to boost engagement',
|
|
'Quote tweet with commentary',
|
|
],
|
|
},
|
|
LINKEDIN: {
|
|
name: 'LinkedIn',
|
|
maxLength: 3000,
|
|
optimalLength: 1300,
|
|
supportsMedia: true,
|
|
supportsLinks: true,
|
|
supportsHashtags: true,
|
|
supportsMentions: true,
|
|
supportsEmoji: true,
|
|
mediaFormats: ['image', 'video', 'document', 'carousel'],
|
|
bestPostingTimes: ['7:30', '12:00', '17:00'],
|
|
engagementTips: [
|
|
'Hook in first line (before "see more")',
|
|
'Use line breaks for readability',
|
|
'End with a question',
|
|
],
|
|
},
|
|
INSTAGRAM: {
|
|
name: 'Instagram',
|
|
maxLength: 2200,
|
|
optimalLength: 1500,
|
|
supportsMedia: true,
|
|
supportsLinks: false,
|
|
supportsHashtags: true,
|
|
supportsMentions: true,
|
|
supportsEmoji: true,
|
|
mediaFormats: ['image', 'video', 'reels', 'carousel', 'stories'],
|
|
bestPostingTimes: ['6:00', '11:00', '19:00'],
|
|
engagementTips: [
|
|
'Use up to 30 hashtags',
|
|
'Put hashtags in first comment',
|
|
'Include CTA in caption',
|
|
],
|
|
},
|
|
FACEBOOK: {
|
|
name: 'Facebook',
|
|
maxLength: 63206,
|
|
optimalLength: 250,
|
|
supportsMedia: true,
|
|
supportsLinks: true,
|
|
supportsHashtags: true,
|
|
supportsMentions: true,
|
|
supportsEmoji: true,
|
|
mediaFormats: ['image', 'video', 'reels', 'stories'],
|
|
bestPostingTimes: ['9:00', '13:00', '19:00'],
|
|
engagementTips: [
|
|
'Shorter posts perform better',
|
|
'Native video preferred',
|
|
'Use Facebook Live for engagement',
|
|
],
|
|
},
|
|
TIKTOK: {
|
|
name: 'TikTok',
|
|
maxLength: 300,
|
|
optimalLength: 150,
|
|
supportsMedia: true,
|
|
supportsLinks: false,
|
|
supportsHashtags: true,
|
|
supportsMentions: true,
|
|
supportsEmoji: true,
|
|
mediaFormats: ['video'],
|
|
bestPostingTimes: ['7:00', '10:00', '19:00', '23:00'],
|
|
engagementTips: [
|
|
'Hook in first 3 seconds',
|
|
'Use trending sounds',
|
|
'Keep videos under 60 seconds',
|
|
],
|
|
},
|
|
YOUTUBE: {
|
|
name: 'YouTube',
|
|
maxLength: 5000,
|
|
optimalLength: 500,
|
|
supportsMedia: true,
|
|
supportsLinks: true,
|
|
supportsHashtags: true,
|
|
supportsMentions: false,
|
|
supportsEmoji: true,
|
|
mediaFormats: ['video', 'shorts'],
|
|
bestPostingTimes: ['12:00', '15:00', '19:00'],
|
|
engagementTips: [
|
|
'Use timestamps in description',
|
|
'Include relevant keywords',
|
|
'Add cards and end screens',
|
|
],
|
|
},
|
|
PINTEREST: {
|
|
name: 'Pinterest',
|
|
maxLength: 500,
|
|
optimalLength: 300,
|
|
supportsMedia: true,
|
|
supportsLinks: true,
|
|
supportsHashtags: true,
|
|
supportsMentions: false,
|
|
supportsEmoji: true,
|
|
mediaFormats: ['image', 'video', 'idea_pin'],
|
|
bestPostingTimes: ['20:00', '21:00', '22:00'],
|
|
engagementTips: [
|
|
'Vertical images perform best',
|
|
'Use keyword-rich descriptions',
|
|
'Create multiple pins per post',
|
|
],
|
|
},
|
|
THREADS: {
|
|
name: 'Threads',
|
|
maxLength: 500,
|
|
optimalLength: 280,
|
|
supportsMedia: true,
|
|
supportsLinks: false,
|
|
supportsHashtags: false,
|
|
supportsMentions: true,
|
|
supportsEmoji: true,
|
|
mediaFormats: ['image', 'video'],
|
|
bestPostingTimes: ['12:00', '17:00', '21:00'],
|
|
engagementTips: [
|
|
'Conversational tone works best',
|
|
'Reply to trending topics',
|
|
'Cross-post from Instagram',
|
|
],
|
|
},
|
|
};
|
|
|
|
@Injectable()
|
|
export class PlatformAdaptersService {
|
|
private readonly logger = new Logger(PlatformAdaptersService.name);
|
|
|
|
/**
|
|
* Get platform configuration
|
|
*/
|
|
getConfig(platform: SocialPlatform): PlatformConfig {
|
|
return PLATFORM_CONFIGS[platform];
|
|
}
|
|
|
|
/**
|
|
* Get all platform configs
|
|
*/
|
|
getAllConfigs(): Record<SocialPlatform, PlatformConfig> {
|
|
return PLATFORM_CONFIGS;
|
|
}
|
|
|
|
/**
|
|
* Adapt content for specific platform
|
|
*/
|
|
adapt(content: string, sourcePlatform: SocialPlatform, targetPlatform: SocialPlatform): string {
|
|
const sourceConfig = PLATFORM_CONFIGS[sourcePlatform];
|
|
const targetConfig = PLATFORM_CONFIGS[targetPlatform];
|
|
|
|
let adapted = content;
|
|
|
|
// Handle length constraints
|
|
if (adapted.length > targetConfig.maxLength) {
|
|
adapted = this.truncate(adapted, targetConfig.maxLength);
|
|
}
|
|
|
|
// Handle link support
|
|
if (!targetConfig.supportsLinks && sourceConfig.supportsLinks) {
|
|
adapted = this.removeLinks(adapted);
|
|
}
|
|
|
|
// Handle hashtag support
|
|
if (!targetConfig.supportsHashtags && sourceConfig.supportsHashtags) {
|
|
adapted = this.removeHashtags(adapted);
|
|
}
|
|
|
|
return adapted;
|
|
}
|
|
|
|
/**
|
|
* Format content for platform
|
|
*/
|
|
format(content: string, platform: SocialPlatform): string {
|
|
const config = PLATFORM_CONFIGS[platform];
|
|
|
|
switch (platform) {
|
|
case 'TWITTER':
|
|
return this.formatForTwitter(content, config);
|
|
case 'LINKEDIN':
|
|
return this.formatForLinkedIn(content, config);
|
|
case 'INSTAGRAM':
|
|
return this.formatForInstagram(content, config);
|
|
default:
|
|
return this.truncate(content, config.maxLength);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if content is valid for platform
|
|
*/
|
|
validate(content: string, platform: SocialPlatform): { valid: boolean; issues: string[] } {
|
|
const config = PLATFORM_CONFIGS[platform];
|
|
const issues: string[] = [];
|
|
|
|
if (content.length > config.maxLength) {
|
|
issues.push(`Content exceeds ${config.maxLength} character limit`);
|
|
}
|
|
|
|
if (!config.supportsLinks && this.containsLinks(content)) {
|
|
issues.push('Platform does not support links in captions');
|
|
}
|
|
|
|
return { valid: issues.length === 0, issues };
|
|
}
|
|
|
|
// Private helpers
|
|
private truncate(content: string, maxLength: number): string {
|
|
if (content.length <= maxLength) return content;
|
|
return content.substring(0, maxLength - 3) + '...';
|
|
}
|
|
|
|
private removeLinks(content: string): string {
|
|
return content.replace(/https?:\/\/[^\s]+/g, '[link in bio]');
|
|
}
|
|
|
|
private removeHashtags(content: string): string {
|
|
return content.replace(/#\w+/g, '').replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
private containsLinks(content: string): boolean {
|
|
return /https?:\/\/[^\s]+/.test(content);
|
|
}
|
|
|
|
private formatForTwitter(content: string, config: PlatformConfig): string {
|
|
let formatted = content;
|
|
|
|
// Ensure hashtags at end
|
|
const hashtags = formatted.match(/#\w+/g) || [];
|
|
formatted = formatted.replace(/#\w+/g, '').trim();
|
|
formatted = `${formatted}\n\n${hashtags.slice(0, 3).join(' ')}`;
|
|
|
|
return this.truncate(formatted, config.maxLength);
|
|
}
|
|
|
|
private formatForLinkedIn(content: string, config: PlatformConfig): string {
|
|
// Add line breaks for readability
|
|
const lines = content.split(/(?<=[.!?])\s+/);
|
|
return lines.slice(0, 20).join('\n\n');
|
|
}
|
|
|
|
private formatForInstagram(content: string, config: PlatformConfig): string {
|
|
let formatted = content;
|
|
|
|
// Add dots before hashtags (common Instagram style)
|
|
if (formatted.includes('#')) {
|
|
const mainContent = formatted.split('#')[0].trim();
|
|
const hashtags = formatted.match(/#\w+/g) || [];
|
|
formatted = `${mainContent}\n.\n.\n.\n${hashtags.join(' ')}`;
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
}
|