How to Create a Next.js Portfolio Website with ElmapiCMS and Cursor AI

Learn how to build a complete portfolio website using Next.js, ElmapiCMS, and Cursor AI. This step-by-step guide shows you how to create the CMS structure and frontend code with AI assistance.

R
Raşit Apalak
·
·
10 min read

Building a portfolio website doesn't have to be time-consuming. With ElmapiCMS as your content backend and Cursor as your AI coding assistant, you can create a professional portfolio site in hours instead of days.

This tutorial will walk you through building a complete portfolio website from scratch, using Cursor to generate both the ElmapiCMS project template and the Next.js frontend code.


Table of Contents


What We're Building

We'll create a portfolio website with:

  • About Section - Personal introduction and skills
  • Projects Gallery - Showcase of your work with images and descriptions
  • Experience Timeline - Work history and achievements
  • Contact Form - Way for visitors to reach out
  • Blog Section (optional) - Share your thoughts and updates

All content will be managed through ElmapiCMS, and the frontend will be built with Next.js 16 using the App Router.


Prerequisites

Before we start, make sure you have:

  • ElmapiCMS installed and running (local or hosted)
  • Cursor installed and configured
  • Node.js 20+ and npm installed (required for Next.js 16)
  • Basic knowledge of React and Next.js

Step 1: Design the Content Structure with Cursor

First, we need to design our ElmapiCMS collections. To do this effectively, you need to run Cursor in your ElmapiCMS installation directory where the existing templates are located.

Setting Up Cursor

  1. Open Cursor in your ElmapiCMS directory:

    cd /path/to/your/elmapi-installation
    cursor .
    
  2. Open an existing template for reference:

    • Navigate to resources/data/project_templates/landing-page-nextjs.json
    • Open this file in Cursor so it can see the template structure

Prompt for Cursor

With the existing template file open, use Cursor's chat feature and say:

"I have an existing ElmapiCMS project template open (landing-page-nextjs.json). I'm building a portfolio website and need a new template. Design a complete project template JSON structure for a professional portfolio website. The template should include:

  • An About section (singleton collection) for personal information, bio, skills, and resume
  • A Projects collection (multi-entry) to showcase portfolio projects with images, descriptions, technologies used, and links
  • An Experience collection (multi-entry) for work history and professional experience
  • A Contact settings collection (singleton) for contact information and social media links

ElmapiCMS supports these field types: text, longtext, richtext, slug, email, password, number, enumeration, boolean, color, date, time, media, relation, json, and group. Use appropriate field types for each field. Include demo data for all collections. Follow the same structure and field conventions as the existing template."

If Cursor needs more information about field types, you can also reference the ElmapiCMS documentation:

"You can also reference the ElmapiCMS documentation at https://elmapicms.com/documentation/ to understand field types and structure better."

Cursor will analyze the existing template structure and generate a complete portfolio template with all appropriate fields, following the same patterns.

Save the Template

  1. Create a new file: resources/data/project_templates/portfolio.json
  2. Paste the generated template structure
  3. Review and adjust if needed

Step 2: Load the Template into ElmapiCMS

Once you have the template file, load it into your ElmapiCMS installation:

cd /path/to/elmapi
php artisan db:seed --class=ProjectTemplateSeeder

Then create a new project in ElmapiCMS:

  1. Go to your ElmapiCMS dashboard
  2. Click "Create project"
  3. Select "Chose from a template" from the project type dropdown
  4. Select "Portfolio" from the template dropdown
  5. Check "Include demo content" to see sample data
  6. Create the project

Step 3: Set Up the Next.js Project

Now let's create the Next.js frontend. In Cursor, open a terminal or use the chat:

Prompt: "Create a new Next.js 16 project with TypeScript, Tailwind CSS, and the App Router."

Or manually run:

npx create-next-app@latest portfolio-website --typescript --tailwind --app
cd portfolio-website

This will create a Next.js 16 project with the latest features and improvements.


Step 4: Install ElmapiCMS SDK

Install the ElmapiCMS JavaScript SDK:

Prompt in Cursor: "Show me how to install and configure @elmapicms/js-sdk in this Next.js project. Include environment variables setup."

