Andy Bell teaches 11ty with the course Learn Eleventy From Scratch.
This repository builds toward the final website following the course lesson by lesson, branch by branch.
Download the starter files and extract the src
folder in the local directory.
Add a .gitignore
file to remove a series of files and folders from the git control.
Create .eleventy.js
as a configuration file.
In .eleventy.js
describe the folders used by the utility.
module.exports = (config) => {
return {
dir: {
input: "src",
output: "dist",
},
};
};
11ty will consider the files in the
src
folder, produce the website in adist
folder.
Initialize a package file.
npm init -y
Install eleventy.
npm install @11ty/eleventy
Create a markdown file index.md
in the src
folder.
Hello world
Serve the website.
npx eleventy --serve
11ty will set up a local environment on localhost:8080
Out of convenience include the command in one of the scripts from the package file.
{
"scripts": {
"start": "npx eleventy --serve"
}
}
Serve the website.
# npx eleventy -serve
npm run start
Nunjucks is a templating language to create markup files with markup syntax and logic — think variables, loops.
Add the njk
extension to the eleventy config file so that .html
files are processed with Nunjucks.
return {
markdownTemplateEngine: "njk",
dataTemplateEngine: "njk",
htmlTemplateEngine: "njk",
// ...
};
Create a folder for layout files in _includes/layouts
.
In the layouts
folder create a base layout base.html
with the skeleton of an HTML page — the initial structure is produced with Emmet and the !
abbreviation.
In the <title>
element inject the value of a title
variable.
<title>{{ title }}</title>
In the <body>
element inject the content in a block.
<body>
{% block content %}
{% endblock %}
</body>
Nunjucks' block works as a placeholder. In the file using the base layout you include the content wrapping the markup in a similar block.
{% block content %}
<h1>Hello world</h1>
{% endblock %}
With the snippet the heading would replace the block placeholder.
<body>
<h1>Hello world</h1>
</body>
Create a separate layout file in home.html
which extends the base layout.
{% extends "layouts/base.html" %}
Add a specific markup structure in the placeholder block.
{% block content %}
<article>
<h1>{{ title }}</h1>
{{ content | safe }}
</article>
{% endblock %}
In the context of markdown documents content
refers to the text beyond the front matter.
---
title: "Hello world"
---
This is the content...
safe
is a Nunjucks' filter to automatically escape the input.
11ty produces markup from the markdown file, which is then included as-is.
<p>This is the content...</p>
Without the filter the page would render the entire string, including the HTML tags.
Update index.md
to define the title
variable in the front matter and reference the layout file.
---
title: "Hello world"
layout: "layouts/home.html"
---
This is pretty _rad_, right?
The index file uses the home layout, the home layout extends the base layout. The result is that 11ty creates an .html
page injecting the variable and content.
Front matter is how you define variables in markdown documents. The syntax follows the YAML language including the variables with key value pairs in between ---
dashes.
---
title: "Hello world"
layout: "layouts/home.html"
---
Update index.md
to define intro
with several fields such as eyebrow
and main
.
---
intro:
eyebrow: "Digital Marketing is our"
main: "Bread & Butter"
---
Update the home layout home.html
to inject the values as if intro
were a JavaScript object.
<h1>
{{ intro.eyebrow }} <em>{{ intro.main }}</em>
</h1>
the elements in
home.html
include several classes later useful to style the website with CSS
Passthrough is how you let 11ty know to copy assets in the output folder — dist
. The feature is useful for static images and stylesheet files, which need to be included as-is.
Update the eleventy config file to keep the images in src/images
.
config.addPassthroughCopy("./src/images/");
the image in the home layout is displayed instead of the alt text describing the missing asset
Partials are fragments, additional .html
files, in which to split the codebase.
Update the base layout base.html
to wrap the content in a <main>
element.
<main tabindex="-1" id="main-content">
{% block content %}
{% endblock %}
</main>
tabindex
and the specific id
attribute help to improve the accessibility of the website.
The idea is to remove the container from keyboard navigation, through tabindex
set to -1, but reachable with an anchor link pointing to the specific id
.
<a href="#main-content">Skip to content</a>
With the link you allow readers to skip previous landmarks and reach the content immediately.
Create a folder for partial files in _includes/partials
.
Create a partial site-head.html
with a <header>
wrapping around a company logo and basic navigation.
The <header>
element in the partial has a role of banner
to improve accessibility.
Include the partial in the layout file base.html
, before the <main>
container.
{% include "partials/site-head.html" %}
<main tabindex="-1" id="main-content"></main>
In site.head.html
notice the partial referencing a vector graphic. This is because partials are not constrained to .html
documents.
{% include "partials/brand.svg" %}
The vector graphic brand.svg
has an aria-hidden
and focusable
attribute to improve accessibility. The graphic is treated as purely decorative
Eleventy works with a global data system.
Create a folder in src/_data
.
Create a data file site.json
.
{
"name": "Issue 33",
"url": "https://issue33.com"
}
11ty makes the information available anywhere in the repository through the name of the file: site.name
, site.url
.
Update site-head.html
to replace the hard-coded name of the website with the value of the variable.
-<a aria-label="Issue 33 - home"></a>
+<a aria-label="{{ site.name }} - home"></a>
Create navigation.json
with an array of items.
{
"items": [
{
"text": "Home",
"url": "/"
}
// {...}
]
}
Update site.head
to loop through the structure with a for
statement.
{% for item in navigation.items %}
<li>
<a href="{{ item.url }}">{{ item.text }}</a>
</li>
{% endfor %}
Nunjucks loops through the collection to produce a list item for each link.
Data is not limited to JSON files. Through JavaScript 11ty allows to export values such as helper functions.
In the data folder create helpers.js
and export an object with a getLinkActiveState
function.
module.exports = {
getLinkActiveState(itemUrl, pageUrl) {},
};
For the specific function return a string with a specific aria-
and data-
attribute if the url of the item matches the url of the page.
Use the function directly in the markup.
<a
href="{{ item.url }}"
{{ helpers.getLinkActiveState(item.url, page.url) | safe }}>
{{item.text}}
</a>
page
is available as a global variable and refers to the current page.
The aria-current
attribute is set to page
for the anchor link matching the current page to improve accessibility.
To reiterate the data system create a data file in cta.json
with fields such as title
and summary
.
{
"title": "Get in touch if we seem like a good fit",
"summary": ""
}
Create a partial cta.html
using the information set on a separate variable, ctaPrefix
<h2>{{ ctaPrefix.title }}</h2>
The idea is to use ctaPrefix
with the data from the JSON file or the information passed through a separate variable.
{% set ctaPrefix = cta %}
{% if ctaContent %}
{% set ctaPrefix = ctaContent %}
{% endif %}
Update home.html
to include two call to actions after the <article>
element, one with the default data, one with a title and content set through the markdown file.
{% set ctaContent = primaryCTA %}
{% include "partials/cta.html" %}
{% set ctaContent = cta %}
{% include "partials/cta.html" %}
Update index.md
to define the variables for the non-default call to action.
---
primaryCTA:
title: "This is an agency that doesn’t actually exist"
summary: "This is ..."
---
Collections are how eleventy provides groups of content. The utility creates a few collections automatically, but it is possible to create your own.
The goal is to create a collection for work items, for the markdown files in the src/work
folder.
In .eleventy.js
add the collection before the return
statement.
config.addCollection("work", (collection) => {});
addCollection
receives the name of the collection and a callback function which describes the collection itself.
To retrieve a reference to files in the src
folder use the getFilteredByGlob
method.
const workItems = collection.getFilteredByGlob("./src/work/*.md");
console.log(workItems);
The function returns an array in which each file is described by an object with a series of keys.
/*
inputPath: './src/work/travel-today.md',
fileSlug: 'travel-today',
filePathStem: '/work/travel-today',
data: {
pkg: [Object],
title: 'Travel Today',
summary: 'A travel website to help make booking easier.',
displayOrder: 5,
}
*/
Among the keys data
describes the front matter.
Sort the collection by the value of displayOrder
.
return collection
.getFilteredByGlob("./src/work/*.md")
.sort((a, b) =>
Number(a.data.displayOrder) > Number(b.data.displayOrder) ? 1 : -1
);
Once added to the config
object 11ty makes the data available through the collections
variable.
<p>There are {{collections.work.length}} items in the work collection</p>
Create a separate collection for featured work items, filtering the markdown files through the featured
key.
config.addCollection("featuredWork", (collection) => {
return (
collection
// get and sort
.filter((d) => d.data.featured)
);
});
Since the two collections share the sorting by displayOrder
create a folder for utility functions in src/utils
.
Create and export a function in sort-by-display-order.js
.
module.exports = (collection) =>
collection.sort((a, b) =>
Number(a.data.displayOrder) > Number(b.data.displayOrder) ? 1 : -1
);
Import the function in the configuration file and use it for both collections.
const sortByDisplayOrder = require("./src/utils/sort-by-display-order");
// config
return sortByDisplayOrder(collection.getFilteredByGlob("./src/work/*.md"));
To use the collection create a partial in featured-work.html
.
{% for item in collections.featuredWork %}
<a href="{{ item.url }}"></a>
{% endfor %}
Include the partial in the home.html
between the call to actions.
{% include "partials/featured-work.html" %}
Using data from a remote source allows 11ty to function as the front-end for a content management system.
The course provides data in the form of a JSON object with an array images following a specific URL: https://11ty-from-scratch-content-feeds.piccalil.li/media.json
.
Install node-fetch
to fetch the data.
npm i node-fetch@2.6.7
Install version 2.*.*
to keep using the require
keyword used in the project.
In the _data
folder create studio.js
to retrieve the data with the fetch
method.
const fetch = require("node-fetch");
Similary to helpers.js
export a function. In this instance export an async function which returns the collection from the provided URL.
module.exports = async () => {
try {
const res = await fetch("...");
const { items } = await res.json();
return items;
} catch (error) {
console.log(error);
return [];
}
};
In the catch block return an empty array as a fallback value.
The data is available like the other files in the same folder, through a studio
variable.
<p>There are {{ studio.length }} items in the remote collection</p>
With node-fetch
and the fetch
method the website requires the data and associated images with each request. The course introduces a separate library @11ty/eleventy-cache-assets
to cache the data so that subsequent visits rely on information which is already available.
Uninstall node-fetch
.
npm uninstall node-fetch
Install a different library provided by 11ty to cache the assets.
npm install @11ty/eleventy-cache-assets
Update studio.js
so that the items are retrieved with the new module.
const Cache = require("@11ty/eleventy-cache-assets");
// try
const { items } = await Cache(
"https://11ty-from-scratch-content-feeds.piccalil.li/media.json",
{
duration: "1d",
type: "json",
}
);
In both instances 11ty creates a studio
variable with the array of images.
Create a partial studio-feed.html
to loop through the images, if any.
{% if studio.length %}
<!-- for item in studio -->
{% endif %}
Include the partial in home.html
after the featured work.
{% include "partials/studio-feed.html" %}
Update index.md
to define a title for the studio feed article — the variable is used in the feed's partial.
---
studioFeed:
title: "From inside the studio"
---
Update the title in index.md
.
---
title: "Issue 33"
---
Wrap the content in home.html
in a <div>
container with a specific class.
<div class="wrapper">
<!-- article -->
</div>
11ty is often touted as an excellent tool for personal websites and blogs. The lesson produces a blog considering the articles in src/posts
.
Each markdown file has a title
, date
and tags
defined in the frontmatter.
---
title: "A Complete Guide to Wireframe Design"
date: "2020-04-13"
tags: ["Tutorial", "Learning"]
---
The tags are defined with a JSON array, but it is possible to rely on YAML syntax as well.
---
tags:
- "Tutorial"
- "Learning"
---
Dates are in ISO format, but 11ty allows to specify alternative values, such as date strings and two keywords: 'Last Modified' and 'Created'. Without a value the date refers to when the file was first created — 'Created'.
Update the config file to create a blog collection.
config.addCollection("blog", (collection) => {
return [...collection.getFilteredByGlob("./src/posts/*.md")].reverse();
});
By default 11ty populates the array in chronological order. By reversing the array you position the most recent articles first.
With the spread operator the array is not modified in place.
-collection.reverse()
+[...collection].reverse()
Create a layout file feed.html
which extends the base layout and shows the articles through a header and list.
{% extends "layouts/base.html" %}
{% set pageHeaderTitle = title %}
{% set pageHeaderSummary = content %}
{% set postListItems = pagination.items %}
{% block content %}
<article>
{% include "partials/page-header.html" %}
{% include "partials/post-list.html" %}
</article>
{% endblock %}
Before the block content the layout sets three variables which are then used in the two partials. title
, content
and pagination
come from a markdown file — created later.
Create the partial page-header.html
to show the title and optionally the summary.
<h1>{{ pageHeaderTitle }}</h1>
{% if pageHeaderSummary %}
<div>{{ pageHeaderSummary | safe }}</div>
{% endif %}
Create the partial post-list.html
to optionally show a headline and the collection with a for loop.
{% if postListHeadline %}
<h2>{{ postListHeadline }}</h2>
{% endif %}
<ol>
{% for item in postListItems %}
<li>
<a href="{{ item.url }}">{{ item.data.title }}</a>
</li>
{% endfor %}
</ol>
Create blog.md
to define the variables ysed by the layout and partials.
---
title: "The Issue 33 Blog"
layout: "layouts/feed.html"
pagination:
data: collections.blog
size: 5
---
The latest articles from around the studio, demonstrating our design thinking, strategy and expertise.
The feed layout receives the title from the front matter, the content from the text after the key value pairs and the pagination from pagination
. The value has a special meaning in 11ty:
-
through
data
consider an array or a collection — in this instance the blog collection -
through
size
create a collection of batches, each with a specific number of items
11ty creates a pagination.items
collection. This is the value the feed layout passes to the list partial.
{% set postListItems = pagination.items %}
There are additional variables in the front matter which will become relevant at a later stage.
---
permalink: "blog{% if pagination.pageNumber > 0 %}/page/{{ pagination.pageNumber }}{% endif %}/index.html"
paginationPrevText: "Newer posts"
paginationNextText: "Older posts"
paginationAnchor: "#post-list"
---
permalink
includes a value through Nunjucks syntax, creating a variety of URL values. pageNumber
is an additional variable included by 11ty in the pagination collection.
blog/index.html
blog/page/1/index.html
blog/page/2/index.html
Create a partial pagination.html
to link to show the different batches in the collection. Condition the markup to the presence of an anchor link pointing to the following or previous batch. 11ty adds href
to the pagination object if necessary.
{% if pagination.href.next or pagination.href.previous %}
{/if}
For each link create an anchor link referencing the URL. For the previous link, for instance:
{% if pagination.href.previous %}
<a href="{{ pagination.href.previous }}{{ paginationAnchor }}">
<span>{{ paginationPrevText if paginationPrevText else 'Previous' }}</span>
</a>
{% endif %}
In the <span>
element inject the label from the chosen variable or a default value with Nunjucks' ternary operator.
In the anchor link use the href
provided in the pagination objct and the anchor defined in the front matter.
In the anchor link the data-direction
attribute helps to improve accessibility by illustrating the navigation purpose.
<a href="/backwards" data-direction="backwards">Previous</a>
<a href="/forwards" data-direction="forwards">Next</a>
Include the partial in the layout feed.html
after the <article>
element.
{% include "partials/pagination.html" %}
To reiterate the concepts the lesson creates an additional page to show articles with a shared tag.
tag/tutorial
tag/design-thinking
Create tags.md
with a series of variables.
---
title: "Tag Archive"
layout: "layouts/feed.html"
pagination:
data: collections
size: 1
alias: tag
filter: ["all", "nav", "blog", "work", "featuredWork", "people", "rss"]
permalink: "/tag/{{ tag | slug }}/"
---
For the pagination:
-
with
data
point to all the collections 11ty creates incollections
-
with
alias
consider a variety of tags, such astutorial
or againdesign-thinking
— notice how the value is included in the permalink -
with
filter
have 11ty skip certain collections
With this setup when you create an article with a tag 11ty creates a collection for the specific tag and then a page for said collection.
The tag page relies on the feed layout exactly as the blog page.
Update feed.html
to handle the generic pagination, for blog.md
, and the one devoted to the tag, tag.md
.
If the markdown file defines a tag use the specific collection for the list of items.
{% if tag %}
{% set postListItems = collections[tag] %}
{% set pageHeaderTitle = 'Blog posts filed under “' + tag + '”' %}
{% endif %}
If the markdown file defines a tag skip the pagination partial.
{% if not tag %}
{% include "partials/pagination.html" %}
{% endif %}
The individual post in the src/posts
folder are rendered as-is, but it is possible to rely on a dedicated layout file to include the content in a specific markup structure.
Create a layout post.html
to render the content of the markdown files.
{% block content %}
<article>
{% include "partials/page-header.html" %}
<div>{{ content | safe }}</div>
</article>
{% endblock %}
In the block include content and the header's partial. For the header define pageHeaderTitle
with the variable set in the markdown file.
{% set pageHeaderTitle = title %}
Again for the header set pageHeaderSummary
to refer to a specific markup structure.
{% set pageHeaderSummary %}
<time>...</time>
<p>...</p>
{% endset %}
Nunjucks makes it possible to store the markup in the summary variable, which is then included in the page-header
partial.
For the tags loop through the input array to generate a list of items.
{% for tag in tags %}
<li>
<a href="/tag/{{ tag | slug }}/">#{{ tag | title | replace(' ', '') }}</a>
</li>
{% endfor %}
In the body of the anchor link use the title
filter to create a capitalized version of the tag.
Use the replace
filter to remove the whitespace.
For the href
attribute use the slug
filter to produce a lowercase label where spaces are replaced with hyphens — 'Design Thinking' becomes 'design-thinking'.
slug
, title
and replace
are provided by 11ty and Nunjucks. Just like with collections, however, it is possible to create custom filters.
Create a folder src/filters
.
Create date-filter.js
and w3-date-filter.js
to format a date object.
For w3-date-filter.js
create and export a function to use the ISO string.
module.exports = (value) => new Date(value).toISOString();
In the config file .eleventy.js
require the filter and add the functionality through the config
object.
const w3DateFilter = require("./src/filters/w3-date-filter.js");
// config
config.addFilter("w3DateFilter", w3DateFilter);
Use the filter like other Nunjucks' filters.
<time datetime="{{ date | w3DateFilter }}"></time>
For the date-filter
repeat the process, but first install the moment
library.
npm install moment
Use the format
method to create a specific string.
return `${dateObject.format("Do")} of ${dateObject.format("MMMM YYYY")}`;
To have the posts benefit from the layout file add a directory data file in the same folder describing the markdown files.
Create src/posts/posts.json
to describe the layout file.
{
"layout": "layouts/post.html"
}
11ty applies the layout file on any markdown document in the same repository which does not specify a separate layout.
With the permalink field create a page in the /blog
route instead of /posts
, the default 11ty uses considering the folder structure.
{
"layout": "layouts/post.html",
"permalink": "/blog/{{ title | slug }}/index.html"
}
In helpers.js
create and export a function which receives a collection, the current page, a number of articles and whether or not to pick the posts at random.
getSiblingContent(collection, page, limit, random) {}
The limit
is set by default to 3, while random
is set by default to true
.
getSiblingContent(collection, page, limit = 3, random = true) {}
Based on this information create a collection of articles skipping the one in the current page.
let filteredItems = collection.filter((d) => d.url !== page.url);
If choosing the article at random loop through the array backwards swapping the current item with a value present earlier in the collection.
Return the entire list or just the items described by the limit.
if (limit > 0) {
filteredItems = filteredItems.slice(0, limit);
}
return filteredItems;
In the post layout post.html
set a variable to refer to the accompanying articles.
{% set recommendedPosts = helpers.getSiblingContent(collections.blog, page) %}
Use the variable to optionally show a footer and a list for the matching posts before the end of the <article>
element.
{% if recommendedPosts %}
<footer>
<!-- -->
</footer>
{% endif %}
Instead of creating a new list component use the post-list.html
partial with the recommended posts in the postListItems
variable.
{% set postListItems = recommendedPosts %}
{% set postListHeadline = "More from the blog" %}
{% include "partials/post-list.html" %}
Create a new layout file in about.html
. With this file extend the base layout, set a specific title and summary.
{% set pageHeaderTitle = title %}
{% set pageHeaderSummary = content %}
<!-- block content -->
{% include "partials/page-header.html" %}
Moreover, set a variable for a collection to show through a dedicated partial, people.html
.
{% set peopleItems = collections.people %}
<!-- block content -->
{% if peopleItems %}
{% include "partials/people.html" %}
{% endif %}
Create the partial people.html
to show the collection with an ordered list and one image for each person.
<ol>
{% for item in peopleItems %}
<!-- list item -->
{% endfor %}
</ol>
Update the configuration file to create a people
collection.
config.addCollection("people", (collection) => {
return collection.getFilteredByGlob("./src/people/.md");
});
Sorts the person by fileSlug
— the alphabetical order of the files without the extension.
return collection
.getFilteredByGlob("./src/people/.md")
.sort((a, b) => (Number(a.fileSlug) > Number(b.fileSlug) ? 1 : -1));
Create the actual page about.html
describing the title, layout and content.
---
title: "About Issue 33"
layout: "layouts/about.html"
---
Wanna see...
With permalink
describe a specific URL instead of the default /about/index.html
.
---
permalink: "/about-us/index.html"
---
Create a layout file work-landing.html
which extends the base layout and marks up the title and content received from a markdown file.
{% extends "layouts/base.html" %}
{% set pageHeaderTitle = title %}
{% set pageHeaderSummary = content %}
In the content block add a partial for the header and loop through a work
collection to show the corresponding image.
{% for item in collections.work %}
<figure>
<img src="{{ item.data.hero.image }}" alt="{{ item.data.hero.imageAlt }}" />
</figure>
{% endfor %}
The named collection was created in a previous lesson, creating our first collection.
Create a new page work.md
detailing the title and layout file.
---
title: "Our finest work"
layout: "layouts/work-landing.html"
---
Create a layout file work-item.html
which extends the base layout. In the block of content describe an individual item from the work collection.
{% block content %}
<h1>{{ title }}</h1>
<p>{{ summary }}</p>
<img src="{{ hero.image }}" alt="{{ hero.imageAlt }}" />
{% endblock %}
From the front matter inject the title, summary, hero image, but also the facts and images associated with two arrays: keyFacts
and gallery
arrays.
{% for item in keyFacts %}
{% endfor %}
{% for item in gallery %}
{% endfor %}
Before the end of the <section>
element the idea is to show the people associated with the specific work.
Considering the markdown documents in the src/work
folder notice the variable team
.
---
team: [1, 2, 5]
---
The integers refer to an identifier set on the different persons in the people collection.
---
title: "Creative director"
key: 1
---
With this in mind the idea is to retrieve a refererence of the people associated to the project by filtering the people collection according to team
.
Create a helper function to filter the input collection according to a set of keys.
filterCollectionByKeys(collection, keys) {
return collection.filter((d) => keys.includes(d.data.key));
},
In the layout file call the function on the people collection with the array of keys.
helpers.filterCollectionByKeys(collections.people, team);
Store the list in a variable.
{% set peopleItems = helpers.filterCollectionByKeys(collections.people, team) %}
Use the value to optionally show the people through the people
partial.
{% if peopleItems %}
<h2>Meet the team behind this project</h2>
{% include "partials/people.html" %}
{% endif %}
Add a data configuration file in src/work/work.json
so that the markdown files rely on the layout.
{
"layout": "layouts/work-item.html"
}
For meta data create a partial meta-info.html
to add meta elements in the <head>
of the document.
<title>{{ pageTitle }}</title>
<link rel="canonical" href="{{ currentUrl }}" />
Set considering multiple configurations. For the title, for instance, initialize the variable with the input title and the site's name retrieved from site.json
.
{% set pageTitle = title + ' - ' + site.name %}
If the partial is used with a specific title override this value.
{% if metaTitle %} {% set pageTitle = metaTitle %} {% endif %}
Include the partial in the base layout, removing the previous <title>
element.
<head>
{% include "partials/meta-info.html" %}
</head>
As needed, set more specific meta variables, such as a description in index.md
.
---
metaDesc: "A made up agency site that you build if you take Learn Eleventy From Scratch, by Piccalilli"
---
For the RSS feed install @11ty/eleventy-plugin-rss
.
npm install @11ty/eleventy-plugin-rss
In .eleventy.js
include the plugin in the configuration object.
const rssPlugin = require("@11ty/eleventy-plugin-rss");
module.exports = (config) => {
config.addPlugin(rssPlugin);
};
Update site.json
to detail additional information on the author.
{
"authorName": "Issue 33",
"authorEmail": "hi@piccalil.li"
}
To generate the feed create rss.html
in the src
folder to specify the necessary XML syntax.
---
title: "Issue 33 Blog"
summary: "A feed of the latest posts from our blog."
permalink: "/feed.xml"
---
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
permalink
works to render the RSS feed in the specific URL and with the .xml
extension.
The <feed>
tag points the Atom specification, followed in the properties argumenting the feed.
<title>{{ title }}</title>
<subtitle>{{ summary }}</subtitle>
{% for post in collections.blog %}
<entry>
<title>{{ post.data.title }}</title>
</entry>
{% endfor %}
Update the base layout so that every page describes the RSS feed in the head of the document.
<link rel="alternate" type="application/rss+xml" href="{{ site.url }}/feed.xml" />
In the RSS feed the course includes two filters which are noted as deprecated in the documentation of the plugin.
-{{ post.date | rssDate }}
+{{ post.date | dateToRfc3339 }}
-{{ collections.blog | rssLastUpdatedDate }}
+{{ collections.blog | getNewestCollectionItemDate | dateToRfc3339 }}
Use Gulp to set up an asset pipeline, to convert source code and other files in the production build.
The website is scheduled to have three lines devoted to code, images and fonts respectively. For each line Gulp helps to process the files in the dist
folder.
Install the task manager.
npm install gulp
In the root of the repository create a task file gulpfile.js
and describe a first task.
exports.default = () => {
console.log("Hello world");
return Promise.resolve();
};
The snippet returns a promise to let Gulp conclude the current task.
Run npx gulp
to print out the message in the console.
Sass extends the CSS native language with a few helpful features.
Install three libraries.
npm install gulp-clean-css gulp-sass@4.1.0 sass
Install gulp-sass
4.1.0 since later versions of the API introduce breaking changes.
Install sass
to have the gulp library use the canonical version of Sass insteed of the version included in gulp-sass
.
Create a gulp-tasks
folder to keep track of the different tasks.
Create sass.js
and require the libraries.
const { dest, src } = require("gulp");
const cleanCSS = require("gulp-clean-css");
const sassProcessor = require("gulp-sass");
sassProcessor.compiler = require("sass");
For the task consider the node environment to potentially compress the output folder.
const isProduction = process.env.NODE_ENV === "production";
The goal is to conceptually divide the stylesheet files in two, critical and standard. Critical CSS is ultimately included inline in a <style>
element, while standard CSS is added through a <link>
tag.
Create criticalStyles
to illustrate the critical files.
const criticalStyles = [
"critical.scss",
"home.scss",
"page.scss",
"work-item.scss",
];
Create calculateOutput
to consider the array and return the output location, ./dist/css
for standard, ./src_includes/css
for critical files.
// Takes the arguments passed by `dest` and determines where the output file goes
const calculateOutput = ({ history }) => {
let response = "./dist/css";
const sourceFileName = /[^(/|\\)]*$/.exec(history[0])[0];
if (criticalStyles.includes(sourceFileName)) {
response = "./src/_includes/css";
}
return response;
};
Create and export a sass
function to process .scss
files in a sequence of operations.
const sass = () => {
return src("./src/scss/*.scss");
//
};
In order:
-
use
gulp-sass
and the associated processor to convert Sass to valid CSS.pipe(sassProcessor().on("error", sassProcessor.logError))
-
use
gulp-clean-css
to optimize the output.pipe( cleanCSS( isProduction ? { level: 2, } : {} ) )
-
use
dest
to locate the output in either the./dist/css
or./src/_includes/css
folder.pipe(dest(calculateOutput, { sourceMaps: !isProduction }));
By default 11ty ignores the files described in .gitignore
, including the output folders.
dist
src/_includes/css
Update the configuration object to avoid this preference.
config.setUseGitIgnore(false);
To avoid considering the folder devoted to node modules create a separate file in .eleventyignore
.
node_modules
Update gulpfile.js
to include the logic of the Sass functon.
const sass = require("./gulp-tasks/sass");
With the task runner require two functions, watch
and parallel
.
const { parallel, watch } = require("gulp");
With watch
and a watcher
helper function process the files every time the .scss
documents change.
const watcher = () => {
watch("./src/scss/**/*.scss", { ignoreInitial: true }, sass);
};
exports.watch = watcher;
exports.fn
is equivalent to module.exports.fn
.
With parallel
run the Sass function, and any task which follows the current lesson.
const { parallel } = require("gulp");
exports.default = parallel(sass);
exports.default.fn
describes the task run by default.
npx gulp
To run other tasks specify the name of the exported function.
npx gulp watch
Install concurrently
to run multiple commands together.
npm install concurrently
Update the node scripts to support npm run start
and npm run production
.
With the first command execute:
-
npx gulp
-
concurrently \"npx gulp watch\" \"npx eleventy --serve\"
With the second command set an environmental variable before running npx gulp
and npx eleventy
.
NODE_ENV=production npx gulp # && ...
The variable is picked up by the task to optimize the output in the dist
folder.
As per the previous section CSS is split between critical and standard. With this in mind create an src/scss
folder.
Create a stylesheet critical.scss
to import a reset file.
@import "reset";
Create _reset.scss
to add global values for the entire application.
*,
*::before,
*::after {
box-sizing: border-box;
}
// ...
Files beginning with an underscore are ignored by the compiler, but included through the import
statement.
Update the base layout to include the critical stylesheet in the <head>
of the document.
<style>
{% include "css/critical.css" %}
</style>
Include additional and critical CSS with a separate variable.
{% if pageCriticalStyles %}
{% for item in pageCriticalStyles %}
<style>
{% include item %}
</style>
{% endfor %}
{% endif %}
The snippet helps to add critical stylesheets from layout files, just by defining the variable.
Past the critical values repeat the operation for non-critical CSS, but through the <link>
element. Include dedicated stylesheet files.
<link rel="stylesheet" href="/fonts/fonts.css" />
Include additional files with a dedicated variable.
{% if pageStylesheets %}
{% for item in pageStylesheets %}
<link rel="stylesheet" href="{{ item }}" />
{% endfor %}
{% endif %}
In the <link>
element the course introduces a hash with the goal of using the value for caching purposes.
<link rel="stylesheet" href="/fonts/fonts.css?{{ assetHash }}" />
Define the variable at the top of the base layout, using a random value for each production build.
{% set assetHash = global.random() %}
Define the helper function in _data/global.js
. The goal is to return a random string to create a distinct hash for each build. A different hash helps to update the cache and avoid outdated CSS.
module.exports = {
random() {
const segment = () => {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return `${segment()}-${segment()}-${segment()}`;
},
};
In the <link>
element the course introduces the media
and onload
attributes as an optimization trick.
<link
rel="stylesheet"
href="/fonts/fonts.css?{{ assetHash }}"
media="print"
onload="this.media='all'"
/>
The media
attribute set to print
tells the browser not to prioritise the stylesheet unless printing. The onload
attribute updates the media to eventually load the resources.
Install get-google-fonts
.
npm install get-google-fonts
The library allows to automate the following:
-
grab the fonts served by Google fonts
-
store the fonts in the output folder
-
create a stylesheet which references the fonts
The goal is to ultimately complete the <link>
element included in the previous lesson in the base layout.
<link rel="stylesheet" href="/fonts/fonts.css?{{ assetHash }}" />
Create a gulp task fonts.js
.
const { dest, src } = require("gulp");
const GetGoogleFonts = require("get-google-fonts");
Export a function which downloads two fonts from Google Fonts, Literata and Red Hat Display.
const fonts = async () => {};
module.exports = fonts;
In the exported function create an isntance of the library specifying an output folder and a CSS file.
const instance = new GetGoogleFonts({
outputDir: "./dist/fonts",
cssFile: "./fonts.css",
});
The library downloads the asssets in ./dist/fonts
and imports them in ./fonts.css
.
For the fonts download the specific files through the download
method.
const result = await instance.download(
"https://fonts.googleapis.com/css2?family=Literata:ital,wght@0,400;0,700;1,400&family=Red+Hat+Display:wght@400;900"
);
return result;
In gulpfile.js
require the task and include its functionality prior to the Sass task.
const fonts = require("./gulp-tasks/fonts");
const sass = require("./gulp-tasks/sass");
Run both tasks through the parallel
function.
exports.default = parallel(fonts, sass);
In the dist
folder the script populates the fonts
sub-folder with the stylesheet.
/* cyrillic-ext */
@font-face {
font-family: "Literata";
font-style: italic;
font-weight: 400;
src: url("./Literata-400-cyrillic-ext1.woff2") format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
/* ... */
Alongside the stylesheet the script adds the font files.
Install gulp-imagemin
.
npm i gulp-imagemin@7.1.0
The library allows to process, optimize and distribute images.
Install gulp-imagemin@7.1.0
since later versions introduce breaking changes.
Create a task images.js
.
const { dest, src } = require("gulp");
const imagemin = require("gulp-imagemin");
Export a function which processes the images through the library.
const images = () => {};
module.exports = images;
In the function consider all the images in the src/images
folder.
return src("./src/images/**/*");
First pass the images through the imagemin
module.
.pipe(imagemin())
The library processes the images with a processor and according to their format.
imagemin([
imagemin.mozjpeg({ quality: 60, progressive: true }),
imagemin.optipng({ optimizationLevel: 5, interlaced: null }),
]);
Second store the processed files in the output folder.
.pipe(dest("./dist/images"));
In gulpfile.js
require the task and include its functionality in the watcher to consider changes in the images
folder.
watch("./src/images/**/*", { ignoreInitial: true }, images);
Add the task to the parallel
function to also run the task alongside the lines dedicated to fonts and CSS.
exports.default = parallel(images, fonts, sass);
As a visual confirmation the console highlights the gains obtained on the different images in terms of size.
gulp-imagemin: Minified 27 images (saved 1.81 MB - 45.7%)
As images are processed through the gulp task it is no longer necessary to rely on the passthrough feature introduced in lesson 5.
-config.addPassthroughCopy("./src/images/");
The course focuses on CSS through the CUBE methodology.
Download the starter files and extract scss
folder in the src
directory.
Install gorko
to complement the cited CUBE methodology.
npm install gorko
Include the library in the critical.scss
stylesheet.
@import "config";
@import "../../node_modules/gorko/gorko.scss";
At high level the idea is to add a configuration file _config.scss
which creates classes on the basis of $gorko-*
variables.
Consider for instance $gorko-colors
.
$gorko-colors: (
'dark': #38445b,
'dark-shade': #263147,
'dark-glare': #505c73,
// ...
}
With $gorko-config
you describe how to create classes.
$gorko-config: (
"bg": (
"items": $gorko-colors,
"output": "standard",
"property": "background",
),
);
The library produces the classes using the key as a name, for each of the items defined in the respective property.
.bg-dark {
background: #38445b;
}
.bg-dark-shade {
background: #263147;
}
.bg-dark-glare {
background: #505c73;
}
The output
field is set to either standard
or responsive
. In this last instance gorko produces as many sets of custom properties as there are breakpoints in the config file.
Consider for instance $gorko-size-scale
, which introduces the perfect fourth scale.
$gorko-size-scale: (
"300": $gorko-base-size * 0.75,
"400": $gorko-base-size,
"500": $gorko-base-size * 1.33,
);
In the configuration block the values are picked up in a flow-space
utility.
$gorko-config: (
"flow-space": (
"items": $gorko-size-scale,
"output": "responsive",
"property": "--flow-space",
),
);
With standard
the library would produce .flow-space-300
, .flow-space-400
and so forth. With responsive
gorko looks at the breakpoints.
$gorko-config: (
//
"breakpoints":
(
"md": "(min-width: 37em)",
"lg": "(min-width: 62em)",
)
);
For each breakpoint the library produces media queries.
@media (min-width: 37em) {
}
@media (min-width: 62em) {
}
In the media queries the library produces additional set of classes: .md\:flow-space-300
, .md\:flow-space-400
and so forth; .lg\:flow-space-300
, .lg\:flow-space-400
and so forth.
The back slash character works to escape the colon.
.flow-space-300 {
--flow-space: 1rem;
}
.md\:flow-space-300 {
--flow-space: 1rem;
}
.lg\:flow-space-300 {
--flow-space: 1rem;
}
The goal is to ultimately add the classes in the markup.
<div class="flow-space-300 md:flow-space-400">
<!-- -->
</div>
The utilities
and blocks
folders create additional classes to complement with the CUBE methodology.
utilities
create classes such as .wrapper
to horizontally center a container.
.wrapper {
max-width: 70rem;
padding: 0 get-size("500");
margin-left: auto;
margin-right: auto;
position: relative;
}
With get-size
gorko picks up the 500
from the size scale.
blocks
provide classes such as .definition-group
to create a grid container.
.definition-group {
display: grid;
grid-template-columns: max-content 1fr;
grid-gap: 0.5rem 1.5rem;
}
In the scss
folder create stylesheet files for the individual pages.
Update critical.scss
to use of some of the utilities provided by gorko
. These utilities help to modify the appearance of the entire website in terms of spacing, color and sizes. Consider these global styles affecting the entire application.
body {
background: get-color("light");
color: get-color("dark-shade");
}
Past the global declarations import some of the blocks and utilities which are mean to be available in all pages.
@import "blocks/definition-group";
// ..
@import "utilities/dot-shadow";
// ..
The lesson works to illustrate how to write CSS in the context of the CUBE methodology and the setup introduced in lesson 22
Update the base layout to wrap the markup in a generic <div>
container with a class of site-wrap
. This helps with horizontal overflow.
<div class="site-wrap">
<!-- ... -->
</div>
Before the end of the container add a footer with general information on the website.
<div class="site-wrap">
<!-- site-head partial -->
<!-- main content -->
<footer>
<!-- -->
</footer>
</div>
To style the base layout create blocks in src/scss/blocks
:
-
with
_site-head.scss
style the header as a flex container to position the nested logo and anchor links side by side. -
with
_nav.scss
to show the links in the navigation side by side -
with
_site-foot.scss
increase the whitespace around the footer's elements
Include the declrations in the critical stylesheet so that the styles affect the website.
@import "blocks/site-head";
@import "blocks/nav";
@import "blocks/site-foot";
Create two blocks:
-
with
_button.scss
style all elements with a class ofbutton
-
with
_skip-link.scss
specifically target the anchor link element which allows to skip the site header and move focus to the container with an id ofmain-content
In terms of CSS and Sass the .button
block incorporates two notable features:
-
@extend
adds the key-value pairs defined in other classes.button { @extend .radius; }
In this instance the button is styled with rounded borders as per the
.radius
utility class.radius { border-radius: 0.25rem; }
-
!import
forces the background and color properties on hover to have precedence over the values specified on buttons with a specificdata
attribute
The skip-link
block includes CSS declarations to hide the element but from assistive technologies.
Include the declrations in the critical stylesheet so that the styles affect the website.
@import "blocks/button";
@import "blocks/skip-link";
The lesson updates the layout of the elements in the .intro
container with CSS grid. For reference the markup is structured with three nested containers.
<article class="intro">
<div class="intro__header"></div>
<div class="intro__content"></div>
<div class="intro__media"></div>
</article>
Create a stylesheet home.scss
to import the configuration and utilities from the gorko library.
@import "config";
$outputTokenCSS: false;
@import "../../node_modules/gorko/gorko.scss";
By setting $outputTokenCSS
to false gorko does not create utility classes. Without this precaution the stylesheet would repeat the same classes produced in critical.css
.
@charset "UTF-8";
.bg-dark {
background: #38445b;
}
.bg-dark-shade {
background: #263147;
}
.bg-dark-glare {
background: #505c73;
}
While the lesson uses $outputTokenCSS
the flag is noted as being deprecated in favor of $generate-utility-classes
.
-$outputTokenCSS: false;
+$generate-utility-classes: false;
Create a block _intro.scss
which accommodates different layouts depending on the viewport width with CSS grid and the media queries produced by gorko.
Focusing on grid properties create a single column layout with four rows of an explicit size.
.intro {
display: grid;
grid-template-rows: get-size("700") minmax(0, 1fr) get-size("700") auto;
grid-gap: get-size("500");
> * {
grid-column: 1;
}
}
With grid-column: 1
every direct children is included in a new row.
Position the content in the last row, sized auto
to be as tall as the nested paragraph and anchor link.
&__content {
grid-row: 4;
}
For the heading and image, so to have the two overlap, position the heading in the second row. Position the image across the first three rows.
&__header {
grid-row: 2;
}
&__media {
grid-row: 1/4;
}
With the explicit size of the first and third row the image is guaranteed to extend above and below the heading.
In the first media query change the layout to explicitly set two columns and four rows.
grid-template-rows: get-size("500") auto auto auto;
grid-template-columns: minmax(15rem, 1fr) 2fr;
In this instance position the content in the first column and penultimate row
&__content {
grid-row: 3/4;
grid-column: 1;
}
Position the heading across the first row,
&__header {
grid-column: 1/3;
}
Position the image in the last column and across all the available rows
&__media {
grid-column: 2/3;
grid-row: 1/5;
}
In the final media query change the layout to increase the size of the second column, the one devoted to the image.
grid-template-columns: 1fr minmax(44rem, 1fr);
Size the image to have a maximum height.
&__media {
height: 28rem;
}
Import the block in home.scss
.
@import "blocks/intro";
Include the stylesheet in home.html
through the pageCriticalStyles
variable.
{% set pageCriticalStyles = ['css/home.css'] %}
As per the configuration in lesson 19 the stylesheet is included in the <head>
element of the base layout.
{% if pageCriticalStyles %}
{% for item in pageCriticalStyles %}
<style>
{% include item %}
</style>
{% endfor %}
{% endif %}
For the call to action import a block cta.scss
in the critical stylesheet. This is to have the component included on every page.
@import "blocks/cta";
Create _cta.scss
with grid properties only within the media query describing larger viewports.
For the medium breakpoint divide the width in 12 columns.
grid-template-columns: repeat(12, 1fr);
Have the heading container span to cover the first nine columns.
&__heading {
grid-column: 1/9;
}
Have the summary container on the second row, from column 12 to 5 in reverse order.
&__summary {
grid-row: 2;
grid-column: 12/5;
}
Have the action container occupy the third row, from column 3 to 12.
&__action {
grid-row: 3;
grid-column: 3/12;
}
For the large breakpoint consider a smaller number of columns for the summary and action.
&__summary {
grid-column: 12/7;
}
&__action {
grid-column: 6/12;
}
In terms of spacing the container adds a margin-top
property through the flow
utility.
<div class="[ cta__inner ] [ flow ]"></div>
If grid properties are supported, however, this default is removed in favor of the gap provided by grid-gap
.
@supports (display: grid) {
> * {
margin: 0;
}
}
Beside the block import a utility to style the heading.
@import "utilities/headline";
Create _headline.scss
as a utility to increase the size of the .heading
element, particularly for large viewports.
For the section devoted to the featured work import a block in the stylesheet devoted to the home layout, home.scss
.
@import "blocks/featured-work";
Create _featured-work.scss
to position the elements in a grid, once again pending the breakpoints.
For the medium breakpoint position the elements in a 12 column grid, with the heading introducing the section spreading across the entirety of the first row.
grid-template-columns: repeat(12, 1fr);
&__intro {
grid-column: 1/13;
align-self: end;
}
For the images position each successive child elements in the first and second half of the container.
&__item {
&:nth-child(odd) {
grid-column: 1/8;
}
&:nth-child(even) {
grid-column: 13/6;
}
}
For the large breakpoint alternate the elements left and right in between two columns.
grid-template-columns: repeat(2, 1fr);
&__intro,
&__item {
&:nth-child(odd) {
grid-column: 1/7;
}
&:nth-child(even) {
grid-column: 13/7;
}
}
For the section devoted to the studio feed import a block in the stylesheet devoted to the home layout, home.scss
.
@import "blocks/studio-feed";
Create _studio-feed.scss
to position the elements in a row allowing for horizontal scroll.
.studio-feed {
&__list {
display: flex;
overflow-x: auto;
}
}
Force each image not to shrink through the `flex-shrink○ property.
> * {
flex-shrink: 0;
}
Before the blog create a block _page-header.scss
to increase the padding around the elements with the corresponding class.
.page-header {
padding: get-size("800") 0;
}
Import the blog in the critical stylesheet.
@import "blocks/page-header";
Past this declaration available to all routes, the blog consists of two pages — feed.html
and post.html
. In each of the layout files add a stylesheet through the pageCriticalStyles
variable.
{% set pageCriticalStyles = ["css/page.css"] %}
Create page.scss
similarly to home.scss
, essentially setting up the configuration for the gorko library and importing the blocks relevant to the page.
@import "blocks/post-list";
Create a block _post-list.scss
to update the design of anchor the link elements pointing to the individual articles.
Create a block _pagination.scss
to separate the two anchor links forwarding to newer and older posts respectively.
Create a block in _page-content.scss
to style blockquote
elements and manage the vertical rhythm of the articles.
Finally, import the different blocks from the stylesheet included in the layout files.
@import "blocks/post-list";
@import "blocks/pagination";
@import "blocks/page-content";
For the about page create a utility _auto-grid.scss
to position the images in a grid.
.auto-grid {
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(var(--auto-grid-min-size, 16rem), 1fr)
);
grid-gap: var(--auto-grid-gap, get-size("500"));
}
By combining auto-fill
with a variable size for the column, between a minimum and 16rem
, the container allocates the child elements in as many columns as allowed by the width of the element.
Import the utility in the critical stylesheet.
@import "utilities/auto-grid";
Past the grid container create a block _people.scss
to increase the spacing between the images in the grid.
Create a block _person.scss
to change the appearance of the image and the connected caption.
Import both blocks in the critical stylesheet.
@import "blocks/people";
@import "blocks/person";
The contact page helps to reinforce the concepts introduced in the course and the asset pipeline.
Create a layout file page.html
which extends the base layout and makes use of the page.scss
stylesheet.
{% extends "layouts/base.html" %}
{% set pageCriticalStyles = ["css/page.css"] %}
In terms of content include the partial for the page header with a given title.
{% set pageHeaderTitle = title %}
<article>
{% include "partials/page-header.html" %}
</article>
Past the partial add the content in a given markup structure.
<div>{{ content | safe }}</div>
Create the page contact.md
to use the layout and define the variables.
---
title: "Contact us"
layout: "layouts/page.html"
---
This is ...
In the critical stylesheet import a utility to change the layout of .gallery
containers.
@import "blocks/gallery";
Create the block _gallery.scss
to position the nested child elements in a single column layout.
On larger viewports modify the align-items
and flex-direction
properties to position the children alternatively left and right.
Past the block, useful for the entirety of the application, create dedicated blocks for the page devoted to work items. Import these blocks from work-item.scss
@import "blocks/hero";
@import "blocks/key-facts";
In the stylesheet specify the same gorko configuration introducing the home or page stylesheet files.
Create the block _hero.scss
to position the heading above a large image describing the work item.
Create the block _key-facts
to increase the whitespace around the statistics.
Since the blocks are imported in the dedicated stylesheet, and not in the critical file, reference the file in work-item.html
.
{% set pageCriticalStyles = ["css/work-item.css"] %}
The lesson concludes the course focusing on a production-ready website.
Download two static assets and extract the images in the src/images/meta
folder.
Update the partial meta-info.html
to include the URL for the social image with a variable.
{% if not socialImage %}
{% set socialImage = site.url + '/images/meta/social-share.png' %}
{% endif %}
The meta properties are already present and pending the socialImage
variable, so that the snippet works to provide a default URL.
{% if socialImage %}
<meta name="twitter:card" content="summary_large_image" />
{% endif %}
Update the same partial to point to the icon through a <link>
element.
<link rel="icon" href="/images/meta/favicon.svg" type="image/svg+xml" />
Minify the HTML output with html-minifier
and an 11ty transform.
Install the node library.
npm install html-minifier
Create a folder src/transforms
.
Create html-min-transform.js
and export a function to minify only html
files.
const htmlmin = require("html-minifier");
module.exports = (value, outputPath) => {
if (outputPath && outputPath.indexOf(".html") > -1) {
}
};
Process the syntax with the installed module.
return htmlmin.minify(value, {
useShortDoctype: true,
removeComments: true,
collapseWhitespace: true,
minifyCSS: true,
});
Outside out markup files return the value as-is.
return value;
Require the transforming function in eleventy.config.js
.
const htmlMinTransform = require("./src/transforms/html-min-transform");
Add the transform only in the production build checking process.env.NODE_ENV
.
const isProduction = process.env.NODE_ENV === "production";
if (isProduction) {
config.addTransform("htmlmin", htmlMinTransform);
}
To test the production build run two commands in the console:
-
npm run production
-
cd dist && npx serve
serve
serves the website on localhost, where the source is minified.