Building a Multi-Brand Dynamics 365 Portal

Overview

  • Sometimes a specific program, product or sub-brand of an organization may require a unique Portal.
  • Provisioning and buying separate Portal subscriptions may potentially be overkill for the particular requirement.
  • The following post describes a method where different branded sections of a portal can be configured using one Portal instance.

Multi-Brand Portals

I have been often asked if it possible to have multiple portals connected to a single Dynamics 365 instance. This is definitely possible, with a few caveats. For example, you could have a Partner Portal and a Customer Self-Service installed with 2 completely different website records and related metadata and have 2 Portal subscriptions so that each portal is running in a separate master Portal. Each site can be branded and will have its own set of authenticated users and URL (universal resource locator).

Multiple Portals

The first caveat is that if you wanted to have 2 separate customer self service portals, this would also be doable, however you would need to re-work the website and metadata record GUIDs to be unique the base portal is loaded from a common set of metadata when the portal is provisioned.

The second issue is cost. This method requires a separate Portal subscription. In some cases, this method makes absolute sense. In other cases, having a completely separate portal may be total overkill for a requirement to have a site with its own separate branding and page navigation.

Additional Portal Licenses

NOTE: The following is not meant as a way around getting an additional Portal License. It is for those cases where there is a requirement to have a separate branding page(s) where provisioning a complete portal is overkill.

Configure a Multi-Brand Portal

The following is an example where we need to have three distinct team sites, each with its own unique branding, navigation and web pages. Users will be authenticated once, but using Web Roles could be assigned or locked out of specific portals.

The following also assumes a fairly solid understanding of Dynamics 365 Portals components and how they work. To get ramped up on portals, visit the Microsoft Docs site. Please watch this blog for further Portal learning activities.

High Level Portal Design

Below is what our main landing page will look like, each link will lead to a Portal sub-site:

Multi-brand portal

We can choose to have a landing page, or provide the URL to the specific sub-site to our users. From here will effectively have three different portal sub sites, each with their own branding, navigation and web pages:

Blue, Green and Red Portal Sub-sites.

In order to configure a Portal with sub-sites, we need to build out a set of Web Templates, Web Link Sets and Content Snippets as well as various web files.

  • Web Template Records (and corresponding Page Template Records) for the main portal landing page as well as each Portal sub-site landing page.
  • Web Page Records for the landing pages for the main landing page and each sub-site.
  • A Web Link Set for each Portal sub-site to act as the Primary Navigation for that particular sub-site.
  • Content Snippets for the sub-site landing pages.
  • Web Files for the various images for each landing page and links.
  • A CSS file (Web File) to hold the main styling for each portal landing page.
  • A custom Header and Footer Web Template that will determine which portal sub-site the user is currently navigating. This particular Web Template is where most of the magic happens.
  • The Header/OutputCache/Enabled Site Setting set to FALSE. This is key for the Header Web Template to properly render the sub-sites.

I have put examples of all the template files my public GitHub repository as well as an export of the records using the XrmToolBox Portal Records mover. DO NOT LOAD THIS INTO YOUR PRODUCTION SYSTEM! These are just an example and could damage an existing portal. Load them into a sandbox or trial system.

In my example, I have a common landing page with links to each of the branded portal sites. This is pretty straight forward HTML with a bit of Liquid code. (ReadyXRMHome)

{% comment %}
ReadyXRMHome Page
Multi-Portal Sample
Nick Doelman
2019.1.18
{% endcomment %}
<div class="home-background">
    <section class="page_section">
        <div class="container">
            <div class="row" style="padding-top:100px;padding-bottom:150px">
                <div class="col-md-12" style="text-align:center;">
                    <!--<h1>{% editable snippets 'Home/Title' type: 'text' tag: 'span' %}</h1>-->
                    <h2><span class="home-subtitle">{% editable snippets 'Home/Subtitle' type: 'text' tag: 'span' %}</span></h2>
                </div>
            </div>
            <div class="row" style="padding-top:0px;padding-bottom:25px">
                <div class="col-sm-4 col-lg-4" style="text-align:center;">
                    <em><a href="/BLUE_Landing" style="font-size: 30px">BLUE TEAM</a></em>
                    <br />
                    <a href="/BLUE_Landing"><img alt="BLUE TEAM" style="width: 200px; height: 200px;" src="/BLUE.png" /></a>
                </div>
                <div class="col-sm-4 col-lg-4" style="text-align:center;">
                    <em><a href="/RED_Landing" style="font-size: 30px">RED TEAM</a></em>
                    <br />
                    <a href="/RED_Landing"><img alt="RED TEAM" style="width: 200px; height: 200px;" src="/RED.png" /></a>                  
                </div>
                <div class="col-sm-4 col-lg-4" style="text-align:center;">
                    <em><a href="/GREEN_Landing" style="font-size: 30px">GREEN TEAM</a></em>
                    <br />
                    <a href="/GREEN_Landing"><img alt="GREEN TEAM" style="width: 200px; height:200px;" src="/GREEN.png" /></a>                  
                </div>
            </div>
        </div>
    </section>
</div>

Each of our sites will require their own Primary Navigation. We will set up Web Link Sets for each site. It’s a good idea to have a link back to the main site, but here would build out the navigation to the unique pages for the sub-site.

Web Link Sets for each site

NOTE: I am currently having an issue with nested Web Links, while the main Web Links work, for some reason nested links are not rendering properly. Watch the GibHub site for updates.

The main magic is the in the Web Template Header file. Essentially it evaluates the urlpath of the portal. If it recognizes the sub-site partial URL it will then replace specific elements (such as CSS classes, Content Snippets) The main thing is that it will set the primary_nav to the specific brand, with the result being each sub-site has it’s own navigation. (ReadyXRMHeader)

{% comment %}
ReadyXRMHeader Template
Multi-Portal Sample
Nick Doelman
2019.1.18
{% endcomment %}

{% assign defaultlang = settings['LanguageLocale/Code'] | default: 'en-us' %}
{% assign homeurl = website.adx_partialurl %}

{% assign urlpath = request.path | downcase %}
{% if urlpath == "/" %}
  {% assign rxrm_brand = "home" %}
  {% assign rxrm_navclass = "navbar rxrm-navbar-home navbar-static-top" %}

{% elsif urlpath contains "red_landing" %}
  {% assign rxrm_brand = "RED" %}
  {% assign rxrm_navclass = "navbar navbar-inverse navbar-static-top" %}

{% elsif urlpath contains "blue_landing" %}
  {% assign rxrm_brand = "BLUE" %}
  {% assign rxrm_navclass = "navbar navbar-inverse navbar-static-top" %}

{% elsif urlpath contains "green_landing" %}
  {% assign rxrm_brand = "GREEN" %}
  {% assign rxrm_navclass = "navbar navbar-inverse navbar-static-top" %}

{% elsif urlpath contains "profile" %}
  {% assign rxrm_brand = "profile" %}
  {% assign rxrm_navclass = "navbar rxrm-navbar-home navbar-static-top" %}

{% else %}
  {% assign rxrm_brand = "other" %}
  {% assign rxrm_navclass = "navbar rxrm-navbar-home navbar-static-top" %}

