Blog posts by Remko Jantzen2021-12-14T11:12:18.0000000Z/blogs/remko-jantzen/Optimizely WorldTwelve points to start with on page editing in CMS 11 or 12/blogs/remko-jantzen/dates/2021/12/twelve-points-to-start-with-on-page-editing-in-cms-11-or-12/2021-12-14T11:12:18.0000000Z<p>Over the past few months, I've received a number of questions regarding the support for on-page editing by the CMS. It's not hard to enable, but you need to know where to look for. As I haven't found a complete overview of all steps needed to make it work, hence this blog post.</p>
<p>I will only cover the configuration needed and constraints to be obeyed to make on-page editing work, but won't go into the specifics of making changes within the CMS and/or building the decoupled frontend. The steps are the same for CMS 11 and CMS 12, on the few points where the version matters, I've highlighted these differences.</p>
<h2>General setup</h2>
<ol>
<li>As the Edit Mode requires communication between the shell and decoupled frontend the current constraint is that both must share the same domain. Thus https://cms.example.com and https://www.example.com will work, but https://www.holding-name.com and https://www.brand-name.com will not work.<br /><br />Also make sure all domains are running either on HTTPS with certificates trusted by the browser or on HTTP, when you'll mix HTTP and HTTPS it will not work.</li>
<li>In the Site configuration, make sure that the website URLs are configured correctly:
<ul>
<li><em>Website URL:</em> The domain where the decoupled frontend is running</li>
<li><em>Host Names:</em> Configure at a minimum two host-names
<ul>
<li>The domain of the decoupled frontend should have the type: <strong><span style="font-family: 'andale mono', monospace;">Primary</span></strong></li>
<li>The domain of the shell should have the type: <strong><span style="font-family: 'andale mono', monospace;">Edit</span></strong></li>
</ul>
</li>
</ul>
</li>
<li>Though not a firm requirement, having the CMS control the URLs and routing within the decoupled frontend gives two major benefits:
<ul>
<li>Editors retain full control over both the website structure and content, at the expense of a slightly more complicated initial build - especially with Server Side Rendering involved.</li>
<li>Routing for edit-mode URLs is much easier to implement.</li>
</ul>
</li>
</ol>
<h2>CMS configuration</h2>
<ol>
<li>Ensure that your CMS is capable to serve unpublished versions of a content item, either by extending the Content Delivery API or using the Content Management API.<br /><br /><em><strong>Tip 1:</strong></em> To get close to feature parity, you'll need to extend the CMS with support for loading by Project ID, Visitor Group ID, and Channel ID. (The last one as 3rd party modules within the CMS may depend on it). <br /><br /><em><strong>Tip 2:</strong></em> By default, the Context Mode resolution differs between the Content Delivery/Management API and the regular CMS, so any 3rd party add-on will behave like you're working on a public endpoint, even when accessing the API from within edit mode. Easy solution? Create a single ContextModeResolver unifying both resolvers into one single implementation.</li>
<li>Ensure that your CMS supports a form of single-sign-on between the Shell and Decoupled frontend. There's no requirement on which IAM solution is used, beyond it enabling the decoupled frontend to send authenticated requests to the Content Delivery/Management API.</li>
<li>Make sure that the ContentDeliveryAPI option "<span style="font-family: 'andale mono', monospace;">OptimizeForDelivery</span>" has been set. Furthermore it is highly recommended to invoke the following extension methods on the service container in your startup: "<span style="font-family: 'andale mono', monospace;">ConfigureForExternalTemplates()</span>" and "<span style="font-family: 'andale mono', monospace;">ConfigureForContentDeliveryClient()</span>"<br /><br /><em><strong>Note: </strong></em>If you make these changes on an application already using the Content Delivery API you will find it changing the data returned by the Content Delivery API.</li>
<li>Make sure the shell updates the <span style="font-family: 'andale mono', monospace;">document.domain</span> value in the browser. Yes, this is a deprecated setting, but at the time of writing, it's the only way to get On-Page Editing to work. This must be the domain, without subdomains, protocol, and port number. So for https://cms.example.com, this becomes "example.com"
<ul>
<li>CMS 11: Add the AppSetting "<span style="font-family: 'andale mono', monospace;">episerver:ManagementDomain</span>" with the value as above</li>
<li>CMS 12: At the time of writing, there's no standard way of doing this. Probably injecting a script into the shell will give you the ability to update this value.</li>
</ul>
</li>
</ol>
<h2>Frontend requirements</h2>
<ol>
<li>The shell determines the currently selected item by URL and uses special URLs to render edit mode. Hence:
<ul>
<li>All navigation between content items must happen by a regular page unload/load. So using the History API or on-page routers is not allowed in this mode.</li>
<li>The Decoupled frontend must support the special URLs generated by the shell to load the content item selected by the editor </li>
</ul>
</li>
<li>The actual DOM tree must have the appropriate attributes to inform the shell where the editable fields actually are presented on the page. See <a href="/link/f80257067ca542288c0b74d4db6ae0c5.aspx">https://world.optimizely.com/documentation/developer-guides/CMS/editing/on-page-editing-with-client-side-rendering/</a> for more details.<br /><br /><em><strong>Note:</strong></em> This is the actual DOM tree, shown by "inspect element" in a browser, not the virtual DOM maintained by React, Vue, etc...</li>
<li>When the page runs in edit mode it must update the <span style="font-family: 'andale mono', monospace;">document.domain</span> value in the browser. This must be the domain, without subdomains, protocol, and port number. So for https://cms.example.com, this becomes "example.com". This must be the same value as for step 4 in the CMS.</li>
<li>To ensure that the edit mode renders correctly and can communicate with the SPA/PWA, the communicator script must be loaded and executed (verify that there’re no CORS or other security measures preventing this). The URL for this script is (assuming that the CMS runs at https://cms.example.com/episerver): https://cms.example.com/episerver/cms/latest/clientresources/epi-cms/communicationinjector.js</li>
<li>Last, but certainly not least, the frontend must listen to the <span style="font-family: 'andale mono', monospace;">contentSaved </span>event to refresh the view with the latest data. The documentation for this is available at: <a href="/link/f80257067ca542288c0b74d4db6ae0c5.aspx">https://world.optimizely.com/documentation/developer-guides/CMS/editing/on-page-editing-with-client-side-rendering</a> <br /><br />When running decoupled, with the communicationInjector (Step 4 above) loaded, this can be done by using the global "epi" variable like so:<br /><span style="font-family: 'andale mono', monospace;">epi.subscribe("contentSaved", event => { /* Your handler here */});</span></li>
</ol>
<p>And that's it, these are my "Twelve points to start with on-page-editing in CMS 11 or 12".</p>
<p>Happy coding!</p>Frontend: Adding application services - Into Foundation Spa React series, Part 6/blogs/remko-jantzen/dates/2021/7/frontend-adding-application-services---into-foundation-spa-react-series-part-6/2021-08-02T08:26:13.0000000Z<p>In this sixth - and final - installment of the "Into Foundation-Spa-React" series, the focus is on the service container bundled within the Foundation Spa React and how it might be leveraged when building your own solution based upon the core libraries that power Foundation Spa React.</p>
<p>First is the use-case. I most cases you wouldn't need to add your own services, as you can leverage the context/provider and hooks from React to make a service available. The case where this will be most useful is where you need to replace one of the standard services provided by the framework, need to interact with the bootstrapping process, and/or have supporting services that you need to be able to reference when needed.</p>
<p>The most used services from the service container are the Content Repository and Content Delivery Client. The first provides a proxy to the second, adding (when running in-browser and non-edit) caching of content items using IndexDB. The second is used directly for "non-content" operations, such as authentication, controller methods.</p>
<p>In order to interact with the bootstrapping process, you'll need to provide an instance of <code>Core.IInitializableModule</code>, which can easily be created by extending <code>Core.BaseInitializableModule</code>. During the bootstrapping process, the system invokes the three methods in the following order:</p>
<ul>
<li>After the core services have been registered, but before any initialization has taken place: <code>ConfigureContainer</code>; This is where you should add/configure services within the container.</li>
<li>After the container has been created, when the initialization creates the Redux state container: <code>GetStateReducer</code>; This is where you can add your reducers for the global state container</li>
<li>As the last step of the bootstrapping process: <code>StartModule</code>; here you can execute any logic needed to bootstrap your own logic.</li>
</ul>
<p>At this time all three methods run synchronously, which could cause a long execution time on the main thread if not used sparsely and critically.</p>
<p>As you might have noticed, the <code>Core.IInitializableModule</code> has a <code>SortOrder</code>, which defines the execution sequence of the modules. They are executed in ascending order, the system guarantees that all modules with a lower <code>SortOrder</code> have been executed successfully, however, modules with the same <code>SortOrder</code> can be executed in parallel, before or after the current module. Modules with a higher <code>SortOrder</code> will always be executed after this module.</p>
<h2>In action</h2>
<p>So, a very long explanation, let's see how this works in practice, by looking at one of the core modules within the SPA: "Core State Engine"</p>
<pre class="language-javascript"><code>export class StateModule extends BaseInitializableModule implements IInitializableModule
{
protected name = "Core State Engine";
public SortOrder = 40;
public StartModule(context: IEpiserverContext): void
{
const store = context.getStore();
const state = store.getState() as PartialAppState;
const cfg = context.serviceContainer.getService<Readonly<IConfig>>(DefaultServices.Config);
// Setup CD-API Language to respond to the state changes, ensuring
// that it always takes the current CD-API instance from the container.
Tools.observeStore<string, CmsAppState>(
store,
(x) => x?.OptiContentCloud?.currentLanguage || cfg.defaultLanguage,
(newValue) => {
if (newValue) {
const cdAPI = context.serviceContainer.getService<IContentDeliveryAPI>(DefaultServices.ContentDeliveryAPI_V2);
cdAPI.Language = newValue;
}
}
);
// Make sure the current language is applied
const language = state?.OptiContentCloud?.currentLanguage;
if (!language) {
store.dispatch({
type: "OptiContentCloud/SetState",
currentLanguage: cfg.defaultLanguage
})
} else {
const cdAPI = context.serviceContainer.getService<IContentDeliveryAPI>(DefaultServices.ContentDeliveryAPI_V2);
cdAPI.Language = language || cfg.defaultLanguage;
}
}
/**
* Return the standard state reducer for the CMS Status
*/
public GetStateReducer : () => IStateReducerInfo<any> = () => CmsStateReducer;
}</code></pre>
<p>So what happens in this module? First, we see that there's no override of the ConfigureContainer method; this is possible as we're extending BaseInitializableModule that provides basic empty methods for each of the three required methods from IInitializableModule. So this module does not affect the Container itself. Then in the second step, it registers the reducer for Redux that will manage the state, then finally, when the site starts it ensures that we have a language in the state and that it is synchronized with the current language setting of the ContentDeliveryAPI.</p>
<p>Ok, you've created your IInitializableModule, great, but JavaScript doesn't allow for automatic class discovery. So the last step is to register it within your configuration.</p>
<pre class="language-javascript"><code>// Partial configuration shown, with just the items relevant to this article.
export const config : SpaConfig = {
// Initialization modules
modules: [
new CommerceInitialization()
]
}</code></pre>
<p>This being JavaScript, the instantiation is only required because the module extends BaseInitializableModule, it is possible to create a static object that implements the interface as well, however that would mean that you need to implement the full interface.</p>
<p>But wait, the title said it was possible to add application services? Yes, it is possible, as shown in the Routing Module. </p>
<pre class="language-javascript"><code>export default class RoutingModule extends BaseInitializableModule implements IInitializableModule
{
protected name = "Optimizely CMS Routing";
public readonly SortOrder = 20;
/**
* Ensure the configuration object within the service container contains a "*" route. If
* this "*" route is not claimed by the implementation, it will be added as fall-back to
* Episerver CMS based routing.
*
* @param {IServiceContainer} container The Service Container to update
*/
public ConfigureContainer(container: IServiceContainer) : void
{
const config = container.getService<AppConfig>(DefaultServices.Config);
let haveStar = false;
config.routes = config.routes || [];
config.routes.forEach(c => haveStar = haveStar || c.path === "*");
if (!haveStar) config.routes.push({
path: "*",
component: RoutedComponent
});
}
}</code></pre>
<p>So instead of getting a service from the container (in this case Configuration), it is also possible to set/add services into the container. The DefaultServices here is just a list of predefined strings, so for your own services, use whatever name you like. A good convention would be [Module].[Service], for example, "Routing.Router";</p>
<p> And that's a wrap. Feel free to star the project on GitHub, leave feedback here or on GitHub, use it to kick-start your own Optimizely powered SPA. Anyway, I hope this series gave a little insight into the inner workings of Foundation-Spa-React.</p>
<div class="row my-3 pt-3">
<div class="col col-4 text-left"><a href="/link/d58ebc0fd7d2407e8866797a1a872698.aspx">Part 5</a></div>
<div class="col col-4 text-center"><a href="/link/7f3e8668f64042cd8d81612654691318.aspx">Introduction</a></div>
<div class="col col-4 text-right">The end</div>
</div>Frontend: Routing & Custom components - Into Foundation Spa React series, Part 5/blogs/remko-jantzen/dates/2021/7/frontend-routing--custom-components---into-foundation-spa-react-series-part-5/2021-07-28T10:29:18.0000000Z<p>With the fifth installment of the "<a href="/link/7f3e8668f64042cd8d81612654691318.aspx">Into Foundation-Spa-React</a>" series, the focus shifts from the backend and high-level frontend implementation towards one of the key capabilities the SPA offers to enable you to not only provide content from within Content Cloud but from any source.</p>
<p>The content and logic from other sources can be exposed through specific components, which may be placed through Content Cloud or fixed within the layout. However, when you want to create "pages" which are not managed by Content Cloud, you need to access the routing - which you have.</p>
<div class="alert alert-secondary">
<h4>Take this example:</h4>
<p>You're building a site for a company in the leisure sector, most of the content will be taken and managed by Content Cloud, yet booking will be handled through an industry-specific booking platform. In this case, there'll be some widgets on some pages that will take input from that booking platform (e.g. date selection, availability, current booking, etc..) and some specific functional pages (e.g. current booking, checkout). Also, the "My Account" section will most likely show data from multiple sources.</p>
</div>
<p>The routing within the library is based upon the React-Router solution, which enables a high level of control over the routing within a SPA. By default, there's only one route, the "catch-all" (or "*") route, which gives the URL to Content Cloud to resolve to a content item. All routes you configure will be pre-pended to this catch-all route and thus take priority over URL managed within Content Cloud.</p>
<p>The configuration itself is pretty straightforward:</p>
<pre class="language-javascript"><code>{
...,
routes: RouteProps[]
}</code></pre>
<p>Here RouteProps, are the properties given to the Route component of React Router (<a href="https://reactrouter.com/web/api/Route">docs here</a>). As this is put in the configuration, all components you reference will be included in the initial bundle. This is something to avoid as there's no way to predict if the visitor will visit that route and thus bloat the initial download. To resolve this there're two options:</p>
<ul>
<li>Use the instruction from React to roll your own solution: <a href="https://reactjs.org/docs/code-splitting.html#reactlazy">Code-Splitting – React (reactjs.org)</a></li>
<li>Use the LazyComponent from the Components namespace, which uses the same extendable component loading logic and spinners as the other parts of the SPA.</li>
</ul>
<h3>Component resolution and loading</h3>
<p>By default the SPA can resolve all components, using their default export, as long as the component path starts with "app/Components". As you might want to alter this logic you can inject your own component loaders. The default component loader can be found <a href="https://github.com/episerver/foundation-lib-spa-core/blob/master/Loaders/CoreIComponentLoader.ts">here</a>.</p>
<p>To register your loaders, add them to the configuration:</p>
<pre class="language-javascript"><code>{
...,
componentLoaders: []
}</code></pre>
<p>You can provide either a fully instantiated object, or a class (implementing IComponentLoader). However, if you provide a class it will be instantiated without any constructor arguments.</p>
<p>To summarize, you have the ability to create your own components within the SPA, which can take data and logic from any system you choose to integrate it with. For those parts where you need to expose pages, the SPA allows you to configure these routes, including the components used to handle these routes.</p>
<p><em><strong>A final remark:</strong></em> When running the SPA within Content Cloud, such as Foundation-Spa-React, be aware that these "extra routes" can neither be resolved by Content Cloud, nor easily be server-side rendered and - if you don't change anything to Content Cloud - they will yield a 404 response when accessed directly, or the visitor hits "Reload".</p>
<div class="row my-3 pt-3">
<div class="col col-4 text-left"><a href="/link/c1de437609df4e36a8af700c926510c5.aspx">Part 4</a></div>
<div class="col col-4 text-center"><a href="/link/7f3e8668f64042cd8d81612654691318.aspx">Introduction</a></div>
<div class="col col-4 text-right"><a href="/link/9d64a1249aea4b3f8e55b40d09901735.aspx">Part 6</a></div>
</div>Frontend: Anatomy of an implementation - Into Foundation Spa React series, Part 4/blogs/remko-jantzen/dates/2021/6/frontend-anatomy-of-an-implementation---into-foundation-spa-react-series-part-4/2021-07-01T13:28:34.0000000Z<p>In this fourth installment of the "<a href="/link/7f3e8668f64042cd8d81612654691318.aspx">Into Foundation Spa React</a>" series, we move to the front end and investigate how that works. This will be a three-part section (parts 4, 5, and 6), where I'll first dive into the basics of implementing the frontend on top of Content Cloud running Headless, before moving into two advanced topics (routing and startup process) that enable you to build out the application.</p>
<p>Building a frontend using this solution, requires the following steps:</p>
<ul>
<li>Creating a development environment using Webpack for bundling, including:</li>
<ul>
<li>Registering the <code>app/</code> and <code>app.server/</code> module prefixes</li>
<li>Creating the Content Cloud config values</li>
<li>Add - if needed - a development server</li>
</ul>
<li>The entry file</li>
<li>Component per iContent type</li>
<li>Layout</li>
</ul>
<p>Throughout this post I will assume that TypeScript is the front end language of choice, to ensure that all code written is fully type-checked against the models created and managed in Content Cloud. There is nothing preventing you from using plain JavaScript, or different tooling, yet you will need to adapt these instructions then to your specific use case.</p>
<h2>Creating the development environment</h2>
<p>The <code>Spa.Frontend</code> folder contains a fully configured development environment, which is great to get a working frontend, yet might a bit daunting when trying to understand how it all goes together. The key bits to understand are:</p>
<ul>
<li>The <a href="https://github.com/episerver/Foundation-lib-spa-core">@episerver/spa-core</a> library contains all the heavy-lifting to start and execute a SPA that is driven by Content Cloud, without sacrificing capabilities editors know from a traditional MVC implementation.</li>
<li>The <a href="https://github.com/episerver/Foundation-lib-spa-webpack">@episerver/webpack</a> library contains the needed helpers to create a Webpack build/bundling setup</li>
<li>The <a href="https://github.com/episerver/Foundation-spa-react/tree/master/src/Spa.Frontend">Spa.Frontend</a> folder uses these libraries to deliver the experience by providing the components that can render each iContent type and the extensions to match extensions to Content Cloud (such as the Settings pages in this CMS implementation).</li>
</ul>
<p>Even though in Foundation-Spa-React bundles both the Content Cloud implementation and the SPA Frontend there is no requirement whatsoever to follow this pattern. There are no cross-dependencies between these projects. I will even go as far as to recommend separating the two if it's two separate teams working on it.</p>
<p>In this post, I'll assume that the frontend application has been created manually, from scratch using NPM 7.7+. It is possible to use <a href="https://github.com/facebook/create-react-app">create-react-app</a> as well, but you would need to set up <a href="https://github.com/gsoft-inc/craco">craco</a> to perform some configuration overrides - however that goes beyond the scope of this blog.</p>
<h3>Initial setup</h3>
<p>The first step is to create the project and add <a href="https://github.com/episerver/Foundation-lib-spa-core">@episerver/spa-core</a> as dependency and <a href="https://github.com/episerver/Foundation-lib-spa-webpack">@episerver/webpack</a> as a development dependency, together with TypeScript, Webpack, Webpack-CLI, and most likely you'll want Webpack-Dev-Server as well. Depending on your preferences and plan you may need to add other tools.</p>
<h3>Environment file creation</h3>
<p>Secondly, an environment file is required to make sure that the CLI tooling from <a href="https://github.com/episerver/Foundation-lib-spa-webpack">@episerver/webpack</a> understands how to communicate with Content Cloud during the development & release process.</p>
<p>So we're creating a .env.local file in the same folder as the package.json, for our local configuration - this file should be ignored by your versioning system. If you want to distribute environment configuration, place it in a .env file.</p>
<table>
<tbody>
<tr>
<td>
<p><strong>EPI_MODEL_PATH</strong></p>
</td>
<td>
<p><code>string</code></p>
</td>
<td>
<p>The path, relative to the folder containing package.json, where model files should be created by the model synchronization process.</p>
</td>
</tr>
<tr>
<td>
<p><strong>SRC_PATH</strong></p>
</td>
<td>
<p><code>string</code></p>
</td>
<td>
<p>The path, relative to the folder containing package.json, where the main application files are stored (the app/ namespace)</p>
</td>
</tr>
<tr>
<td>
<p><strong>SERVER_PATH</strong></p>
</td>
<td>
<p><code>string</code></p>
</td>
<td>
<p>The path, relative to the folder containing package.json, where the server-side rendering files are stored (the app.server/ namespace)</p>
</td>
</tr>
<tr>
<td>
<p><strong>EPI_URL</strong></p>
</td>
<td>
<p><code>string</code></p>
</td>
<td>
<p>The full domain where Content Cloud is running</p>
</td>
</tr>
<tr>
<td>
<p><strong>EPI_SPA_DOMAIN</strong></p>
</td>
<td>
<p><code>string</code></p>
</td>
<td>
<p>The domain where the SPA will be deployed</p>
</td>
</tr>
</tbody>
</table>
<p>There are more options and ways to override configuration, these are documented in the <a href="https://github.com/episerver/Foundation-spa-react/blob/master/src/Spa.Frontend/.env.dist">.env.dist file</a> in the repository.</p>
<h3>Register scripts</h3>
<p>Then in your package.json, add two scripts to get up and running:</p>
<pre class="language-javascript"><code>"scripts": {
"login": "npx epi-auth -e development",
"sync-models": "npx epi-sync-models -e development"
}</code></pre>
<p>The first script allows you to authenticate using the ContentDelivery-OAuth package provided by Optimizely, which stores your tokens locally. The second script uses those tokens to retrieve the models within Content Cloud and generate the appropriate TypeScript files. For both scripts the "-e" parameter sets the name of the environment to run against (and thus also .env selection), as well as a few other parameters execute them directly on the command line to get the help that lists all possible parameters.</p>
<h3>Add Namespaces</h3>
<p>Then the last step is to register the namespaces, this takes two steps:</p>
<ol>
<li>
<p>In your Webpack configuration file, import the <code>@episerver/webpack</code> library and create an instance of the Configuration object.</p>
<pre class="language-javascript"><code>const EpiWebpack = require("@episerver/webpack");
const config = new EpiWebpack.Config( {dirname}, {envvars}, {envname});</code></pre>
<p>The three parameters are:</p>
<ol>
<li><strong>dirname:</strong> The absolute path to the package.json file (e.g. __dirname)</li>
<li><strong>envvars:</strong> The key/value pairs from the current environment (e.g. As reported by Webpack or process.env)</li>
<li><strong>envname:</strong> The name of the environment to build for, taken from envvars or hard-coded (e.g. envvars.EPI_ENV || process.env.EPI_ENV)</li>
</ol>
<p>Then for the "resolve" of Webpack, either extend your config with or replace it with: <code>config.getResolveConfig()</code>.</p>
</li>
<li>In your typescript config file, explicitly create the "path" option within the "compilerOptions", for "app" and "app.server". There's no way to inject scripts in this file, so this needs to be done manually.</li>
</ol>
<p>If this is has been completed, you should be able to run your solution in a local development server, and have access to the models using app/Models/Content/... within your code. Verify that all routes that don't resolve to an asset will be handled by your entry file.</p>
<h2>The entry file</h2>
<p>The entry file is the main script to be executed and differs from a "standard" React entry script in the sense that it only configures and starts the application provided by @episerver/spa-core. As such you won't see any ReactDOM.render or ReactDOM.hydrate calls in this file (though these are used by @episerver/spa-core).</p>
<h3>Application configuration</h3>
<p>Before we can create the entry file, we need to create a configuration file. I've created the one in Foundation-Spa-React in src/config.ts, yet this is fully up to your preferences. The src/config.ts must be a module that has a default export of type Core.IConfig. So in short:</p>
<pre class="language-javascript"><code>import { Core } from '@episerver/spa-core';
export const Config : Core.IConfig = {
...
};
export default Config;</code></pre>
<p>This configuration file has many options, most are documented in the Core.IConfig type, so use that as your guide. In the example in Foundation-Spa-React, you'll see a reference to process.env.EPI_URL; this has been achieved by using the Webpack DefinePlugin, with the configuration created by the config object described under "Add Namespaces" in the previous step.</p>
<h3>The entry file</h3>
<p>Now having a configuration, which ideally leverages the DefinePlugin from Webpack to ensure consistency between the developer tooling and build process, and the bundling process ready to go, it's time to create the entry file.</p>
<p>For an implementation without much custom logic in the SPA, there's no need for a lot of code in the entry file. In fact, it doesn't even need to be a JSX/TSX file as it'll work from just JavaScript/TypeScript.</p>
<p>The first step is to import the start script, which is the default export from foundation-spa-lib-core. Then we need to collect the configuration and run the start script. Optionally it is possible to provide some advanced configuration if you have the need to interact with the bootstrapping process. This is also the place to load any global (non-inline) styles.</p>
<pre class="language-javascript"><code>// Import framework
import start, { Core } from '@episerver/spa-core'
// Import configuration
import config from 'app/Config'
// Get global styles
import './global.scss'
// Construct parameters
const serviceContainer : Core.IServiceContainer | undefined = undefined;
const containerElementId : string | undefined = preload.appId;
const ssr : boolean | undefined = false;
// Start the website
start(config, serviceContainer, containerElementId, ssr);</code></pre>
<h2>Component per iContent type</h2>
<p>The application will query Content Cloud with the current URL to have it resolved to a specific content item, using the contentUrl parameter of the <a href="/link/19575b81cd774e2d9edc96bf8873c703.aspx#/ContentApi/ContentApi_QueryContent">QueryContent function of the Content Delivery API</a>. Then it will use the response to determine the component matching the content returned by the content delivery API and hand the response to that component for rendering. By default, the application will take the default export (or module.exports.default if you're using CommonJS modules) of the module created by <code>app/Components/{ contentItem.contentType.join("/") }</code>. Within a Content Area property, it will prepend a first value (by default: "Block") to the content type, if it doesn't already start with this value.</p>
<h3>Examples</h3>
<table>
<tbody>
<tr>
<td>
<p><strong>Content Type</strong></p>
</td>
<td>
<p><strong>Resolution method</strong></p>
</td>
<td>
<p><strong>Component module</strong></p>
</td>
</tr>
<tr>
<td>
<p>["Page", "StandardPage"]</p>
</td>
<td>
<p>From URL or Content Reference</p>
</td>
<td>
<p>app/Components/Page/StandardPage</p>
</td>
</tr>
<tr>
<td>
<p>["Page", "StandardPage"]</p>
</td>
<td>
<p>Within a Content Area</p>
</td>
<td>
<p>app/Components/Block/Page/StandardPage</p>
</td>
</tr>
<tr>
<td>
<p>["Block", "TextBlock"]</p>
</td>
<td>
<p>From URL or Content Reference</p>
</td>
<td>
<p>app/Components/Block/TextBlock</p>
</td>
</tr>
<tr>
<td>
<p>["Block", "TextBlock"]</p>
</td>
<td>
<p>Within a Content Area</p>
</td>
<td>
<p>app/Components/Block/TextBlock</p>
</td>
</tr>
</tbody>
</table>
<h3>Rendering properties</h3>
<p>Within Content Cloud, there're specific property types, which hold references, such as the ContentReference, PageReference, and ContentArea property types, secondly Content Cloud requires specific additions to the generated HTML structure in order to make the content editable using on-page editing. To take full advantage of these headless capabilities of Content Cloud, there is a specific "Property" component available to render these properties.</p>
<p>The basics for using this component are:</p>
<pre class="language-javascript"><code>import React, { FunctionComponent } from 'react';
import { Components } from '@episerver/spa-core';
import { TextBlockProps } from 'app/Models/Content/TextBlockData';
export const TextBlock : FunctionComponent<TextBlockProps> = (props) => {
return <div className="TextBlock">
<Components.Property iContent={ props.data } field="mainBody" />
</div>
}
export default TextBlock;</code></pre>
<p>What's also directly apparent from this example is the usage of the TextBlockProps from the auto-generated model to inform the TypeScript engine about all properties provided to the TextBlock component.</p>
<h2>Layout</h2>
<p>Last, but certainly not least, is the Layout component, which can be provided through the configuration. This Layout component will be placed within the site and context providers of the application (i.e. you can use the hooks provided by @episerver/spa-core and React-Redux within a layout) and it will be given the router component (based upon React-Router) through its children property. This allows the Layout to serve two specific needs:</p>
<ol>
<li>Provide the header/footer components that surround the routed content. (Though, keep in mind that the provides from React-Router are below the layout in the component tree and thus not accessible.</li>
<li>Provide the capability to inject your own providers for the components you're creating.</li>
</ol>
<h2>Next steps</h2>
<p>This is just scratching the surface of the capabilities provided to a spa implementation using these components. A good starting point is to look at the hooks provided for functional components and the IEpiserverContext (retrieved by the <code>useEpiserver()</code> hook.</p>
<p>Looking to add bespoke providers or capabilities? The Foundation-Spa-React contains a companion implementation for the Settings Pages of Foundation (MVC-CMS) in the lib/foundation-settings folder, which is then injected by app/Components/Shared/MoseyLayout as part of the Layout of the Spa.</p>
<div class="row my-3 pt-3">
<div class="col col-4 text-left"><a href="/link/b9d14940983f4844a598cca16b570f65.aspx">Part 3</a></div>
<div class="col col-4 text-center"><a href="/link/7f3e8668f64042cd8d81612654691318.aspx">Introduction</a></div>
<div class="col col-4 text-right"><a href="/link/d58ebc0fd7d2407e8866797a1a872698.aspx">Part 5</a></div>
</div>Backend: Achieving SEO, Accessibility and separate Frontend deployment - Into Foundation Spa React series, Part 3/blogs/remko-jantzen/dates/2021/6/backend-achieving-seo-accessibility-and-separate-frontend-deployment---into-foundation-spa-react-series-part-3/2021-06-30T07:13:03.0000000Z<p>In the third installment of the "<a href="/link/7f3e8668f64042cd8d81612654691318.aspx">Into Foundation Spa React Series</a>", I'm adding SEO, Accessibility, and a Separated Frontend Workflow. The code for this can be found in the <a href="https://github.com/episerver/Foundation-spa-react/tree/master/src/Foundation.SpaViewEngine">Foundation.SpaViewEngine</a> project, which is part of the Foundation Spa React project on GitHub. This blog post aims to give you a good starting point when investigating that code.</p>
<div class="alert alert-primary">
<p><span style="font-weight: bold; font-style: italic;">Full disclosure:</span> I'm aware that there're controversial decisions in this section and, yes, feel free to suggest improvements. However, the ability to deploy the SPA without any additional services beyond the Optimizely Digital Experience Platform should not be affected.</p>
</div>
<h2>Solution Outline</h2>
<p>Assuming that the frontend will be delivered as a set of JavaScript, CSS, and HTML files, we need to achieve two goals:</p>
<ul>
<li><em><strong>Static asset storage:</strong></em> Allow the storage of these assets within the .Net project, in a way that does not require a .Net deployment to update the assets and serve them to the public when requested.</li>
<li><em><strong>Server-side template execution:</strong></em> Allow server-side (pre-)rendering of static pages that can be <a href="https://reactjs.org/docs/react-dom.html#hydrate">hydrated</a> on the client.</li>
</ul>
<p>These are then also the two parts that create are part of the SpaViewEngine Project, though relatively separated within the project.</p>
<h2>Static Asset Storage</h2>
<p>The first challenge to overcome is the storage location, where the decision needed to be made at which level to interact with Content Cloud. Two criteria's needed to be satisfied by the solution:</p>
<ul>
<li>No duplication of logic already in Content Cloud, but reuse existing logic</li>
<li>Works both on DXP as well as in self-hosted scenario's</li>
<li>Simple and straight-forward solution</li>
</ul>
<p>The sole solution that was identified to meet all these criteria was to use a Media IContent type, so version control, access rights, and storage of the binary data were already covered. With this solution, the risk is introduced to use a lot of storage space (each version) as well as generating potentially hundreds of IContent items to just store the assets. The storage space can not be overcome but can be mitigated by means of file compression and the number of IContent items risk is mitigated by archiving the assets into one archive file prior to uploading into Content Cloud. With support needed for both Windows and non-Windows development environments, the Zip compression & archiving format was selected. Last, but not least, in order to prevent a collision on the file extension with existing Media types, the file extension has been defined to be "spa".</p>
<p>So, let us dive into the actual implementation.</p>
<p>The frontend IContent items are kept out-of-view of most editors by generating a single IContent entry of type SpaFolder under the root, outside of the websites. This IContent entry exists to provide the tree structure that will hold the static assets and can be used to manage access rights.</p>
<p>The creation and update of SpaMedia items are done by the <code>SpaMediaDeploymentApiController</code>, which is a standard .Net API Controller, which takes an uploaded file and stores as binary data with an IContent item. To ensure that this is only accessible by authorized frontend developers, there are two access rights that must be satisfied:</p>
<ol>
<li>The current user must have the function permission: "DeploySpa", for the service to start processing the request.</li>
<li>The current user must have the right to Edit & Publish the new SpaMedia item/version under the SpaFolder node.</li>
</ol>
<p>This allows for a setup where a functional/technical administrative user might be allowed to roll back a newly deployed version, without allowing that user to deploy new versions using the API.</p>
<p>With the assets being able to be deployed into Content Cloud using this setup, the last piece of the puzzle is to let these assets being requested by the browser. This is relatively easily achieved by leveraging the partial routing support within .Net Framework, all routes matching <code>spaview/{container}/{*path}</code> will be given to our <code>SpaMediaAssetController</code>. The job of this controller is pretty straightforward, it will try loading the container (SpaMedia, by its name) and asset, based on the path inside the container. The controller itself modifies the Response so that it will add the appropriate headers to enable caching, and removes the cookies from the Response so that a CDN will recognize the assets as being static.</p>
<h2>Server-Side Template Execution</h2>
<p>Having static assets is great, however, for server-side rendering, we need to make sure that all content entered and (re-)organized by editors is represented correctly when the site is accessed by a browser that does not execute JavaScript. Typical use-cases are search engines (SEO) and assistive technologies (WCAG). With the front end delivered as a JavaScript application, this JavaScript needs to be executed to generate the HTML.</p>
<p>To support this within the current state of the DXP, the SpaViewEngine adds a new ViewEngine to .Net, which takes priority over Razor and leverages the <a href="https://github.com/Taritsyn/JavaScriptEngineSwitcher">JavaScriptEngineSwitcher</a> project to offer Server-Side JavaScript execution. The current implementation defaults to (and has only be tested with) V8 as an engine, but has been designed to have the engine configured through the standard dependency injection of Content Cloud.</p>
<p>To bridge the gap between the .Net and JavaScript, it adds a number of poly-fills (core-js) and the <code>epi</code> global variable. Furthermore, it exposes the current content through <code>__INITIAL__DATA__</code> and some advanced features through <code>__EpiserverAPI__</code> and offers wrappers that make it easier to work with the .Net objects from JavaScript.</p>
<p>The most important part here is that it takes a specific server-side rendering built of the application, allowing to use of different bundling strategies between the browser and server-side rendering capabilities. The server-side rendering is not bound to react, but its signature is highly designed after the capabilities of react-helmet. The ViewEngine assumes that the server-side bundle implements the global, parameter-less function <span style="font-style: italic;">render()</span>, with a return type that can be understood to be of type SSRResponse. Within the render() method, the server-side logic can use the data and APIs in <code>__INITIAL__DATA__</code> and <code>__EpiserverAPI__</code> to render the current content.</p>
<div class="row my-3 pt-3">
<div class="col col-4 text-left"><a href="/link/4bd46a60192c45f5ab6a8a659ed36532.aspx">Part 2</a></div>
<div class="col col-4 text-center"><a href="/link/7f3e8668f64042cd8d81612654691318.aspx">Introduction</a></div>
<div class="col col-4 text-right"><a href="/link/c1de437609df4e36a8af700c926510c5.aspx">Part 4</a></div>
</div>Backend: Enabling .Net MVC parity in a SPA - Into Foundation Spa React series, Part 2/blogs/remko-jantzen/dates/2021/6/backend-enabling--net-mvc-parity-in-a-spa---into-foundation-spa-react-series-part-2/2021-06-29T12:23:01.0000000Z<p>Continuing the "<a href="/link/7f3e8668f64042cd8d81612654691318.aspx">Into Foundation Spa React Series</a>", the first stop we make is the enabling of feature parity with MVC CMS and all editor productivity tooling that comes with that. This post focuses on the backend, so we won't be looking at any frontend capabilities. Some of the steps here are also part of the documentation, however, I've elected to provide a holistic overview of all changes in one place.</p>
<div class="alert alert-primary">
<p><em><strong>NOTE: </strong></em>This solution was created before the Content Management API was released, which is a great add-on to the headless capabilities of the CMS. Yet, in the scope of this endeavor: we will not be using direct updates from the frontend into the CMS, but rely on the built-in capability of the CMS to manage and store content. So we will be extending the Content Delivery API where needed and not use the Content Management API.</p>
</div>
<p>First and foremost, let's start by listing out the capabilities from the CMS we want to enable:</p>
<ul>
<li>On-page editing of <em><strong>any </strong></em>version</li>
<li>Version comparisons</li>
<li>Projects</li>
<li>Visitor group preview</li>
<li>Advanced Reviews</li>
<li>Common drafts preview mode</li>
<li>Hosting business logic</li>
</ul>
<p>Next to these CMS capabilities, each content type can have its own logic in the form of a controller. Whether or not this is a "good thing", considering a headless approach, most of the developers working with the CMS will be accustomed to putting some logic into the controller in order to prepare the data for the view. For example, the execution of queries to Search & Navigation. Nevertheless, within a headless world, it is undesirable to move this logic (e.g. Constructing a search query from content properties), from the backend to the frontend, as that will require that logic to be managed on every head.</p>
<p>So we'll be adding the capability to execute any public controller method to the Content Delivery API in order to retain this development pattern and make migrations from a traditional MVC implementation to headless a lot easier. At the same time, we'll be doing this in a way that feels "natural" within the REST structure of the Content Delivery API.</p>
<h2>Context mode</h2>
<p>The API to get the context mode of the current request is the <code>EPiServer.Web.IContextModeResolver</code> and, specifically within the ContentDeliveryAPI, by <code>EPiServer.ContentApi.Core.IContentModeResolver</code>. However the EPiServer.Web implementation doesn't work for ContentAPI requests and the ContentAPI.Core implementation doesn't set the values expected by other CMS extensions.</p>
<p>Hence, I've added <code>ContentApiContextModeResolver</code>, which implements both interfaces and replaces both standard implementations in the service container. It has been constructed in a way that regardless of entry (regular, content delivery API), the context mode is set and exposed the way it is expected throughout the application. This is the first step to supporting any add-on through the ContentDelivery API.</p>
<pre class="language-csharp"><code> [ServiceConfiguration(Lifecycle = ServiceInstanceScope.Hybrid)]
[ServiceConfiguration(typeof(EPiServer.ContentApi.Core.IContextModeResolver), Lifecycle = ServiceInstanceScope.Hybrid)]
[ServiceConfiguration(typeof(IContextModeResolver), Lifecycle = ServiceInstanceScope.Hybrid)]
[ServiceConfiguration(typeof(ContextModeResolver), Lifecycle = ServiceInstanceScope.Hybrid)]
public class ContentApiContextModeResolver : ContextModeResolver, EPiServer.ContentApi.Core.IContextModeResolver, IContextModeResolver
{
protected readonly ServiceAccessor<HttpContextBase> _httpContextAccessor;
protected readonly ServiceAccessor<IModuleResourceResolver> _moduleResourceResolverAccessor;
protected readonly Regex _editRouteRegex = new Regex(",{2}\\d+");
public ContextMode DefaultContextMode { get; set; } = ContextMode.Default;
public ContentApiContextModeResolver(
ServiceAccessor<HttpContextBase> httpContextAccessor,
ServiceAccessor<IModuleResourceResolver> moduleResourceResolverAccessor
) {
_httpContextAccessor = httpContextAccessor;
_moduleResourceResolverAccessor = moduleResourceResolverAccessor;
}
ContextMode IContextModeResolver.CurrentMode => CurrentMode();
public override ContextMode CurrentMode()
{
var httpContext = _httpContextAccessor();
if (httpContext == null || httpContext.Request == null)
return DefaultContextMode;
if (httpContext.PreviousHandler != null && httpContext.CurrentHandler != httpContext.PreviousHandler)
return DefaultContextMode;
if (httpContext.Request.RequestContext != null && httpContext.Request.RequestContext.HasApiContextMode())
return httpContext.Request.RequestContext.GetApiContextMode(DefaultContextMode);
var contextMode = Resolve(httpContext.Request.RawUrl, DefaultContextMode);
if (httpContext.Request.RequestContext != null)
httpContext.Request.RequestContext.SetApiContextMode(contextMode);
return contextMode;
}
public ContextMode Resolve(string contentUrl, ContextMode defaultContextMode)
{
var urlBuilder = new UrlBuilder(contentUrl);
if (IsCmsUrl(urlBuilder) || IsApiUrl(urlBuilder)) {
if (IsEditingActive(urlBuilder))
return ContextMode.Edit;
if (IsPreviewingActive(urlBuilder))
return ContextMode.Preview;
}
return defaultContextMode;
}
protected virtual bool IsCmsUrl(UrlBuilder contentUrl)
{
var moduleResourceResolver = _moduleResourceResolverAccessor();
return moduleResourceResolver == null || contentUrl.Path.StartsWith(moduleResourceResolver.ResolvePath("CMS", (string)null), StringComparison.OrdinalIgnoreCase);
}
protected virtual bool IsApiUrl(UrlBuilder contentUrl)
{
return contentUrl.Path.StartsWith("/api/episerver/", StringComparison.OrdinalIgnoreCase);
}
protected virtual bool IsEditingActive(UrlBuilder urlBuilder)
{
if (urlBuilder.QueryCollection[PageEditing.EpiEditMode] != null)
{
return urlBuilder.QueryCollection[PageEditing.EpiEditMode].Equals("true", StringComparison.OrdinalIgnoreCase);
}
return false;
}
protected virtual bool IsPreviewingActive(UrlBuilder urlBuilder)
{
return !string.IsNullOrEmpty(urlBuilder.Path) && _editRouteRegex.IsMatch(urlBuilder.Path);
}
}</code></pre>
<p>The next step is to ensure that the context mode is correctly propagated through the URLs returned by the Content Delivery API, so all subsequent requests will have the same context mode. As the content delivery API always returns the "default" URL, we need to provide a custom implementation of the UrlResolverService. This is the CurrentContextUrlResolverService.</p>
<pre class="language-csharp"><code> public class CurrentContextUrlResolverService : UrlResolverService
{
private readonly ContentApiConfiguration _contentApiConfiguration;
protected readonly IContextModeResolver _contextModeResolver;
public CurrentContextUrlResolverService(
UrlResolver urlResolver,
ContentApiConfiguration contentApiConfiguration,
IContextModeResolver contextModeResolver
) : base(urlResolver, contentApiConfiguration)
{
_contentApiConfiguration = contentApiConfiguration;
_contextModeResolver = contextModeResolver;
}
public override string ResolveUrl(string internalLink)
{
var contentApiOptions = _contentApiConfiguration.Default();
return _urlResolver.GetUrl(new UrlBuilder(internalLink), new VirtualPathArguments
{
ContextMode = _contextModeResolver.CurrentMode,
ForceCanonical = true,
ForceAbsolute = contentApiOptions.ForceAbsolute,
ValidateTemplate = contentApiOptions.ValidateTemplateForContentUrl
});
}
public override string ResolveUrl(ContentReference contentLink, string language)
{
var contentApiOptions = _contentApiConfiguration.Default();
return _urlResolver.GetUrl(contentLink, language, new VirtualPathArguments
{
ContextMode = _contextModeResolver.CurrentMode,
ForceCanonical = true,
ForceAbsolute = contentApiOptions.ForceAbsolute,
ValidateTemplate = contentApiOptions.ValidateTemplateForContentUrl
});
}
}</code></pre>
<p>When we combine the above with the ServiceConfigurationContext extension methods <code>.ConfigureForExternalTemplates()</code> and <code>.ConfigureForContentDeliveryClient()</code>, we can now fetch any version of the content as long as the current user is authorized to see that content and the included URLs enable navigation within edit mode. Also, all add-ons, event-handlers, etc.. that are triggered by the content loading of the content delivery API will properly understand if the current load is for an edit mode or not.</p>
<p>Using this we can now check: "<em>On page editing of any version</em>", "<em>Version comparisons</em>", and "<em>Advanced Reviews</em>".</p>
<h2>Projects</h2>
<p>Content Cloud offers projects to easily group multiple changes into one "package" that can be published, it's a great productivity tool, so why not allow projects to be used within a SPA? Great, now does it work with the above? Well, no, to load project content it is referred to by identifier and a project id, within edit mode, not the version id. So we need to extend the Content Delivery API to understand the epiprojects= query string parameter (that's the query string parameter used by the on-page editing) and expose that content.</p>
<p>This is achieved by replacing the default content loader with a slightly extended one that adds support for the project loader. This is the ProjectAwareContentLoaderService, which has two main tasks:</p>
<ol>
<li>Add the project ids to the content loader, once it is being created</li>
<li>Allow content items that are part of the current project to be exposed by the ContentDeliveryAPI</li>
</ol>
<pre class="language-csharp"><code> public class ProjectAwareContentLoaderService : ContentLoaderService
{
protected readonly IProjectResolver projectResolver;
protected readonly EPiServer.Web.IContextModeResolver contextModeResolver;
public ProjectAwareContentLoaderService(
IContentLoader contentLoader,
EPiServer.Web.IPermanentLinkMapper permanentLinkMapper,
IUrlResolver urlResolver,
EPiServer.ContentApi.Core.IContextModeResolver contextModeResolver,
IContentProviderManager providerManager,
IProjectResolver projectResolver,
EPiServer.Web.IContextModeResolver coreContextModeResolver
) : base(contentLoader, permanentLinkMapper, urlResolver, contextModeResolver, providerManager)
{
this.projectResolver = projectResolver;
this.contextModeResolver = coreContextModeResolver;
}
protected override LanguageSelector CreateLoaderOptions(string language, bool shouldUseMasterIfFallbackNotExist = false)
{
var options = base.CreateLoaderOptions(language, shouldUseMasterIfFallbackNotExist);
IEnumerable<int> currentProjects = projectResolver.GetCurrentProjects();
if (currentProjects.Count() > 0)
options.Setup<ProjectLoaderOption>(x => x.ProjectIds = currentProjects);
return options;
}
protected override bool ShouldContentBeExposed(IContent content)
{
if (contextModeResolver.CurrentMode.EditOrPreview())
return true;
IEnumerable<int> currentProjects = projectResolver.GetCurrentProjects();
if (currentProjects.Count() > 0)
{
var projectItems = ServiceLocator.Current.GetInstance<ProjectRepository>().GetItems(new ContentReference[] { content.ContentLink });
if (projectItems.Any(pi => currentProjects.Contains(pi.ProjectID)))
return true;
}
return base.ShouldContentBeExposed(content);
}
}</code></pre>
<p>The project-resolver from the Core CMS already validates access to the projects. The main caveat with the current implementation is that it won't validate access to the content item for the current item. This risks exposing some content to editors for which they do not have read access when it's in a project.</p>
<p>So now we check: "<em>Projects</em>"</p>
<h2>Visitor group preview</h2>
<p>The content delivery API is fully personalizable using visitor groups, so being able to preview as a specific visitor group to get an understanding of the experience that will be generated, is key to leveraging this capability. So here we follow the suggestion from <a href="/link/92a8ac7b541b464482f6b5f32ce873e5.aspx#CustomizeVistiorGroup">Episerver World</a>, by adding a specific model mapper for IContent and edit mode. In this implementation, we digress from the suggestion on Episerver World, as we already have a good solution to determine the current context mode using the standard APIs of the CMS. This is the VisitorGroupContentModelMapper, which checks if we're impersonating a visitor group and then runs the appropriate extension method to set up the impersonation.</p>
<pre class="language-csharp"><code> [ServiceConfiguration(typeof(IContentModelMapper))]
class VisitorGroupContentModelMapper : ContentModelMapperBase
{
private readonly ServiceAccessor<HttpContextBase> _httpContextAccessor;
private readonly ServiceAccessor<EPiServer.Web.IContextModeResolver> _contextModeAccessor;
public VisitorGroupContentModelMapper(IContentTypeRepository contentTypeRepository,
ReflectionService reflectionService,
IContentModelReferenceConverter contentModelService,
IContentVersionRepository contentVersionRepository,
ContentLoaderService contentLoaderService,
UrlResolverService urlResolverService,
ContentApiConfiguration apiConfig,
IPropertyConverterResolver propertyConverterResolver,
ServiceAccessor<HttpContextBase> httpContextAccessor,
ServiceAccessor<EPiServer.Web.IContextModeResolver> contextModeAccessor)
: base(contentTypeRepository,
reflectionService,
contentModelService,
contentVersionRepository,
contentLoaderService,
urlResolverService,
apiConfig,
propertyConverterResolver)
{
_httpContextAccessor = httpContextAccessor;
_contextModeAccessor = contextModeAccessor;
}
public override int Order
{
get
{
return 200;
}
}
public override ContentApiModel TransformContent(IContent content, bool excludePersonalizedContent, string expand)
{
var httpContext = _httpContextAccessor();
var visitorGroupId = httpContext?.Request.QueryString[VisitorGroupHelpers.VisitorGroupKeyByID];
if (!string.IsNullOrEmpty(visitorGroupId))
{
httpContext.SetupVisitorGroupImpersonation(content, AccessLevel.Read);
}
return base.TransformContent(content, excludePersonalizedContent, expand);
}
public override bool CanHandle<T>(T content)
{
return _contextModeAccessor().CurrentMode.EditOrPreview() && content is IContent;
}
}</code></pre>
<p>So now we can check: "<em>Visitor group preview</em>"</p>
<h2>Common drafts preview mode</h2>
<p>The Advanced CMS add-on provides quite a nice capability, the "common drafts preview mode", this allows an editor to see the page with the "unpublished" version of all blocks on that page. This gives a preview of what will happen when the page and all blocks will be published in one go (again a feature of Advanced CMS).</p>
<p>In order to support this, I've replaced the standard model for a content area to load the common draft version when in edit mode and the common draft preview is active. This requires two steps:</p>
<ol>
<li>Create a new instance of the PropertyContentArea, with the updated logic (ContentAreaPropertyModel)</li>
<li>Create a new converter, that takes priority over the default one (ContentAreaPropertyModelConverter) and make it use the new PropertyModel.</li>
</ol>
<pre class="language-csharp"><code> class ContentAreaPropertyModel : CollectionPropertyModelBase<ContentAreaItemModel, PropertyContentArea>
{
protected readonly IContextModeResolver _contextModeResolver;
public ContentAreaPropertyModel(
PropertyContentArea propertyContentArea,
bool excludePersonalizedContent
) : this(
propertyContentArea,
excludePersonalizedContent,
ServiceLocator.Current.GetInstance<ContentLoaderService>(),
ServiceLocator.Current.GetInstance<IContentModelMapper>(),
ServiceLocator.Current.GetInstance<IContentAccessEvaluator>(),
ServiceLocator.Current.GetInstance<ISecurityPrincipal>(),
ServiceLocator.Current.GetInstance<IContextModeResolver>()
) { }
public ContentAreaPropertyModel(
PropertyContentArea propertyContentArea,
bool excludePersonalizedContent,
ContentLoaderService contentLoaderService,
IContentModelMapper contentModelMapper,
IContentAccessEvaluator accessEvaluator,
ISecurityPrincipal principalAccessor,
IContextModeResolver contextModeResolver
) : base(
propertyContentArea,
excludePersonalizedContent,
contentLoaderService,
contentModelMapper,
accessEvaluator,
principalAccessor
) {
_contextModeResolver = contextModeResolver;
}
public ContentAreaPropertyModel(
PropertyContentArea propertyContentArea,
ConverterContext converterContext
) : base(propertyContentArea, converterContext)
{
_contextModeResolver = ServiceLocator.Current.GetInstance<IContextModeResolver>();
}
public ContentAreaPropertyModel(
PropertyContentArea propertyContentArea,
ConverterContext converterContext,
ContentLoaderService contentLoaderService,
ContentConvertingService contentConvertingService,
IContentAccessEvaluator accessEvaluator,
ISecurityPrincipal principalAccessor,
IContextModeResolver contextModeResolver
) : base(
propertyContentArea,
converterContext,
contentLoaderService,
contentConvertingService,
accessEvaluator,
principalAccessor
) {
_contextModeResolver = contextModeResolver;
}
protected virtual IEnumerable<ContentAreaItem> FilteredItems(
ContentArea contentArea,
bool excludePersonalizedContent)
{
IPrincipal principal = excludePersonalizedContent ? _principalAccessor.GetAnonymousPrincipal() : _principalAccessor.GetCurrentPrincipal();
return contentArea.Fragments.GetFilteredFragments(principal).OfType<ContentFragment>().Select(f => new ContentAreaItem(f));
}
protected virtual ContentAreaItemModel CreateItemModel(ContentAreaItem item)
{
ContentVersion contentVersion = null;
if (_contextModeResolver.CurrentMode.EditOrPreview() && ContentDraftView.IsInContentDraftViewMode)
{
contentVersion = GetLatestVersion(item, true);
}
return new ContentAreaItemModel()
{
ContentLink = new ContentModelReference()
{
GuidValue = new Guid?(item.ContentGuid),
Id = new int?(contentVersion != null ? contentVersion.ContentLink.ID : item.ContentLink.ID),
WorkId = new int?(contentVersion != null ? contentVersion.ContentLink.WorkID : item.ContentLink.WorkID),
ProviderName = contentVersion != null ? contentVersion.ContentLink.ProviderName : item.ContentLink.ProviderName
},
DisplayOption = item.RenderSettings.ContainsKey(ContentFragment.ContentDisplayOptionAttributeName) ? item.RenderSettings[ContentFragment.ContentDisplayOptionAttributeName].ToString() : ""
};
}
protected virtual ContentVersion GetLatestVersion(ContentAreaItem item, bool nullIfPublished = true)
{
LanguageResolver languageResolver = ServiceLocator.Current.GetInstance<LanguageResolver>();
var contentVersion = ServiceLocator.Current.GetInstance<IContentVersionRepository>().LoadCommonDraft(item.ContentLink, languageResolver.GetPreferredCulture().Name); //Language issues ahead?
return nullIfPublished && contentVersion.Status == VersionStatus.Published ? null : contentVersion;
}
protected override IEnumerable<ContentAreaItemModel> GetValue() => !(_propertyLongString.Value is ContentArea contentArea) ? null : FilteredItems(contentArea, _excludePersonalizedContent).Select(x => CreateItemModel(x));
}</code></pre>
<pre class="language-csharp"><code> [ServiceConfiguration(typeof(IPropertyModelConverter), Lifecycle = ServiceInstanceScope.Singleton)]
class ContentAreaPropertyModelConverter : DefaultPropertyModelConverter, IPropertyModelConverter
{
public override int SortOrder => 1000;
public ContentAreaPropertyModelConverter() : base () { }
public ContentAreaPropertyModelConverter(ReflectionService reflectionService) : base (reflectionService) { }
protected override IEnumerable<TypeModel> InitializeModelTypes() => new TypeModel[] { new TypeModel {
ModelType = typeof(ContentAreaPropertyModel),
ModelTypeString = typeof(ContentAreaPropertyModel).FullName,
PropertyType = typeof(PropertyContentArea)
} };
}</code></pre>
<p>So now we can now place the second last check: "Common drafts preview mode"</p>
<h2>Hosting business logic</h2>
<p>In a "headless" setup, the assumption is "multi-head". So what about business logic (for example: building a search query from parameters entered by an editor), or any other bespoke logic. In this scenario, the fact that Content Cloud (even on DXP) can be built is key to the solution, as it allows the addition of bespoke APIs to a solution. However what about APIs that require the context of a piece of content? Traditionally this logic would have been placed in the .Net controllers. Well within the project, there's a specific API controller, which enables a developer to use IContent controllers to implement logic tied to content.</p>
<p>This is enabled by the ControllerActionApiController, which adds the capability to execute a controller method, from the ContentDeliveryAPI (<code>/api/episerver/v3/action/{id}/{method}</code>). For convenience, it also adds a specific route to allow access to page methods: <code>{page Route}/{method}</code>, but this requires the appropriate headers to be set to have the request being handled by the Content Delivery API.</p>
<p>Showing the logic of this controller will take quite a bit of space, hence, in this case, I'll suffice by pointing you directly to the implementation on GitHub: <a href="https://github.com/episerver/Foundation-spa-react/blob/master/src/Foundation.ContentDelivery/Controller/ControllerActionApiController.cs">src\Foundation.ContentDelivery\Controller\ControllerActionApiController.cs</a>.</p>
<div class="row my-3 pt-3">
<div class="col col-4 text-left"><a href="/link/7f3e8668f64042cd8d81612654691318.aspx">Part 1</a></div>
<div class="col col-4 text-center"><a href="/link/7f3e8668f64042cd8d81612654691318.aspx">Introduction</a></div>
<div class="col col-4 text-right"><a href="/link/b9d14940983f4844a598cca16b570f65.aspx">Part 3</a></div>
</div>Into Foundation Spa React series/blogs/remko-jantzen/dates/2021/6/into-foundation-spa-react-series/2021-06-29T12:20:01.0000000Z<p>It has been a while since I have written about the Headless CMS and React frontend implementation for Foundation. To say that nothing has changed with the project since is an understatement. Since that blog post, significant parts of the solution have been overhauled and more advanced features have been added, bringing the React frontend almost up-to-par with a .Net MVC-based frontend.</p>
<p>This is the first blog post in a series, that will go into the various aspects of the Headless setup and React SPA and how this enables a headless deployment into Optimizely DXP (Content Cloud). The series will first dive into the backend extensions to Foundation-MVC-CMS (or any Content Cloud implementation) and then focus on the frontend.</p>
<h2>Solution aims and constraints</h2>
<p>Before going into the details of the solution, I will first reiterate and expand on the design constraints and aims I set out with when designing and creating Foundation-Spa-React. I am fully aware that some of these constraints are not there for everyone and have inspired some design decisions that are not always well received. However - if you have the ability to do so - many of the constraints are coded "loosely coupled" into the application, so adjust them however you see fit.</p>
<h3>Technology stack</h3>
<p>The project has been constructed with the following technology stack in mind</p>
<ul>
<li><em><strong>Backend:</strong></em> Optimizely Content Cloud with Search & Navigation, running in a Headless mode to deliver models & business logic</li>
<li><em><strong>Frontend:</strong></em> TypeScript (latest), React (with Router & Helmet), Redux, Axios</li>
</ul>
<h3>Deployment</h3>
<p>The solution should be able to deploy into the Optimizely DXP, without the requirement to procure additional solutions and services. At the time of writing, the Optimizely DXP only allows deploying a .Net solution into an Azure WebApp, without a separate frontend application.</p>
<p>As - in some occasions - the frontend will be a completely separate application, hosted outside the Optimizely DXP, the solution should support such a deployment scenario as well. This scenario is not part of the example, but some tests have shown that the required changes are minimal.</p>
<h3>Workflow</h3>
<p>Typically when talking headless, one of the implications is that there are two teams working on delivering the solution: a (relatively small) backend team creating integrations and platform customizations and a frontend team working in a different project, consuming the backend services and writing JavaScript (TypeScript) / HTML / CSS to create the customer experience. In this setup, it is in many cases undesirable to require a frontend developer to do a complete solution deployment, including the .Net solution.</p>
<p>As such the final solution should overcome this constraint and enable frontend teams to work completely independently of the backend teams once the additions are deployed to the Optimizely DXP. As the .Net solution controls the data models, utilities should be provided to aid the frontend team in ensuring that these are represented correctly in the TypeScript code.</p>
<h3>Frontend</h3>
<p>Full ability to deliver an SEO-friendly and accessible website, which implies that the website must be fully server-side rendered to allow crawlers as well as non-JavaScript aware screen-readers to read the page and understand it. Though this might seem pretty straightforward, consider that no templating at all will be done in .Net MVC, everything will be delivered by the frontend team.</p>
<h3>Use core product capabilities</h3>
<p>With Optimizely Content Cloud being a PaaS solution, it is easy to extend it for every small and single improvement identified. Yet, the constraint applied here is to minimize the number of customizations added on the C# side and to remove these once a viable solution has been added to the core product. This approach has already resulted in the removal of a number of customizations/extensions from the initial version to the version currently on GitHub.</p>
<h2>Continue reading</h2>
<p>So, how are these aims achieved and constraints met? Find out in the subsequent blog posts (these will become links when a post has been published):</p>
<ol>
<li><a href="/link/4bd46a60192c45f5ab6a8a659ed36532.aspx">Backend: Enabling .Net MVC parity in a SPA</a></li>
<li><a href="/link/b9d14940983f4844a598cca16b570f65.aspx">Backend: Achieving SEO, Accessibility, and separate Frontend deployment</a></li>
<li><a href="/link/c1de437609df4e36a8af700c926510c5.aspx">Frontend: Anatomy of an implementation</a></li>
<li><a href="/link/d58ebc0fd7d2407e8866797a1a872698.aspx">Frontend: Routing & Custom components</a></li>
<li><a href="/link/9d64a1249aea4b3f8e55b40d09901735.aspx">Frontend: Adding application services</a></li>
</ol>Introducing Foundation Spa React/blogs/remko-jantzen/dates/2020/5/introducing-foundation-spa-react/2020-05-08T09:58:43.0000000Z<p><em><strong>TL;DR:</strong></em> Head over to <a href="https://github.com/episerver/Foundation-spa-react">https://github.com/episerver/Foundation-spa-react</a> and get the initial alpha release of a React frontend for Episerver CMS developed within the Episerver Pre-sales team. With the invitation to our community to work with us on making this a high quality reference site.</p>
<p>In my role as Solution Architect I've the privilege to work both with prospects as well as with our partners. A topic that did arise during many of these meetings is headless. And even though I personally do not believe headless to be the be-all-end-all for the delivery of customer experience, I do see the benefit of the technology stack used to deliver "headless". This put me on a quest to implement a React based SPA on top of Episerver Foundation. Today we're releasing the first version of this effort as an open source project on GitHub.</p>
<h2>So what are we releasing:</h2>
<ul>
<li>An alpha version to start collaborating with our community</li>
<li>A CMS only implementation of Foundation, with just models and a minimum number of controllers to support the frontend.</li>
<li>A working React based frontend for Episerver CMS, replacing the standard MVC frontend with a new ViewEngine that renders the React website.</li>
</ul>
<h2>The React Based Frontend:</h2>
<p>The frontend is both opinionated (e.g. it has been built enforcing certain practices) as well as convention based (e.g. there are certain patterns you cannot break). By adhering to these you'll get most of the benefits from using this frontend. If you want to break these practices and patterns, you can still use (part of) the library which is included. It just means that more of the boilerplate has to be written by you.</p>
<h3>Included right now:</h3>
<ul>
<li><em>Layouts: </em>Wrapper for the main routed content, typically the header & footer</li>
<li><em>Routing:</em> Resolving the content item based upon the URL as well as handling link clicks</li>
<li><em>Content mapping:</em> Maps content to React Components using the Episerver Content Type (e.g. Each content type in Episerver maps to a React Component</li>
<li><em>On page editing:</em> The base components and routing to support on-page editing are there already, just make sure to use them (or add the appropriate data-attributes yourself) to get on-page editing.</li>
<li><em>Events:</em> Although abandoned by React, they're convenient for working with "unknown" component types</li>
<li><em>Infrastructure:</em> Convenience components, models and abstract classes to keep an frontend implementation clean.</li>
<li><em>Model synchronization:</em> Automatically generate model definitions from the content types stored within Episerver</li>
<li><em>Build-scripts:</em> Configuration files for Webpack to build both the Server Side Rendering scripts as well as the browser scripts. These have different optimizations based upon the environment they run in.</li>
<li><em>Ability to run on CCDXP:</em> A frontend built upon this framework, can be deployed into the CCDXP, providing server side rendering. It's thus not required to add separate frontend servers.</li>
</ul>
<p><img src="/link/a205be4fbe924f21ae51a2eeb6443653.aspx" width="626" height="564" /></p>
<p><span style="color: #808080;"><em>An auto-generated IContent model, to ensure all data-models are identical to those within Episerver.</em></span></p>
<h3>What's still on my wish-/to-do list (in no particular order):</h3>
<p>Like any first version / alpha release there's a lot still on my whish-list, ranging from feature enhancements to structural enhancement.</p>
<ul>
<li>Personalization beyond the current limited support for Visitor Groups</li>
<li>Performance measurement for A/B Testing</li>
<li>Offline mode</li>
<li>Install as launcher / run as app on Android & iOS</li>
<li>Switch from the Fetch API to <a href="https://github.com/axios/axios">Axios</a></li>
<li>Add an <a href="https://expressjs.com/">Express</a> based build for deployments outside of DXC-S</li>
<li>Commerce support</li>
<li>Model Synchronization from frontend to Episerver*</li>
<li>Custom routing (I.e. Enable you to add custom routes to the site)</li>
<li>Change the library into a NPM library and enable more freedom in the implementation</li>
<li>Enable updates of the SPA without deploying the .Net code, whilest running in CCDXP</li>
</ul>
<p>*) This will most likely be done after our upcoming .Net Core release.</p>
<h2>Anatomy of the frontend:</h2>
<p><img src="/link/84777b01950a4470aca1c6a6577d55e9.aspx" width="1164" height="644" /></p>
<p>First things first: the language of choice. I've chosen to go with TypeScript, mostly because I like strongly typed languages and the ability to catch some of the errors already during build-time. For those having no TypeScript experience and only have done JavaScript, you'll catch on quickly.</p>
<p>The pre-configured environment is assuming that you'll deploy into CCDXP and thus requires the full Episerver CMS to be running on the development machine to keep the coupling in place. With the appropriate CORS setup on the Episerver side, the frontend can be deployed independently of Episerver. Personally when working on this, I use two IDE's, Visual Studio for the .Net part and Visual Studio Code for the Frontend.</p>
<h3>Main files & folders:</h3>
<p>The frontend is located within src/Spa.Frontend</p>
<div>
<table>
<tbody>
<tr>
<td>
<p>lib/Episerver</p>
</td>
<td>
<p>Main CMS Library, containing the boilerplate to built a React frontend using Episerver CMS as the main content source.</p>
</td>
</tr>
<tr>
<td>
<p>server</p>
</td>
<td>
<p>The entry point for the Server Side Rendering within Episerver CMS.</p>
</td>
</tr>
<tr>
<td>
<p>src</p>
</td>
<td>
<p>The website/application implementation.</p>
</td>
</tr>
<tr>
<td>
<p>.env.dist</p>
</td>
<td>
<p>The example environment file to configure the connection to the Episerver CMS.</p>
</td>
</tr>
</tbody>
</table>
<h2>At the server side</h2>
<p>At the server side, there're a number of add-ons to Episerver to make the SPA work:</p>
<ul>
<li><em>Foundation.ContentDelivery: </em>This project contains the base controllers you'll need to run Episerver in headless mode and an example (PageListBlock) controller to share business logic across frontends. Furthermore it contains the controllers and routers needed to deliver the following capabilities:
<ul>
<li>Invoke methods on an IContent controller</li>
<li>Retrieve all IContent models from Episerver</li>
</ul>
</li>
<li><em>Foundation.SpaViewEngine:</em> A complete view engine to ensure the server side rendering is done without the overhead of using cshtml files. This engine takes the routed IContent and gives it to the React frontend for rendering. By default it uses V8 to execute the JavaScript, however by the merits of the JavaScriptEngineSwitcher library you are able to change within your own project.</li>
</ul>
<h2>The code</h2>
<p>The code of the Foundation React SPA can be found as open source project at: <a href="https://github.com/episerver/Foundation-spa-react">https://github.com/episerver/Foundation-spa-react</a>. We're inviting you to collaborate with us on improving the solution by raising tickets, sending pull-requests and providing general feedback.</p>
</div>