✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

Ecosystem9 min read

How to Build a Portfolio Site with Strapi and Next.js

June 16, 2025Updated on June 14, 2026
Build a Portfolio Site with Strapi and Nextjs

Having a personal portfolio website is essential these days for showcasing your skills and building a professional online presence. But building one that’s both visually stunning and highly functional can be challenging. That’s where Strapi and Next.js come in. Together, they offer a powerful, flexible solution for creating a portfolio site that not only highlights your work but also provides a seamless, fast, and customizable user experience.

With Strapi as your headless CMS, you gain complete control over your content, making it easy to manage and update your projects, blog posts, and other portfolio items. On the front-end, Next.js brings speed, SEO benefits, and dynamic capabilities, ensuring your site performs well and looks great on any device.

In this guide, we’ll walk you through how to build a portfolio site with Strapi and Next.js that’s not just another template, but a unique digital showcase that reflects your personal brand and creativity. Let’s dive in!

In brief:

  • Strapi and Next.js create a powerful portfolio solution that combines content flexibility with blazing-fast performance and SEO optimization.
  • This headless architecture separates your content (Strapi backend) from presentation (Next.js frontend), giving you complete control over both aspects.
  • You'll learn to create custom content types in Strapi that perfectly match your portfolio needs, then connect them to dynamic Next.js pages.
  • The finished portfolio will achieve excellent performance metrics with static generation while remaining easily maintainable and extensible.

How to Build a Portfolio Site Backend with Strapi

Your portfolio backend starts with Strapi as your content hub. Let's create a robust backend system with step-by-step instructions and code examples.

Step 1: Install and Configure Strapi

First, let's create a new Strapi project:

npx create-strapi@latest portfolio-backend --quickstart

The --quickstart flag sets up SQLite as your database for rapid development. Once installation completes, Strapi automatically launches and prompts you to create an admin user.

Step 2: Create Content Types for Your Portfolio

From the Strapi admin panel, we'll create custom content types for your portfolio projects:

  1. Navigate to Content-Type Builder → Create new collection type
  2. Name it "Project" and create these fields:
Field: title (Text - Short text)
Field: description (Text - Long text)
Field: slug (UID - linked to title)
Field: content (Rich text)
Field: featured_image (Media - Single media)
Field: technologies (Text - Short text) with "Multiple" enabled
Field: project_url (Text - Short text)
Field: github_url (Text - Short text)
Field: published_date (Date - Date)
Field: featured (Boolean)

Click "Save" to create your Project content type. Similarly, create a "Blog Post" content type:

Field: title (Text - Short text)
Field: slug (UID - linked to title)
Field: content (Rich text)
Field: excerpt (Text - Long text)
Field: cover_image (Media - Single media)
Field: tags (Text - Short text) with "Multiple" enabled
Field: published_date (Date - Date)

Step 3: Configure API Permissions

Secure your API by setting proper permissions:

  1. Go to Settings → Roles → Public
  2. Enable the following permissions for Project and Blog Post:
    • find (GET)
    • findOne (GET)
  3. Save your changes

For programmatic access, let's create an API token:

In Strapi admin panel: Settings → API Tokens → Create new API token. Name: "Portfolio Frontend". Token type: "Read-only". Token duration: "Unlimited". Save and copy the generated token for use in your Next.js application.

Step 4: Add Sample Content

Create a few portfolio projects and blog posts using the Strapi admin interface. Here's an example project structure as JSON that you could import:

{
  "title": "E-commerce Platform",
  "description": "A full-stack e-commerce solution with payment processing",
  "slug": "e-commerce-platform",
  "content": "# E-commerce Platform\n\nThis project features user authentication, product management, cart functionality, and Stripe payment integration.",
  "technologies": ["React", "Node.js", "MongoDB", "Stripe"],
  "project_url": "https://myecommerce.example.com",
  "github_url": "https://github.com/yourusername/ecommerce",
  "published_date": "2023-01-15",
  "featured": true
}

Step 5: Customize the API Response

Create a new file at ./src/api/project/content-types/project/schema.json and add a lifecycle hook. In the ./src/api/project/controllers/project.js file, use the following code to optimize Strapi responses:

const { createCoreController } = require('@strapi/strapi').factories;

module.exports = createCoreController('api::project.project', ({ strapi }) => ({
  async find(ctx) {
    const { data, meta } = await super.find(ctx);
    const optimizedData = data.map(item => ({
      ...item,
      id: item.id,
      featured_image: item.featured_image?.data
        ? {
            url: item.featured_image.data.url,
            alt: item.title,
          }
        : null,
    }));
    const sanitizedResults = await this.sanitizeOutput(optimizedData, ctx);
    return this.transformResponse(sanitizedResults, { pagination: meta.pagination });
  },
}));

This customization simplifies consuming the API in your Next.js frontend by flattening the response structure.

How to Build a Portfolio Site Frontend with Next.js

Now that your backend is ready, let's build a powerful Next.js frontend to showcase your portfolio with code examples at each step.