{% endif %}
<div class="{{ rxrm_navclass }}" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <div class="visible-xs-block">
                {% editable snippets 'Mobile Header' type: 'html' %}
            </div>
            <div class="visible-sm-block visible-md-block visible-lg-block navbar-brand">
                {% if rxrm_brand == "RED" %}{% editable snippets 'RED Navbar Left' type: 'html' %}
                {% elsif rxrm_brand == "BLUE" %}{% editable snippets 'BLUE Navbar Left' type: 'html' %}
                {% elsif rxrm_brand == "GREEN" %}{% editable snippets 'GREEN Navbar Left' type: 'html' %}
                {% else %}{% editable snippets 'Navbar Left' type: 'html' %}
                {% endif %}
            </div>
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" onclick="setHeight();">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
            {% if rxrm_brand == "RED" %}{% assign primary_nav = weblinks["RED Primary Navigation"] %}
            {% elsif rxrm_brand == "BLUE" %}{% assign primary_nav = weblinks["BLUE Primary Navigation"] %}
            {% elsif rxrm_brand == "GREEN" %}{% assign primary_nav = weblinks["GREEN Primary Navigation"] %}
            {% elsif rxrm_brand == "profile" %}{% assign primary_nav = weblinks["Profile Primary Navigation"] %}
            {% else %}{% assign primary_nav = weblinks["Primary Navigation"] %}
            {% endif %}

            {% if primary_nav %}
            <div class="navbar-right menu-bar {% if primary_nav.editable %}xrm-entity xrm-editable-adx_weblinkset{% endif %}" data-weblinks-maxdepth="2">
                <ul class="nav navbar-nav weblinks">
                    {% for link in primary_nav.weblinks %}
                    {% unless forloop.first %}
                    <li class="divider-vertical"></li>
                    {% endunless %}
                    {% if link.display_page_child_links %}
                    {% if link.url != null %}
                    {% assign sublinks = sitemap[link.url].children %}
                    {% endif %}
                    {% else %}
                    {% assign sublinks = link.weblinks %}
                    {% endif %}

                    <li class="weblink {% if sublinks.size > 0 %} dropdown{% endif %}">
                        <a {% if sublinks.size>0 -%}
                            href="#" class="dropdown-toggle" data-toggle="dropdown"
                            {%- else -%}
                            href="{{ link.url | escape }}"
                            {%- endif -%}
                            {%- if link.nofollow %} rel="nofollow"{% endif -%}
                            {%- if link.tooltip %} title="{{ link.tooltip | escape }}"{% endif %}>
                            {%- if link.image -%}
                            {%- if link.image.url startswith '.' -%}
                            <span class="{{ link.image.url | split:'.' | join }}" aria-hidden="true"></span>
                            {%- else -%}
                            <img src="{{ link.image.url | escape }}" alt="{{ link.image.alternate_text | default:link.tooltip | escape }}" {% if link.image.width %}width="{{ link.image.width | escape }}" {% endif %} {% if link.image.height %}height="{{ link.image.height | escape }}" {% endif %} />
                            {%- endif -%}
                            {%- endif -%}
                            {%- unless link.display_image_only -%}
                            {{ link.name | escape }}
                            {%- endunless -%}
                            {%- if sublinks.size > 0 -%}
                            <span class="caret"></span>
                            {%- endif -%}
                        </a>
                        {% if sublinks.size > 0 %}
                        <ul class="dropdown-menu" role="menu">
                            {% if link.url %}
                            <li>
                                <a href="{{ link.url }}" {% if link.nofollow %}rel="nofollow" {% endif %} {% if link.tooltip %}title="{{ link.tooltip }}" {% endif %}>{{ link.name }}</a>
                            </li>
                            <li class="divider"></li>
                            {% endif %}
                            {% for sublink in sublinks %}
                            <li>
                                <a href="{{ sublink.url }}" {% if sublink.nofollow %}rel="nofollow" {% endif %} {% if sublink.tooltip %}title="{{ sublink.tooltip }}" {% endif %}>
                                    {{ sublink.name | default:sublink.title }}
                                </a>
                            </li>
                            {% endfor %}
                        </ul>
                        {% endif %}
                    </li>
                    {% endfor %}
                    {% assign search_enabled = settings['Search/Enabled'] | boolean | default:true %}
                    {% if search_enabled %}
                    <li class="divider-vertical"></li>
                    <li class="dropdown">
                        <a class="navbar-icon" href="#" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" aria-label="{{ snippets["Header/Search/ToolTip"] | default:resx["Search_DefaultText"] | escape }}">
                            <span class="glyphicon glyphicon-search">
                        </a>
                        </a>
                        <ul class="dropdown-menu dropdown-search">
                            <li>
                                {% include 'Search' %}
                            </li>
                        </ul>
                    </li>
                    {% endif %}
                    <li class="divider-vertical"></li>
                    {% if website.languages.size > 1 %}
                    <li class="dropdown">
                        <a class="dropdown-toggle" href="#" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="{{ website.selected_language.name | escape }}">
                            <span class="drop_language">{{ website.selected_language.name | escape }}</span>
                            <span class="caret"></span>
                        </a>
                        {% include 'Languages Dropdown' %}
                    </li>
                    <li class="divider-vertical"></li>
                    {% endif %}
                    {% if user %}
                    <li class="dropdown">
                        <a href="#" class="dropdown-toggle" title="{{ user.fullname | escape }}" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
                            <span class="username">{{ user.fullname | escape }}</span>
                            <span class="caret"></span>
                        </a>
                        <ul class="dropdown-menu" role="menu">
                            {% assign show_profile_nav = settings["Header/ShowAllProfileNavigationLinks"] | boolean | default:true %}
                            {% if show_profile_nav %}
                            {% assign profile_nav = weblinks["Profile Navigation"] %}
                            {% if profile_nav %}
                            {% for link in profile_nav.weblinks %}
                            <li>
                                <a href="{{ link.url | escape }}" title="{{ link.name | escape }}">{{ link.name | escape }}</a>
                            </li>
                            {% endfor %}
                            {% endif %}
                            {% else %}
                            <li><a href="{{ sitemarkers['Profile'].url | escape }}">{{ snippets["Profile Link Text"] | default:"Profile" }}</a></li>
                            {% endif %}
                            <li class="divider" role="separator"></li>
                            <li>
                                <a href="{% if homeurl%}/{{ homeurl }}{% endif %}/Account/Login/LogOff?returnUrl={{ request.raw_url_encode }}" title="{{ snippets["links/logout"] | default:resx["Sign_Out"] | h }}">
                                    {{ snippets["links/logout"] | default:resx["Sign_Out"] | h }}
                                </a>
                            </li>
                        </ul>
                    </li>
                    {% elsif rxrm_brand != "home" %}
                    <li>
                        <a href="{% if homeurl%}/{{ homeurl }}{% endif %}/SignIn?returnUrl={{ request.raw_url_encode }}">
                            {{ snippets["links/login"] | default:resx["Sign_In"] | h }}
                        </a>
                    </li>
                    {% endif %}
                </ul>
                {% editable primary_nav %}
            </div>
            {% endif %}
            <div class="navbar-right hidden-xs">
                {% editable snippets 'Navbar Right' type: 'html' %}
            </div>
        </div>
    </div>
</div>
{% assign current_page = page.adx_partialurl %}
{% assign sr_page = sitemarkers["Search"].url | remove: '/' %}
{% assign forum_page = sitemarkers["Forums"].url | remove: '/' %}
{% if current_page %}
  {% if current_page == sr_page or current_page == forum_page %}
<section class="page_section section-landing-{{ current_page }} color-inverse">
    <div class="container">
        <div class="row ">
            <div class="col-md-12 text-center">
                {% if current_page == sr_page %}
                <h1 class="section-landing-heading">{% editable snippets 'Search/Title' default: resx["Discover_Contoso"] %}</h1>
                {% include 'Search' %}
                {% endif %}
            </div>
        </div>
    </div>
</section>
  {% endif %}
{% endif %}

You can also add the same logic to build out a custom footer for each portal sub-site:

{% comment %}
ReadyXRMFooter Template
Multi-Portal Sample
Nick Doelman
2019.1.18
{% endcomment %}

{% assign urlpath = request.path | downcase %}
{% if urlpath == "/" %}
  {% assign rxrm_brand = "home" %}
{% elsif urlpath contains "green" %}
  {% assign rxrm_brand = "GREEN" %}
{% elsif urlpath contains "red" %}
  {% assign rxrm_brand = "RED" %}
{% elsif urlpath contains "blue" %}
  {% assign rxrm_brand = "BLUE" %}
{% else %}
  {% assign rxrm_brand = "other" %}
{% endif %}

{% if rxrm_brand != "home" %}
<section id="gethelp" class="page_section section-diagonal-right color-inverse {% if page %}{% unless page.parent %}home-section{% endunless %}{% endif %} hidden-print">
    <div class="container">
        <div class="row">
        </div>
    </div>
</section>
{% endif %}
<footer role="contentinfo">
    <div class="footer-top hidden-print">
        <div class="container">
            <div class="row">
                <div class="col-md-6 col-sm-12 col-xs-12 text-left">
                    {% editable snippets 'About Footer' type: 'html' %}
                </div>
                <div class="col-md-6 col-sm-12 col-xs-12 text-right">
                    <ul class="list-social-links">
                        <li><a href="#"><span class="sprite sprite-facebook_icon"></span></a></li>
                        <li><a href="#"><span class="sprite sprite-twitter_icon"></span></a></li>
                        <li><a href="#"><span class="sprite sprite-email_icon"></span></a></li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
    <div class="footer-bottom hidden-print">
        <div class="container">
            <div class="row">
                <div class="col-md-4 col-sm-12 col-xs-12 text-left">
                    {% editable snippets 'Footer' type: 'html' %}
                </div>
                {% assign footer_nav = weblinks["Footer"] %}
                {% if footer_nav %}
                <div class="col-md-8 col-sm-12 col-xs-12 text-left {% if footer_nav.editable %}xrm-entity xrm-editable-adx_weblinkset{% endif %}" data-weblinks-maxdepth="2">
                    <ul class="row list-unstyled">
                        {% for link in footer_nav.weblinks %}
                        <li class="col-sm-3">
                            <h4>{{ link.name }}</h4>
                            <ul class="list-unstyled">
                                {% if link.display_page_child_links %}
                                {% assign sublinks = sitemap[link.url].children %}
                                {% else %}
                                {% assign sublinks = link.weblinks %}
                                {% endif %}
                                {% if sublinks.size > 0 %}
                                {% for sublink in sublinks %}
                                <li>
                                    <a href="{{ sublink.url | escape }}">{{ sublink.name | default:sublink.title }}</a>
                                </li>
                                {% endfor %}
                                {% endif %}
                            </ul>
                        </li>
                        {% endfor %}
                    </ul>
                    {% editable footer_nav %}
                </div>
                {% endif %}
            </div>
        </div>
</footer>
<script>
var signLocation = window.location.href;
 if(/signin/i.test(signLocation)) {
   $('form').parents(".col-md-6:last").remove();
 }
</script>

Once the custom Header and Footer Web Templates are built, we need to instruct the website to use these Web Templates, this is done in the Website record:

Website record

You can begin to build out a Web Template for each portal subsite home page. The main thing here is to refer to some custom CSS classes (e.g. section-landing-red) that will be set up to show a different background image:

{% comment %}
REDHome Template
Multi-Portal Sample
Nick Doelman
2019.1.18
{% endcomment %}

<section class="page_section section-landing section-landing-red">
    <div class="container">
        <div class="row ">
            <div class="col-md-12">
                <h2 class="section-landing-sub-heading">{% editable snippets 'RedLanding/Title' type: 'text' tag: 'span' %}</h2>
            </div>
        </div>
    </div>
</section>

<div class="container">
    <div class="page-heading">
        {% block breadcrumbs %}
        {% include 'Breadcrumbs' %}
        {% endblock %}
    </div>
    <div class="row">
        <div class="col-md-12">
            <section class="page_section">
                <div class="container">
                    <div class="content-home">
                        <h2 class="blue_border">{% editable snippets "RedLanding/Heading" type: 'text' tag: 'span' %}</h2>
                        {% include 'Page Copy' %}
                    </div>
                </div>
            </section>
        </div>
    </div>
</div>

We will add a custom CSS file (parented by the main home page) that will indicate special styling for each landing page. Further CSS styling can be done with additional CSS files that are parented by the subsite home pages.

/*
RXRMcustom.css
Multi-Portal Sample
Nick Doelman
2019.1.18
*/

.section-landing-blue {
    background: linear-gradient(transparent, transparent), url("BLUEhero.jpg") no-repeat center;
    background-size: cover; 
  }
  .section-landing-red {
    background: linear-gradient(transparent, transparent), url("REDhero.jpg") no-repeat center;
    background-size: cover; 
  }
  .section-landing-green {
    background: linear-gradient(transparent, transparent), url("GREENhero.jpg") no-repeat center;
    background-size: cover; 
  }
  
  .home-subtitle {
      color: black;
      text-align: center;
  }
  .home-background {
      background: linear-gradient(transparent, transparent), url("Homehero.jpg") no-repeat top;
      background-size: auto 100%;
  }

You will also need to make sure that your header cache is set to false. While this may slightly slow down loading of portal pages, you will need it to force the portal to show the appropriate navigation for each sub-site.

Header/OutputCache/Enabled

Following these steps, you should be able to continue to configure your portal subsites to their specific brand purposes and apply custom theming and styling to each site by aligning a CSS web file parented by the subsite home page.

Branded Sub-site

I have loaded the sample Web Templates as HTML files on my GitHub site as well as the XrmToolBox Portal Mover export file. Note that these are examples and should NOT be applied to an actual production site. You have been warned. 🙂

Summary

Using a custom header Web Template, a portal can be configured to have multiple subsites for specific branding or functionality.

Thanks to my former colleague, Debra Polvi for doing a lot of the original groundwork for these methods.

Cover Photo by Brooke Cagle on Unsplash

Nick Doelman is a Microsoft Business Application MVP. Nick is currently travelling the world teaching Dynamics 365 Portals.  Follow Nick on Twitter @ReadyXRM

9 thoughts on “Building a Multi-Brand Dynamics 365 Portal

  1. How to have different URLs for the sub sites created from the main Portal? Could you please provide some brief articles on this?

    Like

    1. Unfortunately, some other MVPs and I looked into a way to have unique URLs for subsites as well as chatted with folks from the product team and this is not possible. These would need to be separate portals to have unique URLs.

      Like

  2. Hi Nick!

    Thank you for your article. Do I understand correctly the way you’re describing having multiple sub-sites doesn’t require separate Portal subscriptions?

    Also, I am wondering if you have handled the issue with the web link sets you mentioned.
    02.23.2021-17.59.41
    I am asking because it seems the solution you’ve provided might be a good one for our use-case.

    Like

    1. Correct, in this example, all the portal subsites run from one website record, one Dataverse and one portal. However, with the new licensing it comes down to portal logins and page views. So whether you have 5 portals or 1 portal, a hundred users will still consume a hundred portal logins. In terms of the weblink issue, I haven’t found a fix, but I think it only affects the “home page”, this hasn’t been an issue with some of my projects that run multiple programs (and require different branded portal subsites). I hope this makes sense. The program manager for Portals at Microsoft actually liked and commented on this approach (so I know its OK). I hope this info helps. Cheers, Nick

      Like

  3. Thanks Nick, helpful as always.

    Is it possible to have menu items disable / hidden based on Contact field values?

    If Contact.IsAMember === true
    mnuApplyForMembership.hide()

    Like

    1. Hi Glenn, sorry for the delay in replying. If a portal user doesn’t have access to a page (controlled by page permissions and webroles), by default the menu item will disappear. So not directly based on contact values, but based on contact web roles and page permissions. Hope that makes sense.

      Like

Leave a comment