Building a modular marketing landing page with Eleventy / Part 2

This article demonstrates how to build a modular landing page (like Gutenberg blocks) with in Eleventy. Featuring shared content, and multisite scenario.

Building a modular marketing landing page with Eleventy / Part 2

Recap

In the first part of the series we’ve configured Eleventy to use flexible modules which bring all their own layouts and assemble them to a single page. Now let’s crank it up and make it even more flexible and useful.

Next steps (this part)

First of all, we still need to sort the modules in a way that they make sense and tell a story, to bring them into the correct order (or any order at all).

Our second task is to create multiple pages within one site with the same approach, so that we’re not limited to building single-page websites – and from there we will take a quick detour into building similar sites from with content shared between them.

While we touch things, we will also clean up the code (actually the content) a little, so that it is easier to understand and maintain.

Sorting items

When we left off, we’ve had a modular landing page with configurable modules that can have different appearances. The content is still a little unorganised, though. Let’s fix that first.

We want to display the modules in a certain order. We could achieve this by naming the modules accordingly, and rely on the file system’s capability to do it as we need it. Or we make this a little more reliable, with one of Eleventy’s custom filters. I found the solution here, Eleventy docs about sorting can be found here. You can also use as file name based sorting approach, you have to configure your collection accordingly.

What we want to achieve though is to sort all sections by their displayorder front matter property, and then pull out whatever we need.

    config.addCollection('sections', collection => {
        return collection
            .getFilteredByTag('section')
            .sort((a, b) => (Number(a.data.displayorder) > Number(b.data.displayorder) ? 1 : -1));
        });    

One small trick: if your layout still needs to be flexible, you can use increments of 10 between each section and leave some space to easily add sections inbetween without having to renumber everything. A bit like back in the days when we were using line numbers for programming.

Building multiple pages, together

So far, we’ve built one page with different sections. That’s fine for a landing page, but even landing pages tend to consist of multiple (sub-)pages. So let’s build multiple pages with our existing approach.

Multiple pages

Creating multiple pages in principle is easy with Eleventy. You create one Markdown file for each page you want to build, and you’re mostly fine. Eleventy will build the structure for you, and make it all accessible to the pagination functionality. Pagination is a mighty tool. However, we are not talking about pagination today, instead we will use a slightly different approach.

To recap, this is our landing page:

---
title: "My Awesome Landing Page"
layout: "layout.njk"
header:
  heroImage:
  mainHeadline:
  secondaryHeadline:
  primaryCTAText:
  primaryCTALink:

footer:
  companyName: "Awesome, Inc."

config:
  date: 2022

---

We want only a few, distinct pages, that we can link “manually”. So let’s introduce a new piece of data and call it pagealias. You could use the slug of the page here or pretty much anything, as long as it is unique.

In the markdown file, we need to change two things: to our index.md file, we’ll add the following:

alias: landingpage

In the markdown files of the modules or sections we want to be displayed on that page, we need to add something as well to connect them. We do it as such:

pagealias: landingpage

Now it is more obvious, to which page this belongs.

Depending on your preferences, you could map it to something existing (like the permalink), or be a bit more obvious and in the “master file” notate the pagealias too. I’ve chosen the second option here so the approach is more obvious.

Make sure reflect this in your template as such:

{% for item in collections.sections %}
	{% if item.pagealias == alias %}
		{% if item.data.layout %}

			{% include item.data.layout %}

		{% endif %}
	{% endif %}
{% endif %}

Now, only content relevant for this particular page will appear on it.

Next, let’s create a file aboutus.md

---
title: "My Awesome About Us Page"
layout: "layout.njk"
header:
  heroImage:
  mainHeadline:
  secondaryHeadline:
  primaryCTAText:
  primaryCTALink:

footer:
  companyName: "Awesome, Inc."

config:
  date: 2022

alias: aboutus
---

After that, let’s create a folder aboutus in our source folder, and add the following file aboutus1.md

---
title: "Simple text / image section for about us"
tags: "section"
layout: "modules/text-image-basic.njk"
headline: "All about us!"
image: "assets/images/awesome_team.jpg"
imagealttext: "An awesome image"
permalink: false
pagealias: aboutus
---
This is all about us. You can make it about anything you want, but it is really only about us.

