Claude Skills Guide

A Chrome extension that schedules social media posts gives you control over when content publishes across platforms without requiring a full SaaS subscription. This guide walks through building one from scratch, covering the architecture, storage strategies, and the messaging system that ties everything together.

Why Build a Local Scheduler

Most scheduling tools store your posts on their servers. A local-first approach keeps your drafts and scheduled items in browser storage, giving you privacy and eliminating monthly fees. The trade-off is that your computer needs to be running for scheduled posts to trigger—but you can work around that limitation with a background service worker.

Extension Architecture

The core components are the popup interface, a background service worker, and storage. The popup lets users compose posts and set publish times. The background worker handles the actual posting logic when deadlines arrive.

Manifest V3 Configuration

Your manifest.json needs the right permissions to make this work:

{
  "manifest_version": 3,
  "name": "Social Media Scheduler",
  "version": "1.0",
  "permissions": ["storage", "alarms", "notifications"],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icon.png"
  }
}

The storage permission gives you access to chrome.storage.local, where scheduled posts live. Alarms let you set precise timers, and notifications alert users when posts go live or fail.

Storing Scheduled Posts

Use chrome.storage.local to persist posts. Each entry needs a unique ID, the post content, target platform, scheduled timestamp, and status:

// In popup.js - saving a scheduled post
function schedulePost(postData) {
  const scheduledPost = {
    id: crypto.randomUUID(),
    content: postData.content,
    platform: postData.platform,
    scheduledTime: postData.scheduledTime,
    status: 'pending',
    createdAt: Date.now()
  };

  chrome.storage.local.get(['scheduledPosts'], (result) => {
    const posts = result.scheduledPosts || [];
    posts.push(scheduledPost);
    chrome.storage.local.set({ scheduledPosts: posts });
  });
}

Store arrays of posts rather than individual keys. It keeps retrieval simple and makes bulk operations easier.

The Alarm System

Chrome alarms provide precise timing without polling. Set an alarm when a post is scheduled:

// In background.js - setting an alarm for a scheduled post
function setPostAlarm(post) {
  const delay = post.scheduledTime - Date.now();
  
  if (delay > 0) {
    chrome.alarms.create(post.id, {
      delayInMinutes: delay / 60000,
      periodInMinutes: 0
    });
  }
}

Listen for alarm triggers in your service worker:

chrome.alarms.onAlarm.addListener((alarm) => {
  chrome.storage.local.get(['scheduledPosts'], (result) => {
    const posts = result.scheduledPosts || [];
    const post = posts.find(p => p.id === alarm.name);
    
    if (post && post.status === 'pending') {
      publishPost(post);
    }
  });
});

Platform Integration Patterns

Actual posting requires platform-specific APIs. For Twitter/X, you’d use their v2 API with OAuth. For LinkedIn, their Marketing API. The extension acts as a bridge between your stored post and the platform’s publishing endpoint.

A practical approach uses a content script injected into the platform’s web interface:

// In publish-twitter.js
function publishTweet(content) {
  // Twitter's web interface uses these selectors
  const tweetBox = document.querySelector('[data-testid="tweetTextInput"]');
  const submitButton = document.querySelector('[data-testid="tweetButton"]');
  
  if (tweetBox && submitButton) {
    tweetBox.textContent = content;
    submitButton.click();
    return true;
  }
  return false;
}

This “headless browser” approach works because you’re automating the actual web interface rather than calling APIs directly. It bypasses API rate limits and authentication headaches, though it requires the user to be logged into each platform in Chrome.

Handling Failures

Network requests fail. APIs change. Your scheduler needs resilience:

async function publishWithRetry(post, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const success = await publishPost(post);
      if (success) return true;
    } catch (error) {
      console.error(`Attempt ${attempt} failed:`, error);
      await new Promise(r => setTimeout(r, 1000 * attempt));
    }
  }
  
  // Mark as failed after all retries
  updatePostStatus(post.id, 'failed');
  notifyUser(post, 'failed');
  return false;
}

Always update post status in storage after any publish attempt. Failed posts should stay visible in the popup so users can retry or edit them.

User Interface Essentials

The popup needs three sections: a composer, a queue showing upcoming posts, and controls to manage them. Keep it simple:

Use Chrome’s storage API to persist UI state so users don’t lose drafts when closing the popup.

Security Considerations

Never store platform credentials in local storage. If you’re automating web interfaces, users should remain logged in through their normal Chrome sessions. For API-based approaches, use OAuth tokens stored in chrome.storage.session, which clears when the browser closes.

Content scripts run in the context of web pages, so sanitize any user input before inserting it into the DOM to prevent XSS.

Testing Your Extension

Load your extension in chrome://extensions/ with “Developer mode” enabled. Use “Load unpacked” to test changes without repackaging. The service worker logs to the background script console—check there when debugging alarm triggers.

For the content script automation, test manually first. Platform UIs change frequently, so selector-based approaches need maintenance.

Going Further

Once the basic scheduler works, consider adding:

The foundation you build here scales into a full-featured social media management tool. Start with reliable scheduling, then layer on the features that matter to your workflow.

Built by theluckystrike — More at zovo.one