Get started
You can use this starter as a template for your blog and you are ready to go! But there are some adjustments you have to make.
src/_data/meta.js
- sets different “hard coded” text values all over the template, you can change the language, etc.- edit your personal details and social media in
src/_data/personal.yaml
. - Open
src/assets/css/global/blocks/external-link.css
. Replace “eleventy-excellent.netlify.app” with your own domain. This is about the external link indicators, they are matched with your domain. If you don’t want to use external link indicators, feel free to delete the whole style rule. You can also add the classno-indicator
to any anchor element you want to except from this rule.
If you are working with VS Code I recommend installing the “Tailwind CSS IntelliSense” addon, as it works also for our custom utility classes. That said, you will find that Tailwind CSS does not work as you might expect. I explain this in one of the blog posts.
The blog posts also explain / show some features that aren’t covered here.
“Docs” lol
CSS
Add and delete your globally available custom block stylesheets in src/assets/css/global/blocks/*.css
.
The methodology used is CUBE CSS.
The CSS system of this starter was invented by Andy Bell. If you want to know exactly how it all works, and have a look at the (further elaborated) original, read this article on piccalil.li.
New in version 3.0: Inline CSS
The main CSS file is now inline in production to improve performance, see .src/_includes/head/css-inline.njk
.
You can add per-page or component bundles of CSS. Instead of adding your CSS file to the src/assets/css/global/blocks/
directory, you can place them in src/assets/css/bundle/
. All CSS files in there will be stored alongside global.css
in .src/_includes/css/
. You can now include them in the “inline” bundle only on pages or components where you need them:
{% css "inline" %}
{% include "css/your-stylesheet.css" %}
{% endcss %}
New in version 3.0: Component CSS
All CSS files placed in src/assets/css/components/
will be sent to the output folder, where components can reference them: /assets/css/components/*.css
.
New in version 3.0: Debugging CSS
In src/assets/css/global.css
you can decomment @import-glob 'tests/*.css';
to include CSS for debugging.
It makes visible when your code wrapped in <is-land>
elements is being hydrated, where things might overflow and many other warnings and errors that Heydon Pickering came up with.
Card
Previous to version 3, the card component was a Nunjucks include. There are a number of things that had to be set before being able to use it, like the level of the heading or whether tags shall be displayed. WebC makes this easier, as you can now use the custom element and opt in to the different slots.
Available slots:
- image: the
image
shortcode has theslot="image"
assigned to itspicture
orfigure
wrapper by default! - headline: display the card's main title
- date and tag: Grouped within the classes
meta
andcluster
for date and tagging information. - content
- footer: for links or whatever footer information
I added some variants, avaliable via attribute selectors:
img-square
: Enforces a square aspect ratio for images.clickable
: Makes the whole card clickable.no-padding
: Removes padding and background modifications.
Usage
<custom-card>
{% image "path-to-img", "alt-text" %}
<span slot="date"></span>
<span slot="tag" class="button post-tag"></span>
<h2 slot="headline"></h2>
<p slot="content"></p>
<footer slot="footer"></footer>
</custom-card>
Example
Utopia
Among them, there is no sort of traffic, no knowledge of letters, no understanding of numbers, no name of magistrates, nor of politics, only of virtues; and they measure all things by barleycorns; their money, plate, and other ornaments they so diligently polish that no rust can stick to them.
The order does not matter
Just title and content
They have no lawyers among them, for they consider them as a sort of people whose profession it is to disguise matters and to wrest the laws [...].
This card has no padding
Red Hat's first logo appeared on an early invoice. It was a simple, bright red brimmed top hat placed above the words "Red Hat Software."
Config
I like to divide things into small thematic areas, it helps me orient myself better. Configurations are structured into separate modules in src/_config
and are then imported into the main configuration file.
- collections.js: Manages Eleventy collections such as posts and tags: https://www.11ty.dev/docs/collections/
- events.js: For code that should run at certain times during the compiling process: https://www.11ty.dev/docs/events/
- filters.js: Used within templating syntax to transform data into a more presentable format: https://www.11ty.dev/docs/filters/
- plugins.js: Everything I or Eleventy considers to be a plugin: https://www.11ty.dev/docs/plugins/
- shortcodes.js: Defines shortcodes for reusable content: https://www.11ty.dev/docs/shortcodes/
Each configuration category (filters, plugins, shortcodes, etc.) is modularized. or example, dates.js
within the filters
folder contains date-related filters.
import dayjs from 'dayjs';
export const toISOString = dateString => dayjs(dateString).toISOString();
export const formatDate = (date, format) => dayjs(date).format(format);
These individual modules are then imported and consolidated in a central filters.js
file, which exports all the filters as a single default object.
import {toISOString, formatDate} from './filters/dates.js'; // more imports
export default {
toISOString,
formatDate,
// more exports
};
Integration in Eleventy Config
In the main Eleventy configuration file (eleventy.config.js
), these modules are imported:
import filters from './src/_config/filters.js';
import shortcodes from './src/_config/shortcodes.js';
They are then used to register filters and shortcodes with Eleventy, using this nice concise syntax:
eleventyConfig.addFilter('toIsoString', filters.toISOString);
eleventyConfig.addFilter('formatDate', filters.formatDate);
// More filters...
eleventyConfig.addShortcode('svg', shortcodes.svgShortcode);
This method hopefully keeps the Eleventy config clean and focused, only concerning itself with the registration of functionalities, while the logic and definition remain abstracted in their respective modules.
Some time ago I wrote a blog post about how to organize the Eleventy configuration file where I go a little bit deeper into this topic.
Design tokens
Edit all your preferences (colors, fluid text sizes etc.) in src/_data/designTokens/*.json
.
Additional colors, variants and gradients for custom properties are automatically created in src/assets/css/global/base/variables.css
based on the colors set in colors.json
. If you change names you should edit variables.css
as well and check if the names are used elsewhere in the template.
Admittedly, I went a little bit crazy there with the new CSS color syntax stuff.
In the style guide you can see how everything turns out.
Details
The <custom-details>
WebC component has a corresponding Nunjucks include.
It uses the <details>
and <summary>
elements to create a collapsible section and enhances them aesthetically and functionally.
The JavaScript for the <custom-details>
component adds functionality to buttons to expand and collapse the sections with one action. When JavaScript is disabled, the sections are still accessible and collapsible, but the extra buttons are hidden.
On page load, it checks if a hash corresponding to a details ID exists in the URL. If such an ID is found, the corresponding details section is programmatically opened, allowing direct navigation to an open section from a shared URL.
The sorting is set by default on “alphabetic”, but you can also pass in “shuffle” or “reverse” as a parameter (directly in the details.njk
partial).
Usage
{% set itemList = collections.docs %}{% include 'partials/details.njk' %}
Example
You are in the middle of a custom details component!
Easteregg
The <custom-easteregg>
component is by default in the base layout in src/_layouts/base.njk
. Just delete the two lines if you don’t want to use it. The component is
designed to trigger a confetti effect when a user types a specific keyword sequence. It uses the dynamic import of the canvas-confetti
library to render custom-shaped particles based on user input.
Defaults:
- Keywords:
"eleventy"
,"excellent"
- Shape:
"⭐️"
- Particle Count:
30
Customizable Attributes:
keyword
: custom keywordshape
: custom shape for the confetti particles using emojis or textparticle-count
: number of particles to release during the effect
<script type="module" src="/assets/scripts/components/custom-easteregg.js"></script>
<custom-easteregg keyword="yay" shape="🌈" particle-count="50"></custom-easteregg>
Favicons
All “necessary” favicons are in src/assets/images/favicon
, and copied over to the root of the output folder.
I chose the sizes based on the recommendations from the How to Favicon article on evilmartians.com.
You can place them in that directory manually, or use the script to autmate the process:
npm run favicons
In this case define the SVG icon on which all formats are based on in meta.js
:
export const pathToSvgLogo = 'src/assets/svg/misc/logo.svg'; // used for favicon generation
Regardless of whether you generate the icons automatically or create them manually, it is best to keep the names so as not to break any reference to them.
Fonts
This starter uses two custom fonts, Red Hat Display and Inclusive Sans. You can add or delete fonts in src/assets/fonts
.
I often create font subsets using the Fontsquirrel Webfont Generator.
Next, edit src/assets/css/global/base/fonts.css
.
Add your new font aliases in src/_data/designTokens/fonts.json
.
Finally, in src/_includes/head/preloads.njk
edit the font preloads.
JavaScript
This starter has no real JS dependency. If JavaScript is not available, components that rely on it – like the theme switcher – will be hidden. If you opted in for the drawer menu, pills will be shown instead.
There are two kinds of bundles for JavaScript in this starter, see .src/_includes/head/js-inline.njk
and .src/_includes/head/js-defer.njk
.
By default, I include Eleventy’s is-land framework and the theme toggle inline.
You can include more scripts like so:
{% js "inline" %}
{% include "scripts/your-inline-script.js" %}
{% endjs %}
Same goes for scripts that should be defered:
{% js "defer" %}
{% include "scripts/your-defered-script.js" %}
{% endjs %}
Scripts stored in src/assets/scripts/components/
are sent to the output folder, while scripts in src/assets/scripts/bundle/
are sent to .src/_includes/scripts/
, from where you can include them in the respective bundle.
Some components are enhanced with JavaScript.
Masonry
Masonry layout is not yet a native part of CSS grid. There is a debate if using grid-template-rows: masonry;
is actually the best way to implement it.
It should be used carefully so we don’t create confusion with the tabbing order. In version 3 of the starter I made the masonry layout a web component, and no longer a opt-in feature (was: masonry: true
in the front matter).
<custom-masonry>
is designed to function as a masonry grid by dynamically adjusting item positions based on the available column space and the size of its content. The necessary JavaScript (custom-masonry.js
) is loaded only once per component usage due to the data-island="once"
attribute.
Optional: pass in layout="50-50"
to set a 50% width for each column.
If no JavaScript is available, the grid will fall back to the regular grid layout defined in src/assets/css/global/compositions/grid.css
.
<custom-masonry> (children) </custom-masonry>
<custom-masonry layout="50-50"> (children) </custom-masonry>
Open Graph images
You can see a preview of the OG images in a blog post.
They are referenced in meta-info.njk
:
<meta
property="og:image"
content="{{ meta.url }}
{% if (layout == 'post') %}/assets/og-images/{{ title | slugify }}-preview.jpeg
{% else %}{{ meta.opengraph_default }}
{% endif %}"
/>
To change the look and behaviour of those images and replace the SVG background edit src/common/og-images.njk
.
The implementation is based on Bernard Nijenhuis article.
If you want to be inspired, have a look at what Lea is doing here.
Previously, the images were created at build time, but this leads to not rendering the font – if the system executing the build has not installed the font, it will not be used.
A solution would be to always build the page yourself and then place it on the server directly.
Or, as Sophie Koonin does, explicitly specify the system to be used for the build, and select a font that this system has installed by default.
At the moment I have relocated the creation of the images in the development process, so that the font only needs to be installed on your own system. The images are located in src/assets/og-images
and are comitted.
This is fine as long as you only work with markdown and the font is always installed on your system. How this works if a CMS is involved remains to be seen 😅.
Consider that the domain is a hard coded value in the front matter in src/common/og-images.njk
.
Let me know if you encounter any problems.
Pagination
The blog posts use Eleventy’s pagination feature. The logic for this can be found in tha partial src/_includes/partials/pagination.njk
, the layout src/_layouts/blog.njk
includes it, how many entries should be on a page is defined in src/pages/blog.md
.
If you do not want any pagination at all, it is easiest to set a very high number for the pagination size, for example:
pagination:
data: collections.posts
size: 10000
In src/_data_/meta.js
you can set some values for the visible content (previous / next buttons) and the aria labels.
You can also hide the number fields between the previous and next buttons by setting paginationNumbers
to false
.
blog: {
// other adjustments
paginationLabel: 'Blog',
paginationPage: 'Page',
paginationPrevious: 'Previous',
paginationNext: 'Next',
paginationNumbers: true
}
If you want to change the collection that is paginated (by default collections.posts
), you must do so in two places: the front matter of the template, src/pages/blog.md
:
pagination:
data: collections.posts
and where the pagination component is included: src/_layouts/blog.njk
:
<!-- set collection to paginate -->
{% set collectionToPaginate = collections.posts %}
<!-- if the number of items in the collection is greater than the number of items shown on one page -->
{% if collectionToPaginate.length > pagination.size %}
<!-- include pagination -->
{% include 'partials/pagination.njk' %}
{% endif %}
Platforms (icons)
Find and set your platform icons in src/assets/svg
, in the “platform” directory.
In src/_data/personal.yaml
you can edit the platforms. The key should be the same as the name of the icon. For example: mastodon: 'https://front-end.social/@lene'
and src/assets/svg/platform/mastodon.svg
.
https://simpleicons.org/ features a great variety of free SVG icons for popular platforms.
SVG
All SVG icons used in the starter are in src/assets/svg
. There is a directory dedicated to the dividers, the platform icons and a general folder called “misc”.
Shortcode
The svg.js
shortcode, introduced in version 3, allows for the seamless inclusion of SVG files. Located in src/_config/shortcodes/svg.js
, this shortcode requires only the folder and file name of the SVG, omitting the file extension. By default, SVGs are injected with an aria-hidden="true"
attribute. The SVGs should be stored in the src/assets/svg
directory, and referenced using the format "folder/svg-name"
.
{% svg "path", "aria-name", "class-name", "inline-style" %}
{% svg "misc/star", "A yellow star icon", "spin", "block-size: 4ex; fill: var(--color-tertiary);" %}
The star icon resoves to:
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24" aria-label="A yellow star icon" style="block-size: 4ex; fill: var(--color-tertiary)" class="spin"><path> (...) </path></svg>
Theme (dark / light mode)
With version 2.0 I introduced dark and light mode. This is not intended to be a gimmick, but to support the accessibility of the site. And I tend to read blog posts at night, and if there is no dark mode then I leave the tab for later, and we all know how that ends.
Dark and light mode respects the user agent or operating system settings using the prefers-color-scheme
CSS media feature. But there is also a switch in the <footer>
.
If you want to change something here, for example replace the button with a sun/moon icon, make sure to preserve accessibility.
The accessible name (“Theme dark” or “Theme light”) is put together by aria-labelledby="theme-switcher-label"
resolving to “Theme” and the <button>
s “light” and “dark”.
If your site is in another language, you can adjust those values in _data/meta.js
, for both the content of the <button>
s and their aria-label
.
If you remove the “light” and “dark” <button>
and you don’t use the heading with the ID theme-switcher-label
, the value for the accessible name must be obtained differently.
If you find any accessibility issues, please let me know.
What can be deleted
Everything, of course
This is still meant to be a starter, even though it grew to be more like a template.
If you want to keep the defaults, but get rid of the example content, delete the following files and archives:
github.js
insrc/_data/
builtwith.json
insrc/_data/
- all files in
src/posts
- the directory and all files in
src/docs
- all pages in
src/pages
, though you might want to keepindex.njk
as a starting point - You can delete
screenshots
,blog
andgallery
insrc/assets/images
.
Keep thefavicon
andtemplate
folders though.
If you don’t want to feature any code examples, you may delete the whole stylesheet for syntax highlighting: src/assets/css/global/blocks/code.css
.
In general, any CSS block in there is optional.
Youtube
The @slug
attribute is used to pass the video ID to the <lite-youtube>
element.@label
ist used for the nested <custom-youtube-link>
which adds a link to watch on YouTube. This also serves as a fallback in case JS is deactivated.
Uses the Lite YouTube Embed repository.
<custom-youtube @slug="Ah6je_bBSH8" @label="Alberto Ballesteros - Artista Sin Obra"> </custom-youtube>