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 Nuxt.js frontend code.
Table of Contents
- What We're Building
- Prerequisites
- Step 1: Design the Content Structure with Cursor
- Step 2: Load the Template into ElmapiCMS
- Step 3: Set Up the Nuxt.js Project
- Step 4: Install ElmapiCMS SDK
- Step 5: Create the API Client
- Step 6: Build the Home Page
- Step 7: Create Project Detail Pages
- Step 8: Add Skills and Contact Sections
- Step 9: Style and Polish
- Step 10: Deploy
- Tips for Working with Cursor
- Common Patterns
- Nuxt.js Specific Features
- Conclusion
- Next Steps
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 Nuxt.js 4 using the Composition API.
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 Nuxt.js 4)
- Basic knowledge of Vue.js and Nuxt.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
-
Open Cursor in your ElmapiCMS directory:
cd /path/to/your/elmapi-installation cursor . -
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
- Navigate to
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
- Create a new file:
resources/data/project_templates/portfolio.json - Paste the generated template structure
- 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:
- Go to your ElmapiCMS dashboard
- Click "Create project"
- Select "Choose from a template" from the project type dropdown
- Select "Portfolio" from the template dropdown
- Check "Include demo content" to see sample data
- Create the project
Step 3: Set Up the Nuxt.js Project
Now let's create the Nuxt.js frontend. In Cursor, open a terminal or use the chat:
Prompt: "Create a new Nuxt.js 4 project with TypeScript and Tailwind CSS."
Or manually run:
npx nuxi@latest init portfolio-website
cd portfolio-website
npm install
Then install Tailwind CSS:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
This will create a Nuxt.js 4 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 Nuxt.js project. Include environment variables setup."
Or manually:
npm install @elmapicms/js-sdk
Create a .env 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 composable for ElmapiCMS using @elmapicms/js-sdk in Nuxt.js. Include:
- Client initialization with environment variables
- Type-safe functions for fetching about section, projects, experience, and contact settings
- Error handling
- Use Nuxt's useRuntimeConfig for environment variables
- Export as a composable"
Cursor will generate something like:
// composables/useElmapi.ts
import { createClient } from '@elmapicms/js-sdk';
export const useElmapi = () => {
const config = useRuntimeConfig();
const client = createClient(
config.public.elmapiApiUrl,
config.public.elmapiApiKey,
config.public.elmapiProjectId
);
const getAbout = async () => {
try {
const about = await client.getEntry('about');
return about;
} catch (error) {
console.error('Error fetching about:', error);
return null;
}
};
const getProjects = async () => {
try {
const projects = await client.getEntries('projects', {
state: 'published',
sort: { completion_date: 'desc' }
});
return projects;
} catch (error) {
console.error('Error fetching projects:', error);
return [];
}
};
const getExperience = async () => {
try {
const experience = await client.getEntries('experience', {
state: 'published',
sort: { start_date: 'desc' }
});
return experience;
} catch (error) {
console.error('Error fetching experience:', error);
return [];
}
};
const getContact = async () => {
try {
const contact = await client.getEntry('contact-settings');
return contact;
} catch (error) {
console.error('Error fetching contact:', error);
return null;
}
};
return {
getAbout,
getProjects,
getExperience,
getContact
};
};
Update nuxt.config.ts to include runtime config:
export default defineNuxtConfig({
runtimeConfig: {
public: {
elmapiApiUrl: process.env.ELMAPI_API_URL,
elmapiApiKey: process.env.ELMAPI_API_KEY,
elmapiProjectId: process.env.ELMAPI_PROJECT_ID,
}
}
});
Step 6: Build the Home Page
Now let's create the main portfolio page. Use Cursor to generate the layout:
Prompt: "Create a Nuxt.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
- Use the Composition API and fetch data from ElmapiCMS using the composable
- Use NuxtImage for optimized images"
Cursor will generate a complete page component. Here's a simplified example:
<!-- pages/index.vue -->
<template>
<main class="min-h-screen">
<!-- Hero Section -->
<section class="container mx-auto px-4 py-20">
<div class="flex flex-col md:flex-row items-center gap-8">
<NuxtImg
v-if="about?.fields?.profile_image"
:src="about.fields.profile_image"
:alt="about.fields.name"
width="200"
height="200"
class="rounded-full"
/>
<div>
<h1 class="text-4xl font-bold mb-2">
{{ about?.fields?.name || 'Your Name' }}
</h1>
<p class="text-xl text-gray-600 mb-4">
{{ about?.fields?.title || 'Your Title' }}
</p>
<div
class="prose"
v-html="about?.fields?.bio || ''"
/>
</div>
</div>
</section>
<!-- Featured Projects -->
<section class="container mx-auto px-4 py-20">
<h2 class="text-3xl font-bold mb-8">Featured Projects</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<NuxtLink
v-for="project in featuredProjects"
:key="project.uuid"
:to="`/projects/${project.fields?.slug || project.uuid}`"
class="group"
>
<div class="border rounded-lg overflow-hidden hover:shadow-lg transition">
<NuxtImg
v-if="project.fields?.featured_image"
:src="project.fields.featured_image"
:alt="project.fields.title"
width="400"
height="250"
class="w-full h-48 object-cover"
/>
<div class="p-4">
<h3 class="text-xl font-semibold mb-2">
{{ project.fields?.title }}
</h3>
<p class="text-gray-600 line-clamp-2">
{{ project.fields?.description }}
</p>
</div>
</div>
</NuxtLink>
</div>
</section>
<!-- Experience -->
<section class="container mx-auto px-4 py-20">
<h2 class="text-3xl font-bold mb-8">Experience</h2>
<div class="space-y-6">
<div
v-for="exp in recentExperience"
:key="exp.uuid"
class="border-l-4 border-blue-500 pl-4"
>
<h3 class="text-xl font-semibold">{{ exp.fields?.role }}</h3>
<p class="text-gray-600">{{ exp.fields?.company }}</p>
<div
class="prose mt-2"
v-html="exp.fields?.description || ''"
/>
</div>
</div>
</section>
</main>
</template>
<script setup lang="ts">
const { getAbout, getProjects, getExperience } = useElmapi();
const about = await getAbout();
const projects = await getProjects();
const experience = await getExperience();
const featuredProjects = computed(() =>
projects
.filter((p: any) => p.fields?.featured)
.slice(0, 4)
);
const recentExperience = computed(() =>
experience.slice(0, 3)
);
useSeoMeta({
title: about.value?.fields?.name || 'Portfolio',
description: about.value?.fields?.bio || 'My Portfolio'
});
</script>
Step 7: Create Project Detail Pages
Generate dynamic project pages:
Prompt: "Create a dynamic Nuxt.js route for individual project pages at /projects/[slug].vue. Include:
- Project title, description, and images
- Technology stack
- Links to live site and GitHub
- Image gallery
- Use generateStaticParams equivalent for static generation
- Fetch project data from ElmapiCMS"
In Nuxt.js, you'll use definePageMeta and generateStaticParams:
<!-- pages/projects/[slug].vue -->
<template>
<article v-if="project">
<NuxtImg
:src="project.fields.featured_image"
:alt="project.fields.title"
width="1200"
height="600"
class="w-full h-96 object-cover"
/>
<div class="container mx-auto px-4 py-12">
<h1 class="text-4xl font-bold mb-4">{{ project.fields.title }}</h1>
<div v-html="project.fields.description" class="prose" />
<!-- More project details -->
</div>
</article>
</template>
<script setup lang="ts">
const route = useRoute();
const { getProjects } = useElmapi();
const projects = await getProjects();
const project = projects.find((p: any) =>
p.fields?.slug === route.params.slug || p.uuid === route.params.slug
);
if (!project) {
throw createError({ statusCode: 404, message: 'Project not found' });
}
</script>
Step 8: Add Skills and Contact Sections
Use Cursor to generate additional components:
Prompt: "Create a Vue component for a skills section that displays skills from the about collection as badges or tags. Style it with Tailwind CSS."
Prompt: "Create a Vue component for a contact section 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 using Vue transitions
- 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 Nuxt.js portfolio to a hosting service like Vercel, Netlify, or Cloudflare Pages. Include environment variable setup."
For Nuxt.js, you can deploy to:
- Vercel - Automatic deployments with zero config
- Netlify - Great for static sites
- Cloudflare Pages - Fast global CDN
- Any Node.js hosting - For SSR/SSG
Tips for Working with Cursor
Iterative Development
Don't try to generate everything at once. Break it down:
- First iteration: Basic structure and data fetching
- Second iteration: Styling and layout
- Third iteration: Animations and polish
Use Context
Open relevant files when asking Cursor to generate code. For example:
- Open
composables/useElmapi.tswhen 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 v-html="entry.fields.description" />
Working with Media
<NuxtImg
:src="entry.fields.featured_image"
:alt="entry.fields.title"
width="800"
height="600"
/>
Using Composables
<script setup>
const { getProjects } = useElmapi();
const projects = await getProjects();
</script>
Nuxt.js Specific Features
Server-Side Rendering
Nuxt.js automatically handles SSR. Your data fetching in setup functions runs on the server by default.
Static Site Generation
To generate a static site:
npm run generate
This will pre-render all pages at build time.
Auto-Imports
Nuxt.js automatically imports composables, components, and utilities. No need to manually import useElmapi if it's in the composables/ directory.
Image Optimization
Use NuxtImg component for automatic image optimization:
<NuxtImg
:src="imageUrl"
width="800"
height="600"
loading="lazy"
/>
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 with Vue and Nuxt.js
- 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!