> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://contentful.com/developers/docs/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/_mcp/server. # Preview plugin > Preview Experiences before they are published. The Preview plugin allows you to assign your profile to specific audiences, experiences, and variants within preview and development contexts. Add plugin to the `plugins` prop of the `NinetailedProvider` and provide an array of all Ninetailed Experience and Ninetailed Audience entries to the plugin. Ensure that you are supplying both all published and all draft entries of each to the plugin. Additionally, you should ensure that you are not instantiating the plugin in production contexts. ## Installation Add the dependency: #### npm ```bash npm install @ninetailed/experience.js-plugin-preview ``` #### yarn ```bash yarn add @ninetailed/experience.js-plugin-preview ``` Then, add the plugin to the instance: #### React, Next.js ```jsx import { NinetailedPreviewPlugin } from '@ninetailed/experience.js-plugin-preview' ``` ```jsx {}, // Optional: Callback to handle user forwarding to the audience entry in your CMS onOpenAudienceEditor: (audience) => {}, // Optional: Determine the visibility of the Preview Plugin ui: { opener: { hide: false } }, }) ]} > // ... ``` #### Gatsby ```javascript import {audienceQuery, audienceMapper, experienceQuery, experienceMapper } from '../../../lib/yourFunctions' plugins: [ // ... { resolve: `@ninetailed/experience.js-gatsby`, options: { clientId: NINETAILED_CLIENT_ID, environment: NINETAILED_ENVIRONMENT, ninetailedPlugins: [ { resolve: `@ninetailed/experience.js-plugin-preview`, name: "@ninetailed/experience.js-plugin-preview", options: { // Options specific to Gatsby - optional customOptions: { // Query all audiences audienceQuery, // Mapper function for audiences audienceMapper, // Query all experiences experienceQuery, // Mapper function for experiences experienceMapper, }, // Callback to handle user forwarding to the experience entry in your CMS - optional onOpenExperienceEditor: (experience) => { }, // Determine the visibility of the Preview Plugin - optional ui: { opener: { hide: false } }, }, }, ], }, }, ]; ``` Define the functions for retrieving and mapping Gatsby data so they can be used in `gatsby-config.js`. These example queries are compatible with `gatsby-source-contentful`; adjust your queries based on the content source you are using. ```javascript import { ExperienceLike, ExperienceMapper, } from "@ninetailed/experience.js-utils"; export const audienceQuery = ` query NinetailedAudienceQuery { allContentfulNinetailedAudience { edges { node { nt_audience_id nt_name nt_description { nt_description } } } } } ` export const audienceMapper = audienceData => { return audienceData.allContentfulNinetailedAudience.edges.map(audience => { return { id: audience.node.nt_audience_id, name: audience.node.nt_name, description: audience.node.nt_description?.nt_description, } }) } export const experienceQuery = ` query NinetailedExperienceQuery { allContentfulNinetailedExperience { edges { node { id: contentful_id name: nt_name type: nt_type audience: nt_audience { id: contentful_id name: nt_name } config: nt_config { distribution traffic components { baseline { id } variants { hidden id } } } variants: nt_variants { ... on ContentfulEntry { id: contentful_id } } } } } } ` export const experienceMapper = (experienceData: any) => { return experienceData.allContentfulNinetailedExperience.edges .filter(({ node }: { node: any }) => ExperienceMapper.isExperienceEntry(node) ) .map(({ node }: { node: ExperienceLike }) => ExperienceMapper.mapExperience(node) ); }; ``` #### JavaScript ```javascript import { Ninetailed } from '@ninetailed/experience.js'; import { NinetailedPreviewPlugin } from '@ninetailed/experience.js-plugin-preview' export const ninetailed = new Ninetailed( { clientId: // Your client ID environment: // Your Ninetailed environment }, { plugins: [ new NinetailedPreviewPlugin({ // Required: Experiences from your CMS experiences: yourExperiences || [], // Required: Audiences from your CMS audiences: yourAudiences || [], // Optional: Callback to handle user forwarding to the experience entry in your CMS onOpenExperienceEditor: (experience) => {}, // Optional: Callback to handle user forwarding to the audience entry in your CMS onOpenAudienceEditor: (audience) => {}, // Optional: Determine the visibility of the Preview Plugin ui: { opener: { hide: false } } }) ] } ); ``` ## Conditional instantiation The Preview Plugin is designed to be used within preview and development contexts. > **Info** > > This plugin does not automatically turn off in production contexts. You are responsible for instantiating the plugin only when it is required. One way of doing this is through a simple instantiation toggle based on some runtime argument. The code sample below shows conditionally including the plugin only when `process.env.NODE_ENV !== 'production'`, but you are free to use whatever condition best identifies your non-production environments. For example, you might choose to instantiate the plugin based on whether the current visitor has turned on preview or draft mode and use this condition instead or in conjunction. ```typescript const preview = process.env.NODE_ENV !== 'production' ``` ```typescript ... // Children of provider ``` However, this simple toggle will bundle the preview plugin in production settings. A more robust way would be to dynamically import the preview module only when it is required. The following Next.js code sample returns a `` component with the Preview Plugin only when preview data is supplied, leveraging [next/dynamic](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic) to exclude the dependency from the bundle. ```typescript import type { NinetailedProviderProps } from '@ninetailed/experience.js-next'; import type { PropsWithChildren } from 'react'; import dynamic from 'next/dynamic'; import { NinetailedProvider } from '@ninetailed/experience.js-next'; /** * Dynamic import of the preview plugin ensures it it not part of a production bundle */ export function getNinetailedProvider(enablePreview: boolean) { const providerProps: NinetailedProviderProps = { clientId: process.env.NEXT_PUBLIC_NINETAILED_CLIENT_ID || '', environment: process.env.NEXT_PUBLIC_NINETAILED_ENVIRONMENT || 'main', plugins: [ // Any plugins that should always be loaded ], // Any other provider config }; if (!enablePreview) { return function StandardNinetailedProvider({ children, }: PropsWithChildren) { return ( {children} ); }; } return dynamic(() => import('@ninetailed/experience.js-plugin-preview').then((mod) => { return function NinetailedProviderWithPreviewPlugin({ children, }: PropsWithChildren) { providerProps.plugins = [ new mod.NinetailedPreviewPlugin({ audiences: ... // your audiences experiences: ... // your experiences ... // other config }), ...(providerProps.plugins || []), ]; return ( {children} ); }; }) ); } ``` ## Supply preview content The Preview Plugin expects a specific format for the provided `experiences` and `audiences`. Use the appropriate utility library for your content source to map these to the required format. #### Contentful Utility Library ```typescript import { ExperienceMapper, ExperienceEntryLike, AudienceMapper, AudienceEntryLike } from 'experience.js-utils-contentful'; import { yourContentfulPreviewClient } from 'lib/api' export const getAllExperiences = async () => { const entries = await yourContentfulPreviewClient.getEntries({ content_type: 'nt_experience', include: 1, }); return (entries.items as ExperienceEntryLike[]) .filter(ExperienceMapper.isExperienceEntry) .map(ExperienceMapper.mapExperience); }; export const getAllAudiences = async () => { const entries = await yourContentfulPreviewClient.getEntries({ content_type: 'nt_audiences', }); return (entries.items as AudienceEntryLike[]) .filter(AudienceMapper.isAudienceEntry) .map(AudienceMapper.mapAudience); }; ``` #### JavaScript Utility Library ```typescript import { ExperienceMapper, ExperienceEntryLike, Audience } from 'experience.js-utils'; import { yourContentPreviewClient } from 'lib/api' export const getAllExperiences = async () => { const experiencers = await yourContentPreviewClient.getAllExperienceEntries() return experiences .map((entry: any) => { return { id: experience.uid, name: experience.nt_name, type: experience.nt_type, config: experience.nt_config, audience: { id: experience.nt_audience[0].nt_audience_id, // For audience names to display in Preview widget // Not strictly required by ExperienceMapper.isExperienceEntry name: experience.nt_audience[0].title, }, variants: experience.nt_variants?.map((variant: any) => { return { id: variant.uid, }; }), }; }) } .filter((experience) => ExperienceMapper.isExperienceEntry) .map((experience) => ExperienceMapper.mapExperience); }; export const getAllAudiences = async () => { const audiences = await yourContentPreviewClient.getAllAudienceEntries() return audiences.map((audience) => { return { id: audience.nt_audience.id, // Required: Your audience id property, normally audience.nt_audience_id name: audience.nt_name, // Optional: Your audience name property, normally audience.nt_name description: audience.nt_description // Optional: normally audience.nt_description } as Audience }) }; ``` > Preview Experiences before they are published.