Post Featured Image

Control Page Navigation and Redirection

This is appropriate for SuiteCommerce and all versions of SuiteCommerce Advanced, but it will use features and code practices suitable for the most recent versions. For the purposes of this blog post, I will use the term 'SuiteCommerce' to refer to both products.

Today's topic is a little complex and advanced: URL routing. Specifically, I am talking about the type of routing that is done within the context of your web store. We want to know how a user gets from requesting a page to being shown that page.

Depending on how the URL is provided by the user, there are different layers of processing before the final page is shown to users. In order to illustrate this, I have created a simplified version of what is happening below in a flow chart:

There are a number of interesting things in the flow chart that we can talk about:

  1. The difference between processing a URL requested by a user through the browser address bar, and 'internally' within a Backbone single-page application (SPA)
  2. Handling URL redirects in the NetSuite application
  3. Redirecting a user in an SSP once it has begun loading
  4. Routing URLs from within an SPA

There is also another scenario not explicitly highlighted within the flow chart, and that is routing URLs that originate from within the SPA but trigger behavior akin to a user entering it directly into the address bar. In the context of SuiteCommerce, this occurs when a user is directed to an SSP outside of their current one. This commonly happens, for example, when a user is using the touchpoint for shopping (shopping.ssp) and then proceeds to the checkout (checkout.ssp) or their account (myaccount.ssp). I will call this touchpoint traversal.

So, we'll talk about that, and the rest of the above points. We'll also look at some common customization examples:

  1. Changing the exit URL a shopper is sent to when they log out of a web store
  2. Triggering a route/redirect from within the Backbone single-page application
  3. Automatically creating href values in templates (including via the configuration record)
  4. Generating links to parts of the site outside of the current touchpoint, including specific pages (eg creating a link to the orders page in the customer center while the user is in the shopping application)

Let's take a look at these along with the interesting areas mentioned above by first starting with what could happen in the NetSuite application.

NetSuite Redirects

This is a massive simplification, but when a user hits a URL that points to a NetSuite commerce application, the URL is processed to see if there are any domain-level redirects. These are set up by administrators who want to take an incoming URL and point it to a different location; this can be done by changing the domain part of the URL, the URL path after it, or a combination of both. What this means is that you can create a map between two URLs and then when the URL is hit on the server, it will rewrite the requested URL and then process it again.

These are particularly useful if you're migrating your web store from a platform outside of NetSuite to us, or if you've updated your web store application from a pre-Denali version of SuiteCommerce Advanced to a modern version. For example:

  • shopflow.example.com → shop.example.com
  • shop.example.com/my-old-landing-page → shop.example.com/my-new-landing-page
  • shop.example.com/product-that-doesnt-exist-anymore → shop.example.com

A key thing to keep in mind is that the NetSuite application can only process redirects that are sent to the NetSuite server. In other words, they will work if a user has entered the URL into their browser's address bar, but if you internally direct a user to that URL using Backbone, they won't trigger the serverside redirect.

I'm not going to talk too much about NetSuite redirects (as we have documentation on it), but the point is that once URL redirects are done and dusted, we then direct the user to the correct SSP for that URL. This is determined by the touchpoint you have set in your website setup record for that domain. That could mean a web store's shopping SSP when they request the homepage, or it could mean the checkout SSP when they request the login touchpoint. We'll talk about touchpoints later, but for now, we can assume they've been sent to the correct one, which has then begun the process of loading the site's frontend code. But, that process can be interrupted if there is a redirect in the SSP's SuiteScript.

Redirecting Users with SuiteScript

There are methods built into the SuiteScript API (both newer and older versions) which allow users to be redirected around NetSuite. In SuiteScript 1.0, that method is nlapiSetRedirectURL(), and in SuiteScript 2.x we have the N/redirect module. For the purposes of this blog post, I'll stick to SS1.0 as that is currently the most widely used in SuiteCommerce right now.

If you have access to the source code, you can do a search for nlapiSetRedirectURL, and you should see that we use it throughout our SSP code. A simple example is in logOut.ssp, which is the logout touchpoint, and is the SSP file that is called when a user is to be logged out. If you've ever wanted to change where users go when they are logged out, this is where you'd set it. A crude example could be:

return nlapiSetRedirectURL('EXTERNAL', 'https://www.netsuite.com');

However, this isn't particularly useful — why you would you redirect someone to another site? You can of course, set it to a page on your site: but just keep in mind that the type is still set to EXTERNAL as it is, technically speaking, meant to be used to point to a place within the NetSuite application, and the EXTERNAL type is meant to be used to point to a publicly accessible Suitelet. Ah well, we'll use it for HTML pages anyway.

Before using it, you should read our documentation on application navigation APIs.

Example: Change the Exit Page after Logging Out

Let's consider a real-life example that someone asked me about. They said that when a customer logs out, they want to redirect them to a special page that shows them a nice message and then tries to re-engage with them. For example, they may want to display some promotional products in a merchandising zone, or perhaps encourage them to sign up to their newsletter. Whatever they want, the content is not the tricky bit; the question is: how do they send all freshly logged out users to this page?

Well, first you create your page. I'm creating mine as a landing page in the site management tools, and I'm setting the URL path to /come-back-soon. I've added my cool message and re-engagement content and published it, now I want to direct all those users to it.

Now, I've created a copy of logOut.ssp and set up my developer tools to override the existing one (yes, this is the best practice FYI) and then in it I have put:

<%
    var session = require('SC.Models.Init').session;
    var touchpoints = session.getSiteSettings(['touchpoints']);
    var home = touchpoints.touchpoints.home;
    var fragment = 'fragment=/come-back-soon&logoff=T'; // a nuance of logOut.ssp is that we must attach this additional parameter to the end
    var redirectUrl = home + (~home.indexOf('?') ? '&' : '?') + fragment;

    return nlapiSetRedirectURL('EXTERNAL', redirectUrl);
%>

For those who can't follow along: we get the home touchpoint, attach our desired URL path to the end (along with a required URL parameter) and then return it back. Et voilà:

This method is used repeatedly throughout SuiteCommerce. For example, in shopping.ssp we have a conditional set up so that if the site is configured to be password protected, then it will redirect the user to the login page before loading the shopping SPA unless they are already logged in.

URL Routing and Single-Page Applications

We use Backbone to power our single-page application architecture. A SuiteCommerce web store is built up using three SPAs linked together. As a reminder, one of the reasons we use SPAs to cache the code structure of the site, its resources and associated data so that when a user navigates around, it's super fast. This means that everything happens internally within the SPA, so that the page doesn't need to send a request to the server for a new page every time a user wants to move around a site — it only needs to fetch new resources or data.

What this means is that in order to enable this kind of navigation, we need to use special mechanisms to navigate the user around whenever possible.

What to Avoid: Standard JavaScript Mechanisms

If you were working with a site outside of an SPA framework, then you could just use standard JavaScript to change location by using something like this:

var url = '/search';

// Non-fancy
window.location.href = url;

// Fancy
window.location.assign(url);

The problem with this is that this is equivalent to a user entering a URL into their browser's address bar. Therefore, they will go through the full flow chart I detailed above. And this means, above all, that we are killing the SPA and reloading it each time. Bad for performance, that, and not what we want to do most of the time.

What to Do Most of the Time: Rely on Backbone

There are numerous times when we might process a user's location in the SPA:

  1. When the SPA is passed a URL path to process
  2. When a user clicks a navigation link generated in a template
  3. When a user has completed a wizard (such as filling out a form) and needs an exit URL

There are others, but, in short, it means that there needs to be a mechanism within the framework to handle these.

When you create a route in Backbone, by using the routes array in a router or page type, you are creating a link between a URL path and a callback. That callback determines what to do when that route is triggered. From a simplistic point of view, that callback will usually fetch data and then render a specific view.

So, how do we trigger these routes? Underlying the above examples is a method attached to the Backbone.history class called navigate(). It takes a route and then an options object; typically the only option you will need to pass it as a developer is a request to trigger the navigation immediately. For example:

Backbone.history.navigate('/search', {trigger: true})

This will relocate the user to the /search path, which in my case is the route for the main search PLP.

Listing All Backbone Routes

You might be interested in how Backbone 'knows' how to route the URL fragment it's passed. You should, of course, read their documentation for more details (including their annotated source file), but we can also get a sense of it by poking around a site using a browser's developer tools. For example, we may want to get back a list of all routes registered with the current SPA. Load up your site and in your developer console, type in:

Backbone.history.handlers

What you'll see an array of objects, each one with a route and its associated callback. There's a few interesting things to note about this.

First is that the routes are stored as regexes. If you're not familiar with regexes, lucky you. Regexes (also known more formally as regular expressions) are a way of defining a search pattern for strings. In other words, rather than storing an exact string, you store an abstract pattern of that string so that it catches, for example, variations. What this means is that you pass a character string which is then evaluated against a regex to determine whether they 'match'. They're way too complicated to go into detail here but the point is that if, during evaluation, one of them returns true then it triggers that route's callback (and thus starts the navigation process). Neat.

The second is precedence: when Backbone is asked to process a URL path, it will run the string against each of the regexes in order until it finds a match. It is not uncommon for mature sites to have hundreds of routes, particularly if they have many categories and landing pages. When they are exhausted, the default route kicks in, which is to direct the user to a 404 page.

However — and this is the third and final interesting point — on the shopping application, there is one final pre-default check and that is for product pages. Because of the sheer number of possible URL paths for product detail pages they are not individually included in the list of routes and, instead, the shopping SPA is set up to try fetching product data via the items API using the specified URL prior to returning a 404. If that call succeeds, then the user is navigated to the PDP for that URL; if it fails then all routes have been exhausted and the user is directed to the 404 page. If you've wondered why a 404 page coincides with a failed items API call, this is why.

Traversing Touchpoints

Switching from one section of a site to another — ie crossing the boundaries of your current SPA — can be tricky because of the problem of generating the correct URLs, so this is where touchpoints come in.

When you set up your site, you must include a number of touchpoints. The simplest way to understand these is that they are a map between areas of your site and the SSP application that operates that area. You set these in site setup record or in the domain record for your site.

We don't talk about them much on this site, or in the documentation really, but there can be a lot of customization flexibility in this area. However, the main reason why we don't really talk about it is that it is usually unnecessary to make changes to a web store's touchpoint map, or to the SSP files themselves once you've completed the initial setup. Furthermore, customization to the SSP files themselves is a customization avenue only available to SuiteCommerce Advanced sites, so we typically only recommend making changes to them if you are an experienced developer who knows what they're doing (eg, when bootstrapping data). If you've worked with Java web applications before then you may be familiar with a site's web.xml file, which operates as a configuration map similar to the website setup record and its touchpoints.

I'm not going to labor this point, or allow myself to digress too much, but customizations in this area typically come in two forms:

  1. Making a change to an SSP file's code
  2. Swapping a touchpoint's SSP application for another

I'm not going to cover either of those here. However, if you're familiar with Site Builder or older, pre-Denali versions of SuiteCommerce Advanced, then you'll know it used to be pretty common to have great diversity in your touchpoints. For modern sites, however, they are all usually for the same version.

Finally, note that if you're thinking of doing this for generating links to pages via an anchor tag in your template, you should read this section and then scroll down, because there is a template-specific method for doing this stuff.

Getting the Correct URLs

So, I bring in touchpoints because they each have their own entrance URLs. If you want to direct a user to one of those entrances, or if you want to direct them to a specific path in one the applications they operate, you will need to know those URLs. Luckily for us, NetSuite developers have anticipated this need and coded it so that they are automatically generated and easily pulled into your code.

If you're using Aconcagua R2 or newer, you can access them through the Environment component in the extensibility API; older sites can use the Session module. For example:

// Modern sites
// Assuming you have a container/application object available
var Environment = container.getComponent('Environment');
var touchpoints = Environment.getSiteSetting('touchpoints');

// Older sites
// Assuming you've added the Session module as a dependency
var touchpoints = Session.get('touchpoints');

If you log this value to the console you'll an object of the various touchpoints of your site (there'll be some additional ones to the ones in the site setup record, but we don't need to worry about those).

If you were to copy and paste the URLs for any of the main touchpoints (eg login, logout, checkout, etc) into your browser's address bar, you will go to those locations. So let's work them into our code.

The JavaScript Mechanism for Triggering a Touchpoint Traversal

I said earlier that we want to avoid using the standard JavaScript method of changing location using window.location. However, in the context of a touchpoint change, it is correct when you are certain that it will require a change in application.

Before we move on to some examples, it is worth noting that Backbone creates a pointer to the window.location object, which is referenceable with Backbone.history.location. I can't find a specific reason for them doing this, and a developer more knowledgeable than me said that it seems that it's just for housekeeping: they're going to rely on the object throughout their code, so they want to give it a contextual name. Accordingly, I would probably recommend using the Backbone reference to it, but that is up to you.

Anyway, all you really need to know in this regard is:

Backbone.history.location === window.location
> true

In other words, they are identical.

So, knowing what we know about getting the correct URLs and using the mechanisms, it's now just a case of programmatically doing it. For example:

// Assuming you've set the touchpoints object using the code above

// Go to the login page
Backbone.history.location.assign(touchpoints.login)

// Log a user out
Backbone.history.location.assign(touchpoints.logout)

// Go to the checkout
Backbone.history.location.assign(touchpoints.checkout)

You get the idea. So, if you want to perform some sort of operation and then, say, log a user out, you could use logout touchpoint and assign() to navigate them there.

Directing a User to a Specific Page on Another Touchpoint

It is also possible to attach a hashtag to the URL when you direct a user so that they land at a specific page in related to that touchpoint. For example, you may want to send a shopper to their purchase history in their account area.

// Assuming you've set the touchpoints object's value using the code above
var url = touchpoints.customercenter;
var fragment = 'fragment=/purchases';
url+= (~url.indexOf('?') ? '&' : '?') + fragment;
Backbone.history.location.assign(url);

In essence, this is just a slightly more robust way of writing Backbone.history.location.assign(touchpoints.customercenter + '&fragment=/purchases'), but our way ensures that we use the correct syntax as we don't want to include multiple ?s in our request URL (and some touchpoint URLs already have URL parameters attached to them already).

Automatic Link Generation in Templates

The above code is fine if you're writing JavaScript, but if you just want to create a link and have show it in the page, it is better to let the application do the heavy lifting.

One of the features we have built into the SuiteCommerce bundle code is automatic generation of a correct href attribute on anchor tags: all a template developer needs to do is put populated data-touchpoint and data-hashtag attributes on their anchor tags and the template compiler will do the rest. This is particularly useful in the development of themes as it means these customizations can be made without having to write a single line of JavaScript.

A good example of this is in header_profile.tpl, which contains code for generating a link to the login page:

<a class="header-profile-login-link" data-touchpoint="login" data-hashtag="login-register" href="#">

This directs the user to the login page when clicked. But how? There's no href in the template, but there is in rendered page? Don't worry — the application handles that when the template is rendered! Magic.

If a developer had to figure out what to put into their template (or its context) each time, it would certainly be problematic, and so our developers have created the NavigationHelper module to do as much of this automatically as possible. In particular, for templates, we have NavigationHelper.Plugins.DataTouchPoint, but there are other files to handle other situations. Feel free to look around these modules to get an idea.

What you need to know as a developer is that if you want to create a link in your template to another part of the site, you need to define the URL path (ie route) via a data-hashtag attribute, and its application (ie shopping, my account, or checkout) by putting its associated touchpoint in the data-touchpoint attribute.

Automatic Link Generation in the Header Navigation via Configuration Record Changes

I would like to add that you can test the above customization behavior really quickly by adding new links into your site's header navigation by modifying your site's configuration record/files.

In Setup > SuiteCommerce Advanced > Configuration and then Layout > Navigation, you can add new entries in the table like this:

The links in question are:

data-touchpointdata-hashtagDescription
home#/Triggers the home touchpoint, which points to shopping.ssp, and therefore routes the user to the homepage.
home#/searchTriggers the home touchpoint, which points to shopping.ssp, and therefore routes the user to the search PLP.
customercenter#/purchasesTriggers the customercenter touchpoint, which points to my_account.ssp. The SSP checks if the user is logged in: if they're not, they're routed to the login page first, storing their desired location; if they're already logged in (or if they've just done it) they're directed to their purchase history.
login#/login-registerTriggers the login touchpoint, which points to checkout.ssp. The SSP checks if the user is logged in: if they're not, they're directed to the login page; if they are, they are routed to the start of the checkout.

Now, for the record, these are not necessarily good things to put in your header navigation because going to the login touchpoint when you're already logged in will send you to the checkout, so it's slightly misleading, but you should get the idea that I'm trying to convey. The descriptions are also simplifications, as there is more going on under-the-hood, but you get the idea.

The key thing for us is that all of these configurations are used to generate links; these links have their href attributes generated automatically by code in the SuiteCommerce core bundle, and that's really cool and easy!