Embedding External Applications into a Next.js Site

June 1, 2025

One of the coolest features of my portfolio site is the ability to seamlessly embed my standalone projects right within the site experience. These aren't just static code samples or screenshots—they're fully-functional web applications with their own domains. Let me show you how I made it work.

The Challenge

I've built several standalone applications like my hurricane tracker that visualizes NOAA data, my attendance management system, and a directory for UH Computer Science Discord servers. Each has its own repository and deployment.

I wanted users to be able to interact with these applications directly on my portfolio site without being sent away to different domains. This maintains a consistent experience and keeps visitors engaged with my portfolio.

Screenshot showing the hurricane tracker embedded in my portfolio site
My hurricane tracker application embedded directly in my portfolio site

The Solution: iframes with Next.js Dynamic Routing

The core of my implementation uses iframes to embed external applications, combined with Next.js's dynamic routing capabilities. Here's the basic approach:

1. The Demo Page Component

First, I created a dynamic route at app/demos/[slug]/page.tsx that loads the appropriate application based on the URL:


// app/demos/[slug]/page.tsx
export default function DemoPage({ params }: { params: { slug: string } }) {
  const demo = demos.find(d => d.slug === params.slug);
  
  if (!demo || !('url' in demo)) {
    notFound();
  }

  return (
    < Layout fullPage>
      < div className="w-full h-screen">
        < iframe 
          src={demo.url}
          className="w-full h-full border-0"
          allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
          allowFullScreen
        />
      < /div>
    
  );
}

2. The Demo Data Structure

I maintain a central registry of all my demo projects in a TypeScript file:


// lib/demoData.ts (excerpt)
export const demos: Demo[] = [
  {
    slug: "hurricane",
    title: "Hurricane Data Visualizer",
    description: "Track hurricanes and weather patterns, quickly visualize data from NOAA",
    thumbnailBase: "/demos/hurricane/thumbnail",
    ogImage: "/demos/hurricane/og",
    fullPage: true,
    url: "https://hurricane-adam.vercel.app", // The actual URL of the standalone app
    category: 'personal',
    techStack: ['React', 'TypeScript', 'Next.js', 'Mapbox'],
  },
  // Other demo projects...
]

This structure allows me to maintain clean separation of concerns—each project exists in its own repository with its own deployment, but they're presented in a unified way on my portfolio.

3. Clean URLs with Redirects

To make the URLs cleaner and more memorable, I added redirects in my Next.js configuration:


// next.config.js
const nextConfig = {
  // Other configuration...
  async redirects() {
    return [
      {
        source: '/hurricane',
        destination: '/demos/hurricane',
        permanent: true,
      },
      {
        source: '/attendance',
        destination: '/demos/attendance',
        permanent: true,
      }
      // Additional redirects...
    ];
  },
}

This allows visitors to access my hurricane tracker at nelsonarcher.com/hurricane instead of the longer path.

4. Handling External Sites That Can't Be Embedded

For some projects that can't be embedded directly (like my Ping Pong Pi scoreboard), I use a different approach that creates a transition page:


    // For external applications that can't be embedded
    if ('externalUrl' in demo) {
    return (
        < Layout>
        < div className="min-h-screen flex flex-col items-center justify-center p-4">
            < h1 className="text-3xl font-bold mb-4">{demo.title}
            < p className="text-xl mb-8 max-w-2xl text-center">{demo.description}
            
            {/* Display tech stack */}
            < div className="mb-8">
            < h2 className="text-xl font-semibold mb-2">Technologies Used:
            < div className="flex flex-wrap gap-2 justify-center">
                {demo.techStack.map(tech => (
                < span key={tech} className="bg-gray-800 text-white px-3 py-1 rounded-md">
                    {tech}
                < /span>
                ))}
            
            
            
            {/* Link to external site */}
            < a 
            href={demo.externalUrl}
            className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-md"
            target="_blank" 
            rel="noopener noreferrer"
            >
            Visit {demo.title}
            < /a>
            
            {/* Client-side redirect after a short delay */}
            < script
            dangerouslySetInnerHTML={{
                __html: `
                setTimeout(function() {
                    window.location.href = "${demo.externalUrl}";
                }, 3000);
                `
            } }
            />
            
            

Redirecting you in 3 seconds...

); }

Performance Improvements

To ensure good performance with embedded sites, I added a few optimizations:

1. Preconnect Hints

In the document head, I added preconnect hints to establish early connections to external domains:


< link rel="preconnect" href="https://hurricane-adam.vercel.app" / >
< link rel="preconnect" href="https://attendance-tracker-eta.vercel.app" / >

2. Responsive iframe Container

To make sure embedded applications work well on all screen sizes, I use this CSS pattern:


/* CSS for responsive iframes */
.iframe-container {
  position: relative;
  width: 100%;
  height: 0;
  padding-bottom: 56.25%; /* 16:9 aspect ratio */
}

.iframe-container iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

Security Considerations

When embedding external content, security is important. I only embed applications I control and add appropriate security headers:


// next.config.js (excerpt)
async headers() {
  return [
    {
      source: '/(.*)',
      headers: [
        {
          key: 'X-Frame-Options',
          value: 'SAMEORIGIN',
        },
        {
          key: 'Content-Security-Policy',
          value: `frame-src 'self' https://*.vercel.app https://pingpongpi.com;`,
        },
      ],
    },
  ];
}

The End Result

The final implementation creates a seamless experience where visitors can explore my projects without leaving my portfolio. Each project maintains its own independent deployment, but they are presented in a unified way within my site.

This technique is not limited to portfolio sites—it can be applied whenever you want to integrate separate applications into a unified experience. Whether you are building a learning platform with interactive examples or a product showcase with live demos, this approach provides a clean solution for seamless integration.