At a glance
Status, checked 2026-09-18: in Nuxt, real Instagram posts come from the ElectricBlaze widget today. npx electricblaze add instagram-feed gives a Nuxt project sample data plus types, not a Vue component; real data through the JSON API arrives in 0.2.
- Use it when
- the project runs Nuxt 4 (source in
app/) with server rendering,nuxt generateor a mix of both, and a page needs recent Instagram posts. - Do not use it when
- the grid must be server-rendered Vue markup with real posts right now. The widget draws on the client and the CLI data is a demo, so that case needs your own server route and a Meta app.
- What the human does
- the account owner opens a.electricblaze.com, builds an Instagram Feed widget, links their Instagram account to it and passes the widget id to the developer. No Meta app or token changes hands.
- Next step
- with a widget id, copy the component below. Without one, run
npx [email protected] add instagram-feed --jsonand serve the demo feed fromserver/api/.
Where each option runs
A Nuxt page can get posts from the browser, from a Nitro server route, or from a file read at build time. The table matches the three options to those places.
| Option | Real posts | Runs in | Owner provides | Watch out for |
|---|---|---|---|---|
Widget via useHead | Yes, today | The browser | A widget id | Needs <ClientOnly>; Free plan: 5 widgets, 1000 views a month (pricing) |
| Demo JSON from the CLI | No, demo until 0.2 | Nitro or the build | Nothing | Files land in lib/eb/, outside app/ |
| Server route on the Graph API | Yes, today | Nitro | Meta app, token, refresh | Token lifetime and expiring image links |
Start with the widget unless you have a reason not to. It is the only option with real posts that asks nothing technical of the owner. The server route suits teams that already operate a Meta app.
Widget component with useHead and ClientOnly
This is the embed code the dashboard hands out:
<div class="electricblaze-id-WIDGET_ID"></div>
<script src="https://s.electricblaze.com/widget.js" defer></script>
In Nuxt, the script moves into useHead and the container into a component template.
app/components/InstagramWidget.vue
<script setup lang="ts">
useHead({
script: [{ src: "https://s.electricblaze.com/widget.js", defer: true }],
});
</script>
<template>
<ClientOnly>
<!-- electricblaze-id-... has to stay the first class -->
<div class="electricblaze-id-WIDGET_ID" />
<template #fallback>
<div class="ig-placeholder" />
</template>
</ClientOnly>
</template>
Use it in a page as <InstagramWidget />. Nuxt auto-imports everything in app/components/, so no import line is needed.
Why the container sits in ClientOnly
The loader adds electricblaze-state-loading, -mounting, -mounted or -error to the container, then fills it with the grid. If that happens before Vue hydrates the page, Vue meets markup it did not render and can report a hydration mismatch. <ClientOnly> creates the container only after hydration. The #fallback slot holds its place in the server HTML; give .ig-placeholder a height to avoid a jump.
The loader still finds the late container, because it keeps a MutationObserver on the whole document. The same observer handles <NuxtLink> navigation: a container on the next page mounts without a reload.
To load the script on every page, list it under app.head.script in nuxt.config.ts instead. Loading it twice is harmless; the second copy stops at its window.electricblaze check.
Gallery or slider, columns, rows and captions are configured in the dashboard. The Vue component has no props for them.
Demo feed through server/api/ and useAsyncData
The CLI has no Vue template yet. When it finds nuxt in package.json, it writes the feed JSON and its types and nothing more.
The files land in lib/eb/ at the project root, not in app/. The CLI only checks for a src/ directory, and Nuxt 4 keeps its source in app/. In Nuxt 4, ~ points at app/ and ~~ at the root, so app code imports them as ~~/lib/eb/....
npx [email protected] add instagram-feed --json
Result from 0.1.2 in a Nuxt 4 project (dir and notes trimmed)
{
"ok": true,
"framework": "data",
"files": [
{
"path": "lib/eb/feed.d.ts",
"status": "written"
},
{
"path": "lib/eb/demo/instagram.json",
"status": "written"
}
],
"demo": true,
"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."
}
Serve the file from a server route. The page fetches /api/instagram and never learns where the data came from, so the source can change later without touching Vue code.
server/api/instagram.get.ts
import type { Feed } from "../../lib/eb/feed";
import demo from "../../lib/eb/demo/instagram.json";
export default defineEventHandler(() => demo as Feed);
app/pages/index.vue
<script setup lang="ts">
const { data: feed } = await useAsyncData("instagram", () => $fetch("/api/instagram"));
</script>
<template>
<section v-if="feed" class="ig">
<p v-if="feed.demo" class="ig-demo">demo feed</p>
<ul>
<li v-for="post in feed.posts.slice(0, 9)" :key="post.id">
<NuxtLink :to="post.url" target="_blank">
<img :src="post.thumbnailUrl" :alt="post.text" loading="lazy" />
<span v-if="post.type !== 'image'" class="badge">{{ post.type }}</span>
</NuxtLink>
</li>
</ul>
</section>
</template>
useAsyncData runs the request on the server while rendering and ships the result to the browser in the payload, so the grid is not fetched twice. Nitro types $fetch("/api/instagram") from the route, so feed arrives as a typed schema v1 object.
Leave the "demo feed" tag visible as long as feed.demo stays true. The command meant for real data answers like this today, with exit code 2:
npx electricblaze connect instagram --json on 2026-09-18
{
"ok": false,
"command": "connect",
"status": "not_available_yet",
"message": "\"connect\" needs the ElectricBlaze API and ships in 0.2. Everything else works in demo mode today.",
"next": "Keep the demo feed for now; read https://electricblaze.com for the API status."
}
Server route with a private token
Writing the Instagram call yourself fits Nuxt well: the token stays in Nitro and the page keeps its useAsyncData call. The owner's side is heavier:
- a professional Instagram account, because the API with Instagram Login rejects personal ones (Meta: Instagram Platform overview, checked 2026-09-18);
- a Meta app issuing a long-lived token, good for 60 days (Meta: access_token reference, checked 2026-09-18), which a job you run must refresh;
- no fallback to Basic Display, which reached end of life on 2024-12-04 (Meta developer blog, 2024-09-04, checked 2026-09-18).
Declare the token in runtimeConfig outside public, then set NUXT_IG_TOKEN in the host's environment. Nuxt maps a NUXT_ variable only when its key is declared in nuxt.config.ts.
nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
igToken: "", // filled from NUXT_IG_TOKEN at runtime; server only
},
routeRules: {
"/api/instagram": { swr: 3600 },
"/": { isr: 3600 }, // Netlify and Vercel; use prerender: true for a static build
},
});
server/api/instagram.get.ts
const FIELDS = "id,caption,media_type,media_product_type,media_url,thumbnail_url,permalink,timestamp";
export default defineEventHandler(async (event) => {
const { igToken } = useRuntimeConfig(event);
const res = await $fetch<{ data: Record<string, string>[] }>("https://graph.instagram.com/me/media", {
query: { fields: FIELDS, limit: 9, access_token: igToken },
});
return res.data;
});
useRuntimeConfig(event) reads the value when the request arrives, so a refreshed token applies without a rebuild. Anything under runtimeConfig.public is sent to every browser; the token must never go there.
$fetch throws when Instagram returns an error status, and Nitro turns that into a 500 from /api/instagram. Show a fallback in the page through the error ref of useAsyncData.
Return schema v1 from the route and the demo template above keeps working: map permalink to url, caption to text, and thumbnail_url or media_url to thumbnailUrl.
Caching with routeRules
swr: 3600on/api/instagramkeeps the route output in the server cache for an hour and renews it in the background, so Instagram is called roughly once an hour per server cache rather than once per visitor.isr: 3600on the page lets the CDN hold the HTML. The Nuxt rendering docs name Netlify and Vercel as the platforms for it (read 2026-09-18).prerender: true, ornuxt generatefor the whole site, renders at build time. The route runs once, and the posts stay as they were until the next build.
Whatever you cache, the media_url links inside expire on Instagram's side, so an old cache or build ends up with broken images. Media URL expired explains the lifetime problem.
Nuxt-specific problems
- Import of
~/lib/eb/feedfails. In Nuxt 4~meansapp/. Use~~/lib/eb/feedor a relative path. NUXT_IG_TOKENis set butigTokenis empty. The key is missing fromruntimeConfiginnuxt.config.ts, and Nuxt ignores variables it has no key for.- Hydration warning next to the widget. The container is rendered on the server. Wrap it in
<ClientOnly>. - Posts never change on a generated site.
nuxt generatecalled the API route once. Deploy with a server andswr, or use the widget, which fetches in the browser. - Token expired after two months. The long-lived token was never refreshed. Feed not working lists the symptoms.