RSS to Instagram PHP Bot

Ship RSS to Instagram with a Tiny PHP Bot (No Plugins, No CMS)

Most blogs already syndicate updates through RSS—but Instagram is still a manual grind. In this mini-build, we turn any site (ours wasn’t WordPress) into an auto-publisher: new RSS items → square, readable title cards → posted to an Instagram Professional account via the Instagram Graph API.

The goal is practical and reproducible:

  • Input: your site’s RSS feed.
  • Transform: generate a clean 1080×1080 image with the post title (high contrast, safe margins, custom font).
  • Output: publish to your own Instagram Business/Creator account using the two-step Graph flow (/media/media_publish).

Why this approach?

  • No third-party schedulers: Great tools exist, but monthly fees add up. We wanted a zero-subscription path we control.
  • Works with any stack: It’s plain PHP with cURL and GD. Drop-in files; no Composer required.
  • Readable thumbnails: Instead of reusing wide (1200×630) OG images that shrink poorly in the grid, we render a square title card that stays legible on mobile.

What we built (at a glance)

  • rss_to_instagram.php — polls your RSS, de-dupes posts, creates a media container, then publishes.
  • iggen.php — a tiny image endpoint that renders title-only square cards (configurable padding, font, colors). Returns JPEG (IG requirement).
  • Optional helpers used during setup (not required in production): OAuth login to get a user token, exchange for a page token, quick “test publish” scripts, and small diagnostics.

Requirements (the non-negotiables)

  • Instagram Professional account (Business or Creator) linked to a Facebook Page.
  • A Facebook App in Development mode, with you as Admin/Tester.
  • Permissions requested at login: instagram_basic, pages_show_list, pages_read_engagement, and instagram_content_publish.
  • Use a Page Access Token when publishing (user tokens won’t publish).

Security & housekeeping

  • Never commit secrets. In the sample code we’ll publish, all tokens/IDs are redacted and loaded from environment variables or a local config file ignored by Git.
  • Add a simple file lock so your cron job can’t double-run.
  • Consider a tiny de-dup store (file or DB) keyed by RSS guid to avoid accidental reposts.

What you’ll learn

  1. How to grab a long-lived user token, derive a Page Access Token, and publish safely.
  2. How to generate consistently readable IG images with GD (font selection, padding, fallback).
  3. How to harden a quick script for real-world use (timeouts, error logging, limits).

In the next section, I’ll share the redacted code snippets and a step-by-step setup. If you can run PHP and edit a couple of files, you can ship this in under an hour—and finally let Instagram take care of itself.

Prerequisites

  • Instagram Professional (Business or Creator) account linked to a Facebook Page.

  • A Facebook App in Development mode. You (the publisher) are Admin/Tester.

  • When logging in to get tokens, request these scopes:
    instagram_basic, pages_show_list, pages_read_engagement, instagram_content_publish.

  • You will publish with a Page Access Token (not a user token).

File Tree


/your-project
├─ .env # never commit this
├─ rss_to_instagram.php # RSS → IG publisher
├─ rss2ig/iggen.php # 1080×1080 title-card generator (JPEG)
└─ fonts/Inter-ExtraBold.ttf # or any font with Turkish/Latin-Ext

Redacted Config (ENV)

Create a .env (or use your own secrets store). Example keys—do not hardcode:


IG_USER_ID=1784xxxxxxxxxxxxx
PAGE_ACCESS_TOKEN=EAAG-REDACTED
RSS_URL=https://example.com/feed
CAPTION_TEMPLATE={title}

Read more: {link}

#qna #news
MAX_ITEMS=3

In PHP, a tiny loader (put at the top of each script):



