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

Configuration API method to create a content template (aka Virtual Templates) #1612

Closed
zachleat opened this issue Jan 28, 2021 · 34 comments
Closed
Labels
enhancement: favorite Vanity label! The maintainer likes this enhancement request a lot. enhancement feature: 🛠 configuration Related to Eleventy’s Configuration file needs-documentation Documentation for this issue/feature is pending!

Comments

@zachleat
Copy link
Member

Would need:

  • template engine
  • permalink/path
  • content

This would allow automatic content creation in plugins (sitemap.xml, rss feeds, etc)

@zachleat zachleat added enhancement needs-votes A feature request on the backlog that needs upvotes or downvotes. Remove this label when resolved. enhancement: favorite Vanity label! The maintainer likes this enhancement request a lot. labels Jan 28, 2021
@zachleat
Copy link
Member Author

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 👍!

@zachleat zachleat changed the title Configuration API method to create a content template. Configuration API method to create a content template (aka Virtual Templates) Dec 14, 2022
@keithclark
Copy link

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: keithclark#1.

The approach seems to work, albeit with very limited testing. If I'm on the right track then I'll explore further.

@keithclark
Copy link

@zachleat is there anything I can do to help develop this feature request or does it need more votes first?

@gerwitz
Copy link

gerwitz commented Jan 12, 2023

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.

@zachleat zachleat removed the needs-votes A feature request on the backlog that needs upvotes or downvotes. Remove this label when resolved. label Mar 28, 2024
@zachleat zachleat added this to the Eleventy 3.0.0 milestone Mar 28, 2024
@zachleat zachleat reopened this Mar 28, 2024
zachleat added a commit that referenced this issue Mar 28, 2024
@zachleat
Copy link
Member Author

zachleat commented Mar 28, 2024

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.

@keithclark
Copy link

Thanks for adding this.

@nhoizey
Copy link
Contributor

nhoizey commented Mar 29, 2024

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

@jonsage
Copy link

jonsage commented Mar 29, 2024

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?

@nhoizey
Copy link
Contributor

nhoizey commented Mar 29, 2024

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.

@AleksandrHovhannisyan
Copy link
Contributor

AleksandrHovhannisyan commented Mar 29, 2024

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?

@zachleat
Copy link
Member Author

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:

  1. Use an output path instead of input path.
  2. Don’t require an input path or an output path if a permalink is specified for the template.

@zachleat
Copy link
Member Author

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 🤔

@nhoizey
Copy link
Contributor

nhoizey commented Mar 29, 2024

@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?

@zachleat
Copy link
Member Author

zachleat commented Apr 1, 2024

@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.

@nhoizey
Copy link
Contributor

nhoizey commented Apr 2, 2024

I'm not sure I understand, I'll have to test it! 😅

@zachleat
Copy link
Member Author

zachleat commented Apr 8, 2024

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

@zachleat zachleat added the feature: 🛠 configuration Related to Eleventy’s Configuration file label Apr 10, 2024
@AleksandrHovhannisyan
Copy link
Contributor

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.

@zachleat
Copy link
Member Author

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):

@AleksandrHovhannisyan
Copy link
Contributor

@zachleat TIL, thanks!

@zachleat
Copy link
Member Author

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

@zachleat
Copy link
Member Author

A robots.txt bot blocker plugin would be awesome too, a la https://ethanmarcotte.com/wrote/blockin-bots/

@AleksandrHovhannisyan
Copy link
Contributor

AleksandrHovhannisyan commented Apr 17, 2024

@zachleat /others: If you were a user of such a plugin, what would your expected API be? I can think of two options:

  1. The plugin allows you to specify 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.
  2. The plugin allows you to pass in a 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.

@jeromecoupe
Copy link

jeromecoupe commented Apr 17, 2024

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:

  • create a virtual template for each category
  • use that template to paginate posts in each category using pagination

Now how to implement it ...

@AleksandrHovhannisyan
Copy link
Contributor

@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):

https://github.com/AleksandrHovhannisyan/eleventy-plugin-netlify-redirects/blob/0607308641abe6ac5d4c47c5f01d084ea1a4925c/src/index.js#L13-L14

https://github.com/AleksandrHovhannisyan/eleventy-plugin-netlify-redirects/blob/0607308641abe6ac5d4c47c5f01d084ea1a4925c/src/redirects.liquid#L12-L18

@jeromecoupe
Copy link

jeromecoupe commented Apr 19, 2024

@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
  }
}

@AleksandrHovhannisyan
Copy link
Contributor

AleksandrHovhannisyan commented Apr 19, 2024

@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.

@monochromer
Copy link
Contributor

I also need the ability to add virtual templates to collections.

@zachleat
Copy link
Member Author

Virtual pagination template that paginates over various collections should work!

@AleksandrHovhannisyan
Copy link
Contributor

Wrote a short blog post about this: https://www.aleksandrhovhannisyan.com/blog/eleventy-virtual-templates/

@zachleat
Copy link
Member Author

zachleat commented Jul 2, 2024

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.

@zachleat
Copy link
Member Author

zachleat commented Jul 2, 2024

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

@VividVisions
Copy link
Contributor

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.)

@zachleat
Copy link
Member Author

zachleat commented Jul 4, 2024

@VividVisions follow along to #3347 please!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement: favorite Vanity label! The maintainer likes this enhancement request a lot. enhancement feature: 🛠 configuration Related to Eleventy’s Configuration file needs-documentation Documentation for this issue/feature is pending!
Projects
None yet
Development

No branches or pull requests

9 participants