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.

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.