A web application and an API with Salesforce Marketing Cloud using AMPScript, SSSJS and, React JS
A journey as a web developer with Data Extensions, Data Filters, Salesforce Objects, and more. From the web development process to a fully integrated Admin Interface in SFMC!
Introduction
At Almavia CX’s Web & Mobile business unit (former Novactive), we have been working on web applications for the past 20 years using various technologies, frameworks, and so on, with a bit more interest in Open Source technologies and stacks.
But since the acquisition and merge of Novactive with Almavia giving birth to Almavia CX, a new world has opened up: Salesforce. Now we are working with many new colleagues and clients who master different Salesforce “things”.
We are newbies with Salesforce in the Web & Mobile business unit, we have some basic knowledge but nothing else, and that’s why we think this feedback, tips, and tricks might interest you whether you are in the same situation or already an experienced Salesforce person.
Context
For one of our existing customers, the job was to implement a custom Admin interface to build their Offers, to select Audiences, and create some Content to be sent via Email or Text.
Nothing crazy but to do that we had one strong constraint set by the customer itself.
We needed to use Salesforce Marketing Cloud only, to build, run and host this application.
We were introduced to AMP Script, and this good documentation: https://ampscript.guide/ (Thanks Cyril Louis (best Salesforce MVP ever) for the tip.
Of course, Marketing Cloud is huge and therefore we split up the work with our SFMC experts from another Almavia CX unit. They did the Salesforce automation and we implemented the web application.
The brief description was simple:
- Using Landing Pages and AMPScript
- Implement all required forms to search, create, edit, update and remove data
- Retrieve and save data from/into Data Extensions
All the rest of the project (automation based on the Data Extensions) is hidden from the web application itself, and won’t be explained in this publication.
Before anything else, let’s define some concepts:
- Data Extensions: That’s pretty much a database table
- Data Filters: We can see that as a Stored Procedure able to reach more than Data Extensions data.
- Landing Pages: a set of Zones and Blocks made to be WYSIWYG but in which you can also code in HTML/CSS/JS and AMPScript
The application we had to build was quite standard:
- Authentication
- Creation of the Offers, Audiences and, Contents
- Synthesis and Validation pages
- Tracking of the Offer progression over time
So that was 6–7 fancy pages total, with a lot of fields, a lot of validations and pretty much each action needed to be saved into Data Extensions.
That’s how we started our first SF Marketing Cloud(SFMC) project.
Step 1 — Data Modeling
As with every project, one of the first important steps is to define the database.
With SFMC, to build your schema, there is an interface where you can create your table, fields, etc.
You can find it in Audience Builder > Contact Builder > Data Extensions.
For this application, we created around 10 data extensions for Users, Offers, Audiences, Content items, etc.
Note that there is no constraint, nor foreign keys, nor indexes. Also the datamodel is not even driven by the source code. It’s what I could consider a bit old-school.
Step 2 — Authentication
Most of the time that’s something we don’t think about in our projects, most of the applications we develop have an authentication system based on a session provided by the server.
Here, as the only things available to us are Data Extensions and Landing pages we had to rebuild the obvious in terms of authentication.
The mechanism is the following: On the login form submit, we check the login password against the data we have in the Data Extension. If that’s ok we create and save a unique token in the same Data Extension for that specific user (with an expiration) and also in a Cookie (with an expiration). After that, for each request, we check that the Cookie matched the token in the Data Extension. We lookup for the token in the Data Extension to retrieve the current user.
Step 3 — Developments — First Try
We started developing pages and for each page of the application, we created a Landing Page on SFMC.
That was terribly awful in terms of developer experience:
- No local development
- AMP Script is really like PHP 3.x
- You mix HTML and server-side code
- No MVC, no framework, no structure
- No real organization
- You have to code in a small text area on a web page in SFMC
- You cannot test locally
- And to test your code you need to: Save the form, Publish and Wait
And the top of the notch issue: you don’t have any debug/feedback when you have an error, just a dirty 500 error page.
That was pretty much unbearable to build an application in such conditions.
And with no framework, the code was so messy so quickly just to handle forms, validations, and navigation that we decided to improve the developer experience a little bit.
We are usually using Symfony Framework for our project. Here we did not need a full framework but a template system was a good idea to wrap the infamous AMPScript.
Twig integration
So we naturally used Twig as our template system to improve readability and reusability.
Twig Functions
That was the first simple nice addition, we could write our own function to generate AMPScript code.
So we develop some kind of framework on top of the AMPScript framework.
So instead of writing
<h1>%%=v(@myVar)=%%</h1>
We would write
<h1>{{ echo(‘myVar’) }}</h1>
This is a bit more readable but most of all this is a standard: Twig.
Global configuration of our app
Every application needs to have configurations:
- List for selects
- Variables
- Hostname
- Routes
- Etc.
With Twig we just injected that configuration and that was available to us in the templates.
Includes and inheritance
Probably the BEST use case, the template system also brought us the capability to `include` and therefore to reuse code.
That was a huge improvement.
Let’s take the authentication check for instance. In native AMPScript for each landing page, we had to copy and paste the same piece of code. With Twig authenticated pages were just inheriting a specific layout.
{% extends "layout/authenticated_page.amps.twig" %}{% block page %}… code of the page …{% endblock %}
The layout/authenticated_page.amps.twig code is:
{% extends layout %}{% block body %}
{% include "layout/header.html.twig" %}
%%[
set @sessionId = RequestParameter('sessionId');
set @rows = LookupRows("{{ DE.Users }}","sessionId",@sessionId)
if RowCount(@rows) != 1 then
]%%
<p>
SESSION INVALID PLEASE <a href="{{ routes.login }}">LOGIN</a>
</p>
<meta http-equiv="refresh" content="{{ redirect_timeout }}; url={{ routes.login }}">
%%[ else set @firstname = Field(Row(@rows, 1), 'firstname')
set @lastname = Field(Row(@rows, 1), 'lastname')
set @email = Field(Row(@rows, 1), 'email')]%%
<div class="container">
{% block page %}
<h1>AUTHENTICATED PAGE</h1>
<p>
Welcome {{ echo('@firstname') }} {{ echo('@lastname') }} {{ echo('@email') }}
</p>
{% endblock %}
</div>
%%[ endif ]%%
{% endblock %}
That was nice and efficient.
Then we needed to turn that code into final HTML.
HTML, CSS and JS Integration
AMPScript and Twig improved a lot the readability of the code, we wanted to feel the same for CSS and JS.
Same logic as before we wanted to base our development on standard tools and technologies. We decided to use Webpack and SASS.
The Webpack config is really simple and standard. Using MiniCssExtractPlugin to extract the CSS.
And we finish the automation with a small script that injects the CSS and JS created by Webpack in the final HTML
$page = $_SERVER['argv'][1];if (file_exists("build/{$page}.css")) { $config += [ 'layout' => 'layout/raw.html.twig', 'styles' => trim(file_get_contents("build/{$page}.css")), 'javascripts' => trim(file_get_contents("./build/{$page}.js")) ];}echo $twig->render("{$page}.amps.twig", $config);
So we could do:
./generate offerpage > page.amp
The build
Like all good applications, we needed a build step.
So we created a Makefile
build: ## Generate the AMP for all the page in build/@$(YARN) build-prod@$(PHP_BIN) bin/generate login > build/login.amp@$(PHP_BIN) bin/generate offer > build/offer.amp@$(PHP_BIN) bin/generate audience > build/audience.amp@$(PHP_BIN) bin/generate content > build/content.amp@$(PHP_BIN) bin/generate validation > build/validation.amp@$(PHP_BIN) bin/generate edit > build/edit.amp@$(PHP_BIN) bin/generate follow > build/follow.amp
Deploy
Ultimately using the make build
we would generate our pages but we still had to follow those steps per page to deploy:
- Copy the code in the
.amp
file - Open the corresponding Landing Page in SFMC
- Paste the code in that tiny textarea
- Save
- Publish & wait a bit
Conclusion
While this approach is perfectly valid for a small project, with 8 pages already, it was painful to update the code, test the overhaul, etc.
We were wasting too much time and when the functional rules started to change, without debugging tools or logs that was not possible to work in good conditions.
Step 3 (Bis) — Developments — Final try
At this step we knew AMPScript, most of the queries to retrieve/save into the Data Extensions were done.
The biggest problem was the interfaces, the interactions, the form validations, etc. the code was too messy and the impossibility to run code locally was a total waste of time.
And we also deep-dived into Google and Stackoverflow. Enough to discover and understand that Salesforce Server Side Javascript (as they named it in the doc) was probably a good idea to explore as a web developer, because well, javascript is part of our skillset.
So we completely switched our approach, we needed to find a way to:
- develop even more locally
- debug
- Improve developer experience
- be more efficient and ready to changes
And naturally, we came up with a decoupled approach as we actually do all the time!
We decided to reorganize our code to have:
- 1 unique Landing Page for an API
- 1 unique Landing page for the Application (Front End)
The API will be in full AMPScript and the Front End in React JS!
API in AMPScript
We wanted an API that would return JSON, first we tried to use a Code Resource instead of a Landing Page in SFMC but that did not work. Indeed the Code Resources are cached for a good 5 minutes making it impossible to test.
So we switched back to a Landing Page and started to use our existing AMPScript to do the first API endpoint to retrieve the offers.
JSON Conversion Issue:
That was an unexpected failure, there is no way to convert variables in JSON in AMPScript! And that makes sense, it has not been done for that.
Also, to retrieve a row you need to put all fields in a variable:
%%[var @rows, @row, @rowCountset @rows = LookupRows("Offers","enabled", 1)set @rowCount = rowcount(@rows)if @rowCount > 0 then for @i = 1 to @rowCount do var @emailAddress, @firstName set @row = row(@rows, @i) set @firstName = field(@row,"firstName") set @emailAddress = field(@row,"emailAddress")next @i ]%%
Then we should have built our JSON ourselves via CONCAT, managing the quotes, etc.
It was an easy no go.
This is when we looked more in detail at SSSJS
Salesforces Server Side JavaScript (SSSJS)
First, we checked that everything we were doing with AMPScript could be done with SSSJS. The answer is YES. (and if that’s not you can still put AMScript in the middle).
So the fetching rows and convert them to JSON went from 15 to 20 lines of code to one:
Platform.Response.Write(Stringify(Platform.Function.LookupRows("Offers", "enabled", 1)))
And we also found out that it was possible to have some error logs thanks to SSSJS
<script runat="server">Platform.Load("core", "1.1");try {// ...your code}catch (exception) {// something with the exception like: Stringify(exception)}</script>
Here, that was a relief and the confirmation we were on the correct path.
So we split the job, someone did the API and someone else did the front-end using ReactJS.
React JS APP
In General
Here, there is nothing specific to discuss, no specificity at all. Actually, it’s more interesting to look at all the benefits provided by a Single Page Application in the SFMC context.
Benefits:
- Complete autonomy to build the user interface
- Local development fully functional
- The whole world of plugins, components via NPM or Yarn.
Disadvantages:
- Developments have to be done locally, they are unreadable in SFMC as the code will be compiled.
React Router
Using “react-router-dom” we could build as many pages as we wanted using only one Landing Page (Single Page App). No more full reload of the Landing Page. The user experience was also really improved as you know React just reloads what’s necessary.
Local storage
A huge improvement as well here in terms of Developer eXperience but also User eXperience. As mentioned before there is no session mechanism server side. Therefore we have no state that could stay in the application unless that state is client-side.
And using the wonderful “@rehooks/local-storage” we could save the state of the application in the Local storage allowing us to:
- Create some sort of simple state to save context and improve the user experience when they come back, and browse the application.
Fake endpoint
The point is self-explanatory! As the approach is decoupled, the front-end developer was able to have fake endpoints to work waiting for the API to be finished. A small addition but a huge facilitator.
Conclusion
To sum up, at the end of the journey, here is the final stack/approach.
A Landing page that will receive the Front End Application:
- Built with React JS
- Fully standard, no Salesforce knowledge needed here
- Our build step extracts compiled and packed JS, CSS, and HTML that we copy and paste in the SFMC textareas.
A Landing Page that will receive the API SSSJS
- Using SSSJS and/or AMPscript
- Still using Twig as a template system to inject configuration and give more readability.
- Mockable: you can mock/redefine Platform.* functions to also develop locally and in JS
Feedback and going further
To me, that is the way to go. It’s efficient, decoupled, and workable.
That being said, that’s not optimal for many reasons:
- We are still required to copy/paste manually and publish code: We did not find a way to push the code there directly.
- There are no staging environments: You need to do it yourself, using a different set of Landing Pages, it seems odd.
- Data Extensions are not a database: Join must be done manually and that’s cumbersome
- Cookie HTTP Only flag is off “in the sandbox”: And it is on in the real organization. So be aware of this from the beginning.
- Datetimes are always converted to GMT-6 in SFMC: again something you need to know from the start, it will help you when working with many timezones.
I personally think that is a lot of workaround for such a simple project that could have been done with standard tools.
I wonder if we could have reached the Salesforce API to fetch data remotely and handle the application outside of the Salesforce Marketing Cloud while still using its concepts.
But then comes the security arguments, in the approach used and required by our client, everything stays on Salesforce, nothing goes outside nothing transit.
Also, there is no hosting outside of SFMC which might be worth the extra time for a small application.
During this journey, I always wondered: “how are others doing?” There has to be another way. We did not find it and would love to discuss it! Don’t hesitate to comment! This publication is more about sharing an experience rather than showing a unique truth!
Also if we need to do another application like this, we would definitely use the same final approach, we would be really efficient actually.
And because of that, and because we also love Open Source, we released a BootStrapper to accelerate the installation if you ever want to try that approach!
Here is the Bootstrapper: https://github.com/Novactive/SFMC-bootstrapper
How to use Almavia CX SalesForce Marketing Cloud Bootstrapper
We made it really simple for you to bootstrap a project following the approach of this publication.
First, you need the minimum tooling of course:
- PHP
- Composer
- Yarn
- Make
And then that’s really simple
composer create-project almaviacx/sfmc-bootstrapper mysfmcapp --no-interaction
The project will install itself in the `mysfmcapp` directory. You can run the webserver with make serve
and the interface can be reached at a localhost URL.
Then whenever, you are ready to deploy, you need:
make build
- copy/paste the content of build/app.amp file for the app Landing Page
- copy/paste the content of build/api.amp file for the API Landing Page
Enjoy!