Closed Wilto closed 3 years ago
ps. eleventy rulez ok
we have access to page
via this.page
, but why don't we have access to this.collections
?
I have the same issue with regard to collection access in shortcodes. My current workaround is the following:
eleventyConfig.addShortcode('getMfr', mfrId => {
let mfrData = JSON.parse(fs.readFileSync('./src/_data/manufacturers.json'))
let mfr = mfrData.find(obj => obj.mfrId === mfrId)
let text = `<a href="${mfr.url}" target="_blank">${mfr.name}</a>`
return text
}
Called with:
{% getMfr product.mfrId %}
It feel ugly, but it works.
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 👍!
If I'd like to ensure this also adds access to filters, would I file a new bug?
I'm hoping to get collection access inside of shortcakes in order to facilitate migration from Jekyll. In particular, Jekyll's page_url
shortcode for obtaining the URL of an item in the "pages" collection (by providing its slug as an argument) is a core feature of Jekyll that currently has no direct equivalent in Eleventy.
Being able to access this.collections
inside of a shortcode definition would solve this neatly.
I have a potential hacky workaround, but I don't know that I really like it…
I don't know if this would always be guaranteed to work, and my 2 file test case probably isn't that great. I'm just abusing my weak assumption that the .addCollection()
code would always complete before we ever call a custom shortcode.
I assume there's a dozen of ways this wouldn't work in the real world, but who knows…
// .eleventy.js
module.exports = function (eleventyConfig) {
// Create a "semi-global" `$pages` array (which will be populated later via a custom "shadow" collection).
let $pages = [];
// I have no idea how the Jekyll `page_url` shortcode works internally, so this is a rough guess. 🤷
eleventyConfig.addShortcode("page_url", function (slug = "") {
// Try and find the specified page slug in our `$pages[]` array by matching the
// template's `filePathStem`.
const page = $pages.find((page) => page.filePathStem === slug);
// No match found, hard error (again, no idea how Jekyll works, but I liked the idea
// of failing fast-and-furious if we have bad links.
if (!page) {
throw new Error(`Unknown page slug: "${slug}"`);
}
// We had a match, so return the page's `url`.
return page.url;
});
// Create a custom "pages" collection, which basically copies/mirrors the items that
// are already tagged "pages".
// Seems silly, but we really are just using this to copy the collection into our semi-global
// `$pages[]` array for lookup by our custom shortcode.
eleventyConfig.addCollection("pages", function (collectionApi) {
$pages = [...collectionApi.getFilteredByTag("pages")];
return $pages;
});
return {
dir: {
input: "src",
output: "www",
},
};
};
And I can use it like this:
---
# src/index.liquid
title: Page One
---
{% page_url "/nested/page-two" %}
---
# src/nested/page-two.liquid
title: Page Two
---
HOME: {% page_url "/index" %}
{% comment %}
This page slug doesn't exist, so will throw a hard error.
BAD: {% page_url "beep boop" %}
{% endcomment %}
// src/src.11tydata.js
module.exports = {
// All all the pages to a "pages" collection. Not sure why, although you did mention a "pages" collection.
tags: ["pages"]
};
<!-- www/index.html -->
/nested/page-two/
<!-- www/nested/page-two/index.html -->
HOME: /
And for conversation's sake, here's the console output when using our very naughty {% page_url "beep boop" %}
shortcode w/ an "invalid" slug.
> Executing task: npm run build <
> 11ty-813@1.0.0 build
> eleventy
[11ty] Problem writing Eleventy templates: (more in DEBUG output)
[11ty] > Having trouble rendering liquid template ./src/nested/page-two.liquid
`TemplateContentRenderError` was thrown
[11ty] > Unknown page slug: "beep boop", file:./src/nested/page-two.liquid, line:3, col:6
`RenderError` was thrown
[11ty] > Unknown page slug: "beep boop"
`Error` was thrown:
[11ty] Error: Unknown page slug: "beep boop"
at Object.<anonymous> (/private/tmp/11ty-813/.eleventy.js:7:13)
at Object.<anonymous> (/private/tmp/11ty-813/node_modules/@11ty/eleventy/src/BenchmarkGroup.js:32:26)
at Object.render (/private/tmp/11ty-813/node_modules/@11ty/eleventy/src/Engines/Liquid.js:152:25)
at async Template._render (/private/tmp/11ty-813/node_modules/@11ty/eleventy/src/TemplateContent.js:421:22)
at async Template.getTemplateMapContent (/private/tmp/11ty-813/node_modules/@11ty/eleventy/src/Template.js:1056:19)
at async TemplateMap.populateContentDataInMap (/private/tmp/11ty-813/node_modules/@11ty/eleventy/src/TemplateMap.js:461:39)
at async TemplateMap.cache (/private/tmp/11ty-813/node_modules/@11ty/eleventy/src/TemplateMap.js:360:5)
at async TemplateWriter._createTemplateMap (/private/tmp/11ty-813/node_modules/@11ty/eleventy/src/TemplateWriter.js:242:5)
at async TemplateWriter.generateTemplates (/private/tmp/11ty-813/node_modules/@11ty/eleventy/src/TemplateWriter.js:275:5)
at async TemplateWriter.write (/private/tmp/11ty-813/node_modules/@11ty/eleventy/src/TemplateWriter.js:321:23)
[11ty] Wrote 0 files in 0.02 seconds (v1.0.0)
The terminal process "/bin/zsh '-c', 'npm run build'" terminated with exit code: 1.
I've got this working as a Liquid Custom Tag, which does seem to have access to collections:
// Implement Jekyll's post_url tag
// Usage: {% post_url post-filename-without-extension %}
eleventyConfig.addLiquidTag("post_url", function (liquidEngine) {
return {
parse: function (tagToken, remainingTokens) {
this.str = tagToken.args; // post-filename-without-extension
},
render: async function (context) {
const postFilenameWithoutExtension = `./_posts/${this.str}`;
const posts = context.environments.collections.post;
const post = posts.find((p) =>
p.inputPath.startsWith(postFilenameWithoutExtension)
);
if (post === undefined) {
throw new Error(`${this.str} not found in posts collection.`);
} else {
return post.url;
}
},
};
});
Also, if you're using LiquidJS, you might be able to get access to collections in custom filters via this.context.environments.collections
(assuming you're not using arrow functions):
eleventyConfig.addFilter("page_url", function (slug = "") {
const pages = this.context.environments.collections.pages;
const page = pages.find(p => p.filePathStem === slug);
if (page) {
return page.url;
}
throw new Error(`Unknown page slug: "${slug}"`);
});
Actually, it looks like Nunjucks might have access to collections when using non-arrow functions as well via this.ctx.collections
object.
This hot mess might work for both LiquidJS and Nunjucks. I'd have to play some more to see if if I can get it working with JavaScript/Eleventy templates:
eleventyConfig.addFilter("page_url", function (slug = "") {
const collections = this.ctx?.collections || // Nunjucks
this.context?.environments.collections || // LiquidJS
{}; // Default to an empty object.
// Use the custom `pages` collection, or else default to an empty array.
const pages = collections.pages || [];
const page = pages.find(p => p.filePathStem === slug);
if (page) {
// We found a matching slug/filePathStem, return the page's `url` property.
return page.url;
}
// ABORT! ABORT!
throw new Error(`Unknown page slug: "${slug}"`);
});
@sentience That's super cool!
I was reading https://liquidjs.com/tutorials/register-filters-tags.html and wasn't sure if you want to use this.str
directly, versus something like const slug = await liquidEngine.evalValue(this.str, ctx);
By running evalValue()
on the this.str
value, it let me pass raw strings, or use frontmatter keys in the custom tag.
---
title: Page One
var: one
---
{% post_url3 "one" %}
{% post_url3 var %}
My slightly modified custom tag looks something like this:
eleventyConfig.addLiquidTag("post_url", function (liquidEngine) {
return {
parse(tagToken, remainingTokens = []) {
this.str = tagToken.args;
},
async render(ctx) {
const slug = await liquidEngine.evalValue(this.str, ctx);
const postFilename = `./src/_posts/${slug}`;
const posts = ctx.environments.collections.post;
const post = posts.find(p => p.inputPath.startsWith(postFilename));
if (post) {
return post.url;
}
throw new Error(`${slug} not found in post collection.`);
},
};
});
@pdehaan To emulate Jekyll's post_url
as closely as possible, you want to be able to pass the post filename without quotes (i.e. {% post_url post-foo %}
not {% post_url "post-foo" %}
), I don't want to use Liquid's evalValue
. If you want a souped up post_url
that can accept dynamic values (but require quotes around a literal string), then your approach definitely makes sense.
I'm now encountering a need for this in eleventy.after. I was surprised that it wasn't available.
Describe the solution you'd like I’d like the contents of existing
collections
to be accessible from inside a shortcode.Additional context I’m building a pattern library dealie, and I’d like to be able to compose “pages“ from individual “components.”
/_src/_components/hgroup/code.html
/_src/_pages/blogpost/code.html
/_src/.eleventy.js
Needing to include
{% set comps = collections.components %}
on every composed page template and cobbling together a new, single-item collection inside the shortcode both feel a little iffy to me.Ideally, since I already have a collection containing all the components, I’d like to filter on that and return the result from the existing collection.