Next.js Integration

Introduction

This guide shows you how to integrate Feedframer into your Next.js application using both the App Router (Next.js 13+) and Pages Router approaches.

Installation & Setup

Step 1: Install Dependencies

No additional packages required! Next.js includes fetch natively.

Step 2: Add Environment Variables

Create .env.local:

# For server-side usage (Server Components, API Routes)
FEEDFRAMER_API_KEY=your_api_key_here

# For client-side usage (optional - safe for Feedframer!)
NEXT_PUBLIC_FEEDFRAMER_API_KEY=your_api_key_here

Note: Feedframer API keys are safe to expose in the browser since the API only serves publicly-available Instagram data. Using NEXT_PUBLIC_ prefix is perfectly fine for client-side fetching!

App Router (Next.js 13+)

Server Component

Fetch data directly in a server component (recommended for SEO and performance):

// app/instagram/page.tsx

interface Post {
  id: string;
  caption: string | null;
  mediaType: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM' | 'REELS';
  mediaUrl: string;
  thumbnailUrl: string | null;
  permalink: string;
  timestamp: string;
  likeCount: number | null;
  commentsCount: number | null;
}

interface FeedframerResponse {
  username: string;
  posts: Post[];
  pagination: {
    nextCursor: string | null;
    hasMore: boolean;
    perPage: number;
  };
}

async function getPosts(): Promise<Post[]> {
  const apiKey = process.env.FEEDFRAMER_API_KEY;

  const res = await fetch(
    `https://feedframer.com/api/v1/me?api_key=${apiKey}&page[size]=12`,
    {
      next: { revalidate: 3600 } // Cache for 1 hour
    }
  );

  if (!res.ok) {
    throw new Error('Failed to fetch posts');
  }

  const data: FeedframerResponse = await res.json();
  return data.posts;
}

