Open AleksandrHovhannisyan opened 3 years ago
@solution-loisir Thanks, really glad to hear it! I've updated the post to mention noscript
as an enhancement.
Hi @AleksandrHovhannisyan do you have the final code for your post? Im trying to follow up on it but im getting Expected positive integer for width but received 0 of type number
.
const ImageWidths = {
ORIGINAL: null,
PLACEHOLDER: 24,
};
const imageShortcode = async (
relativeSrc,
alt,
widths = [400, 800, 1280],
baseFormat = 'jpeg',
optimizedFormats = ['webp', 'avif'],
sizes = '100vw'
) => {
const { dir: imgDir } = path.parse(relativeSrc);
const fullSrc = path.join('src', relativeSrc);
const imageMetadata = await Image(fullSrc, {
widths: [ImageWidths.ORIGINAL, ImageWidths.PLACEHOLDER, ...widths],
formats: [...optimizedFormats, baseFormat],
outputDir: path.join('dist', imgDir),
urlPath: imgDir,
filenameFormat: (hash, src, width, format) => {
const suffix = width === ImageWidths.PLACEHOLDER ? 'placeholder' : width;
const extension = path.extname(src);
const name = path.basename(src, extension);
return `${name}-${hash}-${suffix}.${format}`;
},
});
// Map each unique format (e.g., jpeg, webp) to its smallest and largest images
const formatSizes = Object.entries(imageMetadata).reduce((formatSizes, [format, images]) => {
if (!formatSizes[format]) {
const placeholder = images.find((image) => image.width === ImageWidths.PLACEHOLDER);
// 11ty sorts the sizes in ascending order under the hood
const largestVariant = images[images.length - 1];
formatSizes[format] = {
placeholder,
largest: largestVariant,
};
}
return formatSizes;
}, {});
// Chain class names w/ the classNames package; optional
// const picture = `<picture class="${classNames('lazy-picture', className)}"> //removed to use without classNames
const picture = `<picture class="lazy-picture">
${Object.values(imageMetadata)
// Map each format to the source HTML markup
.map((formatEntries) => {
// The first entry is representative of all the others since they each have the same shape
const { format: formatName, sourceType } = formatEntries[0];
const placeholderSrcset = formatSizes[formatName].placeholder.url;
const actualSrcset = formatEntries
// We don't need the placeholder image in the srcset
.filter((image) => image.width !== ImageWidths.PLACEHOLDER)
// All non-placeholder images get mapped to their srcset
.map((image) => image.srcset)
.join(', ');
return `<source type="${sourceType}" srcset="${placeholderSrcset}" data-srcset="${actualSrcset}" data-sizes="${sizes}">`;
})
.join('\n')}
<img
src="${formatSizes[baseFormat].placeholder.url}"
data-src="${formatSizes[baseFormat].largest.url}"
width="${width}"
height="${height}"
alt="${alt}"
class="lazy-img"
loading="lazy">
</picture>`;
return picture;
};
@bronze Looks like my post may have a typo. I believe it should be this for the image width and height attributes:
width="${formatSizes[baseFormat].largest.width}"
height="${formatSizes[baseFormat].largest.height}"
Hey @AleksandrHovhannisyan! Your article is amazing. It got met set up and running on my local servers and on Netlify dev. I'm just having an issue which has turned out to be quite a headache -- when I'm deploying to Netlify it just doesn't want to play nice. I get this error:
10:24:28 AM: [11ty] EleventyShortcodeError: Error with Nunjucks shortcode
Image(via Template render error) 10:24:28 AM: [11ty] 3. ENOENT: no such file or directory, stat 'src/images/uploads/Worldwalker_Awakening.png' (via Template render error)
I've tried everything from modifying the fullSrc object, the frontmatter values for my posts, etc... and it always works well on the local server but I can't quite crack it on the actual netlify deploy. Any ideas?
Actually, I fixed it. The real problem was the fact that I was trying to run a Synchronous version of the Image shortcode. The reason for that is that I have a nunjucks macro I was trying to get images in, and the error comes from the synchronous code. The Asynchronous code works perfectly.
I need to either figure out an alternative to macros, or get the synchronous version of the code right. If you have any ideas or insights, that would be cool!
@KingScroll Glad you figured it out! Unfortunately, I don't believe you can use async shortcodes in Nunjucks macros. See the issue here: https://github.com/11ty/eleventy/issues/1613. I believe you'll need to use the synchronous version. But I recall running into issues with that as well, so unfortunately, I had to use Liquid for my site.
@KingScroll Glad you figured it out! Unfortunately, I don't believe you can use async shortcodes in Nunjucks macros. See the issue here: 11ty/eleventy#1613. I believe you'll need to use the synchronous version. But I recall running into issues with that as well, so unfortunately, I had to use Liquid for my site.
Hello again! So I got rid of the macro (it was just for one element), so no more synchronous stuff! Bad news: it just doesn't work. I'm still getting the same error, unfortunately, which means that my problem wasn't quite what I thought it was. I'm still getting this error on Netlify:
4:12:49 PM: [11ty] Problem writing Eleventy templates: (more in DEBUG output) 4:12:49 PM: [11ty] 1. Having trouble rendering njk template ./src/content/projects/projects.njk (via TemplateContentRenderError) 4:12:49 PM: [11ty] 2. (./src/content/projects/projects.njk) 4:12:49 PM: [11ty] EleventyShortcodeError: Error with Nunjucks shortcode
Image(via Template render error) 4:12:49 PM: [11ty] 3. ENOENT: no such file or directory, stat 'src/images/uploads/Worldwalker_Awakening.png' (via Template render error)
I suspect it might have something to do with the path modifications in the shortcode function.
My file structure is as follows:
The images copy over in their optimized format to public/images/uploads/
via PassthroughCopy
.
It all works in my local server, but Netlify doesn't seem to want it. Do you think you can help me with this? I really can't quite crack it
Many thanks for this article, everything is explained in an easy and objective way. Unfortunately, I'm having a problem with my code and I believe is something with my outputDir and urlPath configuration (which is weird since the structure is very similar to the one exemplified in the article). The only difference is that I use /dist/ as the output directory, not /_site/.
So i just changed this line of code
outputDir: path.join('_site', imgDir),
To this
outputDir: path.join('dist', imgDir),
The images were correctly copied to the /dist/assets/images/ directory, but instead of the image I'm receiving a text written "undefined" on my website.
Here's my imageShortcode:
const imageShortcode = async ( relativeSrc, alt, className, widths = [null, 400, 800, 1280], formats = ['jpeg', 'webp'], sizes = '100vw' ) => { const { dir: imgDir } = path.parse(relativeSrc); const fullSrc = path.join('src', relativeSrc); const imageMetadata = await Image(fullSrc, { widths, formats, outputDir: path.join('dist', imgDir), urlPath: imgDir }); };
And this is how I'm using the shortcode inside the njk file:
{% image "/assets/images/image-1.jpg", "image alt text", "(min-width: 30em) 50vw, 100vw" %}
Any idea what could be happening? (I apologize in advance if this is not the right place for my question)
@werls This is the right place to ask, no worries. Sounds like your shortcode maybe isn't returning anything. Either that or some sort of async issue.
Oops, my bad. Actually my shortcode wasn't returning anything. Solved now. Thank you!
Do you see the possibility of having this flow over classical markdown image tags instead of having a liquid shortcodes?
I just want to keep images as simple markdown
![Some alternative text](images/example.jpg]
@muratcorlu I wish! I tried to get that to work at some point but hit some roadblocks along the way. There's an open issue here where I've provided more context on the problem: https://github.com/11ty/eleventy/issues/2428#issuecomment-1152703912. Ben Holmes created a demo here that I almost got working: https://github.com/Holben888/11ty-image-optimization-demo. The TL;DR of the issue is that if you add a custom Markdown extension via 11ty's addExtension
API, you opt out of 11ty processing your Markdown files for templating, so things like shortcodes and partials won't work.
I think it would be possible to write a markdown-it plugin similar to this one (very old), but which would use the 11ty image plugin for processing images. It could be interesting to have both a regular 11ty shortcode and a markdown plugin. I might test this over the weekend (if I find the time) just to see what's possible...
@solution-loisir Ooh, that's a clever idea! Let me know what you figure out.
Hi @muratcorlu and @AleksandrHovhannisyan, I wrote a markdown-it plugin which uses the synchronous version of the eleventy-img plugin. This my first markdown-it plugin so it's probably not perfect. It serves as a proof of concept for the discussion. Here's the code. I did not publish it so it's used as a local function via the regular markdown-it API like:
const markdownIt = require('markdown-it');
const markdownItEleventyImg = require("./markdown-it/markdown-it-eleventy-img");
module.exports = function(config) {
config.setLibrary('md', markdownIt ({
html: true,
breaks: true,
linkify: true
})
.use(markdownItEleventyImg, {
widths: [800, 500, 300],
lazy: false
});
}
I think that the shortcode is more flexible and is much easier to write and maybe to maintain. But still, I see some value in using modern image format while keeping the authoring simple and comfortable. Especially if you have standard dimension for images in markdown. This could be developed much further (adding <figure>
, controlling loading, etc.) Tell me what you think. Feel free to ask questions. Thanks for the challenge!
@solution-loisir Very cool! I wish markdown-it supported async renderers 😞 (And had better docs for how to write plugins.) I bet you could take this idea further and have the plugin take a custom image rendering function as an option. That way, users can either supply a renderer that uses the 11ty image plugin or use something else entirely.
I bet you could take this idea further and have the plugin take a custom image rendering function as an option.
That's a very good idea, and still provide a default function. I like that, I may fiddle with this a little. If it takes shape enough, I may consider publishing eventually. Thanks for your input! ☺️
Hey, just to let you know markdown-it-eleventy-img is now live! @AleksandrHovhannisyan, I did consider your idea of providing a callback function to the user, but decided to go a different way. The main idea here is to provide the ability to use modern image formats while keeping the simplicity and the essence of markdown. I'm pretty new to all this so, check it out, use it, let me know what you think! :-)
@solution-loisir Nice work! I'll take this for a spin when I have some downtime 🙂 My main reasoning for not using the 11ty image plugin directly is that it would make the plugin's API simpler (you wouldn't need to forward 11ty image's options to the plugin), and it would also give users more control over how they want to render their images. For example, my custom 11ty image shortcode is a bit more involved and has some custom rendering logic. But this sounds promising for simpler use cases.
and it would also give users more control over how they want to render their images.
Fair point. I think it could be implemented side by side for a do it your way use case. It would complete the plugin nicely.
@KingScroll Glad you figured it out! Unfortunately, I don't believe you can use async shortcodes in Nunjucks macros. See the issue here: 11ty/eleventy#1613. I believe you'll need to use the synchronous version. But I recall running into issues with that as well, so unfortunately, I had to use Liquid for my site.
Hello again! So I got rid of the macro (it was just for one element), so no more synchronous stuff! Bad news: it just doesn't work. I'm still getting the same error, unfortunately, which means that my problem wasn't quite what I thought it was. I'm still getting this error on Netlify:
4:12:49 PM: [11ty] Problem writing Eleventy templates: (more in DEBUG output) 4:12:49 PM: [11ty] 1. Having trouble rendering njk template ./src/content/projects/projects.njk (via TemplateContentRenderError) 4:12:49 PM: [11ty] 2. (./src/content/projects/projects.njk) 4:12:49 PM: [11ty] EleventyShortcodeError: Error with Nunjucks shortcode
Image(via Template render error) 4:12:49 PM: [11ty] 3. ENOENT: no such file or directory, stat 'src/images/uploads/Worldwalker_Awakening.png' (via Template render error)
I suspect it might have something to do with the path modifications in the shortcode function.
My file structure is as follows:
The images copy over in their optimized format to
public/images/uploads/
viaPassthroughCopy
.It all works in my local server, but Netlify doesn't seem to want it. Do you think you can help me with this? I really can't quite crack it
Hi @KingScroll
I get the exact same error on Netlify.
But I only get it when it is images with transparency (png) I try to convert.
Everything works perfectly on my local machine. But when I try to build on Netlify it fails with the exact same error as you get.
If I the use the image (still png) but without transparency - it works like a charm on Netlify.
Do you know - @AleksandrHovhannisyan - if something related to images with a transparent background could be the cause of trouble?
@MarkBuskbjerg Wish I could help, but it's hard to say without seeing the code for your site. My guess is that this is still a Nunjucks async issue in disguise, although if you say non-transparent PNGs work, that might not be the issue.
I'm having a hard time wrapping my head around how to pass different widths in the shortcode than the defaults. If I'm using your defaults and would rather the img
be 200px and 480px what would the shortcode look like?
@miklb Since Nunjucks supports array expressions natively, you could do:
{% image 'src', 'alt', [100, 200, etc.] %}
Or, if you're using an object argument:
{% image src: 'src', alt: 'alt', widths: [100, 200, etc.] %}
In Liquid, things are unfortunately not as easy because it doesn't support array expressions out of the box; you have to split strings on a delimiter, like this:
{% assign widths = "100,200,300" | split: "," %}
That's a bit of a problem in situations like this where you want to have an array of numbers, not an array of strings. On my site, what I do is create an intermediate include that assembles my arguments as JSON and forwards them to my image shortcode:
Allowing me to do this in Liquid:
{% include image.html src: "src", alt: "alt", widths: "[100, 200, 300]" %}
Such that the string of arrays, when JSON-parsed, becomes an array of numbers. A bit convoluted, but I don't know of any other workarounds. If you find one, do let me know!
Thanks. Seems a little too convoluted for my needs. I may opt for two different shortcodes—one for full content width images and one for floated images.
@miklb That makes a lot more sense! Good call.
@AleksandrHovhannisyan just wanted to say I re-read your post and realized you already covered my question and after reading https://www.aleksandrhovhannisyan.com/blog/passing-object-arguments-to-liquid-shortcodes-in-11ty/ I better understand your include. I hated the idea of duplicating code for one argument. Cheers.
@miklb Fwiw, I think your proposed solution would've also worked. This is what I imagined:
const specialImage = async (args) => {
const image = await imageShortcode({ ...args, widths: [100, 200, etc.] });
return image;
}
And then you could register that as its own shortcode and use it:
{% specialImage 'src', 'alt' %}
Either way works, though! The include approach is a little more flexible in Liquid in case you need to vary other arguments as well and want to use named arguments.
Hello, so I'm using image.liquid not image.html in my _includes to create an intermediate include but I'm running into the following issue:
The include in the index looks like this:
{% include 'image', src: 'assets/image-01.jpg', alt: 'this is s test' %}
And the JS shortcode looks like this as following your guide
const imageShortcode = async ( src, alt, className = undefined, widths = [400, 800, 1280], formats = ['webp', 'jpeg'], sizes = '100vw' ) => { const imageMetadata = await Image(src, { widths: [...widths, null], formats: [...formats, null], outputDir: '_site/assets/images', urlPath: '/assets', }) const imageAttributes = { alt, sizes, loading: 'lazy', decoding: 'async', } return Image.generateHTML(imageMetadata, imageAttributes) }
Along with the global filter
eleventyConfig.addFilter('fromJson', JSON.parse)
Am I missing something?
@truleighsyd This error usually occurs when you pass in the wrong path for the shortcode name, so 11ty/Liquid cannot find the partial (image.liquid
in this case). Can you try 'image.liquid'
with an explicit extension? If that doesn't work, please share your 11ty config's return value (directory config) and your project structure.
With liquid includes/renders you do not need to include the extension name. Unless this is specific to working with 11ty shortcodes? Here's the the eleventy config return value.
UserConfig {
events: AsyncEventEmitter {
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
[Symbol(kCapture)]: false
},
benchmarkManager: BenchmarkManager {
benchmarkGroups: { Configuration: [BenchmarkGroup], Aggregate: [BenchmarkGroup] },
isVerbose: true,
start: 46588.24823799729
},
benchmarks: {
config: BenchmarkGroup {
benchmarks: [Object],
isVerbose: true,
logger: [ConsoleLogger],
minimumThresholdMs: 0,
minimumThresholdPercent: 8
},
aggregate: BenchmarkGroup {
benchmarks: {},
isVerbose: false,
logger: [ConsoleLogger],
minimumThresholdMs: 0,
minimumThresholdPercent: 8
}
},
collections: {},
precompiledCollections: {},
templateFormats: undefined,
liquidOptions: {},
liquidTags: {},
liquidFilters: {
slug: [Function],
slugify: [Function],
url: [Function],
log: [Function],
serverlessUrl: [Function],
getCollectionItem: [Function],
getPreviousCollectionItem: [Function],
getNextCollectionItem: [Function],
makeUppercase: [Function],
toISOString: [Function],
toJson: [Function],
fromJson: [Function]
},
liquidShortcodes: { image: [Function] },
liquidPairedShortcodes: {},
nunjucksEnvironmentOptions: {},
nunjucksFilters: {
slug: [Function],
slugify: [Function],
url: [Function],
log: [Function],
serverlessUrl: [Function],
getCollectionItem: [Function],
getPreviousCollectionItem: [Function],
getNextCollectionItem: [Function],
toJson: [Function],
fromJson: [Function]
},
nunjucksAsyncFilters: {},
nunjucksTags: {},
nunjucksGlobals: {},
nunjucksShortcodes: { image: [Function] },
nunjucksAsyncShortcodes: {},
nunjucksPairedShortcodes: {},
nunjucksAsyncPairedShortcodes: {},
handlebarsHelpers: {
slug: [Function],
slugify: [Function],
url: [Function],
log: [Function],
serverlessUrl: [Function],
getCollectionItem: [Function],
getPreviousCollectionItem: [Function],
getNextCollectionItem: [Function],
toJson: [Function],
fromJson: [Function]
},
handlebarsShortcodes: { image: [Function] },
handlebarsPairedShortcodes: {},
javascriptFunctions: {
slug: [Function],
slugify: [Function],
url: [Function],
log: [Function],
serverlessUrl: [Function],
getCollectionItem: [Function],
getPreviousCollectionItem: [Function],
getNextCollectionItem: [Function],
toJson: [Function],
fromJson: [Function],
image: [Function]
},
pugOptions: {},
ejsOptions: {},
markdownHighlighter: null,
libraryOverrides: {},
passthroughCopies: { 'assets/*.js': 'assets', 'assets/*.css': 'assets' },
layoutAliases: {},
linters: {},
transforms: {},
activeNamespace: '',
DateTime: [Function: DateTime],
dynamicPermalinks: true,
useGitIgnore: true,
ignores: Set { 'node_modules/**' },
dataDeepMerge: true,
extensionMap: Set {},
watchJavaScriptDependencies: true,
additionalWatchTargets: [],
browserSyncConfig: {},
globalData: {},
chokidarConfig: {},
watchThrottleWaitTime: 0,
dataExtensions: Map {},
quietMode: false,
plugins: [],
_pluginExecution: false,
useTemplateCache: true,
dataFilterSelectors: Set {},
dir: undefined,
logger: ConsoleLogger {
_isVerbose: true,
outputStream: Readable {
_readableState: [ReadableState],
readable: true,
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
[Symbol(kCapture)]: false
}
}
}
And here's project structure. It's just a test project to experiment with different 11ty features.
@truleighsyd The reason I mentioned including the extension is because I have some partials that are HTML (with embedded liquid), and I always have to do {% include 'partial.html' %}
. Give that a shot, but if it doesn't work I'm not sure. Looks like you're just using the default 11ty dir config, so that should work.
Hmm weird it doesn't work this way but the following does. The issue is that it reads it as an obj so need to pass the obj. The following code works. Thanks : )
Hi, I was trying to use your short code. However, I have one question: Can it be automated, i.e. if iterating over data, can I include front matter variable in the short code? I tried just plugging it in, but it did not work. Thanks for the hint, if it is possible
@bulecampur Should be possible. For example, if src
is a front-matter variable available in the scope where you're using the shortcode, you should be able to do:
{% image src, 'alt', etc. %}
(Doesn't have to be src
)
Thank you Aleksandr, the front matter variable is not in the scope but pulled from either collection or it is a variable from global data. For example on the homepage:
{% for origin in origins %} <img src="{{ origin.imgurl }}" alt="{{ origin.alt }}" />
{% endfor %}
I am trying to replace the with the shortcode but it returns an error that it could not find the variable. Maybe it's just not possible?
I am new to Eleventy and only have rudimentary coding skills (was using Hugo for a little bit before).
Thanks @AleksandrHovhannisyan, this was very helpful. I am new to 11ty and I was looking for a simple way to process some source images (compression, resizing) that aren't going to end up in <picture>
tags (for example, generating site icons from a svg). It was very simple in the end -- just add the outputDir
to the config -- but I wanted to share what I did in case it is useful for others who don't necessarily need to work through the full shortcode solution.
const Image = require("@11ty/eleventy-img");
const path = require("path");
module.exports = function(config) {
config.addPassthroughCopy("img/*.svg");
(async () => {
/**
* Preserve the original file names
*/
const filenameFormat = function (id, src, width, format, options) {
const extension = path.extname(src);
const name = path.basename(src, extension);
return `${name}-${width}.${format}`;
};
[
'img/footer.png',
'img/footer-dark.png',
'img/header.png',
'img/header-dark.png',
].forEach(async image => {
await Image(image, {
formats: ['webp', 'jpeg'],
widths: [480, 768, 1200],
outputDir: '_site/img',
filenameFormat
})
});
await Image('img/site-icon.svg', {
formats: ['png'],
widths: [32, 180, 192, 512],
outputDir: '_site/img',
filenameFormat
});
};
Happy to be informed if this is a bad way to go about it, generally. :+1:
@NateWr That works! Alternatively, you could add a dedicated shortcode for just your favicons. That's what I do on my site. Although your version is probably faster because you only run that logic once in the config, whereas the shortcode approach would run it on every template build (images would be cached, but still).
I really like your work and I would be happy, if you could help me a bit. I use Elventy 3 and PUG in my template and shortcodes don't work. The latest version of eleventy-img simply convert img tag to picture tag, but it doesn't put a className of img to picture. It looks like a known bug, but I still need to do the work. My rep is here for your reference: https://github.com/hottabov/eleventy-pug-scss-typescript/ I need to put any className of img to generated picture tag, but I can't. Thank you!
Really nice article (and very nice web site by the way)! You just inspire me to refactor my custom and hacky solution to use Eleventy-img instead. I reused some of your concepts and some of your code (I hope you don't mind). Here's what I came up with. I had the need to be able to choose between lazy and eager mode and to work without JavaScript with the
noscript
tag.Thanks, really enjoy reading your blog! :+1: