<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Szymon Uryga</title><link href="http://world.optimizely.com" /><updated>2025-07-15T14:17:08.0000000Z</updated><id>https://world.optimizely.com/blogs/szymon-uryga/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Optimizely Frontend Hosting: Deploy Without PowerShell Using the @kunalshetye/opticloud Package</title><link href="https://world.optimizely.com/blogs/szymon-uryga/dates/2025/7/optimizely-frontend-hosting-deploy-without-powershell-using-the-kunalshetyeopticloud-package/" /><id>&lt;p&gt;In my last two blog posts, I walked through how to get started with deploying a &lt;strong&gt;headless app to Optimizely Frontend Hosting&lt;/strong&gt; using PowerShell and the&amp;nbsp;&lt;strong&gt;EpiCloud &lt;/strong&gt;module. I also demonstrated how to &lt;strong&gt;automate the deployment&lt;/strong&gt; using a script or GitHub Actions.&lt;/p&gt;
&lt;p&gt;While that approach works great, it can be overwhelming for developers who aren&amp;rsquo;t familiar with &lt;strong&gt;PowerShell&lt;/strong&gt; or the &lt;a href=&quot;https://docs.developers.optimizely.com/digital-experience-platform/docs/deployment-api&quot;&gt;&lt;strong&gt;Optimizely Deployment&amp;nbsp;API&lt;/strong&gt;&lt;/a&gt;. Fortunately, there&#39;s now a much easier way to get started - &lt;strong&gt;with a single command&lt;/strong&gt; and &lt;strong&gt;no PowerShell knowledge required&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href=&quot;https://www.npmjs.com/package/@kunalshetye/opticloud&quot;&gt;&lt;strong&gt;@kunalshetye/opticloud&lt;/strong&gt;&lt;/a&gt; - a fantastic NPM package built by &lt;strong&gt;Kunal Shetye&lt;/strong&gt; that takes care of the entire deployment logic for you. I&amp;rsquo;ve recently started using it myself, and I was immediately impressed by how fast and simple it is. If you&#39;re a frontend developer deploying to Optimizely DXP for the first time, this tool should absolutely be your first choice.&lt;/p&gt;
&lt;p&gt;In this article, I&amp;rsquo;ll show you how easy it is to deploy a headless app using this modern CLI.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&lt;strong&gt;Deployment Without PowerShell - The @kunalshetye/opticloud NPM Package&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;For many developers, the first contact with Optimizely happens through the &lt;strong&gt;SaaS CMS&lt;/strong&gt;, and they often have &lt;strong&gt;no experience with Optimizely DXP&lt;/strong&gt; or PowerShell-based deployments. In these cases, the &lt;strong&gt;&amp;nbsp;&lt;a href=&quot;https://www.npmjs.com/package/@kunalshetye/opticloud&quot;&gt;@kunalshetye/opticloud&lt;/a&gt;&lt;/strong&gt;&amp;nbsp;package is an &lt;strong&gt;excellent solution&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This modern CLI tool, created by &lt;strong&gt;Kunal&lt;/strong&gt;, handles the entire deployment process in a simple and developer-friendly way - whether you&#39;re deploying a headless app, CMS or commerce project.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;&lt;strong&gt;&#128640; How to Deploy Using the Optimizely DXP CLI&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;1. Install the Package&lt;/h4&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;npm install -g @kunalshetye/opticloud&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. Authenticate with DXP&lt;/h4&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;opticloud auth:login&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&amp;rsquo;ll be prompted to enter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Client Key&lt;/strong&gt; (from the DXP Cloud portal)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Client Secret&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Project ID&lt;/strong&gt; (the GUID of your DXP project)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. Use the ship Command&lt;/h4&gt;
&lt;p&gt;Once authenticated, you can deploy your app using a single command:&lt;/p&gt;
&lt;div class=&quot;contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary&quot;&gt;
&lt;div class=&quot;overflow-y-auto p-4&quot;&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;opticloud ship ./ \
  --target=test1 \
  --type=head \
  --prefix=optimizely-one \
  --version=1.0.0 \
  --output=./
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The&amp;nbsp;&lt;strong&gt;ship &lt;/strong&gt;command is the ultimate streamlined solution for Optimizely DXP deployments. It orchestrates everything from packaging to deployment in one step.&lt;/p&gt;
&lt;h4&gt;4. Automate It with GitHub Actions&lt;/h4&gt;
&lt;p&gt;Even better,&amp;nbsp;&lt;strong&gt;opticloud &lt;/strong&gt;comes with built-in support for &lt;strong&gt;GitHub Actions&lt;/strong&gt;, making it easy to set up continuous deployment pipelines.&lt;/p&gt;
&lt;p&gt;&#128073; Check out the official GitHub Actions documentation here:&amp;nbsp;&lt;a class=&quot;&quot; href=&quot;https://github.com/kunalshetye/opticloud/blob/main/docs/github-actions.md&quot;&gt;&lt;strong&gt;GitHub Actions Docs&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&lt;strong&gt;&#128282; Summary&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Kunal has done an outstanding job building a &lt;strong&gt;modern deployment tool&lt;/strong&gt; that eliminates the complexity of PowerShell and the legacy &lt;strong&gt;EpiCloud&amp;nbsp;&lt;/strong&gt;module. It&amp;rsquo;s now easier than ever for developers - especially frontend engineers who are new to Optimizely - to deploy to DXP with confidence.&lt;/p&gt;
&lt;p&gt;With &lt;strong&gt;opticloud&lt;/strong&gt;, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Deploy &lt;strong&gt;headless apps&lt;/strong&gt;, CMS sites, commerce platforms, and even SQL databases&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;strong&gt;GitHub Actions&lt;/strong&gt; for seamless CI/CD&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Avoid the &lt;strong&gt;steep learning curve&lt;/strong&gt; of traditional DXP tooling&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&#128216; Highly recommended: Explore the full documentation here &amp;rarr; &lt;a class=&quot;&quot; href=&quot;https://www.npmjs.com/package/@kunalshetye/opticloud&quot;&gt;&lt;strong&gt;opticloud on NPM&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</id><updated>2025-07-15T14:17:08.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How to Set Up CI/CD Pipeline for Optimizely Frontend Hosting Using GitHub Actions</title><link href="https://world.optimizely.com/blogs/szymon-uryga/dates/2025/7/how-to-set-up-cicd-pipeline-for-optimizely-frontend-hosting-using-github-actions/" /><id>&lt;p&gt;As I promised in my &lt;a class=&quot;&quot; href=&quot;/link/ae36e13df40b4bc6849c621eda993915.aspx&quot;&gt;previous blog post&lt;/a&gt; about getting started with Optimizely Frontend Hosting, today I&amp;rsquo;m delivering on that promise by sharing how to automate your deployment process with &lt;strong&gt;GitHub Actions&lt;/strong&gt;. In this step-by-step tutorial, I&amp;rsquo;ll show you how to set up a CI/CD pipeline that automatically deploys your frontend app every time you push to the &lt;strong&gt;main &lt;/strong&gt;branch.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before you start, make sure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A working Optimizely Frontend Hosting setup (with environments like &lt;em&gt;Test1&lt;/em&gt;, &lt;em&gt;Test2&lt;/em&gt;, &lt;em&gt;Production1&lt;/em&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remember to &lt;strong&gt;add all &lt;/strong&gt;necessary env variable &lt;strong&gt;before &lt;/strong&gt;you do deployemnt&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Your Optimizely &lt;strong&gt;Project ID&lt;/strong&gt;, &lt;strong&gt;Client Key&lt;/strong&gt;, and &lt;strong&gt;Client Secret&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A PowerShell deployment script (see below)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A GitHub repo with your project code&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Step 1: Add Secrets to GitHub&lt;/h2&gt;
&lt;p&gt;Go to your GitHub repository &amp;rarr; &lt;strong&gt;Settings&lt;/strong&gt; &amp;rarr; &lt;strong&gt;Secrets and variables&lt;/strong&gt; &amp;rarr; &lt;strong&gt;Actions&lt;/strong&gt; &amp;rarr; click &lt;strong&gt;New repository secret&lt;/strong&gt;:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; border-spacing: 2px; margin-left: 0px; margin-right: auto; border: 2px solid rgb(0, 0, 0);&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border-width: 2px; padding: 2px; border-color: rgb(0, 0, 0);&quot;&gt;Name&lt;/th&gt;
&lt;th style=&quot;border-width: 2px; padding: 2px; border-color: rgb(0, 0, 0);&quot;&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border-width: 2px; padding: 2px; border-color: rgb(0, 0, 0);&quot;&gt;OPTI_PROJECT_ID&lt;/td&gt;
&lt;td style=&quot;border-width: 2px; padding: 2px; border-color: rgb(0, 0, 0);&quot;&gt;Your Optimizely Project ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border-width: 2px; padding: 2px; border-color: rgb(0, 0, 0);&quot;&gt;OPTI_CLIENT_KEY&lt;/td&gt;
&lt;td style=&quot;border-width: 2px; padding: 2px; border-color: rgb(0, 0, 0);&quot;&gt;Your Optimizely Client Key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border-width: 2px; padding: 2px; border-color: rgb(0, 0, 0);&quot;&gt;OPTI_CLIENT_SECRET&lt;/td&gt;
&lt;td style=&quot;border-width: 2px; padding: 2px; border-color: rgb(0, 0, 0);&quot;&gt;Your Optimizely Client Secret&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Step 2: Prepare the PowerShell Deployment Script&lt;/h2&gt;
&lt;p&gt;Create a script called &lt;strong&gt;deploy.ps1&lt;/strong&gt; in your repo (e.g., under a &lt;strong&gt;scripts/&lt;/strong&gt;&amp;nbsp;folder).&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;# scripts/deploy.ps1

# Validate required environment variables
if (-not $env:OPTI_PROJECT_ID -or -not $env:OPTI_CLIENT_KEY -or -not $env:OPTI_CLIENT_SECRET) {
    Write-Host &quot;Missing required environment variables.&quot; -ForegroundColor Red
    exit 1
}

# Install and import EpiCloud module
Install-Module -Name EpiCloud -Scope CurrentUser -Force -ErrorAction SilentlyContinue
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
Import-Module EpiCloud

# Deployment settings
$projectId = $env:OPTI_PROJECT_ID
$clientKey = $env:OPTI_CLIENT_KEY
$clientSecret = $env:OPTI_CLIENT_SECRET
$targetEnvironment = &quot;Test1&quot;

# Paths
$timestamp = Get-Date -Format &quot;yyyyMMdd-HHmmss&quot;
$zipName = &quot;optimizely-one-github.head.app.$timestamp.zip&quot;
$zipPath = &quot;.\$zipName&quot;

# Create ZIP from current directory excluding ignored files
# (GitHub Actions runners will not include `.git`, so you&#39;re safe)
Compress-Archive -Path * -DestinationPath $zipPath

if (-not (Test-Path $zipPath)) {
    Write-Host &quot;Failed to create ZIP file.&quot; -ForegroundColor Red
    exit 1
}

# Deploy using EpiCloud
Connect-EpiCloud -ProjectId $projectId -ClientKey $clientKey -ClientSecret $clientSecret
$sasUrl = Get-EpiDeploymentPackageLocation
Add-EpiDeploymentPackage -SasUrl $sasUrl -Path $zipPath

Start-EpiDeployment `
    -DeploymentPackage $zipName `
    -TargetEnvironment $targetEnvironment `
    -Wait
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Zip everything in the repo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Authenticate to Optimizely&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Upload and deploy the package to the selected environment&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Step 3: Create GitHub Action Workflow&lt;/h2&gt;
&lt;p&gt;Create a file at &lt;strong&gt;.github/workflows/deploy.yml&amp;nbsp;&lt;/strong&gt;in your repository:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;name: Deploy to Optimizely

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: windows-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup PowerShell
        shell: pwsh
        run: |
          Write-Host &quot;PowerShell ready&quot;

      - name: Run deployment script
        shell: pwsh
        run: ./scripts/deploy.ps1
        env:
          OPTI_PROJECT_ID: ${{ secrets.OPTI_PROJECT_ID }}
          OPTI_CLIENT_KEY: ${{ secrets.OPTI_CLIENT_KEY }}
          OPTI_CLIENT_SECRET: ${{ secrets.OPTI_CLIENT_SECRET }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 4: Test It&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Commit your &lt;strong&gt;deploy.ps1&lt;/strong&gt;&amp;nbsp;and workflow to the repository.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Push to &lt;strong&gt;main&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go to &lt;strong&gt;GitHub &amp;rarr; Actions&lt;/strong&gt; and check if the deployment workflow ran successfully.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Check the &lt;strong&gt;Optimizely DXP Portal&lt;/strong&gt; to confirm the deployment on the &lt;em&gt;Test1 &lt;/em&gt;environment.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Result&lt;/h2&gt;
&lt;p&gt;Here are two screenshots showing the final result:&lt;/p&gt;
&lt;h3&gt;GitHub Actions&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/link/0cdbf2cf6ee2458d98ef66966f9c1811.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The workflow completed successfully with detailed logs from the PowerShell deployment.&lt;/p&gt;
&lt;h3&gt;Optimizely PaaS Portal&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/link/46f44cbda765465d8cffba607540c6d3.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You can see the package &lt;strong&gt;optimizely-one-github.head.app.20250702-102756.zip&lt;/strong&gt;&amp;nbsp;was successfully deployed to the &lt;strong&gt;Test1 &lt;/strong&gt;environment.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With just a few steps, you&amp;rsquo;ve automated your frontend deployments to Optimizely using GitHub Actions. This approach ensures that your latest changes in&amp;nbsp;&lt;strong&gt;main &lt;/strong&gt;are always deployed to your specifed environment, improving developer velocity and reducing manual errors.&lt;/p&gt;</id><updated>2025-07-02T11:52:03.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Deploying to Optimizely Frontend Hosting: A Practical Guide</title><link href="https://world.optimizely.com/blogs/szymon-uryga/dates/2025/6/deploying-to-optimizely-frontend-hosting-a-practical-guide/" /><id>&lt;p&gt;&lt;strong&gt;Optimizely Frontend Hosting&lt;/strong&gt; is a cloud-based solution for deploying headless frontend applications - currently supporting only &lt;strong&gt;Next.js&lt;/strong&gt; projects. Its deployment process and core concept are familiar to .NET developers who have worked with &lt;strong&gt;Optimizely DXP&lt;/strong&gt;: code is deployed via a managed pipeline, and environment setup is handled entirely by Optimizely.&lt;/p&gt;
&lt;p&gt;Out of the box, Optimizely Frontend Hosting includes a &lt;strong&gt;Web Application Firewall (WAF)&lt;/strong&gt; and &lt;strong&gt;CDN&lt;/strong&gt; powered by &lt;strong&gt;Cloudflare&lt;/strong&gt;, ensuring your statically generated Next.js content is delivered globally with high performance and security - no additional configuration required.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/0f792590380345c69f54d59c0630e09c.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Key Characteristics&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Next.js support only&lt;/strong&gt; (as of now)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Managed environments&lt;/strong&gt; like Test1, Test2 and Production for SaaS CMS&amp;nbsp;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Automatic CDN and WAF configuration&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Static site hosting&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s worth noting that customization capabilities are currently limited. For advanced features or custom configuration (e.g., setting cache headers, redirects, custom domains, etc.), it&amp;rsquo;s recommended to contact &lt;strong&gt;Optimizely Support&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Compared to frontend-specialized platforms like Vercel, Optimizely Frontend Hosting doesn&amp;rsquo;t aim to deliver a full-featured developer platform. Instead, it offers a &lt;strong&gt;stable and integrated solution&lt;/strong&gt; for teams already working with the Optimizely stack, looking for a &lt;strong&gt;reliable and supported way to host frontend applications&lt;/strong&gt; alongside their CMS and backend infrastructure.&lt;/p&gt;
&lt;p&gt;This makes it ideal for enterprise use cases where &lt;strong&gt;vendor consolidation&lt;/strong&gt;, &lt;strong&gt;support unification&lt;/strong&gt;, and &lt;strong&gt;environment control&lt;/strong&gt; are priorities.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;&#128640; Deployment in Practice&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s how a typical deployment looks:&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Step-by-step manual deployment (recommended to start with):&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Open PowerShell&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install and import the EpiCloud module:&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;Install-Module -Name EpiCloud -Scope CurrentUser -Force
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Import-Module EpiCloud
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Set credentials and environment:&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;$projectId = &quot;&amp;lt;your_project_id&amp;gt;&quot;
$clientKey = &quot;&amp;lt;your_client_key&amp;gt;&quot;
$clientSecret = &quot;&amp;lt;your_client_secret&amp;gt;&quot;
$targetEnvironment = &quot;Test1&quot;  # Or &quot;Production&quot; Or &quot;Test2&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Zip your Next.js application&lt;br /&gt;&lt;/strong&gt;Name format:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;&amp;lt;name&amp;gt;.head.app.&amp;lt;version&amp;gt;.zip&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example: &lt;code&gt;optimizely-one.head.app.20250610.zip&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ If .&lt;strong&gt;head.app.&lt;/strong&gt; is not in the filename, the deployment system will treat it as a .NET NuGet package instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Connect to EpiCloud and get the SAS URL:&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;Connect-EpiCloud -ProjectId $projectId -ClientKey $clientKey -ClientSecret $clientSecret
$sasUrl = Get-EpiDeploymentPackageLocation
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Upload your deployment package:&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;Add-EpiDeploymentPackage -SasUrl $sasUrl -Path .\optimizely-one.head.app.20250610.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Trigger deployment:&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;Start-EpiDeployment `
  -DeploymentPackage &quot;optimizely-one.head.app.20250610.zip&quot; `
  -TargetEnvironment $targetEnvironment `
  -DirectDeploy `
  -Wait `
  -Verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;Automating Deployment&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Once you understand the manual steps, you can fully automate the deployment process using a PowerShell script like the one below. This approach ensures repeatability and helps integrate deployment into your CI/CD pipeline.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&#128161; It&amp;rsquo;s &lt;strong&gt;highly recommended to perform a few deployments manually&lt;/strong&gt; before automating, to build confidence and understanding. This will help later when diagnosing issues or customizing the flow.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;strong&gt;1. .zipignore Support&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;To avoid bundling unnecessary files into your deployment zip package, the script supports a .&lt;strong&gt;zipignore&amp;nbsp;&lt;/strong&gt;file (similar to &lt;strong&gt;.gitignore&lt;/strong&gt;). Place this file in your app&amp;rsquo;s root directory (&lt;strong&gt;$sourcePath&lt;/strong&gt;) and list the paths or folders to &lt;strong&gt;exclude&lt;/strong&gt; from the archive.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Example .zipignore:&lt;/strong&gt;&lt;/h3&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;.next
node_modules
.env
.git
.DS_Store
.vscode
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ If no files remain after filtering with &lt;strong&gt;.zipignore&lt;/strong&gt;, the script will exit with an error.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;strong&gt;2. How to Customize the Script for Your Project&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;You need to adjust &lt;strong&gt;two key values&lt;/strong&gt; in the script:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Path to your Next.js app:&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;$sourcePath = &quot;C:\Workspace\Personal\optimizely-one&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace this with the actual path to your project.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Target deployment environment:&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;$targetEnvironment = &quot;Test1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set the &lt;strong&gt;$targetEnvironment&lt;/strong&gt;&amp;nbsp;variable to match the environment configured in your Optimizely project.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;For SaaS Frontend Hosting:&lt;/strong&gt; use one of the predefined environment names, such as: &quot;Test1&quot;, &quot;Test2&quot;, or &quot;Production&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;For traditional PaaS hosting:&lt;/strong&gt; use one of the standard Optimizely environment names: &quot;Integration&quot;, &quot;Preproduction&quot;, or &quot;Production&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make sure the name you use matches exactly what is defined in the Optimizely project portal.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;3. Required Environment Variables&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Before running the script, you need to define the following &lt;strong&gt;environment variables&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OPTI_PROJECT_ID&lt;/li&gt;
&lt;li&gt;OPTI_CLIENT_KEY&lt;/li&gt;
&lt;li&gt;OPTI_CLIENT_SECRET&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can find these in the &lt;strong&gt;API section&lt;/strong&gt; of the &lt;a class=&quot;cursor-pointer&quot; href=&quot;&quot;&gt;Optimizely PaaS Portal&lt;/a&gt; for your frontend project.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;img src=&quot;/link/b82a89e84c934d06abef2cb3aa64c71a.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;To set them temporarily in PowerShell:&lt;/strong&gt;&lt;/h4&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;$env:OPTI_PROJECT_ID = &quot;&amp;lt;your_project_id&amp;gt;&quot;
$env:OPTI_CLIENT_KEY = &quot;&amp;lt;your_client_key&amp;gt;&quot;
$env:OPTI_CLIENT_SECRET = &quot;&amp;lt;your_client_secret&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&#128221; Alternatively, you can add them permanently to your system environment variables.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4. How to Run the Script&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open &lt;strong&gt;PowerShell.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Save the script below to a &amp;nbsp;&lt;strong&gt;.ps1 &lt;/strong&gt;file, e.g. &lt;strong&gt;deploy-optimizely.ps1&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set the required environment variables (see above).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run the script:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;.\deploy-optimizely.ps1&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;5. PowerShell Deployment Script (with Comments)&lt;/strong&gt;&lt;/h3&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;# Check required environment variables
if (-not $env:OPTI_PROJECT_ID -or -not $env:OPTI_CLIENT_KEY -or -not $env:OPTI_CLIENT_SECRET) {
    Write-Host &quot;Missing one or more required environment variables: OPTI_PROJECT_ID, OPTI_CLIENT_KEY, OPTI_CLIENT_SECRET&quot; -ForegroundColor Red
    exit 1
}

# Install and import EpiCloud module
Install-Module -Name EpiCloud -Scope CurrentUser -Force -ErrorAction SilentlyContinue
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
Import-Module EpiCloud

# Settings
$projectId = $env:OPTI_PROJECT_ID
$clientKey = $env:OPTI_CLIENT_KEY
$clientSecret = $env:OPTI_CLIENT_SECRET
$targetEnvironment = &quot;Test1&quot;  # Change to Production, Test2, etc. as needed

# Path to the root of your Next.js app
$sourcePath = &quot;C:\Workspace\Personal\optimizely-one&quot;  # &amp;lt;- CHANGE THIS

# Generate unique zip filename
$timestamp = Get-Date -Format &quot;yyyyMMdd-HHmmss&quot;
$zipName = &quot;optimizely-one.head.app.$timestamp.zip&quot;
$zipPath = &quot;.\$zipName&quot;

# Remove existing archive if present
if (Test-Path $zipPath) { Remove-Item $zipPath }

