Marketing Automation

Automate Facebook Posting with React and Netlify Functions

Build a scheduled Facebook posting system that captures React components as images and posts them automatically, with Netlify and no paid tools.

Dec 12, 2025
12 min

How to Build an Automated Facebook Posting System with React and Netlify

The Problem

My client had 10 social media ad designs built as React components. They wanted to post one per day to Facebook without:

  • Manually posting every day
  • Paying for Buffer, Hootsuite, or similar tools
  • Exporting images separately and uploading them

The solution? Build it into the existing React app with Netlify scheduled functions.

The Architecture

Here's what we built:

React Component (Design)
        ↓
html-to-image (Capture at 2048x2048)
        ↓
Base64 PNG stored in PostgreSQL
        ↓
Netlify Scheduled Function (runs daily at 9 AM)
        ↓
Facebook Graph API (posts the image)

Step 1: Capturing React Components as Images

The first challenge was converting React components to high-quality images. I used the html-to-image library.

The Key Insight: Capture the Right Element

My first attempts produced images with white bars or shadows. The fix was simple but crucial:

// WRONG - captures shadows and decorative CSS
const dataUrl = await toPng(cardContainer, { ... });

// CORRECT - capture only the content wrapper
const contentWrapper = target.querySelector('[data-content-wrapper]');
const dataUrl = await toPng(contentWrapper, { ... });

The Clone Technique for Crisp Text

For Facebook's 2048px requirement, I clone the element at full size before capturing:

// Clone at 2048px for crisp text rendering
const clone = contentWrapper.cloneNode(true) as HTMLElement;
clone.style.position = 'fixed';
clone.style.left = '-9999px';
clone.style.width = '2048px';
clone.style.height = '2048px';
clone.style.transform = 'none';
document.body.appendChild(clone);

// Capture at 1:1 ratio (element is already at target size)
const base64 = await toPng(clone, {
  quality: 1,
  pixelRatio: 1,
  width: 2048,
  height: 2048,
});

document.body.removeChild(clone);

Step 2: Database Schema for Scheduled Posts

I added a simple table to store scheduled posts:

CREATE TABLE scheduled_posts (
  id SERIAL PRIMARY KEY,
  design_id INTEGER NOT NULL,
  caption TEXT NOT NULL,
  image_base64 TEXT NOT NULL,
  scheduled_for TIMESTAMP NOT NULL,
  status TEXT DEFAULT 'pending',
  facebook_post_id TEXT,
  error_message TEXT,
  created_at TIMESTAMP DEFAULT NOW(),
  posted_at TIMESTAMP
);

The image_base64 field stores the pre-captured image so the scheduled function doesn't need to render React components.

Step 3: The Scheduling UI

I added a "Schedule 10 Days of Posts" button that:

  1. Loops through all 10 designs
  2. Switches to each design tab (to render it)
  3. Captures at 2048x2048
  4. Saves to database with staggered dates
const scheduleAllPosts = async () => {
  for (let i = 0; i < designs.length; i++) {
    const design = designs[i];
    
    // Switch to this design to render it
    setActiveDesign(design.id);
    await new Promise(resolve => setTimeout(resolve, 500));
    
    // Capture the design
    const base64 = await captureDesign();
    
    // Schedule for tomorrow + i days at 9 AM
    const scheduledDate = new Date();
    scheduledDate.setDate(scheduledDate.getDate() + 1 + i);
    scheduledDate.setHours(9, 0, 0, 0);
    
    await apiRequest("POST", "/api/scheduled-posts", {
      designId: design.id,
      caption: generateCaption(design),
      imageBase64: base64,
      scheduledFor: scheduledDate.toISOString(),
    });
  }
};

Step 4: Netlify Scheduled Function

The magic happens in a Netlify scheduled function that runs daily:

// netlify/functions/scheduled-facebook-post.ts
import type { Config } from "@netlify/functions";

export default async function handler() {
  // Find pending posts due now
  const pendingPosts = await sql`
    SELECT * FROM scheduled_posts 
    WHERE status = 'pending' 
    AND scheduled_for <= NOW()
    LIMIT 1
  `;

  if (pendingPosts.length === 0) return;

  const post = pendingPosts[0];
  
  // Post to Facebook
  const formData = new FormData();
  formData.append('source', Buffer.from(post.image_base64, 'base64'));
  formData.append('message', post.caption);
  formData.append('access_token', process.env.FACEBOOK_ACCESS_TOKEN);

  const response = await axios.post(
    `https://graph.facebook.com/v24.0/${pageId}/photos`,
    formData
  );

  // Mark as posted
  await sql`
    UPDATE scheduled_posts 
    SET status = 'posted', facebook_post_id = ${response.data.id}
    WHERE id = ${post.id}
  `;
}

// Run daily at 10 PM UTC = 9 AM Sydney (AEDT)
export const config: Config = {
  schedule: "0 22 * * *"
};

Step 5: Facebook Access Token (The Tricky Part)

Facebook tokens expire. Here's how to get a long-lived one:

Get a Short-Lived Token

  1. Go to Graph API Explorer
  2. Select your app
  3. Add permissions: pages_show_list, pages_manage_posts
  4. Generate token

Exchange for Long-Lived Token (60 days)

GET https://graph.facebook.com/v24.0/oauth/access_token?
  grant_type=fb_exchange_token&
  client_id={APP_ID}&
  client_secret={APP_SECRET}&
  fb_exchange_token={SHORT_LIVED_TOKEN}

Get Page Access Token (Never Expires)

GET https://graph.facebook.com/v24.0/me/accounts?
  access_token={LONG_LIVED_USER_TOKEN}

The Management UI

I also built a simple UI to view and manage scheduled posts:

  • See all pending, posted, and failed posts
  • Manual "Post Now" button for testing
  • Delete button to remove scheduled posts
  • Status indicators (green checkmark for posted)

Common Issues and Fixes

White Bar at Top of Image

Cause: Capturing outer container with shadow-2xl Fix: Capture data-content-wrapper directly

Blurry Text on Facebook

Cause: Scaling up small images Fix: Clone element at 2048px, capture at 1:1 ratio

PayloadTooLargeError

Cause: Express body limit too small Fix: app.use(express.json({ limit: '10mb' }))

Facebook Timeline Looks Blurry

Cause: Normal Facebook behavior - they compress thumbnails Fix: None needed - full-size image is crisp when clicked

The Results

  • One-click scheduling for 10 days of posts
  • Zero manual posting required
  • High-quality images that look crisp on Facebook
  • No third-party tools or monthly fees
  • Full control over timing and content

Why Build This Instead of Using Buffer?

  1. Cost: Free vs $15+/month
  2. Control: Custom capture logic for React components
  3. Integration: Lives in the existing codebase
  4. Learning: Understanding the Facebook API is valuable
  5. Flexibility: Can extend to Instagram, Twitter, etc.

The Tech Stack

  • Frontend: React + TypeScript + TailwindCSS
  • Image Capture: html-to-image
  • Database: PostgreSQL (Neon)
  • Hosting: Netlify (with scheduled functions)
  • API: Facebook Graph API v24.0

Want to Build Something Similar?

The key insights:

  1. Capture the right element - avoid shadows and decorative CSS
  2. Clone at target size - don't scale up, render at full resolution
  3. Use 2048px for Facebook - prevents compression
  4. Store pre-captured images - scheduled functions can't render React
  5. Get a long-lived token - or you'll be refreshing every hour

This entire system was built in a few hours using AI-assisted development. The hardest part wasn't the code - it was figuring out Facebook's image quality quirks and token system.


Have questions about building automated social media systems? Get in touch - I love solving these kinds of problems.

Visual Summary

Shareable social cards summarising this guide.

Social Media Carousel

8 cards • Download as ZIP (images) or PDF (LinkedIn)

Download
1 of 8

Automate Facebook Posting

React + Netlify System

Build a scheduled posting system that captures React components as images and posts them automatically - no third-party tools.

JJM
Download
2 of 8
2048px
Image Quality

Facebook supports 720px, 960px, or 2048px. Using 2048px prevents Facebook from resizing and keeps text crisp.

JJM
Download
3 of 8

The Architecture

  • 1

    React component renders the design

  • 2

    html-to-image captures at 2048x2048

  • 3

    Base64 PNG stored in PostgreSQL

  • 4

    Netlify scheduled function posts daily

  • 5

    Facebook Graph API receives the image

JJM
Download
4 of 8

The Clone Technique

Clone the element at full 2048px size before capturing. This ensures text renders crisply instead of being scaled up from a smaller capture.

JJM
Download
5 of 8

Capture the Right Element

Before

Wrong: Capture container with shadows

After

Right: Capture only content wrapper

JJM
Download
6 of 8

What You Need

  • React app with design components

  • Netlify account with scheduled functions

  • Facebook Developer App with page access

  • PostgreSQL database for image queue

JJM
Download
7 of 8
Key Takeaway

Why Build It Yourself?

No monthly fees for scheduling tools. Full control over image quality. Integrates with your existing React components.

JJM
Download
8 of 8

Need Custom Automation?

We build marketing automation systems that integrate with your tech stack.

Get in Touch
JJM

Share This Article

Spread the knowledge

Free Strategy Session

Stop Guessing.
Start Growing.

Get a custom strategy built around your goals, not generic advice. Real insights. Measurable results.

No obligation
30-min call
Custom strategy

Continue Your Learning Journey

Explore these related articles to deepen your understanding of marketing automation

Why Your Logo Disappears in Dark-Mode Email (One-Line Fix)

Your email logo looks perfect in your editor but vanishes white-on-white in dark-mode inboxes. Here is the raster-bake fix and the one-line CSS guard.

9 min read
Read →

Netlify Site Stopped Updating? Auto-Publish Is Off

Netlify builds go green, deploys say ready, but the live site never changes. The cause is auto-publishing disabled by a forgotten rollback. Here's the fix.

6 min read
Read →

Auto-Draft Blog Posts From AI Sessions (Building JJM 2)

Auto-draft blog posts from every AI coding session with a three-subagent pipeline — sanitised summaries only, never raw logs reach your blog.

14 min read
Read →