DEV Community

Cover image for Build a Link-in-Bio Page That Updates Itself with Your Latest Content
Olamide Olaniyan
Olamide Olaniyan

Posted on

Build a Link-in-Bio Page That Updates Itself with Your Latest Content

Linktree charges $24/month for a page with links on it. Let's build one that's better in 45 minutes.

Not just a static page — a link-in-bio that automatically pulls your latest Instagram posts, TikTok videos, and YouTube uploads. You update your page by posting content. No manual link swapping.

The Concept

Instead of:

🔗 My latest video → (manually updated every time you post)
🔗 My second latest → (always stale)
🔗 Watch my podcast → (from 3 months ago)
Enter fullscreen mode Exit fullscreen mode

You get:

@yourbrand
[auto-pulled profile pic + bio]

📱 Latest TikTok
  [thumbnail] [title] [views] → auto-linked

📸 Latest Instagram
  [grid of 6 recent posts] → each linked

🎬 Latest YouTube
  [thumbnail] [title] → auto-linked

[your permanent links below]
Enter fullscreen mode Exit fullscreen mode

Every time a visitor loads the page, they see your actual latest content. Zero maintenance.

The Stack

  • Next.js with App Router
  • SociaVault API — fetch latest content from each platform
  • Vercel — deploy for free
  • ISR (Incremental Static Regeneration) — cache the page, regenerate every hour

The Core: Server Component That Fetches Latest Content

// app/[username]/page.tsx

const SOCIAVAULT_API = 'https://api.sociavault.com/v1/scrape';
const API_KEY = process.env.SOCIAVAULT_API_KEY!;

interface LinkInBioConfig {
  name: string;
  bio: string;
  avatar: string;
  platforms: {
    instagram?: string;
    tiktok?: string;
    youtube?: string;
  };
  links: { title: string; url: string; emoji?: string }[];
}

// Define your config (this could come from a database in a multi-user setup)
const USERS: Record<string, LinkInBioConfig> = {
  yourusername: {
    name: 'Your Name',
    bio: 'Creator • Developer • Building cool stuff',
    avatar: '/avatar.jpg',
    platforms: {
      instagram: 'your_instagram',
      tiktok: 'your_tiktok',
    },
    links: [
      { title: 'My Newsletter', url: 'https://your-newsletter.com', emoji: '📧' },
      { title: 'My Course', url: 'https://your-course.com', emoji: '🎓' },
      { title: 'Work With Me', url: 'https://your-site.com/contact', emoji: '🤝' },
    ],
  },
};

async function fetchAPI(endpoint: string) {
  try {
    const res = await fetch(`${SOCIAVAULT_API}${endpoint}`, {
      headers: { 'x-api-key': API_KEY },
      next: { revalidate: 3600 }, // Cache for 1 hour
    });
    if (!res.ok) return null;
    return res.json();
  } catch {
    return null;
  }
}

async function getLatestContent(platforms: LinkInBioConfig['platforms']) {
  const content: any = {};

  if (platforms.instagram) {
    const data = await fetchAPI(`/instagram/posts?username=${platforms.instagram}&limit=6`);
    content.instagram = (data?.data || data?.posts || []).slice(0, 6);
  }

  if (platforms.tiktok) {
    const data = await fetchAPI(`/tiktok/profile-videos?username=${platforms.tiktok}&limit=3`);
    content.tiktok = (data?.data || data?.posts || []).slice(0, 3);
  }

  return content;
}

export async function generateMetadata({ params }: { params: { username: string } }) {
  const config = USERS[params.username];
  if (!config) return { title: 'Not Found' };
  return {
    title: `${config.name} — Links`,
    description: config.bio,
  };
}

// Regenerate every hour
export const revalidate = 3600;

export default async function LinkInBioPage({ params }: { params: { username: string } }) {
  const config = USERS[params.username];
  if (!config) return <div>Not found</div>;

  const content = await getLatestContent(config.platforms);

  return (
    <main className="min-h-screen bg-gradient-to-b from-gray-900 to-black text-white">
      <div className="max-w-md mx-auto px-4 py-12">
        {/* Profile */}
        <div className="text-center mb-8">
          <img
            src={config.avatar}
            alt={config.name}
            className="w-24 h-24 rounded-full mx-auto mb-4 border-2 border-white/20"
          />
          <h1 className="text-xl font-bold">{config.name}</h1>
          <p className="text-gray-400 text-sm mt-1">{config.bio}</p>
        </div>

        {/* Latest TikTok */}
        {content.tiktok?.length > 0 && (
          <section className="mb-8">
            <h2 className="text-xs uppercase tracking-wider text-gray-500 mb-3">Latest on TikTok</h2>
            <div className="space-y-2">
              {content.tiktok.map((video: any, i: number) => (
                <a
                  key={i}
                  href={video.webVideoUrl || '#'}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="flex items-center gap-3 bg-white/5 hover:bg-white/10 rounded-xl p-3 transition"
                >
                  {video.coverUrl && (
                    <img src={video.coverUrl} alt="" className="w-12 h-16 rounded object-cover" />
                  )}
                  <div className="flex-1 min-w-0">
                    <p className="text-sm truncate">{video.desc || 'TikTok video'}</p>
                    <p className="text-xs text-gray-500 mt-1">
                      {(video.playCount || 0).toLocaleString()} views
                    </p>
                  </div>
                </a>
              ))}
            </div>
          </section>
        )}

        {/* Latest Instagram */}
        {content.instagram?.length > 0 && (
          <section className="mb-8">
            <h2 className="text-xs uppercase tracking-wider text-gray-500 mb-3">Latest on Instagram</h2>
            <div className="grid grid-cols-3 gap-1 rounded-xl overflow-hidden">
              {content.instagram.map((post: any, i: number) => (
                <a
                  key={i}
                  href={post.url || `https://instagram.com/p/${post.shortcode}`}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="aspect-square relative group"
                >
                  <img
                    src={post.imageUrl || post.thumbnailUrl || ''}
                    alt=""
                    className="w-full h-full object-cover"
                  />
                  <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition flex items-center justify-center">
                    <span className="text-xs">
                      ❤️ {(post.likesCount || 0).toLocaleString()}
                    </span>
                  </div>
                </a>
              ))}
            </div>
          </section>
        )}

        {/* Permanent Links */}
        <section className="space-y-3">
          {config.links.map((link, i) => (
            <a
              key={i}
              href={link.url}
              target="_blank"
              rel="noopener noreferrer"
              className="block w-full text-center bg-white/10 hover:bg-white/20 rounded-xl py-3 px-4 transition font-medium"
            >
              {link.emoji && <span className="mr-2">{link.emoji}</span>}
              {link.title}
            </a>
          ))}
        </section>

        {/* Footer */}
        <p className="text-center text-gray-600 text-xs mt-12">
          Powered by <a href="https://sociavault.com" className="text-gray-500 hover:text-white">SociaVault</a>
        </p>
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

How ISR Makes This Work

The revalidate = 3600 line is doing the heavy lifting. It means:

  1. First visitor → page is generated server-side, API calls happen, result is cached
  2. Next 3,599 visitors → served from cache instantly, no API calls
  3. After 1 hour → next visitor triggers a background regeneration, still gets the cached version while the new one builds

Your latest TikTok video shows up within an hour of posting. With zero manual work. And the page loads in under 100ms for every visitor after the first.

Cost

  • Vercel: Free tier (100K page views/month)
  • SociaVault API: One API call per platform per hour = ~48 credits/day = 1,440 credits/month
  • Domain: $12/year

Total: effectively $0/month for a personal link-in-bio page.

Making It Multi-User (SaaS Mode)

If you want to turn this into a Linktree competitor:

  1. Replace the hardcoded USERS config with a database (Prisma + PostgreSQL)
  2. Add a setup wizard where users connect their social handles
  3. Add Stripe for a $5/month plan
  4. Add custom themes (the easiest upsell — CSS variables)

The SociaVault API calls scale linearly: 10 users = 10x the credits, but with ISR caching it's still cheap. 100 users with hourly revalidation = ~144,000 credits/month.

Why This Beats Linktree

Feature Linktree ($24/mo) This (free)
Latest content auto-pulls ❌ Manual links ✅ Automatic
Custom domain ✅ (paid) ✅ (Vercel)
Analytics Add Plausible ($0)
SEO Weak (shared domain) ✅ Your own domain
Load speed ~2-3s ~100ms (ISR)
Customization Limited themes Full CSS control

Read the Full Guide

Build an Auto-Updating Link-in-Bio → SociaVault Blog


Pull latest posts and profiles from any platform with SociaVault — one API for TikTok, Instagram, YouTube, and 10+ social platforms.

Discussion

What's on your link-in-bio page? Do you actually remember to update the links, or is it pointing to content from 6 months ago? (Be honest.)

nextjs #webdev #react #javascript #saas

Top comments (0)