Experience reference fields for metadata
Overview
Contentful Experiences typically have a title and slug, but for comprehensive page management, you often need additional metadata fields such as SEO descriptions, Open Graph tags, or other contextual information. This guide demonstrates how to enhance experiences with custom fields that can contain both static content and references to other entries, using SEO metadata as the primary example.
Table of contents
- Architecture overview
- Prerequisites
- Setting up the content model
- Creating the metadata extraction utilities
- Implementing the page component with metadata
- Handling referenced content
- Testing the implementation
Architecture overview
The solution extends Contentful Experiences with custom fields that can contain both static values and references to other entries, providing a flexible metadata system for enhanced SEO and page context.
Key components
- Custom experience fields for metadata (e.g.,
metaTitle,metaDescription) - Reference support for linking to other content entries
- Metadata extraction utilities that handle both static and referenced values
- Next.js metadata integration using
generateMetadatafunction - Type-safe field access with proper fallbacks
Data flow
- Content model setup: Define custom fields on the experience content type
- Content creation: Editors populate fields with static values or references
- Metadata extraction: Server-side utilities extract values from experience
- SEO integration: Next.js
generateMetadatafunction uses extracted values - Fallback handling: Graceful degradation when fields are empty
Prerequisites
Before starting this implementation, make sure you have:
- A Next.js project with App Router
- Contentful Experiences SDK installed (
@contentful/experiences-sdk-react) - Access to Contentful Studio for content model configuration
- Basic understanding of Next.js metadata API
- Familiarity with Contentful content types and references
Setting up the content model
The first step is to enhance your experience content type with custom fields for metadata. This example focuses on SEO-related fields, but the same pattern can be applied to any metadata needs.
Required fields
Add the following fields to your experience content type:
metaTitle(Short text) - Custom page title for SEOmetaDescription(Short text or Reference) - SEO description

This way editors can maintain those metadata fields directly in the editor view when working on experiences.

