Erik Jonsson
May 12, 2023
visibility 2490
star star star star star
(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

error Please login to comment.
Latest blogs
Surviving a Dirty Feed. Part 3: Error Handling and Event Batching

Committing a 250,000-row Optimizely catalog import row by row, returning a per-row failure report, and coalescing catalog events into one per batch.

Stanisław Szołkowski | Oct 16, 2026 |

Building UI Extensions for Optimizely SaaS CMS

Introduction Developers have been extending the functionality of Optimizely PaaS CMS for years using addons built in C# and deployed as NuGet...

Mark Stott | Oct 15, 2026

Cheapskate AI Translator: Because Translation Should Not Require a Second Mortgage

There is a particular kind of software project that begins with a detailed architecture document, a product roadmap, and a carefully assembled...

Linh Doan Cuu | Oct 15, 2026

Computed Fields in CMS 13 Content Contracts with Optimizely Graph

Content Contracts in Optimizely CMS 13 allow different content types to share a common structure when querying Optimizely Graph. But what if the...

Mardy | Oct 14, 2026