Loading...
Loading...
Full-stack AI content pipeline for automated research, script generation, Facebook posting, and video rendering using Claude/OpenAI and Remotion
npx skill4agent add aradotso/marketing-skills marketing-pipeline-share-automationSkill by ara.so — Marketing Skills collection.
# Clone the repository
git clone https://github.com/pennydinh/marketing-pineline-share.git
cd marketing-pineline-share
# Install dependencies
npm install
# or
yarn install
# Copy environment template
cp .env.example .env.local.env.local# AI Provider APIs
ANTHROPIC_API_KEY=your_claude_api_key
OPENAI_API_KEY=your_openai_api_key
# Research/Crawling APIs
RAPIDAPI_KEY=your_rapidapi_key
RAPIDAPI_HOST=news-api-host
# Facebook Integration
FACEBOOK_PAGE_ACCESS_TOKEN=your_fb_page_token
FACEBOOK_PAGE_ID=your_page_id
# Remotion Configuration
REMOTION_CLOUDRUN_URL=your_cloudrun_url
REMOTION_API_KEY=your_remotion_key
# Application
NEXT_PUBLIC_APP_URL=http://localhost:3000// Typical project structure
src/
├── app/ # Next.js app directory
│ ├── api/ # API routes
│ │ ├── research/ # News crawling endpoints
│ │ ├── generate/ # AI content generation
│ │ ├── post/ # Facebook posting
│ │ └── render/ # Video rendering
│ └── dashboard/ # UI components
├── lib/
│ ├── ai/ # AI provider integrations
│ ├── crawlers/ # Web scraping utilities
│ ├── facebook/ # FB API integration
│ └── remotion/ # Video rendering logic
└── types/ # TypeScript definitions// lib/ai/content-generator.ts
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
export interface ContentRequest {
keyword: string;
format: 'toplist' | 'pov' | 'case-study' | 'how-to';
language: 'en' | 'vi';
tone: 'expert' | 'friendly' | 'humorous';
researchData?: any[];
}
export class ContentGenerator {
private claude: Anthropic;
private openai: OpenAI;
constructor() {
this.claude = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
this.openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
}
async generateWithClaude(request: ContentRequest): Promise<string> {
const prompt = this.buildPrompt(request);
const message = await this.claude.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 4096,
messages: [{
role: 'user',
content: prompt
}]
});
return message.content[0].type === 'text'
? message.content[0].text
: '';
}
async generateWithOpenAI(request: ContentRequest): Promise<string> {
const prompt = this.buildPrompt(request);
const completion = await this.openai.chat.completions.create({
model: 'gpt-4-turbo',
messages: [{
role: 'user',
content: prompt
}],
temperature: 0.7,
});
return completion.choices[0].message.content || '';
}
private buildPrompt(request: ContentRequest): string {
const { keyword, format, language, tone, researchData } = request;
let prompt = `Create a ${format} article about "${keyword}" in ${language}.`;
prompt += `\nTone: ${tone}`;
if (researchData && researchData.length > 0) {
prompt += `\n\nRecent research data:\n`;
researchData.forEach((item, idx) => {
prompt += `${idx + 1}. ${item.title}\n${item.summary}\n`;
});
}
return prompt;
}
}// lib/crawlers/news-crawler.ts
import axios from 'axios';
export interface NewsArticle {
title: string;
url: string;
source: string;
publishedAt: string;
summary: string;
}
export class NewsCrawler {
private rapidApiKey: string;
private rapidApiHost: string;
constructor() {
this.rapidApiKey = process.env.RAPIDAPI_KEY!;
this.rapidApiHost = process.env.RAPIDAPI_HOST!;
}
async crawlRecentNews(keyword: string, hours: number = 24): Promise<NewsArticle[]> {
try {
const response = await axios.get('https://api.rapidapi.com/v1/news/search', {
params: {
q: keyword,
from: this.getTimeRange(hours),
sortBy: 'publishedAt',
language: 'en'
},
headers: {
'X-RapidAPI-Key': this.rapidApiKey,
'X-RapidAPI-Host': this.rapidApiHost
}
});
return response.data.articles.map((article: any) => ({
title: article.title,
url: article.url,
source: article.source.name,
publishedAt: article.publishedAt,
summary: article.description || ''
}));
} catch (error) {
console.error('News crawling error:', error);
return [];
}
}
private getTimeRange(hours: number): string {
const date = new Date();
date.setHours(date.getHours() - hours);
return date.toISOString();
}
async crawlMultipleSources(keyword: string): Promise<NewsArticle[]> {
// Crawl from TechCrunch, a16z, etc.
const sources = ['techcrunch', 'a16z-blog'];
const allArticles: NewsArticle[] = [];
for (const source of sources) {
const articles = await this.crawlRecentNews(`${keyword} site:${source}`, 24);
allArticles.push(...articles);
}
return allArticles;
}
}// app/api/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { ContentGenerator } from '@/lib/ai/content-generator';
import { NewsCrawler } from '@/lib/crawlers/news-crawler';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { keyword, format, language, tone, useResearch, aiProvider } = body;
let researchData = [];
if (useResearch) {
const crawler = new NewsCrawler();
researchData = await crawler.crawlMultipleSources(keyword);
}
const generator = new ContentGenerator();
let content: string;
if (aiProvider === 'claude') {
content = await generator.generateWithClaude({
keyword,
format,
language,
tone,
researchData
});
} else {
content = await generator.generateWithOpenAI({
keyword,
format,
language,
tone,
researchData
});
}
return NextResponse.json({
success: true,
content,
researchCount: researchData.length
});
} catch (error: any) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: 500 }
);
}
}// lib/facebook/post-manager.ts
import axios from 'axios';
export interface FacebookPost {
message: string;
link?: string;
imageUrl?: string;
}
export class FacebookPostManager {
private pageAccessToken: string;
private pageId: string;
constructor() {
this.pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
this.pageId = process.env.FACEBOOK_PAGE_ID!;
}
async publishPost(post: FacebookPost): Promise<string> {
try {
const endpoint = `https://graph.facebook.com/v18.0/${this.pageId}/feed`;
const response = await axios.post(endpoint, {
message: post.message,
link: post.link,
access_token: this.pageAccessToken
});
return response.data.id;
} catch (error: any) {
throw new Error(`Facebook posting failed: ${error.response?.data?.error?.message || error.message}`);
}
}
async publishPhoto(post: FacebookPost): Promise<string> {
if (!post.imageUrl) {
throw new Error('Image URL required for photo post');
}
const endpoint = `https://graph.facebook.com/v18.0/${this.pageId}/photos`;
const response = await axios.post(endpoint, {
url: post.imageUrl,
caption: post.message,
access_token: this.pageAccessToken
});
return response.data.id;
}
async schedulePost(post: FacebookPost, publishTime: Date): Promise<string> {
const endpoint = `https://graph.facebook.com/v18.0/${this.pageId}/feed`;
const response = await axios.post(endpoint, {
message: post.message,
link: post.link,
published: false,
scheduled_publish_time: Math.floor(publishTime.getTime() / 1000),
access_token: this.pageAccessToken
});
return response.data.id;
}
}// lib/remotion/video-renderer.ts
import { bundle } from '@remotion/bundler';
import { renderMedia, selectComposition } from '@remotion/renderer';
import { webpackOverride } from './webpack-override';
export interface VideoConfig {
title: string;
content: string[];
duration: number;
format: 'reels' | 'tiktok' | 'shorts';
}
export class VideoRenderer {
async renderContentVideo(config: VideoConfig): Promise<string> {
const bundled = await bundle({
entryPoint: './src/remotion/index.ts',
webpackOverride: webpackOverride,
});
const composition = await selectComposition({
serveUrl: bundled,
id: 'ContentVideo',
inputProps: {
title: config.title,
content: config.content,
format: config.format
},
});
const outputLocation = `./output/video-${Date.now()}.mp4`;
await renderMedia({
composition,
serveUrl: bundled,
codec: 'h264',
outputLocation,
inputProps: {
title: config.title,
content: config.content,
format: config.format
},
});
return outputLocation;
}
getAspectRatio(format: string): { width: number; height: number } {
switch (format) {
case 'reels':
case 'tiktok':
case 'shorts':
return { width: 1080, height: 1920 }; // 9:16
default:
return { width: 1920, height: 1080 }; // 16:9
}
}
}// lib/pipeline/content-pipeline.ts
import { ContentGenerator } from '@/lib/ai/content-generator';
import { NewsCrawler } from '@/lib/crawlers/news-crawler';
import { FacebookPostManager } from '@/lib/facebook/post-manager';
import { VideoRenderer } from '@/lib/remotion/video-renderer';
export class ContentPipeline {
private generator: ContentGenerator;
private crawler: NewsCrawler;
private fbManager: FacebookPostManager;
private videoRenderer: VideoRenderer;
constructor() {
this.generator = new ContentGenerator();
this.crawler = new NewsCrawler();
this.fbManager = new FacebookPostManager();
this.videoRenderer = new VideoRenderer();
}
async executeFullPipeline(keyword: string) {
console.log('Step 1: Researching...');
const researchData = await this.crawler.crawlMultipleSources(keyword);
console.log('Step 2: Generating content...');
const content = await this.generator.generateWithClaude({
keyword,
format: 'toplist',
language: 'en',
tone: 'expert',
researchData
});
console.log('Step 3: Rendering video...');
const videoPath = await this.videoRenderer.renderContentVideo({
title: keyword,
content: content.split('\n').filter(line => line.trim()),
duration: 30,
format: 'reels'
});
console.log('Step 4: Posting to Facebook...');
const postId = await this.fbManager.publishPost({
message: `New content about ${keyword}!`,
link: `https://example.com/videos/${videoPath}`
});
return {
success: true,
researchCount: researchData.length,
content,
videoPath,
postId
};
}
}// app/api/pipeline/execute/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { ContentPipeline } from '@/lib/pipeline/content-pipeline';
export async function POST(request: NextRequest) {
try {
const { keyword } = await request.json();
if (!keyword) {
return NextResponse.json(
{ success: false, error: 'Keyword is required' },
{ status: 400 }
);
}
const pipeline = new ContentPipeline();
const result = await pipeline.executeFullPipeline(keyword);
return NextResponse.json(result);
} catch (error: any) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: 500 }
);
}
}# Development
npm run dev
# Production build
npm run build
npm start
# Run specific pipeline steps
npm run research -- --keyword "AI trends"
npm run generate -- --keyword "AI trends" --format toplist
npm run render -- --input content.json// Quick research without content generation
import { NewsCrawler } from '@/lib/crawlers/news-crawler';
const crawler = new NewsCrawler();
const articles = await crawler.crawlRecentNews('AI marketing', 48);
console.log(`Found ${articles.length} articles`);// Generate content in both languages
const generator = new ContentGenerator();
const englishContent = await generator.generateWithClaude({
keyword: 'Marketing automation',
format: 'how-to',
language: 'en',
tone: 'expert'
});
const vietnameseContent = await generator.generateWithClaude({
keyword: 'Marketing automation',
format: 'how-to',
language: 'vi',
tone: 'friendly'
});// Schedule content for future posting
const fbManager = new FacebookPostManager();
const publishTime = new Date();
publishTime.setHours(publishTime.getHours() + 24);
const postId = await fbManager.schedulePost({
message: generatedContent,
link: 'https://example.com/article'
}, publishTime);// Verify API keys are loaded
if (!process.env.ANTHROPIC_API_KEY) {
throw new Error('ANTHROPIC_API_KEY not found in environment');
}
// Test API connection
const testConnection = async () => {
try {
const claude = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
await claude.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 10,
messages: [{ role: 'user', content: 'test' }]
});
console.log('✓ Claude API connected');
} catch (error) {
console.error('✗ Claude API failed:', error);
}
};// Implement retry logic with exponential backoff
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error: any) {
if (error.status === 429 && i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}// Debug Facebook API errors
try {
await fbManager.publishPost(post);
} catch (error: any) {
if (error.message.includes('access token')) {
console.error('Facebook token expired or invalid');
// Refresh token logic here
} else if (error.message.includes('rate limit')) {
console.error('Facebook rate limit reached');
// Queue for later
} else {
console.error('Unknown Facebook error:', error);
}
}// Check Remotion dependencies
import { getCompositions } from '@remotion/renderer';
const checkRemotionSetup = async () => {
try {
const compositions = await getCompositions('./src/remotion/index.ts');
console.log('Available compositions:', compositions.map(c => c.id));
} catch (error) {
console.error('Remotion setup error:', error);
}
};