
This page 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. Structured content is easy to enter, query, analyze and publish to multiple channels. You can publish it to a website or consume it from a client-side application like we do in this tutorial. With the REST 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 the Magnolia REST module
- Think about the REST security
- Test REST access
- Write an Angular app
Architecture overview:

Install Magnolia REST
Make sure that your Magnolia installation contains the REST module. You should have at least the following submodules:
magnolia-rest-integration
magnolia-rest-services
If you use a preconfigured Magnolia bundle or webapp you already have the required modules. If you use a custom bundle check your project dependencies and add Magnolia REST module if not already there.
If you want to use Swagger to test the REST endpoints, also install the magnolia-rest-tools
module. It is not required to run the Angular app but it can be helpful during development.
When using magnolia-rest-tools
, set the apiBasepath
. The default value is most likely not correct for your Magnolia instance.
REST endpoints
The Magnolia REST module comes with preconfigured REST endpoints to read data from content apps. In this tutorial, we access content stored in the JCR so we use the following endpoints:
These two endpoints allow you to create, update and delete nodes and properties in any JCR workspace of your Magnolia instance.
Tip: You can also implement custom REST endpoints. You don't need them in this tutorial but custom endpoints are useful for:
- Getting a very specific data structure which is different from what you get when using the preconfigured endpoints.
- Exposing data from a custom content app which does not store the data in the JCR.
- Getting data from more than one workspace within one request.
REST security
The preconfigured REST endpoints provide not only methods to read but create, edit and update data. Configure a specific role which meets your requirements. Add the role to the user group which should have access to the required REST methods. For instance, you could configure a role which can only read the nodes and properties of a specific workspace, then add this role to the anonymous
user. During development it will help to add this new role to superuser
too.
Install sample content
In this tutorial we use the Products app and the classic cars as sample content. The cars are stored in the products
JCR workspace.

Maven is the easiest way to install the module. Add the following dependency to your
bundle:
<dependency>
<groupId>info.magnolia.documentation</groupId>
<artifactId>magnolia-app-tutorial</artifactId>
<version>1.4.8</version>
</dependency>
If you don't have a Magnolia instance yet, follow Installing Magnolia and a content app, then come back here. You will end up with exactly what we want to have for this tutorial as well.
Grant permissions to access content via REST
Grant permission to read content from the workspace products
to a new role.
- Open the Security app.
- Add a role
read-products
- In Access control lists, grant the new role:
- Read-only access to
/
and its subnodes in the Products
workspace - Read-only access to
/read-products
in the Userroles
workspace.

- In Web access, grant
Get
access to the path /.rest/nodes/v1/products*
. Deny all others.
- On the author instance, add the
read-products
role to the superuser
system account. - On the author instance, publish the
read-products
role. - On the public instance, add the
read-products
role to the anonymous
system account. - Log out and log back in to apply the new permissions to the currently logged-in user. This gets you a new session.
Test the app
Open the Products app installed by the app-tutorial
module and get familiar with it.

Since the items in the app are cars let's talk about cars from now on. Try the tree, list and thumbnail views. Add a car of your own.

