At a glance
Status, checked 2026-09-18: the ElectricBlaze widget is how a SvelteKit app shows real Instagram posts today. In SvelteKit projects npx electricblaze add instagram-feed writes demo JSON and types only; real data for them waits for the 0.2 JSON API.
- Use it when
- the app runs SvelteKit 2 with Svelte 5 runes, on any adapter including
adapter-static, and a route or layout should show recent Instagram posts. - Do not use it when
- reels must look different from ordinary videos with real data today. The widget renderer does not read
media_product_type, so it shows a reel as a plain video; request that field in your own Graph API code instead. - What the human does
- the account owner signs in at a.electricblaze.com, sets up an Instagram Feed widget for their account and shares its id with you. That is the whole manual part.
- Next step
- with a widget id, add the Svelte component below to a route. Without one, run
npx [email protected] add instagram-feed --jsonand render the demo feed from a load function.
Choosing an approach
SvelteKit can produce a feed at build time, on each request, or hand it to the browser. The approaches below map onto those modes.
| Approach | Data | Rendered by | Owner's task | Limitation |
|---|---|---|---|---|
Widget in <svelte:head> | Real posts now | The loader script, in the browser | Share a widget id | Reels appear as plain videos; the Free plan counts 1000 views a month (pricing) |
Demo feed in +page.server.ts | Twelve sample posts | Server or prerender | None | Real data needs the 0.2 API |
| Own Graph API call | Real posts now | Server or prerender | Meta app, token, refresh job | Token expiry after 60 days, expiring image links |
If no one on the project will own a Meta app, take the widget. If you are designing the section before the owner signs up, begin with the demo feed. Write your own call when you need Instagram fields the widget leaves out.
The widget in a Svelte component
The dashboard gives the owner an embed like this one:
<div class="electricblaze-id-WIDGET_ID"></div>
<script src="https://s.electricblaze.com/widget.js" defer></script>
In SvelteKit, split it in two. The script tag goes into <svelte:head>; the container stays in the markup.
src/lib/components/InstagramWidget.svelte
<svelte:head>
<script src="https://s.electricblaze.com/widget.js" defer></script>
</svelte:head>
<!-- Paste the widget id from the dashboard as a literal. The id class must come first. -->
<div class="electricblaze-id-WIDGET_ID"></div>
Render <InstagramWidget /> in any +page.svelte or +layout.svelte. To load the script on every route instead, put the same <script> line into src/app.html right after %sveltekit.head%.
Why the id is a literal, not a prop
The loader looks at the first class only, and it adds state classes to the container while it works. A class built from a prop, such as class="electricblaze-id-{widgetId}", is dynamic. When a dynamic class differs from the server HTML, Svelte 5 writes it again during hydration and drops the state classes. A literal class is static, and hydration leaves it untouched.
Client-side navigation
SvelteKit changes routes without reloading the page. The loader copes on its own: containers added later, for example after a client-side route change, are picked up; removed ones are released.
When <svelte:head> renders in the browser, Svelte 5 recreates its <script> elements so that they execute. A repeat execution is harmless, because the loader exits early when window.electricblaze already exists.
Grid or slider, columns and captions are edited in the dashboard. The Svelte side never grows beyond these few lines.
Demo feed in a server load
For SvelteKit the CLI writes data rather than a component. It sees @sveltejs/kit in package.json, selects the data kit and puts both files under src/lib/eb/, which SvelteKit exposes as $lib/eb/.
npx [email protected] add instagram-feed --json
Printed by 0.1.2 in a SvelteKit project (dir and notes trimmed)
{
"ok": true,
"framework": "data",
"files": [
{
"path": "src/lib/eb/feed.d.ts",
"status": "written"
},
{
"path": "src/lib/eb/demo/instagram.json",
"status": "written"
}
],
"demo": true,
"notes": [
"@sveltejs/kit 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."
]
}
src/routes/+page.server.ts
import type { PageServerLoad } from "./$types";
import type { Feed } from "$lib/eb/feed";
import demo from "$lib/eb/demo/instagram.json";
export const prerender = true;
export const load: PageServerLoad = () => {
const feed = demo as Feed;
return { feed: { ...feed, posts: feed.posts.slice(0, 9) } };
};
src/routes/+page.svelte
<script lang="ts">
import type { PageProps } from "./$types";
let { data }: PageProps = $props();
</script>
<section class="ig">
{#if data.feed.demo}<p class="ig-demo">demo feed</p>{/if}
<ul>
{#each data.feed.posts as post (post.id)}
<li>
<a href={post.url} target="_blank" rel="noopener">
<img src={post.thumbnailUrl} alt={post.text} loading="lazy" />
{#if post.type !== "image"}<span class="badge">{post.type}</span>{/if}
</a>
</li>
{/each}
</ul>
</section>
The load function runs on the server, and export const prerender = true turns the route into a static file at build time. let { data }: PageProps = $props() is the Svelte 5 form; PageProps exists since SvelteKit 2.16.
Posts follow schema v1. Show thumbnailUrl, link to url, badge the video and carousel types, and keep the "demo feed" label while data.feed.demo is true. There is no key to set yet: npx electricblaze connect instagram answers not_available_yet with exit code 2 until 0.2.
Own Graph API code and environment modules
You can skip ElectricBlaze and call Instagram from a server load. The owner then needs a Meta app and a professional account (Meta: Instagram Platform overview, checked 2026-09-18). Access for personal accounts through Basic Display ended on 2024-12-04 (Meta developer blog, 2024-09-04, checked 2026-09-18).
The token is the hard part. A long-lived one is valid for 60 days (Meta: access_token reference, checked 2026-09-18), and some job has to refresh it; see access tokens. Keep the request in src/lib/server/, a folder SvelteKit never lets client code import.
src/lib/server/instagram.ts
import { IG_TOKEN } from "$env/static/private";
const FIELDS = "id,caption,media_type,media_product_type,media_url,thumbnail_url,permalink,timestamp";
export async function fetchInstagram(fetch: typeof globalThis.fetch, 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>>;
}
Call it from +page.server.ts as await fetchInstagram(fetch). Which environment module to import depends on how that route renders:
- Prerendered route. Use
$env/static/private. SvelteKit 2 refuses to read dynamic variables during prerendering (SvelteKit 2 migration guide, read 2026-09-18). Posts freeze at build time, so schedule rebuilds. - Route rendered per request. Use
$env/dynamic/privateand readenv.IG_TOKEN. Static values are written into the bundle at build time, so only the dynamic module picks up a refreshed token without a rebuild.
On a per-request route, add setHeaders({ "cache-control": "public, max-age=600" }) in the load function so repeat visits can come from a cache instead of Instagram.
Image links from media_url point at Instagram's CDN and expire, which leaves a prerendered page with broken images after a while. Media URL expired covers why and what to do.
SvelteKit problems and fixes
- Build error on
$env/static/private. A.sveltefile or a universal+page.tsimported it. Move the code into+page.server.tsorsrc/lib/server/. That error is the guard that keeps the token out of the browser. - Prerendering fails on
$env/dynamic/private. Switch that route to the static module, or stop prerendering it. - The widget renders nothing. Either the first class is not
electricblaze-id-..., orkit.cspinsvelte.config.jsblocks it. Allowhttps://s.electricblaze.comunderscript-src, plushttps://api.electricblaze.comandhttps://proxy.electricblaze.comunderconnect-src. - Content jumps when the grid appears. Reserve height for
.electricblaze-state-loadingwith:global(...)or a global stylesheet. Svelte drops scoped selectors for classes it never renders.
This page targets SvelteKit 2, the stable line on 2026-09-18. SvelteKit 3 is a release candidate that renames $lib to #lib and moves environment variables into src/env.ts, so adjust imports if you run it.