From b5fbffa4da5e513ef44832cbba9748cb0292bdc6 Mon Sep 17 00:00:00 2001 From: Igor Klepacki Date: Thu, 14 Oct 2021 05:13:31 +0200 Subject: [PATCH 1/4] Create ability to provide React template (#6) --- package-lock.json | 78 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ src/index.ts | 31 +++++++++++++++---- 3 files changed, 105 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86d304a..b1519ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ }, "devDependencies": { "@types/node": "^16.10.2", + "@types/react": "^17.0.29", + "@types/react-dom": "^17.0.9", "@types/twemoji": "^12.1.2", "microbundle": "^0.13.3", "prettier": "^2.4.1", @@ -2129,6 +2131,32 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "node_modules/@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", + "dev": true + }, + "node_modules/@types/react": { + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.29.tgz", + "integrity": "sha512-HSenIfBEBZ70BLrrVhtEtHpqaP79waauPtA8XKlczTxL3hXrW/ElGNLTpD1TmqkykgGlOAK55+D3SmUHEirpFw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz", + "integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -2138,6 +2166,12 @@ "@types/node": "*" } }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, "node_modules/@types/twemoji": { "version": "12.1.2", "resolved": "https://registry.npmjs.org/@types/twemoji/-/twemoji-12.1.2.tgz", @@ -3245,6 +3279,12 @@ "node": ">=8.0.0" } }, + "node_modules/csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==", + "dev": true + }, "node_modules/data-uri-to-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", @@ -9868,6 +9908,32 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", + "dev": true + }, + "@types/react": { + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.29.tgz", + "integrity": "sha512-HSenIfBEBZ70BLrrVhtEtHpqaP79waauPtA8XKlczTxL3hXrW/ElGNLTpD1TmqkykgGlOAK55+D3SmUHEirpFw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz", + "integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -9877,6 +9943,12 @@ "@types/node": "*" } }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, "@types/twemoji": { "version": "12.1.2", "resolved": "https://registry.npmjs.org/@types/twemoji/-/twemoji-12.1.2.tgz", @@ -10756,6 +10828,12 @@ "css-tree": "^1.1.2" } }, + "csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==", + "dev": true + }, "data-uri-to-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", diff --git a/package.json b/package.json index fd39796..e4509af 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,8 @@ }, "devDependencies": { "@types/node": "^16.10.2", + "@types/react": "^17.0.29", + "@types/react-dom": "^17.0.9", "@types/twemoji": "^12.1.2", "microbundle": "^0.13.3", "prettier": "^2.4.1", diff --git a/src/index.ts b/src/index.ts index a077a2f..ab9e1d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,25 @@ import type { NextApiRequest, NextApiResponse } from 'next' import type { Page } from 'puppeteer-core' +import type { ReactElement } from 'react' +import { renderToStaticMarkup } from 'react-dom/server' import twemoji from 'twemoji' import core from 'puppeteer-core' import chrome from 'chrome-aws-lambda' export type NextApiOgImageQuery = Record> -export type NextApiOgImageConfig = { +type NextApiOgImageHtmlTemplate = { html: (...queryParams: Array>) => string | Promise + react?: never +} + +type NextApiOgImageReactTemplate = { + react: (...queryParams: Array>) => ReactElement | Promise + html?: never +} + +export type NextApiOgImageConfig = { + template: NextApiOgImageHtmlTemplate | NextApiOgImageReactTemplate contentType?: string cacheControl?: string dev?: Partial<{ @@ -23,7 +35,7 @@ type BrowserEnvironment = { } export function withOGImage(options: NextApiOgImageConfig) { - const defaultOptions: Omit, 'html'> = { + const defaultOptions: Omit, 'template'> = { contentType: 'image/png', cacheControl: 'max-age 3600, must-revalidate', dev: { @@ -34,14 +46,18 @@ export function withOGImage(options: NextApiOgImageCon options = { ...defaultOptions, ...options } const { - html: htmlTemplate, + template: { html: htmlTemplate, react: reactTemplate }, cacheControl, contentType, dev: { inspectHtml }, } = options - if (!htmlTemplate) { - throw new Error('Missing html template') + if (htmlTemplate && reactTemplate) { + throw new Error('Ambigious template provided. You must provide either `html` or `react` template.') + } + + if (!htmlTemplate && !reactTemplate) { + throw new Error('No template was provided.') } const createBrowserEnvironment = pipe( @@ -55,7 +71,10 @@ export function withOGImage(options: NextApiOgImageCon const { query } = request const browserEnvironment = await createBrowserEnvironment() - const html = await htmlTemplate({ ...query } as NextApiOgImageQuery) + const html = + htmlTemplate && !reactTemplate + ? await htmlTemplate({ ...query } as NextApiOgImageQuery) + : renderToStaticMarkup(await reactTemplate({ ...query } as NextApiOgImageQuery)) response.setHeader( 'Content-Type', From 11cf7d0d24259e97a4a5e5d8de8c0a987ed5826a Mon Sep 17 00:00:00 2001 From: Igor Klepacki Date: Thu, 14 Oct 2021 05:14:20 +0200 Subject: [PATCH 2/4] Document usage of React template (#6) --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d5cfc93..5485151 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ _you can treat this project as simpler and configurable version of mentioned ear - [x] 🐄 Super easy usage - [x] 🌐 Suitable for [serverless][vercel-serverless] environment +- [x] :bowtie: Elegant way for defining templates both in [React][react] and [HTML][html] - [x] 🥷 TypeScript compatible ## Installing @@ -23,35 +24,63 @@ yarn add next-api-og-image ## Basic usage and explaination +##### HTML template + +```js +import { withOGImage } from 'next-api-og-image' + +export default withOGImage({ template: { html: ({ myQueryParam }) => `

${myQueryParam}

` } }) +``` + +##### React template + ```js import { withOGImage } from 'next-api-og-image' -export default withOGImage({ html: ({ myQueryParam }) => `

${myQueryParam}

` }) +export default withOGImage({ template: { react: ({ myQueryParam }) =>

{myQueryParam}

} }) ``` ## Creating template -You've may noticed the `html` property in configuration. Its responsibility is to provide HTML document to image creator _(browser screenshot)_, filled with your values. +You've may noticed the `html` and `react` properties in configuration. Their responsibility is to provide HTML document to image creator _(browser screenshot)_, filled with your values. + +> **⚠️ NOTE** +> Template **cannot be ambigious**. You must either +> define `react` or `html`. Never both at once ### Specification -The `html` property is a function _(**it can be asynchronous**)_ which first (and only) parameter is nothing else but [HTTP request's query params][query-params] converted to object notation. +The `html` and `react` properties are template providers functions. Each function's first (and only) parameter is nothing else but [HTTP request's query params][query-params] converted to object notation. This allows you to create fully customized HTML templates by simply accessing these parameters. The preferred way to do that is [object destructuring][object-destructuring]. +> **⚠️ NOTE** +> `html` and `react` template provider functions +> **can be defined as asynchronous** + #### Example +##### HTML template + ```js import { withOGImage } from 'next-api-og-image' -export default withOGImage({ html: ({ myQueryParam }) => `

${myQueryParam}

` }) +export default withOGImage({ template: { html: ({ myQueryParam }) => `

${myQueryParam}

` } }) +``` + +##### React template + +```js +import { withOGImage } from 'next-api-og-image' + +export default withOGImage({ template: { react: ({ myQueryParam }) =>

{myQueryParam}

} }) ``` _if you send GET HTTP request to [api route][next-api-routes] with code presented above e.g. `localhost:3000/api/foo?myQueryParam=hello` - it will render heading with content equal to 'hello'_ ### 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` with code e.g. +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. ```js export default function myTemplate({ myQueryParam }) { @@ -69,8 +98,8 @@ export default function myTemplate({ myQueryParam }: NextApiOgImageQuery${myQueryParam}` } ``` -then importing it and embedding in the `withOGImage`. +then importing it and embedding in the `withOGImage`. ### Loading custom local fonts @@ -78,9 +107,9 @@ In order to load custom fonts from the project source, you need to create source ## Configuration -Apart from `html` configuration property _(which is required)_, you can specify additional info about how `next-api-og-image` should behave. +Apart from `html` and `react` configuration property (in `template`) _(whose are required)_, you can specify additional info about how `next-api-og-image` should behave. -Example configuration with **default values** _(apart from required html prop)_: +Example configuration with **default values** _(apart from template.html or template.react prop)_: ```js const nextApiOgImageConfig = { @@ -95,7 +124,7 @@ const nextApiOgImageConfig = { inspectHtml: true, }, } -```` +``` ## Examples @@ -116,6 +145,8 @@ This project is licensed under the MIT license. All contributions are welcome. [next-homepage]: https://nextjs.org/ +[react]: https://reactjs.org +[html]: https://en.wikipedia.org/wiki/HTML [object-destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#object_destructuring [query-params]: https://en.wikipedia.org/wiki/Query_string [vercel-serverless]: https://vercel.com/docs/concepts/functions/introduction From b744cc7228b8f739d9aa59337c3eae100f494053 Mon Sep 17 00:00:00 2001 From: Igor Klepacki Date: Thu, 14 Oct 2021 05:47:15 +0200 Subject: [PATCH 3/4] Add new examples using react templates and fix existing (#6) --- .../pages/api/advanced-typescript-react.tsx | 56 +++++++++++++++++++ example/pages/api/basic-react.js | 7 +++ example/pages/api/basic-tailwind.js | 22 ++++---- example/pages/api/basic-typescript.ts | 18 +++--- example/pages/api/basic.js | 4 +- 5 files changed, 88 insertions(+), 19 deletions(-) create mode 100644 example/pages/api/advanced-typescript-react.tsx create mode 100644 example/pages/api/basic-react.js diff --git a/example/pages/api/advanced-typescript-react.tsx b/example/pages/api/advanced-typescript-react.tsx new file mode 100644 index 0000000..3233262 --- /dev/null +++ b/example/pages/api/advanced-typescript-react.tsx @@ -0,0 +1,56 @@ +import { withOGImage } from 'next-api-og-image' +import { interRegular } from '../../fonts/inter' + +const style = ` + @font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: normal; + src: url(data:font/woff2;charset=utf-8;base64,${interRegular}) format('woff2'); + } + + body { + font-family: 'Inter', sans-serif; + } + + .container { + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } +` + +export default withOGImage<'testQueryParam'>({ + template: { + react: async ({ testQueryParam }) => { + const value = await someLongRunningValueGetter() + return ( + + +