// env loader (very small)
foreach (@file(__DIR__.’/.env’, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
if (strpos($line,’=’)!==false) { [$k,$v]=explode(‘=’,$line,2); $_ENV[trim($k)]=trim($v); }
}
function env($k,$d=null){ return $_ENV[$k] ?? $d; }

rss2ig/iggen.php (Title Card Generator)

  • Input: ?mode=title&title=...&brand=#0f172a&fg=#ffffff&padx=160&pady=140&font=inter

  • Output: 1080×1080 JPEG with auto-wrapped title, safe margins, custom font.

Keep your TTFs in /fonts and map them by name. Draw text with imagettftext.
Ensure your PHP GD has FreeType. Return Content-Type: image/jpeg.

Minimal skeleton (redacted/short):



<?php
// ==============================================
// This file is redacted and documented in English.
// Secrets are replaced with placeholders. Use env/config in production.
// ==============================================

$W = $H = 1080;
$mode = $_GET[‘mode’] ?? ‘title’;
$title = trim(// Params: title text. Optional brand/fg colors, padx/pady, font.
$_GET[‘title’] ?? ”);
$brand = $_GET[‘brand’] ?? ‘#0f172a’;
$fg = $_GET[‘fg’] ?? ‘#ffffff’;
$FONT = rtrim($_SERVER[‘DOCUMENT_ROOT’] ?? ”, ‘/’).’/fonts/Rubik-SemiBold.ttf’;

function hex2rgb($hex){ $hex=ltrim($hex,’#’); if(strlen($hex)==3){$hex=$hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];}
return [hexdec(substr($hex,0,2)),hexdec(substr($hex,2,2)),hexdec(substr($hex,4,2))]; }

$im = // Canvas: create 1080×1080 square
imagecreatetruecolor($W,$H);
[$r,$g,$b] = hex2rgb($brand);
$bg = imagecolorallocate($im,$r,$g,$b);
// Background fill (brand color)
imagefilledrectangle($im,0,0,$W,$H,$bg);

$margin = 80;
$pad = isset($_GET[‘pad’]) ? max(20, (int)$_GET[‘pad’]) : 80;
$padx = isset($_GET[‘padx’]) ? max(20, (int)$_GET[‘padx’]) : $pad;
$pady = isset($_GET[‘pady’]) ? max(20, (int)$_GET[‘pady’]) : $pad;
$maxFS = isset($_GET[‘maxfs’])? max(24, (int)$_GET[‘maxfs’]): 78;
$minFS = isset($_GET[‘minfs’])? max(20, (int)$_GET[‘minfs’]): 36;

$maxW = $W – 2*$padx;
$maxH = $H – 2*$pady;
$lineGap= 12;

[$fr,$fgc,$fb] = hex2rgb($fg);
$color = imagecolorallocate($im,$fr,$fgc,$fb);

function wrapFit($text,$font,$maxFS,$minFS,$maxW,$maxH,$lineGap){
for($fs=$maxFS; $fs>=$minFS; $fs-=2){
$words=preg_split(‘/\s+/u’,$text); $lines=[]; $cur=”;
foreach($words as $w){
$test = trim($cur==”?$w:”$cur $w”);
$box = imagettfbbox($fs,0,$font,$test); $tw=$box[2]-$box[0];
if($tw <= $maxW){ $cur=$test; } else { $lines[]=$cur; $cur=$w; }
}
if($cur!==”) $lines[]=$cur;
$h=0; foreach($lines as $ln){ $bb=imagettfbbox($fs,0,$font,$ln); $h+=($bb[1]-$bb[7])+$lineGap; }
if($h-$lineGap <= $maxH){ return [$lines,$fs]; }
}
$text=mb_substr($text,0,140).’…’;
return wrapFit($text,$font,$minFS,$minFS,$maxW,$maxH,$lineGap);
}

[$lines,$fs] = wrapFit($title,$FONT,$maxFS,$minFS,$maxW,$maxH,$lineGap);

$totH=0; foreach($lines as $ln){ $bb=imagettfbbox($fs,0,$FONT,$ln); $totH+=($bb[1]-$bb[7])+$lineGap; }
$totH-=$lineGap;
$y = (int)(($H-$totH)/2);
$x = $padx;

foreach ($lines as $ln) {
$bb = imagettfbbox($fs, 0, $FONT, $ln);
$lh = ($bb[1] – $bb[7]);
$baseX = $x;
$baseY = $y + $lh;

$stroke = 2;
$shadow = imagecolorallocate($im, 0, 0, 0);
for ($dx = -$stroke; $dx <= $stroke; $dx++) {
for ($dy = -$stroke; $dy <= $stroke; $dy++) {
if ($dx === 0 && $dy === 0) continue;
// Draw text with TTF font (requires GD + FreeType)
imagettftext($im, $fs, 0, $baseX + $dx, $baseY + $dy, $shadow, $FONT, $ln);
}
}

imagettftext($im, $fs, 0, $baseX, $baseY, $color, $FONT, $ln);

$y += $lh + $lineGap;
}

$label = $_GET[‘label’] ?? ‘qna.com.tr’;
$labelFS = isset($_GET[‘labelfs’]) ? max(18,(int)$_GET[‘labelfs’]) : 28;

$bbL = imagettfbbox($labelFS, 0, $FONT, $label);
$labelH = $bbL[1] – $bbL[7];
$labelX = $padx;
$labelY = $H – $pady + $labelH + 4;
$shadow = imagecolorallocate($im, 0, 0, 0);
for ($dx = -1; $dx <= 1; $dx++) {
for ($dy = -1; $dy <= 1; $dy++) {
if ($dx === 0 && $dy === 0) continue;
imagettftext($im, $labelFS, 0, $labelX + $dx, $labelY + $dy, $shadow, $FONT, $label);
}
}
imagettftext($im, $labelFS, 0, $labelX, $labelY, $color, $FONT, $label);

header(‘Content-Type: image/webp’);
header(‘Cache-Control: public, max-age=604800, immutable’);
imagewebp($im,null,92);
imagedestroy($im);

Tip: If your titles are very long, cap character count (~140) and add .

rss_to_instagram.php (Publisher)

Key points:

  • Reads RSS and limits how many items per run (e.g., 3).

  • De-dupes by guid (file or DB).

  • Creates a media container with the generated image URL → publishes.

  • Uses Page Access Token.


<!--?php // ============================================== // Redacted + English documented version. // Replace placeholders with your own values. // ============================================== // CONFIG: Instagram user id (Graph 'ig_user_id') const IG_USER_ID = 'YOUR_IG_USER_ID'; // CONFIG: Page Access Token (required for publishing) const PAGE_TOKEN = 'YOUR_PAGE_ACCESS_TOKEN'; const RSS_URL = 'https://qna.com.tr/feed'; const FALLBACK_IMG = 'https://qna.com.tr/img/og/og-image.png'; const CAPTION_TPL = "{title}\n\nRead more: {link}\n\n#qna #rss #automation"; const MAX_ITEMS = 3; // Helper: simple GET request with curl function http_get($u){ $ch = curl_init($u); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => 1,<br ?--> CURLOPT_FOLLOWLOCATION => 1,
CURLOPT_TIMEOUT => 20,
CURLOPT_USERAGENT => ‘qna-ig-bot/1.0’
]);
$r = curl_exec($ch);
curl_close($ch);
return $r;
}

