Developing a Headless Vue.js Application with Magnolia
Sep. 1, 2021
--
Vue

Developing a Headless Vue.js Application with Magnolia

When developing digital experiences in a headless architecture, Magnolia offers a unique way for marketers to design pages visually in a WYSIWYG editor while templates and application logic are managed by a decoupled front-end, such as a SPA or PWA.

While it was already possible to use our visual editor with any front-end framework, it required more effort to make it work with anything but React and Angular. With Vue.js becoming one of the most popular JavaScript frameworks for developing front-end applications, we decided it was time to offer an even easier way to integrate Vue.js.

In this blog, I will introduce the vue-editor library and demonstrate how to create a Vue.js application that fetches pages from Magnolia via REST API.

Whenever you need help, you can refer to my code in the sample project.

Setting up a front-end project

Please make sure you’ve installed NodeJS from https://nodejs.org before you proceed.

You will use the Vue CLI to create a Vue project called magnolia-vue-site. Install it and when it asks for the Vue version, choose Vue 3.

Java
  npm install -g @vue/cli
vue create magnolia-vue-site
cd magnolia-vue-site

Then install @magnolia/vue-editor as well as rimraf and copyfiles to help copy compiled code to a Magnolia Light Module.

Java
  npm install @magnolia/vue-editor
npm install -D rimraf copyfiles

After you create the project, you can use npm run serve to start the development server generated by the Vue CLI.

Configuring the Vue server port and REST URLs

The default ports for the Magnolia and Vue server are both 8080, so you need to change one of them.

For this tutorial, set the Vue port to 3000 and configure the publicPath and lintOnSave.

Create the vue.config.js in the root folder magnolia-vue-site:

Java
  module.exports = {
  devServer: {
    port: 3000
  },
  publicPath: `${process.env.VUE_APP_MGNL_BASE}${process.env.VUE_APP_MGNL_STATIC}`,
  lintOnSave: false
};

publicPath tells Vue to append this path to links in index.html when generating the file. Setting lintOnSave to false means I don’t want the Vue IDE extension to check ESLint rules when I save a file.

The above also uses the parameters VUE_APP_MGNL_BASE and VUE_APP_MGNL_STATIC which come from the .env file.

So, let’s create .env in the root folder using the following parameters that are needed when generating index.html and making REST calls in the application:

Java
  VUE_APP_MGNL_HOST=http://localhost:8080

VUE_APP_MGNL_BASE=/magnoliaAuthor

VUE_APP_MGNL_API_TEMPLATES=/.rest/template-annotations/v1
VUE_APP_MGNL_API_PAGES=/.rest/delivery/pages/v1
VUE_APP_MGNL_STATIC=/.resources/vue-gallery/webresources/dist
VUE_APP_MGNL_GALLERY=/.rest/delivery/gallery/v1

Building the project

Add two scripts in the package.json file and modify the build script:

Java
  "clean": "rimraf dist && rimraf light-modules/vue-gallery/webresources/dist",
"deploy": "npm run build && copyfiles -u 1 \"dist/**/*\" light-modules/vue-gallery/webresources/dist"
"build": "npm run clean && vue-cli-service build",

The clean script deletes compiled code from the dist and webresources folders and the deploy script builds and then copies the dist folder to the webresources folder.

Installing Magnolia

Please follow the documentation to install Magnolia. If you are not familiar with Light Development in Magnolia, I also recommend reading up on it before continuing to the next section.

Creating a Magnolia Light Module

Magnolia can be configured through YAML files in so-called "Light Modules". Create a light-modules folder in the root folder and a vue-gallery3 Light Module using the Magnolia CLI:

Java
  mgnl create-light-module vue-gallery  

Creating a Content App

Next, create a ‘photo’ Content Type and the Content App:

Java
  cd vue-gallery
mgnl create-app photo

Edit the Content Type definition in /contentTypes/photo.yaml:

Java
  # Automatically generated contentType demonstrates usage of the common properties.
# Modify them to match your requirements.
datasource:
    workspace: gallery

    # Optionally configure a custom namespace. (Replace [myNamespace] everywhere.)
    # This namespace can then be used below for the nodetype.
    namespaces:
      mt: https://www.magnolia-cms.com/jcr/1.0/mt
    autoCreate: true

model:
    # Optionally supply a specific nodetype, otherwise 'mgnl:content' will be used.
    nodeType: mt:gallery
    properties:
    - name: title
      label: Title
      type: String
      required: true
      i18n: true

    - name: description
      label: Description
      type: String

    - name: image
      label: Image
      type: asset

Edit the Content App definition in /contentApps/photo.yaml:

Java
  !content-type:photo
name: Photos

When you login to Magnolia now, you should see the Photos App and can add data to the Photos App and Assets App to test the REST endpoints in the next step.

If you want to, you can import data from the content-import folder from my source code for the gallery endpoint.

Registering the REST endpoints

Register two REST endpoints, one is for pages and one for photos:

/restEnpoints/delivery/pages_v1.yaml

Java
  class: info.magnolia.rest.delivery.jcr.v2.JcrDeliveryEndpointDefinition
