Skip to content
Franco Mussopower pages
Raindrops hanging from a ceiling
Photograph · Wang Yifang / Unsplash
Guide No.021
Deep diveLiquid · Multimedia

Custom Per-Partner Branding in Power Pages – Part 2: Liquid and CSS

Our goal here is to present contacts from different partners logging into the portal with personalised branding specific to their account / the partner they work for.

Level
04ArchitectStage 4 of 4
Read
16 min
Includes
15 snippets · 5 figures
Written
Nov 2022

Our goal here is to present contacts from different partners logging into the portal with personalised branding specific to their account / the partner they work for. We’ll drive this from custom fields on the Account table.

Another common use for this might be presenting different branding per sub-brand within a parent company’s portal.

Here are some examples of this in effect… I’ve switched my parent account between Sony Music:

Power Pages
Example of custom styling for contact from Sony Records logged into partner portal
Fig. 01Example of custom styling for contact from Sony Records logged into partner portal

…and Atlantic Records:

Power Pages
Example of custom styling for contact from Atlantic Records logged into partner portal
Fig. 02Example of custom styling for contact from Atlantic Records logged into partner portal

What we’ll learn in this session:

The partner portal (as well as Custom and Customer Self-Service) use the following Content Snippets for branding:

These each do much the same thing – providing the header styling (whether you have that set to use text only, or an image). Navbar provides this at medium resolutions and above and then (as the name suggests), the Mobile Header is used for low resolutions e.g. on mobile devices.

You may be wondering, “what about storage costs, isn’t expensive to have a load of images stored in the database?”. The good news is that Dataverse image columns actually use Azure Blob Storage so no issue.

Styling the navbar with our primary and secondary colours

We’ll need somewhere to store our CSS. We need this to load after the usual theme CSS so that it’s not superseded.

For this, we can use (or create) the Head/Bottom content snippet.

Note: for a list of useful Site Settings and Content Snippets, see MVP and friend of mine, Oleksandr Olashyn’s excellent resource at https://github.com/OOlashyn/PowerAppsPortalSiteSettingsAndSnippets . His blog is a fantastic resource for Power Pages / Power Apps Portals also: https://www.dancingwithcrm.com

For the impatient skimmers among you (that’s usually me!), here’s my final code for the Head/Bottom content snippet. Then if you’re still here, we’ll go into what it all means…

CSS made conditional and dynamicLiquid
{%- if user.parentcustomerid -%}
    {%- assign account = entities['account'][user.parentcustomerid.id] -%}
    {%- if account.musdyn_portalbrandingprimarycolour or account.musdyn_portalbrandingsecondarycolour -%}
        <style>
        .navbar.navbar-inverse.navbar-static-top {
            background: {{ account.musdyn_portalbrandingprimarycolour | default: '#000' }} !important;
        }
 
        .navbar-static-top.navbar .menu-bar > .navbar-nav > li > a,
        .navbar-static-top.navbar .menu-bar > .navbar-nav > li > .dropdown-menu > li > a,
        .navbar .navbar-icon span {
            color: {{ account.musdyn_portalbrandingsecondarycolour | default: 'red' }} !important;
        }
        </style>
    {% endif %}
{% endif %}

Now for the how…

In the portal management app, select Content Snippets from the left nav menu.

Search for Head/Bottom

If you get a result, open it for editing. If not, create new.

Here’s some CSS to get us started:

CSS prior to making dynamicHTML
        <style>
        .navbar.navbar-inverse.navbar-static-top {
            background: #000 !important;
        }
 
        .navbar-static-top.navbar .menu-bar > .navbar-nav > li > a,
        .navbar-static-top.navbar .menu-bar > .navbar-nav > li > .dropdown-menu > li > a,
        .navbar .navbar-icon span {
            color: #FFF !important;
        }
        </style>

Getting the data

First step – access the user’s account record

Our partner will be an Account record related to the logged in portal contact via the Company Name (aka parent Customer) column.

We don’t need a query to get the portal user’s contact record. It’s always available to us in liquid via the user object.

We can use this in various ways…

To check if the visitor is a logged in user (contact), like so:

Liquid
{% if user %} whatever {% endif %}

To check whether the user has a specific web role, like so:

Liquid
{% if user.roles contains 'Premium' %} whatever e.g. show members-only content {% endif %}

Or the opposite (if the user does not have a specific web role):

Liquid
{% unless user.roles contains 'Premium' %} whatever e.g show subscription special offers {% endunless %}

Or we can access columns from the contact record using dot notation and the logical / database name of the column e.g.

Liquid
{{ user.firstname }}

would output the portal users’ first name.

Nice and easy BUT more complex column types such as lookups and choice columns (aka option sets) require us to be a little more specific…

For Lookup columns, liquid gives us an object made up of the GUID and the name of the record

To access the GUID, we need to use columnlogicalname.id e.g.

parentcustomerid.id

To access the name of the record, we use columnlogicalname.name e.g.

parentcustomerid.name

For Choice, Status and Status Reason fields, liquid provides an object containing the numeric value and label for each item

To access the numeric value, we use columnlogicalname.value e.g.

statuscode.value

To access the label, we use columnlogicalname.label e.g.

statuscode.label

Here, I need the GUID of the contact’s account. The logical name for the lookup column I need is parentcustomerid. Therefore to get the GUID from this lookup via the user object, I need:

Text
user.parentcustomerid.id

So we’ve got the GUID of the record, now to retrieve the record…

The entities object makes it nice and easy… one line of code to both retrieve the record and store it in a variable.

The entities object requires 2 pieces of information from us:

We can use our user.parentcustomerid.id from earlier

To be able to access that record and actually do something with it, we need it in a variable. That we can do with assign.

So, altogether that’s:

Liquid
{% assign account = entities['account'][ user.parentcustomerid.id] %}

Note – if you have more complex needs e.g. to retrieve a row/record where you don’t have a GUID or a direct relationship with the user object, see here for liquid’s FetchXML tag

Now we can access columns from that account record using dot notation, much like we did with the user object.

To get started, lets just ‘print’ the primary and secondary colour values on the screen

Liquid
{{ account.musdyn_portalbrandingprimarycolour }}
Liquid
{{ account.musdyn_portalbrandingsecondarycolour }}

Not exactly what we want just yet but later on, we’ll pass these values into out CSS to control how things are styled; applying the primary colour as the background and the secondary colour as the font colour.

Next, let’s drop our primary & secondary colours into the CSS. For this, all we need to do is replace the hardcoded hex values in the sample above with liquid to output our text fields.
Let’s provide a fallback for when no primary or secondary colour have been selected. We could use an if condition. However, we’ve used that already and I’m all about expanding your liquid knowledge / toolset so we’re going to use a different approach: liquid’s default filter.

Liquid’s default filter – graceful degradation

Filters in liquid can do all kinds of magical things to transform our output. (I’ll post on this in future with many real world examples, far too many to cover here. If you’re interested, check out https://learn.microsoft.com/en-us/power-apps/maker/portals/liquid/liquid-filters in the meantime)

To add a filter, we type a pipe | character followed by the name of the filter we wish to use

Some filters require parameters e.g. in our example, that’s our fallback value.

For this, we need a : colon after the filter name, followed by the value we want to pass to that parameter.

As the name suggests, the default filter returns a default value for dynamic content (columns / variables) with no assigned value i.e. null.

Liquid
{{ account.musdyn_portalbrandingprimarycolour | default: '#000' }}

Here’s the CSS populated with our dynamic values, with defaults, wrapped in conditions to check whether:

Again, for the impatient skimmers, here’s the final code for my Navbar Left content snippet:

Liquid code to conditionally output Logo field from Account tableLiquid
{%- if user.parentcustomerid -%}
    {%- assign account = entities['account'][user.parentcustomerid.id] -%}
    {%- if account.musdyn_portalbrandinglogoid -%}
        <a href="~/" tile="home"><img class="desktop-logo" style="width: auto; height: 55px; margin-top: -1em;"
        src="~/Image/download.aspx?entity=account&attribute=musdyn_portalbrandinglogo&id={{ user.parentcustomerid.id }}&Full=true" 
        alt="{{ account.name }} logo"></a>
    {%- endif -%}
    {%- else -%}
        <a href="~/" tile="home"><img style="width: 120px; margin-top: -5px;" class="desktop-logo" src="~/songify-logo-coloured.svg" alt="Songify logo"></a>
{% endif %}

We’ll store this code in the content snippet called Navbar Left

Open the Portal Management app and select Content Snippets from the left nav menu

Search for Navbar Left and open the record for editing

Portal Management app
The Navbar Left content snippet in the portal management app. Type is set to HTML
Fig. 05The Navbar Left content snippet in the portal management app. Type is set to HTML

Outputting the image is slightly trickier. We need to use a specific URL format containing:

The logical name of the table we’re getting the image from

The logical name of the image column we want to retrieve

The GUID of the row (record) we want the image from

Here’s a completed example taken from https://learn.microsoft.com/en-us/power-apps/maker/portals/configure/image-column :

yourportalurl.com/Image/download.aspx?entity=contact&attribute=entityimage&id=cb059a4a-b1a6-ec11-9840-00224829604e

By default, this will get you a low resolution copy of te image. To get the full resolution, uncropped image, add the Full=true parameter like so:

Liquid
https://songify.powerappsportals.com/Image/download.aspx?entity=account&attribute=musdyn_portalbrandinglogo&id={{ user.parentcustomerid.id }}&Full=true

We can build this URL dynamically using liquid, like so:

Liquid
https://songify.powerappsportals.com/Image/download.aspx?entity=account&attribute=musdyn_portalbrandinglogo&id={{ user.parentcustomerid.id }}&Full=true

That’ should be looking pretty good so far but what about records where no logo has been uploaded? We don’t want to show the nasty ‘broken  image’ icon.

To work around this, we’ll first check whether the record has an image, using if

We have a helper column for this. It’s the column’s logical name followed by id.

Liquid
{% if account.musdyn_portalbrandinglogoid %}
 
Output the image
 
{% endif %}

Note – for graceful degradation, you could provide an alternative image as default

What we’ll focus on is how to make that dynamic using liquid.

First, let’s replace the placeholder image with our logo. Just cut the link and paste it over the img tag.

Catch / limitation / gotcha

Dataverse Image columns (and File columns for that matter) don’t support SVGs. Due to their tiny file size and infinite scalability, these would’ve been my first choice. Instead I’ll opt for PNGs with transparent backgrounds.

Go deeper · Power Pages Mastery

This guide is one technique. The course is the method.

The course covers this in Level 3 · Advanced customisation: “Liquid templating for dynamic components”, alongside “Transforming out-of-the-box lists”. A structured programme distilled from dozens of real portal launches — from messy stakeholder requirements to successful go-live.

  • 6 weeks
  • 3 levels
  • Live training + video lessons
Where this fits
  1. Level 1Foundations
  2. Level 2Extending the platform
  3. Level 3Advanced customisation
    • Liquid templating for dynamic components
    • Transforming out-of-the-box lists
There is a knowledge gap when we go from traditional Power Apps work to Power Pages. Franco’s depth of platform knowledge, creative problem-solving, and understanding of web design makes using him to skill up a team a no-brainer.
Tyler Page — Director & Solution Architect — MCA Connect
Filed underLiquidMultimedia

Comments

No comments yet. Spotted a gotcha I missed? Say so below.

Leave a comment

Your email address won’t be published. Comments are moderated.

Consulting

Need a Power Pages build that editors love and brands survive?

I help teams design and build Power Pages sites with bespoke UX. Tell me about yours.

Search

Book a discovery call

30 minutes. Bring your trickiest Power Pages problem — I’ll tell you honestly whether I’m the right fit.