At a glance
Status, checked 2026-09-18: the ElectricBlaze widget shows real posts in Next.js today. npx electricblaze add instagram-feed writes a Server Component with demo data; the JSON API that feeds it real posts ships in 0.2.
- Use it when
- the site runs on Next.js 13.4 or newer (App Router or Pages Router, on Vercel or as a static export) and needs a grid of recent Instagram posts or reels.
- Do not use it when
- you already maintain a Meta app with a token refresh job and an image cache. Then keep your own code and read media URL expired before launch.
- What the human does
- the account owner signs in at a.electricblaze.com, connects a Business or Creator Instagram account and copies the widget id. Nobody creates a Meta developer app.
- Next step
- run
npx [email protected] add instagram-feed --jsonfor the demo component, or paste the widget snippet below when the owner has a widget id.
Pick a route
All three routes end with posts on the page. They differ in who touches Meta, where the HTML is rendered and what breaks after launch.
| Route | Real posts | Rendered | The human does | Breaks when |
|---|---|---|---|---|
| Widget embed | Today | In the browser, by widget.js | Connects the account in the ElectricBlaze dashboard, copies the widget id | The Free plan passes 1,000 views a month (pricing) |
| CLI component | Demo data today, real posts in 0.2 | On the server, your JSX and CSS | Nothing until 0.2 | Not yet: it reads a bundled JSON file |
| Your own Graph API code | Today | On the server, your JSX and CSS | Creates a Meta app, converts to a professional account, hands over a token | The token is not refreshed within 60 days, or cached image links expire |
If the site must show real posts this week and the owner will not create a Meta app, use the widget. If the design matters more than the data, start with the CLI component and switch it to real data in 0.2. If you need fields the widget does not expose, such as the items inside a carousel, write your own code.
Route 1: the widget embed, real posts today
The owner creates an Instagram Feed widget in the dashboard and gets a snippet with a widget id. Outside React the snippet is two lines:
<div class="electricblaze-id-WIDGET_ID"></div>
<script src="https://s.electricblaze.com/widget.js" defer></script>
In Next.js load the script with next/script and keep the container as a plain element. This works in the App Router and in the Pages Router, and in a Server Component, because next/script handles the client part itself.
components/InstagramWidget.tsx
import Script from "next/script";
// The widget id comes from the ElectricBlaze dashboard (Add to website).
export function InstagramWidget({ widgetId }: { widgetId: string }) {
return (
<>
{/* Keep electricblaze-id-... as the FIRST class: the loader reads className.split(" ")[0]. */}
<div className={`electricblaze-id-${widgetId}`} />
<Script src="https://s.electricblaze.com/widget.js" strategy="afterInteractive" />
</>
);
}
app/page.tsx
import { InstagramWidget } from "@/components/InstagramWidget";
export default function Home() {
return (
<main>
<h2>On Instagram</h2>
<InstagramWidget widgetId="WIDGET_ID" />
</main>
);
}
Three details of the loader matter in a React app. We read them from widget.js version 0.0.2 on 2026-09-18:
- Client-side navigation works. The loader scans the document when the DOM is ready, then watches it with a MutationObserver (childList, subtree), so a widget on a page reached through
<Link>still mounts. - The id is the first class.
className="electricblaze-id-abc123 my-grid"works,className="my-grid electricblaze-id-abc123"renders nothing. - Loading states are classes. The loader adds electricblaze-state-loading, -mounting, -mounted or -error to the container. Reserve height with CSS on
.electricblaze-state-loadingto avoid layout shift.
Layout, columns, captions and the slider or gallery mode are set in the dashboard, not in code. The published plans: Free has 5 widgets, 1000 views a month and a refresh every 24 hours; Start is $5 per month, billed yearly and refreshes every hour.
Route 2: a Server Component from the CLI
One command writes a typed component, a loader and twelve demo posts. It never prompts, never overwrites files you edited, and prints JSON when asked:
npx [email protected] add instagram-feed --json
Output in a Next.js project with the @/* alias (trimmed)
{
"ok": true,
"framework": "next",
"files": [
{ "path": "components/eb/InstagramFeed.tsx", "status": "written" },
{ "path": "components/eb/InstagramFeed.module.css", "status": "written" },
{ "path": "lib/eb/instagram.ts", "status": "written" },
{ "path": "lib/eb/feed.d.ts", "status": "written" },
{ "path": "lib/eb/demo/instagram.json", "status": "written" },
{ "path": ".env.example", "status": "written" }
],
"demo": true,
"notes": [
"Render it in any App Router page: import InstagramFeed from \"@/components/eb/InstagramFeed\"; then <InstagramFeed limit={8} />"
]
}
app/page.tsx
import InstagramFeed from "@/components/eb/InstagramFeed";
export default function Home() {
return <InstagramFeed limit={8} formats={["reel"]} />;
}
InstagramFeed is an async Server Component: no client bundle, no useEffect, plain <img> tags with width and height. Every post follows schema v1: type says what to render, format says whether Instagram calls it a post or a reel. A "demo feed" label stays on while feed.demo is true.
Leave ELECTRICBLAZE_API_KEY unset until 0.2. With a key the loader calls the 0.2 endpoint, which does not answer yet, and throws. npx electricblaze doctor says so too.
Pages Router and static export
With output: 'export' Next.js renders every page at build time and ships plain files. Both ElectricBlaze routes work there:
- The widget runs in the browser, so a static export on GitHub Pages, S3 or shared hosting shows fresh posts without a rebuild.
- The CLI component renders at build time. With demo data that is all it needs. With the 0.2 API, posts change when the site is rebuilt, so schedule a rebuild or use the widget.
In the Pages Router use the same InstagramWidget component. For the CLI component, import getInstagramFeed from lib/eb/instagram.ts in getStaticProps and pass feed.posts to your own markup.
Route 3: your own code on the Instagram API
This is the route an agent usually writes first. It works, and it comes with four obligations that the snippet does not show:
- The account must be a Business or Creator account; Meta closed the Basic Display API for personal accounts on 2024-12-04 (Meta developer blog, 2024-09-04, checked 2026-09-18).
- Someone creates a Meta app and produces a long-lived token. It lasts 60 days and must be refreshed before it expires (Meta: access_token reference, checked 2026-09-18). See access tokens.
media_urllinks point at Instagram's CDN and stop working after a while, so an ISR page cached for days shows broken images. See media URL expired.- Telling a reel from another video needs
media_product_type, and the IG Media reference from Meta marks it, likecaption, as available with Facebook Login only (Meta: IG Media reference, checked 2026-09-18). Test both fields with your own token. See media fields.
lib/instagram.ts
import "server-only";
// caption is documented for Facebook Login only (checked 2026-09-18): test it with your token.
const FIELDS = "id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,children{media_type,media_url,thumbnail_url}";
export async function getInstagramPosts(limit = 12) {
const url = new URL("https://graph.instagram.com/me/media");
url.searchParams.set("fields", FIELDS);
url.searchParams.set("limit", String(limit));
url.searchParams.set("access_token", process.env.IG_ACCESS_TOKEN!);
// Revalidate well inside the life of a media_url link.
const res = await fetch(url, { next: { revalidate: 3600 } });
if (!res.ok) throw new Error(`Instagram API ${res.status}: ${await res.text()}`);
return (await res.json()).data as Array<Record<string, string>>;
}
Keep the token on the server. import "server-only" makes the build fail if a client component imports this file, and the token never reaches the browser.
What goes wrong in Next.js
next/imagerejects the host. Instagram and demo image hosts are not inimages.remotePatterns. Either add them or use<img>, as the CLI component does.- The widget shows nothing. The container lost its first class, or a Content Security Policy blocks
s.electricblaze.comandapi.electricblaze.com. Add both toscript-srcandconnect-src. - Posts froze after a deploy. A fully static page fetched once at build time. Use
revalidate, a rebuild schedule or the widget. - Error 190 after two months. The long-lived token expired. See feed not working for the error codes.