Next.js Discord

Discord Forum

Generating opengraph-image statically at build time

Unanswered
Pembroke Welsh Corgi posted this in #help-forum
Open in Discord
Pembroke Welsh CorgiOP
Hi, I have built my static blog (https://sadkebab.dev) and I wanted to generate opengraph and twitter images statically at build time, but the opengraph-image.tsx is always compiled down as a dynamic function and I have no clue on what to do to make it static. Any clues?

11 Replies

American Curl
I wrote a small script that fetches the urls i want to generate the image for and then download it.
E.g. with cheerio:
const downloadOgImage = async (url, outFile) => {
  const html = await fetch(url).then((res) => res.text());
  const $ = cheerio.load(html);
  const ogImage = $('meta[property="og:image"]').attr("content");

  console.log('loading image for', url);
  const res = await fetch(ogImage);
  const stream = fs.createWriteStream(outFile);
  await finished(Readable.fromWeb(res.body).pipe(stream));
};

Then during deployment i just delete all opengraph-image.tsx files (find src -name 'opengraph-image.tsx' -type f -exec rm {}) and configure the metadata in my routes to point to the downloaded images (https://nextjs.org/docs/app/api-reference/functions/generate-metadata#opengraph).

This means the process is basically as follows:
- start dev server
- run script on og image urls
- commit images and trigger a deployment
- during deployment remove the tsx files which won't generate functions
Spectacled bear
Oh huh I've been working on the same thing.

My solution was to generate svg image files and save them in /public/og/ are you using @vercel/og library to generate the images?

If so you can just swap it out for satori (what vercel/og uses internally). And write the files into public/ within a generateMetadata function (so you can do something sensible if it fails).

So in your pages.tsx file with generateStaticParams make a generateMetadata function.

And do something like:

   
import satori from "satori";
import { readFileSync, writeFileSync } from "fs";

...

export async function generateMetadata({ params }) { 

  try { 

  ... 

    const fontFile = readFileSync(rubikBoldFP);
    const imageFile = readFileSync(baseImgFP).buffer;

    const outPath = path.join(
      process.cwd(),
      "public",
      "og",
      "og-" + params.slug + ".svg",
    );`

    const svg = await satori ( <div ... > </div>, { .. satori or   vercel/og options });
 
    writeFileSync(outpath, svg); 
  } catch ( e ) { 
  ...
  }
}
Spectacled bear
Beats me, I guess because verce/og only works on edge runtime.
Pembroke Welsh CorgiOP
are you sure about it?
my build command outputs a lambda function for the opengraph-image.tsx file

also I don't see how it can run only on edge since the edge runtime is basically a subset of the node runtime, but maybe I am missing something
@Spectacled bear Oh huh I've been working on the same thing. My solution was to generate svg image files and save them in /public/og/ are you using `@vercel/og` library to generate the images? If so you can just swap it out for `satori` (what vercel/og uses internally). And write the files into public/ within a generateMetadata function (so you can do something sensible if it fails). So in your `pages.tsx` file with `generateStaticParams` make a `generateMetadata` function. And do something like: import satori from "satori"; import { readFileSync, writeFileSync } from "fs"; ... export async function generateMetadata({ params }) { try { ... const fontFile = readFileSync(rubikBoldFP); const imageFile = readFileSync(baseImgFP).buffer; const outPath = path.join( process.cwd(), "public", "og", "og-" + params.slug + ".svg", );` const svg = await satori ( <div ... > </div>, { .. satori or vercel/og options }); writeFileSync(outpath, svg); } catch ( e ) { ... } }
Pembroke Welsh CorgiOP
I just found and tested a cleaner solution for my use case, I added a /blog/[slug]/og.png/route.ts handler that returns an ImageResponse from the next/og package on the GET function and used generateStaticParams there

My code for that route.ts:
import { directoriesInFolder } from "@/lib/fs";
import { buildImage } from "./image";
import { ARTICLES_FOLDER } from "../../articles";
import { NextRequest } from "next/server";
import { ImageResponse } from "next/og";
import { ReactElement } from "react";

export async function GET(
  _: NextRequest,
  { params }: { params: { slug: string } },
) {
  const ret = await buildImage(params.slug);
  return new ImageResponse(ret as ReactElement, {
    width: 1200,
    height: 630,
  });
}

export async function generateStaticParams() {
  const articleSlugs = await directoriesInFolder(ARTICLES_FOLDER);
  return articleSlugs
    .filter((slug) => !slug.startsWith("_"))
    .map((slug) => ({ slug }));
}


the buildImage function comes from a image.tsx file where I load the React Component used for the generation of the image dynamically

export async function buildImage(slug: string) {
  try {
    const Component = await import(`@/articles/${slug}/thumbnail.tsx`).then(
      (m) => m.default,
    );

    return <Component />;
  } catch (e) {
    console.error(e);
    return null;
  }
}


then I just modified the generateMedata function in the blog/[slug]/page.tsx file to set the the openGraph image property with the new url

return {
  ..., 
  openGraph: {
      images: `/blog/${params.slug}/og.png`,
  }
}
Spectacled bear
oh nice! what type of route does that show up as when you build it?
Pembroke Welsh CorgiOP
Pembroke Welsh CorgiOP
so I tried deploying it on vercel and something weird happened and basically the blog/[slug] pages do not show up despite I can see them on my machine if I run the start command after the build... but vercel is having degraded build perfomance right now so I guess I will rollback and try again tomorrow to check if it's the platform or an issue with the new route that I added
Pembroke Welsh CorgiOP
my bad, I introduced a bug on the generateStaticParams, I edited the snippet with the correct solution