Appearance
Project Structure
Understanding the project structure will help you navigate and customize Portfolio CMS effectively.
Directory Overview
portfoliocms/
├── docs/ # VitePress documentation
├── src/
│ ├── access/ # Access control functions
│ ├── app/ # Next.js App Router
│ │ ├── (frontend)/ # Public-facing routes
│ │ └── (payload)/ # Payload admin routes
│ ├── collections/ # Payload collections
│ ├── components/ # React components
│ │ ├── forms/ # Form components
│ │ ├── layout/ # Layout components
│ │ ├── projects/ # Project-specific components
│ │ ├── providers/ # Context providers
│ │ ├── sections/ # Page sections
│ │ └── ui/ # UI primitives (shadcn)
│ ├── fields/ # Reusable Payload fields
│ ├── globals/ # Payload globals
│ ├── lib/ # Utilities and helpers
│ ├── migrations/ # Database migrations
│ ├── payload-types.ts # Auto-generated types
│ └── payload.config.ts # Payload configuration
├── .env.example # Environment template
├── package.json # Dependencies and scripts
└── tsconfig.json # TypeScript configurationDetailed Breakdown
/src/app - Next.js Routes
The app directory uses Next.js 15 App Router with route groups:
app/
├── (frontend)/ # Public website
│ ├── layout.tsx # Frontend layout with nav/footer
│ ├── page.tsx # Homepage
│ ├── global.css # Global styles
│ ├── style.css # Additional styles
│ ├── actions.ts # Server actions
│ ├── projects/
│ │ ├── page.tsx # Projects listing
│ │ └── [slug]/
│ │ └── page.tsx # Individual project
│ └── api/
│ ├── frontend-projects/
│ │ └── route.ts # Projects API
│ └── frontend-categories/
│ └── route.ts # Categories API
├── (payload)/ # Admin panel
│ ├── admin/
│ │ └── [[...segments]]/
│ │ ├── page.tsx # Admin pages
│ │ └── not-found.tsx
│ ├── api/
│ │ ├── [...slug]/
│ │ │ └── route.ts # REST API
│ │ ├── graphql/
│ │ │ └── route.ts # GraphQL API
│ │ └── graphql-playground/
│ │ └── route.ts # GraphQL playground
│ ├── custom.css # Admin panel styles
│ └── layout.tsx # Admin layout
├── robots.ts # robots.txt generation
└── sitemap.ts # sitemap.xml generation/src/collections - Content Types
Collections define your content schema:
collections/
├── index.ts # Exports all collections
├── Users.ts # Admin users (auth)
├── Media.ts # Image/file uploads
├── Categories.ts # Project categories
├── Projects.ts # Portfolio projects
├── Skills.ts # Technical skills
├── Experience.ts # Work experience
├── Educations.ts # Education history
├── Achievements.ts # Awards/certifications
├── Testimonials.ts # Client testimonials
├── Companies.ts # Client/company logos
└── Messages.ts # Contact submissionsExample Collection Structure:
typescript
// src/collections/Projects.ts
import type { CollectionConfig } from 'payload'
export const Projects: CollectionConfig = {
slug: 'projects',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'category', 'featured', 'status'],
},
access: {
read: () => true,
create: authenticated,
update: authenticated,
delete: authenticated,
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', unique: true },
{ name: 'category', type: 'relationship', relationTo: 'categories' },
{ name: 'featured', type: 'checkbox' },
// ... more fields
],
}/src/globals - Site Settings
Globals are singleton configurations:
globals/
├── index.ts # Exports all globals
├── SiteSettings.ts # Site name, logo, theme, SEO
├── Profile.ts # Personal info, bio, stats
├── Navigation.ts # Menu items, CTA
└── Footer.ts # Footer columns, copyright/src/components - React Components
Organized by purpose:
components/
├── forms/
│ └── ContactForm.tsx # Contact form with validation
├── layout/
│ ├── Navbar.tsx # Site navigation
│ ├── Footer.tsx # Site footer
│ ├── MobileNav.tsx # Mobile navigation
│ ├── SocialLinks.tsx # Social media links
│ └── index.ts # Barrel export
├── projects/
│ └── ProjectsGrid.tsx # Projects grid with filtering
├── providers/
│ └── ThemeProvider.tsx # Dark mode provider
├── sections/
│ ├── HeroSection.tsx # Hero banner
│ ├── AboutSection.tsx # About me section
│ ├── ProjectsSection.tsx # Featured projects
│ ├── SkillsSection.tsx # Skills display
│ ├── ExperienceSection.tsx
│ ├── EducationSection.tsx
│ ├── AchievementsSection.tsx
│ ├── TestimonialsSection.tsx
│ ├── CompaniesSection.tsx
│ ├── ServicesSection.tsx
│ ├── StatsSection.tsx
│ ├── ContactSection.tsx
│ └── index.ts # Barrel export
├── ui/ # shadcn/ui components
│ ├── button.tsx
│ ├── card.tsx
│ ├── container.tsx
│ ├── input.tsx
│ └── ... more components
└── RichTextRenderer.tsx # Lexical content renderer/src/lib - Utilities
Helper functions and utilities:
lib/
├── payload.ts # Payload client and data fetchers
├── utils.ts # General utilities (cn, urls)
└── email.ts # Email sending utilitiesData Fetching Example:
typescript
// src/lib/payload.ts
import { getPayload } from 'payload'
import config from '@payload-config'
export async function getProjects(options?: { featured?: boolean; limit?: number }) {
const payload = await getPayload({ config })
const { docs } = await payload.find({
collection: 'projects',
where: options?.featured ? { featured: { equals: true } } : {},
limit: options?.limit || 100,
sort: '-createdAt',
})
return docs
}/src/access - Access Control
Permission functions for collections and globals:
typescript
// src/access/index.ts
import type { Access } from 'payload'
export const authenticated: Access = ({ req: { user } }) => {
return Boolean(user)
}
export const anyone: Access = () => true/src/fields - Reusable Fields
Custom field configurations:
typescript
// src/fields/slug.ts
import type { Field } from 'payload'
export const slugField = (sourceField = 'title'): Field => ({
name: 'slug',
type: 'text',
unique: true,
admin: {
position: 'sidebar',
},
hooks: {
beforeValidate: [
({ data, value }) => {
if (!value && data?.[sourceField]) {
return slugify(data[sourceField])
}
return value
},
],
},
})Configuration Files
payload.config.ts
Main Payload configuration:
typescript
import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { s3Storage } from '@payloadcms/storage-s3'
export default buildConfig({
admin: {
user: 'users',
importMap: { baseDir: path.resolve(dirname) },
livePreview: {
breakpoints: [
{ label: 'Mobile', name: 'mobile', width: 375, height: 667 },
{ label: 'Tablet', name: 'tablet', width: 768, height: 1024 },
{ label: 'Desktop', name: 'desktop', width: 1440, height: 900 },
],
},
},
collections,
globals,
editor: lexicalEditor(),
db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL } }),
plugins: [
s3Storage({
/* config */
}),
],
})next.config.mjs
Next.js configuration with Payload integration.
tsconfig.json
TypeScript configuration with path aliases:
json
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@payload-config": ["./src/payload.config.ts"]
}
}
}Key Patterns
Server Components
Most components are Server Components by default:
tsx
// This is a Server Component (no 'use client')
export default async function ProjectsPage() {
const projects = await getProjects()
return <ProjectsGrid projects={projects} />
}Client Components
Interactive components use the 'use client' directive:
tsx
'use client'
import { useState } from 'react'
export function ContactForm() {
const [isSubmitting, setIsSubmitting] = useState(false)
// ...
}Data Fetching
Data is fetched on the server using Payload's Local API:
tsx
import { getPayload } from 'payload'
import config from '@payload-config'
export default async function Page() {
const payload = await getPayload({ config })
const { docs } = await payload.find({ collection: 'projects' })
return <ProjectsList projects={docs} />
}Next Steps
- Learn about the Admin Panel
- Explore Collections in detail
- Understand Globals configuration