The 5.7 branch of Magnolia reached End-of-Life on December 31, 2023, as specified in our End-of-life policy. This means the 5.7 branch is no longer maintained or supported. Please upgrade to the latest Magnolia release. By upgrading, you will get the latest release of Magnolia featuring significant improvements to the author and developer experience. For a successful upgrade, please consult our Magnolia 6.2 documentation. If you need help, please contact info@magnolia-cms.com.
This tutorial explains how to access Magnolia content from a client-side application. We use Magnolia's built-in REST API and a single-page AngularJS application.
We use the classic cars from the My first content app tutorial as sample content. The cars are typical structured content: each car has the same content type and same fields. You can publish it to a website or consume it from a client-side application like we do in this tutorial. With the Magnolia Delivery endpoint API you can access any Magnolia workspace.
If you want to know how to fetch content app data on the server side, for instance from a FreeMarker template script, see Accessing content on the server side.
Overview
Your tasks:
- Install a Magnolia bundle with a custom content app.
- Configure the delivery endpoint in a light module.
- Test REST access and think about REST security.
- Write an Angular app which consumes JSON from the configured REST endpoint.
Architecture overview:
Install a Magnolia bundle containing the app-tutorial content app
For this tutorial you need a Magnolia bundle with:
- Magnolia REST modules version 2.1 or higher: install Magnolia version 5.6.4 or higher to ensure it contains Magnolia REST modules version 2.1+.
You may install a Community Edition (CE) or Enterprise Edition (EE) Magnolia bundle, with or without the demo for the purpose of this tutorial. We have used the
magnolia-community-demo-webapp. - The latest version of the
app-tutorialcontent app : :
To install the bundle together with the app, we recommend using Magnolia CLI jumpstart.
Once installed, the directory where you exectued the jumpstart command looks similar to this:
erics-cars-tutorial/ ├── apache-tomcat └── light-modules
Remember the light-modules folder. You will add files there later on.
Now start the bundle:
cd erics-cars-tutorial/ mgnl start
Give Magnolia some time to finish the installation during the first start.
Then open a browser, open the URL http://localhost:8080/magnoliaAuthor and login with user superuser / password superuser.
About the custom content app
Click the Products tile open the app.
This content app has two subapps:
- The Browser subapp
- The Detail subapp
Content in the app-tutorial
You need the following information to properly configure REST access to the content:
- The items of this app are cars. They are stored in the JCR workspace
products. - The node type of a car is
mgnl:product. - The items are organized in folders whose content type is
mgnl:folder. - The images of the cars are assets stored in the
damworkspace. Car nodes store a reference to a dam asset.
If you aim to fetch JCR content as JSON, analyze the JCR content using the JCR Browser app to understand all its details.
This is the products workspace displayed in the Tools>JCR browser app:
Look at the image property. It stores an asset ID, a reference to an asset stored on the dam workspace.
Configure a delivery endpoint in a light module
Since Magnolia REST 2.1 you can define multiple delivery endpoints. You must configure at least one delivery endpoint to consume content as JSON from the delivery API. You can configure endpoints using both YAML and JCR.
To get the content from the app-tutorial as JSON, we will configure a delivery endpoint in a light module. In this example, we name the light module content-app-clients-v2.
Within your light-modules folder, create this structure:
content-app-clients-v2/
└── restEndpoints/
└── delivery/
└── carFinder.yaml
Note:
- Endpoint definitions must reside within the folder
restEndpoints. You can also use subfolders; here we created a subfolder calleddelivery. - The location of the endpoint definition within the folder restEndpoints partially defines the URL to access the configured endpoint.
Read Delivery endpoint API v2 - endpointPath property to understand theendpointPathand REST access URLs. - The above configuration leads to the following REST access URL:
<magnolia-base-path>/.rest/delivery/carFinder
The definition file should look like this:
class: info.magnolia.rest.delivery.jcr.v2.JcrDeliveryEndpointDefinition
workspace: products
rootPath: /
nodeTypes:
- mgnl:folder
- mgnl:product
childNodeTypes:
- mgnl:folder
- mgnl:product
depth: 2
includeSystemProperties: false
bypassWorkspaceAcls: true
references:
- name: carAssets
propertyName: image
referenceResolver:
class: info.magnolia.rest.reference.dam.AssetReferenceResolverDefinition
includeAssetMetadata: false
- To understand all details of the configuration, read Delivery endpoint API v2 - Configuration.
Line 12: Do not use the property
bypassWorkspaceAclsin a production environment. See REST security below.- Lines 13-19: Define the reference resolver to resolve the reference to the asset.
Test REST access and understand REST security
Before developping the Angular app, it is good practice to check if the API returns the JSON you expect and will be required by the Angular app. Check this using a browser.
REST URLs
We want to test the requests:
- Access JSON for one car to display one car in detail.
- Access JSON for all the cars and the folders they are wrapped with to display a list of cars to select from.
The requests should look like this:
<magnolia-base-path>/.rest/delivery/carFinder/cars/007/Aston-Martin-DB5(for one car)<magnolia-base-path>/.rest/delivery/carFinder/cars(for all cars)
delivery/carFinder is the endpointPath defined by the endpoint configuration.
On magnoliaAuthor
You must be logged in to test the requests on the magnoliaAuthor webapp. Open a browser, request just http://localhost:8080/magnoliaAuthor/ and log in with superuser as the user name and password.
Now test the REST requests:
- http://localhost:8080/magnoliaAuthor/.rest/delivery/carFinder/cars/007/Aston-Martin-DB5
- http://localhost:8080/magnoliaAuthor/.rest/delivery/carFinder/cars
On magnoliaPublic
In a productive environment you request JSON from the public instance. In this tutorial we request the JSON from the magnoliaPublic webapp. Do not log in so that you test as the anonymous user.
- http://localhost:8080/magnoliaPublic/.rest/delivery/carFinder/cars/007/Aston-Martin-DB5
- http://localhost:8080/magnoliaPublic/.rest/delivery/carFinder/cars
JSON responses
This is what the JSON responses look like:
REST security
REST APIs are a powerful feature but they may also turn into a security risk. This is why it is important you understand how Magnolia handles security for REST.
We strongly recommend you read and understand the page REST security.
For this tutorial, note the following points.
There are two levels of control when REST requests are issued on the delivery API:
- Web access (checking URLs)
- JCR access (checking access to JCR workspaces)
Both web and JCR access must be granted. In Magnolia it is granted using Roles with access control lists that are assigned to users and groups.
When you tested the requests above, you used:
superuseronmagnoliaAuthoranonymoususer onmagnoliaPublic
These users have very different security settings. While superuser typically has a lot of permissions, anonymous has only limited access for both web access and JCR security. Requests made using the anonymous user on magnoliaPublic only worked because:
- Web access is granted by the role
rest-anonymousthat comes by default with the REST modules and is assigned to anonymous user. - JCR access was bypassed with the delivery endpoint configuration by setting the property
bypassWorkspaceAcls.
In a production environment:
- Do not use the property
bypassWorkspaceAclsin endpoint definitions. - Create custom REST roles granting specific access for specific use cases.
Create an AngularJS app
AngularJS is a Web application framework popular among front-end developers. In this tutorial, we use an AngularJS app to access and render content from the Products app. We assume you know enough JavaScript to follow along.
The content items we render are cars. They are organized in the app like this:
cars/
├── 007/
│ ├── Aston-Martin-DB5
│ ├── Aston-Martin-V8
│ ├── Lotus-Esprit-S1
│ └── Sunbeam-Alpine
└── classics/
├── 1927-Hudson
├── Continental-Mark-II
├── Fiat-Cinquecento
├── Pontiac-Chieftain-1952
└── Riley-Brooklands-1930
Where:
cars,007andclassicsare folders.- The cars are of type
mgnl:product.
We will create an Angular app that provides:
- A list of cars to choose from.
- Details of a selected car with a description and image.
- Checkboxes to filter the list into
classicsor007cars. We use the parent folders as pseudo-categories.
App structure
We will create an
ngApp
with two controllers:
carsListrenders a list of all cars. Each car has a clickable label. Clicking on the label shows the car in thecarDetailcontroller. The list controller also has two checkboxes to filter the list; one for each parent folder.carDetailrenders the details of the selected car: title, description and image.
App files
Now create a folder called angular with the files app.js, erics-cars.html and style.css in the light module, to create this structure:
content-app-clients-v2/
├── angular
│ ├── app.js
│ ├── erics-cars.html
│ └── style.css
└── restEndpoints
└── delivery
└── carFinder.yaml
To avoid running into trouble with the same-origin policy, we run the Angular app on the same server as Magnolia. See how to overcome same-origin policy problem.
In a real use case you would serve the Angular app not from Magnolia, but rather from a static webserver like Apache and most probably on a different (sub)domain.
CarsContentClient app
Start creating the Angular app in erics-cars.html. Add an ng-app directive to the body element which wraps the two controllers:
<!doctype html>
<html>
<head>
<title>Angular loves Magnolia :-)</title>
<link rel="stylesheet" href="style.css?u=12">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0-rc.0/angular.min.js"></script>
<script src="app.js?v=w"></script>
</head>
<body ng-app="CarsContentClient">
<h1>Eric's cars - shown with a lightweight angular client</h1>
</body>
</html>
Note:
- Lines 5-7: References to JS and CSS files. We use dummy request parameters such as
?u=12to bypass cache. We recommend that you use such dummy request parameters while developing. Alternatively, you could exclude content from the cache on the Magnolia instance but your browser may also cache the resources, particularly JS and CSS files. Once you're done with development, remove the request parameters. - Line 10:
ng-appdirective in thebodyelement.
Request the file in the browser to make sure that Magnolia serves a static file. Use a dummy parameter ?foo=bar also in the request URL. It ensures we bypass cache on both server and client side.
http://localhost:8080/magnoliaPublic/.resources/content-app-clients-v2/angular/erics-cars.html?foo=bar
Next, configure the Angular app in app.js:
var app = angular.module('CarsContentClient', []);
/**
* Configuration of some constants
*/
app.constant("APP_CONFIG", {
webAppBaseUrl: "http://localhost:8080/magnoliaPublic",
restBaseUrl: "/.rest",
requestHeaders: {headers: {'Accept': 'application/json;odata=verbose'}},
deliveryEndpointCarfinder: "/delivery/carFinder",
workspace: "products",
defaultCarPath: "cars/007/Aston-Martin-DB5"
});
CSS style sheet
To style Eric's car page, add some CSS code to style.css.
carDetail controller and AngularJS utilities
To render a car's details we want a
$scope
object of the carDetail controller with the following properties:
carTitlecarDescriptionimgUrl
Add the following HTML snippet to erics-car.html:
<div ng-controller="carDetail" class="car-detail">
<table width="100%">
<tr valign="top">
<td width="60%" class="image">
<img ng-src="{{imgUrl}}" width="640" class="fancy">
</td>
<td width="40%"> <h2>{{carTitle}}</h2><p ng-bind-html="carDescription | sanitize" class="description"></p></td>
</tr>
</table>
</div>
<!-- eof 'carDetail' ctrl. -->
When you reload the file in the browser you may notice JavaScript errors. To see the errors, open the JavaScript console of your browser. The name of this console depends on the browser you are using. For instance, on Google Chrome it is named DevTools.
globalData component
Add a component named globalData. Both controllers use it. It has a property productPath that is set by one controller and read by the other controller.
app.service('globalData', ['APP_CONFIG', function (APP_CONFIG) {
var productPath = APP_CONFIG.defaultCarPath;
var setProductPath = function (newPath) {
productPath = newPath;
};
var getProductPath = function () {
return productPath;
};
return {
setProductPath: setProductPath,
getProductPath: getProductPath
};
}]);
utils component
Add a component named utils. It provides some useful methods that both controllers can use. We use the function replaceAll in the carsList controller.
app.service('utils', function () {
var escapeRegExp = function (str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
var replaceAll = function (str, find, replace) {
return str.replace(new RegExp(this.escapeRegExp(find), 'g'), replace);
};
return {
replaceAll: replaceAll,
escapeRegExp: escapeRegExp
}
});
sanitize filter
The third helper to add is the sanitize filter. Remember that the description property of the content item contains encoded HTML. By default, Angular refuses to render JSON content that contains (HTML) markup. The sanitize filter makes sure that the (encoded) HTML is rendered properly as HTML.
app.filter("sanitize", ['$sce', function ($sce) {
return function (htmlCode) {
return $sce.trustAsHtml(htmlCode);
}
}]);
$sce
.carDetail controller
Finally, add the carDetail controller:
app.controller('carDetail', ['$scope', '$http', 'globalData', 'utils', 'APP_CONFIG', function ($scope, $http, globalData, utils, APP_CONFIG) {
$scope.$watch(function () {
return globalData.getProductPath();
},
function (productPath) {
var detailNodeRequestPath = APP_CONFIG.webAppBaseUrl + APP_CONFIG.restBaseUrl + APP_CONFIG.deliveryEndpointCarfinder + "/" + productPath;
$http.get(detailNodeRequestPath, APP_CONFIG.requestHeaders)
.then(function (response) {
$scope.carTitle = response.data.title;
$scope.carDescription = response.data.description;
$scope.imgUrl = response.data.image["@link"];
});
}, true);
}]);
- Line 1: Custom components
APP_CONFIG,globalDataandutilsare injected. - Lines 6, 7: Computes a request URL for the REST call using configuration data from the
globalDatacomponent. - Line 7: The controller executes an
HTTP#getmethod on thedeliveryREST endpoint on the prefixcarFinder. The content item path is the selected item. It requests data for one content item as we did in testing REST access. - Line 2 and onward: The
HTTP#getcall is defined as a callback function of$watchwhich is a value change listener forglobalData.productPath. The REST call is executed initially and whenglobalData.productPathchanges.
Reload the file in your browser. Now you can see the detail of the Aston Martin DB5 in your browser. Cool, isn't it!?
carsList controller
The carsList controller renders a list of clickable span elements that contain car titles. The list can be filtered with checkboxes using the parent folders classics and 007 as pseudo-categories.
Add this HTML in erics-cars.html:
<div ng-controller="carsList">
<!-- car folders filters -->
<form class="filters">
<div ng-repeat="cat in carCategories" class="cat">
<label>{{cat.name}} cars</label><input type="checkbox" name="category" ng-click="clickCategory(cat.name)" ng-model="cat.checked" />
<label> </label>
</div>
</form>
<div class="clear"></div>
<!-- car titles to click to change the detail -->
<div class="cars">
<div data-ng-repeat="car in cars | filter:isInCategory" class="car">
<span ng-click="selectCar(car.path)" class="click">{{car.title}}</span>
</div>
</div>
</div><!-- eof 'carsList' ctrl. -->
cars: An array of car items.
Each car item has these properties:pathcategory: actually a pseudo-category, the name of the parent folder such asclassicsor007.title: Title of the carname: Node name of the item.
carCategories: An associative array (map) for the pseudo-categories.
Each item has one property:checked: Whether the pseudo-category is currently selected.
Here is the JavaScript code:
app.controller('carsList', ['$scope', '$http', 'globalData', 'utils', 'APP_CONFIG', function ($scope, $http, globalData, utils, APP_CONFIG) {
$scope.carCategories = {};
$scope.selectCar = function (arg) {
// we need the path relative to the "rootPath" as defined in the endpointPrefix
if (arg.length > 1 && arg.startsWith("")) {
globalData.setProductPath(arg.substr(1));
}
};
$scope.clickCategory = function (catName) {
$scope.carCategories[catName].selected = !($scope.carCategories[catName].checked);
};
// a per scope filter
$scope.isInCategory = function (car) {
return $scope.carCategories[car.category].checked;
};
var carsNodesRequestUrl = APP_CONFIG.webAppBaseUrl + APP_CONFIG.restBaseUrl + APP_CONFIG.deliveryEndpointCarfinder + "/" + "cars";
$http.get(carsNodesRequestUrl, APP_CONFIG.requestHeaders)
.then(function successCallback(response) {
$scope.cars = [];
for (var item in response.data) {
// grab the top level folders with the car categories ("classic-cars", "007-cars")
if ("mgnl:folder" == response.data[item]["@nodeType"]) {
var folderName = response.data[item]["@name"];
var folderNode = response.data[item];
$scope.carCategories[folderName] = {checked: true, name: folderName};
// grab some params for each car
for (var subItem in folderNode) {
if ("mgnl:product" == folderNode[subItem]["@nodeType"]) {
var carNode = folderNode[subItem];
var carPath = folderNode[subItem]["@path"]
// replacing space (" ") with non-breaking to produce button-like labels
var carTitle = utils.replaceAll(carNode.title, " ", "\u00A0");
$scope.cars.push({path: carPath, category: folderName, title: carTitle, name: carNode["@name"]});
}
}
}
}
});
}]);
- Line 1: Again custom components
APP_CONFIG,globalDataandutilsare injected. - Lines 5-10:
selectCaris executed when you click a car in the list. The function setsglobalData.productPathso it indirectly triggers the execution of thecarDetailcontroller.
We remove the slash at the very begining, since we want a path relative to the rootPath as defined in the configuration for thecarFinderprefix of the delivery endpoint. - Lines 12-14:
clickCategorymaintains the $scope variablecarCategories. Since this changes the state of a controller $scope variable, the UI gets repainted - the list items get updated. - Line 21, 22: The controller executes an
HTTP#getmethod. Here the URI for the JSON call requests the cars root folder and contains the argumentdepth.
Final result:
The angular app is now fully functional. Congratulations!
Get the completed files
The files in their final form are available in the Magnolia Git repository.
Clone the repository to get the complete file structure. Use a terminal, go to your light modules folder and clone:
git clone https://git.magnolia-cms.com/scm/documentation/content-app-clients-v2.git
What next?
Where to add the AngularJS app
You can use or host the AngularJS app anywhere:
- As a static web resource served by Magnolia as seen in this tutorial.
- As part of a page or component template, in a template script.
- As a static or dynamic resource in any other server.
Same-origin policy
If you want to run the AngularJS app on a different host – a host which has a different address than the Magnolia server which provides the REST API – you run into the same-origin policy problem. To overcome this problem, some use specific Apache settings, see StackOverflow.
An elegant solution to solve the same-origin policy without changing Apache configuration is the Magnolia Add HTTP Headers filter.
Photo credits:
- Riley Brooklands 1930 and Fiat Cinquecento, Pedro Ribeiro Simões. Creative Commons Attribution 2.0 Generic (CC BY 2.0)
- Pontiac 1952, Continental Mark II and Hudson 1927, John Lloyd. Creative Commons Attribution 2.0 Generic (CC BY 2.0)
Aston Martin DB5, Aston Martin Works
Aston Martin V8, Beltane43, Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)
- Lotus Esprit S1, Don Griffin







