Magnolia 5.6 reached end of life on June 25, 2020. This branch is no longer supported, see End-of-life policy.
HTML Wrap | ||||
---|---|---|---|---|
| ||||
Related topics: Resources:
|
This page 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. 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 the Magnolia Delivery endpoint API you can access any Magnolia 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.
Table of Contents | ||||
---|---|---|---|---|
|
Your tasks:
Architecture overview:
...
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.
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:
Info | ||
---|---|---|
| ||
REST endpoints may be a security risk. Set permissions correctly. Read REST API security and define security for your specific requirements. |
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.
In this tutorial we use the Products app and the classic cars as sample content. The cars are stored in the products
JCR workspace.
...
Artifact maven dependencies snippet | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
...
Artifact resource link | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
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 permission to read content from the workspace products
to a new role.
read-products
/
and its subnodes in the Products
workspace/read-products
in the Userroles
workspace.Get
access to the path /.rest/nodes/v1/products*
. Deny all others.read-products
role to the superuser
system account.read-products
role. read-products
role to the anonymous
system account.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.
Request the app content with a REST call. Try the following request in your browser:
Code Block | ||
---|---|---|
| ||
http://localhost:8080/magnoliaAuthor/.rest/nodes/v1/products/cars/007/Aston-Martin-DB5 |
Magnolia responds with:
...
title | Click here to expand to show the response from the REST endpoint |
---|
app-tutorial
content appFor this tutorial you need a Magnolia bundle with:
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
.
app-tutorial
content app : Artifact resource link | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
Artifact resource link | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
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:
Code Block |
---|
erics-cars-tutorial/
├── apache-tomcat
└── light-modules |
Remember the light-modules
folder. You will add files there later on.
Now start the bundle:
Code Block |
---|
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
.
Click the Products tile open the app.
This content app has two subapps:
app-tutorial
You need the following information to properly configure REST access to the content:
products
.mgnl:product
.mgnl:folder
.dam
workspace. Car nodes store a reference to a dam asset.Tip |
---|
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.
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:
Code Block |
---|
content-app-clients-v2/
└── restEndpoints/
└── delivery/
└── carFinder.yaml |
Note:
restEndpoints
. You can also use subfolders; here we created a subfolder called delivery
.endpointPath
and REST access URLs.<magnolia-base-path>/.rest/delivery/carFinder
The definition file should look like this:
Code Pro | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
Note:
bypassWorkspaceAcls
in a production environment. See REST security below.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.
We want to test the requests:
The requests should look like this:
<magnolia-base-path>/.rest/delivery/carFinder/
...
cars/007/Aston-Martin-DB5
...
<?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>
<magnolia-base-path>/.rest/delivery/carFinder/cars
(for all cars) delivery/carFinder
is the endpointPath defined by the endpoint configuration.
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:
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.
This is what the JSON responses look like:
Expand | |||||
---|---|---|---|---|---|
| |||||
Lines 8-13: Note that the reference to the asset (which is stored in the |
Expand | ||
---|---|---|
| ||
|
Anchor | ||||
---|---|---|---|---|
|
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.
Note |
---|
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:
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:
superuser
on magnoliaAuthor
anonymous
user on magnoliaPublic
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:
rest-anonymous
that comes by default with the REST modules and is assigned to anonymous user.bypassWorkspaceAcls
.Note |
---|
In a production environment:
|
AngularJS is a Web application framework popular among front-end developers. In this tutorial,
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:
Code Block | ||
---|---|---|
| ||
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:
Code Block | ||
---|---|---|
| ||
{
"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:
nodes
API endpoint.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. 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.
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.
...
Code Block | ||
---|---|---|
| ||
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 |
whereWhere:
cars
, 007
and classics
are folders.mgnl:product
.We will create Let's create an Angular app that provides:
classics
or 007
cars. We use the parent folders as pseudo-categories.We will create an ngApp
with two controllers:
carsList
renders a list of all cars. Each car has a clickable label. Clicking on the label shows the car in the carDetail
controller. The list controller also has two checkboxes to filter the list - ; one for each parent folder.
carDetail
renders renders the detail details of the selected car: title, description and image.Create the following file 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:
Code Block | ||
---|---|---|
| ||
$lightmodules └── content-app-clients-v2/ └──├── angular/ │ ├── app.js │ ├── apperics-cars.jshtml │ └── style.css └── restEndpoints ├── erics-cars.html└── delivery └── 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:
/Users/johndoe/dev/lightmodules
C:\Users\johndoe\dev\lightmodules
You may want to set the magnolia.resources.dir
property to reference your light modules folder:
Code Block |
---|
magnolia.resources.dir=/Users/johndoe/dev/lightmodules |
carFinder.yaml |
Info |
---|
To avoid running into |
Tip |
To avoid getting in 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. Instead of a Freemarker template, keep it simple and just add a three static files to a light module 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
appStart creating the Angular app in erics-cars.html
. Add an ng-app
directive to the body
element which will wrap wraps the two controllers:
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
<!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> |
...
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.
Code Block |
---|
http://localhost:8080/magnoliaAuthor/.resources/content-app-clients/angular/erics-cars.html?foo=bar |
server and client side.
Code Block | |
---|---|
http://localhost:8080/magnoliaPublic/.resources/ | |
Hide block | |
Code Pro | |
language | xml |
title | content-app-clients/angular/erics-cars.html |
linenumbers | true |
sections | %%(doctype)%% - %%(</h1>)%% , %%(</body>)%% - %%(</html>)%% | url | https://git.magnolia-cms.com/projects/DOCUMENTATION/repos/
Next, configure the Angular app in app.js
:
Code Pro | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
Now you have created the app and defined some constants to use later on.
To style the Eric's car page, add some CSS code to style.css
.
Code Pro | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
...
Code Pro | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
Info |
---|
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 " and how it is named depends on the browser you are using. For instance, on Google Chrome browser it is named DevTools. |
globalData
componentAdd a component named globalData
. Both controllers will use it. It has a property productPath
which will be that is set by one controller and read by the other controller.
Code Pro | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
...
Add a component named utils
. It provides some useful methods that both controllers can use. The carDetail
controller will We use the function #getPropertyValue
function replaceAll
in the carsList
controller.
Code Pro | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
|
Note:
...
| |
|
...
sanitize
filterThe 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.
Code Pro | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
Internally the filter is using the angular Angular module $sce
.
carDetail
controller...
Code Pro | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
...
APP_CONFIG
, globalData
and utils
are injected.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.globalData
component.$watch
which is a value change listener for globalData.productPath
. The REST call is executed initially and when globalData.productPath
changes.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
controllerThe 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
:
Code Pro | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
|
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.APP_CONFIG
, globalData
and utils
are injected.globalData
component.HTTP#get
method on the delivery
REST endpoint on the prefix carFinder
. The content item path is the selected item. It requests data for one content item as we did in testing REST access.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
changes.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
controllerThe 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
Here is the JavaScript code:
Code Pro | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
|
Note:
APP_CONFIG
, globalData
and utils
are injected.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.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.depth
.Final result:
The three files in their final form are available in Magnolia's Git repository.
Clone the repository to get the complete file structure. Use a terminal, go to your light modules folder, and clone:
| |
|
The $scope of this controller has the following properties:
cars
: An array of car items. 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. checked
: Whether the pseudo-category is currently selected.Here is the JavaScript code:
Code Pro | |||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Code Block | |||||||||||||||||
| |||||||||||||||||
|
| scm
| documentation
| .git
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.
Expose a custom endpoint. This approach requires some Java classes which must be packaged into a Magnolia Maven module.
neat-jsonfn
Another solution is the neat-jsonfn
module. Read the blog post and check out the module in GitHub:
...
| |
|
Note:
APP_CONFIG
, globalData
and utils
are injected.selectCar
is executed when you click a car in the list. The function sets globalData.productPath
so it indirectly triggers the execution of the carDetail
controller. carFinder
prefix of the delivery endpoint.clickCategory
maintains the $scope variable carCategories
. Since this changes the state of a controller $scope variable, the UI gets repainted - the list items get updated.HTTP#get
method. Here the URI for the JSON call requests the cars root folder and contains the argument depth
.Final result:
The angular app is now fully functional. Congratulations!
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:
Code Block | ||
---|---|---|
| ||
git clone https://git.magnolia-cms.com/scm/documentation/content-app-clients-v2.git |
...
You can use or host the AngularJS app anywhere:
Anchor | ||||
---|---|---|---|---|
|
If you want to run the AngularJS app on a distinct 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 CORSFilter:
the Magnolia Add HTTP Headers filter.
...
Photo credits:
Aston Martin DB5, Aston Martin Works
Aston Martin V8, Beltane43, Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)