Try our conversational search powered by Generative AI!

Erik Jonsson
May 12, 2023
  840
(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
From Procrastination to Proficiency: Navigating Your Journey to Web Experimentation Certification

Hey there, Optimizely enthusiasts!   Join me in celebrating a milestone – I'm officially a certified web experimentation expert! It's an exhilarati...

Silvio Pacitto | May 17, 2024

GPT-4o Now Available for Optimizely via the AI-Assistant plugin!

I am excited to announce that GPT-4o is now available for Optimizely users through the Epicweb AI-Assistant integration. This means you can leverag...

Luc Gosso (MVP) | May 17, 2024 | Syndicated blog

The downside of being too fast

Today when I was tracking down some changes, I came across this commit comment Who wrote this? Me, almost 5 years ago. I did have a chuckle in my...

Quan Mai | May 17, 2024 | Syndicated blog

Optimizely Forms: Safeguarding Your Data

With the rise of cyber threats and privacy concerns, safeguarding sensitive information has become a top priority for businesses across all...

K Khan | May 16, 2024