This comprehensive guide covers common issues specific to Payload CMS websites. Since Payload runs embedded within Next.js applications, issues often relate to the integration between the headless CMS and frontend framework, collection hooks, admin panel conflicts, and routing complexities affecting analytics tracking.
Common Issues Overview
Payload CMS troubleshooting typically falls into these categories:
Next.js Integration Issues:
- Admin panel routes conflicting with analytics
- App Router vs Pages Router tracking differences
- Server components affecting client-side analytics
- API routes not triggering tracking events
Collection and Hook Problems:
- AfterChange hooks not firing analytics events
- BeforeValidate hooks interfering with tracking
- Collection access control preventing analytics
- Relationship population affecting performance
Draft and Preview Mode:
- Analytics firing in draft preview mode
- Preview URLs exposing tracking data
- Draft content appearing in analytics
- Version history not tracked properly
Performance and Build:
- Large media collections slowing analytics
- Build-time data fetching timeouts
- Incremental Static Regeneration conflicts
- Cold start performance in serverless
Installation Troubleshooting
Payload Analytics Plugin Installation
Symptom: Analytics plugin not loading after installation
Diagnostic Steps:
- Check plugin is listed in
payload.config.ts:
import { buildConfig } from 'payload/config'
import { googleAnalytics } from '@payloadcms/plugin-google-analytics'
export default buildConfig({
plugins: [
googleAnalytics({
measurementID: 'G-XXXXXXXXXX'
})
]
})
- Verify plugin dependencies installed:
npm list @payloadcms/plugin-google-analytics
# or
yarn why @payloadcms/plugin-google-analytics
- Check build output for plugin errors:
npm run build 2>&1 | grep -i "analytics\|error"
Common Installation Issues:
| Issue | Cause | Solution |
|---|---|---|
| Plugin not found | Not installed | npm install @payloadcms/plugin-google-analytics |
| Type errors | Version mismatch | Ensure plugin version matches Payload version |
| Build fails | Config syntax error | Validate payload.config.ts syntax |
| Plugin doesn't load | Server not restarted | Restart Next.js dev server |
Admin Panel Access Issues
Symptom: Admin routes (/admin) conflict with analytics tracking
Admin Route Exclusion:
// In your analytics component
import { useRouter } from 'next/router'
export function Analytics() {
const router = useRouter()
// Don't load analytics on admin routes
if (router.pathname.startsWith('/admin')) {
return null
}
return <GoogleAnalytics measurementId="G-XXXXXXXXXX" />
}
App Router Approach:
// app/layout.tsx
import { headers } from 'next/headers'
export default function RootLayout({ children }) {
const headersList = headers()
const pathname = headersList.get('x-invoke-path') || ''
const isAdmin = pathname.startsWith('/admin')
return (
<html>
<body>
{!isAdmin && <Analytics />}
{children}
</body>
</html>
)
}
Database Connection Issues
Symptom: Analytics data not being saved to Payload database
MongoDB Connection Check:
// Test MongoDB connection
import { MongoClient } from 'mongodb'
async function testConnection() {
const client = new MongoClient(process.env.DATABASE_URI)
try {
await client.connect()
console.log('MongoDB connected')
const db = client.db()
const collections = await db.listCollections().toArray()
console.log('Collections:', collections.map(c => c.name))
} catch (error) {
console.error('MongoDB connection failed:', error)
} finally {
await client.close()
}
}
Postgres Connection Check:
import { Pool } from 'pg'
const pool = new Pool({
connectionString: process.env.DATABASE_URI
})
async function testConnection() {
try {
const result = await pool.query('SELECT NOW()')
console.log('Postgres connected:', result.rows[0].now)
} catch (error) {
console.error('Postgres connection failed:', error)
}
}
Configuration Issues
Collection Hook Configuration
Issue: Analytics events not firing from collection hooks
Correct Hook Implementation:
// collections/Products.ts
import type { CollectionConfig } from 'payload/types'
import { trackEvent } from '../lib/analytics'
export const Products: CollectionConfig = {
slug: 'products',
hooks: {
afterChange: [
async ({ doc, operation, req }) => {
// Only track in production, not in admin
if (process.env.NODE_ENV === 'production' && !req.user) {
await trackEvent({
event: operation === 'create' ? 'product_created' : 'product_updated',
properties: {
productId: doc.id,
productName: doc.name,
category: doc.category
}
})
}
}
],
afterDelete: [
async ({ doc, req }) => {
if (process.env.NODE_ENV === 'production' && !req.user) {
await trackEvent({
event: 'product_deleted',
properties: {
productId: doc.id
}
})
}
}
]
}
}
Server-Side Analytics Function:
// lib/analytics.ts
export async function trackEvent({ event, properties }: {
event: string
properties: Record<string, any>
}) {
try {
// Use Measurement Protocol for server-side tracking
const response = await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${process.env.GA_MEASUREMENT_ID}&api_secret=${process.env.GA_API_SECRET}`,
{
method: 'POST',
body: JSON.stringify({
client_id: 'server',
events: [{
name: event,
params: properties
}]
})
}
)
if (!response.ok) {
console.error('Analytics tracking failed:', await response.text())
}
} catch (error) {
console.error('Analytics error:', error)
}
}
Draft Mode Tracking Issues
Issue: Analytics firing when viewing draft content
Draft Detection:
// app/components/Analytics.tsx
import { draftMode } from 'next/headers'
export function Analytics() {
const { isEnabled } = draftMode()
if (isEnabled) {
console.log('Draft mode active - analytics disabled')
return null
}
return <GoogleAnalytics measurementId="G-XXXXXXXXXX" />
}
Preview API Route Protection:
// app/api/preview/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const token = searchParams.get('token')
// Validate preview token
if (!isValidPreviewToken(token)) {
return new Response('Invalid token', { status: 401 })
}
// Enable draft mode
draftMode().enable()
return redirect(searchParams.get('redirect') || '/')
}
Access Control and Analytics
Issue: Admin user actions being tracked as public events
User Detection in Hooks:
hooks: {
afterChange: [
async ({ doc, operation, req }) => {
// Don't track admin user actions
if (req.user) {
console.log('Admin action - not tracking:', operation, req.user.email)
return doc
}
// Track public user actions
await trackEvent({ event: 'public_action', properties: { operation } })
return doc
}
]
}
Performance Problems and Solutions
Admin Panel Performance
Symptom: Admin panel loads slowly, affecting overall site analytics
Performance Diagnostics:
// Add timing to admin config
export default buildConfig({
admin: {
webpack: (config) => {
// Enable build timing
config.plugins.push(new (require('speed-measure-webpack-plugin'))())
return config
}
}
})
Optimization Strategies:
| Performance Issue | Solution |
|---|---|
| Slow admin JavaScript bundle | Code-split admin from frontend |
| Large media library | Implement pagination, lazy loading |
| Complex access control | Cache access decisions |
| Many relationships | Limit depth, use select fields |
Admin Bundle Separation:
// next.config.js
module.exports = {
webpack: (config, { isServer }) => {
if (!isServer) {
// Exclude admin bundles from client
config.optimization.splitChunks.cacheGroups.admin = {
test: /[\\/]admin[\\/]/,
name: 'admin',
chunks: 'async'
}
}
return config
}
}
Rich Text Rendering Performance
Symptom: Pages with rich text fields load slowly, delaying analytics
Lazy Load Rich Text:
// components/RichText.tsx
import dynamic from 'next/dynamic'
const RichTextRenderer = dynamic(
() => import('./RichTextRenderer'),
{
loading: () => <p>Loading content...</p>,
ssr: false // Don't render on server if not needed
}
)
export function RichText({ content }) {
return <RichTextRenderer content={content} />
}
Optimize Rich Text Queries:
// Only fetch fields needed for display
const product = await payload.findByID({
collection: 'products',
id: params.id,
select: {
name: true,
price: true,
// Don't fetch large rich text field unless needed
description: showDescription
}
})
Block Component Optimization
Issue: Many block components slow page render and analytics initialization
Optimize Block Loading:
// Lazy load blocks
import dynamic from 'next/dynamic'
const blockComponents = {
hero: dynamic(() => import('./blocks/Hero')),
features: dynamic(() => import('./blocks/Features')),
testimonials: dynamic(() => import('./blocks/Testimonials'))
}
export function BlockRenderer({ blocks }) {
return blocks.map((block) => {
const Component = blockComponents[block.blockType]
return <Component key={block.id} {...block} />
})
}
Track Block Visibility:
// Track which blocks are actually viewed
import { useInView } from 'react-intersection-observer'
export function Block({ blockType, content }) {
const { ref, inView } = useInView({
triggerOnce: true,
threshold: 0.5
})
useEffect(() => {
if (inView) {
trackEvent({
event: 'block_viewed',
properties: { blockType }
})
}
}, [inView, blockType])
return <div ref={ref}>{content}</div>
}
Database Query Performance
Symptom: Slow queries affecting analytics data retrieval
Query Optimization:
// Bad: Fetching all fields and relationships
const products = await payload.find({
collection: 'products',
depth: 3 // Fetches all nested relationships
})
// Good: Selective field fetching
const products = await payload.find({
collection: 'products',
select: {
name: true,
price: true,
category: true
},
depth: 0 // No relationship population
})
Add Database Indexes:
// In collection config
fields: [
{
name: 'slug',
type: 'text',
index: true // Creates database index
},
{
name: 'category',
type: 'relationship',
relationTo: 'categories',
index: true
}
]
Integration Conflicts
Next.js App Router Conflicts
Issue: Server components don't support client-side analytics
Hybrid Approach:
// app/components/ClientAnalytics.tsx (Client Component)
'use client'
import { usePathname, useSearchParams } from 'next/navigation'
import { useEffect } from 'react'
export function ClientAnalytics() {
const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
const url = pathname + (searchParams.toString() ? `?${searchParams}` : '')
gtag('config', 'G-XXXXXXXXXX', { page_path: url })
}, [pathname, searchParams])
return null
}
// app/layout.tsx (Server Component)
import { ClientAnalytics } from './components/ClientAnalytics'
export default function RootLayout({ children }) {
return (
<html>
<head>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX" />
</head>
<body>
<ClientAnalytics />
{children}
</body>
</html>
)
}
Payload Admin API Route Conflicts
Issue: API routes conflicting with analytics endpoints
Route Organization:
app/
├── (payload)/ # Payload admin isolated
│ └── admin/
│ └── [[...slug]]/
│ └── page.tsx
├── api/
│ ├── analytics/ # Custom analytics endpoints
│ └── [payload]/ # Payload API routes
└── (website)/ # Public website
└── page.tsx
Route Configuration:
// payload.config.ts
export default buildConfig({
routes: {
admin: '/admin',
api: '/api',
graphQL: '/graphql'
},
admin: {
// Disable if using custom admin
disable: false
}
})
Serverless Function Timeouts
Issue: Analytics processing exceeds serverless timeout
Async Processing:
// Instead of processing synchronously in API route
export async function POST(request: Request) {
const data = await request.json()
// Queue for background processing
await queue.add('analytics', data)
return Response.json({ queued: true })
}
// Background worker
async function processAnalytics(data) {
// Process without time limits
await trackEvent(data)
}
Increase Vercel Timeout (if applicable):
// vercel.json
{
"functions": {
"app/api/analytics/**": {
"maxDuration": 30
}
}
}
Debugging Steps with Payload Tools
Enable Payload Debug Mode
Configuration:
// payload.config.ts
export default buildConfig({
debug: process.env.NODE_ENV === 'development',
loggerOptions: {
level: 'debug'
}
})
Console Output:
import { getPayload } from 'payload'
const payload = await getPayload({ config })
// Enable detailed logging
payload.logger.level = 'debug'
Hook Debugging
Add Debug Logging to Hooks:
hooks: {
beforeChange: [
async ({ data, req, operation }) => {
console.log('beforeChange hook:', { operation, data, user: req.user?.email })
return data
}
],
afterChange: [
async ({ doc, req, operation }) => {
console.log('afterChange hook:', { operation, docId: doc.id })
// Check if analytics should fire
const shouldTrack = !req.user && process.env.NODE_ENV === 'production'
console.log('Should track analytics:', shouldTrack)
return doc
}
]
}
Next.js Debug Mode
Enable Next.js Debugging:
# Debug mode
NODE_OPTIONS='--inspect' npm run dev
# Then attach debugger (Chrome DevTools or VS Code)
VS Code Launch Configuration:
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": ["<node_internals>/**"]
}
]
}
Browser Developer Tools
Network Monitoring:
// Monitor Payload API calls
fetch = new Proxy(fetch, {
apply(target, thisArg, args) {
const [url] = args
if (url.includes('/api/')) {
console.log('API call:', url)
}
return Reflect.apply(target, thisArg, args)
}
})
React DevTools Profiler:
- Install React DevTools extension
- Use Profiler tab to identify slow components
- Check for unnecessary re-renders affecting analytics
Database Query Logging
MongoDB Query Logging:
// payload.config.ts
import { mongooseAdapter } from '@payloadcms/db-mongodb'
export default buildConfig({
db: mongooseAdapter({
url: process.env.DATABASE_URI,
mongooseOptions: {
// Enable query logging
debug: process.env.NODE_ENV === 'development'
}
})
})
Postgres Query Logging:
import { postgresAdapter } from '@payloadcms/db-postgres'
export default buildConfig({
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URI,
// Log all queries
log: (msg) => console.log('SQL:', msg)
}
})
})
Common Error Messages
| Error | Meaning | Solution |
|---|---|---|
| "Cannot find module 'payload'" | Payload not installed | Run npm install payload |
| "Config not found" | payload.config.ts missing or invalid | Verify config file exists and exports default |
| "Collection not found" | Invalid collection slug | Check collection slug matches config |
| "Unauthorized" | Access control blocking operation | Review access control settings |
| "Hydration error" | Server/client mismatch | Ensure analytics only runs client-side |
When to Contact Support
Contact Payload Community For:
- Collection hook questions
- Access control configuration
- Plugin compatibility issues
- General troubleshooting
Escalate to Payload Enterprise Support For:
- Critical production bugs
- Performance issues at scale
- Custom enterprise features
- Security vulnerabilities
Self-Service Resources:
- Payload Documentation: https://payloadcms.com/docs
- Discord Community: https://discord.com/invite/payload
- GitHub Discussions: https://github.com/payloadcms/payload/discussions
- GitHub Issues: https://github.com/payloadcms/payload/issues
Best Practices
1. Separate Admin from Public Analytics:
- Always exclude admin routes from tracking
- Use server-side tracking for CMS actions
- Implement user detection in hooks
2. Optimize Performance:
- Lazy load heavy components
- Cache analytics scripts
- Use selective field queries
- Implement pagination
3. Test in Staging:
- Use separate analytics properties
- Test with realistic data volumes
- Verify draft mode exclusions
4. Monitor and Alert:
- Set up error tracking (Sentry, LogRocket)
- Monitor API performance
- Track database query times
- Alert on analytics failures