Step 3 of 10 (30% complete)
Project Structure
Step Code
The code for this specific step can be found on the following branch:
Let's walk through the project structure so you understand where everything lives and why.
Top-Level Overview
app/ # Next.js App Router — pages, layouts, API routes components/ # React components lib/ # Shared logic, utilities, cache helpers proxy.ts # Next.js middleware for locale routing optimizely.config.mjs # Optimizely CLI configuration next.config.ts # Next.js configuration
The app/ Directory
app/ layout.tsx # Root layout — imports globals.css and lib/optimizely/init.ts [locale]/ layout.tsx # Locale layout — HTML shell, Header, Footer, fonts page.tsx # Homepage — fetches Start Page content from CMS [...slug]/page.tsx # Catch-all — handles every other CMS page api/revalidate/route.ts # POST webhook — triggers cache revalidation preview/ layout.tsx # Minimal layout for preview mode page.tsx # Renders unpublished draft content from CMS
Two things worth noting:
app/layout.tsxis intentionally minimal. Its only job is to importglobals.cssand — critically —lib/optimizely/init.ts. This import registers all content types and React components on every request. We will look at why this lives in a separate file in the next lesson.app/[locale]/[...slug]/page.tsxis a single catch-all route that handles every CMS page regardless of its URL depth. Whether the page is at/aboutor/work/project-x/details, this one file handles it.
The components/ Directory
components/ optimizely/ block/ # Block-level components (HeroBlock, ProfileBlock, etc.) page/ # Page type components (CMSPage, HeaderPage, FooterPage, StartPage) experience/ # Experience types for Visual Builder (BlankExperience, SEOExperience) section/ # Section types for Visual Builder (BlankSection) layout/ header.tsx # Async Server Component — fetches header content from CMS footer.tsx # Async Server Component — fetches footer content from CMS language-switcher.tsx # 'use client' — interactive locale dropdown ui/ # shadcn/ui components (Button, Card, Avatar, etc.)
The components/optimizely/ folder is the heart of the project. Every piece of CMS-managed content has a component here. Each component file exports two things:
- A content type definition (the schema the CLI pushes to CMS)
- A React component (the UI that renders it)
These two things live in the same file, which is one of the nicest patterns in this stack — the schema and the component are always in sync.
The lib/ Directory
lib/ optimizely/ init.ts # Registers all content types and React components all-pages.ts # getAllPagesPaths() — for generateStaticParams() content-types.ts # AllBlocksContentTypes list language.ts # LOCALES, DEFAULT_LOCALE, mapPathWithoutLocale() cache/ cache-keys.ts # CACHE_KEYS constants and getCacheTag() helper image/ loader.ts # Custom Cloudinary image loader for next/image metadata.ts # generateAlternates() — hreflang + canonical URLs utils.ts # cn(), createUrl(), leadingSlashUrlPath()
The optimizely.config.mjs File
This is the configuration file for the Optimizely CLI. It tells the CLI where to look for content type definitions:
import { buildConfig } from '@optimizely/cms-sdk' export default buildConfig({ components: ['./components/optimizely/**/*.tsx'], })
When you run npm run opti-push, the CLI scans all files matching this pattern, finds every contentType() call, and pushes the schemas to your CMS instance. This is why keeping all your Optimizely components inside components/optimizely/ matters — it is the folder the CLI watches.
Have questions? I'm here to help!