Site Tools


indieweb:eleventy_gotchas

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));
});

See Also



indieweb/eleventy_gotchas.txt · Last modified: by 127.0.0.1