Payload CMSGlobal LayoutsSEOHeadersFooters

Global Layouts with Payload CMS: How to Manage Headers, Footers, and SEO

Master global layout management in Payload CMS. Learn to create reusable headers, footers, and SEO configurations that work across your entire website.

ByDevelopment Team
9 min read

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.

Tagged with:Payload CMSGlobal LayoutsSEOHeadersFooters
More Articles