Or manually:

npm install @elmapicms/js-sdk

Create a .env.local file:

ELMAPI_API_URL=https://your-instance.elmapi.com/api
ELMAPI_API_KEY=your-api-key
ELMAPI_PROJECT_ID=your-project-uuid

Step 5: Create the API Client

Use Cursor to generate a reusable API client:

Prompt: "Create a TypeScript API client utility for ElmapiCMS using @elmapicms/js-sdk. Include:

  • Client initialization with environment variables
  • Type-safe functions for fetching about section, projects, experience, and contact settings
  • Error handling
  • Export the client and all fetch functions"

Cursor will generate something like:

// lib/elmapi.ts
import { createClient } from '@elmapicms/js-sdk';

const client = createClient(
  process.env.ELMAPI_API_URL!,
  process.env.ELMAPI_API_KEY!,
  process.env.ELMAPI_PROJECT_ID!
);

export async function getAbout() {
  try {
    const about = await client.getEntry('about');
    return about;
  } catch (error) {
    console.error('Error fetching about:', error);
    return null;
  }
}

export async function getProjects() {
  try {
    const projects = await client.getEntries('projects', {
      state: 'published',
      sort: { completion_date: 'desc' }
    });
    return projects;
  } catch (error) {
    console.error('Error fetching projects:', error);
    return [];
  }
}

export async function getExperience() {
  try {
    const experience = await client.getEntries('experience', {
      state: 'published',
      sort: { start_date: 'desc' }
    });
    return experience;
  } catch (error) {
    console.error('Error fetching experience:', error);
    return [];
  }
}

export async function getContact() {
  try {
    const contact = await client.getEntry('contact-settings');
    return contact;
  } catch (error) {
    console.error('Error fetching contact:', error);
    return null;
  }
}

Step 6: Build the Home Page

Now let's create the main portfolio page. Use Cursor to generate the layout:

Prompt: "Create a Next.js page component for a portfolio website. Include:

  • Hero section with name, title, and profile image from the about collection
  • Featured projects grid (3-4 projects)
  • Skills section
  • Recent experience (2-3 items)
  • Use Tailwind CSS for styling
  • Make it responsive and modern
  • Fetch data from the ElmapiCMS API using the client functions"

Cursor will generate a complete page component. Here's a simplified example:

// app/page.tsx
import { getAbout, getProjects, getExperience } from '@/lib/elmapi';
import Image from 'next/image';
import Link from 'next/link';

export default async function Home() {
  const about = await getAbout();
  const projects = await getProjects();
  const experience = await getExperience();

  const featuredProjects = projects
    .filter((p: any) => p.fields?.featured)
    .slice(0, 4);

  return (
    <main className="min-h-screen">
      {/* Hero Section */}
      <section className="container mx-auto px-4 py-20">
        <div className="flex flex-col md:flex-row items-center gap-8">
          {about?.fields?.profile_image && (
            <Image
              src={about.fields.profile_image}
              alt={about.fields.name}
              width={200}
              height={200}
              className="rounded-full"
            />
          )}
          <div>
            <h1 className="text-4xl font-bold mb-2">
              {about?.fields?.name || 'Your Name'}
            </h1>
            <p className="text-xl text-gray-600 mb-4">
              {about?.fields?.title || 'Your Title'}
            </p>
            <div 
              className="prose"
              dangerouslySetInnerHTML={{ 
                __html: about?.fields?.bio || '' 
              }}
            />
          </div>
        </div>
      </section>

      {/* Featured Projects */}
      <section className="container mx-auto px-4 py-20">
        <h2 className="text-3xl font-bold mb-8">Featured Projects</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {featuredProjects.map((project: any) => (
            <Link
              key={project.uuid}
              href={`/projects/${project.fields?.slug || project.uuid}`}
              className="group"
            >
              <div className="border rounded-lg overflow-hidden hover:shadow-lg transition">
                {project.fields?.featured_image && (
                  <Image
                    src={project.fields.featured_image}
                    alt={project.fields.title}
                    width={400}
                    height={250}
                    className="w-full h-48 object-cover"
                  />
                )}
                <div className="p-4">
                  <h3 className="text-xl font-semibold mb-2">
                    {project.fields?.title}
                  </h3>
                  <p className="text-gray-600 line-clamp-2">
                    {project.fields?.description}
                  </p>
                </div>
              </div>
            </Link>
          ))}
        </div>
      </section>

      {/* Experience */}
      <section className="container mx-auto px-4 py-20">
        <h2 className="text-3xl font-bold mb-8">Experience</h2>
        <div className="space-y-6">
          {experience.slice(0, 3).map((exp: any) => (
            <div key={exp.uuid} className="border-l-4 border-blue-500 pl-4">
              <h3 className="text-xl font-semibold">{exp.fields?.role}</h3>
              <p className="text-gray-600">{exp.fields?.company}</p>
              <div 
                className="prose mt-2"
                dangerouslySetInnerHTML={{ 
                  __html: exp.fields?.description || '' 
                }}
              />
            </div>
          ))}
        </div>
      </section>
    </main>
  );
}

