Nenad Nicevski, Author at Perficient Blogs https://blogs.perficient.com/author/nnicevski/ Expert Digital Insights Wed, 26 Apr 2023 20:53:40 +0000 en-US hourly 1 https://blogs.perficient.com/files/favicon-194x194-1-150x150.png Nenad Nicevski, Author at Perficient Blogs https://blogs.perficient.com/author/nnicevski/ 32 32 30508587 Anchor/Jump Links – Simple Example, but Useful https://blogs.perficient.com/2023/04/26/anchor-jump-links-simple-example-but-useful/ https://blogs.perficient.com/2023/04/26/anchor-jump-links-simple-example-but-useful/#respond Wed, 26 Apr 2023 20:04:25 +0000 https://blogs.perficient.com/?p=332476

Optimizely Partner Of Year Award

Having additional navigation for your content is a must-have in any modern website.

One example of that kind of navigation is Anchor/Jump links navigation. Simple, but very effective. We are simply using anchor links to jump to a specific part of the page. But since we are using the Optimizely CMS we are gonna make it more dynamic and automate the creation of anchor tags and links.

The Solution

The idea is to create navigation with all anchor links that are on the page. The simplest way is to have a page template with Content Area with blocks and then “just” render the anchor link for every block. To achieve that we are gonna make an interface IJumpLinkable so that every block that we want to have an anchor tag for the anchor navigation will inherit. In that interface we will have a string property that will represent the anchor navigation item label.

public interface IJumpLinkable : IContentData
{
    string LinkText { get; set; }
}

Now add this interface in every block that we want to use. We need to implement the property from the interface and to make it a fail-safe, we will tap in in the publishing event ( Opti Documentation ) and check if the link label is empty, then use the block’s name as a fallback value.

public class MyBlock : BaseBlock, IJumpLinkable
{
      ....

     [Display(
       GroupName = GroupNames.Navigation,
       Name = "Anchor/Jump Link Label",
       Description = "",
       Order = 10)]
    public virtual string LinkText { get; set; }

     ....


    public void PublishingContent(object sender, ContentEventArgs e)
    {
       if (string.IsNullOrEmpty(LinkText))
       {
          LinkText = (this as IContent).Name;
       }
    }
}

In our PageViewModel we have to add a dictionary object that will have key -> link label and value -> block ID pairs.

public Dictionary<string, string> JumpLinks { get; set; }

Page Controller

In our page controller after we create a page view model, we need to populate that dictionary object with the content area items. We will scan the items list for the content that is of IJumpLinkable type and from that content (block) we will take the link label and content ID.

...
public ActionResult Index(MyContentPage currentContent)
{
    var viewModel = CreateModel(currentContent);

    if(currentContent.MainContentArea != null)
    {
        var jumpLinkItems = currentContent.MainContentArea.FilteredItems.GetContentItems<IJumpLinkable>();

        var links = new Dictionary<string, string>();
        foreach (var jumpLinkItem in jumpLinkItems.Where(jumpLinkItem => !string.IsNullOrWhiteSpace(jumpLinkItem.LinkText)))
        {
            links.Add(jumpLinkItem.LinkText, ((IContent)jumpLinkItem).ContentLink.ID.ToString());
        }

        viewModel.JumpLinks = links;
    }

    return View("MyContentPage", viewModel);
}
...

Now that we have a list of links, all we have to do now is to render that list in our page view. We need to loop twice through that list, first to render the navigation menu and second to render the anchor tags before rendering each block from the content area. We have to render the blocks from the content area in the loop because we need to put an anchor tag with the unique ID before every block. In this example the anchor ID is constructed with the prefix “content” plus the block’s ID. ( W3School Anchor example )

Rendering the Navigation Menu:

if (Model.JumpLinks != null && Model.JumpLinks.Count > 0) 
{ 
  <section> 
    <div class="container"> 
     @foreach (var link in Model.JumpLinks) 
     { 
         <div> 
           <a href="#content-@link.Value"> @link.Key </a>
        </div> 
     } 
   </div> 
 </section> 
}

Rendering the content area:

<section class="content">
   <div @Html.EditAttributes(x => x.CurrentContent.MainContentArea)>
    @if (Model.CurrentContent.MainContentArea != null)
    {
      foreach (var item in Model.CurrentContent.MainContentArea.Items)
      {
         <a id="content-@item.ContentLink.ID"></a>
         @Html.DisplayFor(x => item.ContentLink)
      }
    }
   </div>
</section>

And that’s it!

]]>
https://blogs.perficient.com/2023/04/26/anchor-jump-links-simple-example-but-useful/feed/ 0 332476
Custom XhtmlString Render Service – Force Absolute URL for Images https://blogs.perficient.com/2022/10/07/custom-xhtmlstring-render-service-force-absolute-url-for-images/ https://blogs.perficient.com/2022/10/07/custom-xhtmlstring-render-service-force-absolute-url-for-images/#respond Fri, 07 Oct 2022 15:12:55 +0000 https://blogs.perficient.com/?p=317609

Working with a headless website setup sometimes can be challenging.

For example, if you insert an image directly into the TinyMCE text editor sometimes (it depends on a lot of factors) you can see the relative path to the server storage where the image is saved. But because it’s a headless setup, the front end part is at one web address and the back end could be in a different address which can be seen if someone inspects the page source.

The solution for everything is always to write a custom XhtmlString render service because you will mostly use text properties as content working with a headless website setup.

The Solution

The idea is to hijack the final page render and parse the page content.

First, let’s implement an XhtmlStringExtensions class where we will do the parsing of the output content. To do that we will scan the page content for the image tags and replace all image sources attribute with the absolute source path of the image.

public static class XhtmlStringExtensions
{
    /// <summary>
    /// Parses the XHtml String and forces all relative images to be absolute
    /// </summary>
    public static string ForceAbsoluteImgUrls(this XhtmlString html)
    {
        return html == null ? string.Empty : html.ToHtmlString().ForceAbsoluteImgUrls();
    }

    public static string ForceAbsoluteImgUrls(this string input)
    {
        var logger = LogManager.GetLogger(typeof(XhtmlStringExtensions));
        var logPrefix = $"{nameof(XhtmlStringExtensions)}->ForceAbsoluteImgUrls:";

        try
        {
            var doc = new HtmlDocument();
            doc.LoadHtml(input);

            // find all image tags, if there are none just return out- otherwise we need to adjust them

            var imgs = doc.DocumentNode.SelectNodes("//img");
            if (imgs == null)
                return input;

            var scheme = EPiServer.Web.SiteDefinition.Current.SiteUrl.Scheme;
            var serverUrl = new HostString(EPiServer.Web.SiteDefinition.Current.SiteUrl.Host);

            logger.Information($"{logPrefix} scheme={scheme} & serverUrl={serverUrl}");

            // for each image, interrogate src accordingly
            foreach (var img in imgs)
            {
                var src = img.Attributes["src"]?.Value;

                logger.Information($"{logPrefix} img src found: {src}");

                // try parsing uri and determine if it's absolute
                Uri uri = null;
                if (Uri.TryCreate(src, UriKind.Absolute, out uri))
                {
                    if (uri.IsAbsoluteUri)
                    {
                        logger.Information($"{logPrefix} img src is absolute, skipping ({src})");
                        continue; // if it's already absolute, just continue processing
                    }

                }

                // must not be absolute, so go ahead and build an absolute url for it
                var newSrc = UriHelper.BuildAbsolute(scheme, serverUrl, src);
                logger.Information($"{logPrefix} img src is not absolute, fixing setting old {src} to {newSrc}");
                img.SetAttributeValue("src", newSrc);
            }

            var outerHtml = doc.DocumentNode.OuterHtml;
            return outerHtml; // return out the new resulting HTML
        }
        catch (Exception ex)
        {
            logger.Error($"{logPrefix} Error encountered when trying to force absolute URLs in XhtmlStrings", ex);
        }

        return input;
    }
}

Next, we need to implement our custom Xhtml render service from where we will call our Xhtml string extension method.

// hijack the base implementation and force it to use canonical URLs for all src properties
[ServiceConfiguration(typeof(CustomXhtmlRenderService))]
public class CustomXhtmlRenderService : XhtmlRenderService
{
    private ILogger logger;

    public CustomXhtmlRenderService()
        : base(ServiceLocator.Current.GetInstance<ContentApiOptions>(), 
            ServiceLocator.Current.GetInstance<IHtmlHelper>(), 
            ServiceLocator.Current.GetInstance<ITempDataProvider>(),
            ServiceLocator.Current.GetInstance<ICompositeViewEngine>(),
            ServiceLocator.Current.GetInstance<IModelMetadataProvider>())
    {
        logger = ServiceLocator.Current.GetInstance<ILogger<CustomXhtmlRenderService>>();
    }

    public CustomXhtmlRenderService(
        ContentApiOptions options,
        IHtmlHelper htmlHelper,
        ITempDataProvider tempDataProvider,
        ICompositeViewEngine compositeViewEngine,
        IModelMetadataProvider metadataProvider,
        ILogger<CustomXhtmlRenderService> logger) : base(options, htmlHelper, tempDataProvider, compositeViewEngine, metadataProvider)
    {
        this.logger = logger;
    }

    public override string RenderXhtmlString(HttpContext context, XhtmlString xhtmlString)
    {
        var result = base.RenderXhtmlString(context, xhtmlString);

        try
        {
            result = result.ForceAbsoluteImgUrls();
        }
        catch (Exception e)
        {
            logger.LogError($"Unable to correct Absolute Urls in XhtmlString requested from {context?.Request?.GetDisplayUrl()}", e);
        }

        return result;
    }
}

And finally, we need to register our custom Xhtml render service in Startup.cs .

var serviceDescriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(XhtmlRenderService));
services.Remove(serviceDescriptor);
services.AddTransient(typeof(XhtmlRenderService), typeof(CustomXhtmlRenderService));

And that’s it!

 

Additionally, if you want to use this custom Xhtml renderer service in the project that has a front end (regular website) and the headless mode enabled, you can use it simply by adding a template override for the XhtmlString property.

To do so, in your Views folder add this file \Shared\DisplayTemplates\XhtmlString.cshtml .

@using EPiServer.Core
@model XhtmlString
<!-- XhtmlString.cshtml Display Template Start -->
@Html.Raw(Model.ForceAbsoluteImgUrls())
<!-- XhtmlString.cshtml Display Template End -->

 

Happy coding!

]]>
https://blogs.perficient.com/2022/10/07/custom-xhtmlstring-render-service-force-absolute-url-for-images/feed/ 0 317609
Content Delivery – Removing necessary fields and how to add new fields to the API response https://blogs.perficient.com/2022/08/24/content-delivery-removing-necessary-fields-and-how-to-add-new-fields-to-the-api-response/ https://blogs.perficient.com/2022/08/24/content-delivery-removing-necessary-fields-and-how-to-add-new-fields-to-the-api-response/#comments Wed, 24 Aug 2022 10:46:57 +0000 https://blogs.perficient.com/?p=316570

What is a headless CMS?

headless CMS is a back-end only content management system (CMS) built from the ground up as a content repository that makes content accessible via a RESTful API or GraphQL API for display on any device.

The term “headless” comes from the concept of chopping the “head” (the front end, i.e. the website) off the “body” (the back end, i.e. the content repository). A headless CMS remains with an interface to manage content and a RESTful or GraphQL API to deliver content wherever you need it. Due to this approach, a headless CMS does not care about how and where your content gets displayed. It only has one focus: storing and delivering structured content and allowing content editors to collaborate on new content.

With Optimizely you can implement your headless solution using the Content Delivery API.

To filter the API response and remove the fields that you don’t need and/or remove the null field to get the nicer and clearer output you need to use IContentApiModelFilter which is explained in the Optimizely documentation.

But, that didn’t work for me how I wanted.

Here is an example of the default API response in JSON format:

{
    "contentLink": {
        "id": 17,
        "workId": 0,
        "guidValue": "85d21fcb-6b86-47e2-8ac7-40561746f6b8",
        "providerName": null,
        "url": null,
        "expanded": null
    },
    "name": "Rich Text",
    "language": {
        "link": null,
        "displayName": "English",
        "name": "en"
    },
    "existingLanguages": [
        {
            "link": null,
            "displayName": "English",
            "name": "en"
        }
    ],
    "masterLanguage": {
        "link": null,
        "displayName": "English",
        "name": "en"
    },
    "contentType": [
        "Block",
        "RichTextBlock"
    ],
    "parentLink": {
        "id": 16,
        "workId": 0,
        "guidValue": "ff300114-39a2-469e-9304-3703317a4894",
        "providerName": null,
        "url": "http://perficient.local/contentassets/ff30011439a2469e93043703317a4894/",
        "expanded": null
    },
    "routeSegment": null,
    "url": null,
    "changed": "2022-08-05T14:59:05Z",
    "created": "2022-08-05T14:59:05Z",
    "startPublish": "2022-08-05T14:59:05Z",
    "stopPublish": null,
    "saved": "2022-08-11T12:33:39Z",
    "status": "Published",
    "category": {
        "value": [],
        "propertyDataType": "PropertyCategory"
    },
    "globalStyle": {
        "value": "",
        "propertyDataType": "PropertyLongString"
    },
    "mainBody": {
        "value": "<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, quia.</p>\n<p><img src=\"http://perficient.local/globalassets/crop.jpg\" alt=\"crop\" width=\"1280\" height=\"853\" /></p>\n<p>&nbsp;</p>\n<p><img src=\"http://perficient.local/contentassets/b20dbeb471b94ba18ba2f76cfdab5a05/caesars.png\" alt=\"caesars.png\" /></p>",
        "propertyDataType": "PropertyXhtmlString"
    }
}

After adding the Custom Content API Model Filter how is explained in the documentation, you will get something like this:

{
    "contentLink": {
        "id": 17,
        "workId": 0,
        "guidValue": "85d21fcb-6b86-47e2-8ac7-40561746f6b8",
        "providerName": null,
        "url": null,
        "expanded": null
    },
    "name": "Rich Text",
    "language": {
        "link": null,
        "displayName": "English",
        "name": "en"
    },
    "existingLanguages": [
        {
            "link": null,
            "displayName": "English",
            "name": "en"
        }
    ],
    "masterLanguage": {
        "link": null,
        "displayName": "English",
        "name": "en"
    },
    "contentType": [
        "Block",
        "RichTextBlock"
    ],
    "parentLink": null,
    "routeSegment": null,
    "url": null,
    "changed": "2022-08-05T14:59:05Z",
    "created": null,
    "startPublish": null,
    "stopPublish": null,
    "saved": null,
    "status": null,
    "contentTypeGuid": "3c2ed8a86f1a41459d4498cfeb8a5652",
    "globalStyle": {
        "value": "",
        "propertyDataType": "PropertyLongString"
    },
    "mainBody": {
        "value": "<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, quia.</p>\n<p><img src=\"http://perficient.local/globalassets/crop.jpg\" alt=\"crop\" width=\"1280\" height=\"853\" /></p>\n<p>&nbsp;</p>\n<p><img src=\"http://perficient.local/contentassets/b20dbeb471b94ba18ba2f76cfdab5a05/caesars.png\" alt=\"caesars.png\" /></p>",
        "propertyDataType": "PropertyXhtmlString"
    }
}

For this example, I put the null values for these fields:

contentApiModel.StartPublish = null;
contentApiModel.StopPublish = null;
contentApiModel.ParentLink = null;
contentApiModel.RouteSegment = null;
contentApiModel.Created = null;
contentApiModel.Saved = null;
contentApiModel.Status = null;

You can see in the previous JSON response that those fields are still there but with the null value.

To actually remove those nulled fields, you need to add one settings option in Startup.cs services under the Content Delivery section.

// remove null values from serialized data
services.ConfigureContentDeliveryApiSerializer(settings =>
{
     settings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});

The only thing here is that the ConfigureContentDeliveryApiSerializer is in the Internal namespace but that won’t go anywhere any time soon.

Now we have this nice and clean JSON response:

{
    "contentLink": {
        "id": 17,
        "workId": 0,
        "guidValue": "85d21fcb-6b86-47e2-8ac7-40561746f6b8"
    },
    "name": "Rich Text",
    "language": {
        "displayName": "English",
        "name": "en"
    },
    "existingLanguages": [
        {
            "displayName": "English",
            "name": "en"
        }
    ],
    "masterLanguage": {
        "displayName": "English",
        "name": "en"
    },
    "contentType": [
        "Block",
        "RichTextBlock"
    ],
    "changed": "2022-08-05T14:59:05Z",
    "globalStyle": "",
    "mainBody": "<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, quia.</p>\n<p><img src=\"http://perficient.local/globalassets/crop.jpg\" alt=\"crop\" width=\"1280\" height=\"853\"></p>\n<p>&nbsp;</p>\n<p><img src=\"http://perficient.local/contentassets/b20dbeb471b94ba18ba2f76cfdab5a05/caesars.png\" alt=\"caesars.png\"></p>"
}

For example, for some reason I want to have a content type GUID as a field in the JSON response.

Just add this code at the end of your Custom Content API Model Filter.

contentApiModel.Properties[CONTENT_TYPE_GUID] = Guid.Empty.ToString("N");
if (contentApiModel.ContentLink.Id != null && contentApiModel.ContentLink.Id.HasValue)
{
    var content = contentLoader.Get<IContent>(new ContentReference(contentApiModel.ContentLink.Id.Value));
    var contentType = content.ContentTypeID;
    var type = contentTypeRepository.Load(contentType);

    if (type != null && type.GUID != Guid.Empty)
    {
        contentApiModel.Properties[CONTENT_TYPE_GUID] = type.GUID.ToString("N");
    }
}

Now the response will be like this:

{
    "contentLink": {
        "id": 17,
        "workId": 0,
        "guidValue": "85d21fcb-6b86-47e2-8ac7-40561746f6b8"
    },
    "name": "Rich Text",
    "language": {
        "displayName": "English",
        "name": "en"
    },
    "existingLanguages": [
        {
            "displayName": "English",
            "name": "en"
        }
    ],
    "masterLanguage": {
        "displayName": "English",
        "name": "en"
    },
    "contentType": [
        "Block",
        "RichTextBlock"
    ],
    "changed": "2022-08-05T14:59:05Z",
    "contentTypeGuid": "3c2ed8a86f1a41459d4498cfeb8a5652",
    "globalStyle": "",
    "mainBody": "<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, quia.</p>\n<p><img src=\"http://perficient.local/globalassets/crop.jpg\" alt=\"crop\" width=\"1280\" height=\"853\"></p>\n<p>&nbsp;</p>\n<p><img src=\"http://perficient.local/contentassets/b20dbeb471b94ba18ba2f76cfdab5a05/caesars.png\" alt=\"caesars.png\"></p>"
}

Here is our added field.

Json Field

And that’s it!

Here is the code listing for this example.

CustomContentApiModelFilter.cs

using EPiServer;
using EPiServer.ContentApi.Core.Serialization;
using EPiServer.ContentApi.Core.Serialization.Internal;
using EPiServer.ContentApi.Core.Serialization.Models;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.ServiceLocation;
using System;

namespace Perficient.Web.Middleware.ApiModelFilter
{
    [ServiceConfiguration(typeof(IContentApiModelFilter), Lifecycle = ServiceInstanceScope.Singleton)]
    public class CustomContentApiModelFilter : ContentApiModelFilter<ContentApiModel>
    {
        private readonly IContentLoader contentLoader;
        private readonly IContentTypeRepository contentTypeRepository;
        private const string CONTENT_TYPE_GUID = "ContentTypeGuid";

        public CustomContentApiModelFilter(IContentLoader contentLoader, IContentTypeRepository contentTypeRepository)
        {
            this.contentLoader = contentLoader;
            this.contentTypeRepository = contentTypeRepository;
        }

        public override void Filter(ContentApiModel contentApiModel, ConverterContext converterContext)
        {
            // To remove values from the output, set them to null.
            // thus the response output will not contain these "out of the box" fields
            contentApiModel.StartPublish = null;
            contentApiModel.StopPublish = null;
            contentApiModel.ParentLink = null;
            contentApiModel.RouteSegment = null;
            contentApiModel.Created = null;
            contentApiModel.Saved = null;
            contentApiModel.Status = null;

            // remove category as we don't need it at the API level
            contentApiModel.Properties.Remove("Category");

            #region Add Content Type GUID to output
            // add a field called contentTypeGuid which has the ID of the content type in the output, this will be
            // useful for keying off of to understand what type of content is being delivered
            contentApiModel.Properties[CONTENT_TYPE_GUID] = Guid.Empty.ToString("N");
            if (contentApiModel.ContentLink.Id != null && contentApiModel.ContentLink.Id.HasValue)
            {
                var content = contentLoader.Get<IContent>(new ContentReference(contentApiModel.ContentLink.Id.Value));
                var contentType = content.ContentTypeID;
                var type = contentTypeRepository.Load(contentType);

                if (type != null && type.GUID != Guid.Empty)
                {
                    contentApiModel.Properties[CONTENT_TYPE_GUID] = type.GUID.ToString("N");
                }
            }
            #endregion
        }
    }
}

 

]]>
https://blogs.perficient.com/2022/08/24/content-delivery-removing-necessary-fields-and-how-to-add-new-fields-to-the-api-response/feed/ 6 316570
How to enable Webpack on your Episerver Project https://blogs.perficient.com/2020/12/01/how-to-enable-webpack-on-your-episerver-project/ https://blogs.perficient.com/2020/12/01/how-to-enable-webpack-on-your-episerver-project/#respond Tue, 01 Dec 2020 14:21:46 +0000 https://blogs.perficient.com/?p=284258
If you’re building modern web applications, you need to carefully consider the front-end technology stack you use. Webpack, a static module bundler, combined with NPM for package management, is a great starting point. Here’s how you can incorporate these technologies into your Asp.net MVC application built upon Episerver.

Alongside Node.js, we need to have node-sass installed as well so that we can compile scss files.

Here is how you can easily do that in the Alloy Demo project.

First, we need to create these three files: webpack.config.js, package.json and babel.config.json place it in /Static the folder:

  • webpack.config.js is the webpack configuration file where we define the entry point, loaders, and libraries.
  • package.json – build/run configurations and used libraries.
  • babel.config.json – configuration file for Babel JS options. In this case, we are configuring what version to use in the project.

Create app.js file and place it in /Static/js folder:

  • app.js – This is the main JS file, the entry point for all scripts.

Create two folders (empty for now) under the /Static folder : dist and scss.

 

Edit and add these lines to the webpack.config.js.

const path = require('path');
const fs = require("fs");
const webpack = require("webpack");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    entry: './js/app.js',
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'alloy.bundle.js'
    },
    //watch: true, // this line will enable watcher for the js and scss changes (real time)
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.scss$/,
                use: ["style-loader", "css-loader", "sass-loader"]
            },
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                use: ["babel-loader"]
            },
            {
                test: /\.(eot|gif|otf|png|svg|ttf|woff)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
                use: ['file-loader']
            }
        ]
    },
    stats: {
        colors: true
    },
    devtool: 'source-map',
    plugins: [
        new CleanWebpackPlugin(),
        // jQuery globally
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery",
            "window.jQuery": "jquery"
        })
    ]
};

This is the standard webpack config settings:

  • entry – entry point / main JS file
  • output – path to the compiled JS file
  • modules – list of the used modules. Here we have definitions for the CSS, js, and scss file loader. We need this to read js, css, and scss files. If you have images linked inside your scss/css, then the file-loader is needed too.
  • Plugins – list of used plugins. We are using `CleanWebpackPlugin` to clean the previously compiled files before the new compile is started, and here we must define global variables for the jQuery library so that we can use the global variable $ in all the scripts that are defined and used via app.js .

 

Edit and add these lines to the package.json.

{
  "name": "static",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "dependencies": {
    "bootstrap": "^4.5.0",
    "bootstrap-slider": "^11.0.2",
    "bootstrap-validate": "^2.2.0",
    "jquery-match-height": "^0.7.2",
    "popper.js": "^1.16.1",
    "slick-carousel": "^1.8.1",
    "underscore": "^1.10.2"
  },
  "devDependencies": {
    "@babel/core": "^7.10.3",
    "@babel/preset-env": "^7.10.3",
    "axios": "^0.19.2",
    "babel-loader": "^8.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^3.6.0",
    "expose-loader": "^1.0.0",
    "file-loader": "^6.0.0",
    "jquery": "^3.5.1",
    "sass": "^1.26.9",
    "sass-loader": "^8.0.2",
    "style-loader": "^1.2.1",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12"
  },
  "scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production",
    "compile-scss": "node-sass ./scss/main.scss ./css/main.css --source-map true --source-map-contents sass --output-style compressed && node-sass ./scss/editor.scss ./css/editor.css --source-map true --source-map-contents sass --output-style compressed && node-sass ./scss/editmode.scss ./css/editmode.css --source-map true --source-map-contents sass --output-style compressed"
  },
  "keywords": [],
  "author": "Perficient",
  "license": "ISC"
}

All these packages will be installed when you run npm install in the command line in /Static folder. Under the ‘scripts’ section, we have three build/run options:

  • dev – compile js files in development mode; use npm run dev in the command line
  • build – compile js files in production mode; use npm run build in the command line
  • compile-scss – compile all scss files. We are using node-sass for compiling scss files with options/switches. In the Alloy project, we have three css files: main.css, editor.css and editmode.css ; use npm run compile-scss in the command line

 

Edit and add these lines to the babel.config.json.

{
  "presets": [
    "@babel/preset-env"
  ]
}

Edit and add these lines to the app.js.

"use strict";

import 'bootstrap';

Alloy projects use bootstrap and jQuery only, so we don’t have any additional JS code here.

Now because the Alloy project already contains JS and CSS files that are bundled in the Initialization module ( BundleConfig.cs ) we need to make some changes so that we can use those files with webpack:

  • create vendor folder under the \Static\js folder and move all js files from the Alloy project there.
    • Alloy Js
  • create scss folder under the \Static folder and move all files from css folder to the scss folder.
    • Create three new files main.scss,  and editmode.scss.
    • Rename editmode.css to the editmodealloy.css.
    • Alloy Scss
    • Add this code to the main.scss
      @import 
          "bootstrap", 
          "bootstrap-responsive",
          "media",
          "style";
    • Add this code to the editor.scss
      @import 
          "bootstrap",
          "style";
    • Add this code to the editmode.scss
      @import "editmodealloy";
      
      

       

To check if we have done everything right, we can run these two commands in the command line in /Static folder: npm run dev and npm run compile-scss. If everything is OK, in the solution explorer (VS) you will have this file structure

Alloy Static

Now we need to update the main template view – _Root.cshtml file.

<head>
      ...
        
      @*@Styles.Render("~/bundles/css")
      @Scripts.Render("~/bundles/js")*@
        
      <link rel="stylesheet" href="~/Static/css/main.css">
      @if (PageEditing.PageIsInEditMode)
      {
          <link rel="stylesheet" href="~/Static/css/editor.css">
      }
      
      ...
        
</head>

We need to comment (or delete) the bundle lines and add the link to the newly created css files. We added to check if the page is rendered in Edit mode so that we can load editor.css only when we edit the page.

<body>
     ...

     <script type="text/javascript" src="~/Static/dist/alloy.bundle.js"></script>
</body>

Add the link to the webpack generated js file.

 

And that’s it!

 

Additional note

If you want to webpack build and compile js and scss files when you build the project in VS, just add these lines in .csproj file.

<Target Name="CompileSass" AfterTargets="Build">
    <Message Text="Compiling Front-End assets" />
    <!-- always -->
    <Exec Command="npm run compile-scss" IgnoreExitCode="false" WorkingDirectory="$(SolutionDir)/AlloyDemo/Static" />
    <!-- local dev -->
    <Exec Command="npm run dev" Condition="'$(Configuration)' == 'Debug'" IgnoreExitCode="false" WorkingDirectory="$(SolutionDir)/AlloyDemo/Static" />
    <!-- release -->
    <Exec Command="npm run build" Condition="'$(Configuration)' == 'Release'" IgnoreExitCode="false" WorkingDirectory="$(SolutionDir)/AlloyDemo/Static" />  
</Target>

Just replace AlloyDemo with the name of your project.

]]>
https://blogs.perficient.com/2020/12/01/how-to-enable-webpack-on-your-episerver-project/feed/ 0 284258
Page Restrictions in an Initialization Module https://blogs.perficient.com/2020/11/24/page-restrictions-in-an-initialization-module/ https://blogs.perficient.com/2020/11/24/page-restrictions-in-an-initialization-module/#respond Tue, 24 Nov 2020 16:28:32 +0000 https://blogs.perficient.com/?p=284189
Managing your AllowedTypes (insert options) in Episerver can become overwhelming, depending on the strategy you take.
You can read about different options here:
but there is a strategy you can take for a very developer-friendly approach.
As highlighted on Epi World forum thread (Dylan McCurry and I worked together on the project where we implement this and we are using this on all projects moving forward ), you can actually manipulate the IAvailableSettingsRepository during initialization to define all of your restrictions in one place.

The Solution

Create a standard Episerver initialization module and implement Initialize() function.

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class PageRestrictionInitialization : IInitializableModule
{
    private IContentTypeRepository _contentTypeRepository;
    private IAvailableSettingsRepository _availableSettingsRepository;
    private ILogger _log;
    
    public void Initialize(InitializationEngine context)
    {
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

We are using two helper functions that are making the code cleaner and easier to read and maintain.

private void DisallowAll<T>()
{
    var page = _contentTypeRepository.Load(typeof(T));

    var setting = new AvailableSetting
    {
         Availability = Availability.None
    };

    _availableSettingsRepository.RegisterSetting(page, setting);
}
DisallowAll() function will set a page type to not have any available pages to be inserted beneath of it. We are using this function to first set the availability to none and then set the availability how we want.
private void SetPageRestriction<T>(IEnumerable<Type> pageTypes)
{
     var page = _contentTypeRepository.Load(typeof(T));

     var setting = new AvailableSetting
     {
            Availability = Availability.Specific
     };

     foreach (var pageType in pageTypes)
     {
            var contentType = _contentTypeRepository.Load(pageType);
            setting.AllowedContentTypeNames.Add(contentType.Name);
     }

     _availableSettingsRepository.RegisterSetting(page, setting);

     _log.Log(Level.Debug, $"{page.Name} restriction added.");
}

SetPageRestriction() function is where all the restrictions are done. You can pass the list of page types as a function argument and that those page types will be set as allowed page types for the passing page type type <T>.

Now we can implement the Initialize() function.

public void Initialize(InitializationEngine context)
{
    _contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
    _availableSettingsRepository = ServiceLocator.Current.GetInstance<IAvailableSettingsRepository>();
    _log = LogManager.GetLogger();

    var sysRoot = _contentTypeRepository.Load("SysRoot") as PageType;
    var setting = new AvailableSetting
    {
        Availability = Availability.Specific,
        AllowedContentTypeNames =
        {
            nameof(StartPage), // can't use custom interfaces with IContentTypeRepository
        }
    };
    _availableSettingsRepository.RegisterSetting(sysRoot, setting);

    // Disallow insertion for all product and article related pages
    DisallowAll<ProductPage>();
    DisallowAll<ArticlePage>();
    DisallowAll<LandingPage>();
    DisallowAll<FindSearchPage>();

    // Home Page
    SetPageRestriction<StartPage>(new List<Type>
    {
        typeof(ContainerPage),
        typeof(StandardPage),
        typeof(LandingPage),
        typeof(FindSearchPage),
        typeof(ProductPage)
    });

    // Landing Page
    SetPageRestriction<LandingPage>(new List<Type>
    {
        typeof(ArticlePage),
        typeof(NewsPage)
    });

    // Container Page
    SetPageRestriction<ContainerPage>(new List<Type>
    {
        typeof(ContactPage)
    });

    // Product Page
    SetPageRestriction<ProductPage>(new List<Type>
    {
        typeof(ContainerPage),
        typeof(StandardPage)
    });
}

I am using the Episerver Alloy demo website for this example and all the page types are from the Alloy project.

First, we get the SysRoot node as a page type and use it as a first page/node. Then we allow only the StartPage to be inserted under the root page and save the settings.

Next, we disallow inserting any page under the ProductPage, ArticlePage, LandingPage and FindSearchPage. Now we need to allow insertion for every page type how we want it. For this example I did that for the StartPage, LandingPage, ContainerPage and the ProductPage.

And that’s it.

 

In this way, you have a clean model for all page types and everything in one place and it’s easy to maintain and extend later on. And this concept consolidates to the first SOLID principles – Single Responsibility.

]]>
https://blogs.perficient.com/2020/11/24/page-restrictions-in-an-initialization-module/feed/ 0 284189
Episerver YouTube Video Block https://blogs.perficient.com/2020/03/05/nenads-post-for-episerver/ https://blogs.perficient.com/2020/03/05/nenads-post-for-episerver/#respond Thu, 05 Mar 2020 16:47:17 +0000 https://epijocks.com/?p=218

Video for Content Editors

These days, every website has some video content. We wanted to make editing video easier for Epi content editors, so we created the YouTube Video block. This block is a feature of our new SCORE for Episerver platform, which launched in 2019.

To create the best editor experience with on-page editing functionality, we created the YouTube Video block – which actually consists of two blocks: a Dojo widget and two views. In this way, the changes are visible immediately on the page editing view and the block can be used as a stand-alone block or as a property of another block or page type. And of course, you’ll need a working YouTube Data API Key.


YouTube Video block is in the Videos group.

Yt Block

This is an example of a stand-alone YT Video block. After creation, you can see the placeholder image which is clickable (on-page edit).

Yt Onpageedit

YT Video editor is a Dojo widget where you can perform video searches in the same way you’d usually do on the YouTube website.

Yt Dojo

On editor dialog, you have a result list and pagination links.

Yt Dojo Search

Just click on the desired video and the video field will be updated with the selected video right away.

Yt Select
That’s it!
]]>
https://blogs.perficient.com/2020/03/05/nenads-post-for-episerver/feed/ 0 278556