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:
1npx 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:
- Navigate to Content-Type Builder → Create new collection type
- Name it "Project" and create these fields:
1Field: title (Text - Short text)
2Field: description (Text - Long text)
3Field: slug (UID - linked to title)
4Field: content (Rich text)
5Field: featured_image (Media - Single media)
6Field: technologies (Text - Short text) with "Multiple" enabled
7Field: project_url (Text - Short text)
8Field: github_url (Text - Short text)
9Field: published_date (Date - Date)
10Field: featured (Boolean)
Click "Save" to create your Project content type. Similarly, create a "Blog Post" content type:
1Field: title (Text - Short text)
2Field: slug (UID - linked to title)
3Field: content (Rich text)
4Field: excerpt (Text - Long text)
5Field: cover_image (Media - Single media)
6Field: tags (Text - Short text) with "Multiple" enabled
7Field: published_date (Date - Date)
Step 3: Configure API Permissions
Secure your API by setting proper permissions:
- Go to Settings → Roles → Public
- Enable the following permissions for Project and Blog Post:
- find (GET)
- findOne (GET)
- 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:
1{
2 "title": "E-commerce Platform",
3 "description": "A full-stack e-commerce solution with payment processing",
4 "slug": "e-commerce-platform",
5 "content": "# E-commerce Platform\n\nThis project features user authentication, product management, cart functionality, and Stripe payment integration.",
6 "technologies": ["React", "Node.js", "MongoDB", "Stripe"],
7 "project_url": "https://myecommerce.example.com",
8 "github_url": "https://github.com/yourusername/ecommerce",
9 "published_date": "2023-01-15",
10 "featured": true
11}
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:
1const { createCoreController } = require('@strapi/strapi').factories;
2
3module.exports = createCoreController('api::project.project', ({ strapi }) => ({
4 async find(ctx) {
5 const { data, meta } = await super.find(ctx);
6 const optimizedData = data.map(item => ({
7 ...item,
8 id: item.id,
9 featured_image: item.featured_image?.data
10 ? {
11 url: item.featured_image.data.url,
12 alt: item.title,
13 }
14 : null,
15 }));
16 const sanitizedResults = await this.sanitizeOutput(optimizedData, ctx);
17 return this.transformResponse(sanitizedResults, { pagination: meta.pagination });
18 },
19}));
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:
1npx create-next-app@latest portfolio-frontend --typescript
2cd portfolio-frontend
Install essential dependencies:
1npm install axios swr sass
Step 2: Configure Environment Variables
Create a .env.local
file in your project root:
1NEXT_PUBLIC_STRAPI_URL=http://localhost:1337
2STRAPI_API_TOKEN=your-api-token-from-strapi
Next, create a configuration file for API connections:
1// lib/api.ts
2export const STRAPI_URL = process.env.NEXT_PUBLIC_STRAPI_URL || 'http://localhost:1337';
3export const STRAPI_API_TOKEN = process.env.STRAPI_API_TOKEN;
4
5export const fetchAPI = async (path: string) => {
6 const requestUrl = `${STRAPI_URL}/api${path}`;
7 const response = await fetch(requestUrl, {
8 headers: {
9 'Content-Type': 'application/json',
10 Authorization: `Bearer ${STRAPI_API_TOKEN}`
11 }
12 });
13
14 const data = await response.json();
15 return data;
16};
Step 3: Create Type Definitions
Define TypeScript interfaces for your content:
1// types/project.ts
2export interface Project {
3 id: number;
4 title: string;
5 description: string;
6 slug: string;
7 content: string;
8 featured_image: {
9 url: string;
10 alt: string;
11 };
12 technologies: string[];
13 project_url: string;
14 github_url: string;
15 published_date: string;
16 featured: boolean;
17}
18
19// types/blog-post.ts
20export interface BlogPost {
21 id: number;
22 title: string;
23 slug: string;
24 content: string;
25 excerpt: string;
26 cover_image: {
27 url: string;
28 alt: string;
29 };
30 tags: string[];
31 published_date: string;
32}
Step 4: Create API Service Functions
Build functions to fetch data from your Strapi backend:
1// lib/projects.ts
2import { fetchAPI } from './api';
3import { Project } from '../types/project';
4
5export async function getAllProjects(): Promise<Project[]> {
6 const data = await fetchAPI('/projects?populate=featured_image');
7 return data.data;
8}
9
10export async function getFeaturedProjects(): Promise<Project[]> {
11 const data = await fetchAPI('/projects?filters[featured]=true&populate=featured_image');
12 return data.data;
13}
14
15export async function getProjectBySlug(slug: string): Promise<Project> {
16 const data = await fetchAPI(`/projects?filters[slug][$eq]=${slug}&populate=featured_image`);
17 return data.data[0];
18}
Step 5: Create Reusable Components
Let's create a ProjectCard component:
1// components/ProjectCard.tsx
2import Image from 'next/image';
3import Link from 'next/link';
4import { Project } from '../types/project';
5import styles from '../styles/ProjectCard.module.scss';
6
7interface ProjectCardProps {
8 project: Project;
9}
10
11export default function ProjectCard({ project }: ProjectCardProps) {
12 return (
13 <div className={styles.card}>
14 <div className={styles.imageContainer}>
15 {project.featured_image && (
16 <Image
17 src={`${process.env.NEXT_PUBLIC_STRAPI_URL}${project.featured_image.url}`}
18 alt={project.featured_image.alt || project.title}
19 layout="fill"
20 objectFit="cover"
21 />
22 )}
23 </div>
24 <div className={styles.content}>
25 <h3>{project.title}</h3>
26 <p>{project.description}</p>
27 <div className={styles.technologies}>
28 {project.technologies.map(tech => (
29 <span key={tech} className={styles.tech}>{tech}</span>
30 ))}
31 </div>
32 <div className={styles.links}>
33 <Link href={`/projects/${project.slug}`}>
34 <a className={styles.detailsLink}>View Details</a>
35 </Link>
36 {project.github_url && (
37 <a href={project.github_url} target="_blank" rel="noopener noreferrer" className={styles.externalLink}>
38 GitHub
39 </a>
40 )}
41 {project.project_url && (
42 <a href={project.project_url} target="_blank" rel="noopener noreferrer" className={styles.externalLink}>
43 Live Demo
44 </a>
45 )}
46 </div>
47 </div>
48 </div>
49 );
50}
Step 6: Implement Dynamic Routing
Create a dynamic page for individual projects:
1// pages/projects/[slug].tsx
2import { GetStaticProps, GetStaticPaths } from 'next';
3import Image from 'next/image';
4import Head from 'next/head';
5import { getAllProjects, getProjectBySlug } from '../../lib/projects';
6import { Project } from '../../types/project';
7import ReactMarkdown from 'react-markdown';
8import styles from '../../styles/ProjectDetail.module.scss';
9
10interface ProjectDetailProps {
11 project: Project;
12}
13
14export default function ProjectDetail({ project }: ProjectDetailProps) {
15 if (!project) return <div>Loading...</div>;
16
17 return (
18 <>
19 <Head>
20 <title>{project.title} | My Portfolio</title>
21 <meta name="description" content={project.description} />
22 </Head>
23
24 <article className={styles.projectDetail}>
25 <header>
26 <h1>{project.title}</h1>
27 {project.featured_image && (
28 <div className={styles.featuredImage}>
29 <Image
30 src={`${process.env.NEXT_PUBLIC_STRAPI_URL}${project.featured_image.url}`}
31 alt={project.title}
32 width={1200}
33 height={630}
34 layout="responsive"
35 />
36 </div>
37 )}
38 </header>
39
40 <div className={styles.metadata}>
41 <div className={styles.technologies}>
42 {project.technologies.map(tech => (
43 <span key={tech} className={styles.tech}>{tech}</span>
44 ))}
45 </div>
46 <div className={styles.links}>
47 {project.github_url && (
48 <a href={project.github_url} target="_blank" rel="noopener noreferrer" className={styles.link}>
49 GitHub Repository
50 </a>
51 )}
52 {project.project_url && (
53 <a href={project.project_url} target="_blank" rel="noopener noreferrer" className={styles.link}>
54 Live Demo
55 </a>
56 )}
57 </div>
58 </div>
59
60 <div className={styles.content}>
61 <ReactMarkdown>{project.content}</ReactMarkdown>
62 </div>
63 </article>
64 </>
65 );
66}
67
68export const getStaticPaths: GetStaticPaths = async () => {
69 const projects = await getAllProjects();
70
71 return {
72 paths: projects.map(project => ({
73 params: { slug: project.slug }
74 })),
75 fallback: 'blocking'
76 };
77};
78
79export const getStaticProps: GetStaticProps = async ({ params }) => {
80 const slug = params?.slug as string;
81 const project = await getProjectBySlug(slug);
82
83 if (!project) {
84 return { notFound: true };
85 }
86
87 return {
88 props: { project },
89 revalidate: 60 // Revalidate page every 60 seconds for fresh content
90 };
91};
Step 7: Create the Projects Overview Page
Build a page that displays all your projects:
1// pages/projects/index.tsx
2import { GetStaticProps } from 'next';
3import Head from 'next/head';
4import { getAllProjects } from '../../lib/projects';
5import { Project } from '../../types/project';
6import ProjectCard from '../../components/ProjectCard';
7import styles from '../../styles/Projects.module.scss';
8
9interface ProjectsPageProps {
10 projects: Project[];
11}
12
13export default function ProjectsPage({ projects }: ProjectsPageProps) {
14 return (
15 <>
16 <Head>
17 <title>My Projects | Portfolio</title>
18 <meta name="description" content="Explore my latest projects and work" />
19 </Head>
20
21 <section className={styles.projectsSection}>
22 <h1>My Projects</h1>
23 <p className={styles.intro}>
24 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.
25 </p>
26
27 <div className={styles.projectsGrid}>
28 {projects.map(project => (
29 <ProjectCard key={project.id} project={project} />
30 ))}
31 </div>
32 </section>
33 </>
34 );
35}
36
37export const getStaticProps: GetStaticProps = async () => {
38 const projects = await getAllProjects();
39
40 return {
41 props: { projects },
42 revalidate: 60 // Revalidate page every 60 seconds
43 };
44};
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.