page-helpers.ts

src/utils/page-helpers.ts
import type { Metadata } from 'next'
import { baseURL, defaultOGImage, defaultSEOConfig } from '@/app/metadata'
import { seo } from '@/config'
export interface PageMetadata {
title?: string
description?: string
keywords?: string[]
slug?: string
featuredImage?: string
focusKeyword?: string | string[]
readingTimeMinutes?: number
date?: string
categories?: string[]
publishDate?: string
modifiedDate?: string
alternates?: { canonical?: string }
authors?: { name: string; url?: string }[]
category?: string
applicationName?: string
referrer?: string
/**
* Per-page indexing override. Omit to inherit the site default (indexed).
* Set `{ index: false }` on pages that must not reach search results.
* e.g. unwritten legal placeholders.
*/
robots?: { index?: boolean; follow?: boolean }
openGraph?: {
type?: 'website' | 'article'
locale?: string
url?: string
siteName?: string
title?: string
description?: string
image?: {
url: string
alt?: string
}
}
twitter?: {
card?: 'summary' | 'summary_large_image' | 'app' | 'player'
site?: string
creator?: string
title?: string
description?: string
image?: string
}
other?: Record<string, string>
structuredData?: object[]
}
export function generatePageMetadata(
metadata: PageMetadata,
slug?: string[]
): Metadata {
return {
title: metadata?.title || 'Page',
description: metadata?.description || 'A page on our website',
keywords: metadata?.keywords || undefined,
other: {
...(metadata?.publishDate && {
'article:published_time': metadata.publishDate,
}),
...(metadata?.modifiedDate && {
'article:modified_time': metadata.modifiedDate,
}),
...metadata?.other,
},
alternates: metadata?.alternates?.canonical
? { canonical: `${baseURL}${metadata.alternates.canonical}` }
: undefined,
authors: metadata?.authors || undefined,
creator: defaultSEOConfig.creator,
publisher: defaultSEOConfig.publisher,
category: metadata?.category || undefined,
applicationName: metadata?.applicationName || undefined,
referrer: 'origin-when-cross-origin',
// A page-level `robots` override wins over the site default, so a single
// page can be excluded from search without changing global config.
robots: metadata?.robots
? { ...defaultSEOConfig.robots, ...metadata.robots }
: defaultSEOConfig.robots,
openGraph: {
type: metadata?.openGraph?.type || 'website',
locale: metadata?.openGraph?.locale || 'en_US',
url:
metadata?.openGraph?.url ||
`${baseURL}${slug ? `/${slug.join('/')}` : ''}`,
siteName: metadata?.openGraph?.siteName || seo.siteName,
title: metadata?.openGraph?.title || metadata?.title || 'Page',
description:
metadata?.openGraph?.description ||
metadata?.description ||
'A page on our website',
images: [
{
...defaultOGImage,
...(metadata?.openGraph?.image?.alt && {
alt: metadata.openGraph.image.alt,
}),
},
],
},
twitter: {
card: metadata?.twitter?.card || 'summary_large_image',
site: metadata?.twitter?.site || undefined,
creator: metadata?.twitter?.creator || undefined,
title:
metadata?.twitter?.title ||
metadata?.openGraph?.title ||
metadata?.title ||
'Page',
description:
metadata?.twitter?.description ||
metadata?.openGraph?.description ||
metadata?.description ||
'A page on our website',
images: defaultOGImage.url ? [defaultOGImage.url] : undefined,
},
}
}

Support

Talk to the developers of this project to learn more

We have been building professional websites for big clients for over 15 years. Gallop templates and blocks is our best foundation for SEO websites and web apps.

© 2026 Web Plant Media, LLC