Skill 09 · Contentful Personalization
Subchapter 9.8
references/framework-notes.mdMarkdown5 KBView on GitHub
Guidance for personalization readiness by framework.
Use the Optimization pattern for new integrations. Use the Ninetailed pattern only to repair or extend a detected legacy deployment.
Provider placement depends on the SDK family.
Existing legacy deployment — @ninetailed/experience.js:
Use a Client Component boundary, typically app/providers.tsx, and render it from
app/layout.tsx.
// app/providers.tsx
'use client';
export function Providers({ children }) {
return (
<NinetailedProvider clientId={process.env.NEXT_PUBLIC_NINETAILED_CLIENT_ID}>
{children}
</NinetailedProvider>
);
}
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }) {
return <html><body><Providers>{children}</Providers></body></html>;
}Recommended — @contentful/optimization-nextjs:
Create one bound integration with createNextjsAppRouterOptimization from the /app-router
subpath. Import its bound OptimizationRoot, OptimizedEntry, NextAppAutoPageTracker, and
request handler from an application-owned module. The bound root owns server evaluation and browser
takeover; browser-only hooks come from /client. Do not recreate the integration from generic
/client and /server exports.
Server Components cannot use browser hooks or context. With the Optimization App Router adapter, the
bound OptimizedEntry can fetch and resolve an entry during server rendering; use a /client
entry island only for browser-only live-update or loading controls. With the legacy SDK, fetch in a
Server Component and pass the payload into a Client Component that renders <Experience>.
app/ directory exists → App Router'use client' directives present → aware of client/server boundaryproviders.tsx or similar client wrapper patternProvider goes in pages/_app.tsx:
export default function App({ Component, pageProps }) {
return (
<NinetailedProvider
clientId={process.env.NEXT_PUBLIC_NINETAILED_CLIENT_ID}
plugins={[new NinetailedInsightsPlugin()]}
>
<Component {...pageProps} />
</NinetailedProvider>
);
}getStaticProps + revalidate (ISR) → ideal for personalizationgetServerSideProps → works, allows server-side personalizationuseEffect) → works but flash-of-defaultpages/_app.tsx exists → Pages RoutergetStaticProps with revalidate → ISR readygetServerSideProps → SSR ready (can add middleware later)pages/[[...slug]].tsx or similar catch-all → dynamic routing readyProvider goes in gatsby-browser.js (and gatsby-ssr.js for SSR):
// gatsby-browser.js
export const wrapRootElement = ({ element }) => (
<NinetailedProvider clientId={process.env.GATSBY_NINETAILED_CLIENT_ID}>{element}</NinetailedProvider>
);Gatsby uses GraphQL queries at build time. Content is available via
useStaticQuery or page queries. Personalization is client-side only —
the static HTML shows the baseline, and the SDK swaps variants after hydration.
Gatsby requires GATSBY_ prefix for client-side env vars:
GATSBY_NINETAILED_CLIENT_ID
GATSBY_NINETAILED_ENVIRONMENT
GATSBY_CONTENTFUL_SPACE_ID
GATSBY_CONTENTFUL_TOKENgatsby-config.js/ts exists → Gatsby projectgatsby-source-contentful plugin → Contentful integratedgatsby-browser.js exists → can add providerProvider goes in app/root.tsx:
export default function App() {
return (
<NinetailedProvider clientId={...}>
<Outlet />
</NinetailedProvider>
);
}Remix uses loader functions for server-side data fetching. Content
is available via useLoaderData(). This pattern is compatible with
personalization — loaders can fetch Contentful entries and pass them
to components.
remix.config.js or app/root.tsx → Remix projectloader functions in route files → data fetching pattern compatibleuseLoaderData() usage → data flows from server to componentsProvider goes at the app root:
// src/App.tsx or src/main.tsx
ReactDOM.createRoot(root).render(
<NinetailedProvider clientId={...}>
<App />
</NinetailedProvider>
);Typically useEffect + API calls. Personalization is client-side only.
Consider:
react-scripts or vite in deps → CRA or Vite