workspace: website
nodeTypes:
  - mgnl:page
includeSystemProperties: true
bypassWorkspaceAcls: true
limit: 50
depth: 10

references:
  - name: image
    propertyName: image
    referenceResolver:
      class: info.magnolia.rest.reference.dam.AssetReferenceResolverDefinition
      assetRenditions:
        - '480'
        - 1600x1200

/restEnpoints/delivery/gallery_v1.yaml

Java
  class: info.magnolia.rest.delivery.jcr.v2.JcrDeliveryEndpointDefinition
workspace: gallery
nodeTypes:
  - mt:gallery
includeSystemProperties: true
bypassWorkspaceAcls: true
limit: 50
depth: 10

references:
  - name: image
    propertyName: image
    referenceResolver:
      class: info.magnolia.rest.reference.dam.AssetReferenceResolverDefinition
      assetRenditions:
        - '480'
        - 1600x1200

Use the Definitions app to check if the new endpoints got registered in Magnolia:

Definitions_gallery

You can also check if the below URLs are working correctly:

Creating templates

Create a page template and three component templates. Check out our sample project for templates that you can use.

Create a page template in /templates/pages/standard.yaml:

Java
  class: info.magnolia.rendering.spa.renderer.SpaRenderableDefinition
renderType: spa
visible: true
dialog: mte:pages/pageProperties
templateScript: /vue-gallery/webresources/dist/index.html

areas:
  header:
    title: Header
    type: single
    availableComponents:
      header:
        id: vue-gallery:components/page-header
  main:
    renderType: spa
    title: Main
    availableComponents:
      gallery:
        id: vue-gallery:components/gallery
  footer:
    renderType: spa
    title: Footer
    type: single
    availableComponents:
      footer:
        id: vue-gallery:components/page-footer

The three component definitions don’t have any content. Create three empty files, so Magnolia can register these components.

To keep it simple, we won’t create any dialogs.

Creating the Vue components

The Vue CLI has already created main.js, App.vue, and components/HelloWorld.vue for your.

Configuring the App.vue

In this tutorial, you will use a bootstrap template from https://getbootstrap.com/docs/5.0/examples/album/. Install bootstrap and import the bootstrap CSS:

Java
  npm install bootstrap  

In the App.vue file, add the below lines:

Java
  import "../node_modules/bootstrap/dist/css/bootstrap.css";
import "../node_modules/bootstrap/dist/js/bootstrap.js";

Next, import EditablePage from @magnolia/vue-editor and edit the template.

Java
  <template>
 <editable-page
   v-if="content && templateAnnotations"
   :content="content"
   :templateAnnotations="templateAnnotations"
   :config="config"
 />
</template>

EditablePage required three parameters: content, templateAnnotations and config.

  • content is a page object that we need to fetch from the pages endpoint.

  • templateAnnotations is an object from the template annotation endpoint.

  • config is an object that has an attribute componentMappings that I will create later.

Below is the code to initialize parameters for the EditablePage:

Java
  import "../node_modules/bootstrap/dist/css/bootstrap.css";
import "../node_modules/bootstrap/dist/js/bootstrap.js";
import { EditablePage } from '@magnolia/vue-editor';
import componentMappings from './mappings';
import config from './config';
 
function removeExtension(path) {
 let newPath = path;
 if (path.indexOf('.') > -1) {
   newPath = path.substr(0, path.lastIndexOf('.'));
 }
 return newPath;
}
 
export default {
 name: "App",
 components: {
   EditablePage
 },
 data() {
   return {
     content: null,
     templateAnnotations: null,
     config: {
       componentMappings
     }
   };
 },
 methods: {
   async loadPage() {
     const { pathname } = window.location;
     const path = config.BASE ? pathname.substr(config.BASE.length) : pathname;
     const url = `${config.HOST}${config.BASE}${config.PAGES}${removeExtension(path)}`;
     const pageRes = await fetch(url);
     const data = await pageRes.json();
     this.content = data;
 
     const annotationUrl = `${config.HOST}${config.BASE}${config.ANNOTATIONS}${removeExtension(path)}`;
     const annotationRes = await fetch(annotationUrl);
     const annotationData = await annotationRes.json();
     this.templateAnnotations = annotationData;
   }
 },
 mounted() {
   this.loadPage();
 }
};

loadPage must be called after mounted because vue-editor only works after Vue rendered the HTML in the document.

You also need to configure the permissions to call the template annotations endpoint. Check the documentation for the default configuration.

Creating the configuration object

In the App.vue component, use a config object that stores parameters to generate URLs for fetching data from REST endpoints.

Create /config.js:

Java
  const HOST = process.env.VUE_APP_MGNL_HOST || 'http://localhost:8080';
const BASE = process.env.VUE_APP_MGNL_BASE || '/magnoliaAuthor';
const ANNOTATIONS = process.env.VUE_APP_MGNL_API_TEMPLATES || '/.rest/template-annotations/v1';
const PAGES = process.env.VUE_APP_MGNL_API_PAGES || '/.rest/delivery/pages/v1';
const GALLERY = process.env.VUE_APP_MGNL_GALLERY || '/.rest/gallery/gallery/v1';
 
export default {
 HOST,
 BASE,
 ANNOTATIONS,
 PAGES,
 GALLERY
};

Creating a page component to display the standard page template

The App.vue is now ready, so create a page component using the header, main, and footer parameters from EditablePage.

The EditablePage renders a page component dynamically and passes all areas from the template definition to the dynamic page component.

Create StandardPage.vue in the src/components folder:

Java
  <template>
 <header>
   <editable-area :content="header"></editable-area>
 </header>
 <main>
   <section class="py-5 text-center container">
     <div class="row py-lg-5">
       <div class="col-lg-6 col-md-8 mx-auto">
         <h1 class="fw-light">Album example</h1>
         <p class="lead text-muted">
           Something short and leading about the collection below—its contents,
           the creator, etc. Make it short and sweet, but not too short so
           folks don’t simply skip over it entirely.
         </p>
         <p>
           <a
             href="https://getbootstrap.com/docs/5.0/examples/album/#"
             class="btn btn-primary my-2"
             >Main call to action</a
           >
           <a
             href="https://getbootstrap.com/docs/5.0/examples/album/#"
             class="btn btn-secondary my-2"
             >Secondary action</a
           >
         </p>
       </div>
     </div>
   </section>
 
   <div class="album py-5 bg-light">
     <editable-area :content="main"></editable-area>
   </div>
 </main>
 <editable-area :content="footer"></editable-area>
</template>
 
<script>
import { EditableArea } from '@magnolia/vue-editor';
 
export default {
 name: 'StandardPage',
 components: { EditableArea },
 props: ['header', 'main', 'footer']
};
</script>

The StandardPage imports EditableArea from @magnolia/vue-editor and has three properties: header, main and footer. The template specifies each area in the page layout.

Creating component mappings

Now, create the component mappings that map the StandardPage component to the page template ID in src/mappings.js:

Java
  import StandardPage from './components/StandardPage.vue';
export default {
 'vue-gallery:pages/standard': StandardPage
};

Run npm run deploy and check the result in Magnolia.

Editor_album

Creating the card component

Create a card component in src/components/Card.vue with a photo property that you can pass a photo object to:

Java
  <template>
 <div class="card shadow-sm">
   <svg
     class="bd-placeholder-img card-img-top"
     width="100%"
     height="280"
     xmlns="http://www.w3.org/2000/svg"
     role="img"
     aria-label="Placeholder: Thumbnail"
     preserveAspectRatio="xMidYMid slice"
     focusable="false"
   >
     <title>Placeholder</title>
     <rect width="100%" height="100%" fill="#55595c"></rect>
     <text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text>
     <image :href="hostname + photo.image.renditions['480'].link" width="100%" height="100%" ></image>
   </svg>
 
   <div class="card-body">
     <p class="card-text">
       This is a wider card with supporting text below as a natural lead-in to
       additional content. This content is a little bit longer.
     </p>
   </div>
 </div>
</template>
 
<script>
import config from '../config';
 
export default {
 name: 'Card',
 props: ['photo'],
 setup() {
   return {
     hostname: config.HOST
   }
 }
};
</script>

Creating the gallery component

Next, create a gallery component in src/components/Gallery.vue to fetch photos from the gallery endpoint:

Java
  <template>
 <div class="container">
       <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
         <div class="col" v-for="photo in photos" :key="photo['@id']">
           <card :photo="photo"></card>
         </div>
       </div>
     </div>
</template>
 
<script>
import Card from './Card.vue'
import config from '../config';
export default {
 name: 'Gallery',
 data() {
   return {
     photos: []
   }
 },
 components: { Card },
 methods: {
   async loadData() {
     const url = `${config.HOST}${config.BASE}${config.GALLERY}`;
     const res = await fetch(url);
     const data = await res.json();
     this.photos = data.results;
   }
 },
 beforeMount() {
   this.loadData();
 }
}
</script>

You also have to create the HTML tags PageHeader and PageFooter. You can find the code in my project’s source. Note that I added a page prefix to avoid a conflict.

Then, map Gallery, PageHeader and PageFooter to mappings.js:

Java
  import StandardPage from './components/StandardPage.vue';
import Gallery from './components/Gallery.vue';
import PageHeader from './components/PageHeader.vue';
import PageFooter from './components/PageFooter.vue';
 
export default {
 'vue-gallery:pages/standard': StandardPage,
 'vue-gallery:components/gallery': Gallery,
 'vue-gallery:components/page-header': PageHeader,
 'vue-gallery:components/page-footer': PageFooter
};

You’ve now completed this tutorial and can customize your Vue templates to your needs.

Screenshots from the project

The gallery page in edit mode

Gallery_page_in_edit_mode

The gallery page that is hosted in Magnolia

Gallery_page

The gallery page on dev server (npm run serve)

Gallery_page_on_dev

Resources

You can find the project source code below:

We also have another sample project that you may be interested in:

If you’re not familiar with Magnolia Light Modules or the Pages Editor, you can check out these resources:

Über den autor

Dominic Nguyen

Senior Software Developer, Magnolia

Dominic is a senior software developer at Magnolia.