How the Eleventy Blog Starter Works

The Eleventy Blog Starter showcases features such as our native Page Management and shows how you should structure your Eleventy website. This starter serves an example based on our recommended best-practices.pAGE 

We believe that Editors (you know, the folks actually creating content) should have full control over their website pages and not rely on a developer.

This means that editors in the CMS control what pages are available, what their URLs are, and exactly what UI components (we call these Page Modules) make up each page.

Routing and Pages

Page Generation

the following example is based on the Blog Starter instance. Don't have one? Sign up for one!

Page generation in Agility CMS

The above figure represents a sitemap of Pages as well as the UI Components (Page Modules) that are on each page - all are managed in the CMS. 

This means that when a build occurs in your Eleventy site, the following pages will be auto-generated for you:

/
/blog
/blog/* (your dynamic page route for all of your blog posts, i.e. /blog/my-first-post
/about

Page Rendering

When a Page is being generated, the final step is rendering the page to HTML. In the agilitycms-eleventy-starter, we use an index.njk file that takes care of generating pages based on your Sitemap in Agility, and uses a layout template that determines which Page Template to render, and handles your SEO properties.

Page Data

First, we must get all of the Pages in our Sitemap from Agility, and export them for Eleventy.

Sample source code for src/_data/agilitypages.js:

const {
  getSyncClient,
  agilityConfig,
} = require("../../agility/agility.config");

async function getAgilityContent() {
  const languageCode = agilityConfig.languageCode;
  const channelName = agilityConfig.channelName;
  const isPreview = agilityConfig.isPreviewMode;

  if (isPreview) {
    console.log("Agility CMS => Building site in preview mode.");
  } else {
    console.log("Agility CMS => Building site in live mode.");
  }

  const syncClient = getSyncClient({ isPreview });

  let sitemap = await syncClient.store.getSitemap({
    channelName,
    languageCode,
  });

  if (!sitemap) {
    console.warn(
      "Agility CMS => No Sitemap Found - try running a sync (npm run cms-pull)"
    );
  }

  let pages = [];
  for (const key in sitemap) {
    let node = sitemap[key];

    if (node.isFolder || node.redirect) {
      continue;
    }

    //get the page for this sitemap object
    let agilitypage = await syncClient.store.getPage({
      pageID: node.pageID,
      languageCode,
      contentLinkDepth: 3,
    });

    //the first page in the sitemap is always the home page
    if (pages.length === 0) {
      node.path = "/";
    }

    agilitypage.sitemapNode = node;

    //resolve the page template
    if (agilitypage.templateName !== undefined && agilitypage.templateName) {
      agilitypage.templateFileName = `${agilitypage.templateName
        .replace(/ /gi, "-")
        .toLowerCase()}`;
    }

    //if this is a dynamic page item, get the content for it
    agilitypage.dynamicPageItem = null;
    if (node.contentID !== undefined && node.contentID > 0) {
      const dynamicPageItem = await syncClient.store.getContentItem({
        contentID: node.contentID,
        languageCode,
        contentLinkDepth: 2,
      });

      if (dynamicPageItem) {
        agilitypage.title = dynamicPageItem.fields.title;
        if (dynamicPageItem.seo) {
          agilitypage.seo.metaDescription = dynamicPageItem.seo.metaDescription;
          agilitypage.seo.metaKeywords = dynamicPageItem.seo.metaDescription;
        }

        if (dynamicPageItem.properties.definitionName === "Post") {
          // dynamic page item category
          dynamicPageItem.category =
            dynamicPageItem.fields?.category?.fields.title || "Uncategorized";

          // dynamic page item formatted date
          dynamicPageItem.date = new Date(
            dynamicPageItem.fields.date
          ).toLocaleDateString();

          // if we have an image field, use it as seo og:image
          if (dynamicPageItem.fields.image) {
            agilitypage.seo.ogImage = `${dynamicPageItem.fields.image.url}?w=1024`;
          }
        }

        agilitypage.dynamicPageItem = dynamicPageItem;
      }
    }

    pages.push(agilitypage);
  }

  return pages;
}

// export for 11ty
module.exports = getAgilityContent;

Configure Our Index Template

Then we must configure our Index template to tell Eleventy of our Page Data, and what layout we want to use to render our pages.

Sample source code for src/index.njk:

...
pagination:
    data: agilitypages
    size: 1
    alias: agilitypage
	addAllPagesToCollections: true
permalink: "{{ agilitypage.sitemapNode.path }}/"
layout: page-layout.njk
...

Page Modules

Page Modules in Agility CMS are the functional components that make up a page. Editors use these to compose what type of content is on each page and in what order they appear.

Developers define what page modules are available in the CMS and what fields they have. Each module defined in Agility CMS should correspond to a Nunjuck Template in your Eleventy site.

Generally speaking, if you don't have any modules defined or editors don't add modules to pages, then there won't be anything to output for your pages.

What is in a Page Module?

A Page Module in Agility CMS has a name, description, and fields.

The Name is a representation of the module's intended functionality, such as Posts Listing or Rich Text Area.

It is recommended that each module gets a b, this helps users understand when and where to use this definition.

Fields represent the content that can be managed by editors within the module. These fields are then used in code (passed as props) to display the content the editor intended.

An Example

In the agilitycms-eleventy-starter site, the name of the page module is used to find a corresponding Nunjuck template that matches the same name. If a match is found, that component is dynamically imported and rendered.

For example, if a module has a reference name of RichTextArea in the CMS, then while the page is being rendered, it will look for ./src/_includes/agility-pageModules/RichTextArea.njk in the ./src/_includes/agility-pageModules directory.

Agility CMS page modules

Internally, the render-modules.njk component will dynamically import the module and pass all the field values for that module as props.

{# set module fields  #}
{% set item = module.item.fields %}
<div class="relative px-8">
  <div class="max-w-2xl mx-auto my-12 md:mt-18 lg:mt-20">
    <div class="prose max-w-full mx-auto">{{ item.textblob | safe }}</div>
  </div>
</div>

If there is no Nunjuck template for your module, then nothing can be rendered for it on your Eleventy site.

How to Change the Fields

You can alter your fields at any time and the field values passed to your component will update automatically the next time you run another build.

Fetch Additional Data in Page Modules

When using Page Modules, the fields of the module are passed down to the Nunjuck component as props. However, you may run into a scenario where you want to fetch additional data into your Page Modules, such as information from a Content List or Item in Agility.

An example can be seen here on the Post Listing Page Module in the agilitycms-eleventy-starter.

Fetching the Data

In the ./src/_data directory, create a file, then import and set up the Agility Sync Client to utilize the Agility fetch methods:

const {
  getSyncClient,
  agilityConfig,
} = require("../../agility/agility.config");

async function getAgilityContent() {
  const languageCode = agilityConfig.languageCode;
  const isPreview = agilityConfig.isPreviewMode;

  const syncClient = getSyncClient({ isPreview });

  // get posts
  let posts = await syncClient.store.getContentList({
    referenceName: "posts",
    languageCode,
  });

  // get categories
  let categories = await syncClient.store.getContentList({
    referenceName: "categories",
    languageCode,
  });

  if (!posts) return {};

  return posts.map((p) => {
    // categoryID
    const categoryID = p.fields?.category?.contentid;

    // find category
    const category = categories?.find((c) => c.contentID == categoryID);

    // category
    p.category = category?.fields?.title || "Uncategorized";

    // date
    p.date = new Date(p.fields.date).toLocaleDateString();

    // title
    p.title = p.fields.title;

    // slug
    p.slug = p.fields.slug;

    // image
    p.image = p.fields.image;

    return p;
  });
}

// export for 11ty
module.exports = getAgilityContent;

posts.js

Using the Data in your Page Module

To use the Data in your Page Module, we can now call on the posts object/array:

{% if posts.length >= 1 %}
<div class="relative px-8 mb-12">
  <div class="max-w-screen-xl mx-auto">
    <div class="sm:grid sm:gap-8 sm:grid-cols-2 lg:grid-cols-3">
        {% for post in posts %}
          <a href="{{ post.slug }}">
            <div class="flex-col group mb-8 md:mb-0">
              <div class="relative h-64">
                <img
                  src="{{ post.image.url }}"
                  alt="{{ post.image.label }}"
                  class="object-cover object-center rounded-t-lg"
                  style="width: 100%; height: 100%;"
                />
              </div>
              <div class="bg-gray-100 p-8 border-2 border-t-0 rounded-b-lg">
                <div class="uppercase text-primary-500 text-xs font-bold tracking-widest leading-loose">
                  {{ post.category }}
                </div>
                <div class="border-b-2 border-primary-500 w-8"></div>
                <div class="mt-4 uppercase text-gray-600 italic font-semibold text-xs">
                  {{ post.date }}
                </div>
                <h2 class="text-secondary-500 mt-1 font-black text-2xl group-hover:text-primary-500 transition duration-300">
                  {{ post.title }}
                </h2>
              </div>
            </div>
          </a>
      {% endfor %}
    </div>
  </div>
</div>
{% else %}
<div class="mt-44 px-6 flex flex-col items-center justify-center">
  <h1 class="text-3xl text-center font-bold">No posts available.</h1>
    <div class="my-10">
      <a href="/" class="px-4 py-3 my-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-primary-600 hover:bg-primary-500 focus:outline-none focus:border-primary-700 focus:shadow-outline-primary transition duration-300">
        Return Home
      </a>
  </div>
</div>
{% endif %}

Page Templates

Page Templates in Agility CMS are how developers can differentiate the styles of certain types of pages, and define where on the page that editors can add Page Modules (functional components of the page that editors control).

Some sites may only have a single page template and this is re-used across the site, others may have other templates to allow for more flexible layouts.

What's in a Page Template?

When editors create pages in Agility CMS, they must select which template they'd like to use.

A Page Template consists of a Name and Content Zones.

The Name should represent what the editor can expect from using the Page Template. For example, a site may have templates named One Column TemplateTwo Column Template, or Blog Template.

Content Zone is an area defined in the Page Template where an editor can add, edit, or remove modules. A Page Template can have one or many Content Zones.

An Example

In the agilitycms-eleventy-starter site, the Name of the Page Template is used to find a corresponding Nunjuck component that matches the same name. If a match is found, that component is dynamically imported and rendered.

For example, if a Page Template has a reference name of Main Template in the CMS, then while the page is being rendered, it will look for ./src/_includes/agility-pageTemplates/main-template.njk in the ./src/_includes/agility-pageTemplates directory.

Reviewing the example page-layout.njk file from our agilitycms-eleventy-starter site, the include will automatically take care of resolving and rendering the appropriate page template.

Main-template in Agility CMS with Eleventy&nbsp;

From page-layout.njk:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>{{ agilitypage.title }} | My Travel Blog</title>
    <meta charset="UTF-8"/>
	  <meta name="generator" content="Agility CMS" />
	  <meta name="description" content="{{agilitypage.seo.metaDescription}}"/>
    <meta name="keywords" content="{{agilitypage.seo.metaKeywords}}"/>
    <link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
    {% if agilitypage.seo.ogImage %}
		  <meta property="og:image" content="{{agilitypage.seo.ogImage}}" />
    {% endif %}
    <meta http-equiv="x-ua-compatible" content="ie=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
	<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
	<link rel="stylesheet" href="/styles.css">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap"/>
	<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>
  </head>
  <body>
  {% include "./common/preview-bar.njk" %}
  <div class="flex flex-col min-h-screen">
    {% include "./common/SiteHeader.njk" %}
    <main class="flex-grow">
      {% include "./agility-pageTemplates/" + agilitypage.templateFileName + ".njk" %}
    </main>
    {% include "./common/SiteFooter.njk" %}
  </div>
  </body>
</html>

From main-template.njk:

<div>  {% set zoneName = "MainContentZone" %}
  {% include  "../render-modules.njk" %}
</div>

Therender-modules.njk file will handle resolving what modules should be on the page:

{# Loop all the zones #}
{% for zone, modules in agilitypage.zones %}
	{# Only render the current zone name #}
	{% if zone === zoneName %}
		{% for module in  modules  %}
			{# Render the module based on the module reference name  -ignoring missing module files... #}
			{% include  "./agility-pageModules/" + module.module.toLowerCase() + ".njk"  ignore missing %}
		{% endfor %}
	{% endif %}
{% endfor %}

If there is no corresponding Nunjuck template for your Page Template, then nothing can be rendered for it on your Eleventy site.

How to Add a Content Zone

You can alter your content zones at any time, you'll simply have to set the Zone Name within your Page Template Nunjuck component, and utilize the render-modules.njk component to render the modules of that zone.