Code Cleanup
Outline
< >Step Code
The code for this specific step can be found on the following branches:
GitHub - szymonuryga/optimizely-masterclass-step-by-step at 8-code-cleanup-eslint
Contribute to szymonuryga/optimizely-masterclass-step-by-step development by creating an account on GitHub.

GitHub - szymonuryga/optimizely-masterclass-step-by-step at 9-code-cleanup-auto-generated-types
Contribute to szymonuryga/optimizely-masterclass-step-by-step development by creating an account on GitHub.

GitHub - szymonuryga/optimizely-masterclass-step-by-step at 10-code-cleanup-seo-metadata
Contribute to szymonuryga/optimizely-masterclass-step-by-step development by creating an account on GitHub.

GitHub - szymonuryga/optimizely-masterclass-step-by-step at 11-code-cleanup-header-footer
Contribute to szymonuryga/optimizely-masterclass-step-by-step development by creating an account on GitHub.

This guide covers four improvements to your Next.js and Optimizely SaaS CMS integration:
Fixing ESLint Errors
To prevent ESLint errors with generated types:
1. Add the generated types file to .prettierignore
:
lib/optimizely/types/generated.ts
2. Add this to your eslint.config.mjs
:
{
files: ["lib/optimizely/types/generated.ts"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-object-type": "off",
},
}
3. For the block factory mapper, add this comment:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Note: This isn't the best solution, but it works for now.
Using Auto-Generated Types in Blocks
To achieve full integration with Optimizely Graph, we'll change the types to those generated from Codegen CLI. This ensures that our blocks are using the most up-to-date and accurate types.

Replace hardcoded types with those generated from Codegen CLI:
- Remove hardcoded types from v0
- Import types from the generated file
- Use
as
when type names conflict with component names:
import { ContactBlock as ContactBlockProps } from '@/lib/optimizely/types/generated'
Handling Nested Blocks
Since Optimizely returns all possible block types (even when only one is allowed), create this helper:
// lib/optimizely/types/typeUtils.ts
import type { _IContent } from '@/lib/optimizely/types/generated'
export type SafeContent = {
__typename?: string
} & _IContent
// Utility type to extract a specific type from _IContent union
export type ExtractContent<T extends { __typename: string }> = Extract<
_IContent,
{ __typename?: T['__typename'] }
>
// Helper function to safely cast _IContent to a specific type
export function castContent<T extends { __typename?: string }>(
content: SafeContent | null | undefined,
typename: T['__typename']
): T | null {
if (content && content?.__typename === typename) {
return content as unknown as T
}
return null
}
Use it in components like this:
// components/layout/header.tsx
export async function Header({ locale }: { locale: string }) {
// ... (previous code)
return (
<header className="sticky top-0 z-30 border-b bg-white">
{/* ... (other header content) */}
<nav className="hidden items-center gap-6 md:flex">
{navItems?.map((navItem) => {
const item = castContent<NavItem>(
navItem as SafeContent,
'NavItem'
)
if (!item) return null
return (
<Link
key={item.href}
href={item?.href ?? '/'}
className="text-sm font-medium"
>
{item.label}
</Link>
)
})}
</nav>
{/* ... (rest of the header) */}
</header>
)
}
Adding SEO Metadata
Use Next.js's generateMetadata function:
1. Create a helper for generating alternate URLs based on path
and locale
:
// lib/utils/metadata.ts
import { LOCALES } from '@/lib/optimizely/utils/language'
import { AlternateURLs } from 'next/dist/lib/metadata/types/alternative-urls-types'
export function normalizePath(path: string): string {
path = path.toLowerCase()
if (path === '/') {
return ''
}
if (path.endsWith('/')) {
path = path.substring(0, path.length - 1)
}
if (path.startsWith('/')) {
path = path.substring(1)
}
return path
}
export function generateAlternates(
locale: string,
path: string
): AlternateURLs {
path = normalizePath(path)
return {
canonical: `/${locale}/${path}`,
languages: Object.assign(
{},
...LOCALES.map((l) => ({ [l]: `/${l}/${path}` }))
),
}
}
2. Add metadata to your pages:
//app/[locale]/page.tsx
import { generateAlternates } from '@/lib/utils/metadata'
import { Metadata } from 'next'
export async function generateMetadata(props: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await props.params
const locales = getValidLocale(locale)
const pageResp = await optimizely.GetStartPage({ locales })
const page = pageResp.data?.StartPage?.items?.[0]
if (!page) {
return {}
}
return {
title: page.title,
description: page.shortDescription || '',
keywords: page.keywords ?? '',
alternates: generateAlternates(locale, '/'),
}
}
//app/[locale]/[slug]/page.tsx
import { generateAlternates } from '@/lib/utils/metadata'
import { Metadata } from 'next'
export async function generateMetadata(props: {
params: Promise<{ locale: string; slug?: string }>
}): Promise<Metadata> {
const { locale, slug = '' } = await props.params
const locales = getValidLocale(locale)
const { data, errors } = await optimizely.getPageByURL({
locales: [locales],
slug: `/${slug}`,
})
if (errors || !data?.CMSPage?.items?.[0]) {
return {}
}
const page = data.CMSPage.items[0]
if (!page) {
return {}
}
return {
title: page.title,
description: page.shortDescription || '',
keywords: page.keywords ?? '',
alternates: generateAlternates(locale, '/'),
}
}
Note: I use the same methods as to retrieve information about the whole page and all blocks, if we wanted to optimize this we should create a new graphql query that will retrieve information only about SEO.
Adding Header, Footer, and Not Found Page
1. Add a Not Found page:
// app/[locale]/not-found.tsx
import { Button } from '@/components/ui/button'
import Link from 'next/link'
export default function NotFound() {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-background px-3 text-foreground">
<h1 className="mb-4 text-4xl font-bold">404 - Page Not Found</h1>
<p className="mb-8 text-xl text-muted-foreground">
Oops! The page you are looking for does not exist.
</p>
<Button asChild>
<Link href="/">Go back home</Link>
</Button>
</div>
)
}
2. Add Footer
// components/layout/footer.tsx
import Link from 'next/link'
import { Icons } from '@/components/ui/icons'
import { getValidLocale } from '@/lib/optimizely/utils/language'
import { optimizely } from '@/lib/optimizely/fetch'
import { castContent, SafeContent } from '@/lib/optimizely/types/typeUtils'
import {
SocialLink,
FooterColumn,
NavItem,
} from '@/lib/optimizely/types/generated'
export async function Footer({ locale }: { locale: string }) {
const locales = getValidLocale(locale)
const { data } = await optimizely.getFooter(
{ locales: locales },
{ cacheTag: 'optimizely-footer' }
)
const footer = data?.Footer?.items?.[0]
if (!footer) {
return null
}
const { columns, socialLinks, copyrightText } = footer
return (
<footer className="border-t">
<div className="container mx-auto px-4 py-12">
<div className="grid gap-8 md:grid-cols-4">
{columns?.map((columnItem, index) => {
const column = castContent<FooterColumn>(
columnItem as SafeContent,
'FooterColumn'
)
if (!column) return null
return (
<div key={index}>
<h3 className="mb-4 font-bold">{column?.title}</h3>
<nav className="grid gap-2">
{column?.links?.map((linkItem, linkIndex) => {
const link = castContent<NavItem>(linkItem, 'NavItem')
if (!link) return null
return (
<Link
key={linkIndex}
href={link.href ?? '/'}
className="text-sm"
>
{link.label}
</Link>
)
})}
</nav>
</div>
)
})}
</div>
<div className="mt-8 flex justify-center gap-4">
{socialLinks?.map((linkItem, index) => {
const link = castContent<SocialLink>(
linkItem as SafeContent,
'SocialLink'
)
if (!link) return null
const platform = (link?.platform ?? '') as keyof typeof Icons
const Icon = platform ? Icons?.[platform] : null
return (
<Link
key={index}
href={link?.href ?? '/'}
className="text-muted-foreground hover:text-foreground"
>
{Icon && <Icon className="h-5 w-5" />}
</Link>
)
})}
</div>
<div className="mt-8 border-t pt-8 text-center text-sm text-muted-foreground">
{copyrightText}
</div>
</div>
</footer>
)
}
3. Add header
// components/layout/header.tsx
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { optimizely } from '@/lib/optimizely/fetch'
import { getValidLocale } from '@/lib/optimizely/utils/language'
import { castContent, SafeContent } from '@/lib/optimizely/types/typeUtils'
import { NavItem } from '@/lib/optimizely/types/generated'
import Image from 'next/image'
import { LanguageSwitcher } from './language-switcher'
export async function Header({ locale }: { locale: string }) {
const locales = getValidLocale(locale)
const { data } = await optimizely.getHeader(
{ locale: locales },
{ cacheTag: 'optimizely-header' }
)
const header = data?.Header?.items?.[0]
if (!header) {
return null
}
const { logo, ctaHref, ctaText, navItems } = header
return (
<header className="sticky top-0 z-30 border-b bg-white">
<div className="container mx-auto px-4">
<div className="flex h-16 items-center justify-between">
<Link href="/" className="text-xl font-bold lg:min-w-[150px]">
<Image src={logo ?? ''} width={50} height={50} alt="logo" />
</Link>
<nav className="hidden items-center gap-6 md:flex">
{navItems?.map((navItem) => {
const item = castContent<NavItem>(
navItem as SafeContent,
'NavItem'
)
if (!item) return null
return (
<Link
key={item.href}
href={item?.href ?? '/'}
className="text-sm font-medium"
>
{item.label}
</Link>
)
})}
</nav>
<div className="flex items-center gap-4">
<LanguageSwitcher currentLocale={locale} />
<Button variant="outline" asChild>
<Link href={ctaHref ?? '/'}>{ctaText}</Link>
</Button>
</div>
</div>
</div>
</header>
)
}
// components/layout/language-switcher.tsx
'use client'
import { usePathname, useRouter } from 'next/navigation'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Globe } from 'lucide-react'
import { LOCALES } from '@/lib/optimizely/utils/language'
import { Button } from './ui/button'
const LOCALE_NAMES: Record<string, string> = {
en: 'English',
pl: 'Polski',
sv: 'Svenska',
}
export function LanguageSwitcher({ currentLocale }: { currentLocale: string }) {
const pathname = usePathname()
const router = useRouter()
const handleLocaleChange = (newLocale: string) => {
const currentPath = pathname
const newPath = currentPath.includes(`/${currentLocale}`)
? currentPath.replace(`/${currentLocale}`, `/${newLocale}`)
: `/${newLocale}/${currentPath}`
router.push(newPath)
router.refresh()
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Globe className="h-4 w-4" />
<span className="sr-only">Switch language</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{LOCALES.map((loc) => (
<DropdownMenuItem
key={loc}
className={loc === currentLocale ? 'bg-accent' : ''}
onClick={() => handleLocaleChange(loc)}
>
{LOCALE_NAMES[loc]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
Note: It is very important to add router.refresh() because without this all next/Link components will have in cache the previously selected language
4. Add header and footer components to your layout.tsx file
// app/[locale]/layout.tsx
export default async function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode
params: Promise<{ locale: string }>
}>) {
const { locale } = await params
return (
<html lang={locale}>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Header locale={locale} />
<main className="container mx-auto min-h-screen px-4">{children}</main>
<Footer locale={locale} />
</body>
</html>
)
}
By following these steps, you'll improve your code quality, type safety, and SEO to your Next.js project with Optimizely CMS.