Step 1: Initialize a Next.js Project

Create a new Next.js application with TypeScript support:

npx create-next-app@latest portfolio-frontend --typescript
cd portfolio-frontend

Install essential dependencies:

npm install axios swr sass

Step 2: Configure Environment Variables

Create a .env.local file in your project root:

NEXT_PUBLIC_STRAPI_URL=http://localhost:1337
STRAPI_API_TOKEN=your-api-token-from-strapi

Next, create a configuration file for API connections:

// lib/api.ts
export const STRAPI_URL = process.env.NEXT_PUBLIC_STRAPI_URL || 'http://localhost:1337';
export const STRAPI_API_TOKEN = process.env.STRAPI_API_TOKEN;

export const fetchAPI = async (path: string) => {
  const requestUrl = `${STRAPI_URL}/api${path}`;
  const response = await fetch(requestUrl, {
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${STRAPI_API_TOKEN}`
    }
  });
  
  const data = await response.json();
  return data;
};

Step 3: Create Type Definitions

Define TypeScript interfaces for your content:

// types/project.ts
export interface Project {
  id: number;
  title: string;
  description: string;
  slug: string;
  content: string;
  featured_image: {
    url: string;
    alt: string;
  };
  technologies: string[];
  project_url: string;
  github_url: string;
  published_date: string;
  featured: boolean;
}

// types/blog-post.ts
export interface BlogPost {
  id: number;
  title: string;
  slug: string;
  content: string;
  excerpt: string;
  cover_image: {
    url: string;
    alt: string;
  };
  tags: string[];
  published_date: string;
}

Step 4: Create API Service Functions

Build functions to fetch data from your Strapi backend:

// lib/projects.ts
import { fetchAPI } from './api';
import { Project } from '../types/project';

export async function getAllProjects(): Promise<Project[]> {
  const data = await fetchAPI('/projects?populate=featured_image');
  return data.data;
}

export async function getFeaturedProjects(): Promise<Project[]> {
  const data = await fetchAPI('/projects?filters[featured]=true&populate=featured_image');
  return data.data;
}

export async function getProjectBySlug(slug: string): Promise<Project> {
  const data = await fetchAPI(`/projects?filters[slug][$eq]=${slug}&populate=featured_image`);
  return data.data[0];
}

Step 5: Create Reusable Components

Let's create a ProjectCard component:

// components/ProjectCard.tsx
import Image from 'next/image';
import Link from 'next/link';
import { Project } from '../types/project';
import styles from '../styles/ProjectCard.module.scss';

interface ProjectCardProps {
  project: Project;
}

export default function ProjectCard({ project }: ProjectCardProps) {
  return (
    <div className={styles.card}>
      <div className={styles.imageContainer}>
        {project.featured_image && (
          <Image 
            src={`${process.env.NEXT_PUBLIC_STRAPI_URL}${project.featured_image.url}`}
            alt={project.featured_image.alt || project.title}
            layout="fill"
            objectFit="cover"
          />
        )}
      </div>
      <div className={styles.content}>
        <h3>{project.title}</h3>
        <p>{project.description}</p>
        <div className={styles.technologies}>
          {project.technologies.map(tech => (
            <span key={tech} className={styles.tech}>{tech}</span>
          ))}
        </div>
        <div className={styles.links}>
          <Link href={`/projects/${project.slug}`}>
            <a className={styles.detailsLink}>View Details</a>
          </Link>
          {project.github_url && (
            <a href={project.github_url} target="_blank" rel="noopener noreferrer" className={styles.externalLink}>
              GitHub
            </a>
          )}
          {project.project_url && (
            <a href={project.project_url} target="_blank" rel="noopener noreferrer" className={styles.externalLink}>
              Live Demo
            </a>
          )}
        </div>
      </div>
    </div>
  );
}

Step 6: Implement Dynamic Routing

Create a dynamic page for individual projects:

// pages/projects/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next';
import Image from 'next/image';
import Head from 'next/head';
import { getAllProjects, getProjectBySlug } from '../../lib/projects';
import { Project } from '../../types/project';
import ReactMarkdown from 'react-markdown';
import styles from '../../styles/ProjectDetail.module.scss';

interface ProjectDetailProps {
  project: Project;
}

export default function ProjectDetail({ project }: ProjectDetailProps) {
  if (!project) return <div>Loading...</div>;
  
  return (
    <>
      <Head>
        <title>{project.title} | My Portfolio</title>
        <meta name="description" content={project.description} />
      </Head>
      
      <article className={styles.projectDetail}>
        <header>
          <h1>{project.title}</h1>
          {project.featured_image && (
            <div className={styles.featuredImage}>
              <Image 
                src={`${process.env.NEXT_PUBLIC_STRAPI_URL}${project.featured_image.url}`}
                alt={project.title}
                width={1200}
                height={630}
                layout="responsive"
              />
            </div>
          )}
        </header>
        
        <div className={styles.metadata}>
          <div className={styles.technologies}>
            {project.technologies.map(tech => (
              <span key={tech} className={styles.tech}>{tech}</span>
            ))}
          </div>
          <div className={styles.links}>
            {project.github_url && (
              <a href={project.github_url} target="_blank" rel="noopener noreferrer" className={styles.link}>
                GitHub Repository
              </a>
            )}
            {project.project_url && (
              <a href={project.project_url} target="_blank" rel="noopener noreferrer" className={styles.link}>
                Live Demo
              </a>
            )}
          </div>
        </div>
        
        <div className={styles.content}>
          <ReactMarkdown>{project.content}</ReactMarkdown>
        </div>
      </article>
    </>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const projects = await getAllProjects();
  
  return {
    paths: projects.map(project => ({
      params: { slug: project.slug }
    })),
    fallback: 'blocking'
  };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const slug = params?.slug as string;
  const project = await getProjectBySlug(slug);
  
  if (!project) {
    return { notFound: true };
  }
  
  return {
    props: { project },
    revalidate: 60 // Revalidate page every 60 seconds for fresh content
  };
};

Step 7: Create the Projects Overview Page

Build a page that displays all your projects:

// pages/projects/index.tsx
import { GetStaticProps } from 'next';
import Head from 'next/head';
import { getAllProjects } from '../../lib/projects';
import { Project } from '../../types/project';
import ProjectCard from '../../components/ProjectCard';
import styles from '../../styles/Projects.module.scss';

interface ProjectsPageProps {
  projects: Project[];
}

export default function ProjectsPage({ projects }: ProjectsPageProps) {
  return (
    <>
      <Head>
        <title>My Projects | Portfolio</title>
        <meta name="description" content="Explore my latest projects and work" />
      </Head>
      
      <section className={styles.projectsSection}>
        <h1>My Projects</h1>
        <p className={styles.intro}>
          Browse through my recent work and personal projects. Each project includes details about the technologies used and links to [live demo](https://strapi.io/demo)s or repositories.
        </p>
        
        <div className={styles.projectsGrid}>
          {projects.map(project => (
            <ProjectCard key={project.id} project={project} />
          ))}
        </div>
      </section>
    </>
  );
}

export const getStaticProps: GetStaticProps = async () => {
  const projects = await getAllProjects();
  
  return {
    props: { projects },
    revalidate: 60 // Revalidate page every 60 seconds
  };
};

Step 8: Implement Incremental Static Regeneration

Next.js's Incremental Static Regeneration (ISR) allows you to update static content after you've built your site. This is already implemented in the examples above with the revalidate property in getStaticProps.

For example, when you add a new project in Strapi, your Next.js site will automatically update the projects page after the revalidation period (60 seconds in our examples), without requiring a full rebuild.

By following these steps and implementing these code examples, you'll have a fully functional, high-performance portfolio site that showcases your work beautifully while maintaining excellent SEO and user experience.

Build a Portfolio Site That's Custom, Performant, and Easy to Grow

You've built something better than just another developer site learning how to build a portfolio site with Strapi and Next.js. Your custom solution combines flexible content management with performance-focused rendering to create a platform that showcases your work professionally while staying completely under your control.

Your site now includes dynamic project showcases, blog management, global settings for consistent branding, and secure contact functionality. Unlike template-based options, you control every aspect of the data structure, API endpoints, and frontend presentation. The headless architecture you've implemented ensures your content can expand to multiple channels as your career grows.

Security comes built-in through role-based permissions and API tokens, while Next.js's static generation delivers fast load times and excellent SEO.

Want to take it further? Enable internationalization using i18n features for multiple languages, use preview mode to check draft content before publishing, or add the Discussion plugin to manage blog comments. Next.js 14's App Router and Server Actions enhance frontend performance. The App Router supports structured routing with shared layouts and nested routing, while Server Actions handle server-side tasks like data fetching and mutation, reducing client load and improving delivery speed.

our portfolio represents your technical skills—now you have the tools to maintain and expand it exactly as your career demands.

Ready to take your portfolio to the next level? Build with Strapi v5 for enhanced performance and flexibility, or use Strapi Cloud to manage everything effortlessly, ensuring your site grows with your career while staying secure and scalable.

Paul BratslavskyDeveloper Advocate

Related Posts

Epic Next.js and Strapi Tutorial
14 min read

Epic Next.js 15 Tutorial Part 2: Building Out The Home Page

This post is part 2 of many in our Epic Next.js Series. You can find the outline for upcoming posts here. ...

·August 17, 2025
Blog·13 min read

How to Build a Simple 3D Portfolio Website with Vite, React, Three.js, and Strapi

Static screenshots rarely capture a developer's range, but a realtime 3D portfolio does. By blending interactive scenes...

·June 7, 2022
Image Upload to Strapi via REST API
46 min read

Image Upload to Strapi via REST API with Next.js and Postman

Introduction to Media Upload in Strapi via REST API Media uploads and image management is a key feature in modern...

·October 28, 2024