Closed zachleat closed 5 months ago
This repository is now using lodash style issue management for enhancements. This means enhancement issues will now be closed instead of leaving them open.
View the enhancement backlog here. Don’t forget to upvote the top comment with 👍!
I had a quick stab at implementing this to see what's involved and how deep the rabbit hole could go. I got this far: https://github.com/keithclark/eleventy/pull/1.
The approach seems to work, albeit with very limited testing. If I'm on the right track then I'll explore further.
@zachleat is there anything I can do to help develop this feature request or does it need more votes first?
If I had more than one vote, I would use them all here. I need this to finally transition away from my "sparse collection" hacks.
Shipping with 3.0.0-alpha.6
Tests here: https://github.com/11ty/eleventy/blob/main/test/EleventyVirtualTemplatesTest.js
Examples:
eleventyConfig.addTemplate("inputPath.md", "# Template Content");
// Parses front matter
eleventyConfig.addTemplate("inputPath.md", `---
myKey: myValue
---
# Template Content`);
// Supplemental data (overrides front matter if conflicts)
eleventyConfig.addTemplate("inputPath.md", "# Template Content", { myKey: "myValue" });
If you create a virtual template with the same path as a file system template, we throw an error. Virtual templates and file system templates that attempt to write to the same output location will throw a duplicate permalink error as expected.
Otherwise I would expect virtual templates and file system templates to operate the same, in practice.
Update April 8: this was changed so that paths passed to addTemplate
will be input directory relative by default. This simplies things so that plugins will not have to add an extra code to detect the input directory in order to place virtual templates in the input directory.
Thanks for adding this.
@zachleat your example here and all tests are using .md
templates.
Does it work for any template type?
Also, if the purpose is to allow "automatic content creation in plugins", would it make sense to allow the second parameter to be a path to a template file provided by the plugin, path that could be relative to the plugin root?
Or maybe it requires an additional function:
eleventyConfig.addTemplateFile("./src/inputPath.md", "./src/template-in-plugin.njk", { myKey: "myValue" });
If the file is virtual why does it need an input path and not just an output path/permalink? Is it just a workaround for the rest of eleventy expecting files to have an input path?
why does it need an input path and not just an output path/permalink
I understand it as an output path, to the sources that will be then computed by Eleventy.
I know it's too early and that docs will come later for this, but noting my question here so I don't forget: Let's say I'm authoring a plugin that uses this new API. Do I need to ask users for their preferred template language, or can I safely use any template language in the plugin, even one that the user's Eleventy config does not support? e.g., if a user authors all of their templates in Nunjucks but I want to write my plugin's template content in Liquid, can I do that or does my plugin need to accept the template language and switch
on it to write multiple template strings?
Does it work for any template type?
Yes! That’s the goal! Though saying this out loud I think *.11ty.js
might require some additional work on my end.
Also, if the purpose is to allow "automatic content creation in plugins", would it make sense to allow the second parameter to be a path to a template file provided by the plugin, path that could be relative to the plugin root?
I think it might be more ergonomic to make these paths default to relative to the project’s input directory (rather than project root) with maybe an escape hatch via ~/
similar to WebC (see https://www.11ty.dev/docs/languages/webc/#declaring-components-in-front-matter)
If the file is virtual why does it need an input path and not just an output path/permalink?
I think that’s a fair question! Input paths in Eleventy are the default mechanism (they imply both the template syntax and the default output path) for the MVP of this feature but I think we could add additional functionality:
permalink
is specified for the template.Do I need to ask users for their preferred template language, or can I safely use any template language in the plugin, even one that the user's Eleventy config does not support?
This will have the same limitations of a file system template. So if I add a virtual template that uses njk
and the project has excluded njk
from it’s list of template formats, it will skip processing the virtual template.
That said, there are configuration API methods to add valid template formats programmatically (e.g. eleventyConfig.addTemplateFormats
) so you’ll be able to put these in your plugin code. Not sure if that’s best practice or not yet 🤔
@zachleat
I think it might be more ergonomic to make these paths default to relative to the project’s input directory (rather than project root) with maybe an escape hatch via
~/
similar to WebC
So for a plugin, it would be ~/node_modules/name_of_the_plugin/src/template-in-plugin.njk
?
@nhoizey No, that usage seems unlikely to me. The plugin would populate virtual templates in the input directory, so from a plugin usage might be like this: ./template-in-plugin.njk
which implies an output to /template-in-plugin/index.html
(unless permalink is supplied too).
I’d imagine plugins would offer an override inputPath as part of the options you pass in, for further control.
I'm not sure I understand, I'll have to test it! 😅
Update April 8: this was changed so that paths passed to addTemplate will be input directory relative by default. This simplies things so that plugins will not have to add an extra code to detect the input directory in order to place virtual templates in the input directory.
robots.txt
Examples:Using front matter:
let content = `---
permalink: "/robots.txt"
---
User-agent: *
Allow: /`;
eleventyConfig.addTemplate("robots.njk", content);
Using data argument:
let content = `User-agent: *
Allow: /`;
eleventyConfig.addTemplate("robots.njk", content, {
permalink: "/robots.txt"
});
Thanks again for releasing this! I published a little prototype package that uses this on 3.0.0-alpha.6: https://github.com/AleksandrHovhannisyan/eleventy-plugin-netlify-redirects/. Figured I'd drop it here for folks to test with or to use as a reference for other plugin ideas.
Awesome work @AleksandrHovhannisyan! This is a perfect use case!
I added a few notes about feature testing and version checking to the docs that might interest you here (as you use a cutting edge feature in a plugin):
@zachleat TIL, thanks!
Just a bonus thought here, I think Virtual Templates could replace some advanced Pagination use cases, where the declarative configuration in front matter is just too weak (or too confusing). Pagination does buy you shared resources between pages and might be faster, but that is a claim that would require testing!
https://www.11ty.dev/docs/pagination/
export default function(eleventyConfig) {
let pages = [1, 2, 3];
for(let index of pages) {
eleventyConfig.addTemplate(`pagination-${index}.njk`, `Page {{ pageNumber }}`, {
pageNumber: index,
permalink: `/page-${index}/`
});
}
};
A robots.txt
bot blocker plugin would be awesome too, a la https://ethanmarcotte.com/wrote/blockin-bots/
@zachleat /others: If you were a user of such a plugin, what would your expected API be? I can think of two options:
allowedUserAgents/disallowedUserAgents
front matter variables in your templates. It then aggregates/groups these page slugs by User-agent
and programmatically generates the output file for you.Map
from one or more user agents to their corresponding allowed/disallowed paths.(Either way, it would also offer an option to fetch a list of AI crawlers and dump that into the robots.txt
.)
I'm leaning towards the second option at the moment, but I'm not sure. Example usage here: https://github.com/AleksandrHovhannisyan/eleventy-plugin-robotstxt/blob/73b3fd3c67c8505bae498987d13565571399bdd3/example/.eleventy.js#L4-L13
It does make me wonder though what the advantage would be to doing this:
eleventyConfig.addPlugin(thePlugin, {
rules: new Map(
['*', [{ disallow: '/404' }]],
[['agent1', 'agent2'], [{ disallow: '/1', allow: '/1/2/' }]],
);
});
Instead of just creating that robots.txt
manually. So that makes me think maybe the first option would be better... Except then it's a bit more "magic" and harder to see all the rules at a glance unless you check the output file.
This post and this thread seem to indicate that these new virtual templates could help solve the double layered pagination use case somewhat elegantly.
Lets say I have a collection containing unique categories and posts as arrays of objects for each categories. How should I approach paginated category pages ? Conceptually, I suppose I should be able to:
Now how to implement it ...
@jeromecoupe In the virtual template, you would just write the same content as you would in any ordinary template. So for example, you can reference collections
and other Eleventy globals in the virtual template and they will eventually resolve to the correct variables once Eleventy builds the user's website. You can define this virtual template's content as a JavaScript string; for more complex use cases, you can do what I did and stick the template in a separate file and use the fs
package to open and read the file as a string.
In your case of double pagination (which I implemented on my blog using your article, by the way!), you could create a virtual tag/tags template. The challenge here is giving users of the plugin control over how the content is rendered or what permalink format to use. You might need to give your plugin render props/functions for these bits of info.
As an example of how you might implement this, see this code (experimental plugin I wrote that uses this feature):
@AleksandrHovhannisyan Yep I had a look at your plugin for inspiration (let's call it inspiration cross-pollination). My main blocker right now is that I need to create a custom 11ty collection and, within that, create a virtual template for each item in that collection.
I didn't find a way (yet) to use addTemplate
within the scope of addCollection
.
In other words:
export default function (eleventyConfig) {
eleventyConfig.addCollection("blogpostsByCategories", function (collectionApi) {
const blogposts = collectionApi.getFilteredByGlob(
"./src/content/blogposts/*.md"
);
// [ ... more code ... ]
// find a way to use addTemplate here or to pass this collection I just defined to an external function
}
}
@jeromecoupe Hmm, I think I understand the problem now. If your goal is to do something like collectionYouJustDefined.forEach((item) => addTemplate(...))
so you can create paginated pages like /categories/:category/
, it seems like that's not possible with the current API since addCollection
does not return anything. And Eleventy won't invoke the addCollection
callback until 11ty starts building your site.
@zachleat Just brainstorming here, what if collectionApi
had an addTemplate
method too? Or is it possible to just use eleventyConfig.addTemplate
directly inside an addCollection
callback? (I assume it isn't.)
Also wondering if we should open up a GitHub Discussion for virtual templates as it seems like we all have various pending questions about this.
I also need the ability to add virtual templates to collections.
Virtual pagination template that paginates over various collections should work!
Wrote a short blog post about this: https://www.aleksandrhovhannisyan.com/blog/eleventy-virtual-templates/
v3.0.0-alpha.15 will ship with the ability to add Eleventy Layout files as Virtual Templates. (for #2307)
Just map the template virtual path to be inside the layouts
or includes
directory and it’ll operate the same as a file on the file system.
Usage example:
export default function(eleventyConfig) {
eleventyConfig.addTemplate("virtual.md", `# Hello`, {
layout: "virtual.html"
});
eleventyConfig.addTemplate("_includes/virtual.html", `<!-- Layout -->{{ content }}`);
};
These virtual layouts should have the same functionality as an Eleventy layout on the file system.
For plugins, they may want to use the following pattern (if you don’t know what a project’s includes
or layouts
directory might be):
export default function(eleventyConfig) {
eleventyConfig.addTemplate("virtual.md", `# Hello`, {
layout: "virtual.html"
});
let layoutPath = eleventyConfig.directories.getLayoutPathRelativeToInputDirectory("virtual.html");
eleventyConfig.addTemplate(layoutPath, `<!-- Layout -->{{ content }}`);
};
I'm using several JS classes as templates in a few projects. How would I add those as virtual templates?
Do I have to add
import MyTemplate from '#templates/test';
export default new MyTemplate();
as the content string in eleventyConfig.addTemplate([…])
?
(This didn't work the first time I tried it but it could have very well been my fault. Unfortunately, I haven't had the time yet to investigate further.)
@VividVisions follow along to #3347 please!
Would need:
This would allow automatic content creation in plugins (sitemap.xml, rss feeds, etc)