Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions samples/uryga-template/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Key to fetch content from the CMS.
# To get the key go to your CMS instance > Settings > API Keys
OPTIMIZELY_GRAPH_SINGLE_KEY='<SECRET>'

# Client ID and Secret from your CMS instance
# Go to your CMS instance > Settings > API Keys and click "Create API key"
OPTIMIZELY_CMS_CLIENT_ID=
OPTIMIZELY_CMS_CLIENT_SECRET=

# Root CMS instance. For example "https://<something>.cms.optimizely.com"
OPTIMIZELY_CMS_HOST=
OPTIMIZELY_GRAPH_URL="https://cg.optimizely.com/content/v2"

## A secret key used for revalidating cached content. This should also be a secure, randomly generated string.
OPTIMIZELY_REVALIDATE_SECRET=""
## The Route URL of the Start Page. Examlpe: "/start-page"
OPTIMIZELY_START_PAGE_URL=""
43 changes: 43 additions & 0 deletions samples/uryga-template/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

certificates
1 change: 1 addition & 0 deletions samples/uryga-template/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@episerver:registry=https://npm.pkg.github.com
4 changes: 4 additions & 0 deletions samples/uryga-template/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
.next
public
lib/optimizely/types/generated.ts
7 changes: 7 additions & 0 deletions samples/uryga-template/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"plugins": ["prettier-plugin-tailwindcss"]
}
36 changes: 36 additions & 0 deletions samples/uryga-template/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
59 changes: 59 additions & 0 deletions samples/uryga-template/app/[locale]/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { generateAlternates } from '@/lib/metadata'
import { getAllPagesPaths } from '@/lib/optimizely/all-pages'
import { GraphClient } from '@episerver/cms-sdk'
import { OptimizelyComponent } from '@episerver/cms-sdk/react/server'
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import React from 'react'

type Props = {
params: Promise<{
slug: string[]
locale: string
}>
}

export async function generateStaticParams() {
try {
return await getAllPagesPaths()
} catch (e) {
console.error('Error generating static params:', e)
return [] // Return an empty array on error to prevent build failures
}
}

export async function generateMetadata(props: Props): Promise<Metadata> {
const { locale, slug } = await props.params

const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, {
graphUrl: process.env.OPTIMIZELY_GRAPH_URL,
})

const formattedSlug = `/${slug.join('/')}/`

const c = await client.fetchContent(`/${locale}${formattedSlug}`)

return {
title: c?.title ?? '',
description: c?.shortDescription || '',
keywords: c?.keywords ?? '',
alternates: generateAlternates(locale, formattedSlug),
}
}

export default async function Page({ params }: Props) {
const { slug, locale } = await params

const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, {
graphUrl: process.env.OPTIMIZELY_GRAPH_URL,
})

try {
const c = await client.fetchContent(`/${locale}/${slug.join('/')}/`)

return <OptimizelyComponent opti={c} />
} catch (error) {
console.error('Error fetching content:', error)
return notFound()
}
}
44 changes: 44 additions & 0 deletions samples/uryga-template/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Geist, Geist_Mono } from 'next/font/google'
import { Header } from '@/components/layout/header'
import { Footer } from '@/components/layout/footer'
import { LOCALES } from '@/lib/optimizely/language'

const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
})

const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
})

export function generateStaticParams() {
try {
return LOCALES.map((locale) => ({ locale }))
} catch (e) {
console.error(e)
return []
}
}

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>
)
}
46 changes: 46 additions & 0 deletions samples/uryga-template/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { generateAlternates } from '@/lib/metadata'
import { GraphClient } from '@episerver/cms-sdk'
import { OptimizelyComponent } from '@episerver/cms-sdk/react/server'
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import React from 'react'

type Props = {
params: Promise<{
locale: string
}>
}

export async function generateMetadata(props: Props): Promise<Metadata> {
const { locale } = await props.params

const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, {
graphUrl: process.env.OPTIMIZELY_GRAPH_URL,
})

const c = await client.fetchContent(`/${locale}/`)

return {
title: c?.title ?? '',
description: c?.shortDescription || '',
keywords: c?.keywords ?? '',
alternates: generateAlternates(locale, '/'),
}
}

export default async function Page({ params }: Props) {
const { locale } = await params

const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, {
graphUrl: process.env.OPTIMIZELY_GRAPH_URL,
})

try {
const c = await client.fetchContent(`/${locale}/`)

return <OptimizelyComponent opti={c} />
} catch (error) {
console.error('Error fetching content:', error)
return notFound()
}
}
127 changes: 127 additions & 0 deletions samples/uryga-template/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { GraphClient } from '@episerver/cms-sdk'
import { revalidatePath, revalidateTag } from 'next/cache'
import { type NextRequest, NextResponse } from 'next/server'

const OPTIMIZELY_REVALIDATE_SECRET = process.env.OPTIMIZELY_REVALIDATE_SECRET

export async function POST(request: NextRequest) {
try {
validateWebhookSecret(request)
const docId = await extractDocId(request)

if (!docId || !docId.includes('Published')) {
return NextResponse.json({ message: 'No action taken' })
}

const [guid, locale] = docId.split('_')
const formattedGuid = guid.replaceAll('-', '')

const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, {
graphUrl: process.env.OPTIMIZELY_GRAPH_URL,
})

const contentData = await client.request(GET_CONTENT_BY_GUID_QUERY, {
guid: formattedGuid,
locale: [locale],
})
const content = contentData?._Content?.item
const urlType = content?._metadata?.url?.type
// In hierarchical routing, the Start Page in Optimizely does not use "/" as its URL.
// Instead, it has a custom path like "/start-page". We remove the OPTIMIZELY_START_PAGE_URL
// prefix to normalize the URL and make it relative to the site root.
const url =
urlType === 'SIMPLE'
? content?._metadata?.url?.default
: content?._metadata?.url?.hierarchical?.replace(
process.env.OPTIMIZELY_START_PAGE_URL ?? '',
''
)

if (!url) {
return NextResponse.json({ message: 'Page Not Found' }, { status: 400 })
}

const urlWithLocale = normalizeUrl(url, locale)

await handleRevalidation(urlWithLocale)

return NextResponse.json({ revalidated: true, now: Date.now() })
} catch (error) {
return handleError(error)
}
}

// Define the GraphQL query string
const GET_CONTENT_BY_GUID_QUERY = `
query GetContentByGuid($guid: String, $locale: [Locales]) {
_Content(locale: $locale, where: { _metadata: { key: { eq: $guid } } }) {
item {
_metadata {
displayName
url {
hierarchical
default
type
}
}
}
}
}
`

function validateWebhookSecret(request: NextRequest) {
const webhookSecret = request.nextUrl.searchParams.get('cg_webhook_secret')
if (webhookSecret !== OPTIMIZELY_REVALIDATE_SECRET) {
throw new Error('Invalid credentials')
}
}

async function extractDocId(request: NextRequest): Promise<string> {
const requestJson = await request.json()
return requestJson?.data?.docId || ''
}

function normalizeUrl(url: string, locale: string): string {
// Ensure the URL starts with a slash
let normalizedUrl = url.startsWith('/') ? url : `/${url}`

// Remove the trailing slash, if present (e.g. "/about/" -> "/about")
if (normalizedUrl.endsWith('/')) {
normalizedUrl = normalizedUrl.slice(0, -1)
}

// If the URL doesn't already start with the locale (e.g. "/en"), prepend it
return normalizedUrl.startsWith(`/${locale}`)
? normalizedUrl
: `/${locale}${normalizedUrl}`
}

async function handleRevalidation(urlWithLocale: string) {
if (urlWithLocale.includes('footer')) {
console.log(`Revalidating tag: optimizely-footer`)
await revalidateTag('optimizely-footer')
} else if (urlWithLocale.includes('header')) {
console.log(`Revalidating tag: optimizely-header`)
await revalidateTag('optimizely-header')
} else {
console.log(`Revalidating path: ${urlWithLocale}`)
await revalidatePath(urlWithLocale)
}
}

function handleError(error: unknown) {
console.error('Error processing webhook:', error)
if (error instanceof Error) {
if (error.message === 'Invalid credentials') {
return NextResponse.json(
{ message: 'Invalid credentials' },
{ status: 401 }
)
}
return NextResponse.json({ message: error.message }, { status: 500 })
}
return NextResponse.json(
{ message: 'Internal Server Error' },
{ status: 500 }
)
}
Binary file added samples/uryga-template/app/favicon.ico
Binary file not shown.
Loading