November Happy Hour will be moved to Thursday December 5th.

Erik Jonsson
May 12, 2023
  1054
(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
Optimizely SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog