Skip to content

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 configuration

Detailed 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 submissions

Example 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 utilities

Data 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

Released under the MIT License.