Back to the blog
  • Sep 1, 2021
  • 12 MIN
Category Tech
Developing a Headless Vue.js Application with Magnolia

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

  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.

  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:

  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:

  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:

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

  mgnl create-light-module vue-gallery  

Creating a Content App

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

  cd vue-gallery mgnl create-app photo  

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

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

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


  class: workspace: website nodeTypes:   - mgnl:page includeSystemProperties: true bypassWorkspaceAcls: true limit: 50 depth: 10 references:   - name: image     propertyName: image     referenceResolver:       class:       assetRenditions:         - '480'         - 1600x1200  


  class: workspace: gallery nodeTypes:   - mt:gallery includeSystemProperties: true bypassWorkspaceAcls: true limit: 50 depth: 10 references:   - name: image     propertyName: image     referenceResolver:       class:       assetRenditions:         - '480'         - 1600x1200  

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


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:

  class: 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 Install bootstrap and import the bootstrap CSS:

  npm install bootstrap  

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

  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.

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

  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:

  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:

  <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=""              class="btn btn-primary my-2"              >Main call to action</a            >            <a              href=""              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:

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

Run npm run deploy and check the result in Magnolia.


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:

  <template>  <div class="card shadow-sm">    <svg      class="bd-placeholder-img card-img-top"      width="100%"      height="280"      xmlns=""      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:

  <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(); = 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:

  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


The gallery page that is hosted in Magnolia


The gallery page on dev server (npm run serve)



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:

About the Author

Dominic Nguyen Senior Software Developer at Magnolia

Dominic is a senior software developer at Magnolia.

Read more