# Instagram feed in Next.js: App Router, Pages Router and static export

**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.

> Three working ways to put Instagram posts on a Next.js site in 2026, what each one needs from the account owner, and the code for each.

Canonical: https://electricblaze.com/developer/instagram-feed/nextjs.html

## At a glance

- **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](https://electricblaze.com/developer/instagram-feed/media-url-expired.html) before launch.
- **What the human does:** the account owner signs in at [a.electricblaze.com](https://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 electricblaze@0.1.2 add instagram-feed --json` for 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](https://electricblaze.com/pricing.html)) |
| 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:

```html
<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:

```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:

```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-loading` to 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:

```sh
npx electricblaze@0.1.2 add instagram-feed --json
```

Output in a Next.js project with the `@/*` alias (trimmed):

```json
{
  "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:

```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](https://electricblaze.com/developer/instagram-feed/schema.html): `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:

1. 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](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18).
2. 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](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18). See [access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html).
3. `media_url` links 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](https://electricblaze.com/developer/instagram-feed/media-url-expired.html).
4. Telling a reel from another video needs `media_product_type`, and the IG Media reference from Meta marks it, like `caption`, as available with Facebook Login only ([Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18). Test both fields with your own token. See [media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html).

lib/instagram.ts:

```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/image` rejects the host.** Instagram and demo image hosts are not in `images.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.com` and `api.electricblaze.com`. Add both to `script-src` and `connect-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](https://electricblaze.com/developer/instagram-feed/feed-not-working.html) for the error codes.

## FAQ

### Does the ElectricBlaze widget work with the Next.js App Router?

Yes. Load `widget.js` with `next/script` and render a `div` whose first class is `electricblaze-id-` plus the widget id. The loader watches the DOM, so client-side navigation mounts it too.

### Can I show an Instagram feed in Next.js without a Meta developer app?

Yes, with the widget: the account owner connects Instagram in the ElectricBlaze dashboard and you paste the snippet. Your own Graph API code always needs a Meta app and a token.

### Is the CLI component showing real posts?

Not yet. It renders twelve demo posts and labels them. The JSON API that feeds it real posts ships in 0.2; until then use the widget for real posts.

### Does it work with output: 'export'?

Yes. The widget runs in the browser and needs no server. The CLI component renders at build time.

### Can I filter reels only?

In the CLI component pass `formats={["reel"]}`. In your own code the reel flag is `media_product_type` equal to `REELS`, a field Meta documents for Facebook Login only; check that your token returns it.

## Related pages

- [React and Vite](https://electricblaze.com/developer/instagram-feed/react.html)
- [Astro](https://electricblaze.com/developer/instagram-feed/astro.html)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html)
- [Feed schema](https://electricblaze.com/developer/instagram-feed/schema.html)
- [Compare feed APIs](https://electricblaze.com/developer/instagram-feed/comparison.html)

---

ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/nextjs.html