export default async function InstagramPage() {
  const posts = await getPosts();

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Instagram Feed</h1>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
        {posts.map((post) => (
          <div key={post.id} className="bg-white rounded-lg shadow overflow-hidden">
            <img
              src={post.mediaUrl}
              alt={post.caption || 'Instagram post'}
              className="w-full h-64 object-cover"
            />

            <div className="p-4">
              <p className="text-sm text-gray-700 line-clamp-3">
                {post.caption}
              </p>

              {/* Engagement Stats */}
              <div className="flex items-center gap-4 mt-3 text-sm text-gray-600">
                {post.likeCount !== null && (
                  <span className="flex items-center gap-1">
                    ❤️ {post.likeCount.toLocaleString()}
                  </span>
                )}
                {post.commentsCount !== null && (
                  <span className="flex items-center gap-1">
                    💬 {post.commentsCount.toLocaleString()}
                  </span>
                )}
              </div>

              <a
                href={post.permalink}
                target="_blank"
                rel="noopener noreferrer"
                className="mt-4 inline-block text-blue-600 hover:underline"
              >
                View on Instagram →
              </a>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Client Component with useState

For interactive features:

// app/instagram/client-feed.tsx

'use client';

import { useState, useEffect } from 'react';

interface Post {
  id: string;
  caption: string | null;
  mediaType: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM' | 'REELS';
  mediaUrl: string;
  thumbnailUrl: string | null;
  permalink: string;
  timestamp: string;
  likeCount: number | null;
  commentsCount: number | null;
}
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchPosts() {
      try {
        const res = await fetch('/api/instagram/posts');

        if (!res.ok) {
          throw new Error('Failed to fetch posts');
        }

        const data = await res.json();
        setPosts(data.posts);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error');
      } finally {
        setLoading(false);
      }
    }

    fetchPosts();
  }, []);

  if (loading) {
    return <div>Loading Instagram posts...</div>;
  }

  if (error) {
    return <div>Error: {error}</div>;
  }

  return (
    <div className="grid grid-cols-3 gap-4">
      {posts.map((post) => (
        <div key={post.id}>
          <img src={post.mediaUrl} alt="" className="w-full" />
        </div>
      ))}
    </div>
  );
}

API Route (App Router)

Create an API route for server-side caching and data transformation:

// app/api/instagram/posts/route.ts

import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const type = searchParams.get('type');
  const size = searchParams.get('size') || '12';

  const apiKey = process.env.FEEDFRAMER_API_KEY;

  const params = new URLSearchParams({
    api_key: apiKey!,
    'page[size]': size,
  });

  if (type) {
    params.append('filter[type]', type);
  }

  try {
    const res = await fetch(
      `https://feedframer.com/api/v1/me?${params.toString()}`,
      {
        next: { revalidate: 3600 } // Cache for 1 hour
      }
    );

    if (!res.ok) {
      return NextResponse.json(
        { error: 'Failed to fetch posts' },
        { status: res.status }
      );
    }

    const data = await res.json();

    return NextResponse.json({
      posts: data.posts,
      pagination: data.pagination,
    });
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Pages Router (Next.js 12 and below)

getServerSideProps

Fetch data on each request:

// pages/instagram.tsx

import { GetServerSideProps } from 'next';

interface Post {
  id: string;
  caption: string | null;
  mediaType: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM' | 'REELS';
  mediaUrl: string;
  thumbnailUrl: string | null;
  permalink: string;
  timestamp: string;
  likeCount: number | null;
  commentsCount: number | null;
}

export default function InstagramPage({ posts }: Props) {
  return (
    <div>
      <h1>Instagram Feed</h1>

      <div className="grid grid-cols-3 gap-4">
        {posts.map((post) => (
          <div key={post.id}>
            <img src={post.mediaUrl} alt="" />
            <p>{post.caption}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async () => {
  const apiKey = process.env.FEEDFRAMER_API_KEY;

  const res = await fetch(
    `https://feedframer.com/api/v1/me?api_key=${apiKey}&page[size]=12`
  );

  if (!res.ok) {
    return {
      props: {
        posts: [],
      },
    };
  }

  const data = await res.json();

  return {
    props: {
      posts: data.posts,
    },
  };
};

getStaticProps

Fetch data at build time with revalidation:

// pages/instagram.tsx

import { GetStaticProps } from 'next';

export const getStaticProps: GetStaticProps = async () => {
  const apiKey = process.env.FEEDFRAMER_API_KEY;

  const res = await fetch(
    `https://feedframer.com/api/v1/me?api_key=${apiKey}&page[size]=12`
  );

  const data = await res.json();

  return {
    props: {
      posts: data.posts,
    },
    revalidate: 3600, // Revalidate every hour
  };
};

API Route (Pages Router)

// pages/api/instagram/posts.ts

import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'GET') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const apiKey = process.env.FEEDFRAMER_API_KEY;
  const { type, size = '12' } = req.query;

  const params = new URLSearchParams({
    api_key: apiKey!,
    'page[size]': size as string,
  });

  if (type) {
    params.append('filter[type]', type as string);
  }

  try {
    const response = await fetch(
      `https://feedframer.com/api/v1/me?${params.toString()}`
    );

    if (!response.ok) {
      return res.status(response.status).json({
        error: 'Failed to fetch posts',
      });
    }

    const data = await response.json();

    return res.status(200).json({
      posts: data.posts,
      pagination: data.pagination,
    });
  } catch (error) {
    return res.status(500).json({
      error: 'Internal server error',
    });
  }
}

Custom Hook

Create a reusable hook for client-side fetching:

// hooks/useFeedframer.ts

import { useState, useEffect } from 'react';

interface Post {
  id: string;
  caption: string | null;
  mediaType: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM' | 'REELS';
  mediaUrl: string;
  thumbnailUrl: string | null;
  permalink: string;
  timestamp: string;
  likeCount: number | null;
  commentsCount: number | null;
}
}

export function useFeedframer(options: UseFeedframerOptions = {}) {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchPosts() {
      try {
        const params = new URLSearchParams();

        if (options.type) {
          params.append('type', options.type);
        }

        if (options.size) {
          params.append('size', options.size.toString());
        }

        const res = await fetch(`/api/instagram/posts?${params.toString()}`);

        if (!res.ok) {
          throw new Error('Failed to fetch posts');
        }

        const data = await res.json();
        setPosts(data.posts);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error');
      } finally {
        setLoading(false);
      }
    }

    fetchPosts();
  }, [options.type, options.size]);

  return { posts, loading, error };
}

Usage:

'use client';

import { useFeedframer } from '@/hooks/useFeedframer';

export default function InstagramWidget() {
  const { posts, loading, error } = useFeedframer({
    type: 'IMAGE',
    size: 6,
  });

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div className="grid grid-cols-2 gap-2">
      {posts.map((post) => (
        <img key={post.id} src={post.mediaUrl} alt="" />
      ))}
    </div>
  );
}

GraphQL Integration

Fetching with GraphQL

async function getPostsGraphQL() {
  const apiKey = process.env.FEEDFRAMER_API_KEY;

  const query = `
    query {
      posts(first: 12, type: "IMAGE") {
        data {
          id
          caption
          mediaUrl
          publishedAt
          likeCount
          commentsCount
          instagramAccount {
            username
          }
        }
      }
    }
  `;

  const res = await fetch(
    `https://feedframer.com/graphql?api_key=${apiKey}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ query }),
      next: { revalidate: 3600 }
    }
  );

  const data = await res.json();
  return data.posts.posts.data;
}

Filtering

Filter by Type

// Fetch only images
const res = await fetch('/api/instagram/posts?type=IMAGE&size=12');

// Fetch only videos
const res = await fetch('/api/instagram/posts?type=VIDEO&size=12');

Filter by Date Range

const params = new URLSearchParams({
  api_key: apiKey!,
  'filter[publishedAfter]': '2024-01-01T00:00:00Z',
  'filter[publishedBefore]': '2024-12-31T23:59:59Z',
  'page[size]': '50',
});

const res = await fetch(`https://feedframer.com/api/v1/me?${params}`);

Pagination

Infinite Scroll

'use client';

import { useState } from 'react';

interface Post {
  id: string;
  caption: string | null;
  mediaType: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM' | 'REELS';
  mediaUrl: string;
  thumbnailUrl: string | null;
  permalink: string;
  timestamp: string;
  likeCount: number | null;
  commentsCount: number | null;
}
    const res = await fetch(`/api/instagram/posts?${params}`);
    const data = await res.json();

    setPosts((prev) => [...prev, ...data.posts]);
    setCursor(data.pagination.nextCursor);
    setLoading(false);
  }

  return (
    <div>
      <div className="grid grid-cols-3 gap-4">
        {posts.map((post) => (
          <div key={post.id}>
            <img src={post.mediaUrl} alt="" />
          </div>
        ))}
      </div>

      {cursor && (
        <button onClick={loadMore} disabled={loading}>
          {loading ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

Caching Strategies

Static Generation

Best for content that doesn't change often:

// App Router
async function getPosts() {
  const res = await fetch(url, {
    next: { revalidate: 3600 } // Revalidate hourly
  });
  return res.json();
}

API Route Caching

Cache API responses:

// Simple in-memory cache (production: use Redis)
const cache = new Map();

export async function GET(request: Request) {
  const cacheKey = 'instagram_posts';

  if (cache.has(cacheKey)) {
    const { data, timestamp } = cache.get(cacheKey);

    // Return cached data if less than 1 hour old
    if (Date.now() - timestamp < 3600000) {
      return NextResponse.json(data);
    }
  }

  // Fetch fresh data
  const res = await fetch(url);
  const data = await res.json();

  cache.set(cacheKey, { data, timestamp: Date.now() });

  return NextResponse.json(data);
}

Error Handling

Comprehensive Error Handling

async function getPosts() {
  try {
    const res = await fetch(url, {
      next: { revalidate: 3600 }
    });

    if (!res.ok) {
      const error = await res.json();

      if (error.errors?.[0]?.code === 'RATE_LIMIT_EXCEEDED') {
        console.warn('Rate limit exceeded');
        // Return cached data or empty array
        return [];
      }

      throw new Error('Failed to fetch posts');
    }

    const data = await res.json();
    return data.posts;

  } catch (error) {
    console.error('Error fetching Instagram posts:', error);
    return [];
  }
}

TypeScript Types

Define types for better type safety:

// types/feedframer.ts

export interface Post {
  id: string;
  caption: string | null;
  mediaType: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM' | 'REELS';
  mediaUrl: string;
  thumbnailUrl: string | null;
  permalink: string;
  timestamp: string;
  likeCount: number | null;
  commentsCount: number | null;
}

export interface FeedframerResponse {
  username: string;
  posts: Post[];
  pagination: {
    nextCursor: string | null;
    hasMore: boolean;
    perPage: number;
  };
}

export interface FeedframerError {
  errors: Array<{
    status: string;
    code: string;
    title: string;
    detail: string;
  }>;
}

Best Practices

  1. Choose Your Approach - Client-side (simpler, direct API calls) or Server-side (better for caching)
  2. Implement Caching - Cache for at least 1 hour (ISR, API routes, or React Query)
  3. Handle Errors Gracefully - Always provide fallback content
  4. Optimize Images - Use Next.js Image component for optimization
  5. Type Safety - Use TypeScript for better developer experience
  6. Client-Side is OK! - Feedframer keys are safe to expose since data is public

Complete Example

Full working example with App Router:

// app/instagram/page.tsx

import { Suspense } from 'react';
import InstagramFeed from '@/components/InstagramFeed';

export default function InstagramPage() {
  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Instagram Feed</h1>

      <Suspense fallback={<div>Loading posts...</div>}>
        <InstagramFeed />
      </Suspense>
    </div>
  );
}
// components/InstagramFeed.tsx

import Image from 'next/image';

interface Post {
  id: string;
  caption: string | null;
  mediaType: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM' | 'REELS';
  mediaUrl: string;
  thumbnailUrl: string | null;
  permalink: string;
  timestamp: string;
  likeCount: number | null;
  commentsCount: number | null;
}
  const data = await res.json();
  return data.posts;
}

export default async function InstagramFeed() {
  const posts = await getPosts();

  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
      {posts.map((post) => (
        <a
          key={post.id}
          href={post.permalink}
          target="_blank"
          rel="noopener noreferrer"
          className="group"
        >
          <div className="relative aspect-square overflow-hidden rounded-lg">
            <Image
              src={post.mediaUrl}
              alt={post.caption || 'Instagram post'}
              fill
              className="object-cover transition group-hover:scale-105"
            />
          </div>

          <p className="mt-2 text-sm text-gray-600 line-clamp-2">
            {post.caption}
          </p>

          {/* Engagement Stats */}
          <div className="flex items-center gap-3 mt-2 text-xs text-gray-500">
            {post.likeCount !== null && (
              <span className="flex items-center gap-1">
                ❤️ {post.likeCount.toLocaleString()}
              </span>
            )}
            {post.commentsCount !== null && (
              <span className="flex items-center gap-1">
                💬 {post.commentsCount.toLocaleString()}
              </span>
            )}
          </div>
        </a>
      ))}
    </div>
  );
}

Next Steps