Skip to content

Commit

Permalink
Merge pull request #38 from neg4n/hook-functin
Browse files Browse the repository at this point in the history
Add hook function
  • Loading branch information
neg4n authored Jul 8, 2022
2 parents 7bdcfa5 + 968565b commit 82ff025
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 40 deletions.
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 🙏

Expand Down Expand Up @@ -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 }) => <div>🔥 {myQueryParam}</div>,
},
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.
Expand Down Expand Up @@ -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
Expand Down
91 changes: 54 additions & 37 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<StrategyDetails extends string ? StrategyDetails : string, NonNullable<string>>

type NextApiRequestWithOgImage = {
image: string | Buffer
}

export type NextApiOgImageConfig<
Strategy extends StrategyOption,
StrategyDetails extends string | object = string,
> = {
template: RequireExactlyOne<
Partial<{
html: (params: StrategyAwareParams<Strategy, StrategyDetails>) => string | Promise<string>
react: (params: StrategyAwareParams<Strategy, StrategyDetails>) => ReactElement | Promise<ReactElement>
}>,
'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<Strategy, StrategyDetails>) => string | Promise<string>
react: (params: StrategyAwareParams<Strategy, StrategyDetails>) => ReactElement | Promise<ReactElement>
}>,
'html' | 'react'
>
strategy?: StrategyOption
cacheControl?: string
width?: number
height?: number
type?: ImageType
quality?: number
hook?: (request: NextApiRequestWithOgImage) => void | Promise<void>,
dev?: Partial<{
inspectHtml: boolean
errorsInResponse: boolean
}>
}

type BrowserEnvironment = {
envMode: EnvMode
Expand All @@ -56,14 +61,15 @@ type BrowserEnvironment = {
export function withOGImage<
Strategy extends StrategyOption = 'query',
StrategyDetails extends string | object = string,
>(options: NextApiOgImageConfig<Strategy, StrategyDetails>) {
>(options: NextApiOgImageConfig<Strategy, StrategyDetails>) {
const defaultOptions: Except<NextApiOgImageConfig<Strategy, StrategyDetails>, 'template'> = {
strategy: 'query',
cacheControl: 'max-age 3600, must-revalidate',
width: 1200,
height: 630,
type: 'png',
quality: 90,
hook: null,
dev: {
inspectHtml: true,
errorsInResponse: true,
Expand All @@ -78,6 +84,7 @@ export function withOGImage<
strategy,
type,
width,
hook,
height,
quality,
dev: { inspectHtml, errorsInResponse },
Expand All @@ -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)
Expand All @@ -109,16 +116,26 @@ export function withOGImage<
htmlTemplate && !reactTemplate
? await htmlTemplate({ ...params } as StrategyAwareParams<Strategy, StrategyDetails>)
: renderToStaticMarkup(
await reactTemplate({ ...params } as StrategyAwareParams<Strategy, StrategyDetails>),
)
await reactTemplate({ ...params } as StrategyAwareParams<Strategy, StrategyDetails>),
)

const image = await browserEnvironment.createImage(emojify(html));

if (!!hook) {
const extendedRequest: NextApiRequestWithOgImage = {
...request,
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()
}
}
Expand Down Expand Up @@ -195,7 +212,7 @@ function emojify(html: string) {
}

function pipe(...functions: Array<Function>): () => Promise<BrowserEnvironment> {
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),
Expand All @@ -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) {
Expand All @@ -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()
Expand All @@ -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
Expand Down

0 comments on commit 82ff025

Please sign in to comment.