<?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="http://world.optimizely.com" /><updated>2024-09-04T08:25:21.0000000Z</updated><id>https://world.optimizely.com/blogs/linh-hoang/</id> <generator uri="http://world.optimizely.com" 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>&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=&quot;language-csharp&quot;&gt;&lt;code&gt;services.AddOptimizelyFormsService(options =&amp;gt; {
  options.EnableOpenApiDocumentation = true;
  options.FormCorsPolicy = new FormCorsPolicy {
    AllowOrigins = new string[] {
        &quot;*&quot;
    }, //Enter &#39;*&#39; to allow any origins, multiple origins separate by comma 
    AllowCredentials = true
  };
  options.OpenIDConnectClients.Add(new() {
    Authority = &quot;&quot; //Enter the client&#39;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=&quot;language-csharp&quot;&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=&quot;language-csharp&quot;&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=&quot;/link/473f3a8f485540b28cbcad6fb7f07cf2.aspx&quot; width=&quot;893&quot; height=&quot;415&quot; /&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&#39;ll receive a time-limited access token.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/ec921bd271dc40bdbaecdd4ff1e73d0d.aspx&quot; width=&quot;914&quot; height=&quot;704&quot; /&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=&quot;https://docs.developers.optimizely.com/content-management-system/v1.2.0-forms/docs/set-up-headless-optimizely-forms-api#put-or-get-endpoint-description&quot;&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=&quot;/link/cbf50a9b626e4763a4480f796ab8f662.aspx&quot; width=&quot;902&quot; height=&quot;414&quot; /&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=&quot;/link/4df0c7189b5249beaaaddc8f1908579c.aspx&quot; width=&quot;903&quot; height=&quot;451&quot; /&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=&quot;/link/b2290e76cab44813b1c7839910d6b72c.aspx&quot;&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 ): &quot;applicationUrl&quot;: &quot;&lt;a href=&quot;http://localhost:8000&quot;&gt;&lt;strong&gt;http&lt;/strong&gt;://localhost:8000&lt;/a&gt;&quot;,&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=&quot;CodeTabs CodeTabs_initial theme-light&quot;&gt;
&lt;div class=&quot;CodeTabs-inner&quot;&gt;
&lt;pre class=&quot;language-csharp&quot;&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=&quot;CodeTabs CodeTabs_initial theme-light&quot;&gt;
&lt;div class=&quot;CodeTabs-inner&quot;&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;&quot;Optimizely&quot;: {
    &quot;ContentGraph&quot;: {
      &quot;GatewayAddress&quot;: &quot;&amp;lt;&amp;lt;Gateway Address&amp;gt;&amp;gt;&quot;,
      &quot;AppKey&quot;: &quot;&amp;lt;&amp;lt;App Key&amp;gt;&amp;gt;&quot;,
      &quot;Secret&quot;: &quot;&amp;lt;&amp;lt;Secret&amp;gt;&amp;gt;&quot;,
      &quot;SingleKey&quot;: &quot;&amp;lt;&amp;lt;Single Key&amp;gt;&amp;gt;&quot;,
      &quot;AllowSendingLog&quot;: true,
      &quot;ContentVersionSyncMode&quot;: &quot;DraftAndPublishedOnly&quot;,
      &quot;Include&quot;: {
        &quot;ContentIds&quot;: [],
        &quot;ContentTypes&quot;: []
      },
      &quot;SyncReferencingContents&quot;: 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=&quot;language-csharp&quot;&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=&quot;language-csharp&quot;&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(&quot;api/[controller]&quot;)]
[ApiController]
public class ReactController : ControllerBase
{
    public ReactController()
    {
    }

    [HttpGet(&quot;GetFormInPageByUrl&quot;)]
    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(&quot;N&quot;));
                        }
                    }
                }
            }
        }

        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=&quot;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&quot;&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=&quot;mailto:react-router-dom@5.3.4&quot;&gt;react-router-dom@5.3.4&lt;/a&gt; &lt;a href=&quot;mailto:@types/react-router-dom@5.3.3&quot;&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=&quot;mailto:@episerver/forms-react@x.x.x&quot;&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=&quot;mailto:@episerver/forms-sdk@x.x.x&quot;&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=&quot;language-csharp&quot;&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=&quot;language-csharp&quot;&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=&quot;language-csharp&quot;&gt;&lt;code&gt;import { useEffect, useState } from &quot;react&quot;;

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=&quot;language-csharp&quot;&gt;&lt;code&gt;import &#39;./App.css&#39;;

import { useFetch } from &#39;./useFetch&#39;;

import { Form, FormLogin } from &#39;@episerver/forms-react&#39;;

import { FormCache, FormConstants, IdentityInfo, extractParams } from &#39;@episerver/forms-sdk&#39;;

import { useState } from &#39;react&#39;;

import { useHistory, useLocation } from &#39;react-router-dom&#39;;

 

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=&quot;App&quot;&amp;gt;

            Hello

            {loading &amp;amp;&amp;amp; &amp;lt;div className=&#39;loading&#39;&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=&#39;main&#39;&amp;gt;

                        &amp;lt;div className=&#39;left&#39;&amp;gt;

                            {pageData.formKeys.map((key: any) =&amp;gt; (

                                &amp;lt;Form

                                    key={key}

                                    formKey={key}

                                    language={language ?? &quot;en&quot;}

                                    baseUrl={process.env.REACT_APP_HEADLESS_FORM_BASE_URL ?? &quot;/&quot;}

                                    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=&#39;TestClient&#39;

                                authBaseUrl={process.env.REACT_APP_AUTH_BASEURL ?? &quot;&quot;}

                                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;</id><updated>2024-09-04T08:25:21.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>