### Run Development Server Source: https://github.com/episerver/cms-saas-vercel-demo/blob/main/apps/frontend/README.md Execute this command to start the Next.js development server. Open http://localhost:3000 in your browser to view the application. ```bash npm run dev # or yarn dev # or pnpm dev ``` -------------------------------- ### Opti-fx CLI: Setup Feature Experimentation Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Scaffolds and maintains Optimizely Feature Experimentation integration for Next.js App Router projects. This command sets up flags, SDK context, hooks, and registers GeoIP attributes. ```bash # Initial setup: generates flags.ts, well-known routes, SDK context, React hooks, # registers GeoIP attributes in FX, and writes env vars to .env opti-fx setup \ --project 12345 \ --token your-personal-access-token \ --key your-sdk-key \ --path ./apps/frontend # Generated files: # src/flags.ts – Vercel Flags provider definitions # src/opti.ts – FX SDK user context factory # src/hooks/useFlag.ts – Client-side React hook # src/hooks/useFlag.server.ts – Server-side logic for the hook # src/app/.well-known/vercel/flags/route.ts – Vercel Flags API route # src/app/.well-known/optimizely/datafile/route.ts – Datafile webhook handler ``` -------------------------------- ### Next.js Build Scripts for Optimizely CMS Demo Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Utilize these yarn scripts for various build and development tasks. Scripts like `build:full` include GraphQL type generation and webhook registration, while `dev` starts the development server. ```bash # Development server (HTTP) yarn dev # Development server (HTTPS – required for some Optimizely preview features) yarn dev:https # Full production build: # 1. Generates GraphQL types (graphql-codegen) # 2. Pulls Feature Experimentation flag definitions (opti-fx pull) # 3. Builds Next.js # 4. Registers the Graph publish webhook yarn build:full # Quick build (skip codegen & flag pull – use when schema is unchanged) yarn build # Regenerate GraphQL types from the CMS schema yarn compile # one-shot yarn watch # watch mode # Pull Feature Experimentation flag definitions into src/flags.ts yarn flags:pull # TypeScript type-check without emitting yarn type-check ``` -------------------------------- ### Vercel Flags API Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Exposes the current Feature Experimentation flag definitions and their real-time variants to the Vercel Flags toolbar. Generated by `opti-fx setup`. ```APIDOC ## GET /.well-known/vercel/flags ### Description Exposes the current Feature Experimentation flag definitions and their real-time variants to the Vercel Flags toolbar. ### Method GET ### Endpoint `/.well-known/vercel/flags` ### Request Headers - **Authorization** (string) - Required - Bearer ### Response #### Success Response (200) - **definitions** (object) - Flag definitions and their options. - **hints** (array) - Hints for the flags. ### Response Example ```json { "definitions": { "my_feature_flag": { "description": "Enable new checkout flow", "defaultValue": false, "options": [ { "label": "Enabled", "value": true }, { "label": "Disabled", "value": false } ] } }, "hints": [] } ``` ### Request Example ```bash curl -H "Authorization: Bearer $FLAGS_SECRET" \ https://your-site.vercel.app/.well-known/vercel/flags ``` ``` -------------------------------- ### Datafile Webhook Handler Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Receives webhook notifications from Optimizely Feature Experimentation when the feature flag datafile changes, then updates the cached datafile used by the SDK. Generated by `opti-fx setup`. ```APIDOC ## POST /.well-known/optimizely/datafile ### Description Receives webhook notifications from Optimizely Feature Experimentation when the feature flag datafile changes, then updates the cached datafile used by the SDK. ### Method POST ### Endpoint `/.well-known/optimizely/datafile` ### Query Parameters - **token** (string) - Required - The webhook token for authentication. ### Response #### Success Response (200) - **message** (string) - Indicates success, e.g., "ok". - **data** (object) - Data related to the update. #### Error Response (401) - **message** (string) - Indicates unauthorized access, e.g., "Unauthorized". - **data** (null) ### Response Example (Success) ```json { "message": "ok", "data": { ... } } ``` ### Response Example (Error) ```json { "message": "Unauthorized", "data": null } ``` ### Request Example ```bash curl -X POST \ "https://your-site.vercel.app/.well-known/optimizely/datafile?token=your-webhook-token" ``` ``` -------------------------------- ### GET /api/content/articles – Blog Article Listing Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Retrieves a paginated list of blog articles, with options to filter by author and publication date. This endpoint runs on the Edge runtime. ```APIDOC ## GET /api/content/articles ### Description Returns a paginated list of blog post articles with author/date facets for filtering. This endpoint runs on the Edge runtime. ### Method GET ### Endpoint /api/content/articles ### Parameters #### Query Parameters - **locale** (string) - Required - The locale for the articles (e.g., "en"). - **page** (number) - Optional - The page number for pagination (default: 1). - **pageSize** (number) - Optional - The number of articles per page (default: 12). - **author** (string) - Optional - Filter articles by author name. - **published** (string) - Optional - Filter articles by publication date in ISO format. ### Request Example GET /api/content/articles?locale=en&page=1&pageSize=6&author=Jane+Smith ### Response #### Success Response (200) - **totalResults** (number) - The total number of articles found. - **totalPages** (number) - The total number of pages available. - **items** (array) - An array of article objects, each containing `id`, `name`, `title`, `description`, `author`, `published`, `image`, `path`, and `url`. - **facets** (object) - Facets for filtering, including an `author` array with `name` and `count` for each author. ### Response Example { "totalResults": 8, "totalPages": 2, "items": [ { "id": "abc123", "name": "Mortgage Tips", "title": "5 Mortgage Tips for First-Time Buyers", "description": "A short summary...", "author": "Jane Smith", "published": "2024-03-15T10:00:00Z", "image": { "src": "https://example.cms.optimizely.com/globalassets/blog-hero.jpg" }, "path": "/en/blog/mortgage-tips", "url": "https://your-site.vercel.app/en/blog/mortgage-tips" } ], "facets": { "author": [ { "name": "Jane Smith", "count": 8 }, { "name": "John Doe", "count": 3 } ] } } ``` -------------------------------- ### GET /api/me/consent – Visitor Tracking Consent Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Manages visitor consent for analytics and tracking by reading or setting an opt-in/opt-out cookie. This endpoint runs on the Edge runtime. ```APIDOC ## GET /api/me/consent ### Description Reads or sets the visitor's opt-in/opt-out cookie for analytics and tracking. Runs on the Edge runtime. ### Method GET ### Endpoint /api/me/consent ### Parameters #### Query Parameters - **set** (string) - Optional - Set to "true" to opt-in or "false" to opt-out. ### Request Example - Read current consent status: `GET /api/me/consent` - Opt out: `GET /api/me/consent?set=false` - Opt in: `GET /api/me/consent?set=true` ### Response #### Success Response (200) - **allowTracking** (boolean) - Indicates whether tracking is allowed for the visitor. ### Response Example - Reading consent: `{ "allowTracking": true }` - Setting consent: `{ "allowTracking": false }` ### Cookie Handling - This endpoint also sets the `opt_in` cookie with the corresponding boolean value. ``` -------------------------------- ### GET /api/me/search – Visitor's Recent Search Terms Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Retrieves a list of the visitor's recent search terms from Optimizely Data Platform (ODP). Requires ODP to be configured and returns an empty list if ODP is not enabled or the visitor lacks a `vuid` cookie. ```APIDOC ## GET /api/me/search ### Description Returns the visitor's recent search terms from Optimizely Data Platform (ODP). Requires ODP to be configured. Returns an empty list if ODP is not enabled or the visitor has no `vuid` cookie. ### Method GET ### Endpoint /api/me/search ### Parameters #### Query Parameters - **count** (number) - Optional - Limits the number of returned search terms. ### Request Example GET /api/me/search?count=5 ### Response #### Success Response (200) - **terms** (array of strings) - A list of the visitor's recent search terms. ### Response Example { "terms": ["mortgage", "savings account", "credit card"] } ``` -------------------------------- ### `GET /api/content/publish` – On-Demand Revalidation Webhook Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt This endpoint is called by Optimizely CMS after content is published to revalidate affected Next.js pages. It can also revalidate a fixed set of additional paths and is automatically registered during the build process. ```APIDOC ## `GET /api/content/publish` – On-Demand Revalidation Webhook Called by Optimizely CMS after content is published. Revalidates the affected Next.js pages (and optionally a fixed set of additional paths). Registered automatically during `yarn build`. ```typescript // apps/frontend/src/app/api/content/publish/route.ts import createPublishApi from "@remkoj/optimizely-cms-nextjs/publish"; import { createClient } from "@remkoj/optimizely-graph-client"; const handler = createPublishApi({ // Always revalidate these paths in addition to the published content path additionalPaths: [ "/api/content/search", "/api/content/articles", "/api/me/[[...path]]", "/sitemap.xml", "/robots.txt", ], // Only revalidate the specific published page (not the whole site) optimizePublish: true, // Fresh Graph client for path resolution (no cache) client: () => createClient(undefined, undefined, { nextJsFetchDirectives: true, cache: false, queryCache: false, }), }); export const dynamic = "force-dynamic"; export const revalidate = 0; export const runtime = "nodejs"; export const GET = handler; export const POST = handler; // Example: Optimizely CMS calls this automatically when content is published. // Manual trigger: // curl -X POST "https://your-site.vercel.app/api/content/publish?token=optly-your-token" ``` ``` -------------------------------- ### Fetch Vercel Feature Flags Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt The `GET /.well-known/vercel/flags` endpoint exposes feature flag definitions and their variants. It's called by the Vercel Flags toolbar and requires a Bearer token for authorization. ```typescript // apps/frontend/src/app/.well-known/vercel/flags/route.ts // Authorization: Bearer // GET /.well-known/vercel/flags (called by Vercel Flags toolbar) // Returns all flag definitions with their current options from FX. // Example response: // { // "definitions": { // "my_feature_flag": { // "description": "Enable new checkout flow", // "defaultValue": false, // "options": [ // { "label": "Enabled", "value": true }, // { "label": "Disabled", "value": false } // ] // } // }, // "hints": [] // } // curl example: curl -H "Authorization: Bearer $FLAGS_SECRET" \ https://your-site.vercel.app/.well-known/vercel/flags ``` -------------------------------- ### `GET /api/content/search` – Full-Text Content Search Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt This API endpoint performs personalized or standard full-text search against Optimizely Graph. It leverages the visitor's top interest topics from Content Intelligence and runs on the Edge runtime. ```APIDOC ## `GET /api/content/search` – Full-Text Content Search Performs personalized or standard full-text search against Optimizely Graph using the visitor's top interest topics from Content Intelligence. Runs on the Edge runtime. ```typescript // apps/frontend/src/app/api/content/search/route.ts // Query parameters: // query (required) – search term // limit (optional, default 12) – results per page // start (optional, default 0) – offset for pagination // facets (optional) – JSON-encoded filter object: { ctype?: string, locale?: string } // Example request: // GET /api/content/search?query=mortgage&limit=6&start=0 // Example response: // { // "term": "mortgage", // "total": 14, // "paging": { "start": 0, "limit": 6, "pages": 3 }, // "isPersonalized": true, // "items": [ // { // "id": { "key": "abc123", "version": "1", "locale": "en" }, // "url": { "base": "https://example.cms.optimizely.com", "default": "/en/blog/mortgage-tips" }, // "title": "5 Mortgage Tips for First-Time Buyers", // "abstract": "...", // "published": "2024-03-15T10:00:00Z", // "author": "Jane Smith", // "type": "BlogPostPage", // "types": ["BlogPostPage", "Page", "Content"] // } // ], // "facets": [ // { "key": "ctype", "options": [{ "key": "BlogPostPage", "count": 12 }] }, // { "key": "locale", "options": [{ "key": "en", "count": 14 }] } // ] // } ``` ``` -------------------------------- ### Full-Text Content Search API Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt This API endpoint performs personalized or standard full-text search against Optimizely Graph, leveraging visitor interests from Content Intelligence. It runs on the Edge runtime and supports query, limit, start, and facet parameters. ```typescript // apps/frontend/src/app/api/content/search/route.ts // Query parameters: // query (required) – search term // limit (optional, default 12) – results per page // start (optional, default 0) – offset for pagination // facets (optional) – JSON-encoded filter object: { ctype?: string, locale?: string } // Example request: // GET /api/content/search?query=mortgage&limit=6&start=0 // Example response: // { // "term": "mortgage", // "total": 14, // "paging": { "start": 0, "limit": 6, "pages": 3 }, // "isPersonalized": true, // "items": [ // { // "id": { "key": "abc123", "version": "1", "locale": "en" }, // "url": { "base": "https://example.cms.optimizely.com", "default": "/en/blog/mortgage-tips" }, // "title": "5 Mortgage Tips for First-Time Buyers", // "abstract": "...", // "published": "2024-03-15T10:00:00Z", // "author": "Jane Smith", // "type": "BlogPostPage", // "types": ["BlogPostPage", "Page", "Content"] // } // ], // "facets": [ // { "key": "ctype", "options": [{ "key": "BlogPostPage", "count": 12 }] }, // { "key": "locale", "options": [{ "key": "en", "count": 14 }] } // ] // } ``` -------------------------------- ### Sync Project Data Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Use the `opti-fx sync` command to pull project data. Ensure you provide your project ID, a personal access token, and your SDK key. ```bash opti-fx sync --project 12345 --token your-pat --key your-sdk-key ``` -------------------------------- ### Environment Configuration for Optimizely CMS Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Set up required and optional environment variables for Optimizely CMS, Graph, and other integrations. Ensure these are configured in `.env.development.local` for local development. ```bash # .env.development.local (pull with: yarn env:pull) # Required – Optimizely CMS instance OPTIMIZELY_CMS_URL=https://example.cms.optimizely.com/ # Required – Optimizely Graph (Content Graph) keys # Found on CMS Dashboard → "Render Content" OPTIMIZELY_GRAPH_APP_KEY=your-app-key OPTIMIZELY_GRAPH_SECRET=your-secret OPTIMIZELY_GRAPH_SINGLE_KEY=your-single-key # Optional – REST API access (content publish webhook registration) OPTIMIZELY_CMS_CLIENT_ID=your-client-id OPTIMIZELY_CMS_CLIENT_SECRET=your-client-secret # Optional – site domain (defaults to VERCEL_BRANCH_URL in CI) SITE_DOMAIN=localhost:3000 NEXT_PUBLIC_SITE_DOMAIN=localhost:3000 # Optional – publish webhook security token OPTIMIZELY_PUBLISH_TOKEN=optly-your-token # Optional – Google Analytics GA_TRACKING_ID=G-XXXXXXXXXX # Optional – debug output OPTIMIZELY_DEBUG=0 OPTIMIZELY_GRAPH_QUERY_LOG=0 # Optional – Optimizely Feature Experimentation OPTIMIZELY_FX_SDKKEY=your-sdk-key OPTIMIZELY_FX_PROJECT=your-project-id FLAGS_SECRET=your-vercel-flags-secret # base64url, 32 bytes ``` -------------------------------- ### Blog Section Visual Builder Experience Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Combines a CMS-composed structure with a server-rendered blog post listing. Fetches SEO settings via Optimizely Graph and loads initial blog post data server-side. ```typescript // apps/frontend/src/components/cms/experience/BlogSectionExperience/index.tsx // Resolved by the [[...path]] route for BlogSectionExperience content types. // The experience renders: // 1. OptimizelyComposition – the Visual Builder node tree (header, hero, etc.) // 2. BlogPostsSection – a paginated list of blog posts, loading server-side // initial data and handling client-side pagination with React Suspense // getMetaData fetches SEO settings from Optimizely Graph: BlogSectionExperienceExperience.getMetaData = async (contentLink, locale, client) => { // Returns: { title, description, keywords, openGraph, alternates.canonical } }; // The blog listing loads initial data server-side for fast first paint: import { getBlogPosts } from "./actions/getBlogPosts"; const initialData = await getBlogPosts({ locale: "en", parentKey: "experience-guid" }); // { items: [...], total: 42, paging: { page: 1, pageSize: 9 } } ``` -------------------------------- ### Handle Optimizely Datafile Webhook Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt The `POST /.well-known/optimizely/datafile` endpoint receives webhook notifications from Optimizely Feature Experimentation. It updates the SDK's cached datafile upon changes. A webhook token is required for authentication. ```typescript // apps/frontend/src/app/.well-known/optimizely/datafile/route.ts // POST /.well-known/optimizely/datafile?token= // Called automatically by Optimizely FX when a flag is published. // Success response: // { "message": "ok", "data": { ... } } // Error response (invalid token): // { "message": "Unauthorized", "data": null } (HTTP 401) // curl example (manual trigger for testing): curl -X POST \ "https://your-site.vercel.app/.well-known/optimizely/datafile?token=your-webhook-token" ``` -------------------------------- ### LandingPage getMetaData Implementation Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Implements getMetaData to fetch and map CMS-managed SEO settings (title, description, og:image) to Next.js Metadata. Supports click-to-edit in Visual Builder preview. ```typescript // apps/frontend/src/components/cms/page/LandingPage/index.tsx // Registered via the component factory and resolved automatically by the // [[...path]] catch-all route. Supports click-to-edit in Visual Builder preview. // The component itself is simple – all content is driven by the CMS: import { LandingPage } from "@/components/cms/page/LandingPage"; // When rendered by CmsPage, data comes from the GraphQL query: // TopContentArea: [HeroBlock, CarouselBlock, ...] (any CMS blocks) // MainContentArea: [TextBlock, QuoteBlock, CTAElement, ...] (any CMS blocks) // getMetaData fetches SEO fields from Optimizely Graph and maps them to // Next.js Metadata, including Open Graph type, og:image, and description: LandingPage.getMetaData = async (contentLink, locale, client) => { // Returns: { title, description, openGraph: { title, description, images, type } } }; // Example metadata output for a landing page: // { // title: "Home | Mosey Bank", // description: "Innovative banking products for everyone.", // openGraph: { // title: "Home | Mosey Bank", // images: [{ url: "https://example.cms.optimizely.com/.../og-image.jpg" }], // type: "website" // } // } ``` -------------------------------- ### `[[...path]]` – CMS Page Route Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt This catch-all route dynamically resolves any URL path against Optimizely CMS. It generates static parameters at build time and revalidates content on demand via a publish webhook. ```APIDOC ## `[[...path]]` – CMS Page Route The catch-all route resolves every URL path against Optimizely CMS, generates static params at build time, and revalidates on demand via the publish webhook. ```typescript // apps/frontend/src/app/[[...path]]/page.tsx import "server-only"; import { createClient, AuthMode } from "@remkoj/optimizely-graph-client"; import { createPage } from "@remkoj/optimizely-cms-nextjs/page"; import { getContentByPath } from "@gql/functions"; import { factory } from "@components/factory"; import { draftMode } from "next/headers"; const { generateMetadata, // Next.js metadata for each page (title, og:image, etc.) generateStaticParams, // Enumerates all CMS paths at build time CmsPage: Page, // The actual page Server Component } = createPage(factory, { getContentByPath, // Master GraphQL query: loads all page data in one request // Switch to HMAC authentication and enable draft/preview content // when Next.js Draft Mode is active (triggered by the preview page) client: (_, scope) => { const client = createClient(undefined, undefined, { nextJsFetchDirectives: true, cache: true, queryCache: true, }); if (scope === "request" && draftMode().isEnabled) { client.updateAuthentication(AuthMode.HMAC); client.enablePreview(); } return client; }, }); // Throw if the route becomes unexpectedly dynamic (protects performance) export const dynamic = "error"; // Allow CMS pages added after the build to resolve without a rebuild export const dynamicParams = true; // Pages are cached indefinitely and revalidated only via the publish webhook export const revalidate = false; export { generateMetadata, generateStaticParams }; export default Page; ``` ``` -------------------------------- ### BlogPostPage getMetaData Implementation Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Maps CMS SEO fields to Next.js Metadata with full OpenGraph support, including published time, images, authors, and topics for Content Intelligence and personalization. ```typescript // apps/frontend/src/components/cms/page/BlogPostPage/index.tsx // Resolved automatically by the [[...path]] route for BlogPostPage content types. // The "Continue Reading" section has three fallback tiers: // 1. Editor-curated: items in the blog post's own `continueReading` content area // 2. Shared instance: a single ContinueReadingComponent block configured site-wide // 3. Auto-generated: ArticleListElement filtered by the post's topics, excluding self // getMetaData maps CMS SEO fields to Next.js Metadata with full OpenGraph support: BlogPostPage.getMetaData = async (contentLink, locale, client) => { // Returns full metadata including: // - title / description / keywords // - openGraph.type (from CMS GraphType field) // - openGraph.publishedTime // - openGraph.images (SharingImage from SEO settings, falling back to blogImage) // - authors array // - other["article:tag"] (topics list for Content Intelligence) // - other["idio:topic"] (topics list for personalization) }; // Example: fetching blog post metadata manually import { getSdk } from "@/gql/client"; import { createClient } from "@remkoj/optimizely-graph-client"; const client = createClient(); const sdk = getSdk(client); const result = await sdk.getBlogPostPageMetaData({ key: "blog-post-guid-456", version: undefined, locale: "en", }); ``` -------------------------------- ### ArticleListElement Programmatic Usage Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Demonstrates programmatic usage of ArticleListElement to display a randomized grid of blog post cards, optionally filtered by topic. Used for 'Continue Reading' fallback. ```typescript // apps/frontend/src/components/cms/component/ArticleListElement/index.tsx // As a CMS element (placed in Visual Builder), data comes from GraphQL: // { articleListCount: 3, topics: ["mortgage", "savings"] } // Programmatic usage (e.g., as auto-generated "Continue Reading"): import ArticleListElementElement from "@/components/cms/component/ArticleListElement"; // Renders a responsive 3-column grid of article cards with image, author, date, and title. ``` -------------------------------- ### API Endpoint for Blog Article Listing Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt The `/api/content/articles` endpoint provides paginated blog posts with filtering by author and publication date. It runs on the Edge runtime. The underlying `getArticles` function can also be imported directly for use in Server Components. ```typescript // apps/frontend/src/app/api/content/articles/route.ts // Query parameters: // locale (required) – e.g. "en" // page (optional, default 1) // pageSize (optional, default 12) // author (optional) – filter by author name // published (optional) – filter by publication date (ISO date string) // Example request: // GET /api/content/articles?locale=en&page=1&pageSize=6&author=Jane+Smith // Underlying function (also importable directly in Server Components): import { getArticles } from "@/lib/api/articles"; const result = await getArticles( "en", // locale { count: 6, page: 1 }, // paging { author: "Jane Smith" } // filters ); console.log(result.totalResults); // 8 console.log(result.totalPages); // 2 console.log(result.items[0]); // { // id: "abc123", // name: "Mortgage Tips", // title: "5 Mortgage Tips for First-Time Buyers", // description: "A short summary...", // author: "Jane Smith", // published: "2024-03-15T10:00:00Z", // image: { src: "https://example.cms.optimizely.com/globalassets/blog-hero.jpg" }, // path: "/en/blog/mortgage-tips", // url: "https://your-site.vercel.app/en/blog/mortgage-tips" // } console.log(result.facets.author); // [{ name: "Jane Smith", count: 8 }, { name: "John Doe", count: 3 }] ``` -------------------------------- ### useQuickSearch Hook for Debounced Client-Side Search Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Implement real-time search-as-you-type functionality. Configure debounce delay and minimum search characters. Uses SWR for caching. ```typescript // apps/frontend/src/lib/use-quick-search.ts import { useQuickSearch } from "@/lib/use-quick-search"; function SearchBox() { const [inputValue, setInputValue] = useState(""); // Debounced by 250ms, minimum 3 characters before searching const { data, isLoading, error } = useQuickSearch(inputValue, 250, 3); return ( <> setInputValue(e.target.value)} placeholder="Search..." /> {isLoading &&

