Don't miss out Virtual Happy Hour this Friday (April 26).

Try our conversational search powered by Generative AI!

Erik Jonsson
May 12, 2023
  804
(1 votes)

CMS 12: Make it possible to create links to multi-channel content in TinyMCE.

In order to make the same content available in different sites with different layouts we have started to use blocks instead of pages.
These blocks are then stored in the new multi-channel content tree in the CMS 12.
A viewing page for each block type is used when the content shall be viewed on a specific site.

Then we got the request if it could be possible to create link to these blocks in TinyMCE.
Challenge accepted! :-)

Our first thought was to make a TinyMCE plugin base on the epi-link plugin.
After some investigation inspiration to a better solution was was found in older blog post, such as https://blog.ynzen.com/extending-the-hyperlink-editor-in-optimizely-11.
So the new task was to extend the hyperlink editor with a "Multi-channel content" selector.
This was achieved by a editor descriptor.

[EditorDescriptorRegistration(TargetType = typeof (string), UIHint = "HyperLink",
        EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault)]
    public class LinkEditorDescriptor : EditorDescriptor
    {
        private Injected<IContentLoader> _contentLoader;
        private readonly LocalizationService _localizationService;

        public LinkEditorDescriptor() : this(LocalizationService.Current)
        {
        }

        public LinkEditorDescriptor(LocalizationService localizationService)
        {
            _localizationService = localizationService;
        }

        public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
        {
            base.ModifyMetadata(metadata, attributes);
            IEnumerable<IContentRepositoryDescriptor> allInstances =
                ServiceLocator.Current.GetAllInstances<IContentRepositoryDescriptor>();
            List<HyperLinkModel> list = (
                from r in allInstances
                orderby r.SortOrder
                where r.LinkableTypes != null && r.LinkableTypes.Any<Type>()
                select new HyperLinkModel
                {
                    Name = (r.CustomSelectTitle ?? r.Name),
                    Roots = r.Roots,
                    WidgetType = "epi-cms/widget/ContentSelector",
                    LinkableTypes = r.LinkableTypes,
                    SearchArea = r.SearchArea
                }).ToList<HyperLinkModel>();

            list.Insert(list.Count > 1 ? 1 : 0, 
                new HyperLinkModel
                {
                    Name = _localizationService.GetString("/episerver/cms/components/multichannel/title"),
                    Title = null,
                    DisplayName = null,
                    SearchArea = "CMS/blocks",
                    WidgetType = "epi-cms/widget/ContentSelector",
                    Roots = GetMultiChannelRootsFromSettings(),
                    LinkableTypes = new List<Type>() { typeof(FlerKanalInnehallBlockBase) }
                }
            );
            
            metadata.EditorConfiguration["providers"] = list;
            metadata.DisplayName = string.Empty;
            metadata.ClientEditingClass = "epi-cms/widget/HyperLinkSelector";
        }
    }

    internal class HyperLinkModel
    {
        public string Name { get; set; }

        public string DisplayName { get; set; }

        public string Title { get; set; }

        public IEnumerable<ContentReference> Roots { get; set; }

        public string WidgetType { get; set; }

        public IDictionary<string, object> WidgetSettings { get; set; }

        public IEnumerable<Type> LinkableTypes { get; set; }

        public bool Invisible { get; set; }

        public string SearchArea { get; set; }
    }

This makes the create link dialog look like this.

The Roots parameter makes it possible to restrict the subtree(s) that shall be available in the content picker.

By adding a link to a multi-channel content block an UrlFragment which points to the block is added to the XhtmlString.
This link is rendered as a .aspx-link on the page and it generates a 404 page when clicked.

In order to make the link pointing to the correct viewing page for the actual site we implemented a method to convert the link in the viewmodel.

private XhtmlString ConvertMultiSiteContentLinks(XhtmlString xhtmlString, Startpage startpage)
{
            if (string.IsNullOrWhiteSpace(xhtmlString?.ToHtmlString()) || xhtmlString.IsEmpty)
            {
                return xhtmlString;
            }

            var result = new StringBuilder();

            foreach (var fragment in xhtmlString.Fragments)
            {
                  var urlFragment = ParseFragment(fragment, startpage);
                  result.Append(urlFragment);
            }

            return new XhtmlString(result.ToString());
 }

        private string ParseFragment(IStringFragment fragment, Startpage startpage)
        {
            var urlFragment = fragment as UrlFragment;
            if (urlFragment == null)
            {
                return fragment.InternalFormat;
            }

            var contentGuid = urlFragment.ReferencedPermanentLinkIds.FirstOrDefault();

            var url = urlFragment.Url;

            if (contentGuid != Guid.Empty)
            {
                if (_contentLoader.Service.TryGet<MultiChannelBlockBase>(contentGuid, out var block))
                {
                    url = block.GenerateUrl(startpage);                
                }
            }

            return url;
        }    

The startpage parameter is passed to the metods to give information about on which site this link is currently rendered.

All MultiChannelBlockBase blocks needs to implement the GenerateUrl(startpage) method which generates an URL to the appropriate viewing page for the block type on the actual site.
The block ID is passed as the last segment of the URL to make it possible for the viewing page to find the correct block to render.

With these parts in place we know can link to both pages and blocks from the bultin plugin in TinyMCE.

May 12, 2023

Comments

Please login to comment.
Latest blogs
Solving the mystery of high memory usage

Sometimes, my work is easy, the problem could be resolved with one look (when I’m lucky enough to look at where it needs to be looked, just like th...

Quan Mai | Apr 22, 2024 | Syndicated blog

Search & Navigation reporting improvements

From version 16.1.0 there are some updates on the statistics pages: Add pagination to search phrase list Allows choosing a custom date range to get...

Phong | Apr 22, 2024

Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog

Azure AI Language – Abstractive Summarisation in Optimizely CMS

In this article, I show how the abstraction summarisation feature provided by the Azure AI Language platform, can be used within Optimizely CMS to...

Anil Patel | Apr 18, 2024 | Syndicated blog

Fix your Search & Navigation (Find) indexing job, please

Once upon a time, a colleague asked me to look into a customer database with weird spikes in database log usage. (You might start to wonder why I a...

Quan Mai | Apr 17, 2024 | Syndicated blog