Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Virtual Templates: more control over collisions between physical and virtual templates? #3254

Open
danburzo opened this issue Apr 16, 2024 · 4 comments

Comments

@danburzo
Copy link
Contributor

Is your feature request related to a problem? Please describe.

No response

Describe the solution you'd like

With fixing #1612, Eleventy gains the ability to produce ad-hoc, Virtual Templates that work as if they existed as physical files on the disk. Right now (if I understand correctly), Eleventy throws when a virtual template collides with an existing physical template.

This can be guarded against with file-system checks (e.g. fs.exists()), but if Eleventy has a good idea that a physical file already exists, maybe there’s an opportunity for a more graceful API. I’m starting this issue to explore possible directions for this feature.

Describe alternatives you've considered

No response

Additional context

No response

@zachleat
Copy link
Member

“‘first one wins, last one wins, die on error’ with the latter as a default would be super dope”— @eaton

https://fosstodon.org/@eaton@phire.place/112281097866265239

@eaton
Copy link

eaton commented Apr 16, 2024

FYI, the use case that I've been wrestling with for a while in 11ty is slightly tangly, but I believe it's relevant to the issue. I wrote a plugin that

  1. Pulls items from a SaaS content API
  2. Makes each item available as part of the global data, keyed by the unique content ID
  3. Builds different utility collections corresponding for each content type ('articles', 'author bios', etc)

For "special" pages like the home page, customized landing pages, etc, a uuid front matter property combined gets detected and populated with the full content object. For bulk pages (articles, etc) a single paginated template just loops over one of the plugin-built collections.

This works great, EXCEPT when special high profile articles need special presentation/layout treatment. In a perfect world, I'd love to simply drop a custom template into the 11ty directory, add the uuid property to its frontmatter, and have it "take over" that piece of remote content. But because there's no easy way to perform complex build-time alteration of the pagination collection (ie, "process every item that doesn't collide with an existing on-disk output path"), the output paths of the custom template and the paginated record collide and everything grinds to a halt.

I need to do some testing with alpha-6, but using the new "inject a custom template" feature seems like it would simplify some of the work: programmatically inserting a custom template for each content item would allow the plugin to ensure no on-disk templates have "claimed" the content ID before proceeding.

Alternately, allowing finer grain control over the template and output path collision handling could also make these case easier — allowing a paginated page to supply "fallback" versions of pages the can be over-written, for example.

@danburzo
Copy link
Contributor Author

In a perfect world, I'd love to simply drop a custom template into the 11ty directory, add the uuid property to its frontmatter, and have it "take over" that piece of remote content.

This would manifest as something like below, right?

src/posts/my-post.md

---
uuid: 0000-0000-0000-0000
layout: 'layouts/my-custom-layout.njk'
---
// .eleventy.js
eleventyConfig.addTemplate('posts/my-post.md',  "my remote content", {
  uuid: '0000-0000-0000-0000',
  some: 'data',
  more: 'data'
})

In essence, I think of a Virtual Template as providing two pieces of data to an input path:

  • the raw template content (rawInput)
  • front-matter data that participates in the Data Cascade

The collision on an addTemplate() call can have one of these outcomes (more or less corresponding to Node’s system flags):

  • write always writes the template, overwrites existing.
  • add writes template, but throws on existing.
  • skip writes template if it doesn’t already exist.
  • merge writes template, merges with existing template.

So, one declarative approach would be to state your desired outcome, with a method signature like:

addTemplate(path: String, content?: Object, metadata?: String, mode?: String = 'add')

The data types for each argument neatly allow us to have these shortcuts:

// Metadata without content
addTemplate(inputPath, { 
	some: 'data',
	more: 'data'
}, 'merge');

// Content without metadata
addTemplate(inputPath, 'Some content', 'merge')

An alternate, imperative approach:

  • a hasTemplate() method to check, and act upon, a possible conflict
  • the addTemplate() method would always work in write mode, overwriting existing info
  • an additional mergeTemplate() method that works in merge mode to merge in either content or metadata or both.

This set of methods would allow us to express any collision outcome:

if (config.hasTemplate(inputPath)) {
	// mode = write
	config.addTemplate(inputPath, ...);
	// mode = add
	throw new Error('template already exists');
	// mode = skip
	continue;
	// mode = merge
	config.mergeTemplate(inputPath, ...);
}

@danburzo
Copy link
Contributor Author

danburzo commented Apr 17, 2024

A third way to handle collisions, and possibly the most flexible without any additional API surface area would be the callback variant:

config.addTemplate(inputPath, function(content, data) {
   return ({ content: ..., data: ... });
});
  • deciding what to do with existing content and data covers the write and merge modes;
  • throwing inside the function covers the add mode;
  • returning null or undefined equates to skip mode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants