By Kevin Hou
4 minute read
As a photographer and designer, I've always believed images are a multiplier on a website's engagement. While you can often accomplish what you want to render using normal JS + React + CSS, there a few cases where images are simpler and make more sense.
For a site to be successful on socials, Twitter, Slack, iMessage, it's imperative that it have an engaging image preview. These platforms often prioritize the preview image above the site title, description, or URL. Twitter / X even goes so far as to not show the link at all. Instead, it only shows the preview image for you to click.
It's important that the image not only have a graphic relevant to the page, but also contain the title or some other description of the page so that the user knows what the site is doing.
As an aside, I do love the emphasis platforms are making on the preview image. While sometimes it can lead to click-bait, assuming the image is thoughtfully designed, it leads to a more beautiful web.
You can get really crafty with it by adding a URL param to allow a user to select from a list of predefined designs.
If your application has personalization for each user, a great way to help customers feel engaged is to give them someone personal that they can save or share. Images are a great way to create this experience, whether it be for a "Membership Card", "Badge", etc.
Here's an example that I built with Codeium:
There's two reasons for this:
If you use a shared, top-level React component instead of writing all your markup within the ImageResponse
, you'll be able to re-use this component. This helps on two fronts:
.png
or .jpg
route endpoint to generate dynamic images on the fly. When you render the image as a cover image, you can simply make a app/path/to/card.png/route.tsx
file and use that shared component.Here's an example of a component: PhotoAlbumCover.
You'll need to use inline styling since TailwindCSS is still in beta. It's worth noting that you'll need to explicitly specify display: flex
when using a <div>
. If not, you'll receive this very obscure error:
1⨯ Error: failed to pipe response 2 at pipeToNodeResponse (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/pipe-readable.js:126:15) 3 at async sendResponse (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/send-response.js:40:13) 4 at async doRender (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/base-server.js:1407:25) 5 at async cacheEntry.responseCache.get.routeKind (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/base-server.js:1587:40) 6 at async DevServer.renderToResponseWithComponentsImpl (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/base-server.js:1507:28) 7 at async DevServer.renderPageComponent (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/base-server.js:1924:24) 8 at async DevServer.renderToResponseImpl (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/base-server.js:1962:32) 9 at async DevServer.pipeImpl (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/base-server.js:920:25) 10 at async NextNodeServer.handleCatchallRenderRequest (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/next-server.js:272:17) 11 at async DevServer.handleRequestImpl (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/base-server.js:816:17) 12 at async /path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/dev/next-dev-server.js:339:20 13 at async Span.traceAsyncFn (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/trace/trace.js:154:20) 14 at async DevServer.handleRequest (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/dev/next-dev-server.js:336:24) 15 at async invokeRender (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/lib/router-server.js:174:21) 16 at async handleRequest (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/lib/router-server.js:353:24) 17 at async requestHandlerImpl (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/lib/router-server.js:377:13) 18 at async Server.requestListener (/path/to/repo/khou22.github.io/node_modules/.pnpm/next@14.2.3_@babel+core@7.23.6_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/lib/start-server.js:141:13) 19
Your server will disconnect and you'll get no response or error message on the client. The solution is to make sure you always have display: flex
like so.
If you use percentages instead of hard coded values, you can use this component more flexibly. The default OG image size is 1200 x 630 which could be too large for your mobile display. If you're embedding this card as a preview as a React Compoennt (see tip on using shared, top-level React component), then you'll want to make sure the component is scalable.
You can also make the text dynamic by using rem
or doing some rough switch / case math based on the length of the string and size of the card.
And this is how you would use it as a NextJS image endpoint:
1export async function GET(_: NextRequest, context: RouteParams) {
2 return new ImageResponse(
3 <PhotoAlbumCover someProp="Hello, World!"/>,
4 {
5 width: 1200,
6 height: 630,
7 },
8 );
9}
10
Here's a live example: khou22.github.io/src/app/share/[album_id]/cover.png/route.tsx
Hope this helps!