Creating a Website with 11ty
I've been running my personal website for years, but I've never actually documented how I built it. And since I haven't written an article in ages I started by redoing my website from scratch using 11ty. 🙃 In this article, I'll walk through the process of creating my website using 11ty (Eleventy), including the tools and workflows I use to manage content.
Why 11ty?
I chose 11ty for several reasons:
- Simple and focused: It does one thing well: generating static sites from markdown and templates
- Before I use Jekyll: but building Jekyll, was always a hassle to setup. Maybe it's me, maybe it's Ruby.
- Flexible: Works with Nunjucks, Markdown, or any template language
- Fast: Builds are nearly instantaneous,
npm run serveto type and see updates on the fly - JavaScript-based: Full access to Node.js ecosystem, which I'm a lot more familliar with than Ruby.
Project Structure
My site follows the following structure:
src/
├── _includes/ # Reusable templates
├── css/ # Stylesheets
├── img/ # Images
└── posts/ # Content
├── articles/
├── snippets/
└── short-stories/
I also pepper in images within subfolders and collect them with these lines added to .eleventy.js
eleventyConfig.addPassthroughCopy("src/css");
eleventyConfig.addPassthroughCopy({ "src/**/*.pv.*": "img/" });
eleventyConfig.addPassthroughCopy({ "src/**/*.main.*": "img/" });
eleventyConfig.addPassthroughCopy({ "src/**/*.zip": "download/" });
For syntax highlighting I use @11ty/eleventy-plugin-syntaxhighlight.
Image Processing Workflow
I use ImageMagick to process images before publishing. This ensures optimized, web-ready images, with meta data removed.
mk_image.sh
This script resizes images to web-friendly dimensions:
#!/bin/bash
# mk_image.sh - Resize image to 1024x1024 cropped and create 128x128 preview
# Usage: ./mk_image.sh input_image.jpg
set -e
if [ $# -lt 1 ]; then
echo "Usage: $0 input_image.jpg"
exit 1
fi
INPUT_FILE="$1"
# Check if input file exists
if [ ! -f "$INPUT_FILE" ]; then
echo "Error: File '$INPUT_FILE' not found"
exit 1
fi
# Get directory and filename
INPUT_DIR=$(dirname "$INPUT_FILE")
FILENAME=$(basename "$INPUT_FILE")
NAME="${FILENAME%.*}"
EXT="${FILENAME##*.}"
# Convert HEIC to JPG
if [[ "$EXT" == "heic" ]] || [[ "$EXT" == "HEIC" ]]; then
OUTPUT_NAME="${NAME}.jpg"
# Convert HEIC to JPG using sips
sips -s format jpeg "$INPUT_FILE" --out "${INPUT_DIR}/${OUTPUT_NAME}" > /dev/null
INPUT_FILE="${INPUT_DIR}/${OUTPUT_NAME}"
FILENAME="${OUTPUT_NAME}"
NAME="${OUTPUT_NAME%.*}"
EXT="jpg"
fi
# Output paths (same folder as source)
PREVIEW_FILE="${INPUT_DIR}/${NAME}.pv.jpg"
RESIZED_FILE="${INPUT_DIR}/${NAME}.main.jpg"
echo "Processing '$INPUT_FILE'.."
# Check if output files already exist
if [ -f "$RESIZED_FILE" ] && [ -f "$PREVIEW_FILE" ]; then
echo "Output files already exist, skipping"
echo " $RESIZED_FILE"
echo " $PREVIEW_FILE"
exit 0
fi
# Resize to 1024x1024 cropped from center with web optimization
convert "$INPUT_FILE" \
-resize '1024x1024^' \
-gravity center \
-crop '1024x1024+0+0' \
+repage \
-quality 85 \
-strip \
-interlace Plane \
"$RESIZED_FILE"
echo "Created: $RESIZED_FILE"
# Create 128x128 cropped preview from center with web optimization
convert "$INPUT_FILE" \
-resize '128x128^' \
-gravity center \
-crop '128x128+0+0' \
+repage \
-quality 85 \
-strip \
"$PREVIEW_FILE"
echo "Created: $PREVIEW_FILE"
echo "Done!"
mk_all_images.sh
Process all images in a directory at once:
#!/bin/bash
# mk_all_images.sh - Process all images in a folder using mk_image.sh
# Usage: ./mk_all_images.sh input_folder
set -e
if [ $# -lt 1 ]; then
echo "Usage: $0 input_folder"
exit 1
fi
INPUT_FOLDER="$1"
# Check if input folder exists
if [ ! -d "$INPUT_FOLDER" ]; then
echo "Error: Folder '$INPUT_FOLDER' not found"
exit 1
fi
# Get script directory for mk_image.sh path
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Processing all images in '$INPUT_FOLDER'..."
# Find and process all images, excluding .pv. and .main. variants
for file in "$INPUT_FOLDER"/*; do
# Skip if not a file
[ -f "$file" ] || continue
filename=$(basename "$file")
# Skip preview and resized images
if [[ "$filename" == *.pv.* ]] || [[ "$filename" == *.main.* ]]; then
continue
fi
# Skip non-image files (basic check)
if ! [[ "$filename" =~ \.(jpg|jpeg|png|gif|webp|bmp|heic|HEIC)$ ]]; then
continue
fi
echo "Processing: $filename"
"$SCRIPT_DIR/mk_image.sh" "$file"
done
echo "Done!"
Image Macro in Nunjucks
I use a Nunjucks macro to render images across my site:
{% macro image(src, alt, caption=None) %}
<figure class="image-wrapper">
<img src="/img/{{ src }}" alt="{{ alt }}" class="image" />
{% if caption %}
<figcaption>{{ caption }}</figcaption>
{% endif %}
</figure>
{% endmacro %}
And call it with
{% from "image.njk" import image %}
{{ image("image.main.jpg", "ALT text", "Optional Caption") }}
This macro:
- Wraps images in semantic
<figure>elements - Adds the
image-wrapperclass for consistent styling - Optionally includes a caption
- References images from the
/img/directory
The style I use is:
.image-wrapper {
text-align: center;
margin: 2rem 0;
}
.image-wrapper .image {
max-width: 66.666%;
width: 100%;
height: auto;
display: block;
margin: 0 auto;
border-radius: 4px;
}
.image-wrapper figcaption {
margin-top: 0.5rem;
font-size: 0.875rem;
}
Fyi: if you wnat to render nunjucks statements in code blocks, use this syntax around the block:
{% raw %}
...
{% endraw %}
Writing Content
Posts are written in Markdown with YAML frontmatter:
---
title: "My Post Title"
date: 2026-03-01
tags: ["tag1", "tag2"]
layout: article.njk
---
Content goes here...
Building
To build the site, run:
npm run build
The output in the dist/ folder is ready to be deployed to any static hosting service.
Discover content by tag: