This guide will provide a comprehensive overview of Hugo and Eleventy, two popular static site generators, focusing on their latest features, best practices, and practical applications. It is assumed that the reader has foundational knowledge of static site generators or equivalent general programming experience.
Chapter 1: Introduction to Static Site Generators (SSGs)
1.1 What are SSGs?
Static Site Generators (SSGs) are tools that compile content and templates into plain HTML, CSS, and JavaScript files. Unlike traditional Content Management Systems (CMS) like WordPress, which generate pages dynamically on each request, SSGs pre-build all pages. This results in highly performant, secure, and easily deployable websites.
1.2 Why Choose a Static Site Generator?
- Performance: Pre-built files load significantly faster as there’s no server-side processing or database queries on demand. This leads to higher Lighthouse scores and better user experience.
- Security: Without a database or server-side scripting, the attack surface is drastically reduced, making static sites inherently more secure against common web vulnerabilities.
- Scalability: Static sites can be easily served from Content Delivery Networks (CDNs), which distribute content globally and handle high traffic loads efficiently without significant infrastructure costs.
- Developer Experience: Many SSGs offer streamlined workflows, allowing developers to use familiar tools like Markdown and Git for content management.
- Cost-effectiveness: Hosting static sites is often cheaper as they require less server resources. Many platforms offer free static site hosting (e.g., GitHub Pages, Netlify, Vercel, Cloudflare Pages).
- Version Control: Content is typically stored as plain text files, making it easy to track changes using version control systems like Git.
1.3 Hugo vs. Eleventy: A Quick Comparison
Both Hugo and Eleventy are excellent SSGs, but they cater to slightly different preferences:
| Feature | Hugo | Eleventy |
|---|---|---|
| Language | Go | JavaScript (Node.js) |
| Build Speed | Blazing fast (sub-second for thousands of pages) | Very fast (excellent in JavaScript SSG benchmarks) |
| Learning Curve | Moderate (Go templating syntax can be unique) | Beginner-friendly (familiar JavaScript/Node.js ecosystem) |
| Flexibility | Highly opinionated, but very powerful built-in features | Unopinionated, highly flexible, leverages Node.js ecosystem for plugins |
| Plugins | Built-in features, limited external plugin API | Extensive community plugins |
| Templating | Go Templates | Supports multiple (Nunjucks, Liquid, Markdown, WebC, etc.) |
Hugo is often chosen for its unparalleled speed and a rich set of built-in features, making it a powerful choice for large, content-heavy websites. Its Go templating might take some getting used to for developers not familiar with Go.
Eleventy is favored for its simplicity, flexibility, and the ability to leverage the vast JavaScript ecosystem. Its unopinionated nature allows developers to use their preferred templating languages and integrate with existing Node.js tools.
Chapter 2: Deep Dive into Hugo (Latest Stable: v0.128.0)
Hugo is renowned for its speed and powerful features, written in Go. It’s ideal for developers who prioritize performance and comprehensive built-in capabilities.
2.1 Key Concepts and Architecture
- Content Organization: Hugo structures content based on directories. Top-level directories within
content/are automatically treated as sections.. ├── archetypes/ ├── content/ │ ├── posts/ <-- Section │ │ ├── _index.md │ │ └── my-first-post.md │ └── about/ <-- Section │ └── _index.md ├── layouts/ ├── static/ ├── themes/ └── config.toml - Front Matter: Metadata defined at the beginning of content files (YAML, TOML, JSON) that controls page behavior (e.g., title, date, layout, tags).
- Templates (Go Templates): Hugo uses Go’s
html/templateandtext/templatelibraries. Templates render content, resources, and data into HTML.single.html: Renders individual pages.list.html: Renders a collection of related content (e.g., a blog archive, category page).baseof.html: A root template that other templates can extend, defining the overall page structure.partials/: Reusable template components (e.g., header, footer, navigation).
- Shortcodes: Reusable snippets of code within Markdown content, allowing for complex content embedding (e.g.,
).
My Image
- Taxonomies: A flexible system for classifying content (e.g., categories, tags, authors).
- Modules: Hugo’s core building blocks, powered by Go modules. They allow for packaging and reusing themes, archetypes, layouts, data, assets, and more across projects.
2.2 Installation and Basic Usage
Installation:
Hugo is a single binary. You can download it directly from the official GitHub releases or use a package manager:
- macOS (Homebrew):
brew install hugo - Windows (Chocolatey):
choco install hugo -confirm - Linux (APT/Debian):
sudo apt update sudo apt install hugo
Verify Installation:
hugo version
Create a New Site:
hugo new site my-hugo-site
cd my-hugo-site
Add a Theme (Example using hugo-book):
git init
git submodule add https://github.com/alex-shpak/hugo-book themes/hugo-book
echo 'theme = "hugo-book"' >> config.toml
Copy example content:
cp -R themes/hugo-book/exampleSite/content.en/* ./content
Create New Content:
hugo new posts/my-first-post.md
This will create content/posts/my-first-post.md with default front matter.
Run Development Server:
hugo server --buildDrafts --watch
This command starts a local server with live reloading, including draft content. Access it typically at http://localhost:1313.
Build Static Files:
hugo -D
This generates the static site in the public/ directory (including drafts).
2.3 New Features in Hugo v0.126.x - v0.128.0
These versions brought significant enhancements, particularly in handling external data and front-end tooling.
2.3.1 Content Adapters (v0.126.0)
What it is: Content Adapters allow Hugo to dynamically generate pages from external data sources (e.g., JSON, YAML, TOML, XML, CSV) during the build process. This feature was a long-awaited addition, enabling Hugo to build complex sites with data not directly stored as Markdown files.
Why it was introduced: Before content adapters, generating a large number of pages from external data required more complex workarounds. This feature simplifies the process of creating dynamic-like content from static data sources, making Hugo more versatile for applications like product catalogs, directories, or API-driven content.
How it works: You define a content adapter in your config.toml (or similar configuration file) that specifies the remote data source and how it should map to pages.
Example (Complex): Generating pages from a remote JSON API
Let’s assume you have a remote API endpoint that returns a list of products in JSON format:
[
{
"id": "prod-1",
"name": "Product A",
"description": "Description for product A.",
"price": 19.99
},
{
"id": "prod-2",
"name": "Product B",
"description": "Description for product B.",
"price": 29.99
}
]
1. Create data/products.json (or directly use resources.GetRemote):
While you can fetch directly, a common pattern is to cache it or define it more explicitly for reusability. For simplicity, let’s say you’d define a products adapter. In a real scenario, resources.GetRemote would fetch this during the build.
2. Configure the Content Adapter in config.toml:
[contentAdapters.products]
url = "https://api.example.com/products"
type = "json"
path = "products" # This will create content/products/ based on the data
[contentAdapters.products.frontmatter]
title = "{{ .name }}" # Map JSON 'name' to page 'title'
description = "{{ .description }}"
price = "{{ .price }}"
# Define a permalink structure for these pages
_target = "/products/{{ .id | urlize }}/index.md"
Explanation:
contentAdapters.products: Defines an adapter namedproducts.url: The URL of your remote JSON API.type: The format of the data (e.g.,json,yaml,toml,csv).path: The virtual content path where these generated pages will reside (e.g.,content/products/).frontmatter: A mapping from your external data fields to Hugo’s front matter variables. You can use Go template syntax ({{ .fieldName }}) to access data from the current item._target: Crucially, this specifies the output path and file name for each generated page.urlizeconverts theidinto a URL-friendly slug.
Hugo will now fetch the JSON data, iterate over each item, and create a virtual Markdown file for each, populating its front matter based on your mapping. You can then use standard Hugo templates (e.g., layouts/products/single.html) to render these dynamically created pages.
Tip: For performance, use resources.GetRemote and Hugo’s caching mechanisms for remote data.
2.3.2 HTTP Caching and Live Reloading of Remote Resources (v0.127.0)
What it is: This feature introduces proper HTTP caching for external resources fetched using resources.GetRemote, allowing for more efficient development and faster builds, especially when working with remote APIs or large external files. It also enables live reloading in hugo server when these remote resources change.
Why it was introduced: Previously, resources.GetRemote would refetch assets on every build, even if they hadn’t changed, leading to slower build times. This new caching mechanism optimizes this by respecting HTTP cache headers (like ETag and Last-Modified) and polling for changes, drastically improving development workflow for sites relying on external data.
How it works: You configure HTTP caching and polling in your config.toml.
Example:
[httpcache]
[httpcache.cache]
[httpcache.cache.for]
includes = ['https://sheets.googleapis.com/**'] # Cache Google Sheets data
[[httpcache.polls]]
low = '1s' # Check every 1 second after a recent change
high = '30s' # Gradually increase to 30 seconds if stable
[httpcache.polls.for]
includes = ['https://sheets.googleapis.com/**']
Explanation:
[httpcache.cache.for.includes]: Defines which remote URLs should be cached.[[httpcache.polls]]: Defines polling intervals for live reloading inhugo server.lowandhigh: Set the minimum and maximum polling intervals (e.g.,1sfor 1 second,30sfor 30 seconds).
Tip: Combine this with Content Adapters to build highly dynamic-feeling static sites that leverage external data efficiently.
2.3.3 TailwindCSS v4 (Alpha) Support (v0.128.0)
What it is: Hugo now includes experimental (alpha) support for TailwindCSS v4, allowing developers to compile Tailwind utility classes into CSS directly within Hugo’s asset pipeline.
Why it was introduced: TailwindCSS is a popular utility-first CSS framework. Direct integration simplifies the setup and workflow for developers who prefer Tailwind, providing an optimized output.
How it works: You use the new css.TailwindCSS function in your templates.
Example:
{{ $style := resources.Get "css/main.css" | css.TailwindCSS | minify | fingerprint }}
<link rel="stylesheet" href="{{ $style.RelPermalink }}" integrity="{{ $style.Data.Integrity }}">
This snippet would typically process a main.css file that @imports Tailwind directives and then generates the optimized CSS.
Tip: Refer to the official Hugo documentation and TailwindCSS v4 alpha documentation for the most up-to-date configuration details, as this feature is still in early development.
2.3.4 templates.Defer Function (v0.128.0)
What it is: The templates.Defer function allows you to defer the execution of a template block until later in the rendering process. This can be useful for injecting content at specific points or controlling the order of operations.
Why it was introduced: It provides more granular control over template rendering, enabling advanced patterns such as accumulating scripts or styles in a header/footer without needing to pass them explicitly through many partials.
How it works: You define a deferred block using {{ define "deferred_name" }} and then use {{ templates.Defer "deferred_name" }} where you want it to be inserted.
Example (Simple):
In a partial layouts/partials/head.html:
<head>
<title>{{ .Title }}</title>
{{ templates.Defer "additional_head_scripts" }}
</head>
In a single.html layout:
{{ define "additional_head_scripts" }}
<script src="/js/my-page-script.js"></script>
{{ end }}
<h1>{{ .Title }}</h1>
<p>{{ .Content }}</p>
The script defined in single.html will be rendered within the head section of the baseof.html (or wherever partials/head.html is included).
2.3.5 Performance Improvements for $page.GetTerms (v0.128.0)
What it is: The $page.GetTerms function, used to retrieve terms associated with a page, has received significant performance optimizations, especially for larger sites.
Why it was introduced: For sites with many pages and extensive use of taxonomies, $page.GetTerms could become a bottleneck. This improvement makes operations involving taxonomies much faster.
How it works: This is an internal optimization; you simply continue using $page.GetTerms as before, and your site will benefit from the speedup.
Example:
{{ with .GetTerms "tags" }}
<ul class="tags">
{{ range . }}
<li><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></li>
{{ end }}
</ul>
{{ end }}
2.3.6 Goldmark Parser Extensions (v0.126.0, affected by v0.128.0 changes)
What it is: Hugo’s Markdown parser, Goldmark, received extensions for easily producing HTML ins (inserted text), mark (highlighted text), subscripts, and superscripts directly from Markdown, reducing the need for inline HTML.
Why it was introduced: To provide more native Markdown syntax for common text formatting, making content creation simpler and more consistent.
How it works (initial intention):
- Inserted text:
++This is inserted text.++(renders to<ins>This is inserted text.</ins>) - Mark text:
==This is mark text.==(renders to<mark>This is mark text.</mark>) - Subscript:
H~2~O(renders toH<sub>2</sub>O) - Superscript:
1^st^(renders to1<sup>st</sup>)
Note on v0.128.0: A change in the Goldmark parser (related to Hugo Issue #12597) in v0.128.0 might affect how these specific extensions behave or are rendered. Always test thoroughly when upgrading.
2.3.7 Deprecation of paginate to pagination.pagerSize (v0.128.0)
What it is: The paginate configuration option in config.toml has been deprecated in favor of pagination.pagerSize for controlling the number of items per page in pagination.
Why it was introduced: To consolidate pagination-related settings under a more logical pagination key, improving configuration clarity and consistency.
How to migrate:
- Old (deprecated):
paginate = 10 - New (recommended):
[pagination] pagerSize = 10
Tip: Check your config.toml for paginate and update it to pagination.pagerSize to avoid future warnings or breaking changes.
2.3.8 IsServer Field Requirement (Post v0.120.0)
What it is: If you encounter errors like "can't evaluate field IsServer in type interface {}", it means your Hugo version is older than 0.120.0 and a theme or template is trying to use the .IsServer function, which was introduced in that version.
Why it was introduced: The .IsServer function allows templates to conditionally render content or behave differently when running in the Hugo development server (hugo server) versus a production build.
How to fix:
- Recommended: Update your Hugo version to
0.120.0or newer. - Alternative: If updating is not immediately possible, remove the
{{ if hugo.IsServer }}checks from your template code.
2.4 Advanced Hugo Techniques
2.4.1 Module Mounts for Large Projects
What it is: Hugo Modules allow you to create a “virtual union file system” by mounting directories from different sources, including other modules or even non-Hugo projects. This is particularly useful for managing large projects or complex monorepos.
Why it’s useful: For very large sites (e.g., thousands of posts), hugo server can become slow during development as it processes all content. Module mounts with exclusions allow you to “hide” or exclude parts of your content during local development to drastically speed up build and rebuild times, while still including all content in the final production build.
How it works: You define [[mounts]] in your config.toml (or config/development/modules.toml for environment-specific settings) with source, target, and excludeFiles patterns.
Example (Excluding old posts in development):
In config/development/modules.toml:
[[mounts]]
source = "content"
target = "content"
excludeFiles = ['posts/200*', 'posts/201*'] # Exclude posts from 2000-2019
Explanation:
source = "content": The original content directory.target = "content": Where the content should be mounted (the same directory, but with exclusions).excludeFiles: A list of glob patterns to exclude. Here, any file or directory starting with200or201within thepostsdirectory will be ignored.
When you run hugo server --environment development, these older posts will not be processed, making your local development incredibly fast. When you run hugo for production (without --environment development), all content will be built.
Tip: Use this technique to segment your content for faster iteration, particularly in documentation sites or large blogs.
2.4.2 Optimizing URL Management and Routing
Hugo provides flexible URL management to ensure clean and consistent links, especially important for SEO and migration from other platforms.
Key concepts:
- Default Behavior: By default, Hugo generates URLs based on your content file path (e.g.,
content/posts/my-post.mdbecomes/posts/my-post/). - Front Matter
slug: Overwrites the last segment of the URL path. - Front Matter
url: Overwrites the entire URL path for a page. permalinksConfiguration: Global patterns inconfig.tomlto define URL structures for specific content types.
Example (Migrating from WordPress-like routes):
Suppose your old WordPress site had URLs like /YYYY/MM/DD/post-title/. You can configure Hugo to match this structure or redirect.
1. Using url in Front Matter for specific pages:
---
date = '2025-03-15T17:00:00+01:00'
title = 'My Old Blog Post'
url = '/2025/03/15/my-old-blog-post/' # Explicitly set the full URL
---
This is an old post from my WordPress blog.
2. Using permalinks in config.toml for consistent structure:
[permalinks]
posts = "/:year/:month/:day/:slug/"
Explanation: This will generate URLs like /2025/03/15/my-old-blog-post/ for all content under the posts section.
Common Pitfall & Solution (Nginx Redirects): When migrating, old URLs might still be indexed by search engines. You’ll need to set up server-side redirects (e.g., Nginx, Apache, or hosting provider redirects) to point old URLs to new ones.
Example Nginx Redirect:
server {
listen 80;
server_name old-blog.com;
location = /wp-content/uploads/2024/05/MarpCheatsheet.pdf {
return 301 /MarpCheatsheet.pdf; # Redirect a specific asset
}
location ~* ^/category/symfony/?$ {
return 301 /tags/symfony/; # Redirect an old category to a new tag
}
}
Tip: Plan your URL structure carefully during migration. Use Hugo’s url or permalinks for direct mapping, and implement 301 redirects for any URLs that change to preserve SEO.
2.4.3 Segmented Rendering (v0.124.0)
What it is: Segmented rendering allows you to build only specific parts of your Hugo site based on configured segments. This is a powerful performance feature for very large sites.
Why it’s useful: Instead of rebuilding the entire site (which can take minutes for massive projects), you can instruct Hugo to render only relevant sections. This is beneficial for:
- Faster Development: Render only the section you’re currently working on.
- Scheduled Rebuilds: Rebuild frequently updated sections (e.g., homepage, news) more often than the entire site.
- Targeted Output: Generate specific output formats (e.g., a JSON index for search) for a subset of content.
How it works: You define segments in config.toml with include and exclude filters based on page kind, language, output format, or path. Then, you use the --renderSegments flag when building.
Example:
In config.toml:
[segments]
[segments.docsSegment]
[[segments.docsSegment.includes]]
path = '/docs/**' # Include all pages under /docs/
[[segments.docsSegment.excludes]]
lang = 'de' # Exclude German language docs
Building only the documentation segment:
hugo --renderSegments docsSegment
You can specify multiple segments:
hugo --renderSegments blogSegment,newsSegment
Tip: Combine with CI/CD pipelines to create efficient deployment strategies for large, complex Hugo sites.
2.4.4 Fixing Common Encoding Problems
What it is: Hugo, by default, is security-conscious and escapes HTML entities in output to prevent cross-site scripting (XSS) vulnerabilities. However, this can sometimes lead to unwanted HTML entities (<, >, &) appearing in contexts where raw HTML or unescaped text is expected (e.g., OpenGraph meta tags, JSON feeds).
Why it happens: This is a security feature. Hugo’s templating engine automatically escapes output by default.
How to fix: Use Hugo’s built-in template functions to explicitly plainify, htmlUnescape, or control JSON output encoding.
Example 1: OpenGraph Meta Tags
If your OpenGraph description (og:description) shows HTML entities instead of plain text:
{{- $description := .Description | default .Summary | plainify | htmlUnescape | safeHTMLAttr -}}
<meta property="og:description" content="{{ $description }}">
Explanation:
.Description | default .Summary: Gets the page description or summary.plainify: Removes all HTML tags.htmlUnescape: Converts HTML entities (e.g.,&to&,<to<) back to their original characters.safeHTMLAttr: Marks the string as safe for use within an HTML attribute, preventing further escaping for attribute context.
Example 2: JSON Feed Content If you’re generating a JSON feed (e.g., for ActivityPub) and content contains HTML entities or incorrectly serialized quotes:
{
"content": {{ .Content | htmlUnescape | jsonify (dict "noHTMLEscape" true) }}
}
Explanation:
.Content: The raw HTML content of the page.htmlUnescape: Converts HTML entities to their characters.jsonify (dict "noHTMLEscape" true): Converts the result to a JSON string. ThenoHTMLEscape: trueoption is crucial here to prevent JSON from escaping characters like<,>,&as Unicode escape sequences (\u003c). Use with caution, as this can introduce XSS vulnerabilities if the input is untrusted. For self-generated static content, it’s generally safe.
Example 3: Image URLs in RSS/Atom Feeds When embedding an image within an RSS feed’s HTML content, ensure the URL and any HTML tags are correctly encoded.
{{ "<" | html }}img src="{{ .Params.featured_image | absURL | urlquery }}" alt="{{ .Title | urlquery }}"{{ ">" | html }}
Explanation:
{{"<"|html}}and{{"/"|html}}: Explicitly output the<and>characters for the<img>tag without them being HTML-escaped..Params.featured_image | absURL | urlquery: Gets the image path, converts it to an absolute URL, and then URL-encodes it to be safe within a URL query string.
Tip: Always understand the context in which your content is being rendered (HTML attribute, plain text, JSON) and apply the appropriate Hugo template functions (safeHTML, safeHTMLAttr, plainify, htmlUnescape, jsonify, urlquery) to control escaping behavior. Prioritize security by default and only unescape when necessary for specific output formats.
Chapter 3: Deep Dive into Eleventy (Latest Stable: v3.1.2, Canary: v4.0.0-alpha.4)
Eleventy (11ty) is a simpler static site generator, known for its flexibility and JavaScript-centric approach. It empowers developers to use their preferred templating languages and leverage the vast Node.js ecosystem.
3.1 Key Concepts and Architecture
- Data Cascade: Eleventy’s powerful data system where data from various sources (global data files, directory data files, front matter, computed data) merges in a predictable order.
- Template Languages: Supports a wide array of template engines out-of-the-box: Markdown, Nunjucks, Liquid, Handlebars, EJS, Pug, Haml, HTML, JavaScript (as templates), JSX, TypeScript, and WebC.
- Collections: A way to group content dynamically based on tags, directories, or custom logic, making it easy to create lists, archives, and related content.
- Shortcodes: Reusable functions in templates to insert dynamic content or complex HTML structures.
- Filters: Functions applied to template data to transform it (e.g., date formatting, slugification, content truncation).
- Passthrough File Copy: Directly copies files (like CSS, JS, images) from your source to the output directory without processing them.
3.2 Installation and Basic Usage
Prerequisites: Node.js (v18 or higher is recommended for Eleventy v3+).
Installation:
npm install @11ty/eleventy --save-dev
Create a New Project:
mkdir my-eleventy-site
cd my-eleventy-site
npm init -y
npm install @11ty/eleventy --save-dev
Basic .eleventy.js Configuration:
Create a .eleventy.js file in your project root:
module.exports = function(eleventyConfig) {
// Passthrough copy for static assets
eleventyConfig.addPassthroughCopy("src/css");
eleventyConfig.addPassthroughCopy("src/images");
// Input and output directories
return {
dir: {
input: "src",
output: "public"
}
};
};
Create Content:
Create src/index.md:
---
layout: "base.njk"
title: "Welcome to My Eleventy Site"
---
Hello, world! This is my first Eleventy page.
Create src/_includes/base.njk:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>{{ title }}</h1>
</header>
<main>
{{ content | safe }}
</main>
<footer>
<p>© 2025 My Eleventy Site</p>
</footer>
</body>
</html>
Create src/css/style.css:
body {
font-family: sans-serif;
margin: 2em;
background-color: #f4f4f4;
}
h1 {
color: #333;
}
Run Development Server:
npx @11ty/eleventy --serve
Access at http://localhost:8080.
Build Static Files:
npx @11ty/eleventy
This generates the static site in the public/ directory.
3.3 New Features in Eleventy v3.0.0 - v4.0.0-alpha.4
Eleventy’s recent versions focus on modernizing the codebase, improving performance, and enhancing flexibility, particularly around JavaScript module support and data handling.
3.3.1 Full ESM Support (v3.0.0)
What it is: Eleventy core is now written in ECMAScript Modules (ESM), and it provides full support for ESM in your Eleventy projects for configuration files, data files, and 11ty.js templates.
Why it was introduced: To align with modern JavaScript development practices, enable better tree-shaking, and improve module loading. While it supports ESM, it also maintains backward compatibility with CommonJS.
How it works:
You can now write your .eleventy.js config file and JavaScript data files using import/export syntax.
Example (ESM Config):
// .eleventy.js (ESM)
export default async function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("src/assets");
return {
dir: {
input: "src",
output: "public"
}
};
};
Example (CommonJS Config - still supported):
// .eleventy.js (CommonJS)
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("src/assets");
return {
dir: {
input: "src",
output: "public"
}
};
};
Breaking Change (for some cases): If you were requireing bundled Eleventy plugins from CommonJS, you might need to change to await import.
3.3.2 Asynchronous Configuration (v3.0.0)
What it is: Your .eleventy.js configuration file can now be an async function, allowing you to perform asynchronous operations (like fetching data) directly within your configuration.
Why it was introduced: This provides more power and flexibility in setting up your Eleventy project, such as fetching global data or dynamic configurations before the build starts.
How it works:
// .eleventy.js
export default async function(eleventyConfig) {
// Simulate fetching data asynchronously
const myGlobalData = await new Promise(resolve => {
setTimeout(() => {
resolve({ siteName: "My Async Site" });
}, 100);
});
eleventyConfig.addGlobalData("global", myGlobalData);
return {
dir: {
input: "src",
output: "public"
}
};
};
3.3.3 Performance Improvements (Memoization) (v3.0.0)
What it is: Built-in filters like slugify and inputPathToUrl now benefit from memoization, which caches their results for repeated inputs.
Why it was introduced: To significantly speed up build times when these commonly used filters are called multiple times with the same arguments, especially on larger sites.
How it works: This is an internal optimization; you don’t need to change your code. Just ensure you’re using slugify and inputPathToUrl where appropriate.
Example:
<a href="{{ '/posts/' + title | slugify + '/' }}">{{ title }}</a>
The slugify filter will now perform faster if the same title is slugified multiple times.
3.3.4 Virtual Templates (v3.0.0)
What it is: A new configuration API (eleventyConfig.addTemplate()) that allows you to add content programmatically as if it were a file, without actually creating a physical file.
Why it was introduced: This is extremely powerful for plugins or complex custom setups where you need to generate pages (like RSS feeds, sitemaps, or robots.txt) from data or logic, rather than from static Markdown/template files.
How it works:
// .eleventy.js
export default function(eleventyConfig) {
eleventyConfig.addTemplate("robots.njk", "User-agent: *\nAllow: /", {
permalink: "/robots.txt",
eleventyExcludeFromCollections: true // Exclude from content collections
});
// Example for a dynamic page
eleventyConfig.addTemplate("dynamic-page.liquid", "<h2>Generated at {{ page.date | date: '%Y-%m-%d' }}</h2>", {
permalink: "/generated-page/index.html",
date: new Date() // Set a dynamic date
});
return {
dir: {
input: "src",
output: "public"
}
};
};
Explanation: The first addTemplate creates a robots.txt file directly. The second creates an HTML page with a dynamic date, demonstrating how you can pass data to virtual templates.
3.3.5 IdAttribute Plugin (v3.0.0)
What it is: A new official plugin that automatically adds id attributes to headings (H1-H6) in your rendered HTML, making it easy to create on-page anchor links for table of contents or direct linking.
Why it was introduced: To simplify the process of creating accessible and navigable content with internal links, a common requirement for documentation or long-form articles.
How it works: Install the plugin and add it to your .eleventy.js configuration.
Installation:
npm install @11ty/eleventy-plugin-id-attribute
.eleventy.js:
const pluginIdAttribute = require("@11ty/eleventy-plugin-id-attribute");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(pluginIdAttribute);
// ... rest of your config
};
Now, Markdown like ## My Section will automatically render as <h2 id="my-section">My Section</h2>.
3.3.6 Plain-text Bundler (v3.0.0)
What it is: A new built-in bundler that allows you to create per-page CSS or JavaScript bundles using paired shortcodes.
Why it was introduced: Provides a native way to manage and optimize CSS/JS assets within Eleventy, offering more control over which styles/scripts load on which pages without relying on complex external bundlers for simple cases.
How it works: Register the bundler in your config, then use the css or js paired shortcode in your templates.
.eleventy.js:
export default function(eleventyConfig) {
eleventyConfig.addBundle("css"); // Adds {% css %} paired shortcode
eleventyConfig.addBundle("js"); // Adds {% js %} paired shortcode
// ... rest of your config
};
Template (.njk or .liquid):
{% css %}
/* Inline CSS for this page */
.page-specific-style {
color: blue;
}
{% endcss %}
{% js %}
// Inline JS for this page
console.log("Hello from Eleventy bundle!");
{% endjs %}
This content will be extracted and bundled into a separate CSS/JS file, linked in your HTML.
3.3.7 InputPathToUrl Plugin (v3.0.0)
What it is: An official plugin and universal filter that simplifies linking directly to an input file path, and Eleventy will correctly resolve it to the output URL.
Why it was introduced: To make it easier and more robust to create internal links within your project, abstracting away the specifics of how Eleventy maps input paths to output URLs.
How it works: Install the plugin and add it to your .eleventy.js.
Installation:
npm install @11ty/eleventy-plugin-input-path-to-url
.eleventy.js:
const pluginInputPathToUrl = require("@11ty/eleventy-plugin-input-path-to-url");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(pluginInputPathToUrl);
// ... rest of your config
};
Template:
<a href="{{ './src/pages/about.md' | inputPathToUrl }}">About Us</a>
This will render to <a href="/about/">About Us</a> (assuming a standard output structure).
3.3.8 JavaScript Front Matter (v3.0.0)
What it is: Allows you to use arbitrary JavaScript code directly within your content’s front matter block (---js ... ---), which will then be evaluated and its results included in the page’s data cascade.
Why it was introduced: Provides extreme flexibility for dynamic front matter values, enabling calculations or data fetching specific to a content file.
How it works: Add js after the opening --- in your front matter.
Example:
---js
const greeting = "Hello";
const subject = "Eleventy";
const fullMessage = `${greeting}, ${subject}!`;
---
{{ fullMessage }}
This will render “Hello, Eleventy!”.
3.3.9 page.rawInput (v3.0.0)
What it is: A new data variable (page.rawInput) that provides access to the raw, unprocessed content of a template file before any template engine rendering.
Why it was introduced: Useful for scenarios where you need to work with the original source content (e.g., extracting snippets, analyzing markdown structure) without interference from templating logic.
How it works: Access page.rawInput in your templates or computed data.
Example:
The raw content of this page is:
<pre><code>{{ page.rawInput | safe }}</code></pre>
3.3.10 addPreprocessor API (v3.0.0)
What it is: A configuration API (eleventyConfig.addPreprocessor()) to modify raw content before it’s passed to any template engine for rendering.
Why it was introduced: Provides a powerful hook for custom transformations on content, such as cleaning up Markdown, injecting data, or handling special syntax before Eleventy’s core processing.
How it works:
// .eleventy.js
module.exports = function(eleventyConfig) {
eleventyConfig.addPreprocessor("html", function(rawContent, inputPath) {
if (inputPath.endsWith(".html")) {
// Example: Inject a comment into HTML files
return `<!-- Processed by custom preprocessor -->\n${rawContent}`;
}
return rawContent;
});
// ...
};
3.3.11 addDateParsing API (v3.0.0)
What it is: A configuration API (eleventyConfig.addDateParsing()) to add your own custom date parsing logic, overriding Eleventy’s default date parsing.
Why it was introduced: Offers greater control over how dates are interpreted, particularly useful when migrating content with inconsistent date formats or when needing to handle specific timezones.
How it works:
// .eleventy.js
module.exports = function(eleventyConfig) {
eleventyConfig.addDateParsing(input => {
// Custom logic to parse date strings, e.g., from an unusual format
// This example uses Luxon for robust date parsing
const { DateTime } = require('luxon');
return DateTime.fromFormat(input, 'MM-dd-yyyy').toJSDate();
});
// ...
};
3.3.12 eleventyDataSchema (v3.0.0)
What it is: A data option that allows you to define a JSON Schema for validating data cascade values.
Why it was introduced: To ensure data consistency and prevent errors by validating the structure and types of your data at build time, improving the reliability of your project.
How it works: Define an eleventyDataSchema.js file and point to it in your configuration.
Example (.eleventy.js):
// .eleventy.js
module.exports = function(eleventyConfig) {
eleventyConfig.addGlobalData("eleventyDataSchema", require("./.eleventy-data-schema.js"));
// ...
};
Example (.eleventy-data-schema.js):
module.exports = {
type: "object",
properties: {
title: { type: "string", minLength: 5 },
author: { type: "string" },
tags: { type: "array", items: { type: "string" } }
},
required: ["title", "author"]
};
If a page’s front matter or data doesn’t conform to this schema, Eleventy will throw an error.
3.3.13 Asynchronous Plugins (v3.0.0)
What it is: Eleventy now fully supports asynchronous plugins and an async-friendly addPlugin configuration API.
Why it was introduced: Enables plugins to perform asynchronous operations during their initialization, such as fetching data from an API or performing complex setup tasks.
How it works: If a plugin’s main function returns a Promise, Eleventy will await its resolution.
// .eleventy.js
const myAsyncPlugin = require("./my-async-plugin.js");
module.exports = async function(eleventyConfig) { // config can be async
await eleventyConfig.addPlugin(myAsyncPlugin);
// ...
};
Example (my-async-plugin.js):
module.exports = async function(eleventyConfig, pluginOptions = {}) {
// Simulate an async operation during plugin setup
const dynamicSetting = await new Promise(resolve => {
setTimeout(() => {
resolve("some-dynamic-value");
}, 50);
});
eleventyConfig.addGlobalData("dynamicSetting", dynamicSetting);
};
3.3.14 renderTransforms Universal Filter (v3.0.0)
What it is: A new universal filter that allows you to manually run your project’s defined transforms (post-processing functions) on an arbitrary block of content.
Why it was introduced: Useful for scenarios where you generate content programmatically (e.g., for RSS feeds or JSON APIs) and want to apply the same transformations (like minification, HTML cleanup) that apply to your main HTML output.
How it works:
<content>
{{ post.data.content | renderTransforms | safe }}
</content>
This would apply any transforms defined in your .eleventy.js (e.g., HTML minification) to the post.data.content.
3.3.15 Incremental Builds (--incremental) (v3.0.0)
What it is: The --incremental command-line flag enables partial incremental builds, significantly speeding up rebuilds during development by processing only changed files.
Why it was introduced: To provide even faster feedback loops for developers, particularly on larger projects where full rebuilds can be slow.
How it works:
npx @11ty/eleventy --serve --incremental
You can also specify specific files to rebuild incrementally:
npx @11ty/eleventy --incremental=posts/my-new-post.md
3.3.16 renderContent Universal Filter (v3.0.0, with Render Plugin)
What it is: Included with the official plugin-render, this filter allows you to take a string and render it as if it were an Eleventy template, respecting all Eleventy’s features (shortcodes, filters, data).
Why it was introduced: Provides powerful capabilities for dynamic content generation, such as rendering user-submitted Markdown or small template snippets that are not stored as physical files.
How it works:
Installation:
npm install @11ty/eleventy-plugin-render
.eleventy.js:
const pluginRender = require("@11ty/eleventy-plugin-render");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(pluginRender);
// ...
};
Template:
{% set dynamicMarkdown = "This is **dynamic** Markdown with a {% yearsSince '2020-01-01' %} shortcode." %}
<div class="dynamic-content">
{{ dynamicMarkdown | renderContent | safe }}
</div>
3.3.17 Dev Server Updates (onRequest API) (v3.0.0)
What it is: The Eleventy Dev Server (used with --serve) now has an onRequest API, allowing you to handle requests dynamically during development. This is used by plugins like the Image plugin for on-request image transformations.
Why it was introduced: Provides a powerful hook for extending the development server’s capabilities, enabling features like on-the-fly image optimization or serverless-like functions during local development.
How it works: This is primarily for plugin developers but can be accessed for custom server-side logic in development.
3.4 Advanced Eleventy Techniques
3.4.1 Generating Content-Driven CSS
What it is: A technique to dynamically generate CSS classes or variables based on your content’s front matter or data, allowing for highly flexible and customizable styling without manual CSS updates.
Why it’s useful: Instead of hardcoding styles or relying solely on utility classes, you can define styles that are directly linked to your content’s properties (e.g., a “theme” variable in front matter dictating colors). This promotes consistency and reduces CSS bloat by only generating what’s needed.
How it works: Use a JavaScript template file (.11ty.js) to iterate over your collections, extract relevant data, and output CSS.
Example (Complex): Dynamic Themes based on Front Matter
Suppose you want to apply a unique color theme to different project pages, defined in their front matter:
src/projects/project-1.md
---
layout: "project.njk"
title: "My Blue Project"
theme: ["blue", "light"] # [color-name, text-color-name]
---
Content for blue project.
src/projects/project-2.md
---
layout: "project.njk"
title: "My Green Project"
theme: ["green", "dark"]
---
Content for green project.
eleventy.config.js (or .eleventy.js):
module.exports = function(eleventyConfig) {
// Add a custom collection to easily access all posts with themes
eleventyConfig.addCollection("themedPosts", function(collectionApi) {
return collectionApi.getAll().filter(item => item.data.theme);
});
return {
dir: {
input: "src",
output: "public"
}
};
};
src/assets/css/themes.css.11ty.js:
module.exports = class {
data() {
return {
permalink: "/assets/css/themes.css", // Output path
eleventyExcludeFromCollections: true, // Don't create a content page for this CSS file
};
}
render({ collections }) {
const uniqueThemes = new Map(); // Use a Map to store unique themes
for (const post of collections.themedPosts) { // Use our custom collection
const theme = post.data.theme;
if (Array.isArray(theme) && theme.length === 2) {
const themeKey = theme.join("-");
if (!uniqueThemes.has(themeKey)) {
uniqueThemes.set(themeKey, theme);
}
}
}
const cssRules = [];
for (const theme of uniqueThemes.values()) {
const [baseColor, textColor] = theme;
cssRules.push(`
.theme-${baseColor} {
background-color: var(--color-${baseColor});
color: var(--color-${textColor});
}
.theme-${baseColor}:hover {
border-color: var(--color-${textColor});
box-shadow: 0.75rem -0.75rem 0 0 var(--color-dark), -0.75rem 0.75rem 0 0 var(--color-${baseColor});
}
`);
}
// Define your base CSS variables for colors (e.g., in a separate main.css or here)
const baseColors = `
:root {
--color-dark: #0c0c0c;
--color-light: #F3F3F3;
--color-red: #e5633e;
--color-turquoise: #1ed397;
--color-green: #59D376;
--color-yellow: #fdb320;
--color-tangerine: #f18b2f;
--color-orange: #f79f28;
--color-blue: #1E29F9;
--color-purple: #4f0eb8;
}
`;
return `${baseColors}\n${cssRules.join("\n\n")}`.trim();
}
};
src/_includes/project.njk (or your main layout):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link rel="stylesheet" href="/assets/css/themes.css"> {# Include the generated CSS #}
<style>
/* Base styles for the theme */
.project-card {
padding: 1.5em;
margin: 1em 0;
border: 1px solid currentColor;
}
</style>
</head>
<body>
<main>
<article class="project-card {% if theme %}{{ 'theme-' + theme[0] }}{% endif %}">
<h1>{{ title }}</h1>
{{ content | safe }}
</article>
</main>
</body>
</html>
Explanation:
- Directory Data Files: The
themearray is set in the front matter of each project Markdown file. themes.css.11ty.js: This JavaScript template file is treated as a programmatic way to generatethemes.css. It iterates through all pages in thethemedPostscollection, extracts uniquethemevalues, and then generates corresponding CSS classes (e.g.,.theme-blue) using CSS variables.- Layout Usage: The
project.njklayout includes thethemes.cssand applies the dynamically generated class to thearticleelement based on its front matter.
This approach ensures that only the CSS necessary for the themes present in your content is generated, keeping your CSS bundle small and relevant.
3.4.2 Building Search Indexes
What it is: Creating a static JSON file that contains all the searchable content of your website, allowing for client-side search functionality without a backend database.
Why it’s useful: Provides a fast, privacy-friendly search experience directly in the user’s browser, eliminating the need for complex server-side search solutions.
How it works: Use a Nunjucks (or other template language) file to generate a JSON output by looping through your content collections and extracting relevant data. Custom filters can clean and format the text for the index.
Example (Complex):
1. Define search collection and permalink for the JSON output:
src/search.njk
---
permalink: search.json
eleventyExcludeFromCollections: true # Prevents it from being treated as a regular page
---
{{ collections.search | searchIndex | dump | safe }}
2. Configure .eleventy.js:
Add the searchIndex filter and potentially add pages to the search collection.
const pluginRss = require("@11ty/eleventy-plugin-rss"); // Example if you also use RSS
const { DateTime } = require('luxon'); // Example for date formatting
module.exports = function (eleventyConfig) {
eleventyConfig.addPassthroughCopy("./src/styles");
eleventyConfig.addWatchTarget("./src/styles/");
// Add the RSS plugin (optional)
eleventyConfig.addPlugin(pluginRss);
// Define a custom collection for searchable content
eleventyConfig.addCollection("search", function(collection) {
// Filter pages you want to include in search (e.g., exclude drafts, specific types)
return collection.getAll().filter(item => {
// Example: Only include posts and pages that are not explicitly excluded
return (item.data.tags && (item.data.tags.includes("posts") || item.data.tags.includes("page"))) &&
!item.data.eleventyExcludeFromSearch; // Add this custom frontmatter to exclude
});
});
// Register the custom searchIndex filter
eleventyConfig.addFilter("searchIndex", require("./src/_filters/searchIndex.js"));
// Optional: Add a readableDate filter for blog posts (from common Eleventy setups)
eleventyConfig.addFilter('readableDate', (dateObj) => {
return DateTime.fromJSDate(dateObj, { zone: 'utc' }).toFormat('yyyy-LL-dd');
});
return {
dir: {
input: "src",
output: "public",
},
};
};
3. Create src/_filters/searchIndex.js:
/*!
* Creates an index of searchable keywords and an excerpt.
*/
// Function to clean text for indexing
function indexer(text) {
if (!text) return "";
text = text.toLowerCase();
// Remove HTML elements and other unnecessary characters
const plain = text.replace(/<.*?>/gis, " ")
.replace(/[,\?\n\|\\\*]/g, " ")
.replace(/\b(\,|\"|#|\'|;|\:|\"|\"|\'|’|“|”)\b/gi, " ")
.replace(/[ ]{2,}/g, " ")
.trim();
return plain;
}
// Function to create an excerpt
function excerpt(text) {
if (!text) return "";
const plain = text.replace(/\\ (.*)\<\/h1\>/, "").replace(/<.*?>/gis, " ");
return plain
.replace(/["'#]|\n/g, " ")
.replace(/&(\S*)/g, "") // remove HTML entities
.replace(/[ ]{2,}/g, " ")
.replace(/[\\|]/g, "")
.substring(0, 140) // Limit excerpt length
.trim();
}
module.exports = function searchIndex(collection) {
const search = collection.map(({ templateContent, url, data }) => {
const { description = "", title = "" } = data;
// Combine content and description for indexing and excerpt
const textToIndex = `${title} ${description} ${templateContent}`;
return {
url,
title: title || "Untitled", // Fallback title
text: excerpt(textToIndex), // Use excerpt for display
keywords: indexer(textToIndex), // Full cleaned text for search
};
});
return { search }; // Wrap in an object named "search"
};
4. Example Content (src/posts/my-post.md):
---
title: "My Amazing Blog Post"
description: "This post covers interesting topics."
tags: ["posts", "search"]
---
This is the **main content** of my blog post. It has some important keywords and information.
5. Client-side JavaScript to fetch and use the index:
let searchIndex = null;
async function fetchSearchData() {
if (searchIndex === null) {
try {
const response = await fetch("/search.json?v=" + Date.now()); // Cache bust
const data = await response.json();
searchIndex = data.search;
console.log("Search index loaded:", searchIndex);
// Now you can use searchIndex for your client-side search UI
} catch (error) {
console.error("Error loading search index:", error);
}
}
}
// Example usage: call fetchSearchData when search UI is activated
// e.g., on Ctrl+K press
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
fetchSearchData();
// Show search overlay/input
}
});
// Example: rudimentary search function
function performSearch(query) {
if (!searchIndex) {
console.warn("Search index not loaded yet.");
return [];
}
const lowerQuery = query.toLowerCase();
return searchIndex.filter(item =>
item.title.toLowerCase().includes(lowerQuery) ||
item.text.toLowerCase().includes(lowerQuery) ||
item.keywords.toLowerCase().includes(lowerQuery)
);
}
// Example: call performSearch when user types in search input
// let searchInput = document.getElementById('search-input');
// searchInput.addEventListener('input', (e) => {
// const results = performSearch(e.target.value);
// console.log("Search results:", results);
// // Update your UI with results
// });
Explanation:
- A
search.njkfile with apermalinkofsearch.jsontells Eleventy to output a JSON file. - The
searchIndexfilter (defined in_filters/searchIndex.js) processes each page. It usesindexerto create a cleaned string for keywords andexcerptfor display. - The
eleventyExcludeFromCollections: truefront matter prevents thesearch.njkitself from appearing in other collections. - On the client-side, JavaScript fetches this
search.jsonfile once and then performs searches locally.
Tip: For complex content, consider using libraries like lunr.js on the client-side for more advanced search capabilities once you’ve generated your static search index.
3.4.3 Calculating yearsSince with Nunjucks Filters
What it is: A custom Nunjucks filter (or shortcode) that dynamically calculates the number of years passed since a given date. This is useful for “X years ago” displays that automatically update without manual intervention.
Why it’s useful: Avoids hardcoding time-sensitive information, ensuring accuracy and reducing maintenance overhead for frequently updated values (e.g., “Company founded X years ago”).
How it works: Define a JavaScript function that calculates the years, then register it as a Nunjucks filter in your .eleventy.js.
Example:
1. Create src/_filters/yearsSince.js:
module.exports = function yearsSince(dateString) {
// Validate input format (YYYY-MM-DD)
if (typeof dateString !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
throw new Error('The `yearsSince` function requires a valid date string in the format "YYYY-MM-DD".');
}
const startDate = new Date(dateString);
if (isNaN(startDate.getTime())) {
throw new Error(`Invalid date string passed to "yearsSince": ${dateString}`);
}
const startYear = startDate.getFullYear();
const now = new Date();
const currentYear = now.getFullYear();
let years = currentYear - startYear;
// Adjust if the anniversary hasn't occurred yet this year
const hasCompletedThisYear =
now.getMonth() > startDate.getMonth() ||
(now.getMonth() === startDate.getMonth() && now.getDate() >= startDate.getDate());
return hasCompletedThisYear ? years : years - 1;
};
2. Register the filter in .eleventy.js:
// .eleventy.js
const yearsSinceFilter = require("./src/_filters/yearsSince.js");
module.exports = function(eleventyConfig) {
eleventyConfig.addNunjucksFilter("yearsSince", yearsSinceFilter);
// ...
};
3. Use the filter in your Nunjucks templates:
<p>I have been making things for the web for more than {{ "2012-01-01" | yearsSince }} years.</p>
When Eleventy builds your site, it will execute this filter, and the number will update automatically based on the build date.
3.4.4 Migrating from Nunjucks to Vento
What it is: Vento is a new, faster templating language for Eleventy. Migrating involves updating template syntax from Nunjucks to Vento.
Why it’s useful: Vento can offer performance benefits, and some developers might prefer its syntax or features. It’s a sign of Eleventy’s continued evolution and flexibility in templating.
How to migrate (general steps):
- Install
eleventy-plugin-vento:npm install eleventy-plugin-vento - Add to
.eleventy.js:const pluginVento = require("eleventy-plugin-vento"); module.exports = function(eleventyConfig) { eleventyConfig.addPlugin(pluginVento); // You might need to adjust templateLanguages config // eleventyConfig.setTemplateFormats(["md", "njk", "vto"]); // eleventyConfig.addTemplateFormats("vto"); // ... }; - Rename files: Change
.njkfiles to.vto. - Syntax Replacement (key changes):
- Tags: Nunjucks
{% ... %}becomes Vento{{ ... }}. - Comments: Nunjucks
{# ... #}becomes Vento{{# ... #}}. endtags: Nunjucks{% endif %}becomes Vento{{ /if }}. Similarly forfor,set, etc.- Operators:
and->&&,or->||,not->!,in->of(for iteration). - Filters: Nunjucks
| filterNameis generally similar, but some built-in filters might have different names or behavior. - Template inheritance (
extends,block): Vento has similar concepts but might use slightly different syntax.
- Tags: Nunjucks
Example (Partial):
Nunjucks:
{# This is a comment #}
{% set myVar = "Hello" %}
{% if someCondition and anotherCondition %}
<p>{{ myVar }} World!</p>
{% endif %}
{% for item in items %}
<li>{{ item.name }}</li>
{% endfor %}
Vento equivalent:
{{# This is a comment #}}
{{ let myVar = "Hello" }}
{{ if someCondition && anotherCondition }}
<p>{{ myVar }} World!</p>
{{ /if }}
{{ for item of items }}
<li>{{ item.name }}</li>
{{ /for }}
Tip: Migrate templates incrementally (file by file) and use your editor’s find/replace with regular expressions carefully. Test thoroughly after each batch of changes. Consult Vento’s official documentation for a complete syntax reference.
3.4.5 Handling Last Modified Dates
What it is: Automatically capturing and displaying the last modification date of a content file, beyond just its creation date from front matter.
Why it’s useful: Provides dynamic “last updated” information for users and search engines, indicating content freshness.
How it works: Use Eleventy’s Computed Data feature in a JavaScript data file to read the file system’s modification time (mtime) and expose it to your templates.
Example:
1. Create a directory data file for your posts (e.g., src/posts/posts.11tydata.js):
const fs = require('fs');
const path = require('path');
module.exports = {
eleventyComputed: {
lastmod: (data) => {
try {
// data.page.inputPath gives the full path to the source Markdown file
const stats = fs.statSync(data.page.inputPath);
return stats.mtime; // Returns a Date object for the last modified time
} catch (err) {
console.error(`Error checking mtime for ${data.page.inputPath}:`, err);
return null;
}
},
// Optional: Ensure the 'date' property is a Date object if derived from filename
// and you want to compare it with mtime reliably.
// Eleventy's default date parsing is usually good, but this shows the pattern.
// date: (data) => {
// if (typeof data.date === 'string' && data.date.match(/^\d{4}-\d{2}-\d{2}$/)) {
// return new Date(data.date);
// }
// return data.date; // return existing date if not string or not in expected format
// }
}
};
Explanation:
eleventyComputed: This is where you define computed data properties.lastmod: This new data property will be available for each page processed by this data file.data.page.inputPath: Provides the full file path of the current content item.fs.statSync(data.page.inputPath).mtime: Node.jsfsmodule is used to get file statistics, andmtimegives the last modification timestamp.
2. Use lastmod in your templates (e.g., src/_includes/layouts/post.njk):
---
layout: "base.njk"
---
<article>
<h1>{{ title }}</h1>
<p>Published: {{ page.date | readableDate }}</p>
{% if lastmod %}
<p>Last modified: {{ lastmod | readableDate }}</p>
{% endif %}
<div>
{{ content | safe }}
</div>
</article>
Tip: For accurate time zone handling, consider using a library like Luxon or Moment.js in your lastmod filter/computation to format the mtime Date object consistently. Ensure your build environment (CI/CD) preserves file modification times if you rely on mtime for production builds.
Chapter 4: Real-World Projects
These projects are designed to demonstrate the practical application of Hugo and Eleventy, integrating the features discussed in the previous chapters.
4.1 Project 1: A Minimal Blog with Hugo
This project will guide you through setting up a basic, fast blog using Hugo, including posts, an archive page, and basic styling.
Goal: A lightweight, performant blog with a clean interface.
Key Concepts Applied:
- Hugo project structure
- Front Matter
- Go Templates (
single.html,list.html,baseof.html) - Partials
- Taxonomies (tags)
Step-by-Step Guide:
1. Project Setup:
hugo new site my-hugo-blog
cd my-hugo-blog
mkdir layouts/partials # For reusable template components
2. Configure config.toml:
baseURL = "http://example.org/"
languageCode = "en-us"
title = "My Minimal Hugo Blog"
theme = "my-custom-theme" # We'll create this "theme" within layouts/
[taxonomies]
tag = "tags"
category = "categories"
[params]
description = "A fast and simple blog built with Hugo."
3. Create layouts/index.html (Homepage):
This will display a list of your most recent blog posts.
{{ define "main" }}
<main>
<h1>{{ .Site.Title }}</h1>
<p>{{ .Site.Params.description }}</p>
<h2>Recent Posts</h2>
<ul class="post-list">
{{ range first 5 .Pages.ByPublishDate.Reverse }}
<li>
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
</li>
{{ end }}
</ul>
<p><a href="/posts/">View All Posts</a></p>
</main>
{{ end }}
4. Create layouts/_default/list.html (Post List Page - for /posts/ and tags/categories):
This template will handle the display of lists of content (e.g., all posts, posts by tag/category).
{{ define "main" }}
<main>
<h1>{{ .Title }}</h1>
<ul class="post-list">
{{ range .Pages.ByPublishDate.Reverse }}
{{ if (and (ne .Kind "term") (ne .Kind "taxonomy") (ne .Kind "section")) }}
<li>
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
</li>
{{ end }}
{{ end }}
</ul>
</main>
{{ end }}
5. Create layouts/_default/single.html (Single Post Page):
This template will render individual blog posts.
{{ define "main" }}
<main>
<article>
<h1>{{ .Title }}</h1>
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
{{ .Content }}
{{ with .Params.tags }}
<div class="tags">
Tags:
{{ range . }}
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
{{ end }}
</div>
{{ end }}
</article>
<p><a href="/posts/">Back to Posts</a></p>
</main>
{{ end }}
6. Create layouts/_default/baseof.html (Base Layout):
This is the fundamental HTML structure that all other templates will extend.
<!DOCTYPE html>
<html lang="{{ site.LanguageCode }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/posts/">Blog</a>
<a href="/tags/">Tags</a>
</nav>
</header>
{{ block "main" . }}{{ end }}
<footer>
<p>© {{ now.Year }} {{ .Site.Title }}.</p>
</footer>
</body>
</html>
7. Add Basic CSS (static/css/style.css):
body {
font-family: sans-serif;
line-height: 1.6;
margin: 0 auto;
max-width: 800px;
padding: 20px;
background-color: #f8f8f8;
color: #333;
}
header {
background-color: #333;
color: #fff;
padding: 1em 0;
text-align: center;
}
header nav a {
color: #fff;
margin: 0 15px;
text-decoration: none;
}
h1, h2 {
color: #222;
}
.post-list {
list-style: none;
padding: 0;
}
.post-list li {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px dashed #ccc;
}
.post-list li:last-child {
border-bottom: none;
}
.post-list a {
text-decoration: none;
color: #007bff;
font-weight: bold;
}
.post-list time {
font-size: 0.9em;
color: #666;
display: block;
margin-top: 5px;
}
article {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.tags a {
background-color: #e2e6ea;
color: #495057;
padding: 5px 10px;
border-radius: 3px;
text-decoration: none;
margin-right: 5px;
font-size: 0.85em;
}
footer {
text-align: center;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
color: #777;
font-size: 0.9em;
}
8. Create Content:
content/posts/first-post.md
---
title: "My First Blog Post"
date: 2025-08-01T10:00:00+05:30
tags: ["introduction", "hugo"]
---
This is the content of my very first Hugo blog post. It's great to be here!
This post will talk about:
* Static Site Generators
* Performance
* Simplicity
content/posts/second-post.md
---
title: "Getting Started with Hugo"
date: 2025-08-05T14:30:00+05:30
tags: ["tutorial", "hugo", "getting-started"]
draft: true # Mark as draft for development
---
Here's a step-by-step guide to setting up your first Hugo site.
9. Run and Test:
hugo server --buildDrafts --watch
Open your browser to http://localhost:1313. You should see the homepage with your first post. Navigate to /posts/ to see the list. The second post won’t appear unless --buildDrafts is used.
10. Build for Production:
hugo
The public/ directory will contain your complete static site.
4.2 Project 2: A Documentation Site with Eleventy and Search
This project will guide you through building a simple documentation site with Eleventy, incorporating its collection and data cascade features, and adding a client-side search index.
Goal: An organized documentation site with efficient client-side search.
Key Concepts Applied:
- Eleventy project structure
- Collections (custom,
getAll) - Data Cascade (directory data files, front matter)
- Nunjucks Templating
- Custom Filters (for search index)
- Client-side JavaScript (fetching and searching the index)
Step-by-Step Guide:
1. Project Setup:
mkdir my-eleventy-docs
cd my-eleventy-docs
npm init -y
npm install @11ty/eleventy @11ty/eleventy-plugin-rss luxon --save-dev # RSS and Luxon for general utility
mkdir src src/_includes src/_filters src/docs src/css src/js
2. Configure .eleventy.js:
// .eleventy.js
const { DateTime } = require('luxon');
module.exports = function (eleventyConfig) {
// Passthrough copy for static assets
eleventyConfig.addPassthroughCopy("src/css");
eleventyConfig.addPassthroughCopy("src/js");
// Custom collection for documentation pages (for search and navigation)
eleventyConfig.addCollection("docs", function(collectionApi) {
return collectionApi.getFilteredByGlob("src/docs/**/*.md");
});
// Custom collection for search index (includes all docs and any other search-enabled content)
eleventyConfig.addCollection("searchIndex", function(collectionApi) {
return collectionApi.getAll().filter(item => {
// Exclude layouts, partials, and content explicitly marked for exclusion
return !item.inputPath.startsWith("./src/_") && !item.data.eleventyExcludeFromSearch;
});
});
// Filters
eleventyConfig.addFilter("readableDate", (dateObj) => {
return DateTime.fromJSDate(dateObj, { zone: 'utc' }).toFormat("yyyy-LL-dd");
});
// Custom search index filter (from 3.4.2)
eleventyConfig.addFilter("searchIndexFilter", require("./src/_filters/searchIndex.js"));
// Input and output directories
return {
dir: {
input: "src",
output: "public",
includes: "_includes", // This is the default, but good to be explicit
data: "_data"
}
};
};
3. Create src/_filters/searchIndex.js (from 3.4.2):
(Paste the indexer and excerpt functions, and the module.exports section from the “Building Search Indexes” example in Section 3.4.2 into this file)
// src/_filters/searchIndex.js
// Paste the full code from Section 3.4.2's "Building Search Indexes" example here.
// Ensure it matches the example provided, including the 'indexer', 'excerpt', and 'module.exports' parts.
4. Create src/_includes/layouts/base.njk (Base Layout):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} | My Eleventy Docs</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/docs/">Documentation</a>
<button id="search-toggle">Search (Ctrl+K)</button>
</nav>
</header>
<div id="search-overlay" style="display: none;">
<div class="search-modal">
<input type="text" id="search-input" placeholder="Type to search..." autofocus>
<ul id="search-results"></ul>
<button id="close-search">X</button>
</div>
</div>
<main>
{{ content | safe }}
</main>
<footer>
<p>© 2025 My Eleventy Docs. All rights reserved.</p>
</footer>
<script src="/js/search.js"></script>
</body>
</html>
5. Create src/_includes/layouts/doc.njk (Documentation Page Layout):
This layout will be used by individual documentation pages.
---
layout: "base.njk"
eleventyExcludeFromSearch: false # Explicitly include in search index
---
<article class="doc-content">
<h1>{{ title }}</h1>
{{ content | safe }}
</article>
6. Create Documentation Content:
src/docs/_data/docs.json (Directory Data File for default layout for docs)
{
"layout": "layouts/doc.njk"
}
src/docs/index.md (Docs landing page)
---
title: "Documentation Home"
---
Welcome to the documentation site! Navigate using the sidebar.
## Available Sections
* [Getting Started](/docs/getting-started/)
* [Configuration](/docs/configuration/)
src/docs/getting-started.md
---
title: "Getting Started Guide"
description: "Learn how to quickly set up your Eleventy documentation project."
---
This guide will walk you through the initial steps to get your Eleventy documentation site up and running.
1. **Installation**: Install Node.js and Eleventy.
2. **Project Structure**: Understand the recommended file organization.
3. **Content Creation**: Write your first Markdown documentation page.
src/docs/configuration.md
---
title: "Advanced Configuration"
description: "Deep dive into Eleventy's configuration options and best practices."
---
Eleventy offers powerful configuration options via the `.eleventy.js` file.
* **Passthrough Copy**: For static assets.
* **Collections**: Dynamic grouping of content.
* **Filters and Shortcodes**: Extend Eleventy's capabilities.
7. Create src/search.njk (Search Index Generator - from 3.4.2):
---
permalink: search.json
eleventyExcludeFromCollections: true
---
{{ collections.searchIndex | searchIndexFilter | dump | safe }}
8. Add Basic CSS (src/css/style.css):
body {
font-family: Arial, sans-serif;
margin: 0;
line-height: 1.6;
display: flex;
flex-direction: column;
min-height: 100vh;
}
header {
background: #333;
color: #fff;
padding: 1em;
text-align: center;
}
header nav a, #search-toggle {
color: #fff;
margin: 0 15px;
text-decoration: none;
background: none;
border: none;
font-size: 1em;
cursor: pointer;
}
main {
flex: 1;
padding: 20px;
max-width: 960px;
margin: 0 auto;
}
.doc-content {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
footer {
text-align: center;
padding: 1em;
background: #eee;
color: #555;
}
#search-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: flex-start; /* Adjust alignment to be at the top */
padding-top: 50px; /* Space from top */
z-index: 1000;
}
.search-modal {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 600px;
position: relative;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
#search-input {
width: calc(100% - 40px);
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1.1em;
}
#search-results {
list-style: none;
padding: 0;
max-height: 400px;
overflow-y: auto;
border-top: 1px solid #eee;
margin-top: 15px;
}
#search-results li {
padding: 10px;
border-bottom: 1px dashed #eee;
}
#search-results li:last-child {
border-bottom: none;
}
#search-results li a {
text-decoration: none;
color: #007bff;
font-weight: bold;
display: block;
}
#search-results li p {
font-size: 0.9em;
color: #555;
margin: 5px 0 0;
}
#close-search {
position: absolute;
top: 10px;
right: 10px;
background: #eee;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
font-weight: bold;
}
9. Add Client-side JavaScript (src/js/search.js):
// src/js/search.js
let searchIndex = null;
const searchOverlay = document.getElementById('search-overlay');
const searchInput = document.getElementById('search-input');
const searchResultsList = document.getElementById('search-results');
const searchToggleBtn = document.getElementById('search-toggle');
const closeSearchBtn = document.getElementById('close-search');
async function fetchSearchData() {
if (searchIndex === null) {
try {
const response = await fetch("/search.json?v=" + Date.now()); // Cache bust
const data = await response.json();
searchIndex = data.search;
console.log("Search index loaded:", searchIndex);
} catch (error) {
console.error("Error loading search index:", error);
}
}
}
function showSearchOverlay() {
searchOverlay.style.display = 'flex';
searchInput.focus();
// Fetch data only when search is opened
fetchSearchData();
}
function hideSearchOverlay() {
searchOverlay.style.display = 'none';
searchInput.value = '';
searchResultsList.innerHTML = ''; // Clear results
}
function performSearch() {
const query = searchInput.value.toLowerCase();
searchResultsList.innerHTML = ''; // Clear previous results
if (!query.trim() || !searchIndex) {
return;
}
const results = searchIndex.filter(item =>
item.title.toLowerCase().includes(query) ||
item.text.toLowerCase().includes(query) ||
item.keywords.toLowerCase().includes(query)
);
if (results.length === 0) {
searchResultsList.innerHTML = '<li>No results found.</li>';
return;
}
results.forEach(item => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = item.url;
a.textContent = item.title;
const p = document.createElement('p');
p.textContent = item.text || item.description || 'No excerpt available.'; // Display excerpt or description
li.appendChild(a);
li.appendChild(p);
searchResultsList.appendChild(li);
});
}
// Event Listeners
searchToggleBtn.addEventListener('click', showSearchOverlay);
closeSearchBtn.addEventListener('click', hideSearchOverlay);
searchInput.addEventListener('input', performSearch);
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
showSearchOverlay();
} else if (e.key === 'Escape') {
hideSearchOverlay();
}
});
10. Run and Test:
npx @11ty/eleventy --serve
Open http://localhost:8080. Click the “Search” button or press Ctrl+K. Type in the search box to see dynamic results.
11. Build for Production:
npx @11ty/eleventy
The public/ directory will contain your static documentation site and the search.json file.
4.3 Project 3: A Multi-language Portfolio with Hugo and Content Adapters
This project will demonstrate creating a multi-language portfolio site in Hugo, leveraging its built-in multilingual features and content adapters to display dynamic project data from a simulated external source.
Goal: A portfolio site available in multiple languages, with project data sourced externally.
Key Concepts Applied:
- Hugo multilingual mode
- Content Adapters (fetching external JSON for projects)
- Go Templates for dynamic content and language switching
- Module mounts (optional, for development speed on large projects)
Step-by-Step Guide:
1. Project Setup:
hugo new site my-multi-lang-portfolio
cd my-multi-lang-portfolio
mkdir layouts/partials
mkdir data # For a simulated external JSON data
2. Configure config.toml for Multilingual Support:
baseURL = "http://example.org/"
languageCode = "en-us"
title = "My Portfolio"
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true # /en/ and /fr/ URLs
[languages]
[languages.en]
title = "My English Portfolio"
weight = 10
languageName = "English"
[languages.fr]
title = "Mon Portfolio Français"
weight = 20
languageName = "Français"
[contentAdapters.projects]
# In a real scenario, this URL would point to an external API
# For this example, we'll use a local JSON file that mirrors an API response
url = "data/projects.json" # Relative path to our simulated JSON
type = "json"
path = "projects" # Generated pages will be content/projects/
[contentAdapters.projects.frontmatter]
title = "{{ .title }}"
description = "{{ .description }}"
image = "{{ .image }}"
url = "{{ .url }}"
# Permalinks for generated projects. The `url` field from JSON will be the final permalink.
_target = "/projects/{{ .slug }}/index.md"
3. Simulate External Project Data (data/projects.json):
[
{
"id": "project-website",
"slug": "web-portfolio",
"title": {
"en": "Modern Web Portfolio",
"fr": "Portfolio Web Moderne"
},
"description": {
"en": "A sleek and responsive web portfolio built with modern technologies.",
"fr": "Un portfolio web élégant et réactif construit avec des technologies modernes."
},
"image": "/img/portfolio-web.jpg",
"url": "https://example.com/web-portfolio"
},
{
"id": "project-mobile-app",
"slug": "mobile-app-design",
"title": {
"en": "Intuitive Mobile App Design",
"fr": "Conception d'application mobile intuitive"
},
"description": {
"en": "Designing user-friendly interfaces for mobile applications.",
"fr": "Conception d'interfaces conviviales pour les applications mobiles."
},
"image": "/img/mobile-app.jpg",
"url": "https://example.com/mobile-app"
}
]
Important: The title and description fields are objects, allowing for multilingual data directly in the JSON. Hugo’s content adapter mapping needs to handle this (see step 6).
4. Create Basic Layouts (layouts/):
layouts/_default/baseof.html (main structure)
<!DOCTYPE html>
<html lang="{{ .Lang }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<nav>
<a href="{{ "/" | relLangURL }}">Home</a>
<a href="{{ "/projects/" | relLangURL }}">Projects</a>
{{ range .Site.Home.AllTranslations }}
<a href="{{ .Permalink }}" lang="{{ .Lang }}">{{ .Language.LanguageName }}</a>
{{ end }}
</nav>
</header>
{{ block "main" . }}{{ end }}
<footer>
<p>© {{ now.Year }} {{ .Site.Title }}.</p>
</footer>
</body>
</html>
layouts/index.html (Homepage for each language)
{{ define "main" }}
<main>
<h1>{{ .Site.Title }}</h1>
<p>Welcome to my portfolio.</p>
<h2>My Projects</h2>
<div class="project-grid">
{{ range (where .Site.Pages "Type" "projects") }}
<div class="project-card">
<h3><a href="{{ .RelPermalink }}">{{ index .Params.title .Lang }}</a></h3>
{{ with .Params.image }}<img src="{{ . }}" alt="{{ index $.Params.title $.Lang }}">{{ end }}
<p>{{ index .Params.description .Lang }}</p>
{{ with .Params.url }}<a href="{{ . }}" target="_blank" rel="noopener">View Live</a>{{ end }}
</div>
{{ end }}
</div>
</main>
{{ end }}
layouts/projects/single.html (Individual Project Page)
{{ define "main" }}
<main>
<article class="project-detail">
<h1>{{ index .Params.title .Lang }}</h1>
{{ with .Params.image }}<img src="{{ . }}" alt="{{ index $.Params.title $.Lang }}">{{ end }}
<p>{{ index .Params.description .Lang }}</p>
{{ with .Params.url }}<p><a href="{{ . }}" target="_blank" rel="noopener">View Project Live</a></p>{{ end }}
<p><a href="{{ "/projects/" | relLangURL }}">Back to Projects</a></p>
</article>
</main>
{{ end }}
5. Add Static Assets (static/img/ and static/css/):
Create placeholder images portfolio-web.jpg and mobile-app.jpg in static/img/.
static/css/style.css (Basic styling)
body {
font-family: sans-serif;
line-height: 1.6;
margin: 0 auto;
max-width: 960px;
padding: 20px;
background-color: #f8f8f8;
color: #333;
}
header {
background-color: #333;
color: #fff;
padding: 1em 0;
text-align: center;
margin-bottom: 20px;
}
header nav a {
color: #fff;
margin: 0 15px;
text-decoration: none;
font-weight: bold;
}
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.project-card {
background-color: #fff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
text-align: center;
}
.project-card img {
max-width: 100%;
height: auto;
border-radius: 5px;
margin-bottom: 10px;
}
.project-card h3 a {
color: #007bff;
text-decoration: none;
}
.project-card p {
font-size: 0.9em;
color: #555;
}
.project-detail {
background-color: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
}
.project-detail img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin-bottom: 20px;
}
footer {
text-align: center;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
color: #777;
font-size: 0.9em;
}
6. Content Adaptations for Multilingual Data:
In layouts/index.html and layouts/projects/single.html, notice how {{ index .Params.title .Lang }} is used.
.Params.titlefor a content-adapter-generated page will be the object from yourdata/projects.json(e.g.,{ "en": "...", "fr": "..." })..Langis the current language code (e.g., “en”, “fr”).index .Params.title .Langallows you to access the language-specific title from the object. This is a common pattern for multilingual content.
7. Run and Test:
hugo server --watch
Open your browser to http://localhost:1313/en/ and http://localhost:1313/fr/. You should see the portfolio in English and French, with projects dynamically pulled from the JSON data.
8. Build for Production:
hugo
The public/ directory will contain the complete multilingual static site.
Chapter 5: Further Exploration & Resources
To continue your learning journey with Hugo and Eleventy, explore these valuable resources.
5.1 Blogs and Articles
- BryceWray.com: Excellent blog with in-depth articles, news, and insights on static site generators, often covering Hugo and Eleventy features and performance.
- Zach Leatherman’s Blog: The creator of Eleventy shares updates, tips, and philosophical discussions on web development and static sites.
- CSS-Tricks: While not exclusively SSG-focused, it has many articles on front-end techniques that pair well with static sites. Search for Hugo or Eleventy specific posts.
- Smashing Magazine: Publishes high-quality articles on web development, including practical guides and case studies related to SSGs.
- Kinsta Blog - Static Site Generators: Provides comparisons and overviews of various SSGs, including Hugo and Eleventy.
- Modern Web Blog: Often covers topics relevant to component-driven development and modern JavaScript that can be applied to Eleventy.
5.2 Video Tutorials and Courses
- Official Eleventy YouTube Channel: Search for “Eleventy” on YouTube; Zach Leatherman and the community often share talks and updates.
- Netlify YouTube Channel: Many tutorials on deploying static sites, including Hugo and Eleventy, and Jamstack best practices.
- FreeCodeCamp.org YouTube Channel: Offers comprehensive courses on web development, some of which may cover SSG basics or related technologies like Markdown, HTML, CSS, and JavaScript.
- “Learn Eleventy From Scratch” (Online Course, check latest versions): A popular comprehensive course for beginners to intermediate users.
- “Build with Hugo” (YouTube series/online courses): Search for recent tutorials, as many content creators demonstrate building sites with Hugo.
5.3 Official Documentation
- Hugo Documentation: https://gohugo.io/documentation/
- Highly comprehensive, often the first place to check for features and configurations.
- Eleventy Documentation: https://www.11ty.dev/docs/
- Well-structured with examples for multiple template languages. Includes information on breaking changes for newer versions.
5.4 Community Forums
- Hugo Discourse Forum: https://discourse.gohugo.io/
- Active community for asking questions, sharing tips, and getting support for Hugo-related issues.
- Eleventy Discord Server: Join through the official Eleventy website for real-time chat and community help.
- Stack Overflow: Search for
hugooreleventytags for solutions to common problems.
5.5 Project Ideas
- Personal Blog (Advanced): Implement a comment system using a static comments service (e.g., utterances, Disqus, or a custom one with serverless functions).
- Recipe Website: Use a JSON or CSV data file as the source for recipes, and generate individual recipe pages with categories and tags.
- Documentation Portal: Build a multi-version documentation site using Hugo’s modules or Eleventy’s data cascade to manage different versions of content.
- Portfolio with Filtering: Create a portfolio that pulls project data from an external API (simulated or real) and implements client-side filtering (e.g., by technology, client).
- Event Listing Site: Generate an events calendar from a spreadsheet or Google Sheet, displaying event details and allowing filtering by date or type.
- Product Catalog: Use content adapters to generate product pages from a product data feed (CSV or JSON), including images and detailed specifications.
- Simple E-commerce Frontend: Combine a static site with a headless e-commerce API (e.g., Shopify Buy Button, Snipcart) to create a fast, static storefront.
- Interactive Quiz/Survey: Create static quiz pages, and use client-side JavaScript to handle interactions and submit results to a serverless function.
- Online Resume/CV: A single-page or multi-page resume site, with data managed in a structured data file (YAML/JSON) for easy updates.
- Resource Library: Curate a list of resources (articles, videos, tools) with categories, tags, and search, updating from a shared spreadsheet.
5.6 Essential Third-Party Libraries and Tools
These tools commonly complement Hugo and Eleventy projects:
- Build Tools & CSS Frameworks:
- Tailwind CSS: Utility-first CSS framework (Hugo has v4 alpha support, easily integrated with Eleventy via PostCSS).
- PostCSS: Tool for transforming CSS with JavaScript plugins (for both SSGs).
- Dart Sass / Node-Sass: For compiling SASS/SCSS (Hugo has built-in support, Eleventy uses plugins).
- esbuild / Vite: Fast JavaScript bundlers often used with Eleventy for asset optimization.
- Markdown Processors:
- Goldmark: Hugo’s default Markdown processor (CommonMark compliant).
- markdown-it: Popular Markdown parser for Eleventy, highly extensible with plugins.
- Image Optimization:
- Eleventy Image: Official Eleventy plugin for powerful, on-demand image optimization.
- Cloudinary / Imgix: Cloud-based image optimization and delivery services for both.
- Deployment Platforms:
- Netlify: Popular for continuous deployment of static sites (integrates with Git).
- Vercel: Similar to Netlify, focused on frontend frameworks and static sites.
- Cloudflare Pages: Another strong contender for fast static site deployment.
- GitHub Pages: Free hosting directly from GitHub repositories (especially good with Jekyll, but works with any SSG).
- Headless CMS:
- Decap CMS (formerly Netlify CMS): Open-source Git-based CMS that pairs well with static sites.
- Sanity.io / Contentful / DatoCMS: API-driven content platforms that can feed data to your SSG.
- Other Utilities:
- Luxon / Moment.js: For advanced date and time manipulation in JavaScript-based Eleventy projects.
- lunr.js: Client-side search library for implementing search functionality with a static index.
- prettier / ESLint: Code formatters and linters for maintaining code quality.
- VS Code Extensions: Markdown extensions, Go template extensions, Nunjucks/Liquid/Vento syntax highlighting.