The principles of JAMstack static site.
Eleventy, as most static sites, relies on an architectural paradigm called JAMstack, an acronym meaning: a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup.
(see Mathias Biilmann & Phil Hawksworth book: Modern Web development on the JAMstack. ).
In the JAMstack architecture, JavaScript is used not only in the browser for interactivity and presentation but also to build all the files of the site (this is the case of Eleventy. Other generators use different languages: Go, Python, etc. ); the APIs are the programming interfaces to Node packages (Eleventy itself is a Node package. )or other frameworks used as a back end; Markdown is used to set the content of each page of the site, with a settings part (front matter) using either JavaScript or a description language like YAML or TOML and a template part (there are many templating systems: Liquid, Nunjucks, Hanbdlebars, Pug, etc. ).
Trying to stay as simple as possible, 11tyTips is built using a minimal set of the JAMstack architecture: JavaScript, backed by the Node ecosystem, Nunjucks, as templating system, and Markdown as content markup language. It doesn’t follow the canonical dispatching of files promoted by Eleventy and most satic site generators (data, templates, contents), but follows a pattern examplifying the JAMstack paradigm.
The source
directory design reflects the JAMstack categories:
11ty
) and 11tyTips modules (lib
). This is also where the initial JavaScript file firing the building process is located. Instead of .eleventy.js
this file is named make.js
(without the starting dot because it is not hidden). )./
and parts
directories, all the templates producing the site HTML files; plus the Javascript and CSS files concatenated and minified (assets
). )pages
subdirectory), plus some files they include (parts
). )I sometimes like to call it the 3M directory (all of its subdirectory having names begining with an m letter, enumerating these directory names sounds like a slogan: make matrix matter(s)!
). All subdirectories of the three first level directories follow a consistent naming scheme:
This structure departs deliberately from what is generaly adopted by static sites built with Eleventy (see, for instance, Eleventy base blog structure ), examplifying the versatility of the tool (Eleventy configuration object has two properties allowing this precious flexibility: dir.data
and dir.includes
. ): Eleventy doesn’t lock you into a predefined scheme and let you express your specific needs with ease.
11tyTips has been designed to offer the maximum of flexibility and consistency in the development phase, keeping a strong separation of concerns as to ease any needed refactoring and modification of the code. The site source tree gives an overview of the source directory hierarchy.
Following these configuration options leads to a command line requiring the --config
argument:
npx eleventy --config=make/11ty/make.js
Why not use 11tyTips as a boilerplate for your own new site?
11tyTips site has been designed to help those wanting to start with a static site generator such as Eleventy, without spending a lot of time to find the best architecture meeting their needs. All tips explained in its pages are exemplified by its source code: the dogfooding principle!
If you want to use it as a frame for your new site, visit the 11ty Frame site.
A bird’s eye view of 11tyTips site.
.
├── make
│ ├── 11ty
│ │ ├── collections.js
│ │ ├── filters.js
│ │ ├── libraries.js
│ │ ├── make.js
│ │ ├── plugins.js
│ │ └── shortcodes.js
│ └── lib
│ ├── block_replace.js
│ ├── block_split.js
│ ├── css_mixin.js
│ ├── data.js
│ ├── dateToISO.js
│ ├── feed_content.js
│ ├── string.js
│ └── template_process.js
|
├── matrix
│ ├── assets
│ │ ├── icons
│ │ │ └── pencil.svg
│ │ ├── scripts
│ │ │ └── js
│ │ │ ├── parts
│ │ │ │ ├── _comments_.js
│ │ │ │ ├── _dom_.js
│ │ │ │ ├── _events_.js
│ │ │ │ ├── _idb_.js
│ │ │ │ ├── _link_page_.js
│ │ │ │ ├── _menu_.js
│ │ │ │ ├── _notes_.js
│ │ │ │ ├── _service_.js
│ │ │ │ ├── _service_worker_.js
│ │ │ │ └── _ui_color_.js
│ │ │ ├── lib.njk
│ │ │ └── service.njk
│ │ ├── static
│ │ │ ├── fonts
│ │ │ │ ├── athiti-v3-latin-regular.woff2
│ │ │ │ ├── FiraCode-Regular.woff2
│ │ │ │ └── harmattan-v5-latin-regular.woff2
│ │ │ ├── media
│ │ │ │ ├── 1px.webp
│ │ │ │ ├── Adam_de_Coster_color.jpg
│ │ │ │ ├── Adam_de_Coster_gray.jpg
│ │ │ │ ├── android-chrome-192x192.png
│ │ │ │ ├── android-chrome-512x512.png
│ │ │ │ ├── apple-touch-icon.png
│ │ │ │ ├── favicon-16x16.png
│ │ │ │ ├── favicon-32x32.png
│ │ │ │ └── wide_img.webp
│ │ │ ├── scripts
│ │ │ │ └── js
│ │ │ │ ├── clipboard.min.js
│ │ │ │ └── prism.min.js
│ │ │ └── styles
│ │ │ └── css
│ │ │ └── prism.min.css
│ │ └── styles
│ │ └── css
│ │ ├── parts
│ │ │ ├── _aspect_ratio_.css
│ │ │ ├── _color_.css
│ │ │ ├── _comment_.css
│ │ │ ├── _destyle_.css
│ │ │ ├── _font_.css
│ │ │ ├── _font_face_mixin_.css
│ │ │ ├── _full_width_.css
│ │ │ ├── _html_.css
│ │ │ ├── _menu_.css
│ │ │ ├── _note_.css
│ │ │ ├── _prism_theme_.css
│ │ │ ├── _properties_.css
│ │ │ ├── _rootvar_.css
│ │ │ ├── _source_tree_.css
│ │ │ └── _ui_.css
│ │ ├── lib_comment.njk
│ │ ├── lib_menu.njk
│ │ ├── lib_prism.njk
│ │ └── lib.njk
│ ├── parts
│ │ ├── blocks
│ │ │ ├── _body_.njk
│ │ │ ├── _body_offline_.njk
│ │ │ ├── _feed_.njk
│ │ │ ├── _head_.njk
│ │ │ ├── _menu_.njk
│ │ │ └── _sitemap_.njk
│ │ ├── _article_.njk
│ │ ├── _banner_.njk
│ │ ├── _colophon_.njk
│ │ ├── _comment_.njk
│ │ ├── _description_.njk
│ │ ├── _favicon_.njk
│ │ ├── _feed_.njk
│ │ ├── _font_inline_.njk
│ │ ├── _head_.njk
│ │ ├── _instant_page_.njk
│ │ ├── _link_page_.njk
│ │ ├── _menu_iframe_.njk
│ │ ├── _netlify_badge_.njk
│ │ ├── _netlify_headers_.njk
│ │ ├── _script_.njk
│ │ ├── _seo_.njk
│ │ ├── _site_url_.njk
│ │ ├── _style_inline_.njk
│ │ └── _webmanifest_.njk
│ ├── feed.njk
│ ├── frame.njk
│ ├── menu.njk
│ ├── offline.njk
│ └── sitemap.njk
|
└── matter
├── assets
│ ├── scripts
│ │ └── js
│ │ ├── lib
│ │ │ ├── A_o.js
│ │ │ ├── C_o.js
│ │ │ ├── D_o.js
│ │ │ ├── F_o.js
│ │ │ ├── I_o.js
│ │ │ └── U_o.js
│ │ ├── lib.md
│ │ ├── netlify_headers.md
│ │ ├── service.md
│ │ └── webmanifest.md
│ └── styles
│ └── css
│ ├── lib_comment.md
│ ├── lib_menu.md
│ ├── lib_prism.md
│ └── lib.md
└── pages
├── parts
│ └── _source_tree_.txt
├── 404.md
├── creating_menu.md
├── css_mixins.md
├── eleventy_configure.md
├── feed.md
├── frontmatter_data.md
├── frontmatter_function.md
├── index.md
├── lightning_images.md
├── menu.md
├── minifying.md
├── names_guide.md
├── offline.md
├── pre_post_processing.md
├── shortcodes.md
├── shorthands.md
├── site_cloning.md
├── site_configure.md
├── site_structure.md
├── sitemap.md
├── source_tree.md
├── styles_guide.md
└── variables_inheritance.md
You can customize Eleventy starting file as you see fit.
Eleventy uses a simple file, named by default .eleventy.js
(because of the leading dot it’s an hidden file ), to define the settings of the building environment. This file is, by default, located at the root of the source folder but it can be put anywhere. The name itself can also be changed, if you see fit. (11tyTips modifies both the name, calling it make.js
, and the location, putting it in the source/make/11ty
directory. )
This file defines the settings of the main resources used in the building process: libraries, shortcodes, filters, plugins, etc. 11tyTips fragments this monolithic file in specific modules, each devoted to a part of the configuration, all located in the source/make/11ty/
directory. The configuration object returned by the configuration/make file is important because it defines the location of the main components of the building process: the input
and output
directories, the templates processors, etc.
11tyTips declares its own set of directories, modifying some of the default names and locations (for instance, the leading underscore character is suppressed for output
, lib
and matrix
directories, in lieu of _output
, _data
and _includes
; static
directory is named assets
, etc. )
const MAKE_o =
{
markdownTemplateEngine: 'njk',
htmlTemplateEngine: 'njk',
dataTemplateEngine: 'njk',
templateFormats: [ 'njk', 'md' ],
passthroughFileCopy: true,
pathPrefix: '/',
dir:
{
input: '.',
output: '../site',
data: 'matter/assets/scripts/js/lib',
includes: 'matrix',
},
tag_a: //: to create collections
[
{ tag_s: 'tip', sort_f: 'sortByRank__a' },
],
static_o: { "matrix/assets/static": "assets" }, //: static files
dirs_o:
{
makeDir_s: './',
pagesPartsDir_s: './matter/pages/parts',
}
}
module.exports = make_o =>
{
make_o.tag_a = MAKE_o.tag_a
make_o.matrixDir_s = MAKE_o.dir.includes
make_o.pagesPartsDir_s = MAKE_o.dirs_o.pagesPartsDir_s
make_o.addPassthroughCopy( MAKE_o.static_o )
; [ 'libraries',
'shortcodes',
'filters',
'plugins',
'collections'
].forEach( make_s => require( `${MAKE_o.dirs_o.makeDir_s}${make_s}.js` )( make_o ) )
return MAKE_o // : return the configuration object for further customization
}
Eleventy provides you with simple but efficient configuration options.
Eleventy provides a wide range of settings for data used in building stage or inside content: global data as well as template and directory data.
11tyTips uses only global data contained in JavaScript files located inside the data directory.
const A_o = require( './A_o.js' )
const U_o =
{
dev_b: true, //: development/production switch
//dev_b: false,
url_s: null,
DEV_s: A_o.LOCAL_s,
PRO_s: A_o.URL_s,
GIT_s: `https://github.com/${A_o.AUTHOR_s}/${A_o.ID_s}/`,
TWI_s: `https://twitter.com/${A_o.ID_s}/`,
RSS_s: `${A_o.URL_s}feed.xml`,
SERVICE_PATH_s: 'assets/scripts/js/service_worker.min.js',
HOME_s: `[Home page]: ${A_o.URL_s}`,
NODE_s : `[Node.js]: https://nodejs.org`,
COMMENT_s: `[utteranc.es]: https://github.com/utterance/utterances`,
FRAME_s: '11ty Frame',
FRAME_URL_s: `https://11tyframe.netlify.com/`,
ELEVENTY_s: `[Eleventy]: https://11ty.io`,
ELEVENTY_JFM_s: `/docs/data-frontmatter/#javascript-front-matter`,
ELEVENTY_UDF_s: `/docs/data-frontmatter/#user-defined-front-matter-customizations`,
OUTLINK_s: '{target="_blank" rel="noreferrer noopener"}',
}
void function () { U_o.url_s = U_o[U_o.dev_b === true ? 'DEV_s' : 'PRO_s'] } ()
console.log( `Site URL: ${U_o.url_s}` )
module.exports = U_o
The gobal data files can contain not only constants (as in the preceding listing of the U_o.js
file )but also functions (as in the following D_o.js
file )which means that all the power of Node.js is at hand.
const EXPORT_a = // default exported data
[
'date',
'layout',
'permalink',
'tags',
'rank_n',
'title_s',
'subtitle_s',
'abstract_s',
'author_s',
'A_o',
]
module.exports =
{
data__o: ( permalink_s, collection_a ) =>
{
//> console.log( permalink_s )
let export_o = {}
collection_a.forEach( collection_o =>
{
const data_o = collection_o.data
if ( data_o.permalink === permalink_s )
{
if ( data_o.export_a === null ) export_o = data_o //: get all data!
else
{
const export_a = data_o.export_a || EXPORT_a //: get declared or default data only
export_a.forEach( prop_s => export_o[prop_s] = data_o[prop_s] )
}
}
} )
return export_o
},
}
(h1 element)
This is an introduction paragraph which should be limited in length.
(p element, styled with attribute “page_intro”)
font-style: italic
)Paragraph content have a maximum line length fixed to 60ch, yielding to a number of of about 70 characters per line: this is considered the best line length for readibility. (p element)
for ( let at = 0; at < keys_a.length; ++at )
(inline code)
for ( let at = 0; at < keys_a.length; ++at )
(example)
module.exports = make_o =>
{
let markdown_o =
{
html: true,
linkify: true,
typographer: true
}
make_o.setLibrary('md',
require( 'markdown-it' )( markdown_o )
.use( require( 'markdown-it-attrs' ) )
.use( require( 'markdown-it-deflist' ) )
.use( require( 'markdown-it-include' ), make_o.pagesPartsDir_s )
)
make_o.setLibrary('njk',
require('nunjucks')
.configure( make_o.matrixDir_s, { autoescape: false } ) ) //: autoescape for CSS rules
}
(code block)(Unordered list)
(Nested unordered lists)
How to identify variable types using a consistent naming scheme.
Every developer knows that JavaScript is not a static typed language, a useful feature eliminating lots of bugs. A language like Typescript has been created as a remedy to that important lack of safety. Even for code modules counting less than a few tens of lines, it’s easy to forget what kind of type is exactly a variable or constant declared at the begining of the file and then make a mistake when assining a wrong type to a variable.
11tyTips follows the TypesJS naming scheme for a cleaner and more meaningful code. Here is how it looks in action (the following listing is the service worker file integrated in 11tyTips which relies heavily on the excellent article by Thomas Hunter II: On using Service Workers with Static Content ):
/**
* Unlike most Service Workers, this one always attempts to download assets from the network.
* Only when network access fails do we fallback to using the cache.
* When a request succeeds we always update the cache with the new version.
* If a request fails and the result isn't in the cache then we display an offline page.
*/
const ID_s = '{{A_o.ID_s}}'
const KEY_n = 1 //: initial cache version
const CACHE_s = `${ID_s}_${KEY_n}` //: name of the current cache
const URL_a = //: URLs of assets to immediately cache
[
'{{U_o.url_s}}',
'{{U_o.url_s}}index.html',
'{{U_o.url_s}}menu.html',
'{{U_o.url_s}}offline.html',
'{{U_o.url_s}}assets/scripts/js/service_worker.min.js',
'{{U_o.url_s}}assets/scripts/js/lib.min.js',
'{{U_o.url_s}}assets/styles/css/lib.min.css',
'{{U_o.url_s}}favicon.ico',
]
/**
* Iterate thru URL_a and add cache each entry
*/
const install__v = install_o =>
{
install_o.waitUntil( caches.open( CACHE_s )
.then( cache_o => cache_o.addAll( URL_a ) )
.then( self.skipWaiting() ) )
}
/**
* Remove inapplicable caches entries
*/
const activate__v = activate_o =>
{
activate_o.waitUntil( caches.keys()
.then( entry_a => entry_a.filter( entry_s => entry_s !== CACHE_s ) )
.then( remove_a => Promise.all( remove_a.map( remove_s => caches.delete( remove_s ) ) ) )
.then( () => self.clients.claim() ) )
}
/**
* Always try to download from server first
*/
const fetch__v = fetch_o =>
{
fetch_o.respondWith( fetch( fetch_o.request )
.then( response_o =>
{
cache__v( fetch_o.request, response_o ) //: download is successful: cache the result...
return response_o.clone() //... and display it
} )
.catch( error_o => cache__o( error_o ) ) //: error_o means network fail (offline...)
)
}
/**
* Try to fetch a cache version if network access issues
*/
const cache__o = fetch_o =>
{
return caches.match( fetch_o.request )
.then( response_o =>
{
return response_o || //: We have a cached version, display it
caches.open( CACHE_s ) //: We don't have a cached version, display offline page
.then( cache_o => cache_o.match( new Request( `{{U_o.url_s}}offline.html` ) ) )
} )
}
/**
* Put successful fetch in cache
*/
const cache__v = ( request_o, response_o ) =>
{
caches.open( CACHE_s )
.then( cache_o => cache_o.put( request_o, response_o ) )
}
self.addEventListener('install', install_o => install__v( install_o ) )
self.addEventListener('activate', activate_o => activate__v( activate_o ) )
self.addEventListener('fetch', fetch_o => fetch__v( fetch_o ) )
As a side note, it’s worth noting that a declaration like
const ID_s = ‘{{A_o.ID_s}}’
is only made possible by the use of a minification filter during which Nunjucks variables are expanded before writing the JavaScript file! But it’s another story: minifying…
Front matter, the initial section of each Markdown file, offers much more than you think.
Each Markdown file has its own data, declared at the begining of the file in the front matter part (11tyTips uses a JavaScript Object
for the front matter. ), a few ones being mandatory (no so strickly speaking! For instance, you’re not requested to use a Date, but it’s more than useful if you want to sort your posts by date. ), others being used to supply some page specific content or variables (have a look at Eleventy#user-defined-front-matter-customizations for a list of Eleventy properties usable in front matter. ).
Mandatory data
Specific data
---js
{
date: `2019-12-12`,
layout: `frame.njk`,
permalink: `tips/frontmatter_data.html`,
tags: [ `tip` ],
eleventyExcludeFromCollections: false,
rank_n: 8,
title_s: `Front matter`,
subtitle_s: `Front matter data howto`,
abstract_s: `Accessing front matter data in Markdown and templates`,
author_s: `Octoxalis`,
output__s: output_s => output_s.replace( /\[% raw %\]/g, '' ).replace( /\[% endraw %\]/g, '' ),
}
---
To access any property declared in the front matter it has to be enclosed in double parenthesis {{ ... }}
(11tyTips uses Nunjucks. Using other templating systems, this is a bit different. ). For instance, the abstract_s
property in the front matter is injected in this page with the following code: {{ abstract_s }}
and renders as:Accessing front matter data in Markdown and templates
.
If you use JavaScript for the front matter (a very good idea because it gives you the full power of the language to process your content. )and Nunjucks as templating system, you can declare functions as properties (see Eleventy#javascript-front-matter documentation page. ). Usually, apart very specific cases, it’s much more easy to declare content processing functions in a module located inside the data directory (because it will be accessible from any Markdown content or any template and with the possibility to require any Node package that could be useful. ).
However, this page front matter is such a case: the output__s
front matter last property is a conventional way to execute a function after processing the entire page (see also frontmatter function page. ).
11tyTips is full of Eleventy documentation links: we need official references! Some of these references can appear in different pages and therefore they are potential global data. 11tyTips source has an F_o.js
file inside its matter/assets/scripts/js/lib
directory where a eleventyUrl__s
function computes the link to any Eleventy docs page using an acronym of the page and anchor.
eleventyUrl__s: key_s =>
{
const path_s = U_o[ `ELEVENTY_${key_s}` ]
const anchor_n = path_s.indexOf( '#')
if ( anchor_n === -1 ) //: return a link to 11ty.io
{
console.log( `ALERT! no anchor found in path: ${path_s}` )
const ref_n = U_o.ELEVENTY_s.indexOf( ':' )
return { ref_s: U_o.ELEVENTY_s.substring( 0, ref_n ), link_s: U_o.ELEVENTY_s }
}
const anchor_s = path_s.substring( anchor_n )
const anchorLink_s = U_o.ELEVENTY_s.replace( ']', `${anchor_s}]`) + path_s
return { ref_s: anchorLink_s.substring( 0, anchorLink_s.indexOf( ':') ), link_s: anchorLink_s }
}
The acronyms (usually three characters are enough to get a unique identifier:JFM_s
for #javascript-front-matter
,UDF_s
for #user-defined-front-matter-customizations
. ), used as keys, could be set inside each page front matter and used as a key by the function to expand the actual link reference.
Hence to get the reference and the link of a reference-style link is just as easy as:
{{ F_o.eleventyUrl__s( 'JFM_s' ).ref_s }}
{{ F_o.eleventyUrl__s( 'JFM_s' ).link_s }}
Actually, most of Eleventy link keys are gathered in the U_o.js
global data file and not in the front matter!
But we can do more, using Nunjucks {% set %}
tag in each Markdown file referencing an Eleventy documentation page, then call the link function as in the following examples:
{{ _11ty__s( 'JFM_s' ).link_s }}
(Reference-style link located in Links section, after the Aliases section. )
{{ _11ty__s( 'JFM_s' ).ref_s }}
(reference inside content. )
[comment]: # (======== Aliases ========)
{% set _11ty__s = F_o.eleventyUrl__s %}
{{ _11ty__s( 'JFM_s' ).link_s }}
{{ _11ty__s( 'UDF_s' ).link_s }}
[comment]: # (======== Post ========)
Shortcodes are probably the most powerful tool to process Markdown content inline.
They are simple to create and use, almost anything can be done with them, and they represent an important opportunity that should not be missed. Eleventy has two kinds of shortcode:
{% _shortcode_id "argument" %}
{% _shortcode_id %}... Content to be processed ...{% end_shortcode_id %}
11tyTips doesn’t use a lot of shortcodes, but they are essential to its content. Let’s dissect the most omnipresent of it: the _code_block
paired shortcode. It is passed to Eleventy configuration method addPairedShortcode
this way:
make_o.addPairedShortcode('_code_block', content_s => CODES_o.code_block__s( content_s ) )
Here, _code_block
is the shortcode identifier (11tyTips uses a leading underscore character because the shortcode closing tag adds the word end
before the shortcode identifier: end_shortcode
is more readable than endshortcode
, isn’t it? )
Shortcode anatomy (to be continued...)
Use the power of Node packages inside Eleventy.
Build tools are tedious: that’s my opinion, when it comes to simple and static sites. And one of the strength of Eleventy is that you can bypass build tools to accomplish the necessary step of minifying some assets to speed up pages downloading. Of course, you need some processing to reduce all CSS or JavaScript files, but Node is full of packages to do that processing and compress those files and it would be a pity to ignore that power: simply call it up directly within your Eleventy build step.
One of the immediate benefices is that you can therefore split your assets in small chunks to manage them very easily during the development phase.
The realm of filters (to be continued...)
For frequently used bits of content a shorthand notation is a useful practice.
Using shorthand notation in markdown files is not only for lazy authors: it yields a more semantic code. (to be continued...)
For specific processing needs of a page, you can declare functions in the front matter section.
A singular use case of a front matter function (see frontmatter data page for an introduction to front matter properties. )is when you want to process the output of the template engine. For instance, in Nunjucks, if you want to output something that would normaly be processed as a Nunjucks block (exactly what I’m doing writing this page! ), you have to enclose it in a [% raw %][% endraw %]
tags pair. But it doesn’t work if you want to output only one tag of the pair either the [% raw %]
or [% endraw %]
tag. The shortcoming is simple: use a front matter function.
In the markdown file, the raw
tags are writen by replacing the curly bracket characters ({
and }
) by the square bracket characters ([
and ]
) and reverted to curly brackets after the template processing with the front matter function.
By convention, I call this front matter function output__s
and it takes as argument the template engine output, to process it the way I want.
---js
{
//...
output__s: output_s => output_s.replace( /\[% raw %\]/g, '{% raw %}' ).replace( /\[% endraw %\]/g, '{% endraw %}' )
}
---
This output__s
function is automaticaly invoqued (if it exists in the front matter part of any Markdown file) at the end of the global frame template passing the template engine result previously captured by a Nunkucks {% set %} {% endset %}
block.
{%- set _template_s %}
{{- _head_block_s | safe | head_end( data_o ) -}}{# head process #}
{{- _body_block_s | safe | body_end( data_o ) -}}{# body process #}
{% endset -%}
{%- if output__s %}{% set _template_s = output__s( _template_s ) %}{% endif -%}
However, you are not at all constrained to process the output of the template engine globally: you can process only a part of it if you see fit as well as you can use multiple processing functions and multiple invocations. It’s just a question of enclosing the output to process in a set
block.
It can be useful to process the final result of the templating work or prepare it by some pre processing.
All templating engines have limits and Nunjucks (which is used here by 11tyTips )has its own. But you can easily go your way beyond those limits to process the output of the templating engine just before everything is engraved as a static HTML page. For instance you may want to overcome the encoding output of the engine and make some modifications (see also the frontmatter data tip for an example of post processing the template engine output without a global filter, but using a front matter specific callback function. ).
Once again this is acquired thru the use of a filter and an awesome help of the Nunjucks {% set %}
block. Every global template (i.e. a template which produces a full html
page. )has a starting block (in the following listing it’s the ante process comment line. )using a filter whose concern is to initialize some variables needed by the page which is about to be processed by the templating engine (it can be, for instance, a data base access (server-side), or some checking relative to the pages that have been previously processed, etc. ).
Similarly, there is an ending block whose filter processes the output of the template engine, once all the template work has been done, allowing you to further process the output. (in the following listing it’s the post process comment line. )
{% set data_o = D_o.data__o( permalink, collections.all ) %}
{{- '' | template_start( data_o ) -}}{# ante process #}
{%- set _head_block_s %}
{% include "parts/blocks/_head_.njk" %}
{% endset -%}
{%- set _body_block_s %}
{% include "parts/blocks/_body_.njk" %}
{% endset -%}
{%- set _template_s %}
<!doctype html><html lang="{{A_o.LANGUAGE_s}}">
{{- _head_block_s | safe | head_end( data_o ) -}}{# head process #}
{{- _body_block_s | safe | body_end( data_o ) -}}{# body process #}
</html>
{% endset -%}
{%- if output__s %}{% set _template_s = output__s( _template_s ) %}{% endif -%}
{{- _template_s | safe | template_end( data_o ) | minify_html -}}{# post process #}
The filters invoqued as previously described are also used to make any specific processing required just before the first template is to be processed by Eleventy and just after the last template has been processed. It’s kind of a hook, as can be seen is some frameworks, inside Eleventy. This ante or post processing uses only a directory listing to count the number of template files to be processed and invoque the starting function if no one has already been processed or the ending function if the number of files processed is equivalent to the listing count (this simple algorithm is based on the fact that all posts are in a single directory, without any subdirectories (otherwise the algorithm would have to walk thru all subdirectories), and that this flat
directory contains only Markdown files. ).
const STRING_o = require( './string.js' )
let files_a = null
let count_n = 0
let current_n = 0
void function ()
{
const MD_DIR_s = './matter/pages/' //: all Mardown files
const DEPTH_n = 0 //: ...are located at the root level of MD_DIR_s
files_a = require( 'klaw-sync' )( MD_DIR_s, { nodir: true, depthLimit: DEPTH_n } )
if ( files_a ) count_n = files_a.length
} ()
const buildStart__v = data_o =>
{
console.log( `${count_n} Markdown files to process` )
}
const buildEnd__v = data_o =>
{
//... what else?
}
const templateStart__s = ( input_s, data_o ) =>
{
let start_s = input_s
//... what else?
return start_s
}
const templateEnd__s = ( input_s, data_o ) =>
{
let end_s = input_s
//... what else?
return end_s
}
const headEnd__s = ( input_s, data_o ) =>
{
let head_s = input_s
//... what else?
return head_s
}
const bodyEnd__s = ( input_s, data_o ) =>
{
let body_s = input_s
//... what else?
return body_s
}
module.exports =
{
start__s: ( input_s, data_o ) =>
{
if ( !files_a ) return input_s
if ( current_n === 0 ) buildStart__v( data_o )
let start_s = templateStart__s( input_s, data_o )
return start_s
},
head__s: ( input_s, data_o ) => headEnd__s( input_s, data_o ),
body__s: ( input_s, data_o ) => bodyEnd__s( input_s, data_o ),
end__s: ( input_s, data_o ) =>
{
++current_n
let end_s = templateEnd__s( input_s, data_o )
if ( current_n === count_n ) buildEnd__v( data_o )
return end_s
},
}
Any kind of processing can be done inside the starting and ending functions: in 11tyTips, the starting function output the number of Markdown files to be processed and the ending function create the file (menu.html
) listing the pages referenced as links in the menu of the site.
To have full control on the site data, these functions take as argument an Object
gathering all or a selection of (made according to the EXPORT_a
Array declared in the F_o.js
global data file )all declared global data (including the page
Object used for pagination, all the collection Objects and even global properties not usually accessible: content
and layoutContent
… ). These data are retrieve by a simple set
tag at the begining of the base template:
{% set data_o = F_o.data__o( permalink, collections.all ) %}
and are used by all the template processing functions (the data_o
argument ).
In Nunjucks, inheriting templates can have private variables.
When you rely on template inheritance (using the {% extends %}
declaration ), you cant’ have a {% block %}{% endblock %}
inside a variable set
block. The other way, a variable declared inside a {% block %}{% endblock %}
will not be accessible outside. Therefore, you can’t gather all variables in a global variable passed as an argument to the processing filter. However, you still have the possibility to process each block individualy.
When using variables declared with the set
block, never forget that any variable whose name begins with one or more underscore character is private. Therefore it can not be imported outside of its block scope (unfortunatly, the Nunjucks documentation doesn’t state it: I found this important note in the Jinja2 documentation. ).
Creating a menu with previous and next links.
There are many ways to sort a collection of posts, some of them provided out-of-the-box by Eleventy (see Eleventy#sorting documentation page. ). However, 11ty Tips doesn’t use the formated date front matter property but a specific one, named rank_n
(it’s an integer positive number ), allowing to sort a posts collection according a unique value regardless of the date (you could have multiple posts with the same date, unless you use a full date with hours, minutes and seconds… )This rank_n
should therefore be a unique index (because it is used to retrieve the previous and next pages of any given post in the menu list; however, a duplicate rank_n
index doesn’t cause any disturbance when retrieving those links (see infra). ).
const sort_o =
{
//> Sort a tag collection
//> according to rank_n front matter property
sortByRank__a: ( collection_a, tag_s ) =>
{
return collection_a
.getFilteredByTag( tag_s )
.sort( ( current_o, other_o ) => current_o.data.rank_n - other_o.data.rank_n )
}
}
module.exports = make_o =>
{
make_o.tag_a.forEach( tag_o => make_o.addCollection( tag_o.tag_s,
collection_a => sort_o[tag_o.sort_f]( collection_a, tag_o.tag_s ) ) )
}
To display the global menu in every page of the site, an HTML fragment is built with a template listing all pages as links in an ordered list, each one having its page rank_n
front matter property recorded as a data-rank
attribute.
The main loop of the menu template iterates thru the posts collection previously sorted and add every useful data that we want to display in the menu itself or as a clue of the previous and next pages: permalink
, rank_n
, title_s
, etc.
{%- set _collection_s = A_o.COLLECTION_s %}
{# .... #}
{%- set _menu_list_s %}
{% for _item_o in collections[_collection_s] %}
{% if _item_o.data.tags == _collection_s %}
{% set _link_s = _item_o.data.permalink.slice( 0, _html_n ) %}
-
{{pad__s( _item_o.data.rank_n )}}
{{_item_o.data.title_s}}
({{_item_o.data.abstract_s}})
{{_item_o.data.subtitle_s}}
{% endif %}
{% endfor %}
{% endset -%}
Previous and next posts links are much less difficult to retrieve on the client side than at build time. For that reason, the menu template doesn’t try to create a double linked list but instead delegates the work to a JavaScript function run in the browser. For any post page displayed by the browser, we have in the menu HTML fragment built by the template all necessary data about the preceding and following pages relative to the current one (when they exist: the first page in the menu list has no previous page and the last one no next page! ).
For that we have to search the DOM for the nodes having a data-rank
attribute with values surrounding that one of the current page.
//> retrieve previous + next pages info
const linkNear__o = link_s =>
{
const list_e = document.querySelector( `[data--="menu_list"]` )
const extension_n = '.html'.length
const location_s = window.location.pathname.slice( 1, -extension_n ) //: trim '/' at start
const list_a = document.querySelectorAll( `[data--="menu_list"] > li` )
if ( !list_a ) return //: undefined
const list_n = list_a.length
const current_e = list_e.querySelector( `[data-link="${location_s}"]` )
const rank_n = +current_e.getAttribute( 'data-rank' )
const near_n = ( link_s === 'link_previous' ) ? rank_n - 1 : rank_n + 1
if ( near_n < 1 || near_n > list_n ) return //: undefined
const near_e = list_e.querySelector( `[data-rank="${near_n}"]` )
if ( !near_e ) return //: undefined
const a_e = near_e.querySelector( `span > a` )
if ( !a_e ) return //: undefined
const span_e = near_e.querySelector( `span[data--="note_content"]` )
if ( !a_e ) return //: undefined
return {
link_s: near_e.getAttribute( 'data-link' ),
title_s: a_e.innerHTML,
//?? subtitle_s: '',
abstract_s: span_e.innerHTML,
}
}
A link is only a link and doesn’t convey a lot of meaning by itself apart its URL (the href
attribute ). Unveiling the title and some other pieces of data before fetching a previous or next post is a much more useful help. The data previously retrieved in the surrounding links of the current page are there to be used and we can display them as we like (bellow the navigation bar at the top of the page ).
//> insert in DOM previous + next pages info
//> show/hide previous + next pages info block
const linkNear__v = ( event_s, link_e ) =>
{
if ( link_e === null ) return
if ( event_s === 'mouseenter' )
{
const link_s = link_e.getAttribute( 'data--' )
let title_s
let abstract_s
const near_o = linkNear__o( link_s )
if ( near_o !== undefined )
{
title_s = `${near_o.title_s} ⤴`
abstract_s = `${near_o.abstract_s}`
}
else
{
title_s = 'No more tip'
abstract_s = ''
}
document.querySelector( '[data--="link_title"]' ).innerHTML = title_s
document.querySelector( '[data--="link_abstract"]' ).innerHTML = abstract_s
}
document.querySelector( '[data--="link_info"]' )
.classList.toggle( 'retract' )
}
Do you really need SASS?
CSS preprocessors as SASS have become an important step in the front-end landscape to simplify stylesheets creation and maintening. However they have lost a bit of prominance since the apparition of the new CSS rising star: Custom properties AKA CSS variables
. And JAMstack static site generators as Eleventy could be another factor of preprocessors darkening because they give you all the bolts you need to create the elements you want to use and re-use with pure CSS and Vanilla JavaScript.
11tyTips contains an example of a pseudo-CSS file which, preprocessed by the Nunjucks template engine, produces a genuine CSS file with a few repeats of the same @font_face
CSS rule pattern (only three repeats because 11tyTips is very thrifty regarding the number of fonts beeing used ).
/* defaults: styles: 'Regular', type_s: 'woff2' */
{{
[
{ family_s:'Harmattan', file_s: 'harmattan-v5-latin-regular' },
{ family_s:'Athiti', file_s: 'athiti-v3-latin-regular' },
{ family_s:'Fira Code', file_s: 'FiraCode-Regular' }
] | font_face( U_o.url_s )
}}
But where is CSS in this .css
file? Nowhere actually: the CSS fragment is created by the requested filter called font_face
. And, apart the opening and closing double braces {{ ... }}
(pointing out the declaration of a JavaScript variable ), everything is a JavaScript Array
syntax declaration, an Array
to be processed by that font_face
filter.
Once again, the substitution is made by a magical Nunjucks filter registered at configuration step in the 11ty
directory filters.js
file.
//...
const MIXIN_o = require('../lib/css_mixin.js')
make_o.addFilter('font_face', ( face_a, ...args_ ) => MIXIN_o.font_face__s( face_a, ...args_ ) )
//...
The face_a
argument of this filter is the Array
declared in the previous _font_face_mixin_.css
file:
module.exports =
{
font_face__s: ( face_a, ...args_ ) =>
{
if ( !face_a || !args_ ) return ''
let code_s = ''
face_a.forEach( face_o =>
{
const family_s = face_o.family_s
const file_s = face_o.file_s
const style_s = face_o.style_s || 'Regular'
const type_s = face_o.type_s || 'woff2'
code_s += `
@font-face
{
font-display: swap;
font-family: '${family_s}';
font-style: normal;
font-weight: 400;
src:
local('${family_s}'),
local('${family_s}-${style_s}'),
url('${args_[0]}assets/fonts/${file_s}.${type_s}')
format('${type_s}');
}`
}
)
return code_s
},
}
The output of this CSS-like preprocessing is inlined (for performance concerns )in the page head
section thru the _font_note_.njk
template. Of course this is only available if you put a "dataTemplateEngine": "njk"
property inside your Eleventy configuration file (see eleventy configure page ).
Data template engine processing, offered not only for Nunjucks but other templating systems, is a major strengh of Eleventy static site generator relevant not only for CSS preprocessing but also for JavaScript files preprocessing. Hence some 11tyTips JS files make use of global data variables or constants as (emphasized in the following examples ):
const idb_o = new KVIdb( ‘{{A_o.ID_s}}_idb’, ‘{{A_o.ID_s}}_store’ )
const key_s = location_s.slice( location_s.lastIndexOf( ‘{{A_o.COLLECTION_s}}s/’), -extension_n )
But there’s more to explore there…
Where are my images?
Web images are an endless challenge: we want them wide, beautiful, we want their colors, their evocation power, but in no case their slowness. Evidently images have to be optimised to be as light as possible. 11tyTips uses the outstanding Compress-Or-Die site (Every front-end developer should visit this site not only for the impressive compression engine proposed by Christoph Erdmann but also for a thorough understanding of the JPEG format )to lighten and transform all images for a maximum speed gain. Nevertheless, 11tyTips goes further and enforces an on-demand image loading paradigm: any image is loaded only when the site visitor does want to open the Pandora box (this is my personal approach to the lazy loading pattern, both lighter and simpler to implement than the usual Observer pattern )!
To implement this pattern, the trick is to hide by default all images that do not need to be displayed inconditionnaly (therefore any image that is a substantial part of the site page (logo, hero, etc.) is not concerned by this on-demand pattern ), and unhide them, one by one, at a later time, when a specific action is triggered by the visitor. That specific action is simply the opening of an inline note, tailored for the image case, as in the following note
)
{% _note_img %}
![Adam de Coster][1PX]{data-src="{{U_o.url_s}}assets/media/Adam_de_Coster_gray.jpg" data-size="150"}
{% end_note_img %}
[comment]: # (======== Links ========)
[1PX]: {{U_o.url_s}}assets/media/1px.jpg "OnePixel"
The _note_img
shortcode has two parts:
![Adam de Coster][1PX]
part ){data-src="{{U_o.url_s}}assets/media/Adam_de_Coster_gray.jpg"}
part )As soon as the site visitor clicks the note index for the first time, the image is loaded. This is not an automatic displaying but a manual one: There is an image here, do you want to see it? If you do, here it is!
(Of course, if the image has a big size and the network flow is slow, the image will load slowly. Nevertheless, I think the user experience is much better having an overall very fast page loading and a potentially slow image loading, because this is a well known case, and, moreover, there is no need for any blurring or degraded image which only accentuate the waiting perception and finally slow down even more the genuine image display. )
For the sake of convinience, specific sizes can be specified for each image in the Markdown source, for instance 50%
:
) The data-size
attribute takes either a unique value (it will be applied to the width of the image, the height beeing supplied by the browser, keeping the image ratio ):
data-size=“200”
or two values separated by a space
character: (applied to the width then the height of the image ):
data-size=“200 200”
Since size values are converted in a style attribute added to the image HTML tag, any compliant CSS value can be used: percentage
, rem
, etc.
):
data-size=“25%”
When no size is added to the inline attribute, by default the image width is 100% of the page width (and fills the whole available space of its container: if its intrisinc size is less that this one it scales up! ). The _note_img
shortcode also accept an Array
argument to put a legend (each slot of this Array
is a new line of the legend )under the image displayed
Adam de Coster
Young women holding a distaff).
{% _note_img [ 'Adam de Coster', 'Young women holding a distaff' ] %}
Because a true image declaration is needed by the Markdown processor, a one pixel sized image data URI (see this base64 png pixel generator )is systematically used as a placeholder: it’s very light and 11tyTips has a CSS rule to prevent it to be displayed anyway. And it can be inserted at the end of the Markdown file like it is here, with the an helper function:
[comment]: # (======== Links ========)
[1PX]: {{ F_o.img1px__s() }} "A young woman holding a distaff before a lit candle"
And here is the final code produced:
<ins data--="note_img">
<sup></sup>
<span data--="note_content">
<em class="note_link_a">
<a class="note_link"
role="button"
onclick="loadColorImg__v( this, 'gray', )"> ⤵</a>
</em>
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
alt="Adam de Coster"
title="A young woman holding a distaff before a lit candle"
data-src="{{ U_o.url_s }}assets/media/Adam_de_Coster_gray.jpg"
data-size="100%"><br>
<em class="note_img_title">Adam de Coster [1586-1643]</em><br>
<b class="note_img_subtitle">Young women holding a distaff before a lit candle</b>
</span>
</ins>
When images displayed using the on-demand loading mechanism described in this tip are an important matter for the site content (i.e. images are a substantial part of the content, not just an illustration to decorate the text content ); when image compression must preserve a high rendering quality; the image size can still be heavy, more than a few hundreds of kilobytes. A useful technique is to propose, at first, a meaningful presentation of the image in grayscale instead of full color! Actually, grayscale images, eliminating the less meaningful part of the image data: color, and retaining only the most important one: luminosity, are so useful to the sensory perception that, in many cases, they are the most important step of the content understanding.
As a consequence, 11tyTips can fire a dual step loading mechanism: first, a grayscale image then, only if needed (if the site visitor does want it: in the following example, by clicking the button above the image ), second, the full size colored image, which replaces the first one ⤵
Adam de Coster [1586-1643]
Young women holding a distaff before a lit candle).
Most of the time, the image tag title
attribute (an its factotum the alt
attribute )is enough regarding image identification. But sometimes it can be necessary to display more information (for instance a title, a place, a credit, etc. ): this is the purpose of the legend Array
mentioned above. But some reactivity can be necessary too. This is the purpose of another shortcode, named note_link
: this is an Array
of strings, each one enclosing a function name with its dedicated symbol and arguments (if the function has arguments ).
{% note_link [ 'loadColorImg__v, ⤵, gray, color' ] %}
Each slot of this Array
yields a link, displaying the function symbol, which triggers the function itself. In the previous image note, the function loadColorImg__v
loads the heavy full colored image coming to replace the much lighter gray one initialy loaded, which acts as an image preview (Light grayscale image (500 x 720 pixels): 18 698 bytes
Heavy full color image (1388 x 2000 pixels): 158 852 bytes
Ratio is 0.11
This ratio could even be much less using a smaller grayscale image, if the aim were to just have an image preview, instead of a gray image to study contrasts, luminosity, etc. ).