When the build runs, you will see in your dist folder two files: index.html and aboutus.html – with the respective content on each page. Isn’t that great news?

There are some caveats to this. If your website contains a really awful lot of pages, and those contain a lot of section each, the “collections.sections” might hit hard on your memory, and nested loops may impact your build performance.

In the scenarios I’m using this for (approx. 100 pages), I don’t see any impact (Eleventy is blazingly fast!) but your mileage may vary. It would then also pose different challenges in regards of content organisation, but that’s not to be covered here.

DRY up

Let’s clean things up quickly, as we move forward. To do that, we apply the DRY principle here.

DRY stands for Don’t repeat yourself. That’s a principal that got famous with Ruby on Rails, and although it’s not always practical to stick to it by the book, it should be on the list of things to achieve – for once as multiple occurrences of the same code are harder to maintain and might introduce bugs through the back door.

In our case, it’s a bit more simple. We don’t want to litter our front matter with redundant information. So … let’s look at it. We have in every of our module’s front matter

tags: 
- section

pagealias: landingpage
permalink: false

To clean this up a little, Eleventy’s data model helps us. We put all our sections in a folder called sections, and within this place a sections.json with the following content:

{
	permalink: false,
	pagealias: "landingpage"
	tags: ["section"]
}

This would clear our frontend matter to quite an extent. One could argue that this is not a semantic use of tags, but tags in Eleventy are meant to structure your content both in a semantic and technical way – or do anything you like, really.

Re-purposing items

One of the things that you come across is that you have the same items re-appearing over the page. I want to quickly demonstrate a way to re-purpose items. This will get more important in Part 3 of the series, but here’s a start.

You can repurpose items in different ways. The path that we’re taking here is on module level – we’re building a module that uses shared content. With this approach, we have control where this module appears on the page on a page level. For repeating items that have a fixed position within the page structure, the approach might be different.

In our example, we will build a proof section. Because what we need for ultimate credibility is of course some proof from our existing customers, that everything is awesome, and they are the happiest. We choose here the “user quotes” section: “What our customers say”. We would need this kind of section towards the end of the landing page, and around the middle on the “about us” page.

First, let’s create a “shared” folder within our source folder, so we have a place for content that might be shared across pages. This is not necessary for Eleventy, only for us to find it more easily.

Within this folder, we create a “quotes” folder, wherein each Markdown file is one quote. An example would look like this, you can come up with more.

---
tags:
	- quote

avatarimage: https://thispersondoesnotexist.com/image
avatarimagealt: "an image of a random person, pretending to be our customer"
username: Klaus Kleinholz
quote: This building block system is awesome! I don't know how I could live without it before.
---

Next, we make all the quotes accessible in a collection.

    config.addCollection('userquotes', collection => {
        return collection
            .getFilteredByTag('quote');
        });    

Now, we have all the quotes in one place.

Then, we create the module layout file userquotes.njk:

<section class="section_userquotes">
	<h2>{{ item.data.title }}</h2>
	<ul class="quotes">
	{% for quote in userquotes %}
		<li class="quote">
			<img src="{{quote.data.avatarimage}}" alt="{{quote.data.avatarimagealt">
			<q>{{quote.data.quote}}</q>
			<h5>{{quote.data.username}}</h5>
		</li>
	{% endfor %}
	</ul>
</section>

We can even choose to make this configurable with parameters and assign this to only certain pages, but for now let’s use this one as is. We will come back to this in the next episode.

Finally, we need to create a module for each page in the corresponding content area (that is the sections folder and the aboutus folder).

---
title: "And this is what our customers say"
tags: "section"
layout: "modules/userquotes.njk"
permalink: false
pagealias: landinpage
displayorder: 7
---

And

---
title: "But enough about us, here's our customers"
tags: "section"
layout: "modules/userquotes.njk"
permalink: false
pagealias: aboutus
displayorder: 3
---

The items that we’ve already banished to the .json file are just displayed here for clarity. You can remove them if you have applied the above principle.

Multisite, shared content

So now we’ve got this done, let’s imagine the following scenario: you want to build multiple landing pages about the same thing – say a washer and a dryer. There are some differences, but also content items (or even whole pages) that these sites share.

There are Eleventy plugins that take care of that, e.g Eleventy multisite. However, I want to show a very simple approach. This is not a scalable approach by any means, and doesn’t cover edge cases, it is just how far you can get without having to know much about development.

To do so, we go back to a technique that we’ve been using before. Without big changing, we’re creating another alias which we call “site”. This can contain the whole domain, if you want – or just a shortcut / slug of sort. It just needs to be unique.

In our front matter, we add the site property to the initial document, and all the modules. This way, you make sure that information only displays on the sites you want to have it.

The only thing that we need to change is in our base template:

{% for item in collections.sections %}
	{% if site in item.data.site %}
		{% if item.pagealias == alias %}
			{% if item.data.layout %}

				{% include item.data.layout %}

			{% endif %}
		{% endif %}
	{% endif %}
{% endfor %}

(Note we’re using in here instead of == so that we can assign content to multiple sites.)

It might seem cumbersome to attach information to every different content item, but in the end this approach is not meant to be for a multisite with 1.000 pages each. It’s just for a small set of sites that ideally share content between them and are set up the same way. Otherwise you might want to either go a different route, or set up multiple projects individually.

To save the sites in individual folders, we need to modify the permalink in the front matter so that it would like this:

---
# …
site: site1.com
permalink: "{{ site }}/{{filePathStem}}/{{fileSlug}}/index.html"
# …
---

As a result, the generated pages would be available in dist/site1/…. There is a way that you can configure this in the .eleventy.js config file from Eleventy 2.0.0 onwards, which makes it again a bit easier.

There’s one thing missing though. Something important. We have not been dealing with any images or CSS so far. What is very easy understandable to start with is to just add a passthrough copy to the .eleventy.js config file.

The process is described in the docs for passthroughCopy.

Our file can look like this:

module.exports = config => {
    config.addPassthroughCopy("assets/**");
};

When we’re dealing with multiple sites however, things get a little more complicated. The most straightforward approach is to copy it into every single site folder. In a very blunt manner, we can add an after hook and perform a synchronous copy operation. So … let’s do that and start off with quickly creating a new collection of the site property with Eleventy’s custom filtering:

config.addCollection("sites", function(collectionApi) {
        return collectionApi.getAll().filter(function(item) {           
            return "site" in item.data;
        });
});    

Now we have a collection, but as I couldn’t work out how to access this in Javascript through build time, we need a slight addition as such:

//put these outside the module.exports section of .eleventy.js
const fs = require('fs')
const path = require('path')
let siteNames = new Array();

config.addCollection("sites", function(collectionApi) {
        // build our own collection based on the "site" property
        let sites = collectionApi.getAll().filter((item) => item.data.site);        
        
        //make collection contents available to the be accessed by overall config
        for(let i=0;i<sites.length;i++) {
            siteNames.push(sites[i].data.site);
        }

        //make collection available to eleventy
        return sites;
    });

    config.on('eleventy.after', async ({ dir, results, runMode, outputMode }) => {
       sitesNames.forEach(element => {
             fs.cpSync("./src/shared","./dist/"+element+"/shared",{recursive:true});
        });        
    });

Which will copy everything from the “shared” folder into the destination folder for the generated sites. If you have site specific assets, you can add something like this

sitesNames.forEach(element => {
        fs.cpSync("./src/shared","./dist/"+element+"/shared",{recursive:true});
		fs.cpSync("./src/"+element+"/assets/**","./dist/"+element+"/assets",{recursive:true});
        });     

You need to create a folder with the site name in thesrc folder, and after page creation everything will copied automatically. However, this does not trigger reload in an eleventy --serve scenario.

Summary

We’ve learnt how to extend our page to multiple pages and to sites, how to display things in a certain order and how to share content between pages and sites.

Next part

In part 3 we will extend what we have built so far and apply it for the scenario of A/B testing, and will also create a basic personalisation. With static pages? Yes, with static pages. This is where we would need a bit more than Eleventy, but I promise that no Javascript will be involved.

Addendum

I really can recommend you to go through the Learn Eleventy From Scratch website. Even if you might not need the “from scratch” part, some concepts are explained in a more practical and understandable way than in the docs. The docs are brilliant, but (by design) have the abstract, general approach whereas on that site you can find good examples of how things actually look like when you work with them. You then would need to backtrace from them to the overall concept.