Headless Magnolia: Building a Basic SPA Using Magnolia Templates
Feb 8, 2022
--
Headless Magnolia: Building a Basic SPA Using Magnolia Templates

Headless Magnolia: Building a Basic SPA Using Magnolia Templates

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:

1_Magnolia_home

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:

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

2_add_page

When you open the “spa-home” page for editing, you should see your SPA.

3_SPA_page

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

Java
  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

Java
  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

Java
  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

Java
  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

Java
  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

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

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

4_sample_page1

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

Java
  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,
 },
};
5_sample_page2

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:

  1. The Delivery Endpoint API

  2. REST Endpoint Security and CORS

  3. Configuring SPAs for Editing in Magnolia

  4. Building a Basic SPA Using Magnolia Templates (you are here)

  5. Mapping Virtual URIs

  6. Nodes Endpoint API and Commands Endpoint API

  7. Inheritance in Headless Magnolia

About the author

Bartosz Staryga

Front-End Solution Architect, Magnolia

Bartosz is an expert in headless content management and front-end development at Magnolia. He designs and develops new Magnolia features and supports customers with their headless implementations from content types to APIs to integrations. Bartosz enjoys building new things and seeing them in action. He is also a trainer for Magnolia’s Headless training.