// Helper: simple POST request with curl
function http_post($u, $f){
$ch = curl_init($u);
curl_setopt_array($ch, [
CURLOPT_POST => 1,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_POSTFIELDS => $f,
CURLOPT_TIMEOUT => 30,
CURLOPT_USERAGENT => ‘qna-ig-bot/1.0’
]);
$r = curl_exec($ch);
curl_close($ch);
return $r;
}

// Find an image on the article page (og:image first, fallback to first <img />)
function find_img($html){
if (preg_match(‘/property=[“\’]og:image[“\’][^>]+content=[“\’]([^”\’]+)/i’, $html, $m)) {
return html_entity_decode($m[1]);
}
if (preg_match(‘/<img[^>]+src=[“\’]([^”\’]+)/i’, $html, $m)) {
return html_entity_decode($m[1]);
}
return null;
}

// Build caption within 2200 chars
function caption($title, $link){
$c = str_replace([‘{title}’,'{link}’], [$title, $link], CAPTION_TPL);
return mb_strlen($c) > 2200 ? mb_substr($c, 0, 2190).’…’ : $c;
}

// Fetch and parse RSS
$rss = @simplexml_load_string(http_get(RSS_URL));
if (!$rss) { exit(“RSS error\n”); }

$posted = 0;

// Iterate RSS items and publish up to MAX_ITEMS
foreach ($rss->channel->item as $it) {
if ($posted >= MAX_ITEMS) break;

$title = (string)$it->title;
$link = (string)$it->link ?: (string)$it->guid;

// Resolve an image from the article (optional, we generate our own title card anyway)
$html = http_get($link);
$img = find_img($html) ?: FALLBACK_IMG;

// Generate a square title-card image via your generator endpoint (JPEG)
$image_url = ‘https://qna.com.tr/rss2ig/iggen.php?mode=title’
. ‘&title=’ . urlencode($title)
. ‘&brand=’ . urlencode(‘#0f172a’)
. ‘&fg=’ . urlencode(‘#ffffff’)
. ‘&padx=160&pady=140&maxfs=84&minfs=40’
. ‘&label=’ . urlencode(‘qna.com.tr’)
. ‘&labelfs=28’;

// Step 1: create media container (image_url + caption)
$create = http_post(“https://graph.facebook.com/v20.0/”.IG_USER_ID.”/media”, [
‘image_url’ => $image_url,
‘caption’ => caption($title, $link),
‘access_token’ => PAGE_TOKEN
]);

$cj = json_decode($create, true);
if (empty($cj[‘id’])) {
error_log(“container error: $create”);
continue;
}

// Step 2: publish the media (after container creation)
$pub = http_post(“https://graph.facebook.com/v20.0/”.IG_USER_ID.”/media_publish”, [
‘creation_id’ => $cj[‘id’],
‘access_token’ => PAGE_TOKEN
]);

$pj = json_decode($pub, true);
if (!empty($pj[‘id’])) {
echo “OK: {$title}\n”;
$posted++;
} else {
error_log(“publish error: $pub”);
}
}

echo “Done: $posted\n”;

RSS to Instagram PHP Bot Cron

Run every 30 minutes (adjust to taste):



*/30 * * * * /usr/bin/curl -fsS ‘https://yourdomain.com/rss_to_instagram.php’ >/dev/null

RSS to Instagram PHP Bot –> Troubleshooting (fast)

  • “Requires instagram_content_publish” → Re-login requesting that scope; use the new Page Access Token.

  • “This method must be called with a Page Access Token” → You’re using a user token. Exchange user → page token via /{PAGE_ID}?fields=access_token.

  • Containers fail silently → Ensure iggen.php returns JPEG, 200 OK, publicly reachable.

  • Overposting → Use the same $posted variable to check and increment; add a file lock.

 

FAQ — RSS to Instagram PHP Bot

1) Why won’t publishing work with a user token?
Instagram publishing requires a Page Access Token. Convert your user token via: /{PAGE_ID}?fields=access_token.

2) “Requires instagram_content_publish permission”
Re-login requesting: instagram_basic, pages_show_list, pages_read_engagement, instagram_content_publish. Then generate a new Page Access Token.

3) “This method must be called with a Page Access Token”
You’re using a user token. Publishing endpoints must use the Page Access Token.

4) /me/accounts returns empty (“No pages”)
Common causes:

  • IG is not Professional (Business/Creator) or not linked to a Facebook Page.

  • The login FB user isn’t an Admin (New Pages Experience: Full control required).

  • Missing pages_show_list in scopes.

5) What image URL formats work?
Publicly reachable HTTP(S) 200 URL; JPEG/PNG only (no WebP/private URLs/CDN 403).

6) Why 1080×1080 instead of OG 1200×630?
Square grid shrinks wide OG images; text becomes unreadable. Use iggen.php to render square title cards.

7) It posts too many items—how do I limit?
Use one counter for check and increment:



$posted=0; foreach(…) { if($posted>=MAX_ITEMS) break; if($publish_ok) $posted++; }

Also add a file lock to avoid overlapping cron runs.

8) Avoiding duplicates?
Store a hash of RSS guid (or link) after a successful publish (file or DB). Skip if seen.

9) Title text missing on images?
Ensure GD + FreeType is enabled and the TTF path exists (imagettftext() available).

10) Ideal title length/lines?
Aim for 2–4 lines in the grid. Tune padx/pady and maxfs/minfs; truncate with an ellipsis when needed.

11) Can I post videos?
Yes, via Graph API, but it needs different upload params and processing/wait steps. This guide covers JPEG photos.

12) Cross-post to the Facebook Page too?
Possible with extra permissions (e.g., pages_manage_posts). IG’s built-in auto-share isn’t guaranteed for API-posted items.

13) Token refresh cadence?
Long-lived user token ≈ 60 days. Refresh user token and regenerate a Page Access Token on a schedule.

14) Rate limits / cron best practice?
Run every 30–60 min, cap with MAX_ITEMS, log errors (error_log) and inspect container/publish JSON.

15) How do I keep secrets out of the repo?
Use a .env or secrets store. Replace hardcoded values with YOUR_… placeholders and load from env at runtime.

RSS to Instagram PHP Bot Related Links:

Github : Github RSS to Instagram

How to Use AI to Generate Blog Content Ideas in 2025

Best AI Tools for Writing Code Faster in 2025

1 thought on “RSS to Instagram PHP Bot”

Leave a Comment