Global Layouts with Payload CMS: How to Manage Headers, Footers, and SEO
Managing global layouts, headers, footers, and SEO configurations is crucial for maintaining consistency across your website. Payload CMS provides powerful tools for creating reusable global elements that can be managed by content editors while maintaining the flexibility developers need. This guide covers everything you need to build scalable global layout systems.
Understanding Global Elements in Payload
Global elements in Payload are singleton collections that store site-wide configuration data. Unlike regular collections that can have multiple documents, globals represent single instances of data that are shared across your entire site.
Basic Global Configuration
// globals/SiteSettings.ts
import { GlobalConfig } from 'payload/types'
export const SiteSettings: GlobalConfig = {
slug: 'site-settings',
label: 'Site Settings',
fields: [
{
name: 'siteName',
type: 'text',
required: true,
},
{
name: 'siteUrl',
type: 'text',
required: true,
},
{
name: 'description',
type: 'textarea',
},
{
name: 'logo',
type: 'upload',
relationTo: 'media',
},
],
}
Creating a Header Global
1. Header Configuration
// globals/Header.ts
import { GlobalConfig } from 'payload/types'
export const Header: GlobalConfig = {
slug: 'header',
label: 'Header',
fields: [
{
name: 'logo',
type: 'group',
fields: [
{
name: 'image',
type: 'upload',
relationTo: 'media',
},
{
name: 'alt',
type: 'text',
required: true,
},
{
name: 'url',
type: 'text',
defaultValue: '/',
},
],
},
{
name: 'navigation',
type: 'array',
fields: [
{
name: 'label',
type: 'text',
required: true,
},
{
name: 'type',
type: 'select',
options: [
{ label: 'Internal Link', value: 'internal' },
{ label: 'External Link', value: 'external' },
{ label: 'Dropdown', value: 'dropdown' },
],
required: true,
},
{
name: 'url',
type: 'text',
admin: {
condition: (data, siblingData) =>
siblingData.type === 'internal' || siblingData.type === 'external',
},
},
{
name: 'subItems',
type: 'array',
admin: {
condition: (data, siblingData) => siblingData.type === 'dropdown',
},
fields: [
{
name: 'label',
type: 'text',
required: true,
},
{
name: 'url',
type: 'text',
required: true,
},
],
},
],
},
{
name: 'ctaButton',
type: 'group',
fields: [
{
name: 'show',
type: 'checkbox',
defaultValue: false,
},
{
name: 'text',
type: 'text',
admin: {
condition: (data, siblingData) => siblingData.show,
},
},
{
name: 'url',
type: 'text',
admin: {
condition: (data, siblingData) => siblingData.show,
},
},
],
},
],
}
2. Header Component
// components/Header.tsx
import React from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { Button } from '@/components/ui/button'
interface HeaderProps {
header: {
logo: {
image: {
url: string
alt: string
}
alt: string
url: string
}
navigation: Array<{
label: string
type: 'internal' | 'external' | 'dropdown'
url?: string
subItems?: Array<{
label: string
url: string
}>
}>
ctaButton: {
show: boolean
text?: string
url?: string
}
}
}
export function Header({ header }: HeaderProps) {
return (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
<div className="container flex h-16 items-center">
{/* Logo */}
<Link href={header.logo.url} className="mr-6 flex items-center space-x-2">
{header.logo.image?.url && (
<Image
src={header.logo.image.url}
alt={header.logo.alt}
width={32}
height={32}
className="h-8 w-8"
/>
)}
</Link>
{/* Navigation */}
<nav className="hidden md:flex flex-1 items-center space-x-6 text-sm font-medium">
{header.navigation?.map((item, index) => (
<div key={index} className="relative group">
{item.type === 'dropdown' ? (
<>
<button className="flex items-center space-x-1 text-foreground/60 hover:text-foreground">
<span>{item.label}</span>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
<div className="absolute top-full left-0 mt-2 w-48 rounded-md bg-popover shadow-lg ring-1 ring-black ring-opacity-5 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
<div className="py-1">
{item.subItems?.map((subItem, subIndex) => (
<Link
key={subIndex}
href={subItem.url}
className="block px-4 py-2 text-sm text-popover-foreground hover:bg-accent"
>
{subItem.label}
</Link>
))}
</div>
</div>
</>
) : (
<Link
href={item.url || '#'}
className="text-foreground/60 hover:text-foreground transition-colors"
{...(item.type === 'external' && { target: '_blank', rel: 'noopener noreferrer' })}
>
{item.label}
</Link>
)}
</div>
))}
</nav>
{/* CTA Button */}
{header.ctaButton?.show && header.ctaButton.text && header.ctaButton.url && (
<Button asChild className="ml-auto">
<Link href={header.ctaButton.url}>
{header.ctaButton.text}
</Link>
</Button>
)}
</div>
</header>
)
}
Creating a Footer Global
1. Footer Configuration
// globals/Footer.ts
export const Footer: GlobalConfig = {
slug: 'footer',
label: 'Footer',
fields: [
{
name: 'sections',
type: 'array',
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'links',
type: 'array',
fields: [
{
name: 'label',
type: 'text',
required: true,
},
{
name: 'url',
type: 'text',
required: true,
},
{
name: 'newTab',
type: 'checkbox',
defaultValue: false,
},
],
},
],
},
{
name: 'socialLinks',
type: 'array',
fields: [
{
name: 'platform',
type: 'select',
options: [
{ label: 'Twitter', value: 'twitter' },
{ label: 'Facebook', value: 'facebook' },
{ label: 'Instagram', value: 'instagram' },
{ label: 'LinkedIn', value: 'linkedin' },
{ label: 'GitHub', value: 'github' },
],
required: true,
},
{
name: 'url',
type: 'text',
required: true,
},
],
},
{
name: 'copyright',
type: 'textarea',
},
],
}
2. Footer Component
// components/Footer.tsx
import React from 'react'
import Link from 'next/link'
interface FooterProps {
footer: {
sections: Array<{
title: string
links: Array<{
label: string
url: string
newTab: boolean
}>
}>
socialLinks: Array<{
platform: string
url: string
}>
copyright: string
}
}
const socialIcons = {
twitter: '🐦',
facebook: '📘',
instagram: '📷',
linkedin: '💼',
github: '🐙',
}
export function Footer({ footer }: FooterProps) {
return (
<footer className="border-t bg-muted/50">
<div className="container py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{footer.sections?.map((section, index) => (
<div key={index}>
<h3 className="font-semibold mb-4">{section.title}</h3>
<ul className="space-y-2">
{section.links?.map((link, linkIndex) => (
<li key={linkIndex}>
<Link
href={link.url}
className="text-muted-foreground hover:text-foreground transition-colors"
{...(link.newTab && { target: '_blank', rel: 'noopener noreferrer' })}
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
{/* Social Links */}
{footer.socialLinks && footer.socialLinks.length > 0 && (
<div className="mt-8 pt-8 border-t flex items-center justify-between">
<div className="flex space-x-4">
{footer.socialLinks.map((social, index) => (
<Link
key={index}
href={social.url}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label={`Follow us on ${social.platform}`}
>
<span className="text-xl">
{socialIcons[social.platform as keyof typeof socialIcons] || '🔗'}
</span>
</Link>
))}
</div>
{footer.copyright && (
<p className="text-sm text-muted-foreground">
{footer.copyright}
</p>
)}
</div>
)}
</div>
</footer>
)
}
SEO Global Configuration
1. SEO Global Settings
// globals/SEO.ts
export const SEO: GlobalConfig = {
slug: 'seo',
label: 'SEO Settings',
fields: [
{
name: 'siteName',
type: 'text',
required: true,
},
{
name: 'defaultTitle',
type: 'text',
required: true,
},
{
name: 'titleTemplate',
type: 'text',
defaultValue: '%s | Your Site',
admin: {
description: 'Use %s as placeholder for page title',
},
},
{
name: 'defaultDescription',
type: 'textarea',
required: true,
},
{
name: 'defaultImage',
type: 'upload',
relationTo: 'media',
},
{
name: 'twitterHandle',
type: 'text',
admin: {
description: 'Twitter username without @',
},
},
{
name: 'googleAnalytics',
type: 'text',
admin: {
description: 'Google Analytics tracking ID',
},
},
{
name: 'structuredData',
type: 'group',
fields: [
{
name: 'organizationName',
type: 'text',
},
{
name: 'organizationType',
type: 'select',
options: [
{ label: 'Organization', value: 'Organization' },
{ label: 'Corporation', value: 'Corporation' },
{ label: 'LocalBusiness', value: 'LocalBusiness' },
],
},
{
name: 'logo',
type: 'upload',
relationTo: 'media',
},
{
name: 'contactInfo',
type: 'group',
fields: [
{ name: 'phone', type: 'text' },
{ name: 'email', type: 'email' },
{ name: 'address', type: 'textarea' },
],
},
],
},
],
}
2. SEO Component
// components/SEO.tsx
import Head from 'next/head'
interface SEOProps {
title?: string
description?: string
image?: string
url?: string
article?: boolean
seo: {
siteName: string
defaultTitle: string
titleTemplate: string
defaultDescription: string
defaultImage?: { url: string }
twitterHandle?: string
}
}
export function SEO({
title,
description,
image,
url,
article = false,
seo
}: SEOProps) {
const pageTitle = title
? seo.titleTemplate.replace('%s', title)
: seo.defaultTitle
const pageDescription = description || seo.defaultDescription
const pageImage = image || seo.defaultImage?.url
const pageUrl = url || process.env.NEXT_PUBLIC_SITE_URL
return (
<Head>
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
{/* Open Graph */}
<meta property="og:type" content={article ? 'article' : 'website'} />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDescription} />
<meta property="og:site_name" content={seo.siteName} />
{pageImage && <meta property="og:image" content={pageImage} />}
{pageUrl && <meta property="og:url" content={pageUrl} />}
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={pageDescription} />
{seo.twitterHandle && (
<meta name="twitter:site" content={`@${seo.twitterHandle}`} />
)}
{pageImage && <meta name="twitter:image" content={pageImage} />}
{/* Additional SEO tags */}
<meta name="robots" content="index, follow" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="canonical" href={pageUrl} />
</Head>
)
}
Layout Component Integration
1. Main Layout Component
// components/Layout.tsx
import React from 'react'
import { Header } from './Header'
import { Footer } from './Footer'
import { SEO } from './SEO'
interface LayoutProps {
children: React.ReactNode
globals: {
header: any
footer: any
seo: any
}
seo?: {
title?: string
description?: string
image?: string
url?: string
}
}
export function Layout({ children, globals, seo }: LayoutProps) {
return (
<>
<SEO {...seo} seo={globals.seo} />
<div className="min-h-screen flex flex-col">
<Header header={globals.header} />
<main className="flex-1">
{children}
</main>
<Footer footer={globals.footer} />
</div>
</>
)
}
2. Fetching Globals in Next.js
// lib/getGlobals.ts
import { getPayloadClient } from './payload'
export async function getGlobals() {
const payload = await getPayloadClient()
const [header, footer, seo] = await Promise.all([
payload.findGlobal({ slug: 'header' }),
payload.findGlobal({ slug: 'footer' }),
payload.findGlobal({ slug: 'seo' }),
])
return { header, footer, seo }
}
// Usage in pages
export async function getStaticProps() {
const globals = await getGlobals()
return {
props: {
globals,
},
revalidate: 60,
}
}
Best Practices
1. Global Validation
// Add validation to globals
{
name: 'siteUrl',
type: 'text',
validate: (val) => {
if (val && !val.startsWith('http')) {
return 'Site URL must start with http:// or https://'
}
},
}
2. Admin Interface Customization
// Organize globals in admin
export default buildConfig({
admin: {
components: {
beforeNavLinks: [
{
Component: '/components/GlobalsNavLinks',
},
],
},
},
globals: [Header, Footer, SEO, SiteSettings],
})
3. Type Safety
// types/globals.ts
export interface HeaderGlobal {
logo: {
image: { url: string; alt: string }
alt: string
url: string
}
navigation: NavigationItem[]
ctaButton: {
show: boolean
text?: string
url?: string
}
}
Conclusion
Global layouts in Payload CMS provide a powerful foundation for managing site-wide elements while maintaining flexibility and ease of use for content editors. By properly structuring your globals, you can create maintainable, scalable websites with consistent branding and SEO optimization.
Next: Learn how to implement multilingual themes and localization in Payload blocks.