Step 3 of 10 (30% complete)

Project Structure

Step Code

The code for this specific step can be found on the following branch:

Click on a link to view the code for this step on GitHub.

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.tsx is intentionally minimal. Its only job is to import globals.css and — 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.tsx is a single catch-all route that handles every CMS page regardless of its URL depth. Whether the page is at /about or /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:

  1. A content type definition (the schema the CLI pushes to CMS)
  2. 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!

Contact Me