11ty / eleventy

A simpler site generator. Transforms a directory of templates (of varying types) into HTML.
https://www.11ty.dev/
MIT License
16.91k stars 491 forks source link

Accessing data.collections in eleventyComputed.permalink kills paginated pages #3148

Open eaton opened 8 months ago

eaton commented 8 months ago

Operating system

macOS Sonoma 14.2.1

Eleventy

3.0.0-alpha.3

Describe the bug

Using eleventyComputed to build permalinks for paginated pages works great… unless the permalink function access data.collections.anyCollectionName.

If that variable is touched (checking the length of a collection, console logging its existence, etc) the permalink function's return value is ignored, and all of the paginated items produce conflicting permalinks.

Reproduction steps

eleventy.config.js:

export default async function(eleventyConfig) {

  // Manually add the documents to global data
  eleventyConfig.addGlobalData('documents', [
    { "title": "page 1", "id": "page-1", "content": "this is the first data page." },
    { "title": "page 2", "id": "page-2", "content": "this is the second data page." },
    { "title": "page 3", "id": "page-3", "content": "this is the third data page." }
  ]);

  // Build a dummy collection
  eleventyConfig.addCollection("test", () => []);

  // Define computed functions
  eleventyConfig.addGlobalData('eleventyComputed.permalink', computedPermalink);
}

const computedPermalink = function() {
  return( data ) => {
    if (data.collections.test || 1) {
      return `/${data.document.id}/`;
    }
  }
}

page.md:

---
pagination:
  data: documents
  size: 1
  alias: document
---
# {{ document.title }}

{{ document.content }}

Expected behavior

When the data.collections.test reference is removed from the code above, all three pages are generated correctly.

  Eleventy:Logger Writing _site/page-1/index.html from ./page.md (liquid) +0ms
  Eleventy:Logger Writing _site/page-2/index.html from ./page.md (liquid) +0ms
  Eleventy:Logger Writing _site/page-3/index.html from ./page.md (liquid) +0ms
  Eleventy:Template _site/page-3/index.html written.. +5ms
  Eleventy:Template _site/page-1/index.html written.. +0ms
  Eleventy:Template _site/page-2/index.html written.. +0ms
[11ty] Wrote 3 files in 0.05 seconds (v3.0.0-alpha.3)

When data.collections.test is referenced in any way, building the site generates the following error:

[11ty] Problem writing Eleventy templates:
[11ty] Output conflict: multiple input files are writing to `_site/page/index.html`. Use distinct `permalink` values to resolve this conflict.
[11ty]   1. ./page.md
[11ty]   2. ./page.md
[11ty]   3. ./page.md (via DuplicatePermalinkOutputError)
[11ty] Wrote 0 files in 0.05 seconds (v3.0.0-alpha.3)

Reproduction URL

GitHub Repo

Screenshots

No response

Kein1945 commented 8 months ago

I've spend two hours to understand why it's happening. Solutions is easy. 11ty generating N pages for each pagination page, and trying to create files with same name. Solution to add permalink, which is depends on pagination page number:


permalink: /my-awesome-listing/{% if pagination.pageNumber > 0 %}/page-{{ pagination.pageNumber }}{% endif %}
Kein1945 commented 8 months ago

I think it would be greate to have a warning at least if we have paggination and missing permalink properties.

eaton commented 8 months ago

I think there might be some misunderstanding — the issue isn't that a paginated page without a permalink is generating permalink collisions. It's that the eleventyComputed permalink is working correctly until the permalink generation function access one of the collections in data.collection; once a collection is touched via the data proxy object, the permalink function's return value appears to be ignored.

export default async function(eleventyConfig) {
  // ...
  eleventyConfig.addGlobalData('eleventyComputed.permalink', computedPermalink);
}

const computedPermalink = function() {
  return( data ) => {
      // Accessing data.collections.[whatever] in any way -- with a conditional check,
      // iterating its contents, etc -- prevents the next line from returning the permalink value.
      console.log(data.collections.all);

      // This return value is ignored unless the console.log call is commented out
      return `/${data.document.id}/`;
  }
}

The discarding of the returned permalink is what results in the collisions — just adding a hard-coded permalink line to the frontmatter works because it doesn't touch any collections.

eaton commented 8 months ago

After some additional digging, I think I'm a bit closer to figuring out what's going on:

  1. TemplateMap.cache() gets called whenever any collections data is retrieved. (This includes collections.all, etc)
  2. TemplateMap.checkForDuplicatePermalinks() is called inside of TemplateMap.cache(); by that point all of the paginated outputs for a single template have already been expanded. If duplicates are found, an error is thrown.
  3. If an eleventyComputed.permalink function accesses any collections, the permalinks are checked for collisions before the actual permalink can be returned; eleventy uses a fallback permalink based on the filepath, which causes collisions for paginated templates.

I'm not sure if this is something that can be resolved (I think i remember some poking near this in the serverless code?), or if I'm just pushing computed permalinks too far for my own good. Will continue poking at it, but at least I understand what order of operations triggers the issue.

olets commented 7 months ago

I ran into this too.

I think our problem is actually "accessing data.collections in eleventyComputed.permalink makes the permalink empty", and that the collision error you're getting is a secondary effect of multiple items getting the same buggy permalink (and that the secondary error prevents the primary from being logged).

Stackblitz reproduction https://stackblitz.com/edit/stackblitz-starters-ukmhij (where you used addGlobalData I used files; the repro uses v3 alpha but I confirmed that the same thing happens in the latest v2)

The bug trigger:

export default {
  eleventyComputed: {
    permalink: (data) => {
      console.log(data?.collections?.length);

      return;
    },
  },
}

With the computed permalink applied to only one input file, the error is (with a real path instead of <input file path>)

   [11ty] 1. Having trouble writing to "" from "<input file path>" (via EleventyTemplateError)

The debug mode has more details (with a real path instead of <input file path>)

  Eleventy:Template First round of computed data for '<input file path>' +8ms
  Eleventy:Template Rendering permalink for '<input file path>': (((11ty(((permalink)))11ty))) becomes '(((11ty(((permalink)))11ty)))' +3ms
  Eleventy:ComputedData 'page.url' accesses [ 'permalink' ] variables +0ms
  Eleventy:Template Rendering permalink for '<input file path>': (((11ty(((permalink)))11ty))) becomes '(((11ty(((permalink)))11ty)))' +1ms
  Eleventy:ComputedData 'page.outputPath' accesses [ 'permalink' ] variables +0ms
  Eleventy:ComputedData 'eleventyExcludeFromCollections' accesses [] variables +5ms
  Eleventy:ComputedData 'permalink' accesses [ 'collections', 'collections.length' ] variables +0ms
  Eleventy:ComputedData Computed data order of execution: [ 'eleventyExcludeFromCollections' ] +1ms
  Eleventy:TemplateMap Collection: collections.all size: 1 +13ms
  Eleventy:TemplateMap Collection: collections.posts size: 1 +0ms
  Eleventy:TemplateMap Collection: collections.all size: 1 +0ms
  Eleventy:Template Second round of computed data for '<input file path>' +6ms
  Eleventy:ComputedData Computed data order of execution: [ 'collections.length', 'permalink', 'page.url', 'page.outputPath' ] +0ms
  Eleventy:TemplateWriter Template map created. +124ms
  Eleventy:Logger Writing  from <input file path> (liquid) +0ms
[11ty] Problem writing Eleventy templates:
[11ty] 1. Having trouble writing to "" from "<input file path>" (via EleventyTemplateError)
  Eleventy:EleventyErrorHandler (error stack): EleventyTemplateError: Having trouble writing to "" from "<input file path>"
  Eleventy:EleventyErrorHandler     at eval (/home/projects/stackblitz-starters-ukmhij/node_modules/@11ty/eleventy/src/TemplateWriter.js:390:8)
  Eleventy:EleventyErrorHandler     at async Eleventy.executeBuild (/home/projects/stackblitz-starters-ukmhij/node_modules/@11ty/eleventy/src/Eleventy.js:1259:10) +0ms

The source lines linked in that error are

  1. https://github.com/11ty/eleventy/blob/main/src/TemplateWriter.js#L374
  2. https://github.com/11ty/eleventy/blob/main/src/Eleventy.js#L1259