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.


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-tutorial content app :  

    Error rendering macro 'artifact-resource-macro'

    com.sun.jersey.api.client.ClientHandlerException: A message body reader for Java class, and Java type class, and MIME media type application/octet-stream was not found

    Error rendering macro 'artifact-resource-macro'

    com.sun.jersey.api.client.ClientHandlerException: A message body reader for Java class, and Java type class, and MIME media type application/octet-stream was not found

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:

├── 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 dam workspace. 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:

└── restEndpoints/
    └── delivery/
        └── carFinder.yaml


  • Endpoint definitions must reside within the folder restEndpoints. You can also use subfolders; here we created a subfolder called delivery.
  • 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 the endpointPath and 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:

workspace: products
rootPath: /
  - mgnl:folder
  - mgnl:product
  - mgnl:folder
  - mgnl:product
depth: 2
includeSystemProperties: false
bypassWorkspaceAcls: true
  - name: carAssets
    propertyName: image
      includeAssetMetadata: false

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.


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:

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. 

JSON responses

This is what the JSON responses look like:

  "@name": "Aston-Martin-DB5",
  "@path": "/cars/007/Aston-Martin-DB5",
  "@id": "ce39509e-e1d5-4cdd-b254-125948e2aeec",
  "@nodeType": "mgnl:product",
  "description": "<p><strong>Movie:</strong>&nbsp;Goldfinger, GoldenEye, Casino Royale and Skyfall<br />\n<strong>Year:</strong>&nbsp;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&#39;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=\"\">Wikipedia</a></p>\n",
  "title": "Aston Martin DB5",
  "image": {
    "@name": "Aston-Martin-DB5.jpg",
    "@path": "/cars/007-cars/Aston-Martin-DB5.jpg",
    "@id": "jcr:4cd02639-167d-405a-923f-607aa20d0bc0",
    "@link": "/magnoliaPublic/dam/jcr:4cd02639-167d-405a-923f-607aa20d0bc0/Aston-Martin-DB5.jpg"
  "@nodes": []

Lines 8-13: Note that the reference to the asset (which is stored in the dam workspace) has been resolved.

  "@name": "cars",
  "@path": "/cars",
  "@id": "6254df84-93b1-4431-b6dd-53b7b601c03d",
  "@nodeType": "mgnl:folder",
  "classics": {
    "@name": "classics",
    "@path": "/cars/classics",
    "@id": "45aa1b8d-0cd3-4829-9f17-02b4603910c1",
    "@nodeType": "mgnl:folder",
    "Continental-Mark-II": {
      "@name": "Continental-Mark-II",
      "@path": "/cars/classics/Continental-Mark-II",
      "@id": "dfeb5ad9-d06c-4e76-85e1-f30ef97c8bca",
      "@nodeType": "mgnl:product",
      "description": "<p>The Continental Mark II is a personal luxury car that was produced by Continental in 1956 and 1957. An attempt to build a post-World War II car to rival the greatest of the pre-War era, or anything produced in Europe, it is regarded as a rare and elegant classic.</p>\n\n<p>Ford wanted a superior and standalone up-market brand &ndash; aside from Lincoln &ndash; to compete with General Motors&#39; Cadillac, Packard, and Chrysler Corporation&#39;s Imperial brands.</p>\n\n<p>The new Continental was not intended to be the largest or most powerful automobile; rather, the most luxurious and elegant American car available, designed to recapture the spirit of the great classics of the prewar period&mdash;with prices to match. The Mark II&#39;s inspiration was the celebrated V12-powered Lincoln Continental of the 1940s, among the most notable cars of that War-interrupted decade.</p>\n",
      "title": "Continental Mark II",
      "image": {
        "@name": "7791746590_b5d3139a3f_b.jpg",
        "@path": "/cars/classic-cars/7791746590_b5d3139a3f_b.jpg",
        "@id": "jcr:5f29b11a-1c37-4f3e-a0e1-ef033b84998c",
        "@link": "/magnoliaPublic/dam/jcr:5f29b11a-1c37-4f3e-a0e1-ef033b84998c/7791746590_b5d3139a3f_b.jpg"
      "@nodes": []
    "1927-Hudson": {
      "@name": "1927-Hudson",
      "@path": "/cars/classics/1927-Hudson",
      "@id": "3e171627-43b4-47f8-bb82-dccbfac2c07f",
      "@nodeType": "mgnl:product",
      "description": "<p>The Hudson Motor Car Company made Hudson and other brand automobiles in Detroit, Michigan, from 1909 to 1954. In 1954, Hudson merged with Nash-Kelvinator Corporation to form American Motors (AMC). The Hudson name was continued through the 1957 model year, after which it was discontinued.</p>\n\n<p>The company had a number of firsts for the auto industry; these included dual brakes, the use of dashboard oil-pressure and generator warning lights, and the first balanced crankshaft, which allowed the Hudson straight-six engine, dubbed the &quot;Super Six&quot; (1916), to work at a higher rotational speed while remaining smooth, developing more power for its size than lower-speed engines. The dual brake system used a secondary mechanical emergency brake system, which activated the rear brakes when the pedal traveled beyond the normal reach of the primary system; a mechanical parking brake was also used. Hudson transmissions also used an oil bath and cork clutch mechanism that proved to be as durable as it was smooth.</p>\n",
      "title": "1927 Hudson",
      "image": {
        "@name": "5773574199_2c5cc1d891_b.jpg",
        "@path": "/cars/classic-cars/5773574199_2c5cc1d891_b.jpg",
        "@id": "jcr:8290ac0e-d9ab-4349-8ff9-100dc5d73eb8",
        "@link": "/magnoliaPublic/dam/jcr:8290ac0e-d9ab-4349-8ff9-100dc5d73eb8/5773574199_2c5cc1d891_b.jpg"
      "@nodes": []
    "Pontiac-Chieftain-1952": {
      "@name": "Pontiac-Chieftain-1952",
      "@path": "/cars/classics/Pontiac-Chieftain-1952",
      "@id": "bc84be8c-d9d5-4d2f-987b-e2cc5302aee0",
      "@nodeType": "mgnl:product",
      "description": "<p>The Pontiac Chieftain is an automobile that was produced by the Pontiac from 1949 to 1958. Chieftains were one of the first all new car designs to come to Pontiac in the post World War II years. Previous cars had been 1942 models with minor revisions.</p>\n\n<p>The Chieftain was initially introduced with four models: Sedan, Sedan Coupe, Business Coupe and Deluxe Convertible Coupe. In 1950, a Catalina Coupe was added to the range while a station wagon was added in 1952, with the demise of the top of the line Streamliner wagon.</p>\n\n<p>Some of the more interesting optional items available for the first generation Chieftain included a radio with seven vacuum tubes, tissue dispenser, under seat heaters, and a Remington Auto-Home shaver. In 1951, the horsepower on the 8-cylinder rose to 116. The Chieftain came with a gas gauge, ammeter, oil pressure gauge, and a temperature gauge which had marks for 160, 180, and 220 degrees Fahrenheit.</p>\n",
      "title": "Pontiac Chieftain 1952",
      "image": {
        "@name": "6218486709_743bd40d36_b.jpg",
        "@path": "/cars/classic-cars/6218486709_743bd40d36_b.jpg",
        "@id": "jcr:6df7ee03-5b11-46d4-a15f-579bcffb50b9",
        "@link": "/magnoliaPublic/dam/jcr:6df7ee03-5b11-46d4-a15f-579bcffb50b9/6218486709_743bd40d36_b.jpg"
      "@nodes": []
    "Fiat-Cinquecento": {
      "@name": "Fiat-Cinquecento",
      "@path": "/cars/classics/Fiat-Cinquecento",
      "@id": "ed450660-c375-443b-84bb-95a11846ccf0",
      "@nodeType": "mgnl:product",
      "description": "<p>The Fiat 500 (Italian: Cinquecento) was a city car produced by the Italian manufacturer Fiat between 1957 and 1975.</p>\n\n<p>Launched as the Nuova (new) 500 in July 1957, it was a cheap and practical town car. Measuring only 2.97 metres (9 feet 9 inches) long, and originally powered by an appropriately sized 479 cc two-cylinder, air-cooled engine, the 500 redefined the term &quot;small car&quot; and is considered one of the first city cars.</p>\n\n<p>Despite its diminutive size, the 500 proved to be an enormously practical and popular vehicle throughout Europe. Besides the two-door coup&eacute;, it was also available as the &quot;Giardiniera&quot; station wagon; this variant featured the standard engine laid on its side, the wheelbase lengthened by 10 cm (3.9 in) to provide a more convenient rear seat, a full-length sunroof, and larger brakes from the Fiat 600.</p>\n",
      "photoCredit": "werwer",
      "title": "Fiat Cinquecento",
      "image": {
        "@name": "6791962744_dc749ec56b_b.jpg",
        "@path": "/cars/classic-cars/6791962744_dc749ec56b_b.jpg",
        "@id": "jcr:1bbae8c4-26e3-4e2e-83b1-7658565e6e09",
        "@link": "/magnoliaPublic/dam/jcr:1bbae8c4-26e3-4e2e-83b1-7658565e6e09/6791962744_dc749ec56b_b.jpg"
      "@nodes": []
    "Riley-Brooklands-1930": {
      "@name": "Riley-Brooklands-1930",
      "@path": "/cars/classics/Riley-Brooklands-1930",
      "@id": "ba03c082-a68d-46c0-afe8-84d3fdcc6bd2",
      "@nodeType": "mgnl:product",
      "description": "<p>The Riley Nine was one of the most successful light cars produced by the British motor industry in the inter war period. It was made by the Riley company of Coventry, England with a wide range of body styles between 1926 and 1938.</p>\n\n<p>The car was largely designed by two of the Riley brothers, Percy and Stanley. Stanley was responsible for the chassis, suspension and body and the older Percy designed the engine.</p>\n\n<p>The 1,087 cc four-cylinder engine had hemispherical combustion chambers with the valves inclined at 45 degrees in a crossflow head. To save the expense and complication of overhead camshafts, the valves were operated by two camshafts mounted high in the crankcase through short pushrods and rockers. The engine was mounted in the chassis by a rubber bushed bar that ran through the block with a further mount at the rear of the gearbox. Drive was to the rear wheels through a torque tube and spiral bevel live rear axle mounted on semi elliptic springs.</p>\n",
      "title": "Riley Brooklands 1930",
      "image": {
        "@name": "5158386021_1fffd2fb4d_b.jpg",
        "@path": "/cars/classic-cars/5158386021_1fffd2fb4d_b.jpg",
        "@id": "jcr:57cc5aa2-4d72-4b05-be2a-b02856e9f91c",
        "@link": "/magnoliaPublic/dam/jcr:57cc5aa2-4d72-4b05-be2a-b02856e9f91c/5158386021_1fffd2fb4d_b.jpg"
      "@nodes": []
    "@nodes": [
  "007": {
    "@name": "007",
    "@path": "/cars/007",
    "@id": "8ae34efb-52bd-4b94-a484-2677be490c9b",
    "@nodeType": "mgnl:folder",
    "Aston-Martin-DB5": {
      "@name": "Aston-Martin-DB5",
      "@path": "/cars/007/Aston-Martin-DB5",
      "@id": "ce39509e-e1d5-4cdd-b254-125948e2aeec",
      "@nodeType": "mgnl:product",
      "description": "<p><strong>Movie:</strong>&nbsp;Goldfinger, GoldenEye, Casino Royale and Skyfall<br />\n<strong>Year:</strong>&nbsp;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&#39;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=\"\">Wikipedia</a></p>\n",
      "title": "Aston Martin DB5",
      "image": {
        "@name": "Aston-Martin-DB5.jpg",
        "@path": "/cars/007-cars/Aston-Martin-DB5.jpg",
        "@id": "jcr:4cd02639-167d-405a-923f-607aa20d0bc0",
        "@link": "/magnoliaPublic/dam/jcr:4cd02639-167d-405a-923f-607aa20d0bc0/Aston-Martin-DB5.jpg"
      "@nodes": []
    "Sunbeam-Alpine": {
      "@name": "Sunbeam-Alpine",
      "@path": "/cars/007/Sunbeam-Alpine",
      "@id": "7ff3597f-bd68-4293-969f-9a9d13619ea6",
      "@nodeType": "mgnl:product",
      "description": "<p><strong>Movie:</strong>&nbsp;Dr. No<br />\n<strong>Year:&nbsp;</strong>1962</p>\n\n<p>The Sunbeam Alpine is a sporty two-seat open car produced by Sunbeam from 1953 to 1955, and then 1959 to 1968. The name was then used on a two-door fastback from 1969 to 1975. The original Alpine was launched in 1953 as the first vehicle from Sunbeam-Talbot to bear the Sunbeam name alone since the 1935 takeover of Sunbeam and Talbot by the Rootes Group.</p>\n\n<ul>\n\t<li>A metallic blue 1953 Sunbeam Alpine Mk.1 is driven by Grace Kelly in To Catch a Thief (1955) with Cary Grant.</li>\n\t<li>A lake blue &quot;Series II&quot; Alpine roadster is amongst the first on-screen &quot;Bond cars&quot; when it is rented and driven by James Bond in Dr. No of 1962. It was reportedly borrowed from a local resident, as the only suitable sports car available on the island used for filming.</li>\n\t<li>A Sunbeam Alpine is driven by Michael Caine in the 1967 film Gambit. The film also starred Shirley MacLaine and Herbert Lom.</li>\n</ul>\n\n<p>Source: <a href=\"\">Wikipedia</a></p>\n\n<p>&nbsp;</p>\n",
      "title": "Sunbeam Alpine",
      "image": {
        "@name": "Sunbeam-Alpine.jpg",
        "@path": "/cars/007-cars/Sunbeam-Alpine.jpg",
        "@id": "jcr:061b70de-07fb-4fa0-be60-ab82e4b43c70",
        "@link": "/magnoliaPublic/dam/jcr:061b70de-07fb-4fa0-be60-ab82e4b43c70/Sunbeam%20Alpine.jpg"
      "@nodes": []
    "Aston-Martin-V8": {
      "@name": "Aston-Martin-V8",
      "@path": "/cars/007/Aston-Martin-V8",
      "@id": "40938f8b-37d6-4bc8-80eb-1e06d3c46557",
      "@nodeType": "mgnl:product",
      "description": "<p><strong>Movie:</strong>&nbsp;The Living Daylights<br />\n<strong>Year:</strong>&nbsp;1987</p>\n\n<p>At the beginning of the film, the car is a V8 Volante (convertible). Later, the car is fitted with a hardtop (&quot;winterised&quot;) at Q Branch.</p>\n\n<p>The alterations and gadgets featured were:</p>\n\n<ul>\n\t<li>Police band radio</li>\n\t<li>Tire Spikes</li>\n\t<li>Jet engine behind rear number plate</li>\n\t<li>Retractable outriggers</li>\n\t<li>Heat-seeking missiles behind fog lights</li>\n\t<li>Lasers in front wheel hubcaps</li>\n\t<li>Bulletproof windows</li>\n\t<li>Fireproof body</li>\n\t<li>Self-destruct system</li>\n</ul>\n\n<p>Source: <a href=\"\">Wikipedia</a></p>\n",
      "title": "Aston Martin V8",
      "image": {
        "@name": "Aston-Martin-V8.jpg",
        "@path": "/cars/007-cars/Aston-Martin-V8.jpg",
        "@id": "jcr:129b1675-c383-46ae-97a8-b1ba18c0ab9d",
        "@link": "/magnoliaPublic/dam/jcr:129b1675-c383-46ae-97a8-b1ba18c0ab9d/Aston%20Martin%20V8.jpg"
      "@nodes": []
    "Lotus-Esprit-S1": {
      "@name": "Lotus-Esprit-S1",
      "@path": "/cars/007/Lotus-Esprit-S1",
      "@id": "c9342333-4b85-4ebb-b978-478742ecee96",
      "@nodeType": "mgnl:product",
      "description": "<p><strong>Movie:</strong>&nbsp;The Spy Who Loved Me<br />\n<strong>Year:</strong>&nbsp;1977</p>\n\n<p>Delivered to Bond by Q in Sardinia, this Lotus is capable of transforming into a submarine. In this mode, it is equipped with anti-aircraft missiles.&nbsp;</p>\n\n<p>Source <a href=\"\">Wikipedia</a></p>\n\n<p>The Lotus Esprit is a sports car that was built by Lotus in the United Kingdom between 1976 and 2004. The silver Italdesign concept that eventually became the Esprit was among the first of designer Giorgetto Giugiaro&#39;s polygonal &quot;folded paper&quot; designs.&nbsp;</p>\n\n<p>The Esprit was powered by the Lotus 907 4-cylinder engine, as previously used in the Jensen Healey. This engine displaced 2.0 L, produced 160 bhp (119 kW; 162 PS) in European trim 140 bhp (104 kW; 142 PS) in US/Federal trim, and was mounted longitudinally behind the passengers, as in its predecessor.&nbsp;</p>\n\n<p>Source: <a href=\"\">Wikipedia</a></p>\n",
      "title": "Lotus Esprit S1",
      "image": {
        "@name": "Lotus-Esprit-S1.jpg",
        "@path": "/cars/007-cars/Lotus-Esprit-S1.jpg",
        "@id": "jcr:e675a1cf-2569-40ab-b105-4f9870a7493a",
        "@link": "/magnoliaPublic/dam/jcr:e675a1cf-2569-40ab-b105-4f9870a7493a/Lotus-Esprit-S1.jpg"
      "@nodes": []
    "@nodes": [
  "@nodes": [

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:

  • 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:

  • Web access is granted by the role rest-anonymous that 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 bypassWorkspaceAcls in 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:

├── 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


  • cars, 007 and classics are 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 classics or 007 cars. We use the parent folders as pseudo-categories.

App structure

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 the details of the selected car: title, description and image.

App files

Now create a folder called angular with the files app.jserics-cars.html and style.css in the light module, to create this structure:

├── 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:

lightmodules/content-app-clients-v2/angular/erics-cars.html (intermediate state)
<!doctype html>
    <title>Angular loves Magnolia :-)</title>
    <link rel="stylesheet" href="style.css?u=12">
    <script src=""></script>
    <script src="app.js?v=w"></script>

<body ng-app="CarsContentClient">

<h1>Eric's cars - shown with a lightweight angular client</h1>



  • 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.


Next, configure the Angular app in app.js:

lightmodules/content-app-clients-v2/angular/app.js (fragment)
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"
Now you have created the app and defined some constants to use later on.

CSS style sheet

To style Eric's car page, add some CSS code to style.css

 body, li{
    font-family: "Trebuchet MS", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", geneva, helvetica, arial, tahoma, verdana, sans-serif;

    color: #666666;
    font-size: 0.768em;

p.intro a{
    color: dimgray;
} .car{
    font-family: "Courier New";
    font-weight: 800;
    font-style: italic;
    padding: 5px;
} .car .click{
    cursor: pointer;
    margin: 5px;
    background-color: aquamarine;
    color: #666;
    padding:  2px 10px;
    border-radius: 3px;
} .car .click.selected{
    color: red;
    float: left;

    display: inline-block;
    margin: 0;
    padding: 10px 10px;
    float: right;
    font-size: 0.768em;
    font-family: "Courier New";
    font-weight: 800;
    font-style: italic;

    display: inline-block;

    clear: both;
    height: 20px;
    width: auto;
    margin: 10px;

.car-detail h2{
    font-family: "Courier New";
    display: inline-block;
    margin: 10px;
    padding: 5px 10px;
    border-radius: 3px;

.car-detail p.description{
    font-size: 0.923em;
    padding: 0 15px;

table td.image{
    width: auto;
    height: inherit;
    padding: 15px;
    margin: 20px;

    border:1px solid #ccc;
    -webkit-border-radius: 20px;
    -moz-border-radius: 20px;
    border-radius: 20px;

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:

lightmodules/content-app-clients-v2/angular/erics-cars.html (fragment)
<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 width="40%"> <h2>{{carTitle}}</h2><p ng-bind-html="carDescription | sanitize" class="description"></p></td>
<!-- 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.

lightmodules/content-app-clients-v2/angular/app.js (fragment)
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.

lightmodules/content-app-clients-v2/angular/app.js (fragment)
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. 

lightmodules/content-app-clients-v2/angular/app.js (fragment)
app.filter("sanitize", ['$sce', function ($sce) {
  return function (htmlCode) {
    return $sce.trustAsHtml(htmlCode);
Internally the filter is using the Angular module  $sce .

carDetail controller

Finally, add the carDetail controller:

lightmodules/content-app-clients-v2/angular/app.js (fragment)
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 =;
          $scope.carDescription =;
          $scope.imgUrl =["@link"];
    }, true);

  • Line 1: Custom components APP_CONFIGglobalData and utils are injected.
  • Lines 6, 7: Computes a request URL for the REST call using configuration data from the globalData component.
  • Line 7: The controller executes an 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.
  • Line 2 and onward: 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  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 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:

lightmodules/content-app-clients-v2/angular/erics-cars.html (fragment)
<div ng-controller="carsList">
    <!-- car folders filters -->
    <form class="filters">
        <div ng-repeat="cat in carCategories" class="cat">
            <label>{{}} cars</label><input type="checkbox" name="category"  ng-click="clickCategory(" ng-model="cat.checked" />
            <label> &nbsp; &nbsp;</label>
    <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><!-- eof 'carsList' ctrl. -->
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 car
    • name: 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:

lightmodules/content-app-clients-v2/angular/app.js (fragment)
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("")) {
  $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) {
      $ = [];
      for (var item in {
        // grab the top level folders with the car categories ("classic-cars", "007-cars")
        if ("mgnl:folder" ==[item]["@nodeType"]) {
          var folderName =[item]["@name"];
          var folderNode =[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");
              ${path: carPath, category: folderName, title: carTitle, name: carNode["@name"]});

  • Line 1: Again custom components APP_CONFIGglobalData and utils are injected.
  • Lines 5-10: 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.
    We remove the slash at the very begining, since we want a path relative to the rootPath as defined in the configuration for the carFinder prefix of the delivery endpoint.
  • Lines 12-14: 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.
  • Line 21, 22: 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:

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

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:

#trackbackRdf ($trackbackUtils.getContentIdentifier($page) $page.title $trackbackUtils.getPingUrl($page))