Create A Searchable Content Directory With CloudPages
Use SSJS, VueJS and Code Resources in order to create a searchable index of HTML Content Blocks.
Whether you're struggling with a chaotic folder structure, or just generally having a hard time finding what you want, having a convenient way of searching your Content Builder assets can save quite a lot of time and frustration. Using the REST API and Code Resources, we can create a solution that allows us to efficiently view, or modify, any Content Builder asset within a searchable index.
Setup:
Before getting started, we'll need to setup a data extension that will serve as our asset table that we can filter and search on. Let's call this data extension Content Reference Directory Lookup
. Our new data extension should have the following columns for this example:
contentID
(Primary Key)name
content
catID
custKey
We'll also need to create an installed package so that we can access Content Builder assets programmatically with the REST API. If you are unsure of how to create and install a package in your SFMC account, please refer to the following documentation for more information:
Create JSON Code Resource:
Next, we'll create a JSON Code Resource that will serve our Content Directory CloudPage with asset data. To enable this functionality, we'll simply need to initialize our data extension from the previous step and output the JSON response. This ensures that the asset data we store in our asset table will be updated and served to our CloudPage when a user visists or refreshes the directory.
<script runat=server>
Platform.Load("Core", "1");
// User Filter For Limited Response Size Or Pagination
var data = DataExtension.Init("Content Reference Directory Lookup").Rows.Retrieve(filter);
Write(Stringify(data));
</script>
Now that we've got our data extension created, and JSON Code Resource configured to serve our asset data, we can move on to implementing our CloudPage solution.
Configuring The CloudPage Directory:
The CloudPage that will execute on the data setup from the previous step can be broken down into three distinct pieces:
- Script to retrieve asset data and update our content database.
- VueJS function to retrieve our JSON Code Resource and allow a user to search for assets by name whilst sorting the result set.
- HTML/CSS to output Content Builder asset information.
Using SSJS To Update Data Extension
In order to generate the list of assets, and its associated data, within our solution we'll need to retrieve them from the REST API. First, we'll make a call to retrieve our API access token and then we'll make a second call to retrieve our assets. For this example, we'll only retireve HTML Content Blocks (assetType 197), but the below solution can be extended to allow all asset types or only those defined by a user.
We'll then setup our script to parse the response from our call to retrieve the assets in order to extract the relevant data we need. After that, we'll upsert the data to our asset table so that our JSON Code Resource can access our updated content.
Note: When dealing with a large volume of Content Builder assets, it is important to paginate your API requests and asset retrieval in order to improve performance and decrease the likelihood of an error. This is not included in the example below, but can be added with modest changes. Also, it is advisable that you execute the following script in an activity that runs hourly, rather than on page load. This implementation is for demonstration only.
<script runat="server">
Platform.Load("Core", "1");
var url = 'https://YOUR-AUTH-ENDPOINT.com/v2/token';
var contentType = 'application/json';
var authPayload = {
"grant_type": "client_credentials",
"client_id": "your client id",
"client_secret": "your client secret",
"account_id": "your mid"
};
authPayload = Platform.Function.Stringify(request);
var result = HTTP.Post(url, contentType, authPayload);
var aRes = Platform.Function.ParseJSON(result.Response.toString());
var auth = aRes.access_token;
var assetURL = 'https://YOUR-REST-ENDPOINT.com/asset/v1/content/assets/query';
var contentType = 'application/json';
var contentPayload = {
"query": {
"property": "assetType.id",
"simpleOperator": "equals",
"valueType": "int",
"value": 197
},
"page": {
"pageSize": 1000
}
};
contentPayload = Platform.Function.Stringify(contentPayload);
var headerNames = ["Authorization"];
var headerValues = ["Bearer " + auth];
var req = HTTP.Post(assetURL, contentType, contentPayload, headerNames, headerValues);
var res = req.Response.toString();
var json = Platform.Function.ParseJSON(res);
var items = json.items;
for (var i = 0; i < items.length; ++i) {
var itr = items[i];
var contentName = itr["name"];
var content = itr["content"];
var contentID = itr["id"];
var custKey = itr["customerKey"];
var catID = itr["category"]["id"];
Platform.Function.UpsertData("Content Reference Directory Lookup", ["contentID"], [contentID], ["name", "content", "catID", "custKey"], [contentName, content, catID, custKey]);
}
</script>
VueJS Function For Retrieving And Searching Data
Now that we've updated our data extension with the most recent assets, and have our Code Resource in place to output it, we'll retrieve the data and bind it for filtering and display on our CloudPage.
First we'll create a new Vue instance and specify the DOM element that we want it to control (an element with an id of app
in this instance). Then, after the instance has been mounted, we'll make an AJAX request in order to retrieve the data from our Code Resource. We'll use computed properties in order to provide our functionality for filtering, and sorting, our assets by name as our user enters their search input.
<script>
new Vue({
el: "#app",
data: {
Data: [],
filterValue: "",
sortAsc: true,
},
mounted: function () {
var self = this;
// Get Code Resource To Retrieve Content Block Data
$.ajax({
url: "YOUR-JSON-CODE-RESOURCE-URL",
method: "GET",
success: function (data) {
self.Data = data;
},
error: function (error) {
console.log(error);
},
});
},
computed: {
filteredAndSortedData() {
// Apply Filter From User Input
let result = this.Data;
if (this.filterValue) {
result = result.filter((item) =>
item.name.toLowerCase().includes(this.filterValue.toLowerCase())
);
}
// Sort The Filtered Response
let ascDesc = this.sortAsc ? 1 : -1;
return result.sort((a, b) => ascDesc * a.name.localeCompare(b.name));
},
},
});
</script>
The last piece of this implementation will be to implement the markup and styles portion of the CloudPage that will show our filtered lists of assets and their properties.
Adding Markup and Styles
In order to iterate over our filtered asset JSON and display the relevant information to the user, we'll need to use the v-for
directive to map our array to elements in order to retrieve each instance of an asset. Notice that we are executing our functionality within the DOM element we defined in our Vue instance initialization and that we are accessing each element property within our v-for
directive using the alias n
. Using this approach, we're able to a content block's name, id, raw HTML and to generate a preview of its contents.
<div id="app">
<div class="container">
<div class="col-sm-12">
<p>
Start Typing The Content Name To Filter Blocks:
</p>
<input type="text" v-model="filterValue" placeholder="Content Block Name">
</div>
</div>
<div class="container">
<div v-for="n in filteredAndSortedData" class="col-sm-12">
<div class="col-sm-12">
<div class="col-xs-3 left" >
<div class="padding">
<h6 class="reference_title"> {{n.name}}</h6>
<p>ID: {{n.contentID}}</p>
<textarea readonly onfocus="this.select();" onmouseup="return false;">{{n.content}}</textarea>
</div>
</div>
<div class="col-xs-9 right">
<div class="content_render" v-html="n.content">{{n.content}}</div>
</div>
</div>
</div>
</div>
</div>
The only thing left to do is add in our styles and include the relevant external libraries related to our UI and VueJS functionality.
@media only screen and (max-width: 480px) {
.content_render {
width: 100%;
height: auto !important;
}
a.button {
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
text-decoration: none;
color: initial;
background: #154172;
color: white;
border-radius: 5px;
padding: 10px 20px;
}
body {
background: #fafafa;
padding-bottom: 20px;
}
h6 {
word-break: break-all;
}
header {
background: #3949d8;
color: rgb(255,255,255);
}
input {
min-width: 350px;
margin: 0 auto;
padding-left: 10px;
}
input {
width: 100%;
}
textarea {
height: 140px;
font-size: 12px;
}
}
.buttons {
background: #e3e3e3;
margin-top: 0;
padding: 20px 0 20px 0;
max-width: 956px;
}
.center {
margin: 0 auto;
text-align: center;
}
.col-sm-12 {
padding-top: 30px;
text-align: center;
}
.col-xs-9, .col-xs-3 {
padding-top: 20px;
display: inline-block;
height: 325px;
vertical-align: middle;
}
.content_render {
width: 100%;
}
.left {
background: #e3e3e3;
color: #222;
width: 250px;
}
.logo {
margin: 0 auto;
display: block;
padding: 20px 0 20px 0;
}
.padding {
padding: 15px;
}
.right {
padding-left: 50px;
border: 1px solid #e3e3e3;
width: 700px;
overflow: auto;
}
.right, .left {
display: block !important;
width: 100%;
}
.row {
margin-top: 20px;
margin-bottom: 20px;
}
.text_center {
padding-top: 10px;
}
Conclusion:
Now we are able to provide users with an easily extendable, and searchable, solution for accessing their Content Builder assets. This solution could be modified to provide functionality for performing CRUD operations on assets or be integrated to work with external content management systems to provide a more holistic view of the asset model.
In addition to this blog post, you can find the code for this example in this github repository.