Distributing AEM Content to a JAMstack Site

In my previous post I provided some considerations for integrating a JAMstack app with a content management system. In this post I will follow up with how to use Adobe Experience Manager to distribute content to a JAMstack app. AEM will act as a headless CMS serving content to a site generator.

One of the many benefits of this setup is loosely coupling your enterprise content infrastructure from your web applications. JAMstack apps can be deployed and scaled cheaply and quickly allowing for quick to market products. Combining AEM with a JAMstack app gives you the best of both worlds; a powerful content management platform supporting powerful web apps built on the latest technology.

We will first setup a few simple schemas using AEM content fragments. This content will then be pulled into a site generator to drive the content of some predefined pages. Our site generator will use AEM’s default get servlet to create page content. Finally we will discuss how to setup the site with various deployment platforms. While this blog only demonstrates the basics of utilizing AEM as a headless CMS in a JAMstack app, it can serve as a starting point for creating blogs, news, product listings, and much more.

Setup the CMS

The first step is to setup AEM as a headless CMS using content fragments. While this configuration could be managed as code and deployed to an AEM environment, we’ll treat it as an administrative task in order to learn more about the setup of content fragments. Any time you see the phrase “AEM Start” that means going to the root url of your AEM author server such as http://localhost:4502.

Create the aemjam configuration

The first step is to create the configuration for our sample project, aemjam. AEM configurations allow you to do many things such as editable templates, contextual site configurations, and content fragment configurations.

  1. Go to AEM Start > Tools > General > Configuration Browser > Create
  2. Enter “aemjam” for the title
  3. Check the “Content Fragment Models” checkbox
  4. Click create

Create aemjam DAM Folder

Content fragments are stored in the AEM DAM and are simply treated as an asset like any image or pdf. We will want to create a DAM folder for storing all of our content fragments.

  1. Go to AEM Start > Assets > Files > Create > Folder
  2. Enter the title “aemjam”
  3. Click create

Apply aemjam configuration to aemjam DAM folder

Next we need to apply our aemjam configuration to our aemjam DAM folder so that we can create content fragments with custom schemas within this folder.

  1. Go to AEM Start > Assets > Files
  2. Edit the properties of the aemjam folder
  3. Go to the “Cloud Services” tab
  4. In the “Cloud Configuration” field select “aemjam”

Create content fragment models

The last step of setup is to actually create some custom content fragment models. These act as essentially a schema for your headless CMS. In our case we will create two models: Site and Page.

Site model

The site model will be a schema that represents the entirety of our JAMstack app. It will contain application metadata that can go in our apps <head> tag as well as in our web manifest. This way we can drive the metadata of our app from our CMS. This also allows us to manage multiple apps from AEM. Creating the bare minimum schema that we will need for a site is described below, but keep in mind that many more fields could be added to drive the color scheme of our app, global padding and margin definitions, mobile app icons, and much more.

  1. Go to AEM Start > Tools > Assets > Content Fragment Models > aemjam > Create
  2. Set the title to “Site” and then click “Open”.
  3. Add a single line text field with a field label of “Title” and a field name of “title”
  4. Add a single line text field with a field label of “Description and a field name of “description”
  5. Add a content reference field with the label “Hero Image” and a field name of “heroImage”
Page model

We will also need a page model. This schema will contain the information needed to implement a page, with the most interesting field being the “body”. This will be a rich text field which will be plugged into the content section of the pages within our app. Each page in our app will have a corresponding page content fragment so that authors can edit the content fragment which will update the JAMstack app.

  1. Go to AEM Start > Tools > Assets > Content Fragment Models > aemjam > Create
  2. Set the title to “Page” and then click “Open”.
  3. Add a single line text field with a field label of “Title” and a field name of “title”
  4. Add a multi line text field with a field label of “Body” and a field name of “body”
  5. Add a content reference field with the label “Hero Image” and a field name of “heroImage”

Create content fragments

Finally we need to use these models to create some content fragments. For the sake of this blog article we will create one site and two pages, a home page and an info page. Fill these content fragments with some lorem ipsum text and example images as desired so that we can see the content reflected in our app.

  1. Go to AEM Start > Assets > Files > aemjam > Create > Content Fragment > Site and enter “Example JAMstack Site” into the title field and edit the content fragment.
  2. Add a description
  3. Add an image using the We.Retail site assets

Follow these steps to create two “Page” content fragments with the title “Home” and “Info”. Author these content fragments with data for each field.

Integrate the JAMstack site

Now that our CMS is ready to go we need to create our JAMstack site. To do this we need to choose a static site generator to serve as our build tool. You can explore many options on StaticGen. While many will work for our purpose and they all have strengths and weaknesses, I have chosen OrisonJS for the purpose of this blog post.

To start open up a terminal and go to the directory where you would like to create your project. Install OrisonJS using NPM and then use the init command to create an aemjam project. Below are modified setup steps taken from the OrisonJS homepage.

nvm use 10
npm install -g orison
orison init aemjam
cd aemjam
npm install
npx orison build
npx orison serve

The final command serves the JAMstack site at http://localhost:3000. Navigate around the site in order to get a feel for what we will be customizing. Note that during development we will serve our site and so each page will be rendered at the time of the request. However in a production setup we will build and deploy our site as a set of HTML files and there necessary assets following a JAMstack architecture.

Connect to AEM

Now open up the aemjam project in your favorite text editor and have the terminal nearby. Navigate around the folder structure and see that the src folder contains the source contents of our app which get compiled into an easily deployable package of flat HTML files, again following a JAMstack architecture. Before we get started on the code changes we have some setup work to do to make our connection to AEM easier to manage.

Use dotenv to manage the connection to AEM

Install dotenv by running this command in the aemjam-app project directory:

npm install dotenv --save

Then add a “.env” on a new line of the “.gitignore” file. This will make sure that we do not commit our environment variables into source control. Now create a “.env” file in the root of the project with the below contents. This will give us environment variables that we can use to connect to AEM.

  • /.env
AEM_URL=http://localhost:4502
AEM_USERNAME=admin
AEM_PASSWORD=admin
Use node-fetch to connect to the AEM API

We will connect to AEM from the app using the node-fetch NPM library. This is a promise based HTTP library for Node.js that implements the browser fetch interface.

npm install node-fetch --save

Now that we have an easy to use library for making HTTP requests and environment variables for storing our connection information we can bring it all together. We will create an easy to use interface for retrieving content fragment JSON data from anywhere in our JAMstack app. To do this create a file at /src/aem.js and import both the node-fetch library and the dotenv library. Finally export a method which accepts an aemPath, makes the request to AEM, and returns a promise with the data of the content fragment. Notice that we are accessing the “data.master” property of the JSON which means we always access the master variant of the content fragment. We could make this configurable in the site in some manner, so that this specific channel (the JAMstack app) would use a different variant of the content fragment if available.

  • /src/aem.js
import fetch from 'node-fetch';
import dotenv from 'dotenv';

dotenv.config();

export const getJSON = aemPath =>
  fetch(process.env.AEM_URL + aemPath + '.infinity.json', {
    'headers': {
      'Authorization': 'Basic ' + Buffer.from(process.env.AEM_USERNAME + ":" + process.env.AEM_PASSWORD).toString('base64')
    }
  })
  .then(res => res.json())
  .then(json => json['jcr:content'].data.master);

Notice that we need an “aemPath” to the content fragment. From our previous work in setting up AEM we should have these three paths in AEM:

  1. /content/dam/aemjam/example-jamstack-site
  2. /content/dam/aemjam/home
  3. /content/dam/aemjam/info

We will need to add these paths to our JAMstack app as metadata so that our pages can pass these identifiers to our getJSON method. To do this update the following files in the aemjam project.

  • /src/pages/data.json
{
  "title": "Example JAMstack Site",
  "aemPath": "/content/dam/aemjam/home",
  "aemSitePath": "/content/dam/aemjam/example-jamstack-site"
}
  • /src/pages/info/data.json
{
  "title": "Info",
  "aemPath": "/content/dam/aemjam/info"
}

OrisonJS uses these JSON files as contextual metadata. We will use this contextual metadata to allow our pages to access AEM content fragments.

Connect the layout to the site content fragment

Finally we can begin implementing our JAMstack pages. As you follow along refresh http://localhost:3000 to see the changes reflected in the browser.

OrisonJS uses a layout which provides the top level structure of each page. The content of each particular page can be injected into the layout with ${context.page.html}. Also notice that we are using “context.root.data.aemSitePath” to get the AEM path to the Site content fragment. Then we pass this identifier to the getJSON method that we created earlier in order to get the JSON of the Site. The rest we leave mostly as is. Go ahead and update the layout file as shown below.

  • /src/pages/layout.js
import { html } from 'orison';
import header from '../partials/header.js';
import nav from '../partials/nav.js';
import footer from '../partials/footer.js';
import { getJSON } from '../aem.js';

export default async context => {
  const cfLayout = await getJSON(context.root.data.aemSitePath);

  return html`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>${cfLayout.title}</title>
        <script src="/app.js"></script>
        <link rel="stylesheet" type="text/css" href="/app.css">
      </head>
      <body>
        <header>
          <h1>${cfLayout.title}</h1>
        </header>
        ${nav(context.path, context.root)}
        <main>
          ${context.page.html}
        </main>
        ${footer()}
      </body>
    </html>
  `;
}

Implement the JAMstack pages

Now we’ll again use the getJSON but this time we will pass in the “context.data.aemPath” value in order to retrieve the page content fragment JSON data from AEM. This accesses the JSON files that we created earlier. We then use this JSON data to add in the page description and the body of the page. Notice how AEM already provides us the HTML of the body of the page, and so we simply pass this to the “unsafeHTML” method.

  • /src/pages/index.js
import { html, unsafeHTML } from 'orison';
import { getJSON } from '../aem.js';

export default async context => {
  const cfPage = await getJSON(context.data.aemPath)

  return html`
    <section>
      <div>${cfPage.description}<div>
      <div>${unsafeHTML(cfPage.body)}<div>
    </section>
  `;
}

Lastly we do almost the exact same thing for the info page but this time we do not use the content fragment description. Also we add the the title of the page directly instead of using the value from the content fragment. As you can see each page or page type within your JAMstack app can utilize the content fragment data in different ways.

  • /src/pages/info/index.js
import { html, unsafeHTML } from 'orison';
import { getJSON } from '../../aem.js';

export default async context => {
  const cfPage = await getJSON(context.data.aemPath)

  return html`
    <section>
      <h2>Info page</h2>
      <div>${unsafeHTML(cfPage.body)}<div>
    <section>
  `;
};

Now go ahead and checkout http://localhost:3000 and see the content that you authored in AEM showing up on your JAMstack site. It would also be worth going into AEM, updating the content fragments, and then seeing that reflected on http://localhost:3000.

Deployment

Now that you have your website implemented we will want to build and deploy our site. To build the project run the following command:

npm run build

This will build your JAMstack site at the /docs path. The docs directory can be deployed to any static hosting solution such as Netlify, Firebase Hosting, AWS, or any web server or CDN. If your hosting solution is also capable of building your project, such as Netlify, then it will need access to the environment variables from our local .env file in order to build the project.

Here are some of the benefits of this setup:

  • Lightning fast: you are only delivering flat HTML files to the end user. All the work happens upfront at build time with no server side logic or client side rendering when the user makes a request for a page.
  • Less runtime dependencies: Once the project is built it has no dependence or connectivity to AEM other than for asset delivery.
  • Secure deployment: All you are doing is deploying HTML, CSS, and JavaScript. This reduces the surface area for attacks and mistakes and makes regression testing predictable.
  • Progressive web app: Many JAMstack build tools such as OrisonJS provide service workers and web manifests for free which means that your site is installable and available offline by default.
  • For a more full list of the benefits refer to jamstack.org.

Next steps

  1. Blog: Create a blog content fragment type with a “slug” field and use a tag based servlet to deliver a list of blogs to the aemjam build process. Each blog content fragment should be built as an individual page within the aemjam app and it should use the “slug” field to generate the blog post url.
  2. Build Hook: Setup AEM to notify the hosting solution such as Netlify to rebuild the app when an update has been made.
  3. Continuous Integration: Setup your code hosting solution such as GitHub to notify your hosting solution such as Netlify to rebuild the app when a code update has been merged.
  4. Images: Use the heroImage field of the content fragments in order to add an image to your JAMstack page. Notice how your asset management system such as the AEM DAM needs to be available to your JAMstack site even after the build process, as it only contains the url of the image.
  5. Implement the same aemjam project but using another site generator such as Gatsby, Jekyll, or Hugo.

Conclusion

While we did not have the time to look into every detail of powering a JAMstack app with AEM we were able to go through the initial integration. Hopefully you can now see how this type of setup works, most importantly the difference between the business logic which generates the JAMstack pages at build time verses the flat files that are deployed and available at request time. Combined with some of the broader considerations discussed in my previous post you should be empowered to build JAMstack apps that are loosely coupled to your content infrastructure and yet integrated in a powerful way.

Leave a Reply

avatar
  Subscribe  
Notify of