Searching…

} {error &&

Search failed

} {data?.items.map((item) => ( {item.title} ))} ); } ``` -------------------------------- ### Generate Next.js Sitemap from Optimizely Graph Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Automatically generates a Next.js sitemap by fetching all routable content from Optimizely Graph. The sitemap is revalidated every 6 hours. ```typescript // apps/frontend/src/app/sitemap.ts import { RouteResolver } from "@remkoj/optimizely-graph-client"; export default async function sitemap() { const resolver = new RouteResolver(); const routes = await resolver.getRoutes(); // Returns entries like: // [ // { // url: "https://your-site.vercel.app/en/home", // lastModified: "2024-03-15T10:00:00.000Z", // changeFrequency: "daily", // priority: 1 // }, // ... // ] return routes.map((r) => ({ url: r.url.href, lastModified: r.changed ?? new Date(), changeFrequency: "daily", priority: 1, })); } export const revalidate = 21600; // 6 hours ``` -------------------------------- ### Fetch Top Topics for Personalization Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Retrieves the visitor's top interest topics from Optimizely Content Intelligence for personalized search. Ensure Optimizely One environment variables are configured. ```typescript // apps/frontend/src/components/cms/component/ContentRecsElement/index.tsx // CMS-managed data fields: // { // ElementRecommendationCount: 3, // number of recommendations to show // ElementDeliveryApiKey: "abc123" // API key from Content Recommendations // } // The component resolves the visitor's identity and fetches recommendations client-side. // No server-side configuration needed beyond the API key in the CMS. // To enable Content Recommendations site-wide, configure the Optimizely One environment // variables (see @remkoj/optimizely-one-nextjs documentation): // OPTIMIZELY_CONTENT_RECS_DELIVERY_KEY=... // OPTIMIZELY_CONTENT_RECS_API_KEY=... // Server-side: retrieve the visitor's top interest topics (used to personalize search): import { getTopTopics } from "@/lib/integrations/optimizely-content-intelligence"; const topics = await getTopTopics(); // e.g. ["mortgage", "savings", "credit"] – top 3 topics from Content Intelligence ``` -------------------------------- ### useLastTerms Hook for Recent Search Terms Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Fetches and displays recent search terms from Optimizely Data Platform. Configurable number of terms and refresh interval. ```typescript // apps/frontend/src/lib/use-last-terms.ts import { useLastTerms } from "@/lib/use-last-terms"; function RecentSearches() { // Fetch 5 terms, refresh every 10 seconds const { data: terms, isLoading } = useLastTerms(5, 10000); if (isLoading || !terms?.length) return null; return (
    {terms.map((term) => (
  • {term}
  • ))}
); } ``` -------------------------------- ### CMS Page Route with Dynamic Routing Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt This Next.js Server Component handles dynamic URL paths, resolving them against Optimizely CMS. It generates static parameters at build time and revalidates on demand via webhooks. Ensure `dynamic` is set to 'error' to prevent unexpected dynamic rendering. ```typescript // apps/frontend/src/app/[[...path]]/page.tsx import "server-only"; import { createClient, AuthMode } from "@remkoj/optimizely-graph-client"; import { createPage } from "@remkoj/optimizely-cms-nextjs/page"; import { getContentByPath } from "@gql/functions"; import { factory } from "@components/factory"; import { draftMode } from "next/headers"; const { generateMetadata, // Next.js metadata for each page (title, og:image, etc.) generateStaticParams, // Enumerates all CMS paths at build time CmsPage: Page, // The actual page Server Component } = createPage(factory, { getContentByPath, // Master GraphQL query: loads all page data in one request // Switch to HMAC authentication and enable draft/preview content // when Next.js Draft Mode is active (triggered by the preview page) client: (_, scope) => { const client = createClient(undefined, undefined, { nextJsFetchDirectives: true, cache: true, queryCache: true, }); if (scope === "request" && draftMode().isEnabled) { client.updateAuthentication(AuthMode.HMAC); client.enablePreview(); } return client; }, }); // Throw if the route becomes unexpectedly dynamic (protects performance) export const dynamic = "error"; // Allow CMS pages added after the build to resolve without a rebuild export const dynamicParams = true; // Pages are cached indefinitely and revalidated only via the publish webhook export const revalidate = false; export { generateMetadata, generateStaticParams }; export default Page; ``` -------------------------------- ### API Endpoint for Visitor's Recent Search Terms Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt The `/api/me/search` endpoint retrieves a visitor's recent search terms from Optimizely Data Platform (ODP). It requires ODP to be configured and returns an empty list if ODP is not enabled or the visitor lacks a `vuid` cookie. The `getLastSearchTerms` function can be used directly in server-side code. ```typescript // apps/frontend/src/app/api/me/search/route.ts // Query parameters: // count (optional) – limit number of returned terms // GET /api/me/search?count=5 // Response: { "terms": ["mortgage", "savings account", "credit card"] } // Direct server-side usage: import { getLastSearchTerms } from "@/lib/integrations/optimizely-data-platform"; const terms = await getLastSearchTerms(); // ["mortgage", "savings account", "credit card"] ``` -------------------------------- ### HeroBlockComponent for CMS-Managed Hero Sections Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt A configurable CMS component for hero sections with theme, image, text, and CTA. Supports inline editing in Optimizely Visual Builder. ```typescript // apps/frontend/src/components/cms/component/HeroBlock/index.tsx // Registered via the component factory. Rendered automatically by CmsPage // when the CMS page includes a HeroBlock content item. // Data shape (from GraphQL fragment HeroBlockData): // { // heroImage: ReferenceData | null, // eyebrow: string | null, // heroHeading: string | null, // heroDescription: { html: string, json: string } | null, // heroColor: "dark-blue" | "blue" | "orange" | "green" | "red" | "purple", // heroButton: ButtonBlockPropertyData | null // } // Example: rendering HeroBlock directly (outside of CmsPage) in a Server Component import HeroBlockComponent from "@/components/cms/component/HeroBlock"; import { type HeroBlockDataFragment } from "@/gql/graphql"; const heroData: HeroBlockDataFragment = { eyebrow: "Welcome to Mosey Bank", heroHeading: "Banking Made Simple", heroDescription: { html: "

Open an account today.

", json: "..." }, heroColor: "dark-blue", heroButton: { children: "Get Started", buttonType: "primary", buttonVariant: "cta", url: { default: "/open-account" } }, heroImage: null, }; // Renders a full-width dark-blue hero section with heading, description, and CTA button. ``` -------------------------------- ### Server-Side Content Search Function Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Use `contentSearch` for server-side content retrieval. It supports personalization and can be filtered by content type and locale. Ensure the Content Intelligence integration is active for personalization. ```typescript import contentSearch from "@/lib/api/search"; import { Locales } from "@/gql/graphql"; // Server Component or API Route usage: const results = await contentSearch("savings account", { limit: 12, // results per page (default: 12) start: 0, // pagination offset (default: 0) locale: Locales.en, personalize: true, // boost results by visitor's top interest topics filters: { ctype: "BlogPostPage", // filter to a specific content type locale: "en", // filter to a specific locale }, }); console.log(results.total); // 42 console.log(results.isPersonalized); // true console.log(results.items[0].title); // "Understanding High-Yield Savings Accounts" console.log(results.paging); // { start: 0, limit: 12, pages: 4 } ``` -------------------------------- ### CmsImage Component Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt A thin wrapper around Next.js `` that accepts Optimizely Graph `LinkData` or `ReferenceData` fragments directly, resolving the absolute image URL from the CMS base URL and path. ```APIDOC ## CmsImage Component ### Description A component that wraps Next.js `` and accepts Optimizely Graph `LinkData` or `ReferenceData` fragments to resolve image URLs. ### Props - **src** (ReferenceData | LinkData) - Required - The image data fragment from the CMS. - **fallbackSrc** (string) - Optional - A fallback image source URL if `src` is null. - **alt** (string) - Required - The alternative text for the image. - **width** (number) - Required - The width of the image. - **height** (number) - Required - The height of the image. - **className** (string) - Optional - CSS classes to apply to the image. - **priority** (boolean) - Optional - If true, the image will be considered high priority for preloading. ### Usage Example ```typescript import { CmsImage } from "@/components/shared/cms_image"; import { type ReferenceData } from "@/lib/urls"; const imageRef: ReferenceData = { url: { base: "https://example.cms.optimizely.com", default: "/globalassets/hero.jpg", }, }; // Renders: ``` ``` -------------------------------- ### URL Utility Functions for Optimizely Graph Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt The `getLinkData` and `linkDataToUrl` functions help resolve Optimizely Graph URL fragments (`LinkData`, `ReferenceData`) into standard JavaScript `URL` objects. `getLinkData` normalizes various fragment types, while `linkDataToUrl` converts the normalized data into a URL. ```typescript // apps/frontend/src/lib/urls.ts import { getLinkData, linkDataToUrl } from "@/lib/urls"; // getLinkData: normalizes LinkData, ReferenceData, or LinkItemData → LinkData const linkData = getLinkData(someReferenceOrLinkFragment); // { base: "https://example.cms.optimizely.com", default: "/en/home" } // linkDataToUrl: converts LinkData → URL object const url = linkDataToUrl(linkData); // URL { href: "https://example.cms.optimizely.com/en/home", pathname: "/en/home", ... } // Combined usage (resolving an image src from a content item): import { getFragmentData } from "@/gql/fragment-masking"; import { ReferenceDataFragmentDoc, LinkDataFragmentDoc } from "@/gql/graphql"; const ref = getFragmentData(ReferenceDataFragmentDoc, item.heroImage); const link = getLinkData(ref); const imageUrl = linkDataToUrl(link)?.href ?? "/fallback.jpg"; // "https://example.cms.optimizely.com/globalassets/hero-image.jpg" ``` -------------------------------- ### getLinkData / linkDataToUrl Utility Functions Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt Helper functions for resolving Optimizely Graph URL fragments into standard JavaScript `URL` objects, handling both `LinkData` and `ReferenceData` fragment shapes. ```APIDOC ## getLinkData / linkDataToUrl Utility Functions ### Description These utility functions help in resolving Optimizely Graph URL fragments (`LinkData`, `ReferenceData`) into standard JavaScript `URL` objects. ### `getLinkData` Function #### Description Normalizes `LinkData`, `ReferenceData`, or `LinkItemData` into the `LinkData` shape. #### Parameters - **fragment** (LinkData | ReferenceData | LinkItemData) - The content fragment containing URL information. #### Returns - **LinkData** - An object with `base` and `default` URL properties. ### `linkDataToUrl` Function #### Description Converts a `LinkData` object into a standard JavaScript `URL` object. #### Parameters - **linkData** (LinkData) - The `LinkData` object to convert. #### Returns - **URL** - A JavaScript `URL` object representing the resolved URL, or `null` if conversion fails. ### Usage Example ```typescript import { getLinkData, linkDataToUrl } from "@/lib/urls"; // Assuming 'someReferenceOrLinkFragment' is a valid content fragment const linkData = getLinkData(someReferenceOrLinkFragment); // linkData will be like: { base: "https://example.cms.optimizely.com", default: "/en/home" } const url = linkDataToUrl(linkData); // url will be a URL object like: URL { href: "https://example.cms.optimizely.com/en/home", pathname: "/en/home", ... } // Combined usage for resolving an image src: import { getFragmentData } from "@/gql/fragment-masking"; import { ReferenceDataFragmentDoc, LinkDataFragmentDoc } from "@/gql/graphql"; const ref = getFragmentData(ReferenceDataFragmentDoc, item.heroImage); const link = getLinkData(ref); const imageUrl = linkDataToUrl(link)?.href ?? "/fallback.jpg"; // imageUrl will be: "https://example.cms.optimizely.com/globalassets/hero-image.jpg" ``` ``` -------------------------------- ### contentSearch – Server-Side Search Function Source: https://context7.com/episerver/cms-saas-vercel-demo/llms.txt The core server-side search implementation. It automatically applies personalization if the Content Intelligence integration is active. This function can be used directly in Server Components or via the API route. ```APIDOC ## `contentSearch` ### Description Performs a server-side search with optional personalization. It's used by both the API route and React Server Components. ### Parameters - **query** (string) - Required - The search term. - **options** (object) - Optional - Configuration for the search. - **limit** (number) - Optional - Number of results per page (default: 12). - **start** (number) - Optional - Pagination offset (default: 0). - **locale** (Locales) - Required - The locale for the search. - **personalize** (boolean) - Optional - Boost results by visitor's top interest topics (default: false). - **filters** (object) - Optional - Filters to apply to the search. - **ctype** (string) - Optional - Filter to a specific content type. - **locale** (string) - Optional - Filter to a specific locale. ### Returns - **total** (number) - The total number of results found. - **isPersonalized** (boolean) - Indicates if the results were personalized. - **items** (array) - An array of search result items, each with a `title` property. - **paging** (object) - Pagination information including `start`, `limit`, and `pages`. ```