Headless Magnolia: Building a Basic SPA Using Magnolia Templates
Magnolia in just 12 minutes
Why not learn more about what Magnolia can do while you have a coffee?
If you want to learn about Magnolia’s take on headless content management but don't know where to start, you've come to the right place. As part of our “Headless Magnolia” series, we’ll break down relevant features that allow you to use Magnolia as a headless CMS.
In this article, we'll create a basic Single Page Application (SPA) that you can edit in Magnolia. Before installing Magnolia locally read about the benefits of SPAs and when it is recommended to use them.
Installing Magnolia Locally
To create and start a Magnolia instance on your local machine, please follow our documentation.
Once complete, you should be able to log into Magnolia at http://localhost:8080/magnoliaAuthor/, seeing a brand new Magnolia installation:
Creating the SPA Light Module
Now, download our SPA Light Module that we’ve prepared for you. Unpack the archive and move the spa directory into the light-modules folder of your local Magnolia instance.
The Light Module contains YAML definitions for 1 page, 3 components, and 1 delivery endpoint:
spa
└── dialogs
| └── components
| | └── Item.yaml
| | └── Text.yaml
| └── pages
| └── Home.yaml
└── restEndpoints
| └── delivery
| └── pages.yaml
└── templates
└── components
| └── Item.yaml
| └── List.yaml
| └── Text.yaml
└── pages
└── Home.yaml
I recommend you inspect all YAML definitions to familiarize yourself with the configuration.
Setting Up the Initial SPA
Outside the Magnolia folder, create and start a basic project with React, Angular, or Vue.
React
npx create-react-app magnolia-spa
npm start
Angular
ng new magnolia-spa
ng serve
Vue
vue create magnolia-spa
npm run serve
In the page definition at /spa/templates/pages/Home.yaml set the baseUrl to your SPA server, for example, http://localhost:3000 for React.
Then create a new page in the Magnolia Pages App using the “SPA Home” template. Name it “spa-home”.
When you open the “spa-home” page for editing, you should see your SPA.
Magnolia Front-End Helpers for SPAs
To enable SPA editing in Magnolia, you have to use a front-end helper. Magnolia provides 3 helpers out of the box:
All helpers export 3 wrapping components:
EditablePage is the wrapping component for pages
EditableArea is the wrapping component for areas
EditableComponent the wrapping component for components
The Visual SPA Editor is here
Next-level headless streamlines how marketing and developer teams work and create together.
Managing your SPA in Magnolia
For starters, let’s make sure anonymous users have access to the pages endpoint. To do so, follow the instructions in my previous blog post in this series Headless Magnolia: REST Endpoint Security and CORS.
Next, install the Magnolia connector for your framework:
npm install @magnolia/react-editor
npm install @magnolia/angular-editor
npm install @magnolia/vue-editor
Now, create and modify your SPA project.
React
File: /magnolia-spa/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { EditablePage } from '@magnolia/react-editor';
import Home from './pages/Home';
const config = {
componentMappings: {
'spa:pages/Home': Home,
},
};
class App extends React.Component {
state = {};
async componentDidMount() {
const nodeName = '/spa-home';
const pagePath = nodeName + window.location.pathname.replace(new RegExp('(.*' + nodeName + '|.html)', 'g'), '')
const isPagesApp = window.location.search.includes('mgnlPreview');
let templateAnnotations;
const pageRes = await fetch('http://localhost:8080/magnoliaAuthor/.rest/delivery/pages' + pagePath);
const page = await pageRes.json();
if (isPagesApp) {
const templateAnnotationsRes = await fetch(
'http://localhost:8080/magnoliaAuthor/.rest/template-annotations/v1' + pagePath
);
templateAnnotations = await templateAnnotationsRes.json();
}
this.setState({ page, templateAnnotations });
}
render() {
const { page, templateAnnotations } = this.state;
return (
<div className='App'>
{page && config && <EditablePage content={page} config={config} templateAnnotations={templateAnnotations} />}
</div>
);
}
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
File: /magnolia-spa/src/pages/Home.js
import React from 'react';
import { EditableArea } from '@magnolia/react-editor';
function Home(props) {
const { title, main } = props;
return (
<div>
<h1>{title}</h1>
{main && <EditableArea content={main}/>}
</div>
);
}
export default Home;
Angular
File: /magnolia-spa/src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { MagnoliaModule } from '@magnolia/angular-editor';
import { HomeComponent } from './pages/home.component';
@NgModule({
declarations: [AppComponent, HomeComponent],
imports: [BrowserModule, MagnoliaModule],
providers: [],
bootstrap: [AppComponent],
entryComponents: [HomeComponent],
})
export class AppModule {}
File: /magnolia-spa/src/app/app.component.ts
import { Component, Input } from '@angular/core';
import { EditorContextService } from '@magnolia/angular-editor';
import { HomeComponent } from './pages/home.component';
const config = {
componentMapping: {
'spa:pages/Home': HomeComponent,
},
};
@Component({
selector: 'app-root',
template: `<editable-page [content]="content"></editable-page>`,
styles: [],
})
export class AppComponent {
@Input() content: any;
constructor(private editorContext: EditorContextService) {
this.editorContext.setComponentMapping(config.componentMapping);
this.getContent();
}
async getContent() {
const nodeName = '/spa-home';
const pagePath = nodeName + window.location.pathname.replace(new RegExp('(.*' + nodeName + '|.html)', 'g'), '')
const isPagesApp = window.location.search.includes('mgnlPreview');
const contentRes = await fetch('http://localhost:8080/magnoliaAuthor/.rest/delivery/pages' + pagePath);
const content = await contentRes.json();
if (isPagesApp) {
const templateAnnotationsRes = await fetch(
'http://localhost:8080/magnoliaAuthor/.rest/template-annotations/v1' + pagePath
);
const templateAnnotations = await templateAnnotationsRes.json();
this.editorContext.setTemplateAnnotations(templateAnnotations);
}
this.content = content;
}
}
File: /magnolia-spa/src/app/pages/home.component.ts
import { Component, Input } from '@angular/core';
@Component({
template: `<div>
<h1>{{ title }}</h1>
<div editable-area [content]="main"></div>
</div>`,
})
export class HomeComponent {
@Input() title: any;
@Input() main: any;
}
Vue
File: /magnolia-spa/src/App.vue
<template>
<EditablePage
v-if="page"
v-bind:content="page"
v-bind:config="config"
v-bind:templateAnnotations="templateAnnotations"
/>
</template>
<script>
import { EditablePage } from "@magnolia/vue-editor";
import Home from './pages/Home.vue'
const config = {
componentMappings: {
'spa:pages/Home': Home,
},
};
export default {
name: 'App',
components: {
EditablePage
},
data() {
return {
page: null,
templateAnnotations: {},
config,
};
},
async mounted() {
const nodeName = '/spa-home';
const pagePath = nodeName + window.location.pathname.replace(new RegExp('(.*' + nodeName + '|.html)', 'g'), '')
const isPagesApp = window.location.search.includes('mgnlPreview');
const pageRes = await fetch('http://localhost:8080/magnoliaAuthor/.rest/delivery/pages' + pagePath);
const page = await pageRes.json();
if (isPagesApp) {
const templateAnnotationsRes = await fetch(
'http://localhost:8080/magnoliaAuthor/.rest/template-annotations/v1' + pagePath
);
const templateAnnotations = await templateAnnotationsRes.json();
this.templateAnnotations = templateAnnotations;
}
this.page = page;
}
}
</script>
File: /magnolia-spa/src/components/HelloWorld.vue
<template>
<div>
<h1>{{ title }}</h1>
<EditableArea v-if="main" v-bind:content="main" />
</div>
</template>
<script>
import { EditableArea } from '@magnolia/vue-editor';
export default {
name: "Home",
components: {
EditableArea
},
props: ["title", "main"]
};
</script>
When looking at this code, you might notice that all implementations have 3 things in common:
fetching the page content
fetching template annotations
component mappings
The starting point for each implementation is an editable page. EditablePage consumes the content of the page from the pages endpoint that we defined in the Light Module.
Notice how the JavaScript code builds the pagePath using a regular expression and the name of the root directory “spa-home”.
The config object componentMappings is responsible for the mappings between the page and component templates in Magnolia, mgnl:template, and the SPA components.
When a page is displayed in the Magnolia Pages App, the code fetches and sets the template annotations to render the green editing bars in Magnolia’s Visual SPA Editor.
The template annotations endpoint that is needed to provide the annotations data is created by Magnolia automatically.
The page, area, and component properties that you defined in the dialog definitions are exposed as properties in your SPA components.
When your setup is complete, you should have a basic page.
Expand Your Project
Try to add list and text components in the main area of the page. Make sure to create the SPA components to render these components and map them in componentMappings, for example:
File: /magnolia-spa/src/components/Text.js
import React from 'react';
function Text(props) {
const { text } = props;
return <div>{text}</div>;
}
export default Text;
File: /magnolia-spa/src/index.js
import Text from './components/Text';
const config = {
componentMappings: {
'spa:pages/Home': Home,
'spa:components/Text': Text,
},
};
That’s it. You created a basic SPA using Magnolia templates.
You can keep expanding your project by creating new page and component templates in the Light Module and mapping them to your SPA.
Learn more about Magnolia’s headless features
This Headless Magnolia series will cover the following topics:
Building a Basic SPA Using Magnolia Templates (you are here)
Nodes Endpoint API and Commands Endpoint API