DEV Community

Olamide Olaniyan
Olamide Olaniyan

Posted on

Build an AI Content Calendar Generator from Competitor Data

Content calendars are broken.

You spend Sunday night staring at an empty spreadsheet. "What should I post tomorrow?" becomes existential dread.

Meanwhile, your competitors post consistently. Their content performs. They've figured something out.

What if you could reverse-engineer their strategy and generate a week of content ideas in 30 seconds?

In this tutorial, we'll build an AI Content Calendar Generator that:

  1. Analyzes your competitors' top-performing content
  2. Identifies winning themes, formats, and posting patterns
  3. Generates a personalized 7-day content calendar

No more blank page syndrome.

The Strategy Behind This

Great content calendars aren't creativeβ€”they're strategic.

Top creators:

  • Post at consistent times (their audience expects it)
  • Repeat formats that work (series, templates, hooks)
  • Ride trends while they're still fresh
  • Balance content types (educational, entertaining, promotional)

We're going to extract these patterns automatically.

The Stack

  • Node.js: Runtime
  • SociaVault API: To analyze competitor content
  • OpenAI API: To generate calendar recommendations

Step 1: Setup

mkdir content-calendar-generator
cd content-calendar-generator
npm init -y
npm install axios openai dotenv
Enter fullscreen mode Exit fullscreen mode

Create .env:

SOCIAVAULT_API_KEY=your_sociavault_key
OPENAI_API_KEY=your_openai_key
Enter fullscreen mode Exit fullscreen mode

Step 2: Competitor Content Fetcher

Create index.js:

require('dotenv').config();
const axios = require('axios');
const OpenAI = require('openai');

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const SOCIAVAULT_BASE = 'https://api.sociavault.com';
const headers = { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` };

// Fetch TikTok content
async function getTikTokContent(handle) {
  console.log(`πŸ“₯ Fetching TikTok content from @${handle}...`);

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/videos`, {
      params: { handle, amount: 50 },
      headers
    });

    const videos = response.data.data || [];

    return videos.map(v => ({
      platform: 'tiktok',
      description: v.desc || v.description || '',
      likes: v.diggCount || v.stats?.diggCount || 0,
      comments: v.commentCount || v.stats?.commentCount || 0,
      shares: v.shareCount || v.stats?.shareCount || 0,
      views: v.playCount || v.stats?.playCount || 0,
      created: v.createTime ? new Date(v.createTime * 1000) : null,
      music: v.music?.title || 'Original Sound',
      hashtags: v.challenges?.map(c => c.title) || extractHashtags(v.desc || '')
    }));
  } catch (error) {
    console.error('TikTok error:', error.message);
    return [];
  }
}

// Fetch Instagram content
async function getInstagramContent(handle) {
  console.log(`πŸ“₯ Fetching Instagram content from @${handle}...`);

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/instagram/posts`, {
      params: { handle },
      headers
    });

    const posts = response.data.data || [];

    return posts.map(p => ({
      platform: 'instagram',
      description: p.caption || '',
      likes: p.like_count || p.likes || 0,
      comments: p.comment_count || p.comments || 0,
      type: p.media_type, // IMAGE, VIDEO, CAROUSEL_ALBUM
      created: p.taken_at ? new Date(p.taken_at * 1000) : null,
      hashtags: extractHashtags(p.caption || '')
    }));
  } catch (error) {
    console.error('Instagram error:', error.message);
    return [];
  }
}

// Fetch Twitter content
async function getTwitterContent(handle) {
  console.log(`πŸ“₯ Fetching Twitter content from @${handle}...`);

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/twitter/user-tweets`, {
      params: { handle },
      headers
    });

    const tweets = response.data.data || [];

    return tweets.map(t => ({
      platform: 'twitter',
      description: t.full_text || t.text || '',
      likes: t.favorite_count || t.likes || 0,
      retweets: t.retweet_count || 0,
      replies: t.reply_count || 0,
      views: t.views_count || t.views || 0,
      created: t.created_at ? new Date(t.created_at) : null,
      hashtags: extractHashtags(t.full_text || t.text || '')
    }));
  } catch (error) {
    console.error('Twitter error:', error.message);
    return [];
  }
}

// Fetch LinkedIn content
async function getLinkedInContent(profileUrl) {
  console.log(`πŸ“₯ Fetching LinkedIn content...`);

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/linkedin/profile`, {
      params: { url: profileUrl },
      headers
    });

    const profile = response.data.data;
    const posts = profile.posts || [];

    return posts.map(p => ({
      platform: 'linkedin',
      description: p.text || p.commentary || '',
      likes: p.num_likes || p.reactions || 0,
      comments: p.num_comments || 0,
      reposts: p.num_reposts || 0,
      created: p.posted_on?.day ? new Date(p.posted_on.year, p.posted_on.month - 1, p.posted_on.day) : null,
      hashtags: extractHashtags(p.text || '')
    }));
  } catch (error) {
    console.error('LinkedIn error:', error.message);
    return [];
  }
}

function extractHashtags(text) {
  const matches = text.match(/#[\w]+/g) || [];
  return matches.map(h => h.replace('#', ''));
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Content Analysis Engine

function analyzeContentPatterns(content) {
  if (content.length === 0) {
    return { error: 'No content to analyze' };
  }

  // Sort by engagement
  const sorted = [...content].sort((a, b) => {
    const aEng = (a.likes || 0) + (a.comments || 0) * 2 + (a.shares || a.retweets || 0) * 3;
    const bEng = (b.likes || 0) + (b.comments || 0) * 2 + (b.shares || b.retweets || 0) * 3;
    return bEng - aEng;
  });

  // Top performers (top 20%)
  const topCount = Math.max(Math.floor(content.length * 0.2), 3);
  const topPerformers = sorted.slice(0, topCount);

  // Posting frequency analysis
  const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  const postsByDay = {};
  const postsByHour = {};

  content.forEach(c => {
    if (c.created) {
      const day = daysOfWeek[c.created.getDay()];
      const hour = c.created.getHours();

      postsByDay[day] = (postsByDay[day] || 0) + 1;
      postsByHour[hour] = (postsByHour[hour] || 0) + 1;
    }
  });

  // Find most active posting times
  const sortedDays = Object.entries(postsByDay).sort((a, b) => b[1] - a[1]);
  const sortedHours = Object.entries(postsByHour).sort((a, b) => b[1] - a[1]);

  // Hashtag analysis
  const hashtagCounts = {};
  content.forEach(c => {
    (c.hashtags || []).forEach(h => {
      hashtagCounts[h.toLowerCase()] = (hashtagCounts[h.toLowerCase()] || 0) + 1;
    });
  });
  const topHashtags = Object.entries(hashtagCounts)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10)
    .map(([tag]) => tag);

  // Content length analysis
  const avgLength = content.reduce((sum, c) => sum + (c.description?.length || 0), 0) / content.length;
  const topAvgLength = topPerformers.reduce((sum, c) => sum + (c.description?.length || 0), 0) / topPerformers.length;

  return {
    totalPosts: content.length,
    topPerformers,
    postingPattern: {
      bestDays: sortedDays.slice(0, 3),
      bestHours: sortedHours.slice(0, 3),
      postsPerWeek: Math.round(content.length / 4) // Assuming ~1 month of data
    },
    topHashtags,
    contentLength: {
      average: Math.round(avgLength),
      topPerformerAverage: Math.round(topAvgLength)
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Step 4: AI Calendar Generator

async function generateContentCalendar(analysis, niche, platform) {
  console.log('πŸ€– Generating content calendar...');

  const topContent = analysis.topPerformers.map(c => ({
    description: c.description?.substring(0, 200),
    engagement: c.likes + (c.comments * 2)
  }));

  const prompt = `
    You are a social media strategist. Create a 7-day content calendar.

    CONTEXT:
    - Platform: ${platform}
    - Niche: ${niche}
    - Best posting days: ${analysis.postingPattern.bestDays.map(d => d[0]).join(', ')}
    - Best posting hours: ${analysis.postingPattern.bestHours.map(h => h[0] + ':00').join(', ')}
    - Top hashtags in niche: ${analysis.topHashtags.join(', ')}
    - Top performing caption length: ~${analysis.contentLength.topPerformerAverage} characters

    TOP PERFORMING CONTENT FROM COMPETITORS:
    ${JSON.stringify(topContent, null, 2)}

    CALENDAR REQUIREMENTS:
    1. One post per day for 7 days
    2. Mix of content types (educational, entertaining, promotional, engagement-bait)
    3. Include specific post ideas, not generic advice
    4. Suggest optimal posting time for each day
    5. Include relevant hashtags for each post
    6. Make hooks attention-grabbing

    Return JSON:
    {
      "calendar": [
        {
          "day": "Monday",
          "postTime": "9:00 AM",
          "contentType": "educational|entertaining|promotional|engagement",
          "hook": "first line that grabs attention",
          "fullCaption": "complete caption ready to post",
          "hashtags": ["relevant", "hashtags"],
          "callToAction": "what you want viewers to do",
          "notes": "why this will work"
        }
      ],
      "strategy": {
        "weeklyTheme": "overarching theme for the week",
        "contentMix": {
          "educational": "X posts",
          "entertaining": "X posts",
          "promotional": "X posts",
          "engagement": "X posts"
        },
        "keyInsights": ["3 key insights from competitor analysis"]
      }
    }
  `;

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    response_format: { type: 'json_object' }
  });

  return JSON.parse(completion.choices[0].message.content);
}
Enter fullscreen mode Exit fullscreen mode

Step 5: The Main Generator

async function createContentCalendar(config) {
  const { platform, competitors, niche } = config;

  console.log('\nπŸ“… AI CONTENT CALENDAR GENERATOR\n');
  console.log('═══════════════════════════════════════════\n');

  // 1. Fetch competitor content
  let allContent = [];

  for (const competitor of competitors) {
    let content;

    switch (platform) {
      case 'tiktok':
        content = await getTikTokContent(competitor);
        break;
      case 'instagram':
        content = await getInstagramContent(competitor);
        break;
      case 'twitter':
        content = await getTwitterContent(competitor);
        break;
      case 'linkedin':
        content = await getLinkedInContent(competitor);
        break;
    }

    if (content.length > 0) {
      console.log(`  βœ… @${competitor}: ${content.length} posts`);
      allContent = [...allContent, ...content];
    }

    // Rate limiting
    await new Promise(r => setTimeout(r, 1000));
  }

  console.log(`\nπŸ“Š Total content analyzed: ${allContent.length} posts\n`);

  // 2. Analyze patterns
  const analysis = analyzeContentPatterns(allContent);

  console.log('───────────────────────────────────────────');
  console.log('πŸ“ˆ COMPETITOR INSIGHTS');
  console.log('───────────────────────────────────────────\n');

  console.log('Best Posting Days:');
  analysis.postingPattern.bestDays.forEach(([day, count]) => {
    console.log(`  β€’ ${day}: ${count} posts`);
  });

  console.log('\nBest Posting Hours:');
  analysis.postingPattern.bestHours.forEach(([hour, count]) => {
    const time = hour < 12 ? `${hour}:00 AM` : `${hour - 12}:00 PM`;
    console.log(`  β€’ ${time}: ${count} posts`);
  });

  console.log('\nTop Hashtags:');
  console.log(`  ${analysis.topHashtags.slice(0, 5).map(h => `#${h}`).join('  ')}`);

  console.log(`\nOptimal Caption Length: ~${analysis.contentLength.topPerformerAverage} characters`);

  // 3. Generate calendar
  const calendar = await generateContentCalendar(analysis, niche, platform);

  // 4. Display calendar
  console.log('\n═══════════════════════════════════════════');
  console.log('πŸ“… YOUR 7-DAY CONTENT CALENDAR');
  console.log('═══════════════════════════════════════════\n');

  calendar.calendar.forEach((day, i) => {
    console.log(`β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”`);
    console.log(`β”‚ ${day.day.toUpperCase().padEnd(39)}β”‚`);
    console.log(`β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€`);
    console.log(`β”‚ πŸ• ${day.postTime.padEnd(36)}β”‚`);
    console.log(`β”‚ πŸ“ ${day.contentType.toUpperCase().padEnd(36)}β”‚`);
    console.log(`β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜`);
    console.log(`\nπŸͺ HOOK:`);
    console.log(`"${day.hook}"\n`);
    console.log(`πŸ“„ FULL CAPTION:`);
    console.log(`${day.fullCaption}\n`);
    console.log(`#️⃣  ${day.hashtags.map(h => `#${h}`).join(' ')}\n`);
    console.log(`πŸ‘‰ CTA: ${day.callToAction}`);
    console.log(`πŸ’‘ Why: ${day.notes}\n`);
    console.log('───────────────────────────────────────────\n');
  });

  // 5. Strategy summary
  console.log('═══════════════════════════════════════════');
  console.log('πŸ“‹ WEEKLY STRATEGY');
  console.log('═══════════════════════════════════════════\n');

  console.log(`Theme: ${calendar.strategy.weeklyTheme}\n`);

  console.log('Content Mix:');
  Object.entries(calendar.strategy.contentMix).forEach(([type, count]) => {
    console.log(`  β€’ ${type}: ${count}`);
  });

  console.log('\nKey Insights:');
  calendar.strategy.keyInsights.forEach((insight, i) => {
    console.log(`  ${i + 1}. ${insight}`);
  });

  return { analysis, calendar };
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Run It

async function main() {
  await createContentCalendar({
    platform: 'tiktok',
    competitors: [
      'garyvee',
      'alexhormozi',
      'noahkagan'
    ],
    niche: 'entrepreneurship and business advice'
  });
}

main();
Enter fullscreen mode Exit fullscreen mode

Run with:

node index.js
Enter fullscreen mode Exit fullscreen mode

Sample Output

πŸ“… AI CONTENT CALENDAR GENERATOR
═══════════════════════════════════════════

πŸ“₯ Fetching TikTok content from @garyvee...
  βœ… @garyvee: 50 posts
πŸ“₯ Fetching TikTok content from @alexhormozi...
  βœ… @alexhormozi: 50 posts
πŸ“₯ Fetching TikTok content from @noahkagan...
  βœ… @noahkagan: 50 posts

πŸ“Š Total content analyzed: 150 posts

───────────────────────────────────────────
πŸ“ˆ COMPETITOR INSIGHTS
───────────────────────────────────────────

Best Posting Days:
  β€’ Tuesday: 28 posts
  β€’ Wednesday: 24 posts
  β€’ Thursday: 22 posts

Best Posting Hours:
  β€’ 9:00 AM: 32 posts
  β€’ 12:00 PM: 28 posts
  β€’ 6:00 PM: 25 posts

Top Hashtags:
  #business  #entrepreneur  #mindset  #motivation  #success

Optimal Caption Length: ~145 characters

πŸ€– Generating content calendar...

═══════════════════════════════════════════
πŸ“… YOUR 7-DAY CONTENT CALENDAR
═══════════════════════════════════════════

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ MONDAY                                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ πŸ• 9:00 AM                              β”‚
β”‚ πŸ“ EDUCATIONAL                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸͺ HOOK:
"The #1 reason your business isn't growing (it's not what you think)"

πŸ“„ FULL CAPTION:
Most people blame their marketing.

But I've seen 1000+ businesses and the real problem is simpler:

You're solving a problem nobody cares about.

Before you spend another dollar on ads, ask 10 potential customers:
"Would you pay $100 to solve this problem?"

If less than 7 say yes, pivot.

Comment "PROBLEM" and I'll help you find yours.

#️⃣  #business #entrepreneur #startuptips #businessadvice #growth

πŸ‘‰ CTA: Comment "PROBLEM" for personalized feedback
πŸ’‘ Why: Question-based hooks drove 3x engagement in competitor data. Educational content performs best on Mondays.

───────────────────────────────────────────

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ TUESDAY                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ πŸ• 12:00 PM                             β”‚
β”‚ πŸ“ ENTERTAINING                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸͺ HOOK:
"POV: You just quit your 9-5 to start a business"

