TL;DR
In this article, I'll show you how to build a complete portfolio with a backend to manage your projects. We'll use bolt for the frontend and Manifest to spin up a backend in seconds. No complexity, no bloat—just a fast, efficient, and streamlined workflow. Your portfolio will be ready to deploy in minutes.
Introduction
As developers, We often need to build content-driven websites like blogs, portfolios, or showcases, whether for personal use or clients. Most backend solutions are bloated. They require long setups, endless tutorials, and unnecessary complexity just to get started.
Building a modern portfolio with content management shouldn’t be a burden.
Manifest fixes this. No over-engineering, no wasted time. Just a backend that works instantly.
Prerequisites
Before getting started, make sure you have Node.js and npm installed on your machine.
Used Tools
Here’s an overview of the tools We’ll be working with:
- Bolt.new: To generate a portfolio frontend from a simple prompt.
- Manifest: To generate a complete backend. It includes a documented API, a database, an admin panel with authentication, and storage for images, and much more.
Setting Up the Project
The goal here is to show you how to easily add a backend to any frontend. I won’t go into too much detail on frontend development. Instead, I’ll simply add a backend to a random portfolio frontend generated by Bolt.new.
Initializing the Frontend with Bolt.new
Let's start by generating the frontend. Head over to bolt.new and enter a prompt. I used: 'Create a beautiful one-page portfolio with a contact form.'
Bolt will then generate a set of specs. We can iterate on them until we're happy with the proposed frontend.
Overview of our portfolio
After a few iterations, I ended up with the following portfolio.
Let’s take a look at the project’s code. The generated structure is clean, with each section of the site having its own .tsx component.
These files handle only the frontend. There's no business logic yet. We'll use Manifest to add a backend and make the projects dynamic.
Bolt doesn't yet have direct integration with Manifest, but adding it manually is still quick and easy. ⚡
Initialize the backend with manifest
Still from bolt.new, we'll open a second terminal and install manifest using the following command:
npx add-manifest
After installing the backend, a Manifest repository appears at the root of the project.
Now, let’s define our data model. Since projects are defined by the following fields:
- Project title
- Image with two different sizes
- Description
- External link
- Role
- Date
Let's open the manifest/backend.yml
file and replace the existing code with this:
name: my portfolio
entities:
Project:
properties:
- title
- description
- role
- { name: date, type: date }
- { name: url, type: link }
- {
name: photo,
type: image,
options:
{
sizes:
{
small: { height: 403, width: 805 },
large: { height: 806, width: 1610 },
},
},
}
Contacts:
properties:
- name
- email
- { name: message, type: text }
Starting the backend
Still from our second terminal, We run the following command:
manifest run dev
When the backend is up and running, the terminal provides two links:
- 🖥️ Admin panel: http://localhost:1111
- 📚 API documentation: http://localhost:1111/api
From the "preview" button in bolt, We can choose to view either the portfolio frontend, the admin panel, or the API documentation.
Let's open the admin panel (http://localhost:1111) and log in using the pre-filled credentials.
In the admin panel sidebar, We'll click on "Collections", then, "Projects". We should now see an empty list of projects.
Let's click "Create a new project", fill in all the fields, and submit the project.
Once our project is submitted, We should see it appear in the project list.
Replacing static data with dynamic content
Now, we need to replace the static projects with data from the backend. The first step is to install the Manifest SDK so we can interact with our backend.
Installing the Manifest SDK
Open another terminal and install the manifest SDK:
npm i @mnfst/sdk
Use the Manifest JS SDK to fetch our data
Once the installation is complete, We'll open the work.tsx file and take a look.
This file contains:
- Static data
- The HTML structure displaying that data
work.tsx
import { ExternalLink } from 'lucide-react'
import { FadeIn } from '../animations/FadeIn'
const projects = [
{
photo: {
large: 'https://via.placeholder.com/800x450',
small: 'https://via.placeholder.com/400x225',
},
title: 'Modern E-Commerce Platform',
description: 'An intuitive e-commerce solution for small businesses.',
role: 'Lead Designer',
date: '2023-06-15',
url: 'https://example.com/case-study-1',
},
{
photo: {
large: 'https://via.placeholder.com/800x450',
small: 'https://via.placeholder.com/400x225',
},
title: 'AI-Powered Analytics Dashboard',
description: 'A comprehensive analytics tool leveraging AI.',
role: 'Frontend Developer',
date: '2022-11-10',
url: 'https://example.com/case-study-2',
},
{
photo: {
large: 'https://via.placeholder.com/800x450',
small: 'https://via.placeholder.com/400x225',
},
title: 'Creative Portfolio Website',
description: 'A portfolio showcasing creative work in digital design.',
role: 'Full Stack Developer',
date: '2021-09-20',
url: 'https://example.com/case-study-3',
},
]
export function Work() {
return (
<section id="work" className="py-32 bg-white">
<div className="container mx-auto px-6">
<FadeIn>
<h2 className="text-4xl md:text-5xl font-bold mb-16 text-center">
Selected Work
</h2>
</FadeIn>
<div className="space-y-32">
{projects.map((project, index) => (
<FadeIn key={index} delay={index * 0.2}>
<div className="group">
<div
className={`grid md:grid-cols-3 gap-12 items-center ${
index % 2 === 1 ? 'md:grid-flow-dense' : ''
}`}
>
<div
className={`md:col-span-2 ${
index % 2 === 1 ? 'md:col-start-2' : ''
}`}
>
<div className="relative aspect-[16/9] overflow-hidden rounded-xl">
<img
src={project.photo.large || project.photo.small}
alt={project.title}
className="w-full h-full object-cover bg-slate-100 transition-transform duration-700 group-hover:scale-105"
/>
</div>
</div>
<div className="space-y-6">
<h3 className="text-3xl font-bold">{project.title}</h3>
<p className="text-gray-600 text-lg leading-relaxed">
{project.description}
</p>
<div className="flex flex-col gap-4">
<div>
<p className="text-sm text-gray-500">Role</p>
<p className="text-lg font-medium">{project.role}</p>
</div>
<div>
<p className="text-sm text-gray-500">Date</p>
<p className="text-lg font-medium">{project.date}</p>
</div>
<a
href={project.url}
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 font-medium"
>
View Case Study <ExternalLink size={18} />
</a>
</div>
</div>
</div>
</div>
</FadeIn>
))}
</div>
</div>
</section>
)
}
To connect work.tsx
to the backend, We first need to import the manifest SDK. Let's add this import at the top of the file:
import Manifest from '@mnfst/sdk'
Now, We'll add the types/types.ts file and define the project interface:
export interface Project {
photo: { large: string; small: string }
title: string
description: string
role: string
date: string
url: string
}
This file provides a clear and reusable type definition for projects.
Next, in work.tsx, We'll remove the static code. In my case, it looks like this:
/* [...] */
const projects = [
{
photo: {
large: 'https://via.placeholder.com/800x450',
small: 'https://via.placeholder.com/400x225',
},
title: 'Modern E-Commerce Platform',
description: 'An intuitive e-commerce solution for small businesses.',
role: 'Lead Designer',
date: '2023-06-15',
url: 'https://example.com/case-study-1',
},
{
photo: {
large: 'https://via.placeholder.com/800x450',
small: 'https://via.placeholder.com/400x225',
},
title: 'AI-Powered Analytics Dashboard',
description: 'A comprehensive analytics tool leveraging AI.',
role: 'Frontend Developer',
date: '2022-11-10',
url: 'https://example.com/case-study-2',
}
]
export function Work() {
/* [...] */
and replace it with:
/* [...] */
import Manifest from '@mnfst/sdk'
import { useEffect, useState } from 'react'
import { Project } from '../../types/types'
export function Work() {
const [projects, setProjects] = useState<Project[]>([])
useEffect(() => {
const fetchProjects = async () => {
const manifest = new Manifest('http://localhost:1111')
const paginator = await manifest
.from('projects')
.orderBy('date', { desc: true })
.find()
const projects = paginator.data as Project[]
setProjects(projects)
}
fetchProjects()
}, [])
/* [...] */
I used two React hooks to manage the data flow:
- useState: Handles local state inside a functional component.
- useEffect: Runs code in response to state changes, component mounting, or unmounting.
Let's check the portfolio interface. The newly added project should now appear.
At this point, the static content has been successfully replaced with dynamic data from Manifest! 🚀
Projects are now loaded directly from the database and displayed dynamically on the frontend.
Even better, it's now possible to add, edit, or delete projects directly from the admin panel! 🎯
Conclusion
With just a few steps, we’ve turned a static portfolio into a dynamic, fully managed application. What’s powerful here isn’t just the speed of setup—it’s the flexibility this approach provides.
This isn’t just about portfolios. The same workflow can be applied to blogs, dashboards, or any content-driven project. With Manifest handling the backend, you’re free to iterate, scale, and focus on the frontend experience rather than backend maintenance.
Next Steps
With the project ready, the next logical step is to download it and push it to GitHub before deployment. This ensures proper version control and makes future updates easier.
Manifest is built for speed and simplicity. From setup to deployment, everything is designed to be as smooth and effortless as possible. Deployment is just as fast. With a few steps, your backend is live.
Plus, Manifest Cloud is in development, allowing you to deploy instantly, without any setup. If you want to be notified when this feature launches, sign up here:
🔹 Live portfolio on StackBlitz
🔹 GitHub repository
If you found this post useful, feel free to share it with others who might appreciate it.
Top comments (0)