When working with nextjs you get all this crazy stuff like SSG, ISG and SSR. While leveraring the advantages of SSG I thought: How cool would it be, if the link preview (og:image or twitter:image) would be inidividual per page? I played around and came up with a tiny project: covid.krey.io. For this project, I statically build one page per day for every german city. Every page has a custom og:image with the daylie covid-19 inzidenz number. See here how I did this.
bild einfügen
To dynamically generate the image I thought about rendering a react component and creating a image out of it. The solution I found was to acutally render the component by a headless browser and take a screenshot of it. A approach I found at repng. What they do is pretty cool:
Cool! Let's apply this idea.
The first thing we need to do is to acutally render our component to static markup. We can do this with: renderToStaticMarkup
from react-dom/server
. What this does is the following:
const ShowMyEmoji = ({ emoji }) => <div>{emoji}</div>renderToStaticMarkup(<ShowMyEmoji emoji="🕵️♂️" />)// <div>🕵️♂️</div>
Since we want a browser to show this component we need think about two things:
The width and height should be set to the component and to the brower window of our headless browser:
// the componentconst ShowMyEmoji = ({ emoji, width, height }) => (<divstyle={{width: width,height: height,backgroundColor: "gray",display: "flex",justifyContent: "center",alignItems: "center",}}>{emoji}</div>)
// imagetocomponentawait page.setViewportSize(size)
With this, the component will take up the whole viewport and we can easilly screenshot it.
To prevent the browser applying default styles we need to wrap our component within a body:
<!DOCTYPE html><head><meta charset="utf-8" /><style>* {box-sizing: border-box;}body {margin: 0;font-family: system-ui, sans-serif;}</style></head><body style="display:inline-block">${html}</body>
Of course you can add here what ever you want. This is the stuff the the awesome maintaines from repng are using.
With this we are ready to put everthing together like so:
import { renderToStaticMarkup } from "react-dom/server"import { chromium } from "playwright"const imageFromComponent = async (Component: React.ReactElement,size: { width: number; height: number },filename: string): Promise<Buffer> => {// Render the component to htmlconst body = renderToStaticMarkup(Component)// Open a browser window and set the wrap th component with a unstyled bodyconst browser = await chromium.launch()const page = await browser.newPage()await page.setContent(`<!DOCTYPE html><head><meta charset="utf-8"><style>*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif}</style></head><body style="display:inline-block">${body}</body>`)// Set the viewport size to the desired image sizeawait page.setViewportSize(size)const result = await page.screenshot({path: filename,clip: {x: 0,y: 0,...size,},omitBackground: true,})await browser.close()return result}export { imageFromComponent }
There are some pittfalls you should think about: