@saketsawrav/instagram-feed 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/dist/chunk-QEYBYUKH.mjs +92 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +120 -0
- package/dist/index.mjs +10 -0
- package/dist/nextjs.d.mts +7 -0
- package/dist/nextjs.d.ts +7 -0
- package/dist/nextjs.js +155 -0
- package/dist/nextjs.mjs +44 -0
- package/dist/types-CJuNiTch.d.mts +12 -0
- package/dist/types-CJuNiTch.d.ts +12 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# @saketsawrav/instagram-feed
|
|
2
|
+
|
|
3
|
+
Drop-in Instagram feed for Next.js apps. Fetches recent posts and proxies images server-side — no third-party widgets, no client-side scraping, no API keys required.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Fetches posts via Instagram's public web profile API
|
|
8
|
+
- Server-side image proxy (avoids Instagram's cross-origin blocking)
|
|
9
|
+
- In-memory caching with configurable TTL (default: 1 hour)
|
|
10
|
+
- Ready-made Next.js route handler factories
|
|
11
|
+
- Framework-agnostic core — use the fetcher anywhere Node.js runs
|
|
12
|
+
- Zero runtime dependencies
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @saketsawrav/instagram-feed
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start (Next.js)
|
|
21
|
+
|
|
22
|
+
### 1. Create the feed API route
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// app/api/instagram/route.ts
|
|
26
|
+
import { createFeedHandler } from '@saketsawrav/instagram-feed/nextjs';
|
|
27
|
+
|
|
28
|
+
export const GET = createFeedHandler({ username: 'your_username' });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Create the image proxy route
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// app/api/instagram/image/route.ts
|
|
35
|
+
import { createImageProxyHandler } from '@saketsawrav/instagram-feed/nextjs';
|
|
36
|
+
|
|
37
|
+
export const GET = createImageProxyHandler({ username: 'your_username' });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Fetch posts from your component
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
'use client';
|
|
44
|
+
|
|
45
|
+
import { useEffect, useState } from 'react';
|
|
46
|
+
import type { InstagramPost } from '@saketsawrav/instagram-feed';
|
|
47
|
+
|
|
48
|
+
export default function InstagramGrid() {
|
|
49
|
+
const [posts, setPosts] = useState<InstagramPost[]>([]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
fetch('/api/instagram')
|
|
53
|
+
.then((res) => res.json())
|
|
54
|
+
.then(setPosts)
|
|
55
|
+
.catch(() => {});
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}>
|
|
60
|
+
{posts.map((post) => (
|
|
61
|
+
<a
|
|
62
|
+
key={post.shortcode}
|
|
63
|
+
href={`https://www.instagram.com/p/${post.shortcode}/`}
|
|
64
|
+
target="_blank"
|
|
65
|
+
rel="noopener noreferrer"
|
|
66
|
+
>
|
|
67
|
+
<img
|
|
68
|
+
src={`/api/instagram/image?shortcode=${post.shortcode}`}
|
|
69
|
+
alt=""
|
|
70
|
+
style={{ width: '100%', aspectRatio: '1', objectFit: 'cover' }}
|
|
71
|
+
loading="lazy"
|
|
72
|
+
/>
|
|
73
|
+
</a>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
Both `createFeedHandler` and `createImageProxyHandler` accept the same options:
|
|
83
|
+
|
|
84
|
+
| Option | Type | Default | Description |
|
|
85
|
+
|--------------|----------|---------------|--------------------------------|
|
|
86
|
+
| `username` | `string` | **required** | Instagram username to fetch |
|
|
87
|
+
| `count` | `number` | `9` | Number of posts to return |
|
|
88
|
+
| `cacheTtlMs` | `number` | `3600000` (1h)| In-memory cache TTL in ms |
|
|
89
|
+
| `igAppId` | `string` | built-in | Instagram app ID for API calls |
|
|
90
|
+
|
|
91
|
+
## Framework-Agnostic Usage
|
|
92
|
+
|
|
93
|
+
The core fetcher works in any Node.js environment:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { fetchInstagramPosts } from '@saketsawrav/instagram-feed';
|
|
97
|
+
|
|
98
|
+
const posts = await fetchInstagramPosts({ username: 'your_username', count: 6 });
|
|
99
|
+
console.log(posts);
|
|
100
|
+
// [{ shortcode: 'abc123', thumbnailUrl: 'https://...' }, ...]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Available exports from `@saketsawrav/instagram-feed`
|
|
104
|
+
|
|
105
|
+
| Export | Description |
|
|
106
|
+
|-------------------------|--------------------------------------------------|
|
|
107
|
+
| `fetchInstagramPosts()` | Fetch posts for a username (returns `InstagramPost[]`) |
|
|
108
|
+
| `getInstagramImageUrl()`| Get the full-res image URL for a shortcode |
|
|
109
|
+
| `proxyInstagramImage()` | Fetch and stream an image from Instagram CDN |
|
|
110
|
+
| `InstagramPost` | Type: `{ shortcode: string; thumbnailUrl: string }` |
|
|
111
|
+
| `InstagramFeedOptions` | Type: configuration options |
|
|
112
|
+
|
|
113
|
+
### Available exports from `@saketsawrav/instagram-feed/nextjs`
|
|
114
|
+
|
|
115
|
+
| Export | Description |
|
|
116
|
+
|-----------------------------|---------------------------------------------|
|
|
117
|
+
| `createFeedHandler()` | Returns a Next.js GET handler for the feed |
|
|
118
|
+
| `createImageProxyHandler()` | Returns a Next.js GET handler for image proxy |
|
|
119
|
+
|
|
120
|
+
## Why server-side?
|
|
121
|
+
|
|
122
|
+
Instagram blocks cross-origin image requests from browsers. This package solves that by:
|
|
123
|
+
|
|
124
|
+
1. Fetching post metadata server-side (no browser CORS issues)
|
|
125
|
+
2. Proxying images through your own API route (your domain serves the images)
|
|
126
|
+
3. Caching both metadata and image URLs in memory (one Instagram API call per TTL window)
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/fetcher.ts
|
|
2
|
+
var DEFAULT_COUNT = 9;
|
|
3
|
+
var DEFAULT_CACHE_TTL_MS = 36e5;
|
|
4
|
+
var DEFAULT_IG_APP_ID = "936619743392459";
|
|
5
|
+
var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
|
|
6
|
+
var cacheByUsername = /* @__PURE__ */ new Map();
|
|
7
|
+
function buildHeaders(username, igAppId) {
|
|
8
|
+
return {
|
|
9
|
+
"User-Agent": USER_AGENT,
|
|
10
|
+
"x-ig-app-id": igAppId,
|
|
11
|
+
Accept: "*/*",
|
|
12
|
+
"Sec-Fetch-Mode": "cors",
|
|
13
|
+
"Sec-Fetch-Site": "same-origin",
|
|
14
|
+
"Sec-Fetch-Dest": "empty",
|
|
15
|
+
Referer: `https://www.instagram.com/${username}/`
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function fetchFromInstagram(username, count, igAppId) {
|
|
19
|
+
const res = await fetch(
|
|
20
|
+
`https://www.instagram.com/api/v1/users/web_profile_info/?username=${username}`,
|
|
21
|
+
{ headers: buildHeaders(username, igAppId) }
|
|
22
|
+
);
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
return { posts: [], imageMap: {}, timestamp: Date.now() };
|
|
25
|
+
}
|
|
26
|
+
const json = await res.json();
|
|
27
|
+
const edges = json?.data?.user?.edge_owner_to_timeline_media?.edges ?? [];
|
|
28
|
+
const posts = [];
|
|
29
|
+
const imageMap = {};
|
|
30
|
+
for (const edge of edges) {
|
|
31
|
+
const node = edge?.node;
|
|
32
|
+
if (!node?.shortcode) continue;
|
|
33
|
+
const thumbnailUrl = node.thumbnail_src ?? node.display_url ?? "";
|
|
34
|
+
const displayUrl = node.display_url ?? node.thumbnail_src ?? "";
|
|
35
|
+
imageMap[node.shortcode] = displayUrl;
|
|
36
|
+
if (posts.length < count) {
|
|
37
|
+
posts.push({ shortcode: node.shortcode, thumbnailUrl });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { posts, imageMap, timestamp: Date.now() };
|
|
41
|
+
}
|
|
42
|
+
function getCached(username, cacheTtlMs) {
|
|
43
|
+
const entry = cacheByUsername.get(username);
|
|
44
|
+
if (entry && Date.now() - entry.timestamp < cacheTtlMs) return entry;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
async function ensureCache(opts) {
|
|
48
|
+
const existing = getCached(opts.username, opts.cacheTtlMs);
|
|
49
|
+
if (existing) return existing;
|
|
50
|
+
const fresh = await fetchFromInstagram(opts.username, opts.count, opts.igAppId);
|
|
51
|
+
if (fresh.posts.length > 0) {
|
|
52
|
+
cacheByUsername.set(opts.username, fresh);
|
|
53
|
+
}
|
|
54
|
+
return fresh;
|
|
55
|
+
}
|
|
56
|
+
function resolveOptions(opts) {
|
|
57
|
+
return {
|
|
58
|
+
username: opts.username,
|
|
59
|
+
count: opts.count ?? DEFAULT_COUNT,
|
|
60
|
+
cacheTtlMs: opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
61
|
+
igAppId: opts.igAppId ?? DEFAULT_IG_APP_ID
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function fetchInstagramPosts(opts) {
|
|
65
|
+
const resolved = resolveOptions(opts);
|
|
66
|
+
const cached = await ensureCache(resolved);
|
|
67
|
+
return cached.posts;
|
|
68
|
+
}
|
|
69
|
+
async function getInstagramImageUrl(opts, shortcode) {
|
|
70
|
+
const resolved = resolveOptions(opts);
|
|
71
|
+
const cached = await ensureCache(resolved);
|
|
72
|
+
return cached.imageMap[shortcode] ?? null;
|
|
73
|
+
}
|
|
74
|
+
async function proxyInstagramImage(imageUrl) {
|
|
75
|
+
const res = await fetch(imageUrl, {
|
|
76
|
+
headers: {
|
|
77
|
+
"User-Agent": USER_AGENT,
|
|
78
|
+
Referer: "https://www.instagram.com/"
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok || !res.body) return null;
|
|
82
|
+
return {
|
|
83
|
+
body: res.body,
|
|
84
|
+
contentType: res.headers.get("content-type") ?? "image/jpeg"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
fetchInstagramPosts,
|
|
90
|
+
getInstagramImageUrl,
|
|
91
|
+
proxyInstagramImage
|
|
92
|
+
};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { I as InstagramFeedOptions, a as InstagramPost } from './types-CJuNiTch.mjs';
|
|
2
|
+
|
|
3
|
+
declare function fetchInstagramPosts(opts: InstagramFeedOptions): Promise<InstagramPost[]>;
|
|
4
|
+
declare function getInstagramImageUrl(opts: InstagramFeedOptions, shortcode: string): Promise<string | null>;
|
|
5
|
+
declare function proxyInstagramImage(imageUrl: string): Promise<{
|
|
6
|
+
body: ReadableStream<Uint8Array>;
|
|
7
|
+
contentType: string;
|
|
8
|
+
} | null>;
|
|
9
|
+
|
|
10
|
+
export { InstagramFeedOptions, InstagramPost, fetchInstagramPosts, getInstagramImageUrl, proxyInstagramImage };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { I as InstagramFeedOptions, a as InstagramPost } from './types-CJuNiTch.js';
|
|
2
|
+
|
|
3
|
+
declare function fetchInstagramPosts(opts: InstagramFeedOptions): Promise<InstagramPost[]>;
|
|
4
|
+
declare function getInstagramImageUrl(opts: InstagramFeedOptions, shortcode: string): Promise<string | null>;
|
|
5
|
+
declare function proxyInstagramImage(imageUrl: string): Promise<{
|
|
6
|
+
body: ReadableStream<Uint8Array>;
|
|
7
|
+
contentType: string;
|
|
8
|
+
} | null>;
|
|
9
|
+
|
|
10
|
+
export { InstagramFeedOptions, InstagramPost, fetchInstagramPosts, getInstagramImageUrl, proxyInstagramImage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
fetchInstagramPosts: () => fetchInstagramPosts,
|
|
24
|
+
getInstagramImageUrl: () => getInstagramImageUrl,
|
|
25
|
+
proxyInstagramImage: () => proxyInstagramImage
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/fetcher.ts
|
|
30
|
+
var DEFAULT_COUNT = 9;
|
|
31
|
+
var DEFAULT_CACHE_TTL_MS = 36e5;
|
|
32
|
+
var DEFAULT_IG_APP_ID = "936619743392459";
|
|
33
|
+
var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
|
|
34
|
+
var cacheByUsername = /* @__PURE__ */ new Map();
|
|
35
|
+
function buildHeaders(username, igAppId) {
|
|
36
|
+
return {
|
|
37
|
+
"User-Agent": USER_AGENT,
|
|
38
|
+
"x-ig-app-id": igAppId,
|
|
39
|
+
Accept: "*/*",
|
|
40
|
+
"Sec-Fetch-Mode": "cors",
|
|
41
|
+
"Sec-Fetch-Site": "same-origin",
|
|
42
|
+
"Sec-Fetch-Dest": "empty",
|
|
43
|
+
Referer: `https://www.instagram.com/${username}/`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function fetchFromInstagram(username, count, igAppId) {
|
|
47
|
+
const res = await fetch(
|
|
48
|
+
`https://www.instagram.com/api/v1/users/web_profile_info/?username=${username}`,
|
|
49
|
+
{ headers: buildHeaders(username, igAppId) }
|
|
50
|
+
);
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
return { posts: [], imageMap: {}, timestamp: Date.now() };
|
|
53
|
+
}
|
|
54
|
+
const json = await res.json();
|
|
55
|
+
const edges = json?.data?.user?.edge_owner_to_timeline_media?.edges ?? [];
|
|
56
|
+
const posts = [];
|
|
57
|
+
const imageMap = {};
|
|
58
|
+
for (const edge of edges) {
|
|
59
|
+
const node = edge?.node;
|
|
60
|
+
if (!node?.shortcode) continue;
|
|
61
|
+
const thumbnailUrl = node.thumbnail_src ?? node.display_url ?? "";
|
|
62
|
+
const displayUrl = node.display_url ?? node.thumbnail_src ?? "";
|
|
63
|
+
imageMap[node.shortcode] = displayUrl;
|
|
64
|
+
if (posts.length < count) {
|
|
65
|
+
posts.push({ shortcode: node.shortcode, thumbnailUrl });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { posts, imageMap, timestamp: Date.now() };
|
|
69
|
+
}
|
|
70
|
+
function getCached(username, cacheTtlMs) {
|
|
71
|
+
const entry = cacheByUsername.get(username);
|
|
72
|
+
if (entry && Date.now() - entry.timestamp < cacheTtlMs) return entry;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
async function ensureCache(opts) {
|
|
76
|
+
const existing = getCached(opts.username, opts.cacheTtlMs);
|
|
77
|
+
if (existing) return existing;
|
|
78
|
+
const fresh = await fetchFromInstagram(opts.username, opts.count, opts.igAppId);
|
|
79
|
+
if (fresh.posts.length > 0) {
|
|
80
|
+
cacheByUsername.set(opts.username, fresh);
|
|
81
|
+
}
|
|
82
|
+
return fresh;
|
|
83
|
+
}
|
|
84
|
+
function resolveOptions(opts) {
|
|
85
|
+
return {
|
|
86
|
+
username: opts.username,
|
|
87
|
+
count: opts.count ?? DEFAULT_COUNT,
|
|
88
|
+
cacheTtlMs: opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
89
|
+
igAppId: opts.igAppId ?? DEFAULT_IG_APP_ID
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function fetchInstagramPosts(opts) {
|
|
93
|
+
const resolved = resolveOptions(opts);
|
|
94
|
+
const cached = await ensureCache(resolved);
|
|
95
|
+
return cached.posts;
|
|
96
|
+
}
|
|
97
|
+
async function getInstagramImageUrl(opts, shortcode) {
|
|
98
|
+
const resolved = resolveOptions(opts);
|
|
99
|
+
const cached = await ensureCache(resolved);
|
|
100
|
+
return cached.imageMap[shortcode] ?? null;
|
|
101
|
+
}
|
|
102
|
+
async function proxyInstagramImage(imageUrl) {
|
|
103
|
+
const res = await fetch(imageUrl, {
|
|
104
|
+
headers: {
|
|
105
|
+
"User-Agent": USER_AGENT,
|
|
106
|
+
Referer: "https://www.instagram.com/"
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok || !res.body) return null;
|
|
110
|
+
return {
|
|
111
|
+
body: res.body,
|
|
112
|
+
contentType: res.headers.get("content-type") ?? "image/jpeg"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
116
|
+
0 && (module.exports = {
|
|
117
|
+
fetchInstagramPosts,
|
|
118
|
+
getInstagramImageUrl,
|
|
119
|
+
proxyInstagramImage
|
|
120
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { I as InstagramFeedOptions, a as InstagramPost } from './types-CJuNiTch.mjs';
|
|
2
|
+
import { NextResponse, NextRequest } from 'next/server';
|
|
3
|
+
|
|
4
|
+
declare function createFeedHandler(opts: InstagramFeedOptions): () => Promise<NextResponse<InstagramPost[]>>;
|
|
5
|
+
declare function createImageProxyHandler(opts: InstagramFeedOptions): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
6
|
+
|
|
7
|
+
export { InstagramFeedOptions, InstagramPost, createFeedHandler, createImageProxyHandler };
|
package/dist/nextjs.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { I as InstagramFeedOptions, a as InstagramPost } from './types-CJuNiTch.js';
|
|
2
|
+
import { NextResponse, NextRequest } from 'next/server';
|
|
3
|
+
|
|
4
|
+
declare function createFeedHandler(opts: InstagramFeedOptions): () => Promise<NextResponse<InstagramPost[]>>;
|
|
5
|
+
declare function createImageProxyHandler(opts: InstagramFeedOptions): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
6
|
+
|
|
7
|
+
export { InstagramFeedOptions, InstagramPost, createFeedHandler, createImageProxyHandler };
|
package/dist/nextjs.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/nextjs.ts
|
|
21
|
+
var nextjs_exports = {};
|
|
22
|
+
__export(nextjs_exports, {
|
|
23
|
+
createFeedHandler: () => createFeedHandler,
|
|
24
|
+
createImageProxyHandler: () => createImageProxyHandler
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(nextjs_exports);
|
|
27
|
+
|
|
28
|
+
// src/handlers.ts
|
|
29
|
+
var import_server = require("next/server");
|
|
30
|
+
|
|
31
|
+
// src/fetcher.ts
|
|
32
|
+
var DEFAULT_COUNT = 9;
|
|
33
|
+
var DEFAULT_CACHE_TTL_MS = 36e5;
|
|
34
|
+
var DEFAULT_IG_APP_ID = "936619743392459";
|
|
35
|
+
var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
|
|
36
|
+
var cacheByUsername = /* @__PURE__ */ new Map();
|
|
37
|
+
function buildHeaders(username, igAppId) {
|
|
38
|
+
return {
|
|
39
|
+
"User-Agent": USER_AGENT,
|
|
40
|
+
"x-ig-app-id": igAppId,
|
|
41
|
+
Accept: "*/*",
|
|
42
|
+
"Sec-Fetch-Mode": "cors",
|
|
43
|
+
"Sec-Fetch-Site": "same-origin",
|
|
44
|
+
"Sec-Fetch-Dest": "empty",
|
|
45
|
+
Referer: `https://www.instagram.com/${username}/`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function fetchFromInstagram(username, count, igAppId) {
|
|
49
|
+
const res = await fetch(
|
|
50
|
+
`https://www.instagram.com/api/v1/users/web_profile_info/?username=${username}`,
|
|
51
|
+
{ headers: buildHeaders(username, igAppId) }
|
|
52
|
+
);
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
return { posts: [], imageMap: {}, timestamp: Date.now() };
|
|
55
|
+
}
|
|
56
|
+
const json = await res.json();
|
|
57
|
+
const edges = json?.data?.user?.edge_owner_to_timeline_media?.edges ?? [];
|
|
58
|
+
const posts = [];
|
|
59
|
+
const imageMap = {};
|
|
60
|
+
for (const edge of edges) {
|
|
61
|
+
const node = edge?.node;
|
|
62
|
+
if (!node?.shortcode) continue;
|
|
63
|
+
const thumbnailUrl = node.thumbnail_src ?? node.display_url ?? "";
|
|
64
|
+
const displayUrl = node.display_url ?? node.thumbnail_src ?? "";
|
|
65
|
+
imageMap[node.shortcode] = displayUrl;
|
|
66
|
+
if (posts.length < count) {
|
|
67
|
+
posts.push({ shortcode: node.shortcode, thumbnailUrl });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { posts, imageMap, timestamp: Date.now() };
|
|
71
|
+
}
|
|
72
|
+
function getCached(username, cacheTtlMs) {
|
|
73
|
+
const entry = cacheByUsername.get(username);
|
|
74
|
+
if (entry && Date.now() - entry.timestamp < cacheTtlMs) return entry;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
async function ensureCache(opts) {
|
|
78
|
+
const existing = getCached(opts.username, opts.cacheTtlMs);
|
|
79
|
+
if (existing) return existing;
|
|
80
|
+
const fresh = await fetchFromInstagram(opts.username, opts.count, opts.igAppId);
|
|
81
|
+
if (fresh.posts.length > 0) {
|
|
82
|
+
cacheByUsername.set(opts.username, fresh);
|
|
83
|
+
}
|
|
84
|
+
return fresh;
|
|
85
|
+
}
|
|
86
|
+
function resolveOptions(opts) {
|
|
87
|
+
return {
|
|
88
|
+
username: opts.username,
|
|
89
|
+
count: opts.count ?? DEFAULT_COUNT,
|
|
90
|
+
cacheTtlMs: opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
91
|
+
igAppId: opts.igAppId ?? DEFAULT_IG_APP_ID
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function fetchInstagramPosts(opts) {
|
|
95
|
+
const resolved = resolveOptions(opts);
|
|
96
|
+
const cached = await ensureCache(resolved);
|
|
97
|
+
return cached.posts;
|
|
98
|
+
}
|
|
99
|
+
async function getInstagramImageUrl(opts, shortcode) {
|
|
100
|
+
const resolved = resolveOptions(opts);
|
|
101
|
+
const cached = await ensureCache(resolved);
|
|
102
|
+
return cached.imageMap[shortcode] ?? null;
|
|
103
|
+
}
|
|
104
|
+
async function proxyInstagramImage(imageUrl) {
|
|
105
|
+
const res = await fetch(imageUrl, {
|
|
106
|
+
headers: {
|
|
107
|
+
"User-Agent": USER_AGENT,
|
|
108
|
+
Referer: "https://www.instagram.com/"
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok || !res.body) return null;
|
|
112
|
+
return {
|
|
113
|
+
body: res.body,
|
|
114
|
+
contentType: res.headers.get("content-type") ?? "image/jpeg"
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/handlers.ts
|
|
119
|
+
function createFeedHandler(opts) {
|
|
120
|
+
return async function GET() {
|
|
121
|
+
const posts = await fetchInstagramPosts(opts);
|
|
122
|
+
return import_server.NextResponse.json(posts, {
|
|
123
|
+
headers: {
|
|
124
|
+
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=7200"
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function createImageProxyHandler(opts) {
|
|
130
|
+
return async function GET(request) {
|
|
131
|
+
const shortcode = request.nextUrl.searchParams.get("shortcode");
|
|
132
|
+
if (!shortcode) {
|
|
133
|
+
return new import_server.NextResponse("Missing shortcode", { status: 400 });
|
|
134
|
+
}
|
|
135
|
+
const imageUrl = await getInstagramImageUrl(opts, shortcode);
|
|
136
|
+
if (!imageUrl) {
|
|
137
|
+
return new import_server.NextResponse("Not found", { status: 404 });
|
|
138
|
+
}
|
|
139
|
+
const result = await proxyInstagramImage(imageUrl);
|
|
140
|
+
if (!result) {
|
|
141
|
+
return new import_server.NextResponse("Image fetch failed", { status: 502 });
|
|
142
|
+
}
|
|
143
|
+
return new import_server.NextResponse(result.body, {
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": result.contentType,
|
|
146
|
+
"Cache-Control": "public, max-age=86400, s-maxage=86400"
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
152
|
+
0 && (module.exports = {
|
|
153
|
+
createFeedHandler,
|
|
154
|
+
createImageProxyHandler
|
|
155
|
+
});
|
package/dist/nextjs.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchInstagramPosts,
|
|
3
|
+
getInstagramImageUrl,
|
|
4
|
+
proxyInstagramImage
|
|
5
|
+
} from "./chunk-QEYBYUKH.mjs";
|
|
6
|
+
|
|
7
|
+
// src/handlers.ts
|
|
8
|
+
import { NextResponse } from "next/server";
|
|
9
|
+
function createFeedHandler(opts) {
|
|
10
|
+
return async function GET() {
|
|
11
|
+
const posts = await fetchInstagramPosts(opts);
|
|
12
|
+
return NextResponse.json(posts, {
|
|
13
|
+
headers: {
|
|
14
|
+
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=7200"
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function createImageProxyHandler(opts) {
|
|
20
|
+
return async function GET(request) {
|
|
21
|
+
const shortcode = request.nextUrl.searchParams.get("shortcode");
|
|
22
|
+
if (!shortcode) {
|
|
23
|
+
return new NextResponse("Missing shortcode", { status: 400 });
|
|
24
|
+
}
|
|
25
|
+
const imageUrl = await getInstagramImageUrl(opts, shortcode);
|
|
26
|
+
if (!imageUrl) {
|
|
27
|
+
return new NextResponse("Not found", { status: 404 });
|
|
28
|
+
}
|
|
29
|
+
const result = await proxyInstagramImage(imageUrl);
|
|
30
|
+
if (!result) {
|
|
31
|
+
return new NextResponse("Image fetch failed", { status: 502 });
|
|
32
|
+
}
|
|
33
|
+
return new NextResponse(result.body, {
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": result.contentType,
|
|
36
|
+
"Cache-Control": "public, max-age=86400, s-maxage=86400"
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
createFeedHandler,
|
|
43
|
+
createImageProxyHandler
|
|
44
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface InstagramPost {
|
|
2
|
+
shortcode: string;
|
|
3
|
+
thumbnailUrl: string;
|
|
4
|
+
}
|
|
5
|
+
interface InstagramFeedOptions {
|
|
6
|
+
username: string;
|
|
7
|
+
count?: number;
|
|
8
|
+
cacheTtlMs?: number;
|
|
9
|
+
igAppId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type { InstagramFeedOptions as I, InstagramPost as a };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface InstagramPost {
|
|
2
|
+
shortcode: string;
|
|
3
|
+
thumbnailUrl: string;
|
|
4
|
+
}
|
|
5
|
+
interface InstagramFeedOptions {
|
|
6
|
+
username: string;
|
|
7
|
+
count?: number;
|
|
8
|
+
cacheTtlMs?: number;
|
|
9
|
+
igAppId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type { InstagramFeedOptions as I, InstagramPost as a };
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saketsawrav/instagram-feed",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop-in Instagram feed for Next.js — fetches posts and proxies images server-side, no third-party widgets",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./nextjs": {
|
|
15
|
+
"types": "./dist/nextjs.d.ts",
|
|
16
|
+
"import": "./dist/nextjs.mjs",
|
|
17
|
+
"require": "./dist/nextjs.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup src/index.ts src/nextjs.ts --format cjs,esm --dts",
|
|
25
|
+
"dev": "tsup src/index.ts src/nextjs.ts --format cjs,esm --dts --watch",
|
|
26
|
+
"lint": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"next": ">=13.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"next": "^14.0.0",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"instagram",
|
|
39
|
+
"nextjs",
|
|
40
|
+
"feed",
|
|
41
|
+
"embed",
|
|
42
|
+
"image-proxy"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/lifesciencetrust/nextjs-instagram-feed"
|
|
48
|
+
}
|
|
49
|
+
}
|