Table of Contents
Eleventy Gotchas
Common bugs and non-obvious behaviours in Eleventy (11ty) static site generator.
1. HTML Escaping in Nunjucks
Symptom: Raw HTML tags visible on the rendered page — angle brackets everywhere instead of formatted content.
Cause: Nunjucks escapes HTML by default for security. Correct when handling user input; wrong when rendering your own compiled content.
Fix: Add the | safe filter in the base layout:
<main id="main"> {{ content | safe }} </main>
Static site generators are secure by default. You have to explicitly declare when you trust your own content.
2. Timezone Off-By-One (Dates)
Symptom: A post dated 2025-01-02 renders as January 1st. Archive grouping puts December posts in November.
Cause: JavaScript's Date object interprets bare YAML dates (2025-01-02) as local time, then converts based on system timezone. In a negative UTC offset, midnight January 2nd becomes the afternoon of January 1st. This is a known issue documented in the 11ty docs.
Fix 1: Parse dates as UTC explicitly:
const date = new Date(dateObj + 'T00:00:00Z');
Fix 2: Use string comparison for sorting instead of date arithmetic:
.sort((a, b) => String(b.data.date).localeCompare(String(a.data.date)))
Fix 3: When grouping by month, use UTC methods — getFullYear()/ getMonth() still convert to local time even after parsing as UTC:
const utcYear = date.getUTCFullYear(); const utcMonth = date.getUTCMonth(); const monthKey = `${utcYear}-${String(utcMonth + 1).padStart(2, '0')}`;
Rule: When working with dates in JavaScript, use UTC everywhere or prepare for timezone pain.
3. collections.all Includes Template Files
Symptom: collections.all returns more items than you have posts — index.njk or other template files appear as collection items.
Fix: Be explicit with glob patterns instead of relying on collections.all:
collectionApi.getFilteredByGlob("src/posts/**/*.md")
4. Data Context Differs in Collection vs. Layout
Symptom: post.data.gratitude works in a loop on the homepage, but post.data.gratitude is undefined in the individual post layout.
Cause: Eleventy's data cascade gives layouts a different context. When rendering a collection item in a loop, the item is post and frontmatter is post.data.fieldname. When the template is the post's own layout, frontmatter is available directly as fieldname.
{# In homepage loop #}
{{ post.data.gratitude }}
{# In post's own layout #}
{{ gratitude }}
5. Archive Month Exclusion Logic
Symptom: Excluding the “current month” from archives causes posts from that month to vanish even when they're not the latest post.
Fix: Exclude the specific latest post, not the entire month:
{% for post in month.posts %}
{% if not (collections.latest and post.data.date == collections.latest[0].data.date) %}
{# render post #}
{% endif %}
{% endfor %}
Monthly Archive Collection
Full pattern for grouping posts by month with correct UTC date handling:
eleventyConfig.addCollection("byMonth", function(collectionApi) { const posts = collectionApi.getFilteredByGlob("src/posts/**/*.md"); const byMonth = {}; posts.forEach(post => { const date = typeof post.data.date === 'string' ? new Date(post.data.date + 'T00:00:00Z') : post.data.date; const monthKey = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`; if (!byMonth[monthKey]) { byMonth[monthKey] = { year: date.getUTCFullYear(), month: date.toLocaleString('en-US', { month: 'long', timeZone: 'UTC' }), monthKey, posts: [] }; } byMonth[monthKey].posts.push(post); }); return Object.values(byMonth).sort((a, b) => b.monthKey.localeCompare(a.monthKey)); });