# Load .zipignore file if it exists
$zipIgnorePath = Join-Path $sourcePath &quot;.zipignore&quot;
$excludeRoot = @()
if (Test-Path $zipIgnorePath) {
    $excludeRoot = Get-Content $zipIgnorePath | Where-Object { $_ -and $_ -notmatch &quot;^#&quot; }
}
$rootExcludes = $excludeRoot | ForEach-Object { Join-Path $sourcePath $_ }

# Collect files excluding those in .zipignore
$includeFiles = Get-ChildItem -Path $sourcePath -Recurse -File | Where-Object {
    foreach ($ex in $rootExcludes) {
        if ($_.FullName -like &quot;$ex\*&quot; -or $_.FullName -eq $ex) {
            return $false
        }
    }
    return $true
}

if ($includeFiles.Count -eq 0) {
    Write-Host &quot;ERROR: No files to archive after applying .zipignore filters.&quot; -ForegroundColor Red
    exit 1
}

# Prepare temporary folder for zipping
$tempPath = Join-Path $env:TEMP &quot;nextjs-build-zip&quot;
if (Test-Path $tempPath) { Remove-Item -Recurse -Force $tempPath }
New-Item -ItemType Directory -Path $tempPath | Out-Null

foreach ($file in $includeFiles) {
    $relativePath = $file.FullName.Substring($sourcePath.Length).TrimStart(&#39;\&#39;)
    $destPath = Join-Path $tempPath $relativePath
    $destDir = Split-Path -Path $destPath -Parent
    if (-not (Test-Path -LiteralPath $destDir)) {
        New-Item -ItemType Directory -Path $destDir -Force | Out-Null
    }
    Copy-Item -LiteralPath $file.FullName -Destination $destPath -Force
}

# Create the ZIP archive
Compress-Archive -Path &quot;$tempPath\*&quot; -DestinationPath $zipPath
if (-not (Test-Path $zipPath)) {
    Write-Host &quot;ERROR: Failed to create ZIP file.&quot; -ForegroundColor Red
    exit 1
}
Remove-Item -Recurse -Force $tempPath

# Authenticate and deploy
Connect-EpiCloud -ProjectId $projectId -ClientKey $clientKey -ClientSecret $clientSecret
$sasUrl = Get-EpiDeploymentPackageLocation
Add-EpiDeploymentPackage -SasUrl $sasUrl -Path $zipPath

Start-EpiDeployment `
    -DeploymentPackage $zipName `
    -TargetEnvironment $targetEnvironment `
    -DirectDeploy `
    -Wait `
    -Verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&lt;strong&gt;⚠️ Don&amp;rsquo;t Repeat My Mistakes&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Based on my own experience, here are some common pitfalls to avoid when deploying your application to Optimizely Hosting:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Always set all environment variables in the PaaS Portal &lt;em&gt;before&lt;/em&gt; starting the deployment.&lt;/strong&gt;&lt;br /&gt;I&amp;rsquo;ve made the mistake of launching the deployment first and then realizing the environment variables were missing. This caused the production build to fail, as the deployment process triggers a production build command right away. Without access to required environment variables, the build will break, and the environment can remain locked for a while.&lt;br /&gt;&#128073; &lt;strong&gt;Set the variables first, then deploy.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Avoid using &amp;ldquo;Send to ZIP&amp;rdquo; on Windows.&lt;/strong&gt;&lt;br /&gt;This method wraps your files inside an additional folder, which causes the package to be invalid for Optimizely Hosting. The platform expects to find files like &lt;strong&gt;package.json&lt;/strong&gt;&amp;nbsp;at the root of the ZIP &amp;ndash; it doesn&amp;rsquo;t dig deeper into nested folders.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Make sure your ZIP includes all necessary files.&lt;/strong&gt;&lt;br /&gt;When working with frameworks like Next.js, routing is often handled via special folder structures like route groups &lt;strong&gt;(draft)&lt;/strong&gt; or dynamic routes &lt;strong&gt;article/[slug]&lt;/strong&gt;. If you use an automation script, sometimes files like &lt;strong&gt;page.tsx&lt;/strong&gt;&amp;nbsp;don&#39;t get copied correctly. Double-check your ZIP content before uploading.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Follow the required ZIP naming convention:&lt;/strong&gt;&lt;br /&gt;Use the format: &lt;strong&gt;&amp;lt;name&amp;gt;.head.app.&amp;lt;version&amp;gt;.zip&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Don&amp;rsquo;t include node_modules or .next folders in your ZIP.&lt;/strong&gt;&lt;br /&gt;These are unnecessary for the deployment and will significantly increase your file size, slowing down the upload process.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;Result:&amp;nbsp;&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;After completing the above steps, the deployment process will begin. It usually takes just a few minutes to complete.&lt;/p&gt;
&lt;p&gt;You can monitor the deployment status directly in the &lt;strong&gt;Optimizely admin panel&lt;/strong&gt;, as shown in the screenshot below:&lt;/p&gt;
&lt;p&gt;Once the deployment finishes successfully, your application will be live and ready to use. The status will indicate that the deployment was successful, confirming that everything went smoothly.&lt;/p&gt;
&lt;h2&gt;&lt;br /&gt;&lt;img src=&quot;/link/0c9445cabdbc4621948f095cad29195b.aspx&quot; /&gt;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&lt;strong&gt;Coming Soon: GitHub CI/CD Integration&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;In the near future, I&amp;rsquo;ll publish a guide showing how to &lt;strong&gt;integrate this deployment flow with GitHub Actions&lt;/strong&gt;, enabling automatic deployment to Optimizely Frontend Hosting on every merge to the&amp;nbsp;&lt;strong&gt;main&amp;nbsp; &lt;/strong&gt;branch.&lt;/p&gt;
&lt;p&gt;Stay tuned!&lt;/p&gt;</id><updated>2025-06-25T12:49:54.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Common Mistakes in Headless Projects with Optimizely</title><link href="https://world.optimizely.com/blogs/szymon-uryga/dates/2025/5/common-mistakes-in-headless-projects-with-optimizely/" /><id>&lt;p&gt;Adopting a headless architecture with Optimizely is a major shift from the traditional MVC-based development that has been the standard for years. While the flexibility and performance of headless setups are often worth the transition, many teams make avoidable mistakes during their first headless implementations. In this post, I&amp;rsquo;ll highlight some of the most common issues I&amp;rsquo;ve seen - and how to avoid them.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;1. Overemphasis on PaaS vs. SaaS&lt;/h2&gt;
&lt;p&gt;One of the first decisions teams face is whether to use Optimizely&#39;s PaaS (Platform as a Service) or SaaS (Software as a Service) version. Unfortunately, many technical teams get stuck in the weeds of this decision.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The reality?&lt;/strong&gt; From a headless application perspective, both versions ultimately store and serve content via &lt;strong&gt;Optimizely Graph&lt;/strong&gt;. That&amp;rsquo;s what your frontend connects to. So the technical differences are minimal in the context of a headless frontend.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What matters more&lt;/strong&gt; is whether the SaaS version meets all your business and integration requirements. If you need heavy customizations, advanced workflows, or .NET-based extensions, then PaaS might be necessary. Otherwise, SaaS can offer faster onboarding, better scalability, and less maintenance.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;2. Ignoring Auto-Generated Types from Optimizely Graph&lt;/h2&gt;
&lt;p&gt;Optimizely Graph supports &lt;strong&gt;code generation (codegen)&lt;/strong&gt;, which allows developers to generate strongly typed classes based on your content model.&lt;/p&gt;
&lt;p&gt;Yet, in many projects, teams skip this and manually type out GraphQL queries and expect responses as raw JSON. This leads to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Guesswork about what fields are available&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Increased chance of typos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fragile code due to schema mismatches&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Slower development and debugging&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Recommendation: &lt;/strong&gt;Use the codegen tools provided by Optimizely Graph to automatically generate TypeScript (or your preferred language) types. This gives developers instant access to the schema, autocomplete, and type checking &amp;mdash; significantly reducing runtime bugs and increasing productivity.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;3. Header and Footer Misplaced as Homepage Blocks&lt;/h2&gt;
&lt;p&gt;A common anti-pattern in headless Optimizely projects is placing global components like the &lt;strong&gt;header and footer &lt;/strong&gt;directly on the homepage as blocks. While it may seem convenient, it creates serious architectural problems.&lt;/p&gt;
&lt;p&gt;Why it&amp;rsquo;s a problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Webhooks only detect changes to the homepage, so &lt;strong&gt;header/footer changes won&amp;rsquo;t trigger cache invalidation&lt;/strong&gt; unless the entire homepage is republished.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The frontend is forced to &lt;strong&gt;rebuild or refetch the entire homepage&lt;/strong&gt;, even if only the footer changes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It creates a tight coupling between unrelated content.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Recommendation: &lt;/strong&gt;Treat the header and footer as &lt;strong&gt;independent content items&lt;/strong&gt; in the CMS - for example, create two separate pages: &lt;em&gt;SiteHeader &lt;/em&gt;and &lt;em&gt;SiteFooter&lt;/em&gt;. This allows fine-grained cache invalidation, better content reuse, and simpler frontend logic.&lt;/p&gt;
&lt;h2&gt;4. Exposing Internal CMS URLs&lt;/h2&gt;
&lt;p&gt;Another common mistake is exposing &lt;strong&gt;internal CMS URLs &lt;/strong&gt;(e.g., for images or links) directly in frontend code. This happens when frontend apps render URLs exactly as returned by Optimizely - which often point to internal environments or CMS instances.&lt;/p&gt;
&lt;p&gt;This is both a &lt;strong&gt;security issue&lt;/strong&gt; and a &lt;strong&gt;performance problem&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;URLs may break if environments change&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Internal infrastructure is exposed to the public&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Recommendation: &lt;/strong&gt;Always serve media and links via a &lt;strong&gt;frontend-friendly abstraction layer&lt;/strong&gt;. Ideally, images and files should be proxied through a CDN or asset management service. Any CMS-specific URLs should be sanitized or mapped before being passed to the client.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Headless CMS development offers flexibility, scalability, and performance - but it comes with new patterns and pitfalls. Many of the issues listed here stem from applying traditional CMS thinking to a modern headless setup.&lt;/p&gt;
&lt;p&gt;If you&#39;re starting a headless project with Optimizely, take time to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Think in terms of APIs, not pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Embrace tooling like GraphQL codegen.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Decouple global content from page-specific content.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Abstract all internal CMS data behind your frontend.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By avoiding these common mistakes, you&amp;rsquo;ll not only improve the developer experience but also deliver faster, more maintainable digital experiences.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</id><updated>2025-05-19T11:53:07.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>An Introduction to Composable Commerce Architecture with Optimizely </title><link href="https://world.optimizely.com/blogs/szymon-uryga/dates/2025/4/an-introduction-to-composable-commerce-architecture-with-optimizely-/" /><id>&lt;p&gt;In today&#39;s fast-moving digital economy, businesses need flexibility, speed, and the ability to adapt their commerce ecosystems rapidly. Traditional monolithic platforms often struggle to keep pace with changing market demands and evolving customer expectations. This is where composable commerce architecture emerges as a game-changer.&lt;/p&gt;
&lt;p&gt;As a professional specializing in modern commerce architectures, I&#39;ve successfully delivered composable commerce solutions and conducted extensive research into optimizing these systems with various platforms. Recently, I&#39;ve been particularly focused on exploring Optimizely&#39;s powerful capabilities in this space, developing several advanced demonstration implementations that showcase its potential. Through this work, I&#39;ve gained valuable insights into architectural approaches that leverage Optimizely&#39;s strengths for composable commerce. Let me share what I&#39;ve learned about building effective, enterprise-ready composable commerce solutions with Optimizely.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s start with the basics.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;What is Composable Commerce?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Composable commerce is all about building your ideal system by combining best-in-class services for each specific business need. Rather than relying on a single platform to handle everything, you select specialized tools - such as a CMS, product database, checkout system - and connect them through APIs.&lt;/p&gt;
&lt;p&gt;The key principles behind composable commerce include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Best-of-breed approach&lt;/strong&gt;: Selecting the optimal services for each specific function instead of using a single all-in-one platform&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;API-first connectivity&lt;/strong&gt;: Connecting everything through APIs and utilizing headless frontends for maximum flexibility&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Business agility&lt;/strong&gt;: Innovating faster, scaling easily, and avoiding vendor lock-in&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Simply put, composable commerce enables businesses to move faster, experiment with new ideas more easily, and remain adaptable to whatever the market demands.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;How Optimizely Enables Composable Commerce&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Optimizely plays a crucial role in building robust composable commerce solutions. Through its SaaS CMS and PaaS CMS, Optimizely Graph, and other services, it provides powerful tools for managing content, products, and transactional data in a flexible, scalable way.&lt;/p&gt;
&lt;p&gt;Let&#39;s explore two different architectural approaches to implementing composable commerce with Optimizely. These examples differ primarily in whether the Optimizely Connect Platform is used as an integration hub, each with its own advantages and trade-offs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Traditional Composable Commerce (Without Optimizely Connect Platform)&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;img src=&quot;/link/d861e4d7d0e04a0cbab92da4dc8ab438.aspx?1747646433342&quot; width=&quot;1200&quot; height=&quot;876&quot; /&gt;&lt;br /&gt;&lt;br /&gt;This architecture represents a more traditional approach to composable commerce, where Optimizely SaaS CMS, Optimizely Graph, and transactional commerce services are integrated directly without a central integration hub. In this model, the headless application acts as the orchestrator that combines all services.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&#128994; Product Information Management (PIM)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The PIM system enriches product data beyond what&#39;s typically supported by SaaS commerce providers, handling both core product data and extended attributes stored as metafields,for example including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Subscription Flag&lt;/strong&gt;: Determines if a product is eligible for subscription&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Default Variant SKU&lt;/strong&gt;: Defines which variant should be preselected&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Group-Based Pricing&lt;/strong&gt;: Stored as a JSON object for dynamic price rendering based on user groups&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Product Status&lt;/strong&gt;: JSON field for availability (e.g., backorder, out of stock)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Order Limits&lt;/strong&gt;: Restrictions on how many items a customer can order within a specific timeframe&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All this enriched data is sent to the transactional commerce system and consumed by the frontend application.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&#128992; Transactional Commerce Services&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;This layer manages all commerce operations including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Order Processing &amp;amp; Checkout&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Inventory and Pricing&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Customer Account Data&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Payment Gateway Configuration&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Promotions &amp;amp; Discount Logic&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Marketing Automation (e.g., abandoned carts)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Category Management and Filtering&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It exposes APIs that allow the headless frontend to interact with real-time data for carts, customer orders, and stock availability.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&#128309; Optimizely Graph&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Optimizely Graph functions as a high-performance GraphQL content delivery platform, acting as a CDN-distributed index for all CMS content and localized product information (titles, descriptions, media, etc.).&lt;/p&gt;
&lt;p&gt;It delivers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CMS Pages and Product Content Blocks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Localized translations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SEO metadata&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Product descriptions and assets tied to SKU/product IDs&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All this data is accessible by the frontend using GraphQL, enabling dynamic and multilingual rendering.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&#128309; Optimizely SaaS CMS&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;This is the content management service used by marketers and editors. It includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A Visual Builder for assembling reusable content components&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Content publishing workflows&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Management of content blocks, pages, and translations&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All published content is indexed in Optimizely Graph for frontend consumption.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&#128995; Headless Application&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The headless frontend (built in a framework like Next.js) orchestrates data across services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Retrieves CMS pages and product content from Optimizely Graph&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fetches inventory, pricing, and promotion details from transactional commerce APIs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Manages cart and checkout flows using commerce services&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Renders localized product detail pages by combining data from both CMS and transactional commerce systems&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;Product Page Rendering:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Request commerce service for product data (SKU, price, inventory, status)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use the SKU or product ID to query Optimizely Graph for multilingual CMS content (title, description, media)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Cart Rendering:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Dynamically loads product title, image, and description from Optimizely Graph using SKU or variant ID&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Category Pages:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Retrieved from commerce backend, then enriched with CMS data (if available)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;&lt;strong&gt;Integration Logic&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PIM &amp;rarr; Transactional Commerce&lt;/strong&gt;: Sends core product data + metafields&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SaaS CMS &amp;rarr; Optimizely Graph&lt;/strong&gt;: Publishes content to the GraphQL index&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Frontend &amp;rarr; Optimizely Graph&lt;/strong&gt;: Loads CMS pages and multilingual product info&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Frontend &amp;rarr; Commerce Services&lt;/strong&gt;: Fetches real-time data like cart contents, prices, promotions, and inventory&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can see an example implementation of this approach using Optimizely SaaS CMS with Shopify at&lt;a href=&quot;https://optimizely-one-nu.vercel.app/&quot;&gt; https://optimizely-one-nu.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Composable Commerce with Optimizely Connect Platform&amp;nbsp;&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Now let&#39;s explore how the architecture changes when we leverage the Optimizely Connect Platform as a central integration hub.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;What is the Optimizely Connect Platform (OCP)?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The Optimizely Connect Platform (OCP) empowers developers to streamline data ingestion and channel activation by creating connectors that integrate seamlessly with any marketing suite. Developers can publish these connectors to the App Directory, and marketers can easily install and use them to compose digital experiences. - &lt;a href=&quot;/link/4ca2b24d63b344bc845aacd96cd88290.aspx&quot;&gt;https://world.optimizely.com/products/ocp/overview/&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;In essence, OCP allows you to create applications that take information from transactional commerce services and add it to Optimizely Graph. It serves as a hub that aggregates data from multiple sources into one cohesive system.&lt;/p&gt;
&lt;p&gt;Let&#39;s move on to architecture where we will learn about the strength of the Optimizely Connect Platform&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/692d1e0c3a0840fa9d57d5f8cf82b056.aspx?1747646410084&quot; width=&quot;1200&quot; height=&quot;476&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This architecture leverages Application in Optimizely Connect Platform as a central integration hub between content and commerce systems.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&#128995; PIM (Product Information Management)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The PIM system handles the same product data enrichment as in the previous architecture, managing attributes like subscription flags, default variant SKUs, group-based pricing, product status, and order limits.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&#128992; Transactional Commerce Services&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;This layer handles all core commerce operations, including order processing, inventory management, checkout and payment gateways, customer data, marketing campaigns, and product categorization, filtering, and promotion logic.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&#128995;&amp;nbsp; Optimizely Connect Platform (Integration Hub)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Application in The Optimizely Connect Platform acts as an integration layer between commerce and content systems. It:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Listens to webhooks from the commerce layer (e.g., product updates)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Synchronizes product and content data into Optimizely Graph&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Updates the content stored in Optimizely SaaS CMS&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes it easy to keep the product catalog and content in sync without manual intervention.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&#128309; Optimizely Graph&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Serves as the central source of truth for the frontend, containing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Product data&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CMS content&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Searchable indexes&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The headless frontend communicates with this API to render all pages dynamically and efficiently.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&#128994; Optimizely SaaS CMS&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Allows content authors to create and manage content using the Visual Builder. Published content is automatically indexed in Optimizely Graph, making it available to the frontend without delay.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;⚫ Headless Application (Frontend)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The frontend is fully headless and consumes all data via APIs. It:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Takes product information from only one place - Optimizely Graph&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fetches CMS, product, and category pages from Optimizely Graph&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Handles cart, checkout, and order history by communicating with the commerce layer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Uses identifiers like productId, variantId, and SKU to render product data&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;Data Flow Summary&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PIM &amp;rarr; Commerce&lt;/strong&gt;: Product data and metafields are synced&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Commerce &amp;rarr; Connect:&lt;/strong&gt; Webhooks notify changes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Connect &amp;rarr; Graph &amp;amp; CMS:&lt;/strong&gt; Data is synced into Optimizely&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CMS &amp;rarr; Graph:&lt;/strong&gt; Content is indexed&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Frontend &amp;rarr; Graph:&lt;/strong&gt; Frontend fetches all content and product data via GraphQL&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Frontend &amp;harr; Commerce:&lt;/strong&gt; Handles user interactions like checkout, login and cart&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Comparison: With OCP vs. Without OCP&lt;/strong&gt;&lt;/h2&gt;
&lt;div align=&quot;left&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; border: 2px solid rgb(0, 0, 0); background-color: rgb(255, 255, 255); width: 71.307%; height: 447.938px;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 15.435%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Feature&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 46.1964%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Without OCP&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 38.3702%; height: 47.5938px;&quot;&gt;
&lt;p&gt;With OCP&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 15.435%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Data Ownership&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 46.1964%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Split: PIM, Commerce, CMS all manage data&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 38.3702%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Centralized: Optimizely Graph owns all product data&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 67.1875px;&quot;&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 15.435%; height: 67.1875px;&quot;&gt;
&lt;p&gt;Commerce Data Flow&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 46.1964%; height: 67.1875px;&quot;&gt;
&lt;p&gt;PIM &amp;rarr; Commerce &amp;rarr; Headless App&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 38.3702%; height: 67.1875px;&quot;&gt;
&lt;p&gt;PIM &amp;rarr; Commerce &amp;rarr; OCP &amp;rarr; Graph &amp;rarr; Headless App&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 15.435%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Revalidation Speed&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 46.1964%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Fast (direct read from Commerce &amp;amp; Graph)&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 38.3702%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Slower (delayed by syncs from PIM via OCP)&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 15.435%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Cache Invalidation&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 46.1964%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Manual or real-time via CMS/Commerce hooks&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 38.3702%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Delayed due to webhook-based multi-step sync&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 15.435%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Dev Complexity&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 46.1964%; height: 47.5938px;&quot;&gt;
&lt;p&gt;High &amp;ndash; headless app merges two APIs&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 38.3702%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Low &amp;ndash; one API (Graph) handles CMS/product info&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 15.435%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Sync Complexity&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 46.1964%; height: 47.5938px;&quot;&gt;
&lt;p&gt;High &amp;ndash; headless app needs to listen to changes from two sources&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 38.3702%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Low &amp;ndash; one API (Graph)&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 15.435%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Real-time Accuracy&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 46.1964%; height: 47.5938px;&quot;&gt;
&lt;p&gt;High &amp;ndash; data is fetched live&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 38.3702%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Lower &amp;ndash; sync delays might cause stale content&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 15.435%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Scalability&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 46.1964%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Moderate &amp;ndash; more API calls from frontend&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border-color: rgb(0, 0, 0); border-width: 2px; width: 38.3702%; height: 47.5938px;&quot;&gt;
&lt;p&gt;High &amp;ndash; Graph acts as single fast CDN source&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Pros of the Hub-based Architecture:&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Central source of truth&lt;/strong&gt; &amp;ndash; all data is stored in the Optimizely Graph&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Simplified frontend&lt;/strong&gt; &amp;ndash; all product and CMS data comes from Optimizely Graph&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Better control&lt;/strong&gt; over content and data ownership across teams&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;More scalable&lt;/strong&gt; due to optimized data serving from Graph/CDN&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Easier to manage&lt;/strong&gt; localization and content enrichment&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;Cons of the Hub-based Architecture:&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Slower cache revalidation&lt;/strong&gt; &amp;ndash; changes need to pass through multiple systems before appearing in the frontend&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data freshness challenges&lt;/strong&gt; &amp;ndash; frontend content may be affected by delays in webhook chains&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Transactional Commerce Services&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Both architectures support a wide range of modern SaaS commerce platforms, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Shopify&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BigCommerce&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;commercetools&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Commerce Layer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Salesforce Commerce Cloud&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;And many others&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While the core functionality of these platforms is similar, there are subtle differences in data models and APIs. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Shopify &lt;/strong&gt;does not expose a product SKU directly. Instead, SKUs exist only at the variant level.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BigCommerce&lt;/strong&gt;, on the other hand, supports both product-level SKUs and variant SKUs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Different commerce platforms may have varying data models, but the &lt;strong&gt;overall architecture remains consistent&lt;/strong&gt; regardless of the specific platform chosen.&lt;/p&gt;
&lt;h2&gt;Real-World Example: Custom Composable Commerce Architecture&lt;/h2&gt;
&lt;p&gt;Let me share a more complex real-world implementation that we at &lt;a title=&quot;Hatimeria &quot; href=&quot;https://www.hatimeria.com/&quot;&gt;Hatimeria&lt;/a&gt; developed for a client. This architecture demonstrates how composable commerce can be customized to meet specific business needs:&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp;&lt;img src=&quot;/link/848cfd62dd354003b76da793d76b4df8.aspx?1747646375099&quot; width=&quot;1200&quot; height=&quot;490&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The following example illustrates:&lt;/p&gt;
&lt;ul&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;PIM (Pimcore)&lt;/strong&gt;: Stores enriched product data in BigCommerce metafields - availability, variants, pricing per user group, inventory, status, subscription flag, loyalty points, alternatives, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;SaaS CMS&lt;/strong&gt;: Single source of truth for product content (title, images, description), CMS pages, blocks, and transactional content. Product content linked via SKU.&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;BigCommerce&lt;/strong&gt;: Core commerce engine - customer data, cart logic, checkout setup, payment, and reporting. Stores product base info and PIM metafields.&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;Checkout (BigCommerce SDK + Azure Functions)&lt;/strong&gt;: Custom checkout UI. Promo codes and final calculations are processed via Azure Functions, not native BigCommerce.&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;Promotions (.NET Engine)&lt;/strong&gt;: Custom UI for managing promotions - define conditions (customer, product, time-based), rewards (discounts, gifts, points). Stored in a database.&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;Azure Functions&lt;/strong&gt;: Handle advanced cart logic - fetch data from BigCommerce, CMS, and customer service, validate products, apply promotions, and update cart metafields.&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;Algolia&lt;/strong&gt;: Fast search and category results. Index updated hourly and via real-time webhooks from CMS and BigCommerce.&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;Headless App (Next.js)&lt;/strong&gt;: Orchestrates frontend - content from CMS, products merged via SKU from BigCommerce + CMS, search from Algolia, cart/promo logic via Azure. Checkout handled on separate domain via BigCommerce SDK.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;Data Flow Highlights&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;PIM ➜ BigCommerce &lt;/strong&gt;(metafields)&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;CMS ➜ Headless App&lt;/strong&gt; &amp;nbsp;(content, product info)&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;BigCommerce ➜ Headless App&lt;/strong&gt; (products, cart)&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;Headless App ➜ Azure Functions&lt;/strong&gt; (cart calculation &amp;amp; validation)&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;Azure Functions ➜ BigCommerce&lt;/strong&gt; (cart metafield updates)&lt;/p&gt;
&lt;/li&gt;
&lt;li class=&quot;&quot;&gt;
&lt;p class=&quot;&quot;&gt;&lt;strong&gt;Webhooks (BigCommerce/CMS) ➜ Headless App ➜ Algolia&lt;/strong&gt; (real-time reindex)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This modular setup ensures agility, enables deep customization at every stage of the customer journey, and supports a real-time, high-performance commerce experience.&lt;br /&gt;Implementing such an architecture wasn&amp;rsquo;t easy - it required significant effort and deep technical alignment across services. But to stay ahead of the competition and meet ambitious business requirements, such as fully custom promotion logic that no out-of-the-box platform could support, this level of complexity was essential. &lt;strong&gt;After all, this shouldn&amp;rsquo;t be simple &amp;mdash; this is business critic&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Without Optimizely Connect Platform:&lt;/strong&gt; Offers greater flexibility and real-time access to data but can be more complex and harder to manage, especially when dealing with multiple APIs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;With Optimizely Connect Platform:&lt;/strong&gt; Simplifies integration, centralizes data management, and enhances scalability, though there may be some delay in data synchronization.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both approaches are powerful, and the choice depends on the specific needs of your business and the complexity of the system you&amp;rsquo;re building.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;The architectures discussed demonstrate the power and flexibility of building composable commerce solutions with Optimizely. By separating concerns between Product Information Management, Transactional Commerce Services, Optimizely Graph, and a Headless Application, we can create highly scalable, customizable, and performant ecosystems tailored exactly to business needs.&lt;/p&gt;
&lt;p&gt;While the diagrams presented here are conceptual and simplified for clarity, in real-world scenarios many more factors must be considered, including authentication, caching, synchronization challenges, failover strategies, and others. However, the core beauty of composable architecture remains clear: it allows us to easily integrate new services, extend features from different providers, and orchestrate everything within a single, unified headless application.&lt;/p&gt;
&lt;p&gt;The&lt;strong&gt; Optimizely Connect Platform&lt;/strong&gt; can be an &lt;strong&gt;incredibly powerful tool &lt;/strong&gt;in this landscape, offering a modern way to stitch together content, commerce, and search across distributed systems with great performance and flexibility.&lt;/p&gt;
&lt;p&gt;Combining multiple systems is never trivial - it requires deep technical expertise, a strong architectural vision, and the ability to navigate integration challenges. But when executed properly, it unlocks tremendous value and creates best-in-class digital experiences.&lt;/p&gt;
&lt;h2&gt;Explore More&lt;/h2&gt;
&lt;p&gt;If you&#39;d like to see composable commerce with Optimizely in action, check out these resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://opti-sass.vercel.app/&quot;&gt;Composable Commerce with Feature Experimentation&lt;/a&gt; - See how feature flags can help you test different commerce services (Shopify vs BigCommerce)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://optimizely-one-nu.vercel.app/&quot;&gt;Optimizely SaaS CMS with Shopify&lt;/a&gt; - A working example of the first architecture&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://opti-masterclass.vercel.app/&quot;&gt;Headless using Optimizely SaaS CMS course&lt;/a&gt; - Learn how to get started&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://optimizely-masterclass.vercel.app/&quot;&gt;Content-rich website using Optimizely SaaS CMS&lt;/a&gt; - See a simple implementation&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</id><updated>2025-04-30T13:01:23.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>A Free Course for Building Headless Applications with Next.js and Optimizely SaaS CMS</title><link href="https://world.optimizely.com/blogs/szymon-uryga/dates/2025/3/introducing-the-optimizely-masterclass-a-free-self-paced-course-for-building-headless-applications-with-next.js" /><id>&lt;p&gt;I am excited to announce the transformation of Optimizely Headless CMS webinar into a comprehensive, &lt;strong&gt;completely free&lt;/strong&gt; self-paced course that&#39;s available to everyone - &lt;strong&gt;no registration required&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a title=&quot;Visit the Optimizely Masterclass website now&amp;nbsp;&quot; href=&quot;https://opti-masterclass.vercel.app&quot;&gt;Visit the Course website now&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/86c3bc71343442938c2ec7405bfb5d3a.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;From Time-Limited Webinar to Always-Available Learning Resource&lt;/h2&gt;
&lt;p&gt;What started as a one-time webinar has evolved into something much more valuable: a permanent, accessible learning resource for developers of all skill levels. I&#39;ve reimagined the content to create a step-by-step guide that allows you to learn at your own pace, whenever it fits your schedule.&lt;/p&gt;
&lt;p&gt;The best part? &lt;strong&gt;No registration forms, no email collection, no barriers to entry&lt;/strong&gt;. Simply visit the site and start learning immediately.&lt;/p&gt;
&lt;h2&gt;Learn to Build Scalable Headless Applications with Next.js and Optimizely SaaS CMS&lt;/h2&gt;
&lt;p&gt;The course guides you through building a modern, high-performance website using Next.js and Optimizely&#39;s SaaS CMS. Whether you&#39;re new to headless architecture or looking to refine your skills, this course provides clear, practical instruction for developers at every level.&lt;/p&gt;
&lt;p&gt;Here&#39;s what you&#39;ll learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setting up a Next.js project with Optimizely SaaS CMS&lt;/li&gt;
&lt;li&gt;Creating a flexible block factory pattern for content components&lt;/li&gt;
&lt;li&gt;Implementing efficient image loading and optimization&lt;/li&gt;
&lt;li&gt;Building dynamic routes with Next.js App Router&lt;/li&gt;
&lt;li&gt;Designing responsive layouts with v0&lt;/li&gt;
&lt;li&gt;Modeling content effectively in Optimizely CMS&lt;/li&gt;
&lt;li&gt;Writing optimized GraphQL queries for content delivery&lt;/li&gt;
&lt;li&gt;Implementing multi-language support and localization&lt;/li&gt;
&lt;li&gt;Handling preview mode&lt;/li&gt;
&lt;li&gt;Implementing Visual Builder support&lt;/li&gt;
&lt;li&gt;Mastering cache revalidation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each concept is explained with practical code examples that you can immediately apply to your own projects.&lt;/p&gt;
&lt;h2&gt;Follow Along with GitHub: Start from Any Point in the Journey&lt;/h2&gt;
&lt;p&gt;One of the most powerful features of this course is its companion GitHub repository, where each step is meticulously documented on a separate branch. This innovative approach allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jump in at any point in the learning process&lt;/li&gt;
&lt;li&gt;Compare your code with working examples&lt;/li&gt;
&lt;li&gt;See exactly how the application evolves from start to finish&lt;/li&gt;
&lt;li&gt;Experiment with different approaches without breaking your progress&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br /&gt;If you get stuck or want to skip ahead, simply check out the corresponding branch and continue your learning journey without missing a beat.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/7e944ad51a894274813f02bfd1949cf4.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Designed for Real-World Application&lt;/h2&gt;
&lt;p&gt;This isn&#39;t just a theoretical exercise - you&#39;ll build a complete, production-ready website that showcases the power of headless architecture. The course emphasizes best practices for performance, scalability, and maintainability, ensuring that what you learn can be directly applied to your professional projects.&lt;/p&gt;
&lt;p&gt;By the end of the course, you&#39;ll have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A fully functional Next.js website powered by Optimizely SaaS CMS&lt;/li&gt;
&lt;li&gt;A deep understanding of headless architecture principles&lt;/li&gt;
&lt;li&gt;Practical experience with modern development workflows&lt;/li&gt;
&lt;li&gt;Code patterns you can reuse in future projects&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Start Learning Today - No Strings Attached&lt;/h2&gt;
&lt;p&gt;I believe in removing barriers to education, which is why this course is completely free and requires no registration. You won&#39;t need to create an account, provide your email, or jump through any hoops to access the full content.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a title=&quot;Visit the Optimizely Masterclass website now&amp;nbsp;&quot; href=&quot;https://opti-masterclass.vercel.app&quot;&gt;Visit the Course website now&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;</id><updated>2025-03-24T19:00:05.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Graph and Next.js: Building Scalable Headless Solutions</title><link href="https://world.optimizely.com/blogs/szymon-uryga/dates/2023/11/optimizely-graph-and-next-js-building-scalable-headless-solutions/" /><id>&lt;div&gt;Optimizely Graph harnesses the capabilities of GraphQL, an intuitive and efficient query language to, transform content within an Optimizely CMS into a structured, interconnected graph. Complementing this, Next.js, a React-based frontend framework, empowers developers to build performant and SEO-friendly web applications, enabling static and dynamic rendering and many more amazing features that can enhance your headless solution.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;In this article we delve into the symbiotic relationship between Optimizely Graph and Next.js, exploring how this pairing facilitates the crafting of dynamic headless experiences, from enabling draft modes and On-Page Editing to harnessing static site generation. We&#39;ll also investigate the utilization of webhooks for efficient page revalidation, enabling seamless updates across web applications.&lt;/div&gt;
&lt;h2&gt;What is Optimizely Graph used for?&lt;/h2&gt;
&lt;div&gt;Using Optimizely Graph we are able to separate our admin panel from our application, the so called headless, since we have two separate applications that are independent of each other. Headless in Optimizely has long been possible by using the Content Delivery API, but Graph simplifies a lot of things.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;https://hatimeriacom-strapi.s3.eu-central-1.amazonaws.com/What_is_the_Optimizely_Graph_used_for_8ca91c5920.png&quot; width=&quot;600&quot; alt=&quot;What is the Optimizely Graph used for?&quot; height=&quot;307&quot; style=&quot;display: block; margin-left: auto; margin-right: auto;&quot; /&gt;&lt;/div&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h2&gt;How does Optimizely Graph work?&lt;/h2&gt;
&lt;div&gt;Optimizely Graph is a separate service that is hosted on a CDN, made fast by implementing the latest, most modern technologies using global serverless Edge computing, auto scaling microservices and the latest search engine.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;There are two ways to deliver content to the Graph service:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Scheduled job&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Event Triggering, for example publishing a page.&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;The Content Delivery API is used under the hood, delivering content to Graph. When you want to introduce something custom, such as adding business logic to a block, you can do so using an override of the serialization used in the Content Delivery API.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;In the image below we can see how the Optimizely Graph works. It all starts in the admin panel, in the picture labeled with the Optimizely logo. From there, using the Content Delivery API, the content goes to the GraphQl Server. GraphQl Server is hosted on Azure and is embedded on a CDN. The GraphQl Server combines the admin panel and frontend application used by users. The frontend application communicates directly with GraphQl Server, receiving all the necessary data.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;https://hatimeriacom-strapi.s3.eu-central-1.amazonaws.com/How_does_Optimizely_Graph_work_a603841a7d.png&quot; width=&quot;600&quot; alt=&quot;How does Optimizely Graph work?&quot; height=&quot;433&quot; style=&quot;display: block; margin-left: auto; margin-right: auto;&quot; /&gt;&lt;/div&gt;
&lt;div&gt;Source: &lt;a href=&quot;/link/386fb3fd598c4978aaf6906a1bbc1505.aspx&quot;&gt;world.optimizely.com&lt;/a&gt;&lt;/div&gt;
&lt;h2&gt;Support for Commerce&lt;/h2&gt;
&lt;div&gt;As of early December, Optimizely Graph has been officially released and is production ready. The released version supports CMS only and allows read-only options. At a recent webinar held by Optimizely, the company said that they are getting a lot of requests for Commerce support and it is in the approval stage by Project Management. Keeping in mind that commerce is much more complicated than CMS, it may take longer to work on this. In summary, currently no work is being done to implement support for commerce, and once this plan is approved, we will have to wait a while for it to be ready for use in production. However, using other Optimizely products using Content Delivery API, we are able to implement Headless Commerce. There is also an option to use Graph for CMS-related stuff, and Content Delivery API for commerce-related stuff.&lt;/div&gt;
&lt;h2&gt;Personalization&lt;/h2&gt;
&lt;div&gt;Personalization as we know it so far, i.e. creating visitor groups and adding personalization in Content Area and XHTML strings, **will not work**.&lt;/div&gt;
&lt;div&gt;Here&#39;s where other products can help, like Optimizely Web Experimentation, where you can create personalized campaigns and immediately measure which variance converts better. However, this is an additional product charged separately.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;There is another option that is being worked on: the integration of Optimizely Graph with Optimizely Data Platform RLS (Real Time Segment), which is the next generation of &amp;ldquo;Visitor Groups&amp;rdquo;. ODP is also an additional product that is charged separately.&lt;/div&gt;
&lt;h2&gt;Content Recommendations&lt;/h2&gt;
&lt;div&gt;Content Recommendation works by scraping the page source of the site. if it is valid HTML, there should be no problem with it. If we use Server Side Rendering, there is pure HTML code in the page source, so with SSR, Content Recommendations works. The problem with Content Recs is that if you choose client-side rendering, it means that non-native HTML elements such as &amp;lt;teaser-block/&amp;gt; are included in HTML, so the scraper is not able to collect the correct data.&lt;/div&gt;
&lt;h2&gt;Hosting on DXP&lt;/h2&gt;
&lt;div&gt;It is possible to continue to have the frontend application hosted on DXP, then the client app (Node.js) and the backend (.net) are served on the same address by proxying requests from .net to the Node.js process. An example of such a solution can be found &lt;a href=&quot;https://github.com/episerver/Foundation-spa-react&quot;&gt;here&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;https://hatimeriacom-strapi.s3.eu-central-1.amazonaws.com/Hosting_on_DXP_440821d35c.png&quot; width=&quot;600&quot; alt=&quot;Hosting on DXP&quot; height=&quot;241&quot; style=&quot;display: block; margin-left: auto; margin-right: auto;&quot; /&gt;&lt;/div&gt;
&lt;div&gt;Source: &lt;a href=&quot;https://github.com/episerver/content-delivery-js-sdk/tree/master/samples/music-festival-vue-coupled#backend&quot;&gt;Github&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Vercel is the recommended hosting platform for Next.js to fully leverage its capabilities, such as &lt;a href=&quot;https://vercel.com/docs/incremental-static-regeneration&quot;&gt;Incremental Static Regeneration (ISR)&lt;/a&gt; &lt;a href=&quot;https://vercel.com/templates/next.js/on-demand-incremental-static-regeneration&quot;&gt;(example of code)&lt;/a&gt;, image and font optimization. By hosting Next.js on Vercel, you can achieve better performance due to its seamless integration with Next.js and support for these advanced features.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Vercel&#39;s administration panel provides a user-friendly interface for managing deployments, making it accessible even to non-technical users. This allows for easy deployment to production after reviewing changes on staging. While there are inherent risks in allowing non-technical users to deploy changes, for small changes like increasing font size, the risks are minimal. The clear and intuitive interface of Vercel&#39;s administration panel makes it convenient for non-technical users to manage deployments with confidence.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;An example of a starter for Optimizely Feature Flags with Next.js is available for preview and use here.&lt;/div&gt;
&lt;h2&gt;Understanding Next.js Rendering Strategies&lt;/h2&gt;
&lt;div&gt;Next.js employs three main server rendering strategies:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;1. &lt;strong&gt;Static Rendering (Default)&lt;/strong&gt;: Routes are pre-rendered at build time or after data revalidation. The rendered result is cached and can be distributed via Content Delivery Networks (CDN). This strategy is suitable for static content like blog posts or product pages.&lt;/div&gt;
&lt;div&gt;2. &lt;strong&gt;Dynamic Rendering&lt;/strong&gt;: Routes are rendered for each user at request time. This method is ideal for personalized data or information specific to each user, such as cookies or search parameters.&lt;/div&gt;
&lt;div&gt;3. &lt;strong&gt;Dynamic Routes with Cached Data&lt;/strong&gt;: Next.js allows for routes with a mix of cached and uncached data. This flexibility means you can have personalized, dynamic content while benefiting from caching. For instance, an e-commerce page might use cached product data along with uncached, personalized customer information.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;These server rendering strategies offer benefits like enhanced performance, improved security and better initial page load times. Leveraging Next.js&#39;s capabilities, developers can efficiently manage and render content, allowing for highly personalized and dynamic web experiences in your headless solution.&lt;/div&gt;
&lt;h3&gt;Fetching data from Optimizely Graph&lt;/h3&gt;
&lt;div&gt;A very useful thing when working with Optimizely Graph is codegen for GraphQL Schema - `@graphql-codegen/cli`. With this tool, all we need to do is create the appropriate snippets or queries for our Optimizely Graph, and the CLI will generate a fully-typed SDK for us.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Let&amp;rsquo;s configure our codegen. First, install following dependencies:&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;npm i -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-generic-sdk @graphql-codegen/typescript-operations&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Now, we need to create a config file where we define plugins, schema source, destination file etc.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;// codegen.yaml
schema: ${OPTIMIZELY_API_URL}?auth=${OPTIMIZELY_SINGLE_KEY}
documents: &#39;./src/graphql/**/*.graphql&#39;
generates:
  &#39;./src/types/generated.ts&#39;:
    plugins:
      - &#39;typescript&#39;
      - &#39;typescript-operations&#39;
      - &#39;typescript-generic-sdk&#39;
    config:
      rawRequest: true
      avoidOptionals: true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;This configuration will make sure that our SDK and types will be generated in the &lt;strong&gt;./src/types/generated.ts&lt;/strong&gt; based on the Optimizely Graph schema and fragments and queries defined in the &lt;strong&gt;./src/graphql/*&lt;/strong&gt;.&lt;/div&gt;
&lt;div&gt;Now, let&amp;rsquo;s create our custom fetcher that we will use to query data from the Optimizely Graph.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;interface OptimizelyFetchOptions {
  headers?: Record&amp;lt;string, string&amp;gt;
  cache?: RequestCache
  preview?: boolean
}
interface OptimizelyFetch&amp;lt;Variables&amp;gt; extends OptimizelyFetchOptions {
  query: string
  variables?: Variables
}
export const optimizelyFetch = async &amp;lt;Response, Variables = {}&amp;gt;({
  query,
  variables,
  headers,
  cache = &#39;force-cache&#39;,
  preview,
}: OptimizelyFetch&amp;lt;Variables&amp;gt;): Promise&amp;lt;GraphqlResponse&amp;lt;Response&amp;gt; &amp;amp; { headers: Headers }&amp;gt; =&amp;gt; {
  const configHeaders = headers ?? {}
  if (preview) {
    configHeaders.Authorization = `Basic ${process.env.OPTIMIZELY_PREVIEW_SECRET}`
  }
  try {
    const endpoint = `${process.env.OPTIMIZELY_API_URL}?auth=${process.env.OPTIMIZELY_SINGLE_KEY}`
    const response = await fetch(endpoint, {
      method: &#39;POST&#39;,
      headers: {
        Accept: &#39;application/json&#39;,
        &#39;Content-Type&#39;: &#39;application/json&#39;,
        ...configHeaders,
      },
      body: JSON.stringify({
        ...(query &amp;amp;&amp;amp; { query }),
        ...(variables &amp;amp;&amp;amp; { variables }),
      }),
      cache,
    })
    const result = await response.json()
    return {
      ...result,
      headers: response.headers,
    }
  } catch (e) {
    if (isVercelCommerceError(e)) {
      throw {
        status: e.status || 500,
        message: e.message,
        query,
      }
    }
    throw {
      error: e,
      query,
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;The &lt;strong&gt;`process.env.OPTIMIZELY_PREVIEW_SECRET`&lt;/strong&gt; variable is a generated base64 string based on your AppKey and AppSecret credentials. For more details I recommend you to take a look at &lt;a href=&quot;https://kunalshetye.com/posts/optimizely-graph-using-appkey-appsecret/&quot;&gt;Kunal&amp;rsquo;s article&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;
&lt;div&gt;Let&amp;rsquo;s create a wrapper function around our &lt;strong&gt;`optimizelyFetch`&lt;/strong&gt; that maps the values properly to the &lt;strong&gt;`getSdk`&lt;/strong&gt; function which is an auto-generated SDK.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;async function requester&amp;lt;R, V&amp;gt;(doc: DocumentNode, vars?: V, options?: OptimizelyFetchOptions) {
  const request = await optimizelyFetch&amp;lt;R&amp;gt;({
    query: print(doc),
    variables: vars ?? {},
    ...options,
  })
  return {
    data: request.data,
    _headers: request.headers,
  }
}
export const optimizely = getSdk(requester)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;In the &lt;strong&gt;`./src/graphql`&lt;/strong&gt; (path defined in the `&lt;strong&gt;documents&lt;/strong&gt;` field in the codegen configuration file) create your first query:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;// GetContentById.graphql
query GetContentById($id: Int, $workId: Int) {
  Content(where: { ContentLink: { Id: { eq: $id }, WorkId: { eq: $workId } } }) {
    items {
      Name
      RelativePath
      ContentLink {
        Id
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;After properly configuring codegen and firing the command that generates the SDK, we get a fully-typed client.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;https://hatimeriacom-strapi.s3.eu-central-1.amazonaws.com/typed_client_89b42f0bf5.png&quot; width=&quot;490&quot; alt=&quot;fully typed client&quot; height=&quot;209&quot; style=&quot;display: block; margin-left: auto; margin-right: auto;&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;h3&gt;Draft Mode with Optimizely Graph and Next.js&lt;/h3&gt;
&lt;div&gt;With Optimzely and Next.js we can easily configure previews, drafts and on-page editing to fully take advantage of the headless architecture. In order to let Optimizely know what is the URL to our Frontend, we need to follow a few simple steps.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Firstly, go to the &lt;strong&gt;`Manage Websites`&lt;/strong&gt; section in the CMS settings. Go ahead and create a website. The important thing at this point will be to set the appropriate host names. You need to add one line for the backend application and one for the frontend application, remembering to set the Primary type for the frontend application:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;https://hatimeriacom-strapi.s3.eu-central-1.amazonaws.com/manage_websites_cms_settings_532e7bb0ed.png&quot; width=&quot;600&quot; alt=&quot;Manage websites cms settings&quot; height=&quot;512&quot; style=&quot;display: block; margin-left: auto; margin-right: auto;&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;At this point, Optimizely will know what URL to use for Preview Mode iframe and for the &lt;strong&gt;`View on website`&lt;/strong&gt; button.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Now, let&amp;rsquo;s handle displaying correct route in the Preview Mode. By default, the generated URL adds &lt;strong&gt;`/episerver/cms/content/**/*`&lt;/strong&gt; pathname to the configured host and passes it to the iframe. We can redirect all requests from that URL to our custom API handler that will turn on the &lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/configuring/draft-mode&quot;&gt;draft mode&lt;/a&gt;&amp;nbsp;for the corresponding page.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;// next.config.js
async redirects() {
  return [
    {
      source: &#39;/episerver/cms/content/:path*&#39;,
      destination: &#39;/api/draft/:path*&#39;,
      permanent: false,
    },
  ]
},&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Example: &lt;strong&gt;`EPiServer/CMS/Content/en/artists/almost-up-today,,35/`&lt;/strong&gt; is redirected to &lt;strong&gt;`api/draft/en/artists/almost-up-today,,35/`&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;&amp;nbsp;&amp;nbsp;&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;Create an &lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing/route-handlers&quot;&gt;API route&lt;/a&gt;&amp;nbsp;that will handle the draft mode:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;// api/draft/[[...content]]/route.ts
export async function GET(request: NextRequest, { params }: { params: { content: string[] } }) {
  const { searchParams } = new URL(request.url)
  const token = searchParams.get(&#39;preview_token&#39;)
  const payload = decodeToken(token)
  const contentId = payload?.contentId
  const contentWorkId = payload?.contentWorkId
  const { slugs } = getSlugs(params.content)
  if (!slugs || !contentId || !token) {
    return notFound()
  }
  const newPathname = slugs.join(&#39;/&#39;)
  const { data, errors } = await optimizely.GetContentById(
    { id: contentId, workId: contentWorkId ?? null },
    { preview: true }
  )
  if (errors) {
    const errorsMessage = errors.map((error) =&amp;gt; error.message).join(&#39;, &#39;)
    return new Response(errorsMessage, { status: 401 })
  }
  draftMode().enable()
  cookies().set(PREVIEW_TOKEN_COOKIE_NAME, token)
  redirect(`/${newPathname}`)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;The &lt;strong&gt;`[[&amp;hellip;content]]`&lt;/strong&gt; segment is a &lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing#file-conventions&quot;&gt;naming convention&lt;/a&gt;&amp;nbsp;that let&amp;rsquo;s you catch all route segments, which will be available in the params object argument.&lt;/div&gt;
&lt;div&gt;Let&amp;rsquo;s handle the draft mode in the Page component:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;export default async function HomePage() {
  const { isEnabled: isDraftModeEnabled } = draftMode()
  let pageResponse
  if (isDraftModeEnabled) {
    const preview_token = cookies().get(PREVIEW_TOKEN_COOKIE_NAME)
    if (!preview_token) {
      return notFound()
    }
    const payload = decodeToken(preview_token.value)
    const contentId = payload?.contentId ?? null
    const workId = payload?.contentWorkId ?? null
    pageResponse = await optimizely.StartPageQuery(
      { contentId, workId },
      { preview: true, cache: &#39;no-store&#39; }
    )
  } else {
    pageResponse = await optimizely.StartPageQuery()
  }
  const startPage = pageResponse.data?.StartPage?.items?.[0]
  if (!startPage) {
    return notFound()
  }
  return (
    &amp;lt;div className=&quot;container mx-auto py-10&quot;&amp;gt;
      &amp;lt;div className=&quot;w-full px-4&quot;&amp;gt;
        &amp;lt;h1 className=&quot;text-3xl font-bold mb-10 max-w-prose&quot; data-epi-edit=&quot;TeaserText&quot;&amp;gt;
          {startPage.TeaserText}
        &amp;lt;/h1&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;ContentAreaMapper blocks={startPage.MainContentArea} /&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Be careful here though - using cookies in a page component turns it to be dynamically rendered - unless it&amp;rsquo;s used in &lt;strong&gt;`if`&lt;/strong&gt; block that checks for the draft mode.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;The optimizely script to utilize On-Page Editing feature:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;import Script from &#39;next/script&#39;
import { draftMode } from &#39;next/headers&#39;
export default function RootLayout({ children }: { children: React.ReactNode }) {
  const { isEnabled: isDraftModeEnabled } = draftMode()
return (
    &amp;lt;&amp;gt;
{isDraftModeEnabled &amp;amp;&amp;amp; &amp;lt;Script src={`${process.env.OPTIMIZELY_BACKEND_URL}/episerver/cms/latest/clientresources/communicationinjector.js`} /&amp;gt;}      
  &amp;lt;html lang=&quot;en&quot;&amp;gt;
        &amp;lt;body className={inter.className}&amp;gt;
          &amp;lt;MainLayout&amp;gt;{children}&amp;lt;/MainLayout&amp;gt;
        &amp;lt;/body&amp;gt;
      &amp;lt;/html&amp;gt;
    &amp;lt;/&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;https://hatimeriacom-strapi.s3.eu-central-1.amazonaws.com/draft_mode_60b2777b4c.gif&quot; width=&quot;600&quot; alt=&quot;Draft Mode&quot; height=&quot;314&quot; style=&quot;display: block; margin-left: auto; margin-right: auto;&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;h4&gt;data-epi-edit&lt;/h4&gt;
&lt;div&gt;To enable on page editing (OPE) we need to add an attribute to the html that will enable this, you can read more about it in the &lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/on-page-editing-using-content-graph#make-a-property-editable&quot;&gt;documentation&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;container mx-auto py-10&quot;&amp;gt;
  &amp;lt;div className=&quot;w-full px-4&quot;&amp;gt;
    &amp;lt;h1 data-epi-edit=&quot;TeaserText&quot; className=&quot;text-3xl font-bold mb-10 max-w-prose&quot;&amp;gt;
      {startPage?.TeaserText}
    &amp;lt;/h1&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;ContentAreaMapper blocks={startPage?.MainContentArea} /&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp; &lt;img src=&quot;https://hatimeriacom-strapi.s3.eu-central-1.amazonaws.com/ope_8c403ea48a.png&quot; width=&quot;600&quot; alt=&quot;ope&quot; height=&quot;270&quot; style=&quot;display: block; margin-left: auto; margin-right: auto;&quot; /&gt;&lt;/div&gt;
&lt;h2&gt;Static Site Generation&lt;/h2&gt;
&lt;div&gt;Static Site Generation (SSG) is a powerful feature in Next.js that enables the pre-rendering of pages at build time. During the build process, Next.js generates HTML files for each page in your application. These HTML files are static, meaning they represent the state of the page at the time of the build. The pre-rendered HTML files can be served to users without requiring server-side rendering for every request.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;&lt;strong&gt;Benefits of Static Site Generation in Next.js:&lt;/strong&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;1. &lt;strong&gt;Improved Performance:&lt;/strong&gt;&amp;nbsp;Since the pages are pre-rendered at build time, users receive static HTML files, reducing the need for server-side processing during runtime. This leads to faster page loads and improved overall performance.&lt;/div&gt;
&lt;div&gt;3. &lt;strong&gt;SEO Optimization&lt;/strong&gt;: Search engines favor static HTML content. SSG helps improve search engine optimization (SEO) by providing crawlers with easily indexable and readable content. This can positively impact a website&#39;s search engine ranking.&lt;/div&gt;
&lt;div&gt;4. &lt;strong&gt;Lower Server Load&lt;/strong&gt;: Static pages can be served directly from a content delivery network (CDN) without the need for server-side processing. This reduces the load on the server, allowing it to handle more concurrent users efficiently.&lt;/div&gt;
&lt;div&gt;5. &lt;strong&gt;Cost Efficiency:&lt;/strong&gt; Serving static content is typically more cost-effective than dynamically generating content for each user request. With SSG, you can generate pages during the build process, reducing the need for server resources during runtime.&lt;/div&gt;
&lt;div&gt;6. &lt;strong&gt;Better User Experience:&lt;/strong&gt; Static pages load quickly, providing a smoother and more responsive user experience. Users see the content faster, leading to higher user satisfaction and engagement.&lt;/div&gt;
&lt;div&gt;7. &lt;strong&gt;CDN Compatibility:&lt;/strong&gt; Static files can be easily distributed across a content delivery network, ensuring that the content is delivered from servers geographically closer to the user. This minimizes latency and further enhances page load times.&lt;/div&gt;
&lt;div&gt;8. &lt;strong&gt;Offline Support:&lt;/strong&gt; Static pages can be easily cached, enabling offline access for users. Once the pages are loaded, they can be stored in the browser cache, allowing users to access the content even without an internet connection.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;How to Achieve This with Optimizely Graph:&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;&amp;nbsp;&amp;nbsp;&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;In Next.js, there is a method called &lt;strong&gt;&lt;a href=&quot;https://nextjs.org/docs/app/api-reference/functions/generate-static-params&quot;&gt;generateStaticParams&lt;/a&gt;&amp;nbsp;&lt;/strong&gt;that must return all static routes. To obtain this information, we need to query Optimizely Graph for all routes:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;The GraphQL query to the Content Graph:&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;query AllPages($pageType: [String]) {
  Content(locale: en, where: { ContentType: { in: $pageType } }) {
    items {
      Name
      RelativePath
      RouteSegment
      ContentType
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Implementation in TypeScript:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;import { optimizely } from &#39;@/api/optimizely&#39;
export const getStandardPagesUrls = async () =&amp;gt; {
  const pages = await optimizely.AllPages({ pageType: &#39;StandardPage&#39; })
  const pagesWithUrls = pages?.data?.Content?.items?.filter((page) =&amp;gt; !!page?.RelativePath)
  const paths = pagesWithUrls?.map((page) =&amp;gt; {
    const segments = page?.RelativePath?.split(&#39;/&#39;).filter(String)
    return {
      name: page?.Name,
      segments: segments,
      path: page?.RelativePath,
    }
  })
  return paths
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Considering that our project uses &lt;strong&gt;`[[&amp;hellip;page]]`&lt;/strong&gt; (check &lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#optional-catch-all-segments&quot;&gt;Next.js Dynamic Routes)&lt;/a&gt;, we need to split the path into a string array, such as /en/alloy-meet &amp;rArr; [&amp;rsquo;en&amp;rsquo;, &amp;lsquo;alloy-meet&amp;rsquo;]. Therefore, we perform a split operation&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;export async function generateStaticParams() {
  try {
    const paths = await getStandardPagesUrls()
    return paths?.map((path) =&amp;gt; path.segments) ?? []
  } catch (e) {
    console.error(`Failed to retrieve static pages: ${e}`)
    return []
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;In summary, Static Site Generation in Next.js offers a way to generate performant, SEO-friendly, and cost-effective websites by pre-rendering pages at build time. It leverages the benefits of static content while providing the flexibility and convenience of a dynamic web framework.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;h3&gt;Using Webhooks for Page Revalidation (ISR)&lt;/h3&gt;
&lt;div&gt;Let&#39;s start by understanding what ISR (Incremental Static Regeneration) is.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Next.js empowers you to create or update static pages *after* building your site. Incremental Static Regeneration (ISR) allows you to utilize static generation on a per-page basis, &lt;strong&gt;eliminating the need to rebuild the entire site&lt;/strong&gt;. With ISR, you can maintain the advantages of static pages while effortlessly scaling to millions.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;In essence, ISR enables you to instruct Next.js to update the cache for a specific page and use the most recent published content.&lt;/div&gt;
&lt;div&gt;Now the question arises: How do we know when the content in the content graph has been updated and returns the freshest content? This is where webhooks come into play. You can configure a webhook to inquire about content revalidation from the provided API when it&#39;s ready. The link to configure the webhook can be found here:&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/reference/create-webhookhandler&quot;&gt;Optimizely Webhook Configuration&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;On this page, you need to choose the type of authentication. I opted for Header HMAC authentication (epi-hmac xxx), and the token used here is also employed for fetching unpublished content in draft mode. The token is a combination of AppKey and AppSecret. You can learn more about it &lt;a href=&quot;https://kunalshetye.com/posts/optimizely-graph-using-appkey-appsecret/&quot;&gt;here&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;.&lt;/div&gt;
&lt;div&gt;Before configuring the webhook, we need to create an API route that will handle the revalidation logic. For us, this route is &lt;strong&gt;`api/revalidate`&lt;/strong&gt;.&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;import { optimizely } from &quot;@/api/optimizely&quot;;
import { revalidatePath } from &quot;next/cache&quot;;
export async function POST(request: Request) {
  const { searchParams } = new URL(request.url);
  const webhookSecret = searchParams?.get(&quot;cg_webhook_secret&quot;);
  if (webhookSecret !== process.env.CG_WEBHOOK_SECRET) {
    return new Response(&quot;Invalid credentials&quot;, {
      status: 401,
    });
  }
  const requestJson = await request.json();
  const docId = requestJson?.data?.docId || &quot;&quot;;
  if (docId &amp;amp;&amp;amp; docId.includes(&quot;Published&quot;)) {
    const [guid] = docId.split(&quot;_&quot;);
    try {
      const { data, errors } = await optimizely.GetContentByGuid({ guid });
      if (errors) {
        console.error(errors);
        return new Response(&quot;Error fetching content&quot;, { status: 500 });
      }
      const url = data?.Content?.items?.[0]?.RelativePath;
      if (!url) {
        return new Response(&quot;Page Not Found&quot;, { status: 400 });
      }
      const normalizeUrl = url.startsWith(&quot;/&quot;) ? url : `/${url}`;
      // Someone published new changes, flush the cache
      console.log(`Flushing cache for: ${normalizeUrl}`);
      revalidatePath(normalizeUrl);
    } catch (error) {
      console.error(&quot;Error processing webhook:&quot;, error);
      return new Response(&quot;Internal Server Error&quot;, { status: 500 });
    }
  }
  return new Response(&quot;OK&quot;, {
    status: 200,
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;&amp;nbsp;&amp;nbsp;&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;Tip:&amp;nbsp;&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;To locally test the webhook communication, you can use ngrok tunneling. It will proxy your http://localhost:3000 to https and generate a randomly assigned link. After creating it, you can use that link for webhook configuration.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;h4&gt;Webhook Configuration:&lt;/h4&gt;
&lt;div&gt;&lt;img src=&quot;https://hatimeriacom-strapi.s3.eu-central-1.amazonaws.com/webhook_configuration_b746ac9b29.png&quot; width=&quot;600&quot; alt=&quot;Webhook configuration&quot; height=&quot;645&quot; style=&quot;display: block; margin-left: auto; margin-right: auto;&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;Disabled: false
url: &amp;lt;your-url&amp;gt;/api/revalidate?cg_webhook_secret=&amp;lt;secret from env&amp;gt;
method: POST&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;&amp;nbsp;&amp;nbsp;&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;Example `requestJson`:&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;{
  timestamp: &#39;2023-11-24T13:48:58.1384743+00:00&#39;,
  tenantId: &#39;bd1b0e79e7cf4548b046292ce22ea898&#39;,
  type: { subject: &#39;doc&#39;, action: &#39;updated&#39; },
  data: { docId: &#39;7b08c7d5-7585-47e5-add7-5822da68c3ce_en_Published&#39; }
}
flushing cache for: /en&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Unfortunately, the webhook only returns the &lt;strong&gt;`GUID`&lt;/strong&gt; and not the &lt;strong&gt;`RelativePath`&lt;/strong&gt;. To extract the path, we need to query the Optimizely graph for information about the page based on the GUID.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;query GetContentByGuid($guid: String) {
  Content(where: { ContentLink: { GuidValue: { eq: $guid } } }) {
    items {
      RelativePath
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;All these components allow us to generate the entire site during production builds and return all pages from the cache. When we publish a new change to one of the pages, we only revalidate that specific page, enabling a seamless and efficient process.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;h3&gt;Pros and Cons of Optimizely Graph and Next.js Headless Solution&lt;/h3&gt;
&lt;table&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pros&amp;nbsp;&lt;/td&gt;
&lt;td&gt;Cons&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance (SSR, SSG, ISR)&lt;/td&gt;
&lt;td&gt;Additional layer of complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uses Edge&lt;/td&gt;
&lt;td&gt;No support for Commerce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uses latest technology&lt;/td&gt;
&lt;td&gt;A more expensive solution than the traditional Hybrid in terms of development&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attractive for devs&lt;/td&gt;
&lt;td&gt;Publishing content in CMS does not equate to an immediate change on the site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good SEO&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A single source for content CMS -&amp;gt; web, mobile app&amp;hellip;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Optimizely Graph has several advantages, including improved performance through server-side rendering (SSR) and static site generation (SSG). It leverages edge computing technology and utilizes the latest technology stack, making it attractive for developers. It also offers good search engine optimization (SEO) capabilities and provides a centralized content management system (CMS) for managing content across web and mobile applications.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;However, there are some drawbacks to consider. Implementing the solution adds an additional layer of complexity to the development process. It currently lacks support for commerce functionality. The development costs can be higher compared to traditional hybrid solutions. It&#39;s also important to note that publishing content in the CMS does not guarantee immediate changes on the website.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;In summary, while the solution offers performance benefits, modern technology and a centralized content management system, it also presents complexities, limitations in commerce support and higher development costs. It&#39;s crucial to assess these factors based on specific requirements before deciding on its adoption.&lt;/div&gt;
&lt;h2&gt;Best Practices and Tips&lt;/h2&gt;
&lt;h4&gt;&lt;strong&gt;Avoid using Apollo Client&lt;/strong&gt;&lt;/h4&gt;
&lt;div&gt;Avoid using Apollo Client in your Next.js project. While it is a powerful GraphQL client, Next.js provides robust native support for fetching data using the built-in &lt;strong&gt;`fetch`&lt;/strong&gt; function. Apollo Client, being feature-rich, can introduce unnecessary overhead to your project.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Next.js comes with several built-in features that make it well-suited for handling GraphQL requests. It provides extensive capabilities, such as cache tags and revalidation, making it unnecessary to rely on external heavyweight libraries like Apollo Client.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;By sticking to Next.js native features and optimizing your GraphQL requests using the built-in fetch function, you can keep your project lightweight, efficient, and well-aligned with the framework&#39;s best practices.&lt;/div&gt;
&lt;h4&gt;&lt;strong&gt;&amp;nbsp;&amp;nbsp;&lt;/strong&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;strong&gt;Use Tailwind CSS&lt;/strong&gt;&lt;/h4&gt;
&lt;div&gt;I recommend using &lt;a href=&quot;https://tailwindcss.com/&quot;&gt;Tailwind CSS&lt;/a&gt;&amp;nbsp;as it provides an optimal solution tailored for Next.js. Tailwind CSS generates all styles during the build process into a single file, which is typically very small (in our Next.js projects, the CSS file is around 16-30kb). This approach enhances performance and aligns well with Next.js conventions.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;h4&gt;&lt;strong&gt;UI Librabry&lt;/strong&gt;&lt;/h4&gt;
&lt;div&gt;If you&#39;re in search of a UI library with versatile components, we highly recommend choosing &lt;a href=&quot;https://ui.shadcn.com/&quot;&gt;Shadcn UI&lt;/a&gt;. It has proven to be an excellent choice in several of our projects. Shadcn UI offers extensive customization options and has consistently met all our requirements.&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://www.hatimeria.com/blog/article/react-and-shadcn-ui-a-match-made-in-heaven&quot;&gt;More about shadcn&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;h4&gt;&lt;strong&gt;Use Codegen&lt;/strong&gt;&lt;/h4&gt;
&lt;div&gt;Utilizing code generation alongside Optimizely Graph can save a significant amount of time. This is because you won&#39;t need to manually type page or block types. Code generation also creates TypeScript methods for interacting with Optimizely Graph. All you have to do is create a GraphQL query and run the code generation command. Notably, you can pass your custom fetcher, enabling customization according to your requirements.&lt;/div&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;div&gt;In conclusion, the integration of Optimizely Graph and Next.js offers a powerful solution for building scalable headless applications. By leveraging GraphQL capabilities through Optimizely Graph, developers can structure and interconnect content within Optimizely CMS in a more efficient and intuitive manner. This is complemented by Next.js, a React-based frontend framework that enables the creation of performant and SEO-friendly web applications, supporting both static and dynamic rendering.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;The symbiotic relationship between Optimizely Graph and Next.js allows for the crafting of dynamic headless experiences. This includes features like draft modes, On-Page Editing, static site generation, and efficient page revalidation through webhooks.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;In summary, the Optimizely Graph and Next.js integration provides a robust foundation for developing scalable, performant, and SEO-friendly headless applications, with the flexibility to adapt to various use cases and requirements.&lt;/div&gt;</id><updated>2023-11-27T15:52:03.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How to configure Alpine.js and Tailwind CSS with Optimizely</title><link href="https://world.optimizely.com/blogs/szymon-uryga/dates/2023/10/how-to-configure-alpine-js-and-tailwind-css-with-optimizely/" /><id>&lt;h2&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Why Tailwind CSS and Alpine.js&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Let&#39;s start with a definition for those who haven&#39;t heard of these technologies yet:&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tailwind CSS&lt;span style=&quot;font-weight: 400;&quot;&gt; is a popular utility-first CSS framework that provides a set of predefined classes to quickly build responsive and customizable user interfaces.&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Alpine.js&lt;span style=&quot;font-weight: 400;&quot;&gt; is a lightweight (&lt;/span&gt;&lt;a href=&quot;https://bundlephobia.com/package/alpinejs@3.12.1&quot;&gt;14.1kB&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;) JavaScript framework that focuses on providing a declarative syntax for building interactive web interfaces.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;When we create an online store we usually don&#39;t need much logic for generic pages where we display the blocks themselves. In this case using frameworks like Vue.js or React is unnecessary because .net is a modern technology and we can do everything without using JavaScript just by using the styles in Razor pages. With that approach we have a great chance of passing Core Web Vitals without worrying about them, because of the much smaller amount of JavaScript loaded. For dynamic pages with a lot of logic, like the cart or checkout page, we already need a framework in which the frontend developer can communicate with the backend. Here we wanted to choose the lightest possible framework that would provide responsiveness and a global state.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;At Hatimeria we have extensive experience implementing the Hyva theme for Magento-based stores, so we decided to use our knowledge in projects with Optimizely.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The combination of Alpine.js and Tailwind CSS works very well there, so we decided to check out these technologies with Optimizely, since on paper it seems to be a very good combination and, in addition, it works well for many stores that run on Magento.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;In summary, we chose Alpine.js and Tailwind CSS because we use a small amount of JavaScript and only load the styles that are used in our application, resulting in faster loading times and better performance.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Pros and cons of Tailwind CSS&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Here are some pros and cons of using Tailwind CSS:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Pros:&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rapid Development&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: Tailwind CSS allows developers to quickly build user interfaces by utilizing pre-defined utility classes. This speeds up the development process as you don&#39;t have to write custom CSS styles from scratch.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Highly Customizable&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: Tailwind CSS provides a vast number of utility classes that can be combined and customized to create unique designs. It offers extensive control over spacing, typography, colors and more, allowing you to easily match your design requirements.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: Tailwind CSS is designed to be highly optimized for production use. By utilizing utility classes, you can eliminate unused CSS and keep the final bundle size minimal, resulting in faster page load times.&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy Maintenance&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: the CSS file contains the styles that are used in the files and automatically removes unnecessary styles without having to worry that removing a particular line will break the layout somewhere.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Cons:&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Conflicts in Git&lt;/strong&gt;: &lt;span style=&quot;font-weight: 400;&quot;&gt;can occur when multiple developers make changes to the same class and all styles are written in a single line. These conflicts may become more frequent with larger teams. However, the good news is that resolving them is not difficult.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Styles added from the admin panel may not work&lt;/strong&gt;: &lt;span style=&quot;font-weight: 400;&quot;&gt;Tailwind classes may not work if the added classes in the admin panel are not previously used in the code and added in the generated CSS file.&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning Curve&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: While the utility-first approach of Tailwind CSS offers great flexibility, it can have a steep learning curve, especially for developers who are accustomed to traditional CSS frameworks.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lack of Free Design Constraints&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: While Tailwind CSS provides flexibility, it doesn&#39;t impose any design constraints or opinionated free components out of the box (That option is a paid extra). This means you&#39;ll have to design and structure your UI components from scratch, which may require additional effort and expertise.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Pros and cons of Alpine js&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Here are some pros and cons of using Alpine.js:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Pros:&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lightweight&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: Alpine.js is designed to be lightweight (&lt;/span&gt;&lt;a href=&quot;https://bundlephobia.com/package/alpinejs@3.12.1&quot;&gt;14.1kB&lt;/a&gt;) &lt;span style=&quot;font-weight: 400;&quot;&gt;and has a small footprint. It adds only a minimal amount of JavaScript code to your project, resulting in faster load times and improved performance.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Familiar Syntax&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;strong&gt;:&lt;/strong&gt; The syntax of Alpine.js is similar to popular frontend frameworks like Vue.js, making it easier for developers already familiar with these frameworks to quickly grasp Alpine.js and start building interactive components. So far I have written frontend in Vue.js version 2, writing js is virtually identical, the only difference is that we do not have key words like data, methods, mounted, etc everything is written on one level. It took me several hours to change from Vue.js to Alpine.j&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stable&lt;/strong&gt;:&lt;span style=&quot;font-weight: 400;&quot;&gt; The &lt;/span&gt;&lt;a href=&quot;https://github.com/alpinejs/alpine/issues&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;repository on github&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; looks very good as it has 23.8k stars and 0 issues at the moment. In comparison, the &lt;/span&gt;&lt;a href=&quot;https://github.com/vuejs/core&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Vue.js repository&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; has 37.7k stars and 615 issues.&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Progressive Enhancement&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: Alpine.js follows a progressive enhancement approach, which means you can progressively add interactivity to your project without requiring a complete rewrite. You can selectively apply Alpine.js to specific elements and enhance the user experience without disrupting the existing functionality.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Cons:&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Learning Curve&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: While Alpine.js has a relatively low learning curve compared to more complex frameworks, developers who are new to JavaScript frameworks may still need to invest some time in understanding its concepts and syntax.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Community and Ecosystem&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: Alpine.js has a growing community, but it may not have the same level of community support or extensive ecosystem as larger frameworks. This means that finding pre-built components or libraries specifically tailored for Alpine.js might be more challenging.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JavaScript Dependency&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: Alpine.js relies on JavaScript to function, which means that if a user has disabled JavaScript in their browser, the interactivity provided by Alpine.js will not be available. This can be a limitation for projects that require broad accessibility or compatibility with older browsers.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Tailwind CSS in a .NET Optimizely project&lt;/span&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Configuration&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;We did the whole configuration based on an &lt;/span&gt;&lt;a href=&quot;https://khalidabuhakmeh.com/install-tailwind-css-with-aspnet-core#install-tailwind&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;article&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; and the &lt;/span&gt;&lt;a href=&quot;https://tailwindcss.com/docs/installation/using-postcss&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Tailwind documentation&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;, so to sum up here are the steps to follow:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Run the command &lt;/span&gt;&lt;strong&gt;npm init -y&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&amp;nbsp;to quickly and automatically create a package.json file for the project. Flag &lt;/span&gt;-y&lt;span style=&quot;font-weight: 400;&quot;&gt; means &quot;yes&quot;, which means that all configuration questions will be automatically accepted with default values.&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;npm init -y&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;/h4&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Next, install Tailwind and its dependencies.&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Run the Tailwind initialization command `&lt;/span&gt;npx tailwind init -p&lt;span style=&quot;font-weight: 400;&quot;&gt;`&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;npx tailwind init -p&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;After running the Tailwind initialization command, two files should be generated:&amp;nbsp; &lt;/span&gt;&lt;strong&gt;postcss.config.js&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; and&lt;/span&gt; t&lt;strong&gt;ailwind.config.js&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Here is the configuration used in our project for those files:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;postcss.config.js&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;module.exports = {
  plugins: {
    tailwindcss: { config: &#39;./tailwindcss-config.js&#39; },
    &#39;postcss-import&#39;: {},
    &#39;tailwindcss/nesting&#39;: {},
    autoprefixer: {},
    ...(process.env.NODE_ENV === &#39;production&#39; ? { cssnano: {} } : {}),
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;t&lt;/strong&gt;&lt;strong&gt;ailwind.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;/** @type {import(&#39;tailwindcss&#39;).Config} */


module.exports = {
  content: [
    &#39;./Views/**/*.cshtml&#39;,
    &#39;./Features/**/*.cshtml&#39;,
    &#39;./Features/CMS/ContentAreaRenderer/ContentAreaItemRenderer.cs&#39;,
  ],
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Tailwind CSS works by scanning all of your HTML files, JavaScript components and any other templates for class names, generating the corresponding styles and then writing them to a static CSS file. So it is even possible to generate CSS from a .cs file, when you add the paths in the content. In our project, in the &lt;/span&gt;ContentAreaItemRenderer.cs&lt;span style=&quot;font-weight: 400;&quot;&gt; file, we added support for &lt;/span&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/display-options&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Display Options&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; and the corresponding classes for the selected option.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;  private static string GetCssClassForTag(string tagName)
{
    if (string.IsNullOrEmpty(tagName))
    {
        return string.Empty;
    }


    return tagName.ToLowerInvariant() switch
    {
        ContentAreaTags.FullWidth =&amp;gt; &quot;w-full&quot;,
        ContentAreaTags.WideWidth =&amp;gt; &quot;w-full lg:w-8/12&quot;,
        ContentAreaTags.HalfWidth =&amp;gt; &quot;w-full md:w-6/12&quot;,
        ContentAreaTags.NarrowWidth =&amp;gt; &quot;w-full md:w-6/12 lg:w-4/12&quot;,
        _ =&amp;gt; string.Empty,
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Add the &lt;/span&gt;@tailwind&lt;span style=&quot;font-weight: 400;&quot;&gt; directives for each of Tailwind&amp;rsquo;s layers to your main CSS file.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;site.css&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code&gt;/*! @import */
@import &quot;tailwindcss/base&quot;;
@import &quot;tailwindcss/components&quot;;
@import &quot;tailwindcss/utilities&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Add build scripts for Tailwind in &lt;/span&gt;package.json&lt;/h4&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;&quot;scripts&quot;: {   
    &quot;css:build&quot;: &quot;npx tailwindcss -i ./wwwroot/css/site.css -o ./wwwroot/css/output.css --minify&quot;,
    &quot;css:watch&quot;: &quot;npx tailwindcss -i ./wwwroot/css/site.css -o ./wwwroot/css/output.css --watch&quot;
},&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;css:build&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; is a script for production with minify output&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;strong&gt;css:watch&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; is a script for development &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Then in your project &lt;/span&gt;&lt;strong&gt;.csproj&lt;/strong&gt; &lt;span style=&quot;font-weight: 400;&quot;&gt;add the configured npm commands to run automatically every time you build a project&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;  &amp;lt;Target Name=&quot;Tailwind&quot; BeforeTargets=&quot;Build&quot; Condition=&quot;&#39;$(Configuration)&#39; == &#39;Debug&#39;&quot;&amp;gt;
    &amp;lt;Exec Command=&quot;npm run css:watch&quot; /&amp;gt;
  &amp;lt;/Target&amp;gt;


  &amp;lt;Target Name=&quot;Tailwind&quot; BeforeTargets=&quot;Build&quot; Condition=&quot;&#39;$(Configuration)&#39; == &#39;Release&#39;&quot;&amp;gt;
    &amp;lt;Exec Command=&quot;npm run css:build&quot; /&amp;gt;
  &amp;lt;/Target&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Add import for the generated CSS file in View Layout&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1, maximum-scale=1&quot;&amp;gt;

    @Html.CanonicalLink()
    @Html.AlternateLinks()
    @Html.RequiredClientResources(&quot;Header&quot;)

    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;~/css/output.css&quot; /&amp;gt;
&amp;lt;/head&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cache&lt;/strong&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;In a development environment, the provided code will function correctly if the &quot;disabled cache&quot; checkbox is selected in the network tab of the DevTools. However, in a production project, it&#39;s important to handle versioning of the CSS file to ensure that users are not using an outdated version.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Here&#39;s an improved version of the code that addresses this issue by generating an ID based on the latest modification of the CSS file:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace Web.Infrastructure
{
    public static class CssHelper
    {
        private static string? _cssFileVersion;

        public static string GetCssFileVersion()
        {
            if (_cssFileVersion == null)
            {
                var cssFilePath = &quot;wwwroot/css/output.css&quot;;
                var cssFileLastModified = File.GetLastWriteTime(cssFilePath);
                _cssFileVersion = cssFileLastModified.ToString(&quot;yyyyMMddHHmmss&quot;);
            }

            return _cssFileVersion;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br /&gt;Updated import:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;@using Web.Infrastructure;

&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;~/css/output.css?v=@CssHelper.GetCssFileVersion()&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;In this code, you can adjust the &lt;/span&gt;&lt;strong&gt;cssFilePath &lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;variable to the appropriate path of your CSS file. The &lt;/span&gt;File.&lt;strong&gt;GetLastWriteTime()&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; method retrieves the last modification time of the CSS file.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;By calling the &lt;/span&gt;&lt;strong&gt;CssHelper.GetCssFileVersion()&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; method in your CSHTML file as &lt;/span&gt;&lt;strong&gt;@CssHelper.GetCssFileVersion()&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;, you will get a unique ID based on the last edit of the CSS file. This ensures the effective handling of caching and guarantees that users receive the latest version of the CSS file in a production environment.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Start using Tailwind in your CSHTML&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;br /&gt;&lt;img src=&quot;https://www.hatimeria.com/_next/image?url=https%3A%2F%2Fhatimeriacom-strapi.s3.eu-central-1.amazonaws.com%2Fpasted_image_0_7c735af76d.png&amp;amp;w=828&amp;amp;q=75&quot; width=&quot;828&quot; alt=&quot;&quot; height=&quot;124&quot; /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h3&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Hot Reload&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;There is a package on NuGet called &lt;/span&gt;&lt;a href=&quot;https://www.nuget.org/packages/Tailwind.Extensions.AspNetCore/1.0.0-beta3&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Tailwind.Extensions.AspNetCore&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; that allows hot reload to work with CSHTML and makes life easier for frontend developers. It is worth noting that it is in beta version, but during development we didn&#39;t encounter major problems with its use except for the problems described in Limitation.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Configuration&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;In order to configure hot reloading you need to follow the steps below:&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Instal package&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;dotnet add package Tailwind.Extensions.AspNetCore --version 1.0.0-beta3&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h3&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Add configuration in Startup.cs&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Add the npm command you configured in &lt;/span&gt;&lt;strong&gt;package.json&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; to run Tailwind in development mode. In our application it is &lt;/span&gt;&amp;ldquo;&lt;strong&gt;css:watch&lt;/strong&gt;&amp;rdquo;&lt;span style=&quot;font-weight: 400;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using Tailwind;

namespace Web;

public class Startup
{
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.RunTailwind(&quot;css:watch&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The &lt;/span&gt;&lt;strong&gt;RunTailwind&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;strong&gt; &lt;/strong&gt;method gets two parameters:&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;string npmScript,&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;string workingDir = &quot;./&quot;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The default value for &lt;/span&gt;workingDir &lt;span style=&quot;font-weight: 400;&quot;&gt;is &lt;/span&gt;&amp;ldquo;./&amp;rdquo;&lt;span style=&quot;font-weight: 400;&quot;&gt; so we omitted it in Startup.cs&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Limitation&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;There is a &lt;/span&gt;&lt;a href=&quot;https://github.com/Practical-ASP-NET/Tailwind.Extensions.AspNetCore#known-issues&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;known issue&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; with using Visual Studio 2022 because hot reloading does not always work. We also experienced this when building our application.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;So a much better solution is to use the&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; command&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;dotnet watch run&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;and make changes in Visual Studio Code.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Alpine js in a .net Optimizely project&lt;/span&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Configuration&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Adding Alpine.js to a project is very simple. We chose the &lt;/span&gt;&lt;a href=&quot;https://alpinejs.dev/essentials/installation#from-a-script-tag&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;from a script tag&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; way to add in the head tag. In Views we added it once in the Layout:&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1, maximum-scale=1&quot;&amp;gt;

    @Html.CanonicalLink()
    @Html.AlternateLinks()
    @Html.RequiredClientResources(&quot;Header&quot;)

    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;~/css/output.css&quot; /&amp;gt;
    
    //alpine import
     &amp;lt;script defer src=&quot;https://cdn.jsdelivr.net/npm/alpinejs@3.12.1/dist/cdn.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    // import api helper
    &amp;lt;script src=&quot;~/js/store/api.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;After that we have full access to all the features offered by Alpine.js.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Writing code&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;In the case of Alpine.js, we have two options for writing code in CSHTML files or creating new js files and adding them in a script tag. In our project we decided to add simple functionality like opening, closing the mobile menu in CSHTML, and all logic for checkout or the cart in JavaScript files. Since it had more than 300 lines of code we wanted to separate the code in CSHTML to make it more readable. In these files we add the whole logic to the Alpine store and in CSHTML we added the appropriate import in the script tag.&lt;/span&gt;&lt;/p&gt;
&lt;h4&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;CSHTML example&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;&amp;lt;div 
    class=&quot;z-20 order-2 sm:order-1 lg:order-2 navigation lg:hidden&quot; 
    x-data=&quot;initMenuMobile()&quot;&amp;gt;
    &amp;lt;div 
        class=&quot;bg-container-lighter&quot; 
        x-bind:class=&quot;{&#39;h-screen overflow-x-hidden overflow-y-auto fixed top-0 left-0 w-full&#39; : open}&quot;                      
        x-on:keydown.window.escape=&quot;open=false&quot; 
        x-on:toggle-mobile-menu.window=&quot;open = !open&quot;&amp;gt;
         //mobile menu 
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
    &#39;use strict&#39;;
    function initMenuMobile() {
        return {
            open: false,
        }
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;JavaScript files&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;checkout.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;document.addEventListener(&#39;alpine:init&#39;, () =&amp;gt; {
  Alpine.store(&#39;checkout&#39;, {
    isLoading: false,
    //...
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;checkout.cshtml&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;@model CheckoutPage;

&amp;lt;script defer src=&quot;~/js/store/checkout.js&quot;&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;section x-data class=&quot;py-12 relative flex flex-col lg:flex-row lg:gap-10&quot; x-bind:class=&quot;{&#39;blur pointer-events-none&#39; : $store.checkout.isLoading }&quot;&amp;gt;
&amp;lt;/section&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;$store.checkout.isLoading&lt;/strong&gt; &lt;span style=&quot;font-weight: 400;&quot;&gt;is a boolean responsible for loading a state on action like adding a shipping address, choosing a shipping method etc&amp;hellip;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The &lt;/span&gt;&lt;a href=&quot;https://alpinejs.dev/globals/alpine-store&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Alpine Store&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; can be used for Vue.js mixins. For example in our application we use fetch and every time we would have to repeat the logic for it by attaching anti-forgery token in headers. Instead we created an API helper file for fetch.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;strong&gt;api.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;document.addEventListener(&#39;alpine:init&#39;, () =&amp;gt; {
  Alpine.store(&#39;apiHelper&#39;, {
    REQUEST_VERIFICATION_TOKEN: document.querySelector(&#39;[name=__RequestVerificationToken]&#39;)
      ? document.querySelector(&#39;[name=__RequestVerificationToken]&#39;).getAttribute(&#39;value&#39;)
      : &#39;&#39;,
    async fetch(options) {
      const requestOptions = {
        url: options.url || &#39;&#39;,
        method: options.method || &#39;GET&#39;,
        body: options.body || null,
        headers: options.headers || {},
        prefix: options.prefix || `/api/${window.GLOBAL_LOCALE}/`
      };

      try {
        const response = await fetch(`${window.location.origin}${requestOptions.prefix}${requestOptions.url}`, {
          method: requestOptions.method,
          headers: {
            &#39;Content-Type&#39;: &#39;application/json&#39;,
            ...requestOptions.headers,
            RequestVerificationToken: this.REQUEST_VERIFICATION_TOKEN,
          },
          body: requestOptions.body,
        });
        return response;
      } catch (error) {
        // handle exception
      }
    },
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;checkout.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt; async loadShippingMethods() {
  try {
    this.startLoading();
    const options = {
      url: &#39;checkout/shipping/methods&#39;,
    };
    const response = await Alpine.store(&#39;apiHelper&#39;).fetch(options);
    if (response.status !== 200) {
      this.initErrorMessages();
    }
    const data = await response.json();

    this.shippingMethods = data || [];
    this.stopLoading();
  } catch (error) {
    this.initErrorMessages();
  }
},&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;With this approach when we want to change something in fetch like adding default headers, we can do it in one place.&lt;/span&gt;&lt;/p&gt;
&lt;h4&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Cache&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;&amp;lt;script defer src=&quot;~/js/store/checkout.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The code provided may lead to production issues due to the lack of versioning for the JavaScript file. This can cause caching problems for end users. To address this, we have a couple of potential solutions:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;strong&gt;Option 1:&lt;/strong&gt; Implement versioning based on the last edit of each file, similar to how we handle styles. This way, each file will have a unique version, allowing for effective cache management.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;strong&gt;Option 2: &lt;/strong&gt;Alternatively, we can opt for a uniform versioning approach where all files receive the same version number. In this case, we would need to increment the version number with each deployment. One possible approach is to base the version on the Assembly of Startup.cs or Program.cs.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;By implementing one of these solutions, we can ensure proper cache handling and mitigate potential issues in production caused by outdated JavaScript files.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Let me know your thoughts on which option would work best for your project.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;h3&gt;&lt;strong&gt;Limitations of Alpine.js in .net&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;In CSHTML files, symbols like &quot;@&quot; and &quot;:&quot; have special meanings in Razor&#39;s syntax. Therefore, using shorthand syntax for x-bind (using &quot;:&quot;) and x-on (using &quot;@&quot;) in CSHTML files will not work as expected. These symbols are reserved for Razor&#39;s syntax and cannot be used as shorthand syntax for Alpine.js directives in CSHTML files.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;To ensure proper usage, developers should remember not to use shorthand syntax for x-bind and x-on in CSHTML files and instead use the full syntax provided by Alpine.js for these directives. This will prevent conflicts with Razor&#39;s syntax and ensure the correct interpretation of the code.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Nevertheless, there is also the option to use the shorthand syntax for x-on via &amp;ldquo;@@&amp;rdquo;, but this can lead to confusion and in our case we chose not to use shorthand syntax.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Results - performance&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;We expected good results on Lighthouse but didn&#39;t think they would be that good without thinking too much about performance when developing the application. Every page on mobile and desktop achieves a score of &lt;/span&gt;&lt;strong&gt;100&lt;/strong&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;strong&gt;.&lt;/strong&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.hatimeria.com/_next/image?url=https%3A%2F%2Fhatimeriacom-strapi.s3.eu-central-1.amazonaws.com%2F1_b1144a335f.png&amp;amp;w=828&amp;amp;q=75&quot; width=&quot;805&quot; alt=&quot;performance-100&quot; height=&quot;757&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&amp;nbsp;&lt;img src=&quot;https://www.hatimeria.com/_next/image?url=https%3A%2F%2Fhatimeriacom-strapi.s3.eu-central-1.amazonaws.com%2F2_ae1cdd482b.png&amp;amp;w=828&amp;amp;q=75&quot; width=&quot;819&quot; alt=&quot;perofrmance-100&quot; height=&quot;761&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;In summary, my transition from Vue.js to Alpine.js was very smooth and the performance results are much better.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Conclusion&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;If you have encountered any problems while trying to set up your project I will be more than happy to help you resolve them.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</id><updated>2023-10-12T13:51:13.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>