How to generate dynamic OG image using new NextJs with App directory

titasmallick

TITAS MALLICK

Posted on March 29, 2023

How to generate dynamic OG image using new NextJs with App directory

AN API THAT YOU CAN USE NOW TO DO THE FETCHING AT ONCE
https://randomimagedesc.creativegunfilms.workers.dev/

//It will return something like this
{
  "id": 539,
  "imageURL": "https://picsum.photos/id/539/300",
  "description": "a black and white photo of some windows",
  "apiCreator": "Titas",
  "timestamp": 1680238902543
}
Enter fullscreen mode Exit fullscreen mode

OG or opengraph images are important tool for improving website SEO. Most of the time we use a static image generally placed inside the 'public' directory and link it to the index.html or jsx if we are using React or NextJs.

<head>
  <title>Hello world</title>
  <meta
    property="og:image"
    content="https://domainname.tld/fieName.jpg"
  />
</head>
Enter fullscreen mode Exit fullscreen mode

But sometime it is required to dynamically generate this og images. Use cases may include dynamic routes or dynamic product pages for an ecommerce site. If you are building your app using NextJs and planned to host that in Vercel, then vercel has a very good tool to cover you. 'Vercel Open Graph (OG) Image Generation', it does the same, it allows you to use an Api endpoint to fetch data if required and then dynamically create the og image at request time.
But the vercel documentation covers it's implementation using NextJs with older 'Page' or 'src/pages' folder structure. Where there is an dedicated 'Api' folder to handle api requests.
But it still doesn't covers how to implement the same feature using newly announce 'App' folder that replaces the older 'Pages' folder in version 13. In NextJs Beta Docs it is clearly mentioned that '/Api' is not supported inside the 'App' directory, though NextJs allows you to incrementally update to the newer structure and 'Pages' directory will be supported alongside the 'App' directory, it is good to know how to implement og image generation when using NextJs v13 or above using only 'App' directory.
Start with creating a new NextJs project.

npx create-next-app@latest
# or
yarn create next-app
# or
pnpm create next-app
Enter fullscreen mode Exit fullscreen mode

Select the following options

? Would you like to use `src/` directory with this project? »** No** / Yes
// Select "NO"
? Would you like to use experimental `app/` directory with this project? » No / **Yes**
// Select "YES"
Enter fullscreen mode Exit fullscreen mode

Now locally run your project using npm run dev and open the vscode using code ..
You will be represented with the 'App' directory instead of the 'Pages' or 'src/Pages' directory. And obviously there is no 'Api' directory inside the 'App' directory.
the 'layout.js' handles the layout obviously and 'page.js' is where you will write the codes for the index page of your app.
Please look into the NextJs Beta documentation to understand how the actual routing works in the new NextJs.
Now while 'Api' is gone, we now have 'route.js'. It can be present inside any route. But that route must not contain any 'Page.js'. Say for example create a folder, name it 'ogimage' inside the 'App', and place 'route.js' inside the folder. The 'route.js' acts as the route handler for that particular route. Please look into the route handler section in the NextJs Beta Docs. This route handler is now accessible through:

http://localhost:3000/ogimage
Enter fullscreen mode Exit fullscreen mode

The 'route.js' inside a directory, here 'App/ogimage/route.js' receives the request from the client and send responses.
Where 'page.js' returns the rendered html the 'route.js' is supposed to send responses in form of json or other formats just like the api used to do.
Write the following code to test your route handler:

export async function GET(request) {
    return new Response('Hello, World!');
}
Enter fullscreen mode Exit fullscreen mode

Refresh your browser, you will se 'Hello, World!' coming from your route handler.
Now install the vercel og image library to your project.

npm i @vercel/og
or
yarn add @vercel/og 
or
pnpm i @vercel/og 
Enter fullscreen mode Exit fullscreen mode

import the library in your 'route.js'.

export async function GET(request) {
   return  new ImageResponse(
      (
        <div
          style={{
            fontSize: 128,
            background: 'white',
            width: '100%',
            height: '100%',
            display: 'flex',
            textAlign: 'center',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          Hello world!
        </div>
      ),
      {
        width: 1200,
        height: 600,
      },
    );
}
Enter fullscreen mode Exit fullscreen mode

Here you are returning an ImageResponse instead of the Response, alternatively you can also extend the request and response web api using 'NextRequest' and 'NextRespone', to do that you can import them using import { NextResponse, NextRequest } from 'next/server';, though for this example it is not required. Now if you refresh your browser you will get an image generated by your 'route.js' at request time.
Well we are almost done. You can render whatever dynamic data in your image you want and customize your image using og playground, you can even generate 'SVG' on request as the og image.
For this example we will fetch a random number from random.org api, then we will use that number as an id and fetch an image from Lorem Picsum, with the same image url we will fetch the description for the image from the Alt Image Generator and generate an image on request with the image that we fetched and the description we have fetched and use it in a design to create the og image. Kind of like that.

Generated Image by this method

So first inside our async GET function we generate a random id:

//Inside 'App/ogimage/route.js'
let randomdid;
    const getRandom = await fetch(
      'https://www.random.org/integers/?num=1&min=0&max=1083&col=1&base=10&format=plain',
      { mode: 'no-cors', next: { revalidate: 10 } },
    );
    const numbget = await getRandom.json();
    randomdid = numbget || Math.floor(Math.random() * 1083);
Enter fullscreen mode Exit fullscreen mode

We are fetching a random integer between 0 - 1083 (as we found lorem picsum provides 1084 images), the 'revalidate: 10' is to tell the server to revalidate the fetch in every 10 seconds if another request is received.
Now we will fetch the image from the lorem picsum by using the random id as the photo id.

var = URL;
 const imagef = await fetch(`https://picsum.photos/id/${randomdid}/info`, {
      next: { revalidate: 10 },
    });
    const imageD = await imagef.json();
    URL = `https://picsum.photos/id/${imageD.id}/300`;
Enter fullscreen mode Exit fullscreen mode

We could have fetched the whole image and could have used the method 'await res.blob()' but we found it unnecessarily slows the process. So this step can be skipped.
Now, we will fetch the description of the image from Alt Text Generator by passing the image url as a parameter like the following.

var fetchdesc;
const res = await fetch(
      `https://alt-text-generator.vercel.app/api/generate?imageUrl=https://picsum.photos/id/${randomdid}/300`,
      { mode: 'no-cors', next: { revalidate: 10 } },
    );
    fetchDesc = await res.json();
Enter fullscreen mode Exit fullscreen mode

We will wrap the whole fetching thing inside a 'try catch' block and handle any error.
Now we have the url of the image that we want to show, as well as the description.
Alternatively you can use OUR API to do the complete fetching at once.
Just fetch https://randomimagedesc.creativegunfilms.workers.dev/
It will return a JSON response like:

{
  "id": 232,
  "imageURL": "https://picsum.photos/id/232/300",
  "description": "a street light in the snow at night",
  "apiCreator": "Amit Sen",
  "randomQ": "Be polite to all, but intimate with few.",
  "qAuthor": "Thomas Jefferson",
  "c": "Extend Alt Text",
  "u": "https://extend-alt-text.vercel.app/",
  "g": "https://github.com/creativegunfilms/ExtendAltText.git",
  "timestamp": 1680252198911
}
Enter fullscreen mode Exit fullscreen mode

By using this API, the fetch can be now done like that:

export async function GET(request) {
  //..
  let data;
  try {
    const fetchData = await fetch(
      'https://randomimagedesc.creativegunfilms.workers.dev/',
      { mode: 'no-cors', cache: 'no-store' },
    );
    data = await fetchData.json();
  } catch (error) {
    //Using Fallback to narrow error boundary
    //...
  }

  return new ImageResponse(
    //...
    // You can now use {data.imageURL} and {data.description} here.
  );
}
Enter fullscreen mode Exit fullscreen mode

Anyway, now We will construct the og image inside the returned ImageResponse like the following:

return new ImageResponse(
    (
      <div
        style={{
          height: '100%',
          width: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: '#fff',
          fontSize: 32,
          fontWeight: 600,
        }}
      >
        <div
          style={{
            left: 42,
            top: 42,
            position: 'absolute',
            display: 'flex',
            alignItems: 'center',
          }}
        >
          <span
            style={{
              width: 24,
              height: 24,
              background: 'black',
            }}
          />
          <span
            style={{
              marginLeft: 8,
              fontSize: 20,
            }}
          >
            Extend Alt Text
          </span>
        </div>
        <img height={300} width={300} src={URL} />
        <div
          style={{
            fontSize: '20px',
            marginTop: 40,
            background: 'black',
            color: 'white',
            padding: '25px',
            width: '300px',
          }}
        >
          {fetchDesc}
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 600,
    },
  );
Enter fullscreen mode Exit fullscreen mode

We are done. We have now a dynamically generated og image.
The whole 'route.js' will now look like the following:

import { ImageResponse } from '@vercel/og';

export const config = {
  runtime: 'edge',
};

export async function GET(request) {
  console.log(request);
  //Setting the image url
  var URL;
  //Fetch the Alt Text Generator
  let fetchDesc;
  try {
    // Generating random id between 0 - 1083 to get the image
    // let randomdid = Math.floor(Math.random() * 1083);
    let randomdid;
    const getRandom = await fetch(
      'https://www.random.org/integers/?num=1&min=0&max=1083&col=1&base=10&format=plain',
      { mode: 'no-cors', next: { revalidate: 10 } },
    );
    const numbget = await getRandom.json();
    randomdid = numbget || Math.floor(Math.random() * 1083);
    //Fetching the alt text linked to id
    const res = await fetch(
      `https://alt-text-generator.vercel.app/api/generate?imageUrl=https://picsum.photos/id/${randomdid}/300`,
      { mode: 'no-cors', next: { revalidate: 10 } },
    );
    fetchDesc = await res.json();
    //fetching the actual image based on id
    const imagef = await fetch(`https://picsum.photos/id/${randomdid}/info`, {
      next: { revalidate: 10 },
    });
    const imageD = await imagef.json();
    URL = `https://picsum.photos/id/${imageD.id}/300`;
  } catch (error) {
    //Using Fallback to narrow error boundary
    URL = '/fallbackog.jpg';
    fetchDesc = 'Extend Alt Text Can Generate Descriptions';
  }

  return new ImageResponse(
    (
      <div
        style={{
          height: '100%',
          width: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: '#fff',
          fontSize: 32,
          fontWeight: 600,
        }}
      >
        <div
          style={{
            left: 42,
            top: 42,
            position: 'absolute',
            display: 'flex',
            alignItems: 'center',
          }}
        >
          <span
            style={{
              width: 24,
              height: 24,
              background: 'black',
            }}
          />
          <span
            style={{
              marginLeft: 8,
              fontSize: 20,
            }}
          >
            Extend Alt Text
          </span>
        </div>
        <img height={300} width={300} src={URL} />
        <div
          style={{
            fontSize: '20px',
            marginTop: 40,
            background: 'black',
            color: 'white',
            padding: '25px',
            width: '300px',
          }}
        >
          {fetchDesc}
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 600,
    },
  );
}
Enter fullscreen mode Exit fullscreen mode

Now refresh your browser you will see newly generated og image each time. Simply made a git commit and let it built on the vercel.
use the link of the route and place it in the

section of your 'layout.js' and your website now has an dynamically generated og image.
<meta
    property="og:image"
    content="https://domainname.vercel.app/directoryName"
  />
Enter fullscreen mode Exit fullscreen mode

You can test it here.
As the route.js here is not handelling the incoming request you can send random request parameter to genarate og images in the hosted environment.

Refreshing http://localhost:3000/ogimage will generate new og image each time.

But refreshing https://extend-alt-text.vercel.app/ogimage will not generate new og image each time, as it is remaining as cache in the edge, refreshing is not an unique request.

Passing a request param like https://extend-alt-text.vercel.app/ogimage?=[anything_Random_here] will be treated as unique request and will generate new og image. Now this newly generated image can be cached for 10 second. Sending same parameter again will prompt the cached version of the og image.
The caching parameters are supposted to work like this -

   // Cached until manually invalidated
    fetch(`https://...`),
    // Refetched on every request
    fetch(`https://...`, { cache: 'no-store' }),
    // Cached with a lifetime of 10 seconds
    fetch(`https://...`, { next: { revalidate: 10 } }),
    // Cached
    fetch(`https://...`, { 'only-cache' }),
    // or,
    fetch(`https://...`, { 'force-cache' })
Enter fullscreen mode Exit fullscreen mode

_Though after multiple times fiddling with the cache parameters even by setting 'cache-control' headers we didn't experienc any changes in the cache behaviors. At this moment it is safe to assume, may be the Apis are not fully ready yet, thus they are getting overridden forcefully to their default pattern, hopefully in the stable release these might get fixed. _
Frequently changing og image can influence the SEO, but it is absolutely okay to create Og images dynamically or for dynamic routes.
Handelling request and request.param to get the passed value can be used to dynamically add text in the og image by using {request.param} while structuring SVG in the returned imageResponse.
You can clone our project from our github repo. Our project is called Extend Alt Text and it takes the vercel Alt Text Generator and extends its functionality. It let users upload multiple files at once and generate alt texts in bulk. We can describe how we did that in a separate article.
By design the 'route.js' here is behaving like an 'edge function' here, which is strategically placed behind the 'edge network cache' inside the vercel infrastructure. So the response cache is automatically stored to reduce network latency. Alternatively middlewares can be placed as the 'edge middleware' which seats infront of the network cache and intercepts network requests before it actually hits the server.
Theoretically, we could have used this image generation on the middleware to override this caching behavior alltogether, but the middewares are not designed to run edge functions in them and can induce network latency, it is recommended to use middlewares to geolocate, authenticate and redirect and things like A/B testing.
NextJs Beta Document says 'To Do' in the 'middleware' section, it is not clear if it will be supported with the new 'App' directory or not as of now, we have tested implementing middleware with 'App' directory in NextJs Beta V13.2.4, the wrong 'matcher' gives error in the server console but the 'redirect' or 'rewrite' doesn't work as of now. It may change in the future, please look into the docs before you use it.
We tried to use edge cache control headers to control the caching behavior.

  return new ImageResponse(
    (...
    ),
    {...
    },
  ), {status: 200, headers: {
    'Cache-Control': 's-maxage=3600',
  }};
}
Enter fullscreen mode Exit fullscreen mode

But with 'ImagResponse' setting up 'cache-control' headers didn't actually work. But it works fine with 'NextResponse'.
We will try to implement cache controlling in image generation in a future article.
Thanks for Now.

💖 💪 🙅 🚩
titasmallick
TITAS MALLICK

Posted on March 29, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related