import { RenderPlugin } from '@11ty/eleventy'; /* eslint-disable import/no-unresolved */ // https://github.com/import-js/eslint-plugin-import/issues/2132 import { html as htmlToSatori } from 'satori-html'; /* eslint-enable import/no-unresolved */ import satori from 'satori'; import { Resvg } from '@resvg/resvg-js'; import sharp from 'sharp'; import crypto from 'node:crypto'; import { TemplatePath } from '@11ty/eleventy-utils'; import path from 'node:path'; import url from 'node:url'; import { sortObject } from './utils/index.js'; /** @implements {import('eleventy-plugin-og-image').OgImage} */ export class OgImage { /** @type {string} */ inputPath; /** @type {Record} */ data; /** @type {import('eleventy-plugin-og-image').EleventyPluginOgImageMergedOptions} */ options; /** @type {import('@11ty/eleventy/src/TemplateConfig').default} */ templateConfig; /** * @private * @type {{ html?: string; svg?: string; pngBuffer?: Buffer }} */ results = { html: undefined, svg: undefined, pngBuffer: undefined, }; /** * @param {string} inputPath * @param {Record} data * @param {import('eleventy-plugin-og-image').EleventyPluginOgImageMergedOptions} options * @param {import('@11ty/eleventy/src/TemplateConfig').default} templateConfig */ constructor({ inputPath, data, options, templateConfig }) { this.inputPath = inputPath; this.data = data; this.options = options; this.templateConfig = templateConfig; } /** @returns {Promise} */ async html() { if (!this.results.html) { this.results.html = await ( await RenderPlugin.File(this.inputPath, { templateConfig: this.templateConfig }) )(this.data); } return this.results.html; } /** @returns {Promise} */ async svg() { if (!this.results.svg) { this.results.svg = await satori(htmlToSatori(await this.html()), this.options.satoriOptions); } return this.results.svg; } /** @returns {Promise} */ async pngBuffer() { if (!this.results.pngBuffer) { this.results.pngBuffer = await new Resvg(await this.svg(), { font: { loadSystemFonts: false } }).render().asPng(); } return this.results.pngBuffer; } /** * Returns the configured output format * * @returns {Promise} */ async render() { return sharp(await this.pngBuffer()).toFormat(this.options.outputFileExtension, this.options.sharpOptions); } /** @returns {Promise} */ async hash() { const hash = crypto.createHash('sha256'); hash.update(await this.html()); hash.update(JSON.stringify(sortObject(this.options.satoriOptions || {}))); hash.update(JSON.stringify(sortObject(this.options.sharpOptions || {}))); return hash.digest('hex').substring(0, this.options.hashLength); } /** @returns {Promise} */ async outputFileSlug() { return this.options.outputFileSlug(this); } /** @returns {Promise} */ async outputFileName() { return `${await this.outputFileSlug()}.${this.options.outputFileExtension}`; } /** @returns {Promise} */ async outputFilePath() { return TemplatePath.standardizeFilePath(path.join(this.options.outputDir, await this.outputFileName())); } /** @returns {Promise} */ async outputUrl() { const fileUrl = new url.URL('file://'); fileUrl.pathname = path.join(this.options.urlPath, await this.outputFileName()); return fileUrl.pathname; } /** @returns {Promise} */ async cacheFilePath() { return this.outputFilePath(); } /** @returns {Promise} */ async shortcodeOutput() { return this.options.shortcodeOutput(this); } /** @returns {string} */ previewFilePath() { return TemplatePath.standardizeFilePath( path.join( this.options.previewDir, `${this.data.page.url.replace(/\/$/, '') || 'index'}.${this.options.outputFileExtension}`, ), ); } /** @returns {Promise} */ async previewHtml() { return ` OG Image: ${this.data.page.url}
${await this.html()}
${await this.svg()} OG Image: ${this.data.page.url} `; } }