- The JAMStack, AJAX and Static Site Generation
Create a blog folder and display the blog posts on the home page.
- create a collection
- use the Liquid loop to display your posts on the home page
- 11ty
- 11ty Rocks
- an extensive 11ty course
- introduce static site generation (SSG) with eleventy (11ty)
- introduce the Markdown language
- use templates and markdown to generate a web site
- introduce templating languages (Liquid) and YAML
A "stack" is a collection of software used to solve a common problem. In web development common stacks include MEAN (MongoDB, ExpressJS, Angular and Node), MERN (MongoDB, ExpressJS, React and Node) and LAMP (Linux, Apache, MySQL, and PHP).
The JAMstack is an architecture that uses a build process to create web pages and sites that are deployed to a content delivery network.
Recall the design patterns we examined previously. JAMstack sites are the simplest and most traditional - static HTML pages - but they way they are created is thoroughly modern.
As we just learned, JAMstack sites use pre-rendering tools that use a build process to create the multiple pages that comprise a web site.
Eleventy (aka 11ty) is a simple static site generator (SSG). SSG websites are very popular due to their simplicity, superior speed, SEO and security.
The benefits of 11ty over other completing generators include the fact that it is written in JavaScript (Node) and its simplicity.
Note the .gitignore file at the top level targeting the node_modules folder:
node_modules$ npm init -y
$ npm install @11ty/eleventyAdd a script to package.json:
"scripts": {
"start": "eleventy --serve"
},Since 11ty converts Markdown files to HTML we need to either delete the readme.md file in this repo or create an .eleventyignore file with the contents readme.md. Here's the documentation for Eleventy ignore files.
Add passthroughs for our static assets in an .eleventy.js file.
module.exports = function (config) {
config.addPassthroughCopy("./src/css/");
config.addPassthroughCopy("./src/img/");
config.addPassthroughCopy("./src/js/");
};This is our eleventy configuration file. It is a function that exports its contents for use by the Eleventy publishing system.
Add instructions for input and output folders:
module.exports = function (config) {
config.addPassthroughCopy("./src/css/");
config.addPassthroughCopy("./src/img/");
config.addPassthroughCopy("./src/js/");
return {
dir: {
input: "src",
output: "_site",
},
};
};Run npm start and open the localhost address in Chrome.
Note:
- the generated
_sitefolder - the folders specified in our config are copied into
_site
Add the _site folder to the .gitignore file:
node_modules
_siteCreate src/index.md on the top level with the following text:
## Articles
A list of articles will appear hereNote the conversion to HTML.
Create src/_includes/layout.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/css/styles.css" />
<title>My Blog</title>
</head>
<body>
<main tabindex="-1" class="content">
<h1>{{ pageTitle }}</h1>
{{ content }}
</main>
</body>
</html>Note the {{ pageTitle }} and {{ content }} template regions. Our content will be inserted there.
Eleventy supports many templating languages:
If template languages are new to you don't worry, they are generally quite simple, resemble JavaScript and can be mastered easily.
We'll use Liquid today. Liquid is the in-house templating language created and maintained by Shopify.
Add a new block at the top of index.md:
---
layout: layout.html
pageTitle: New York Today
---
## Articles
A list of articles will appear hereThe material at the top between the ---'s is commonly called front matter and is written in YAML (YAML Ain't Markup Language). YAML is typically used for processing instructions.
Add more content:
---
layout: layout.html
pageTitle: New York Today
---
## Articles
> Dorothy followed her through many of the beautiful rooms in her castle.
A list of articles will appear here
- bullet one
- etcThe index file we created has been merged with _includes/layout because of the layout: layout.html front matter instruction.
Now that the page is linked to our template - layout.html - it includes the <h1> tag referenced there <h1>{{ pageTitle }}</h1>.
layout.html uses a liquid snippet. We will be using a handful of these.
Because the _site folder is generated by eleventy we can add it to our .gitignore.
Markdown is an extremely simple language used extensively in web development.
It allows you to create content using an easy-to-read, easy-to-write plain text format. It converts to structurally valid HTML. Invented by John Gruber - it (or one of its many flavors) is ubiquitous in web publishing. This readme file is written in markdown.
Note: many of the conventions for Markdown arose from how people used email when it was confined to simple text documents, e.g. a bulleted list:
- item one
- item two
- item threeWe wll create a collection of pages using tags in our front matter.
In a new pages folder, pages/about.md:
---
layout: layout.html
pageTitle: About Us
tags: page
navTitle: About
---
## We are
- a group of commited citizens
- a caring community
- a force in national politics
We are New Yorkers.
[Home](/)Note:
- the changes in the
_sitefolder (navigate tohttp://localhost:8080/pages/about/) - the transformation of markdown to HTML (examine the HTML in dev tools)
Create a navbar in layout.html:
<nav>
<ul>
{% for page in collections.page %}
<li>{{ page.data.navTitle }}</li>
{% endfor %}
</ul>
</nav>Add a tag and nav title to index.md:
---
layout: layout.html
pageTitle: New York Today
navTitle: Home
tags: page
---You should see a list of navTitles at the top.
The front matter navTitle and tags in our two pages are used in the template's navbar.
Add anchor tags to the template:
<nav>
<ul>
{% for page in collections.page %}
<li>
<a href="{{ page.url | url }}">{{ page.data.navTitle }}</a>
</li>
{% endfor %}
</ul>
</nav>Let's a few more pages to the pages folder:
contact.md:
---
layout: layout.html
pageTitle: Contact Us
tags: page
navTitle: Contact
---
## Here's how:
- 917 865 5517
[Home](/)And pictures.md:
---
pageTitle: Photos
navTitle: Pictures
---
## Markdown, single image:
Navigate to http://localhost:8080/pages/pictures/
Add the following to use the image reference in the front matter:
Note the URLs in the ements inspector.
Link it to the template with layout: layout.html, add a tag tags: page and multiple images:
---
layout: layout.html
pageTitle: Photos
navTitle: Pictures
tags: page
singleImage: /img/apples.png
images:
- apples.png
- apples-red.png
- apples-group.png
---
## Markdown, single image:

Examine the image options:
---
layout: layout.html
pageTitle: Photos
navTitle: Pictures
tags: page
singleImage: /img/apples.png
images:
- apples.png
- apples-red.png
- apples-group.png
---
## Markdown, single image:


## HTML, single image:
<img src="{{ singleImage }}" alt="info goes here" style="transform: scale(50%) rotate(20deg);" />
## Markdown in Liquid for loop:
{% for filename in images %}

{% endfor %}
## HTML in Liquid for loop:
{% for filename in images %}
<img src="/img/{{ filename }}" alt="A nice picture of apples." />
{% endfor %}Note the use of HTML in the Markdown file.
As noted, you can use HTML in a markdown file.
Change contact.md:
---
layout: layout.html
pageTitle: Contact Us
pageClass: contact
tags: page
navTitle: Contact
---
<h2>Here's how:</h2>
<ul>
<li>917 865 5517</li>
</ul>
<a href="/">Home</a>You can also use HTML files alongside markdown.
Change the name of contact.md to contact.html.
Note: you cannot use markdown in an HTML file.
We can use the front matter to define a class for this page.
Add the following to the pictures.md front matter:
pageClass: pictures<body class="p-{{ pageClass }}">
<nav>
<ul>
{% for page in collections.page %}
<li class="t-{{page.data.pageClass}}">
<a href="{{ page.url | url }}">{{ page.data.navTitle }}</a>
</li>
{% endfor %}
</ul>
</nav>
</body>Add a pageClass entry to the front matter of all the files in the pages directory.
Now we can use CSS to style a highlight state for navigation:
nav ul {
padding: 0;
list-style: none;
display: flex;
justify-content: space-around;
}
nav ul a {
padding: 0.5rem 1rem;
}
nav a:hover,
.p-home .t-home a,
.p-about .t-about a,
.p-pictures .t-pictures a,
.p-contact .t-contact a {
background-color: #007eb6;
color: #fff;
border-radius: 4px;
}Since we have unique page class we can address elements on a specific page using CSS:
.p-pictures img {
max-width: 50%;
}
.p-pictures h1 {
color: rgb(11, 123, 11);
}11ty allows you to create a _data folder which can then be used to store information you need for your site.
Create a _data folder in src and add site.json with the following json:
{
"siteTitle": "My first 11ty Site"
}Then use it in your template:
<title>{{site.siteTitle}}</title>Collections use tags to group content.
Note: you can use multiple tags, e.g.:
---
layout: layout.html
pageTitle: Contact Us
tags:
- page
- contact
navTitle: Contact
---Tags can be written
tags: page or
tags: [page]
i.e. if you need multiple tags use: tags: [page, other].
Here's the tagging documentation.
You can use json to simplify data management.
We will add additional tags that can be used to reorganize content.
Create pages/pages.json:
{
"layout": "layout.html",
"tags": ["page", "nav"]
}Note: it might be necessary to restart 11ty.
Any document in the pages folder will inherit these properties. We can now remove the tags and layout metadata from all files in the pages directory.
E.g.: pages/about.md:
---
pageTitle: About Us
navTitle: About
pageClass: about
---
## We are
- a group of commited citizens
- a caring community
- a force in national politics
We are New Yorkers.Perform the same deletions on all files in pages.
Let's use the page collection to display all the posts.
In index.md :
---
layout: layout.html
pageTitle: New York Today
tags: page
navTitle: Home
---
## Articles
{% for page in collections.page %}
<h2><a href="{{ page.url }}">{{ page.data.pageTitle }}</a></h2>
<em>{{ page.date | date: "%Y-%m-%d" }}</em>
{% endfor %}Note: the | character in post.date | date: "%Y-%m-%d" is a filter.
There are quite a number of available filters for example: upcase:
{% for page in collections.page %}
<h2><a href="{{ page.url }}">{{ page.data.pageTitle | upcase }}</a></h2>
<em>{{ page.date | date: "%Y-%m-%d" }}</em>
{% endfor %}Commit your changes, merge them into the main branch and push your site to a new Github repository.
Sign into Netlify and create a new site from Git. Check the settings to ensure that Netlify has auto detected 11ty and deploy the site.
Examine the deploy logs. Note that Netlify will download and install 11ty in order to generate your _site folder and deploy that folder to a web server.
Fetch allows you to get data from your own or another's service. Web services expose data in the form of an API which allow you to get, delete, update or create data via routes.
Today, we will get data from the New York Times and display it on our home page.
Edit index.md in VS Code:
---
layout: layout.html
pageTitle: New York Today
pageClass: home
---
## Articles
<button>Show Stories</button>Add a hard coded link to the page in the template:
<nav>
<ul>
<!-- NEW -->
<li><a href="/">Home</a></li>
{% for page in collections.page %}
<li>
<a href="{{ page.url | url }}">{{ page.data.navTitle }}</a>
</li>
{% endfor %}
</ul>
</nav>The fetch() API takes one mandatory argument, the path to the resource you want to fetch. It returns something known as a Promise that returns a response after the content is received.
API stands for Application Programming Interface.
We need data we can fetch from the internet. We'll start with the Typicode playground. Note that you can do more than just get data, you can also post, create, delete and update data. Together these functions are often refered to as CRUD.
Go to https://jsonplaceholder.typicode.com/posts and examine the response in the browser.
Open a console in the browser.
Try other resources such as comments or photos.
Note the basic structure - an array of objects:
[
{ ... },
{ ... }
]The format is json - JavaScript Object Notation
fetch is a built in browser function that returns a promise:
fetch("https://jsonplaceholder.typicode.com/posts");A resolved promise using .then:
fetch("https://jsonplaceholder.typicode.com/posts").then(function (response) {
return response.json();
});Since the promise is resolved you can see the actual data in the console. It returns an array of 100 fake posts which we can console.log:
fetch("https://jsonplaceholder.typicode.com/posts")
.then(function (response) {
return response.json();
})
.then(function (data) {
console.log(data);
});Let's start out our script with event delegation.
In layout.html:
<script src="/js/scripts.js"></script>
In scripts.js:
document.addEventListener("click", clickHandlers);
function clickHandlers(event) {
console.log(event.target);
}Use matches in the context of in if statement to run a function:
document.addEventListener("click", clickHandlers);
function clickHandlers(event) {
if (!event.target.matches("button")) return;
fetch("https://jsonplaceholder.typicode.com/posts")
.then((response) => response.json())
.then((json) => console.log(json));
}Add an empty element to the home page:
<main class="stories"></main>Instead of logging the data we will call another function:
document.addEventListener("click", clickHandlers);
function clickHandlers(event) {
if (!event.target.matches("button")) return;
fetch("https://jsonplaceholder.typicode.com/posts")
.then((response) => response.json())
.then((data) => showData(data));
}
function showData(data) {
document.querySelector(".stories").innerText = data[1].body;
}Note:
document.querySelector(".stories")- targets an empty divdata[1]- we use[1]to get the second entrydata[1].body- we use dot notation to access just one of the properties of the object
Try:
document.querySelector(".stories").innerText = data;
Arrow functions are commonly used as a shorter syntax for anonymous functions.
Named functions in JavaScript are written using the function keyword:
function exclaim(string) {
return string + "!";
}Anonymous functions do not have a name:
fetch("https://jsonplaceholder.typicode.com/posts")
.then(function (response) {
return response.json();
})
.then(function (data) {
console.log(data);
});The equivalent arrow function omits the function keyword and uses a "fat arrow":
function(response){
return response.json()
}becomes:
(response) => {
return response.json();
};Arrow functions have an "implicit" return and can be written using an even shorter form:
(response) => response.json();The parentheses are optional.
Thus we can write:
fetch("https://jsonplaceholder.typicode.com/posts")
.then((response) => response.json())
.then((data) => showData(data));instead of:
fetch("https://jsonplaceholder.typicode.com/posts")
.then(function (response) {
return response.json();
})
.then(function (data) {
return showData(data);
});Demo : a quick tour of the network panel in Chrome's dev tools.
function showData(data) {
console.log(data);
for (let i = 0; i < data.length; i++) {
console.log("foo::", i, data[i]);
}
document.querySelector(".stories").innerText = data;
}Display only the titles:
function showData(data) {
console.log(data);
let content = "";
for (let i = 0; i < data.length; i++) {
console.log("foo::", i, data[i]);
content = content + data[i].title;
}
document.querySelector(".stories").innerText = content;
}Use template strings and the += operator to construct HTML for a better display:
for (let i = 0; i < data.length; i++) {
content += `<h3>${data[i].title}</h3>`;
}Use innerHTML: document.querySelector(".stories").innerHTML = content;
Add the body content to the output:
for (let i = 0; i < data.length; i++) {
content += `<h3>${data[i].title}</h3><p>${data[i].body}</p>`;
}Many prefer usings a for of style loop:
for (let post of data) {
content += `<h3>${post.title}</h3><p>${post.body}</p>`; // new
}With array.map():
function showData(data) {
let content = data.map((post) => `<h3>${post.title}</h3><p>${post.body}</p>`);
document.querySelector(".stories").innerHTML = content;
}Let's use the New York Times developers site for our data.
document.addEventListener("click", clickHandlers);
// store the link plus the API key in a variable
const API =
"https://api.nytimes.com/svc/topstories/v2/nyregion.json?api-key=XJYe53T8oZ9wRgPqxGVAs2NtPqId5pdL";
function clickHandlers(event) {
if (!event.target.matches("button")) return;
fetch(API)
.then((response) => response.json())
.then((data) => showData(data));
}
function showData(data) {
console.log(data);
}Examine the nature of the returned data in the console. The results property contains the data we are interested in.
function showData(data) {
console.log(data.results);
}document.addEventListener("click", clickHandlers);
var API =
"https://api.nytimes.com/svc/topstories/v2/nyregion.json?api-key=XJYe53T8oZ9wRgPqxGVAs2NtPqId5pdL";
function clickHandlers(event) {
if (!event.target.matches("button")) return;
fetch(API)
.then((response) => response.json())
.then((data) => showData(data.results));
}
function showData(stories) {
console.log(stories[0].title);
// initialize an empty string
var looped = "";
// use += in a for loop that uses the length of the results
for (let story of stories) {
console.log(story);
looped =
looped +
`
<div class="item">
<h3>${story.title}</h3>
<p>${story.abstract}</p>
</div>
`;
}
console.log(looped);
}Here's the script so far:
document.addEventListener("click", clickHandlers);
var API =
"https://api.nytimes.com/svc/topstories/v2/nyregion.json?api-key=XJYe53T8oZ9wRgPqxGVAs2NtPqId5pdL";
function clickHandlers(event) {
if (!event.target.matches("button")) return;
fetch(API)
.then((response) => response.json())
.then((data) => showData(data.results));
}
function showData(stories) {
var looped = "";
for (let story of stories) {
looped += `
<div class="item">
<h3>${story.title}</h3>
<p>${story.abstract}</p>
</div>
`;
}
document.querySelector(".stories").innerHTML = looped;
}An alternative method: use the map() method on the array:
document.addEventListener("click", clickHandlers);
var API =
"https://api.nytimes.com/svc/topstories/v2/nyregion.json?api-key=XJYe53T8oZ9wRgPqxGVAs2NtPqId5pdL";
function clickHandlers(event) {
if (!event.target.matches("button")) return;
fetch(API)
.then((response) => response.json())
.then((data) => showData(data.results));
}
function showData(stories) {
var looped = stories
.map(
(result) => `
<div class="item">
<h3>${result.title}</h3>
<p>${result.abstract}</p>
</div>
`
)
.join("");
document.querySelector(".stories").innerHTML = looped;
}Add CSS to format the data:
@media (min-width: 480px) {
.stories {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 2rem;
}
}
.stories .item {
border-bottom: 1px dashed #aaa;
}Remove the button:
---
layout: layout.html
pageTitle: New York Today
---
## Articles
<div class="stories"></div>And the script's dependency on it:
var API =
"https://api.nytimes.com/svc/topstories/v2/nyregion.json?api-key=XJYe53T8oZ9wRgPqxGVAs2NtPqId5pdL";
function getStories(event) {
fetch(API)
.then((response) => response.json())
.then((data) => showData(data.results));
}
function showData(stories) {
var looped = stories
.map(
(result) => `
<div class="item">
<h3>${result.title}</h3>
<p>${result.abstract}</p>
</div>
`
)
.join("");
document.querySelector(".stories").innerHTML = looped;
}
getStories();Add elements from the API:
function showData(stories) {
var looped = stories
.map(
(story) => `
<div class="item">
<picture>
<img src="${story.multimedia[2].url}" alt="" />
<caption>${story.multimedia[2].caption}</caption>
</picture>
<h3><a href="${story.url}">${story.title}</a></h3>
<p>${story.abstract}</p>
</div>
`
)
.join("");
document.querySelector(".stories").innerHTML = looped;
}The script will error on all pages other than the home page.
Add a page class and make getStories run only when that class is present on the page.
---
layout: layout.html
pageTitle: New York Today
pageClass: home
---
## Articles
<div class="stories">Loading...</div>if (document.querySelector(".home")) {
getStories();
}cd into the db folder in a separate terminal.
Run $ npm install
Examine the package.json file.
Use $ npm run start the db directory to start a server.
Commit, merge and push the content to Github. Log in to app.netlify.com and ensure that the deploy has succeeded.