<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Linh Hoang</title><link href="https://world.optimizely.com/blogs/linh-hoang/" /><updated>2024-09-04T08:25:21.0000000Z</updated><id>https://world.optimizely.com/blogs/linh-hoang/</id><generator uri="https://world.optimizely.com/blogs/linh-hoang/" version="2.0">Optimizely World</generator><entry><title>Optimizely Headless Form Setup</title><link href="https://world.optimizely.com/blogs/linh-hoang/dates/2024/8/optimizely-headless-form-setup/" /><id>https://world.optimizely.com/blogs/linh-hoang/dates/2024/8/optimizely-headless-form-setup/</id><published>2024-09-04T08:25:21.0000000Z</published><updated>2024-09-04T08:25:21.0000000Z</updated><author><name>linh.hoang@optimizely.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;1. &lt;strong&gt;Create empty CMS applications&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First, let&amp;rsquo;s setup an empty CMS application.&lt;/p&gt;
&lt;p&gt;Install the NuGet packages in your solution using the NuGet Package Manager in Visual Studio or with the command line:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dotnet add package EPiServer.CMS&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.&amp;nbsp;Setup headless Optimizely Forms API&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Optimizely Headless Form API consists of one main NuGet package, Optimizely.Cms.Forms.Service, and some additional NuGet packages let you install only the necessary functionality.&lt;/p&gt;
&lt;p&gt;Install the NuGet packages in your solution using the NuGet Package Manager in Visual Studio or with the command line:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dotnet add package Optimizely.Cms.Forms.Service&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Configure form headless API options&lt;/p&gt;
&lt;p&gt;Configure the API in ConfigureServices in startup.cs.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;services.AddOptimizelyFormsService(options =&amp;gt; {
  options.EnableOpenApiDocumentation = true;
  options.FormCorsPolicy = new FormCorsPolicy {
    AllowOrigins = new string[] {
        "*"
    }, //Enter '*' to allow any origins, multiple origins separate by comma 
    AllowCredentials = true
  };
  options.OpenIDConnectClients.Add(new() {
    Authority = "" //Enter the client's domain that requires authentication. The domain must include the protocol, e.g., https://localhost:3000.
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Authentication using OpenIDConnect&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Install&amp;nbsp;Episerver.OpenIDConnect&amp;nbsp;package using the NuGet Package Manager in Visual Studio or with the command line:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dotnet add package Episerver.OpenIDConnect.UI&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Configure encryption key for OpenIDConnect.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public class FormServiceOptionsPostConfigure : IPostConfigureOptions&amp;lt;OptimizelyFormsServiceOptions&amp;gt;
{
    private readonly OpenIddictServerOptions _options;

    public FormServiceOptionsPostConfigure(IOptions&amp;lt;OpenIddictServerOptions&amp;gt; options)
    {
        _options = options.Value;
    }

    public void PostConfigure(string name, OptimizelyFormsServiceOptions options)
    {
        foreach (var client in options.OpenIDConnectClients)
        {
            foreach (var key in _options.EncryptionCredentials.Select(c =&amp;gt; c.Key))
            {
                client.EncryptionKeys.Add(key);
            }
 
            foreach (var key in _options.SigningCredentials.Select(c =&amp;gt; c.Key))
            {
                client.SigningKeys.Add(key);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Configure OpenIdConnect in Startup.cs.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;services.AddOpenIDConnect&amp;lt;ApplicationUser&amp;gt;(
    useDevelopmentCertificate: true,
    signingCertificate: null,
    encryptionCertificate: null,
    createSchema: true
    );

services.AddOpenIDConnectUI();
services.TryAddEnumerable(ServiceDescriptor.Singleton&amp;lt;IPostConfigureOptions&amp;lt;OptimizelyFormsServiceOptions&amp;gt;, FormServiceOptionsPostConfigure&amp;gt;());&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Create key/secret using OIDC UI and test your authentication&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Open &amp;ldquo;OpenID Connect&amp;rdquo; tab in Settings, create new application then input your client id and client secret&lt;/p&gt;
&lt;p&gt;&lt;img src="/link/473f3a8f485540b28cbcad6fb7f07cf2.aspx" width="893" height="415" /&gt;&lt;/p&gt;
&lt;p&gt;Test if it all works by making a POST request in Postman to&amp;nbsp;/api/episerver/connect/token&amp;nbsp;with the following parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;ul&gt;
&lt;li&gt;client_id: api-client&lt;/li&gt;
&lt;li&gt;client_secret: SuperSecret&lt;/li&gt;
&lt;li&gt;grant_type: client_credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p&gt;You'll receive a time-limited access token.&lt;/p&gt;
&lt;p&gt;&lt;img src="/link/ec921bd271dc40bdbaecdd4ff1e73d0d.aspx" width="914" height="704" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5.&amp;nbsp;Api specification&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You should have the following endpoints available on your host site:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GET &amp;ndash;&amp;nbsp;/_forms/v1/forms/{ContentKey}?language={Language}&amp;nbsp;&amp;ndash; Retrieve form data with form elements by content key&lt;/li&gt;
&lt;li&gt;PUT &amp;ndash;&amp;nbsp;/_forms/v1/forms/&amp;nbsp;&amp;ndash; Submit the form.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To verify that the API is working properly, open the Form Headless API in your browser:&lt;/p&gt;
&lt;p&gt;http://&amp;lt;your-site-url&amp;gt;/_forms/v1/forms/&amp;lt;ContentKey&amp;gt;?language=en&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: ContentKey is the content guid id without hypen&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For more api details: &lt;a href="https://docs.developers.optimizely.com/content-management-system/v1.2.0-forms/docs/set-up-headless-optimizely-forms-api#put-or-get-endpoint-description"&gt;Headless Form Api Specification&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Create simple hello world form with single text input&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In right menu panel, create New Form, enter your form name.&lt;/p&gt;
&lt;p&gt;&lt;img src="/link/cbf50a9b626e4763a4480f796ab8f662.aspx" width="902" height="414" /&gt;&lt;/p&gt;
&lt;p&gt;Add textbox and submit button to form then publish form.&lt;/p&gt;
&lt;p&gt;&lt;img src="/link/4df0c7189b5249beaaaddc8f1908579c.aspx" width="903" height="451" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. Retrieve form using Postman&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Import postman collection &lt;a href="/link/b2290e76cab44813b1c7839910d6b72c.aspx"&gt;Headless Sample.postman_collection.json&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8. Prepare backend site for form render&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8.1. Update Manage Website setting for both case&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Browse the site &amp;gt; &lt;strong&gt;Manage Websites&lt;/strong&gt;: Update &lt;strong&gt;Delivery site&lt;/strong&gt; = localhost:&lt;strong&gt;3000&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Browse the site &amp;gt; &lt;strong&gt;Manage Websites&lt;/strong&gt; : Remove &lt;strong&gt;Host name = *&lt;/strong&gt; the list&lt;/li&gt;
&lt;li&gt;(Optional) Update ManagementSite to use http instead of https on &lt;strong&gt;json&lt;/strong&gt; (if you deploy your site on the local ): "applicationUrl": "&lt;a href="http://localhost:8000"&gt;&lt;strong&gt;http&lt;/strong&gt;://localhost:8000&lt;/a&gt;",&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;Browse React site:http://localhost:3000/en/[pageURL]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;8.2. Render form using ContentGraph&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up management site&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Install &lt;em&gt;Optimizely.ContentGraph.Cms&lt;/em&gt; and &lt;em&gt;Optimizely.Cms.Forms.ContentGraph&lt;/em&gt; using the NuGet Package Manager in Visual Studio or with the command line:&lt;/p&gt;
&lt;div class="CodeTabs CodeTabs_initial theme-light"&gt;
&lt;div class="CodeTabs-inner"&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;dotnet add package Optimizely.ContentGraph.Cms
dotnet add package Optimizely.Cms.Forms.ContentGraph&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Add CG key to &lt;em&gt;appsettings.json&lt;/em&gt;&amp;nbsp;(at the same level as &lt;em&gt;Episerver&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="CodeTabs CodeTabs_initial theme-light"&gt;
&lt;div class="CodeTabs-inner"&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;"Optimizely": {
    "ContentGraph": {
      "GatewayAddress": "&amp;lt;&amp;lt;Gateway Address&amp;gt;&amp;gt;",
      "AppKey": "&amp;lt;&amp;lt;App Key&amp;gt;&amp;gt;",
      "Secret": "&amp;lt;&amp;lt;Secret&amp;gt;&amp;gt;",
      "SingleKey": "&amp;lt;&amp;lt;Single Key&amp;gt;&amp;gt;",
      "AllowSendingLog": true,
      "ContentVersionSyncMode": "DraftAndPublishedOnly",
      "Include": {
        "ContentIds": [],
        "ContentTypes": []
      },
      "SyncReferencingContents": true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Add settings to &lt;em&gt;Startup.cs&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Register ContentGraph for HeadlessForm
services.AddContentDeliveryApi(op =&amp;gt; {
  op.DisableScopeValidation = true;
  op.RequiredRole = null;
});
services.ConfigureContentApiOptions(o =&amp;gt; {
  o.FlattenPropertyModel = true;
  o.IncludeNumericContentIdentifier = true;
});
services.AddContentGraph();

services.AddOptimizelyFormsContentGraph();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;8.3. Render form using Rest API&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Add &lt;em&gt;ReactController.cs&lt;/em&gt; file in &lt;em&gt;Controllers &lt;/em&gt;folder to get all form keys in page&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;using EPiServer;
using EPiServer.Cms.Shell;
using EPiServer.Core;
using EPiServer.Forms.Implementation.Elements;
using EPiServer.ServiceLocation;
using EPiServer.SpecializedProperties;
using EPiServer.Web;
using EPiServer.Web.Routing;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace AlloyMvcTemplates.Controllers;
[Route("api/[controller]")]
[ApiController]
public class ReactController : ControllerBase
{
    public ReactController()
    {
    }

    [HttpGet("GetFormInPageByUrl")]
    public IActionResult GetFormInPageByUrl(string url)
    {
        var builder = new UrlBuilder(url);
        var content = UrlResolver.Current.Route(builder, ContextMode.Default);

        if (content is null)
        {
            return NoContent();
        }

        var contentLoader = ServiceLocator.Current.GetInstance&amp;lt;IContentLoader&amp;gt;();
        var pageModel = new PageModel();

        if (content is not null)
        {
            pageModel.Title = content.Name;
            pageModel.PageUrl = content.PublicUrl();

            foreach (var props in content.Property)
            {
                if (!props.IsNull &amp;amp;&amp;amp; props is PropertyContentArea)
                {
                    var contentArea = props as PropertyContentArea;
                    foreach (var item in contentArea.PublicContentArea?.FilteredItems)
                    {
                        var contentItem = contentLoader.Get&amp;lt;IContent&amp;gt;(item.ContentLink);

                        if (contentItem is FormContainerBlock)
                        {
                            pageModel.FormKeys.Add(contentItem.ContentGuid.ToString("N"));
                        }
                    }
                }
            }
        }

        return Ok(pageModel);
    }
}

public class PageModel
{
    public string Title { get; set; }
    public string PageUrl { get; set; }
    public List&amp;lt;string&amp;gt; FormKeys { get; set; } = new List&amp;lt;string&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;9. Render form using simple react app&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9.1. Create a React app template&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create an empty folder : \&lt;strong&gt;ClientApp&lt;/strong&gt; for example&lt;/li&gt;
&lt;li&gt;Run cmd on the folder : &lt;code&gt;npx create-react-app headless-form --template typescript&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Result: \&lt;strong&gt;headless-form&lt;/strong&gt; folder is added&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9.2. Install JS SDK&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Navigate to&lt;strong&gt; \headless-form&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Create a file &lt;strong&gt;.npmrc&lt;/strong&gt; with content:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;@episerver:registry=https://&lt;span class="ui-provider a b c d e f g h i j k l m n o p q r s t u v w x y z ab ac ae af ag ah ai aj ak"&gt;pkgs.dev.azure.com/EpiserverEngineering/netCore/_packaging/HeadlessForms/npm/registry&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install react-router-dom. Run cmd: &lt;code&gt;npm install &lt;a href="mailto:react-router-dom@5.3.4"&gt;react-router-dom@5.3.4&lt;/a&gt; &lt;a href="mailto:@types/react-router-dom@5.3.3"&gt;@types/react-router-dom@5.3.3&lt;/a&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Install @episerver/forms-react. Run cmd: &lt;code&gt;npm install &lt;a href="mailto:@episerver/forms-react@x.x.x"&gt;@episerver/forms-react@1.0.0&lt;/a&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Install @episerver/forms-sdk. Run cmd: &lt;code&gt;npm install &lt;a href="mailto:@episerver/forms-sdk@x.x.x"&gt;@episerver/forms-sdk@1.0.0&lt;/a&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;9.3. Add file .env with content in both case&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;REACT_APP_ENDPOINT_GET_FORM_BY_PAGE_URL=https://localhost:8000/api/React/GetFormInPageByUrl?url=

REACT_APP_HEADLESS_FORM_BASE_URL=https://localhost:8000/

REACT_APP_AUTH_BASEURL=https://localhost:8000/api/episerver/connect/token&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Render form using Content Graph, add the lines below to the .env file&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;REACT_APP_CG_PREVIEW_URL={GatewayAddress}/content/v2

REACT_APP_CONTENT_GRAPH_GATEWAY_URL={GatewayAddress}/content/v2?auth={SingleKey}

REACT_APP_LOGIN_CLIENT_ID=frontend

REACT_APP_HEADLESS_FORM_BASE_URL=https://localhost:8000/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Update the port = ManagementSite&amp;rsquo;s URL if needs&lt;/li&gt;
&lt;li&gt;GatewayAddress, SingleKey are from appsetting.json&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;9.4. Add the file &lt;em&gt;\src\useFetch.ts&lt;/em&gt; that contains a function to request form data&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;import { useEffect, useState } from "react";

export const useFetch = (url: string) =&amp;gt; {

    const [data, setData]= useState&amp;lt;any&amp;gt;(null);

    const [loading, setLoading] = useState&amp;lt;boolean&amp;gt;(false);

    const [error, setError] = useState&amp;lt;any&amp;gt;(null);

    useEffect(()=&amp;gt;{

        const fetchData = async () =&amp;gt; {

            setLoading(true);

            fetch(url)

                .then(async (response: Response)=&amp;gt;{

                    if(response.ok){

                        setData(await response.json());

                    }

                })

                .catch((err: any)=&amp;gt;{

                    setError(err);

                })

                .finally(()=&amp;gt;{

                    setLoading(false);

                });

        };

        if(!loading){

            fetchData();

        }

    // eslint-disable-next-line react-hooks/exhaustive-deps

    },[url]);

    return {data, loading, error};

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;9.5. Update \src\App.tsx&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;import './App.css';

import { useFetch } from './useFetch';

import { Form, FormLogin } from '@episerver/forms-react';

import { FormCache, FormConstants, IdentityInfo, extractParams } from '@episerver/forms-sdk';

import { useState } from 'react';

import { useHistory, useLocation } from 'react-router-dom';

 

function App() {

    const location = useLocation();

    const { language } = extractParams(window.location.pathname)

    const url = `${process.env.REACT_APP_ENDPOINT_GET_FORM_BY_PAGE_URL}${location.pathname}`;

    const { data: pageData, loading } = useFetch(url);

    const formCache = new FormCache();

    const [identityInfo, setIdentityInfo] = useState&amp;lt;IdentityInfo&amp;gt;({

        accessToken: formCache.get&amp;lt;string&amp;gt;(FormConstants.FormAccessToken)

    } as IdentityInfo);

    const history = useHistory()

    const handleAuthen = (identityInfo: IdentityInfo) =&amp;gt; {

        setIdentityInfo(identityInfo);

    }

    return (

        &amp;lt;div className="App"&amp;gt;

            Hello

            {loading &amp;amp;&amp;amp; &amp;lt;div className='loading'&amp;gt;Loading...&amp;lt;/div&amp;gt;}

            {!loading &amp;amp;&amp;amp; pageData &amp;amp;&amp;amp; (

                &amp;lt;&amp;gt;

                    &amp;lt;h1&amp;gt;{pageData.title}&amp;lt;/h1&amp;gt;

                    &amp;lt;div className='main'&amp;gt;

                        &amp;lt;div className='left'&amp;gt;

                            {pageData.formKeys.map((key: any) =&amp;gt; (

                                &amp;lt;Form

                                    key={key}

                                    formKey={key}

                                    language={language ?? "en"}

                                    baseUrl={process.env.REACT_APP_HEADLESS_FORM_BASE_URL ?? "/"}

                                    identityInfo={identityInfo}

                                    history={history}

                                    currentPageUrl={pageData.pageUrl}

                                   // optiGraphUrl={process.env.REACT_APP_CONTENT_GRAPH_GATEWAY_URL}//uncomment this line if you want to render using content graph

                                /&amp;gt;

                            ))}

                        &amp;lt;/div&amp;gt;

                        &amp;lt;div className={`right`}&amp;gt;

                            &amp;lt;h2&amp;gt;Login&amp;lt;/h2&amp;gt;

                            &amp;lt;FormLogin

                                clientId='TestClient'

                                authBaseUrl={process.env.REACT_APP_AUTH_BASEURL ?? ""}

                                onAuthenticated={handleAuthen} /&amp;gt;

                        &amp;lt;/div&amp;gt;

                    &amp;lt;/div&amp;gt;

                &amp;lt;/&amp;gt;

            )}

        &amp;lt;/div&amp;gt;

    );

}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</summary></entry></feed>