Test REST access
Request the app content with a REST call. Try the following request in your browser:
http://localhost:8080/magnoliaAuthor/.rest/nodes/v1/products/cars/007/Aston-Martin-DB5
Magnolia responds with:
Click here to expand to show the response from the REST endpoint
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<node>
<identifier>ce39509e-e1d5-4cdd-b254-125948e2aeec</identifier>
<name>Aston-Martin-DB5</name>
<path>/cars/007/Aston-Martin-DB5</path>
<properties>
<property>
<multiple>false</multiple>
<name>description</name>
<type>String</type>
<values>
<value><p><strong>Movie:</strong>&nbsp;Goldfinger, GoldenEye, Casino Royale and Skyfall<br
/>
<strong>Year:</strong>&nbsp;1964, 1995, 2006 and 2012</p>
<p>The Aston Martin DB5 is one of the most famous cars in the world thanks to Oscar-winning special
effects expert John Stears, who created the deadly silver-birch DB5 for use by James Bond in Goldfinger
(1964). Although Ian Fleming had placed Bond in a DB Mark III in the novel, the DB5 was the company&#39;s
latest model when the film was being made.</p>
<p>A different Aston Martin DB5 (registration BMT 214A) was used in the 1995 Bond film, GoldenEye, in
which three different DB5s were used for filming. The BMT 214A also returned in Tomorrow Never Dies (1997) and
was set to make a cameo appearance in the Scotland-set scenes in The World Is Not Enough (1999), but these
were cut in the final edit. Yet another DB5 appeared in Casino Royale (2006), this one with Bahamian number
plates and left-hand drive (where the previous British versions had been right-hand drive).</p>
<p>Source: <a href="https://en.wikipedia.org/wiki/Aston_Martin_DB5">Wikipedia</a></p>
</value>
</values>
</property>
<property>
<multiple>false</multiple>
<name>title</name>
<type>String</type>
<values>
<value>Aston Martin DB5</value>
</values>
</property>
<property>
<multiple>false</multiple>
<name>image</name>
<type>String</type>
<values>
<value>jcr:4cd02639-167d-405a-923f-607aa20d0bc0</value>
</values>
</property>
</properties>
<type>mgnl:product</type>
</node>
We got an XML response but actually we want JSON. The nodes
endpoint requires an HTTP request header to return JSON. Request the content with
cURL
so you can pass the desired return type.
Open a terminal and type the following command:
curl -H "Accept: application/json" http://localhost:8080/magnoliaAuthor/.rest/nodes/v1/products/cars/007/Aston-Martin-DB5 -u superuser:superuser
Now we get JSON:
{
"name": "Aston-Martin-DB5",
"type": "mgnl:product",
"path": "/cars/007/Aston-Martin-DB5",
"identifier": "ce39509e-e1d5-4cdd-b254-125948e2aeec",
"properties": [{
"name": "description",
"type": "String",
"multiple": false,
"values": ["<p><strong>Movie:</strong> Goldfinger, GoldenEye, Casino Royale and Skyfall<br />\n<strong>Year:</strong> 1964, 1995, 2006 and 2012</p>\n\n<p>The Aston Martin DB5 is one of the most famous cars in the world thanks to Oscar-winning special effects expert John Stears, who created the deadly silver-birch DB5 for use by James Bond in Goldfinger (1964). Although Ian Fleming had placed Bond in a DB Mark III in the novel, the DB5 was the company's latest model when the film was being made.</p>\n\n<p>A different Aston Martin DB5 (registration BMT 214A) was used in the 1995 Bond film, GoldenEye, in which three different DB5s were used for filming. The BMT 214A also returned in Tomorrow Never Dies (1997) and was set to make a cameo appearance in the Scotland-set scenes in The World Is Not Enough (1999), but these were cut in the final edit. Yet another DB5 appeared in Casino Royale (2006), this one with Bahamian number plates and left-hand drive (where the previous British versions had been right-hand drive).</p>\n\n<p>Source: <a href=\"https://en.wikipedia.org/wiki/Aston_Martin_DB5\">Wikipedia</a></p>\n"]
}, {"name": "title", "type": "String", "multiple": false, "values": ["Aston Martin DB5"]}, {
"name": "image",
"type": "String",
"multiple": false,
"values": ["jcr:4cd02639-167d-405a-923f-607aa20d0bc0"]
}]
}
If you installed the magnolia-rest-tools
module, request the same with Swagger:
- Open the REST Tools app.
rest-tools-launch-tile - Click GET in the
nodes
API endpoint.
rest-tools-swagger-nodes-api-overview - Set workspace to
products
and path to a car such as /cars/007/Aston-Martin-DB5
and click Try it out. You get JSON data for one content item.
rest-tools-swagger-nodes-params rest-tools-swagger-response
Play around a little bit to familiarize yourself with the Swagger tool. Figure out the correct parameters to get a JSON representation of all cars.
Create an AngularJS app
AngularJS is a popular Web application framework 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
, 007
and classics
are folders- the cars are of type
mgnl:product
Let's an Angular app that provides:
- List of cars to choose from
- Details of a selected car with description and image
- Checkboxes to filter the list into
classics
or 007
cars. We use the parent folders as pseudo-categories.
App structure
We will create an ngApp
with two controllers:

App files
Create the following file structure:
$lightmodules
└── content-app-clients/
└── angular/
├── app.js
├── erics-cars.html
└── style.css
$lightmodules
is your light module directory. It can be anywhere on your file system but it must be a real directory such as:
- Mac OS X
/Users/johndoe/dev/lightmodules
- Windows
C:\Users\johndoe\dev\lightmodules
You may want to set the magnolia.resources.dir
property to reference your light modules folder:
magnolia.resources.dir=/Users/johndoe/dev/lightmodules
CarsContentClient
app
Start creating the Angular app in erics-cars.html
. Add an ng-app
directive to the body
element which will wrap 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=12
to 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-app
directive in the body
element.
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/magnoliaAuthor/.resources/content-app-clients/angular/erics-cars.html?foo=bar
<meta name="application-name" content="Bitbucket"><link rel="shortcut icon" type="image/x-icon" href="/s/756145691/61dba0f/4efxux/1.0/_/download/resources/com.atlassian.bitbucket.server.bitbucket-webpack-INTERNAL:favicon/favicon.ico" /><link rel="search" href="https://git.magnolia-cms.com/plugins/servlet/opensearch-descriptor" type="application/opensearchdescription+xml" title="Bitbucket code search"/></head><body class="aui-page-focused aui-page-focused-small aui-page-size-small bitbucket-theme user-login"><ul id="assistive-skip-links" class="assistive"><li><a href="#content">Skip to content</a></li></ul><div id="page"><!-- start #header --><header id="header" role="banner"><section class="notifications"></section><nav class="aui-header aui-dropdown2-trigger-group" aria-label="site"><div class="aui-header-inner"><div class="aui-header-before"><button class=" aui-dropdown2-trigger app-switcher-trigger aui-dropdown2-trigger-arrowless" aria-controls="app-switcher" aria-haspopup="true" role="button" data-aui-trigger href="#app-switcher"><span class="aui-icon aui-icon-small aui-iconfont-appswitcher">Linked Applications</span></button><div id="app-switcher" class="aui-dropdown2 aui-style-default" role="menu" hidden data-is-user-admin="false" data-is-switcher="true"><div class="app-switcher-loading">Loading…</div></div></div><div class="aui-header-primary"><span id="logo" class="aui-header-logo bitbucket-header-logo"><a href="https://git.magnolia-cms.com"><img src="/s/756145691/61dba0f/4efxux/1.0/_/download/resources/com.atlassian.bitbucket.server.bitbucket-webpack-INTERNAL:bitbucket-logo/images/logo/bitbucket.svg" alt="Bitbucket"/></a></span><ul class="aui-nav"></ul></div><div class="aui-header-secondary"><ul class="aui-nav"><li class=" help-link"title="Help"><a class=" aui-dropdown2-trigger aui-dropdown2-trigger-arrowless" aria-controls="com.atlassian.bitbucket.server.bitbucket-server-web-fragments-help-menu" aria-haspopup="true" role="button" tabindex="0" data-aui-trigger><span class="aui-icon aui-icon-small aui-icon-small aui-iconfont-question-circle">Help</span></a><div id="com.atlassian.bitbucket.server.bitbucket-server-web-fragments-help-menu" class="aui-dropdown2 aui-style-default" role="menu" hidden data-aui-dom-container="body"><div class="aui-dropdown2-section help-items-section"><ul class="aui-list-truncate" role="presentation"><li role="presentation"><a href="https://docs.atlassian.com/bitbucketserver/docs-088/Bitbucket+Data+Center+and+Server+documentation?utm_campaign=in-app-help&amp;utm_medium=in-app-help&amp;utm_source=stash" title="Go to the online documentation for Bitbucket" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:general-help">Online help</a></li><li role="presentation"><a href="https://www.atlassian.com/git?utm_campaign=learn-git&utm_medium=in-app-help&utm_source=stash" title="Learn about Git commands & workflows" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:learn-git">Learn Git</a></li><li role="presentation"><a href="/getting-started"class="getting-started-page-link" title="Overview of Bitbucket features" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:getting-started-page-help-link">Welcome to Bitbucket</a></li><li role="presentation"><a href="/#"class="keyboard-shortcut-link" title="Discover keyboard shortcuts in Bitbucket" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:keyboard-shortcuts-help-link">Keyboard shortcuts</a></li><li role="presentation"><a href="https://go.atlassian.com/bitbucket-server-whats-new?utm_campaign=in-app-help&utm_medium=in-app-help&utm_source=stash" title="Learn about what's new in Bitbucket" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:whats-new-link">What's new</a></li><li role="presentation"><a href="https://go.atlassian.com/bitbucket-server-community?utm_campaign=in-app-help&utm_medium=in-app-help&utm_source=stash" title="Explore the Atlassian community" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:community-link">Community</a></li><li role="presentation"><a href="/about" title="About Bitbucket" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:about">About</a></li></ul></div></div></li><li class=" alerts-menu"title="View system alerts"><a href="#alerts" id="alerts-trigger"class="alerts-menu" title="View system alerts" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:global-alerts-menu-item">Alerts</a></li></ul></div></div> <!-- End .aui-header-inner --></nav> <!-- End .aui-header --></header><!-- End #header --><!-- Start #content --><section id="content" role="main" tabindex="-1" data-timezone="-120" ><div class="aui-page-panel content-body" ><div class="aui-page-panel-inner"><main role="main" id="main" class="aui-page-panel-content" ><h2>Log in</h2><form class="aui top-label prevent-double-submit " action="/j_atl_security_check" method="post" accept-charset="UTF-8"><div class="field-group"><label for="j_username" >Username</label><input class="text long-field" type="text" id="j_username" name="j_username" autofocus accesskey="u"/></div><div class="field-group"><label for="j_password" >Password</label><input class="text long-field" type="password" id="j_password" name="j_password" accesskey="p"/></div><div class="aui-group"><fieldset class="group checkbox"><div class="checkbox"><input class="checkbox" type="checkbox" id="_atl_remember_me" name="_atl_remember_me" checked="checked" accesskey="r"/><label for="_atl_remember_me" >Keep me logged in</label></div></fieldset></div><div class="aui-group"><input name="next" type="hidden" value="/projects/DOCUMENTATION/repos/content-app-clients/browse/angular/erics-cars.html?at=master&raw"/><input name="queryString" type="hidden" value="next=%2Fprojects%2FDOCUMENTATION%2Frepos%2Fcontent-app-clients%2Fbrowse%2Fangular%2Ferics-cars.html%3Fat%3Dmaster%26raw"/><input class="aui-button aui-button-primary" type="submit" id="submit" name="submit" value="Log in" accesskey="s"/><a id="forgot" class="aui-button aui-button-link" name="forgot" href="/passwordreset" autocomplete="off" tabindex="0">Unable to access your account?</a></div></form></main></div></div></section><!-- End #content --><!-- Start #footer --><footer id="footer" role="contentinfo"><section class="notifications"></section><section class="footer-body"><ul><li data-key="footer.license.message">Git repository management for enterprise teams powered by <a href="https://www.atlassian.com/software/bitbucket/">Atlassian Bitbucket</a></li></ul><ul><li>Atlassian Bitbucket <span title="61dba0f991a9ce6734cb5f2ab06a396f91e9b836" id="product-version" data-commitid="61dba0f991a9ce6734cb5f2ab06a396f91e9b836" data-system-build-number="61dba0f"> v8.8.2</span></li><li data-key="footer.links.documentation"><a href="https://docs.atlassian.com/bitbucketserver/docs-088/Bitbucket+Data+Center+and+Server+documentation?utm_campaign=in-app-help&utm_medium=in-app-help&utm_source=stash" target="_blank">Documentation</a></li><li data-key="footer.links.jac"><a href="https://jira.atlassian.com/browse/BSERV?utm_campaign=in-app-help&utm_medium=in-app-help&utm_source=stash" target="_blank">Request a feature</a></li><li data-key="footer.links.about"><a href="/about">About</a></li><li data-key="footer.links.contact.atlassian"><a href="https://www.atlassian.com/company/contact?utm_campaign=in-app-help&utm_medium=in-app-help&utm_source=stash" target="_blank">Contact Atlassian</a></li></ul><div id="footer-logo"><a href="https://www.atlassian.com/" target="_blank">Atlassian</a></div></section></footer><!-- End #footer --></div><script>require('bitbucket/internal/layout/base/base').onReady(null, "Magnolia Bitbucket" ); require('bitbucket/internal/widget/keyboard-shortcuts/keyboard-shortcuts').onReady();</script><script type="text/javascript">require('bitbucket/internal/page/login/login').onReady();</script></body></html>
Next, configure the Angular app in app.js
:Now you have created the app and defined some constants to use later on.
CSS style sheet
To style the Eric's car page, add some CSS code to style.css
.
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><title>Log in - Magnolia Bitbucket</title><script>
window.WRM=window.WRM||{};window.WRM._unparsedData=window.WRM._unparsedData||{};window.WRM._unparsedErrors=window.WRM._unparsedErrors||{};
WRM._unparsedData["com.atlassian.bitbucket.server.bitbucket-webpack-INTERNAL:user-keyboard-shortcuts-enabled.data"]="true";
WRM._unparsedData["com.atlassian.analytics.analytics-client:programmatic-analytics-init.programmatic-analytics-data-provider"]="false";
WRM._unparsedData["com.atlassian.plugins.atlassian-plugins-webresource-plugin:context-path.context-path"]="\u0022\u0022";
WRM._unparsedData["com.atlassian.plugins.atlassian-clientside-extensions-runtime:runtime.atlassianDevMode"]="false";
WRM._unparsedData["com.atlassian.bitbucket.server.bitbucket-webpack-INTERNAL:date-format-preference.data"]="\u0022\u0022";
WRM._unparsedData["com.atlassian.analytics.analytics-client:policy-update-init.policy-update-data-provider"]="false";
WRM._unparsedData["com.atlassian.bitbucket.plugins.bitbucket-slack-server-integration-plugin:slack-link-error-resources.slack-link-error"]="{}";
WRM._unparsedData["com.atlassian.bitbucket.server.feature-wrm-data:user.time.zone.onboarding.data"]="true";
if(window.WRM._dataArrived)window.WRM._dataArrived();</script>
<link rel="stylesheet" href="/s/fb12b5cedd333afe8a88d6d03457e493-CDN/756145691/61dba0f/4efxux/aad85a899ed1c68cf9653e735ad438ff/_/download/contextbatch/css/_super/batch.css" data-wrm-key="_super" data-wrm-batch-type="context" media="all">
<link rel="stylesheet" href="/s/6ec7d9b361593ba81316e14b8daab20c-CDN/756145691/61dba0f/4efxux/6f9d8fea346063f8a3e3e0b41d4ef6db/_/download/contextbatch/css/bitbucket.page.login,-_super/batch.css" data-wrm-key="bitbucket.page.login,-_super" data-wrm-batch-type="context" media="all">
<link rel="stylesheet" href="/s/9c613ca41350d0286e27a28f4f654d2c-CDN/756145691/61dba0f/4efxux/677c343d65f84ac85c6bbe5d0c559b63/_/download/contextbatch/css/bitbucket.layout.focused,bitbucket.layout.base,atl.general,-_super/batch.css?slack-enabled=true" data-wrm-key="bitbucket.layout.focused,bitbucket.layout.base,atl.general,-_super" data-wrm-batch-type="context" media="all">
<script src="/s/51be751c00bb1fe52396c2e453249f88-CDN/756145691/61dba0f/4efxux/aad85a899ed1c68cf9653e735ad438ff/_/download/contextbatch/js/_super/batch.js?locale=en-US" data-wrm-key="_super" data-wrm-batch-type="context" data-initially-rendered></script>
<script src="/s/3e1ba58a8f2d3afd8a531c18c027a99e-CDN/756145691/61dba0f/4efxux/6f9d8fea346063f8a3e3e0b41d4ef6db/_/download/contextbatch/js/bitbucket.page.login,-_super/batch.js?locale=en-US" data-wrm-key="bitbucket.page.login,-_super" data-wrm-batch-type="context" data-initially-rendered></script>
<script src="/s/289391e007d2e01842e9f693e13cedfd-CDN/756145691/61dba0f/4efxux/677c343d65f84ac85c6bbe5d0c559b63/_/download/contextbatch/js/bitbucket.layout.focused,bitbucket.layout.base,atl.general,-_super/batch.js?locale=en-US&slack-enabled=true" data-wrm-key="bitbucket.layout.focused,bitbucket.layout.base,atl.general,-_super" data-wrm-batch-type="context" data-initially-rendered></script>
<meta name="application-name" content="Bitbucket"><link rel="shortcut icon" type="image/x-icon" href="/s/756145691/61dba0f/4efxux/1.0/_/download/resources/com.atlassian.bitbucket.server.bitbucket-webpack-INTERNAL:favicon/favicon.ico" /><link rel="search" href="https://git.magnolia-cms.com/plugins/servlet/opensearch-descriptor" type="application/opensearchdescription+xml" title="Bitbucket code search"/></head><body class="aui-page-focused aui-page-focused-small aui-page-size-small bitbucket-theme user-login"><ul id="assistive-skip-links" class="assistive"><li><a href="#content">Skip to content</a></li></ul><div id="page"><!-- start #header --><header id="header" role="banner"><section class="notifications"></section><nav class="aui-header aui-dropdown2-trigger-group" aria-label="site"><div class="aui-header-inner"><div class="aui-header-before"><button class=" aui-dropdown2-trigger app-switcher-trigger aui-dropdown2-trigger-arrowless" aria-controls="app-switcher" aria-haspopup="true" role="button" data-aui-trigger href="#app-switcher"><span class="aui-icon aui-icon-small aui-iconfont-appswitcher">Linked Applications</span></button><div id="app-switcher" class="aui-dropdown2 aui-style-default" role="menu" hidden data-is-user-admin="false" data-is-switcher="true"><div class="app-switcher-loading">Loading…</div></div></div><div class="aui-header-primary"><span id="logo" class="aui-header-logo bitbucket-header-logo"><a href="https://git.magnolia-cms.com"><img src="/s/756145691/61dba0f/4efxux/1.0/_/download/resources/com.atlassian.bitbucket.server.bitbucket-webpack-INTERNAL:bitbucket-logo/images/logo/bitbucket.svg" alt="Bitbucket"/></a></span><ul class="aui-nav"></ul></div><div class="aui-header-secondary"><ul class="aui-nav"><li class=" help-link"title="Help"><a class=" aui-dropdown2-trigger aui-dropdown2-trigger-arrowless" aria-controls="com.atlassian.bitbucket.server.bitbucket-server-web-fragments-help-menu" aria-haspopup="true" role="button" tabindex="0" data-aui-trigger><span class="aui-icon aui-icon-small aui-icon-small aui-iconfont-question-circle">Help</span></a><div id="com.atlassian.bitbucket.server.bitbucket-server-web-fragments-help-menu" class="aui-dropdown2 aui-style-default" role="menu" hidden data-aui-dom-container="body"><div class="aui-dropdown2-section help-items-section"><ul class="aui-list-truncate" role="presentation"><li role="presentation"><a href="https://docs.atlassian.com/bitbucketserver/docs-088/Bitbucket+Data+Center+and+Server+documentation?utm_campaign=in-app-help&amp;utm_medium=in-app-help&amp;utm_source=stash" title="Go to the online documentation for Bitbucket" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:general-help">Online help</a></li><li role="presentation"><a href="https://www.atlassian.com/git?utm_campaign=learn-git&utm_medium=in-app-help&utm_source=stash" title="Learn about Git commands & workflows" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:learn-git">Learn Git</a></li><li role="presentation"><a href="/getting-started"class="getting-started-page-link" title="Overview of Bitbucket features" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:getting-started-page-help-link">Welcome to Bitbucket</a></li><li role="presentation"><a href="/#"class="keyboard-shortcut-link" title="Discover keyboard shortcuts in Bitbucket" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:keyboard-shortcuts-help-link">Keyboard shortcuts</a></li><li role="presentation"><a href="https://go.atlassian.com/bitbucket-server-whats-new?utm_campaign=in-app-help&utm_medium=in-app-help&utm_source=stash" title="Learn about what's new in Bitbucket" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:whats-new-link">What's new</a></li><li role="presentation"><a href="https://go.atlassian.com/bitbucket-server-community?utm_campaign=in-app-help&utm_medium=in-app-help&utm_source=stash" title="Explore the Atlassian community" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:community-link">Community</a></li><li role="presentation"><a href="/about" title="About Bitbucket" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:about">About</a></li></ul></div></div></li><li class=" alerts-menu"title="View system alerts"><a href="#alerts" id="alerts-trigger"class="alerts-menu" title="View system alerts" data-web-item-key="com.atlassian.bitbucket.server.bitbucket-server-web-fragments:global-alerts-menu-item">Alerts</a></li></ul></div></div> <!-- End .aui-header-inner --></nav> <!-- End .aui-header --></header><!-- End #header --><!-- Start #content --><section id="content" role="main" tabindex="-1" data-timezone="-120" ><div class="aui-page-panel content-body" ><div class="aui-page-panel-inner"><main role="main" id="main" class="aui-page-panel-content" ><h2>Log in</h2><form class="aui top-label prevent-double-submit " action="/j_atl_security_check" method="post" accept-charset="UTF-8"><div class="field-group"><label for="j_username" >Username</label><input class="text long-field" type="text" id="j_username" name="j_username" autofocus accesskey="u"/></div><div class="field-group"><label for="j_password" >Password</label><input class="text long-field" type="password" id="j_password" name="j_password" accesskey="p"/></div><div class="aui-group"><fieldset class="group checkbox"><div class="checkbox"><input class="checkbox" type="checkbox" id="_atl_remember_me" name="_atl_remember_me" checked="checked" accesskey="r"/><label for="_atl_remember_me" >Keep me logged in</label></div></fieldset></div><div class="aui-group"><input name="next" type="hidden" value="/projects/DOCUMENTATION/repos/content-app-clients/browse/angular/style.css?at=master&raw"/><input name="queryString" type="hidden" value="next=%2Fprojects%2FDOCUMENTATION%2Frepos%2Fcontent-app-clients%2Fbrowse%2Fangular%2Fstyle.css%3Fat%3Dmaster%26raw"/><input class="aui-button aui-button-primary" type="submit" id="submit" name="submit" value="Log in" accesskey="s"/><a id="forgot" class="aui-button aui-button-link" name="forgot" href="/passwordreset" autocomplete="off" tabindex="0">Unable to access your account?</a></div></form></main></div></div></section><!-- End #content --><!-- Start #footer --><footer id="footer" role="contentinfo"><section class="notifications"></section><section class="footer-body"><ul><li data-key="footer.license.message">Git repository management for enterprise teams powered by <a href="https://www.atlassian.com/software/bitbucket/">Atlassian Bitbucket</a></li></ul><ul><li>Atlassian Bitbucket <span title="61dba0f991a9ce6734cb5f2ab06a396f91e9b836" id="product-version" data-commitid="61dba0f991a9ce6734cb5f2ab06a396f91e9b836" data-system-build-number="61dba0f"> v8.8.2</span></li><li data-key="footer.links.documentation"><a href="https://docs.atlassian.com/bitbucketserver/docs-088/Bitbucket+Data+Center+and+Server+documentation?utm_campaign=in-app-help&utm_medium=in-app-help&utm_source=stash" target="_blank">Documentation</a></li><li data-key="footer.links.jac"><a href="https://jira.atlassian.com/browse/BSERV?utm_campaign=in-app-help&utm_medium=in-app-help&utm_source=stash" target="_blank">Request a feature</a></li><li data-key="footer.links.about"><a href="/about">About</a></li><li data-key="footer.links.contact.atlassian"><a href="https://www.atlassian.com/company/contact?utm_campaign=in-app-help&utm_medium=in-app-help&utm_source=stash" target="_blank">Contact Atlassian</a></li></ul><div id="footer-logo"><a href="https://www.atlassian.com/" target="_blank">Atlassian</a></div></section></footer><!-- End #footer --></div><script>require('bitbucket/internal/layout/base/base').onReady(null, "Magnolia Bitbucket" ); require('bitbucket/internal/widget/keyboard-shortcuts/keyboard-shortcuts').onReady();</script><script type="text/javascript">require('bitbucket/internal/page/login/login').onReady();</script></body></html>
carDetail
controller and AngularJS utilities
To render a car's details we want a $scope
object of the carDetail
controller with the following properties:
carTitle
carDescription
imgUrl
Add the following HTML snippet to erics-car.html
:
globalData
component
Add a component named globalData
. Both controllers will use it. It has a property productPath
which will be set by one controller and read by the other controller.
utils
component
Add a component named utils
. It provides some useful methods that both controllers can use. The carDetail
controller will use the function #getPropertyValue
.Note:
getPropertyValue(node, propertyName)
is a convenience method to fetch a property from the JSON structure is returned by the standard nodes endpoint.replaceAll(str, find, replace)
manipulates strings. The carDetail
controller uses this function just to style things a bit.
sanitize
filter
The third little helper to add is the sanitize
filter. Remember that the description
property of the content item contains HTML, encoded HTML even. 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. Internally the filter is using the angular module $sce
.
carDetail
controller
Finally, add the carDetail
controller:Note:
-
APP_CONFIG
, globalData
and utils
are injected. - Line 9: The controller executes an HTTP#get method on the
nodes
REST endpoint. The content item path is the selected item. It requests data for one content item as we did in testing REST access. - Line 8, 9: Computes a request URL for the REST call using configuration data from the
globalData
component. - Line 3ff: The HTTP#get call is defined as a callback function of
$watch
which is a value change listener for globalData.productPath
. The REST call is executed initially and when globalData.productPath
.
Reload the file in your browser again. 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
:The $scope of this controller has the following properties:
cars
: An array of car items. Each car item has these properties:path
category
: actually a pseudo-category, the name of the parent folder such as classics
or 007
.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:Note:
- Line 1: Again custom components
APP_CONFIG
, globalData
and utils
are injected. - Lines 5-7:
selectCar
is executed when you click a car of the list. The function sets globalData.productPath
so it indirectly triggers the execution of the carDetail
controller. - Lines 9-11:
clickCategory
maintains the $scope variable carCategories
. Since this changes the state of a controller $scope variable, the UI gets repainted - the list items gets updated. - Line 3: The controller executes an HTTP#get method. Here the URI for the JSON call requests the cars root folder and contains the argument
depth
.
Final result:

Get the completed files
The three files in their final form are available in Magnolia's Git repository.
Option 1: Clone the 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.git
Option 2: Download
What next?
Custom JSON format
The JSON format provided by the nodes
endpoint is not very handy. In this tutorial we managed to get the required properties with an extra JavaScript function in the utils
component of the Angular app. But at some point you may want a JSON representation that you cannot get easily with the default JCR nodes
and properties
endpoints.
With a custom JSON service you can also get data from different JCR workspaces in one request. This cannot be done with the default endpoints for nodes and properties. A custom service therefore saves resources on the network.
With Java: create a custom endpoint
Expose a custom endpoint. This approach requires some Java classes which must be packaged into a Magnolia Maven module.
Without Java: neat-jsonfn
Another solution is the neat-jsonfn
module. Read the blog post and check out the module in:
Combine your content app with the Categorization module
The Products app used in this tutorial is very simple. We use folders to mimic car categories. Better use the Categorization module to add multiple categories.
Where to add the AngularJS app?
You can use or host the AngularJS app anywhere:
- As 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 distinct 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 CORSFilter:
Photo credits:
#trackbackRdf ($trackbackUtils.getContentIdentifier($page) $page.title $trackbackUtils.getPingUrl($page))