-
Notifications
You must be signed in to change notification settings - Fork 26
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
Optimizing Images with the 11ty Image Plugin #118
Comments
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 const Image = require("@11ty/eleventy-img");
const outdent = require("outdent");
const path = require("path");
const placeholder = 22;
module.exports = async ({
input,
width = [300, 600],
alt = "",
baseFormat = "jpeg",
optimalFormat = ["avif", "webp"],
lazy = false,
className = ["shadow-black-transparent"],
sizes = "100vw"
}) => {
const { dir, base } = path.parse(input);
const inputPath = path.join(".", dir, base);
const metadata = await Image(inputPath, {
widths: [placeholder, ...width],
formats: [...optimalFormat, baseFormat],
urlPath: dir,
outputDir: path.join("docs", dir)
});
const lowSrc = metadata[baseFormat][0];
const highSrc = metadata[baseFormat][metadata[baseFormat].length - 1];
if(lazy) {
return outdent`
<picture class="lazy-picture" data-lazy-state="unseen">
${Object.values(metadata).map(entry => {
return `<source type="${entry[0].sourceType}" srcset="${entry[0].srcset}" data-srcset="${entry.filter(imageObject => imageObject.width !== 1).map(filtered => filtered.srcset).join(", ")}" sizes="${sizes}" class="lazy">`;
}).join("\n")}
<img
src="${lowSrc.url}"
data-src="${highSrc.url}"
width="${highSrc.width}"
height="${highSrc.height}"
alt="${alt}"
class="lazy ${className.join(" ")}"
loading="lazy">
</picture>
<noscript>
<picture>
${Object.values(metadata).map(entry => {
return `<source type="${entry[0].sourceType}" srcset="${entry.filter(imageObject => imageObject.width !== 1).map(filtered => filtered.srcset).join(", ")}" sizes="${sizes}">`;
}).join("\n")}
<img
src="${highSrc.url}"
width="${highSrc.width}"
height="${highSrc.height}"
alt="${alt}"
class="${className.join(" ")}">
</picture>
</noscript>`;
} else if(!lazy) {
return outdent`
<picture>
${Object.values(metadata).map(entry => {
return `<source type="${entry[0].sourceType}" srcset="${entry.filter(imageObject => imageObject.width !== 1).map(filtered => filtered.srcset).join(", ")}" sizes="${sizes}">`;
}).join("\n")}
<img
src="${highSrc.url}"
width="${highSrc.width}"
height="${highSrc.height}"
alt="${alt}"
class="${className.join(" ")}"
</picture>`;
}
} Thanks, really enjoy reading your blog! 👍 |
@solution-loisir Thanks, really glad to hear it! I've updated the post to mention |
Hi @AleksandrHovhannisyan do you have the final code for your post? Im trying to follow up on it but im getting 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:
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: 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:
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 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 To this 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: And this is how I'm using the shortcode inside the njk file: Any idea what could be happening? |
@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: 11ty/eleventy#2428 (comment). 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 |
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 |
@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. |
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. |
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. |
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 |
@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: aleksandrhovhannisyan.com/src/_includes/image.html Lines 1 to 24 in 7ed63df
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:
And the JS shortcode looks like this as following your guide
Along with the global filter
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 ( |
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 |
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 {% image src, 'alt', etc. %} (Doesn't have to be |
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 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. 👍 |
@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). aleksandrhovhannisyan.com/config/shortcodes/favicon.js Lines 9 to 31 in 15195f7
aleksandrhovhannisyan.com/src/_layouts/base.html Lines 46 to 47 in 15195f7
|
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/ |
No description provided.
The text was updated successfully, but these errors were encountered: