Developing a Headless Vue.js Application with Magnolia
Magnolia in action
Take 12 minutes and a coffee break to discover how Magnolia can elevate your digital experience.
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.
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: 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:
!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
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
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:
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: 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:
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="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:
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="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:
<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:
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)
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: