Step 12 of 21 (57% complete)
Code Cleanup and Enhancement Guide
Step Code
The code for this specific step can be found on the following branches:
This guide covers four improvements to your Next.js and Optimizely SaaS CMS integration:
1. Fixing ESLint Errors
To prevent ESLint errors with generated types:
- Add the generated types file to
.prettierignore
:
lib/optimizely/types/generated.ts
- 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", }, }
- For the block factory mapper, add this comment:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2. 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> ) }
3. Adding SEO Metadata
Use Next.js's generateMetadata function:
- Create a helper for generating alternate URLs based on
path
andlocale
:
// 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}` })) ), } }
- 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, '/'), } }
4. Adding Header, Footer, and Not Found Page
- 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> ) }
- 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> ) }
- 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> ) }
- 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.
Have questions? I'm here to help!