Field configuration
Configure your fields with appropriate validation and help text:
- Set field IDs that match your extraction logic
- Use appropriate field types (Short text, Long text, Media, Reference)
- Add validation rules for required fields
- Provide clear help text for content editors
Creating the metadata extraction utilities
The core of this solution is a set of utility functions that can extract metadata from experiences, handling both static values and references to other entries.
// src/utils/meta.ts
import { EntityStore, isEntry } from '@contentful/experiences-core';
import type {
Experience,
ExperienceFields,
} from '@contentful/experiences-core';
import type { Entry } from 'contentful';
type FooEntry = Entry<
{
contentTypeId: 'foo';
fields: {
description?: string;
};
// Using `undefined` for the modifier assuming that locales are already resolved (e.g., CDA/CPA)
},
undefined
>;
type BarEntry = Entry<
{
contentTypeId: 'bar';
fields: {
text?: string;
};
},
undefined
>;
/** Utility function to extract a specific field from the entry fields based on its content type */
const getFieldValueFromEntry = (input: Entry): string | undefined => {
// Since entry references don't point to a specific field, the whole entry is resolved
// and we have to extract the desired field based on the content type.
switch (input.sys.contentType.sys.id) {
case 'foo':
return (input as FooEntry).fields.description;
case 'bar':
return (input as BarEntry).fields.text;
default:
return undefined;
}
};
const hasCustomExperienceField = (
fields: undefined | ExperienceFields,
key: string
): fields is ExperienceFields & { [key]: unknown } =>
typeof fields === 'object' && fields !== null && key in fields;
/** Utility function to get a specific field from the experience while supporting both raw values and references to other entries */
const getFieldFromExperience = (
experience: Experience<EntityStore>,
key: string
): string | undefined => {
// Parsing the tree is a NOT OFFICIALLY SUPPORTED strategy.
// We’re exploring having a standalone SDK method for this in a future version, so use at your own risk.
const fields = experience?.entityStore?.experienceEntryFields;
const value = hasCustomExperienceField(fields, key) ? fields[key] : undefined;
// Value is a reference
if (isEntry(value)) {
return getFieldValueFromEntry(value);
}
// Value is a raw value already
if (typeof value === 'string') {
return value;
}
// Ignores everything else which is not a string (arrays, numbers, objects, etc.)
return undefined;
};
/** Utility function to get the meta title from the experience entry fields */
export const getMetaTitleFromExperienceEntryFields = (
experience: Experience<EntityStore>
): string | undefined =>
getFieldFromExperience(experience, 'metaTitle') ||
getFieldFromExperience(experience, 'title');
/** Utility function to get the meta description from the experience entry fields */
export const getMetaDescriptionFromExperienceEntryFields = (
experience: Experience<EntityStore>
): string | undefined => getFieldFromExperience(experience, 'metaDescription');
Key features of the extraction utilities
- Reference support: Automatically resolves references to other entries
- Fallback handling: Uses experience title as fallback for meta title
- Type safety: Proper type checking for different content types
- Flexible field access: Works with any custom field name
Implementing the page component with metadata
The page component integrates the metadata extraction with Next.js's generateMetadata function to provide proper SEO support.
// src/app/[locale]/[slug]/page.tsx
import type { Metadata } from 'next';
import Experience from '@/components/Experience';
import Header from '@/components/Header/Header';
import Footer from '@/components/Footer/Footer';
import { getExperience } from '@/getExperience';
import { detachExperienceStyles } from '@contentful/experiences-sdk-react';
import { Layout } from 'antd';
import {
Footer as LayoutFooter,
Header as LayoutHeader,
Content as LayoutContent,
} from 'antd/es/layout/layout';
import styles from './page.module.css';
import '../studio-config';
import {
getMetaDescriptionFromExperienceEntryFields,
getMetaTitleFromExperienceEntryFields,
} from '@/utils/meta';
type Page = {
params: Promise<{ locale?: string; slug?: string; preview?: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
// Taking the experience, extract the meta data from it and provide it to the Next page
export async function generateMetadata({
params,
searchParams,
}: Page): Promise<Metadata> {
const { locale = 'en-US', slug = 'home-page' } = (await params) || {};
const { isPreview, expEditorMode } = (await searchParams) || {};
const preview = isPreview === 'true';
const editorMode = expEditorMode === 'true';
const { experience } = await getExperience(slug, locale, preview, editorMode);
const metaTitle = experience
? getMetaTitleFromExperienceEntryFields(experience)
: undefined;
const metaDescription = experience
? getMetaDescriptionFromExperienceEntryFields(experience)
: undefined;
return {
title: metaTitle,
description: metaDescription,
openGraph: {
title: metaTitle,
description: metaDescription,
},
};
}
export default async function ExperiencePage({ params, searchParams }: Page) {
const { locale = 'en-US', slug = 'home-page' } = (await params) || {};
const { isPreview, expEditorMode } = (await searchParams) || {};
const preview = isPreview === 'true';
const editorMode = expEditorMode === 'true';
const { experience, error } = await getExperience(
slug,
locale,
preview,
editorMode
);
if (error) {
return <>{error.message}</>;
}
// extract the styles from the experience
const stylesheet = experience ? detachExperienceStyles(experience) : null;
// experience currently needs to be stringified manually to be passed to the component
const experienceJSON = experience ? JSON.stringify(experience) : null;
return (
<Layout className={styles.layout}>
{stylesheet && <style>{stylesheet}</style>}
<LayoutHeader className={styles.header}>
<Header />
</LayoutHeader>
<LayoutContent className={styles.content}>
<Experience experienceJSON={experienceJSON} locale={locale} />
</LayoutContent>
<LayoutFooter className={styles.footer}>
<Footer />
</LayoutFooter>
</Layout>
);
}
Key features of the page implementation
- Server-side metadata generation: Uses Next.js
generateMetadatafor optimal SEO - Preview support: Handles both published and preview content
- Fallback handling: Graceful degradation when metadata is missing
- Open Graph integration: Automatic Open Graph tag generation
Handling referenced content
When using references in your metadata fields, the system needs to know how to extract values from different content types. This is handled in the getFieldValueFromEntry function.
Content type mapping
/** Utility function to extract a specific field from the entry fields based on its content type */
const getFieldValueFromEntry = (input: Entry): string | undefined => {
// Since entry references don't point to a specific field, the whole entry is resolved
// and we have to extract the desired field based on the content type.
switch (input.sys.contentType.sys.id) {
case 'foo':
return (input as FooEntry).fields.description;
case 'bar':
return (input as BarEntry).fields.text;
case 'product':
return (input as ProductEntry).fields.name;
default:
return undefined;
}
};
Testing the implementation
Testing metadata generation
To test the metadata generation:
- Create an experience in Contentful Studio.
- Populate the custom metadata fields with both static values and references.
- Publish the experience.
- Visit your site and inspect the page source to verify metadata.
Testing with references
To test metadata with references:
- Create referenced content entries (e.g., SEO descriptions, authors).
- Link these entries to your experience metadata fields.
- Verify that the referenced content appears in the generated metadata.
- Test with different content types to ensure proper value extraction.
Preview mode testing
To test metadata in preview mode:
- Create draft content with custom metadata.
- Use preview mode to verify metadata is correctly displayed.
- Test both static values and references in preview mode.
Type safety considerations
ExperienceFields type currently only provides typing for slug and title, so additional fields are currently validated at run time.
Conclusion
This implementation provides a robust solution for enhancing Contentful Experiences with custom metadata fields. The flexible architecture supports both static values and references, making it suitable for various SEO and metadata needs.
Key benefits of this approach
- Flexibility: Supports both static values and references
- SEO optimization: Proper metadata generation for search engines
- Content reusability: Reference system allows content reuse
- Type safety: Proper handling of different content types
- Maintainability: Clean separation of concerns
For more advanced use cases, consider implementing additional features like:
- Dynamic metadata: Context-aware metadata generation
- A/B testing: Multiple metadata variants for testing
- Analytics integration: Metadata tracking and optimization
- Multi-language support: Localized metadata handling