πŸ“„ FULL CAPTION:
Day 1: Freedom! I'm my own boss!
Day 7: Wait, I have to do EVERYTHING?
Day 30: Why did no one tell me about taxes?
Day 90: Finally made $1
Day 180: Starting to figure this out
Day 365: Would never go back

The first year is survival. The second year is growth.

Tag someone who needs to see this πŸ‘‡

#️⃣  #entrepreneurlife #startup #business #quityourjob #hustle

πŸ‘‰ CTA: Tag a friend starting their journey
πŸ’‘ Why: POV format is trending. Relatable struggle content gets high shares.

───────────────────────────────────────────

[... continues for 7 days ...]

═══════════════════════════════════════════
πŸ“‹ WEEKLY STRATEGY
═══════════════════════════════════════════

Theme: "From Employee to Entrepreneur" - A week of content for people considering or starting their business journey

Content Mix:
  β€’ educational: 3 posts
  β€’ entertaining: 2 posts
  β€’ promotional: 1 post
  β€’ engagement: 1 post

Key Insights:
  1. Question-based hooks outperform statements by 2.3x in this niche
  2. Competitors post most frequently Tue-Thu, suggesting higher engagement windows
  3. Personal story content (failures, lessons) drives 40% more comments than pure advice
Enter fullscreen mode Exit fullscreen mode

Export to Notion/Google Sheets

function exportToCSV(calendar) {
  const headers = 'Day,Time,Type,Hook,Caption,Hashtags,CTA\n';
  const rows = calendar.calendar.map(day => 
    `"${day.day}","${day.postTime}","${day.contentType}","${day.hook.replace(/"/g, '""')}","${day.fullCaption.replace(/"/g, '""')}","${day.hashtags.join(' ')}","${day.callToAction}"`
  ).join('\n');

  const fs = require('fs');
  fs.writeFileSync('content-calendar.csv', headers + rows);
  console.log('\nβœ… Exported to content-calendar.csv');
}
Enter fullscreen mode Exit fullscreen mode

Adding More Competitors

// Analyze multiple platforms at once
async function multiPlatformCalendar(config) {
  const results = {};

  for (const [platform, competitors] of Object.entries(config.platforms)) {
    console.log(`\nπŸ“± Analyzing ${platform}...`);
    results[platform] = await createContentCalendar({
      platform,
      competitors,
      niche: config.niche
    });
  }

  return results;
}

// Usage
multiPlatformCalendar({
  niche: 'fitness and wellness',
  platforms: {
    tiktok: ['blogilates', 'sydneycummings'],
    instagram: ['kaikiakoe', 'whitneyysimmons'],
    twitter: ['mindpumpmedia']
  }
});
Enter fullscreen mode Exit fullscreen mode

Cost Comparison

Content planning services:

  • Later: $25/month (just scheduling)
  • Hootsuite: $99/month (basic analytics)
  • Sprout Social: $249/month (competitor analysis)
  • Content agency: $2,000+/month

Your version:

  • SociaVault: ~$0.15 per competitor analysis
  • OpenAI: ~$0.03 per calendar generation
  • Total: ~$0.50 per week of content

That's 500x cheaper than hiring an agency.

What You Just Built

This is literally what social media managers do for 40 hours a week:

  • Research competitors
  • Identify what works
  • Plan content calendar
  • Write captions and hooks

You just automated the entire process.

Get Started

  1. Get your SociaVault API Key
  2. Add your OpenAI key
  3. List your top 3 competitors
  4. Generate a week of content in 30 seconds

Writer's block is now a solved problem.


The best content strategies aren't creative, they're analytical.

Top comments (0)