From 9aa02c881ef47aa1aacde8fb79f1792b87c61b3e Mon Sep 17 00:00:00 2001 From: Igor Klepacki Date: Fri, 8 Jul 2022 07:33:50 +0200 Subject: [PATCH 1/3] Add hook function --- src/index.ts | 91 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/src/index.ts b/src/index.ts index e5816d5..4701314 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import type { NextApiRequest, NextApiResponse } from 'next' +import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' import type { Except, RequireExactlyOne } from 'type-fest' import type { Page, Viewport } from 'puppeteer-core' import type { ReactElement } from 'react' @@ -19,32 +19,37 @@ type ImageType = 'png' | 'jpeg' | 'webp' type StrategyAwareParams< T extends StrategyOption = 'query', StrategyDetails extends string | object = string, -> = T extends 'body' + > = T extends 'body' ? StrategyDetails : Record> +type NextApiRequestWithOgImage = { + ogImage: string | Buffer +} + export type NextApiOgImageConfig< Strategy extends StrategyOption, StrategyDetails extends string | object = string, -> = { - template: RequireExactlyOne< - Partial<{ - html: (params: StrategyAwareParams) => string | Promise - react: (params: StrategyAwareParams) => ReactElement | Promise - }>, - 'html' | 'react' - > - strategy?: StrategyOption - cacheControl?: string - width?: number - height?: number - type?: ImageType - quality?: number - dev?: Partial<{ - inspectHtml: boolean - errorsInResponse: boolean - }> -} + > = { + template: RequireExactlyOne< + Partial<{ + html: (params: StrategyAwareParams) => string | Promise + react: (params: StrategyAwareParams) => ReactElement | Promise + }>, + 'html' | 'react' + > + strategy?: StrategyOption + cacheControl?: string + width?: number + height?: number + type?: ImageType + hook?: (request: NextApiRequestWithOgImage) => void | Promise, + quality?: number + dev?: Partial<{ + inspectHtml: boolean + errorsInResponse: boolean + }> + } type BrowserEnvironment = { envMode: EnvMode @@ -56,7 +61,7 @@ type BrowserEnvironment = { export function withOGImage< Strategy extends StrategyOption = 'query', StrategyDetails extends string | object = string, ->(options: NextApiOgImageConfig) { + >(options: NextApiOgImageConfig) { const defaultOptions: Except, 'template'> = { strategy: 'query', cacheControl: 'max-age 3600, must-revalidate', @@ -64,6 +69,7 @@ export function withOGImage< height: 630, type: 'png', quality: 90, + hook: null, dev: { inspectHtml: true, errorsInResponse: true, @@ -78,6 +84,7 @@ export function withOGImage< strategy, type, width, + hook, height, quality, dev: { inspectHtml, errorsInResponse }, @@ -99,7 +106,7 @@ export function withOGImage< createImageFactory({ inspectHtml, type, quality }), ) - return async function (request: NextApiRequest, response: NextApiResponse) { + return async function(request: NextApiRequest, response: NextApiResponse) { checkStrategy(strategy, !isProductionLikeMode(envMode) ? errorsInResponse : false, request, response) const params = stringifyObjectProps(strategy === 'query' ? request.query : request.body) @@ -109,16 +116,26 @@ export function withOGImage< htmlTemplate && !reactTemplate ? await htmlTemplate({ ...params } as StrategyAwareParams) : renderToStaticMarkup( - await reactTemplate({ ...params } as StrategyAwareParams), - ) + await reactTemplate({ ...params } as StrategyAwareParams), + ) + + const image = await browserEnvironment.createImage(emojify(html)); + + if (!!hook) { + const extendedRequest: NextApiRequestWithOgImage = { + ...request, + ogImage: image + } + + await hook(extendedRequest) + } response.setHeader( 'Content-Type', !isProductionLikeMode(envMode) && inspectHtml ? 'text/html' : type ? `image/${type}` : 'image/png', ) response.setHeader('Cache-Control', cacheControl) - - response.write(await browserEnvironment.createImage(emojify(html))) + response.write(image); response.end() } } @@ -195,7 +212,7 @@ function emojify(html: string) { } function pipe(...functions: Array): () => Promise { - return async function () { + return async function() { return await functions.reduce( async (acc, fn) => await fn(await acc), Promise.resolve({ envMode: process.env.NODE_ENV as EnvMode } as BrowserEnvironment), @@ -208,14 +225,14 @@ function getChromiumExecutable(browserEnvironment: BrowserEnvironment) { process.platform === 'win32' ? 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe' : process.platform === 'linux' - ? '/usr/bin/google-chrome' - : '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' + ? '/usr/bin/google-chrome' + : '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' return { ...browserEnvironment, executable } } function prepareWebPageFactory(viewPort: Viewport) { - return async function (browserEnvironment: BrowserEnvironment) { + return async function(browserEnvironment: BrowserEnvironment) { const { page, envMode, executable } = browserEnvironment if (page) { @@ -225,10 +242,10 @@ function prepareWebPageFactory(viewPort: Viewport) { const chromiumOptions = !isProductionLikeMode(envMode) ? { args: [], executablePath: executable, headless: true } : { - args: chrome.args, - executablePath: await chrome.executablePath, - headless: chrome.headless, - } + args: chrome.args, + executablePath: await chrome.executablePath, + headless: chrome.headless, + } const browser = await core.launch(chromiumOptions) const newPage = await browser.newPage() @@ -247,12 +264,12 @@ function createImageFactory({ type: ImageType quality: number }) { - return function (browserEnvironment: BrowserEnvironment) { + return function(browserEnvironment: BrowserEnvironment) { const { page, envMode } = browserEnvironment return { ...browserEnvironment, - createImage: async function (html: string) { + createImage: async function(html: string) { await page.setContent(html) const file = !isProductionLikeMode(envMode) && inspectHtml From e3fe1879d8bb18b5091939d163ee33cbb9bec7ca Mon Sep 17 00:00:00 2001 From: Igor Klepacki Date: Fri, 8 Jul 2022 08:48:33 +0200 Subject: [PATCH 2/3] Update README.md --- README.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ae63e73..99d3beb 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,19 @@ _you can treat this project as simpler and configurable version of mentioned ear ## Installing -In your [Next.js][next-homepage] project, execute: +In your [Next.js][next-homepage] project, execute: ```sh npm i next-api-og-image chrome-aws-lambda # or yarn add next-api-og-image chrome-aws-lambda ``` + ### Short note about the peer dependencies > ℹī¸ If your serverless function does not fit in the allowed size frames on [Vercel][vercel] **(50MB)**, you may want to install older versions of `chrome-aws-lambda` -In order to do so, replace `chrome-aws-lambda` _(while adding the dependencies)_ with `chrome-aws-lambda@6.0.0` **(47.6 MB)** +In order to do so, replace `chrome-aws-lambda` _(while adding the dependencies)_ with `chrome-aws-lambda@6.0.0` **(47.6 MB)** Please, refer to https://github.com/neg4n/next-api-og-image/issues/23#issuecomment-1090319079 for more info 🙏 @@ -144,6 +145,30 @@ When strategy is set to `query` and you're sending POST HTTP request with JSON b 2. Set appropiate response message to the client You can disable this behaviour by setting `dev: { errorsInResponse: false }` in the configuration +### Hooking the post-generate process + +In some scenarios you may want to do something _(in other words - execute some logic)_ **after generation of the image**. +This can be easily done by providing function to `hook` configuration property. The only parameter is `NextApiRequest` object with `image` attached to it. + +example (JavaScript): + +```js +import { withOGImage } from 'next-api-og-image' + +export default withOGImage({ + template: { + react: ({ myQueryParam }) =>
đŸ”Ĩ {myQueryParam}
, + }, + dev: { + inspectHtml: false, + }, + hook: (innerRequest) => { + console.log(innerRequest.image) + // will print the generated image on the server as Buffer + }, +}) +``` + ### Splitting files Keeping all the templates inline within [Next.js API route][next-api-routes] should not be problematic, but if you prefer keeping things in separate files you can follow the common pattern of creating files like `my-template.html.js` or `my-template.js` when you define template as react _(naming convention is fully up to you)_ with code e.g. @@ -187,10 +212,13 @@ const nextApiOgImageConfig = { quality: 90, // Width of the image in pixels width: 1200, - // Height of the image in pixels + // Height of the image in pixels height: 630, // 'Cache-Control' HTTP header cacheControl: 'max-age 3600, must-revalidate', + // Hook function that allows to intercept inner NextApiRequest with `ogImage` prop attached. + // useful for e.g. saving image in the database after the generation. + hook: null, // NOTE: Options within 'dev' object works only when process.env.NODE_ENV === 'development' dev: { // Whether to replace binary data (image/screenshot) with HTML From 968565b09753762b50ec66d8ec1984232a241f85 Mon Sep 17 00:00:00 2001 From: Igor Klepacki Date: Fri, 8 Jul 2022 08:49:09 +0200 Subject: [PATCH 3/3] Rename hook property ogImage to image for better semantics --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4701314..e7d79e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,7 @@ type StrategyAwareParams< : Record> type NextApiRequestWithOgImage = { - ogImage: string | Buffer + image: string | Buffer } export type NextApiOgImageConfig< @@ -43,8 +43,8 @@ export type NextApiOgImageConfig< width?: number height?: number type?: ImageType - hook?: (request: NextApiRequestWithOgImage) => void | Promise, quality?: number + hook?: (request: NextApiRequestWithOgImage) => void | Promise, dev?: Partial<{ inspectHtml: boolean errorsInResponse: boolean @@ -124,7 +124,7 @@ export function withOGImage< if (!!hook) { const extendedRequest: NextApiRequestWithOgImage = { ...request, - ogImage: image + image } await hook(extendedRequest)