Step 7: Create Project Detail Pages

Generate dynamic project pages:

Prompt: "Create a dynamic Next.js route for individual project pages at /projects/[slug]. Include:

  • Project title, description, and images
  • Technology stack
  • Links to live site and GitHub
  • Image gallery
  • Use generateStaticParams for static generation
  • Fetch project data from ElmapiCMS"

Step 8: Add Skills and Contact Sections

Use Cursor to generate additional components:

Prompt: "Create a skills section component that displays skills from the about collection as badges or tags. Style it with Tailwind CSS."

Prompt: "Create a contact section component that displays contact information and social links from the contact settings. Include a contact form."


Step 9: Style and Polish

Ask Cursor to help with styling:

Prompt: "Improve the design of this portfolio website. Add:

  • Better typography
  • Smooth animations
  • Dark mode support
  • Better mobile responsiveness
  • Loading states"

Step 10: Deploy

Once everything is working locally, deploy your portfolio:

Prompt: "Show me how to deploy this Next.js portfolio to Vercel. Include environment variable setup."

Or follow Vercel's deployment guide:

  1. Push your code to GitHub
  2. Import the project in Vercel
  3. Add your environment variables
  4. Deploy

Tips for Working with Cursor

Iterative Development

Don't try to generate everything at once. Break it down:

  1. First iteration: Basic structure and data fetching
  2. Second iteration: Styling and layout
  3. Third iteration: Animations and polish

Use Context

Open relevant files when asking Cursor to generate code. For example:

  • Open lib/elmapi.ts when asking for API functions
  • Open existing components when asking for similar ones
  • Open the template JSON when asking about data structure

Refine and Iterate

Use follow-up prompts to refine:

"Make the project cards more visually appealing with hover effects."

"Add a filter by technology feature to the projects page."

"Improve the mobile layout for the hero section."


Common Patterns

Fetching Singleton Collections

const about = await client.getEntry('about');

Fetching Multi-Entry Collections

const projects = await client.getEntries('projects', {
  state: 'published',
  sort: { completion_date: 'desc' },
  limit: 10
});

Handling Rich Text

<div 
  dangerouslySetInnerHTML={{ __html: entry.fields.description }} 
/>

Working with Media

<Image
  src={entry.fields.featured_image}
  alt={entry.fields.title}
  width={800}
  height={600}
/>

Conclusion

Building a portfolio with ElmapiCMS and Cursor is a powerful combination. You can:

  • Design your content structure with AI assistance
  • Generate template files automatically
  • Build frontend components quickly
  • Iterate and refine with AI help

The key is breaking the work into small, focused prompts and iterating on the results. Start with the structure, then build out features one at a time.

Your portfolio is now ready to showcase your work, and you can easily update content through ElmapiCMS without touching code.


Next Steps

  • Customize the design to match your brand
  • Add more sections (testimonials, certifications, etc.)
  • Set up a blog section
  • Add analytics and SEO optimization
  • Deploy and share your portfolio!

Happy building with ElmapiCMS and Cursor!

Share this post:

Ready to build your content API?

Try ElmapiCMS free with our live demo, or get the full source code for $149.

Comparing options first? Read our best headless CMS comparison.

Related Posts