# Instagram feed in Astro: widget, build-time JSON and an on-demand endpoint

**Status, checked 2026-09-18:** real Instagram posts reach an Astro site through the ElectricBlaze widget today. For Astro, `npx electricblaze add instagram-feed` writes demo JSON and types only; the JSON API with real data ships in 0.2.

> How to show Instagram posts on an Astro 5, 6 or 7 site deployed to Cloudflare or Netlify, what survives a fully static build, and where the Instagram token is allowed to live.

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

## At a glance

- **Use it when:** the site is built with Astro 5 or newer, deploys to Cloudflare, Netlify or any static host, and one section should list recent Instagram posts.
- **Do not use it when:** real posts must be baked into the prerendered HTML today. The widget draws in the browser and the CLI JSON is sample data, so before 0.2 only your own build-time code does that.
- **What the human does:** the site owner creates an Instagram Feed widget at [a.electricblaze.com](https://a.electricblaze.com/), connects the Instagram account there and sends you the widget id. No Meta developer app is involved.
- **Next step:** paste the widget component below if a widget id exists. Otherwise run `npx electricblaze@0.1.2 add instagram-feed --json` and build the grid from the demo JSON.

## Three options for Astro

Astro renders at two moments: during the build and, with an adapter, on each request. The widget adds a third moment, the visitor's browser. Each option below lives in one of them.

| Option | Posts | Runs at | Needs from the owner | Weak spot |
|---|---|---|---|---|
| ElectricBlaze widget | Real, today | Visitor's browser | A widget id from the dashboard | Posts are not in the static HTML; the Free plan includes 1000 views a month ([pricing](https://electricblaze.com/pricing.html)) |
| CLI demo JSON | Sample data until 0.2 | Build | Nothing | Twelve fixed posts from a fictional coffee roaster |
| Your own Graph API code | Real, today | Build or request | A Meta app, a professional account, a token | The token lasts 60 days and `media_url` links expire |

Pick the widget when the owner wants real posts soon and nobody will look after a Meta app. Pick the demo JSON when the layout comes first and the account can wait. Pick your own code when posts have to exist in the HTML at build time.

## The widget as an Astro component

The owner copies a two-line embed from the dashboard:

```html
<div class="electricblaze-id-WIDGET_ID"></div>
<script src="https://s.electricblaze.com/widget.js" defer></script>
```

Wrap it in a component and mark the script `is:inline`. Astro processes and bundles `<script>` tags by default. The [Astro docs](https://docs.astro.build/en/guides/client-side-scripts/) require `is:inline` for scripts that come from outside `src/`, such as a CDN (read 2026-09-18). With the directive Astro emits the tag exactly as written.

src/components/InstagramWidget.astro:

```astro
---
interface Props {
  /** From the ElectricBlaze dashboard, "Add to website". */
  widgetId: string;
}
const { widgetId } = Astro.props;
---
<!-- electricblaze-id-... must be the first class on this element -->
<div class={`electricblaze-id-${widgetId}`}></div>
<script is:inline src="https://s.electricblaze.com/widget.js" defer></script>
```

Drop `<InstagramWidget widgetId="abc123" />` into any page or layout. The grid appears once the page has loaded. Columns, rows, captions and the choice between gallery and slider are chosen in the dashboard, so the component takes no layout props.

The loader reads the id from the first class only. Your own classes may follow it, never precede it.

## View Transitions with ClientRouter

With `<ClientRouter />` from `astro:transitions` in the layout, Astro fetches the next page and replaces the whole `<body>` instead of doing a full load. We read `widget.js` 0.0.2 on 2026-09-18 to check how it copes:

- It scans the document when the DOM is ready, then watches it with a MutationObserver (childList, subtree). The observer sits on `document` itself, so a swapped-in `<body>` that carries a widget container is noticed and mounted.
- Astro may execute an inline script again when a visitor comes back to a page. That costs nothing: the loader checks for `window.electricblaze` first and a second copy exits.

You therefore need neither `data-astro-rerun` nor `transition:persist` for the widget. Moving the `<script is:inline>` tag into the layout `<head>` also works, because Astro leaves head scripts in place when the next page has them too.

To avoid a jump when the grid arrives, give `.electricblaze-state-loading` a `min-height` in a global stylesheet or a `<style is:global>` block.

## Demo JSON in the frontmatter

The CLI ships no Astro component. It finds `astro` in `package.json`, switches to its data-only kit and writes two files: the feed and its types.

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

Output of 0.1.2 in an Astro project that has a `src/` directory (notes trimmed):

```json
{
  "ok": true,
  "component": "instagram-feed",
  "source": "instagram",
  "framework": "data",
  "files": [
    {
      "path": "src/lib/eb/feed.d.ts",
      "status": "written"
    },
    {
      "path": "src/lib/eb/demo/instagram.json",
      "status": "written"
    }
  ],
  "demo": true,
  "notes": [
    "astro detected: no component template for it yet, so only the feed JSON and types are written. Build the component against them (see the skill), or pass --framework=html for the plain HTML kit."
  ],
  "next": "To show real posts today, connect the account in the ElectricBlaze widget and paste its embed snippet (https://electricblaze.com). The JSON API for this component (npx electricblaze connect instagram) ships in 0.2. Until then this is a demo feed."
}
```

Import the JSON in a component frontmatter. The import is resolved during the build, so the browser receives plain HTML with `<img>` tags and no feed code.

src/components/InstagramFeed.astro:

```astro
---
import type { Feed } from "../lib/eb/feed";
import demo from "../lib/eb/demo/instagram.json";

interface Props { limit?: number }
const { limit = 9 } = Astro.props;
const feed = demo as Feed;
const posts = feed.posts.slice(0, limit);
---
<section class="ig">
  {feed.demo && <p class="ig-demo">demo feed</p>}
  <ul>
    {posts.map((post) => (
      <li>
        <a href={post.url} target="_blank" rel="noopener">
          <img src={post.thumbnailUrl} alt={post.text} loading="lazy" />
          {post.type !== "image" && <span class="badge">{post.type}</span>}
        </a>
      </li>
    ))}
  </ul>
</section>

<style>
  .ig ul { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 4px; padding: 0; list-style: none; }
  .ig a { position: relative; display: block; }
  .ig img { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; }
  .badge { position: absolute; top: 6px; right: 6px; font-size: 12px; }
</style>
```

Each post follows [schema v1](https://electricblaze.com/developer/instagram-feed/schema.html). Images, videos and carousels always carry `thumbnailUrl`, so one `<img>` per tile is enough. `type` chooses the badge, and `format` separates a reel from a regular post.

The sample media mixes square, 4:5, 9:16 and 16:9 shapes. Keep the "demo feed" label for as long as `feed.demo` is `true`. `npx electricblaze connect instagram` exits with code 2 today; real data for this component comes with the 0.2 API.

## Your own code: at build time or in an endpoint

Calling the Instagram API directly from Astro is possible. The owner has to supply three things first:

1. A professional account. The Instagram API with Instagram Login does not serve personal accounts ([Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18). The Basic Display API that once did reached end of life 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. A Meta app and a long-lived token. It is valid for 60 days ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18), so a scheduled job must refresh it. [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html) walks through that job.
3. Tolerance for image links that die. `media_url` points at Instagram's CDN and those URLs expire; [media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html) has the details.

Declare the token as a server secret with `astro:env`. Avoid `import.meta.env` here: since Astro 6 its values are always inlined into the build output ([Astro v6 upgrade guide](https://docs.astro.build/en/guides/upgrade-to/v6/), read 2026-09-18). Secrets from `astro:env/server` stay out of the bundle. A `PUBLIC_` prefix would hand the token to every visitor.

astro.config.mjs:

```js
import { defineConfig, envField } from "astro/config";

export default defineConfig({
  env: {
    schema: {
      IG_TOKEN: envField.string({ context: "server", access: "secret" }),
    },
  },
});
```

src/lib/instagram.ts:

```ts
import { IG_TOKEN } from "astro:env/server";

const FIELDS = "id,caption,media_type,media_product_type,media_url,thumbnail_url,permalink,timestamp";

export async function getInstagramMedia(limit = 9) {
  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", IG_TOKEN);
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Instagram API ${res.status}`);
  return (await res.json()).data as Array<Record<string, string>>;
}
```

Called from the frontmatter of a prerendered page, `getInstagramMedia()` runs once per build. The posts then stay as they were on build day, and their image links break later. Rebuild on a schedule through a Cloudflare or Netlify deploy hook, or render on request.

Rendering on request needs `@astrojs/cloudflare` or `@astrojs/netlify`, and the adapter supplies `astro:env` secrets at runtime.

src/pages/api/instagram.json.ts:

```ts
import type { APIRoute } from "astro";
import { getInstagramMedia } from "../../lib/instagram";

// Rendered per request by the Cloudflare or Netlify adapter.
export const prerender = false;

export const GET: APIRoute = async () => {
  const media = await getInstagramMedia();
  return Response.json(media, { headers: { "Cache-Control": "public, max-age=600" } });
};
```

A page can opt out of prerendering the same way: put `export const prerender = false` in its frontmatter and call the helper there. In both cases the token remains in server code and the browser receives only post data.

## When the feed misbehaves in Astro

- **Empty box on the deployed site.** Check the class order first. Then check the Content Security Policy, often set in a `_headers` file on Cloudflare and Netlify: `script-src` must allow `https://s.electricblaze.com`, and `connect-src` must allow `https://api.electricblaze.com` and `https://proxy.electricblaze.com`.
- **Posts frozen on build day.** A frontmatter fetch on a static page ran once. Schedule rebuilds, set `export const prerender = false`, or switch to the widget.
- **Broken thumbnails on an older build.** Instagram image links expired. [Feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html) lists the causes and fixes.
- **Token works locally, fails in production.** `IG_TOKEN` exists in `.env` but not in the Cloudflare or Netlify environment.

## FAQ

### Does the ElectricBlaze widget work with Astro ClientRouter?

Yes. Astro swaps the `<body>` on navigation, and the loader watches the whole document with a MutationObserver, so it mounts the container in the new body. A second run of the script does nothing.

### Why does the widget script need is:inline in Astro?

Astro bundles `<script>` tags by default. A file served from `s.electricblaze.com` has to stay a plain tag, and `is:inline` keeps it that way.

### Can a fully static Astro site show real Instagram posts?

Yes, with the widget: it fetches posts client-side, so no server is required. Your own build-time code also works, but its posts stay the same until the next build.

### Where does the CLI put its files in an Astro project?

In `src/lib/eb/`: `feed.d.ts` holds the types and `demo/instagram.json` holds twelve demo posts. A project without `src/` gets them in `lib/eb/`.

### Can the Instagram token go in an Astro client script?

No. Every visitor can read client code. Keep the token in `astro:env/server` and call Instagram from frontmatter or an on-demand endpoint.

## Related pages

- [Next.js App Router](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [SvelteKit](https://electricblaze.com/developer/instagram-feed/sveltekit.html)
- [Static HTML sites](https://electricblaze.com/developer/instagram-feed/static-site.html)
- [Instagram access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Expired media URLs](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)
- [Schema v1 reference](https://electricblaze.com/developer/instagram-feed/schema.html)

---

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