Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEXT-1089] Custom fonts in ImageResponse not working in App Dir #48081

Closed
1 task done
surjithctly opened this issue Apr 7, 2023 · 31 comments · Fixed by #54274
Closed
1 task done

[NEXT-1089] Custom fonts in ImageResponse not working in App Dir #48081

surjithctly opened this issue Apr 7, 2023 · 31 comments · Fixed by #54274
Labels
area: app App directory (appDir: true) locked Metadata Related to Next.js' Metadata API. Pages Router Related to Pages Router.

Comments

@surjithctly
Copy link

surjithctly commented Apr 7, 2023

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: arm64
      Version: Darwin Kernel Version 22.1.0: Sun Oct  9 20:14:30 PDT 2022; root:xnu-8792.41.9~2/RELEASE_ARM64_T8103
    Binaries:
      Node: 18.12.0
      npm: 8.19.2
      Yarn: 1.22.18
      pnpm: 7.25.1
    Relevant packages:
      next: 13.3.1-canary.1
      eslint-config-next: N/A
      react: 18.2.0
      react-dom: 18.2.0

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true), Data fetching (gS(S)P, getInitialProps), Metadata (metadata, generateMetadata, next/head, head.js)

Link to the code that reproduces this issue

https://stackblitz.com/edit/github-f6jt4y?file=app%2Fopengraph-image.js

To Reproduce

  1. Run the Server
  2. Check the error in console
  3. Alternatively open the generated path in new URL to get the error.

Describe the Bug

While importing custom fonts, it throws the following error.

error - unhandledRejection: Error [TypeError]: Failed to parse URL from /_next/static/media/Inter-Regular.f356e84a.woff
    at new Request (node:internal/deps/undici/undici:9474:19)
    at fetch1 (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.3.1-canary.1_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react/cjs/react.shared-subset.development.js:193:39)
    at doOriginalFetch (/Users/.../opengraph/node_modules/.pnpm/next@13.3.1-canary.1_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/server/lib/patch-fetch.js:159:24)
    at /Users/.../opengraph/node_modules/.pnpm/next@13.3.1-canary.1_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/server/lib/patch-fetch.js:253:20 {
  digest: undefined
}

Expected Behavior

It should not throw error. Instead it should load properly.

Which browser are you using? (if relevant)

Any

How are you deploying your application? (if relevant)

No response

NEXT-1089

@surjithctly surjithctly added the bug Issue was opened via the bug report template. label Apr 7, 2023
@github-actions github-actions bot added area: app App directory (appDir: true) Pages Router Related to Pages Router. Metadata Related to Next.js' Metadata API. labels Apr 7, 2023
@steven-tey
Copy link
Contributor

I can repro this on latest canary (13.3.1-canary.11) as well, will let the team know!

@iamhectorsosa
Copy link

Also had this same issue

@khuezy
Copy link
Contributor

khuezy commented Apr 21, 2023

I'm having the same issue; as a workaround, you can do this:
Put your font in /public/fonts/here.tff

async function getFont(): Promise<Buffer> {
  const url = process.env.APP_URL
  return new Promise((resolve, reject) => {
    http.get(`${url}/fonts/Font.ttf`, (res) => {
      const chunks: any[] = []
      res.on('data', c => chunks.push(c))
      res.on('end', () =>
        resolve(Buffer.concat(chunks))
      )
    }).on('error', (err) => {
      reject(err)
    })
  })
}
fonts: [
{
  data: await getFont()
}
]

This is pretty hacky, it would be nice if we could use next/font w/ ImageResponse

@huozhi
Copy link
Member

huozhi commented May 1, 2023

Hi, you’re using the default nodejs runtime, you can use fs read to access the font data in nodejs runtime.

Check out the nodejs example list here
https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation/og-image-examples#load-files-in-node.js-runtime

Or you can just add export const runtime = “edge” to your opengraph-image

@huozhi huozhi closed this as completed May 1, 2023
@huozhi
Copy link
Member

huozhi commented May 1, 2023

Can you try to move the font load inside the route handler it resolves the problem for me @surjithctly

const font = fetch(new URL('Inter-Regular.woff', import.meta.url)).then((res) =>
  res.arrayBuffer()
);

@lukadev-0
Copy link

Hi, you’re using the default nodejs runtime, you can use fs read to access the font data in nodejs runtime.

Whenever I try this, the file ends up not existing when deploying to Vercel. And when using the edge runtime the bundle size ends up being larger than 1 MB.

@surjithctly
Copy link
Author

@huozhi For me, moving into the handler doesn't fix it. But export const runtime = "edge" did the trick.

@huozhi
Copy link
Member

huozhi commented May 3, 2023

@lukadev-0 can you check the nodejs usage of og section if you want to use nodejs runtime, it's just using fs operations instead of fetch to load font or extra assets. The bundle size is bit larger because it contains more necessary dependencies for edge runtime, but the speed is ideal.

@huozhi
Copy link
Member

huozhi commented May 3, 2023

@surjithctly yeah the same as above, sorry for the confusion of posting the comments separately. If you want to use edge runtime, you need to move it to the font assets loading handler function, and also you need to enable edge runtime

@lukadev-0
Copy link

@lukadev-0 can you check the nodejs usage of og section if you want to use nodejs runtime, it's just using fs operations instead of fetch to load font or extra assets. The bundle size is bit larger because it contains more necessary dependencies for edge runtime, but the speed is ideal.

I tried that however the files didn't get included in the deployment

@huozhi
Copy link
Member

huozhi commented May 3, 2023

@lukadev-0 could you provide a reproduction? Can take a look on that to see what's going wrong

@lukadev-0
Copy link

@lukadev-0 could you provide a reproduction? Can take a look on that to see what's going wrong

Here you go: https://github.com/lukadev-0/next-og-image-font-repro

Whenever fetching the open graph image the following error is thrown:

error - [Error: ENOENT: no such file or directory, open '/vercel/path0/src/app/Inter-Medium.ttf'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/vercel/path0/src/app/Inter-Medium.ttf'
}

@kochie
Copy link

kochie commented May 4, 2023

Doesn't look like the font is in the public directory. Try moving it into public/Inter-Medium.ttf and referencing it as /Inter-Medium.ttf from your app.

@iamhectorsosa
Copy link

can you check the nodejs usage of og section if you want to use nodejs runtime, it's just using fs operations instead of fetch to load font or extra assets. The bundle size is bit larger because it contains more necessary dependencies for edge runtime, but the speed is ideal.

@huozhi I ran into this issue as well. Had the same exact config in the pages dir. Changing the runtime isn't a good solution here. Any suggestions here or increasing then plan size limit is the only option?

Error: The Edge Function "api/og" size is 1.01 MB and your plan size limit is 1 MB. Learn More: https://vercel.link/edge-function-size

@huozhi
Copy link
Member

huozhi commented May 4, 2023

@lukadev-0 Can you try the latest canary it resolves the deployment issue for me

@huozhi
Copy link
Member

huozhi commented May 4, 2023

@ekqt would you mind filing another issue with a reproduction for that as it's not related to the original problem of this issue

@lukadev-0
Copy link

@lukadev-0 Can you try the latest canary it resolves the deployment issue for me

I've tried with 13.4 (its newer than canary) and it worked. Thanks!

@lukadev-0
Copy link

@huozhi Turns out it doesn't work when there's a dynamic data fetch. (I've updated my reproduction)

export default async function og() {
   const res = await fetch("https://jsonplaceholder.typicode.com/todos/1", {
     cache: "no-store",
   });

@huozhi
Copy link
Member

huozhi commented May 4, 2023

Yeah can repro that, we take a look.

@huozhi huozhi reopened this May 4, 2023
@huozhi huozhi added kind: bug and removed bug Issue was opened via the bug report template. labels May 4, 2023
@huozhi huozhi changed the title Custom fonts in ImageResponse not working in App Dir [NEXT-1089] Custom fonts in ImageResponse not working in App Dir May 4, 2023
@surjithctly
Copy link
Author

surjithctly commented May 22, 2023

I changed my font and started to get this strange error. If I remove the fonts[], it works.

- error Error [TypeError]: Cannot read properties of undefined (reading '256')
    at parseFvarAxis (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.4.2_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/compiled/@vercel/og/index.edge.js:10980:22)

UPDATE: Seems it cannot handle variable font yet? Replaced with a static one and worked again.

@mwskwong
Copy link

mwskwong commented May 28, 2023

Yeah, using the exact example in the doc in the latest/canary Next.js version will still fail with the above error.
https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#generate-images-using-code-js-ts-tsx

@huozhi Turns out it doesn't work when there's a dynamic data fetch. (I've updated my reproduction)

export default async function og() {
   const res = await fetch("https://jsonplaceholder.typicode.com/todos/1", {
     cache: "no-store",
   });

And this workaround no longer works as well.

Edit
Interestingly, if I load the font with a function, then it works, i.e.

const getInterSemiBold = async () => {
 const response = await fetch(
    new URL('./Inter-SemiBold.ttf', import.meta.url)
  );
  const interSemiBold = await response.arrayBuffer();

  return interSemiBold;
}

// ...
new ImageResponse(
  <OgImage />,
  {
    fonts: [
      {
        name: 'Inter',
        data: await getInterSemiBold(),
        style: 'normal',
        weight: 400,
      },
    ],
  }
)

@ecto
Copy link

ecto commented Jun 12, 2023

I was getting the Failed to parse URL error, and loading the font in a function as described in the comment above worked for me - thanks @mwskwong!

@riccardolardi
Copy link

Yep, loading the font in a function as described by @mwskwong does the trick

@florianjuengermann
Copy link

Confirmed, got it working with:

  • edge function
  • function in global scope const getFont = async () => { const res = await fetch( new URL('@/fonts/Inter/static/Inter-SemiBold.ttf', import.meta.url) ); return await res.arrayBuffer(); };
  • not in public directory

@prokopsimek
Copy link

prokopsimek commented Jul 3, 2023

The weird thing is, that function getFont does not work with a argument font. Do you know why?

const getFont = async (font: string) => {
  const res = await fetch(new URL(`./${font}.ttf`, import.meta.url));
  return await res.arrayBuffer();
};
Server Error
TypeError: fetch failed

This error happened while generating the page. Any console logs will be displayed in the terminal window.

Following code works:

const getFont = async (url: URL) => {
  const res = await fetch(url);
  return await res.arrayBuffer();
};

and then

await getFont(new URL('./Inter-Medium.ttf', import.meta.url))

@langovoi
Copy link

Hi, you’re using the default nodejs runtime, you can use fs read to access the font data in nodejs runtime.

Check out the nodejs example list here https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation/og-image-examples#load-files-in-node.js-runtime

Link doesn't work. Looks like this example was silently removed. Proof: web.archive.org. There is same last updated date, but different content.

Example from old doc, it works for node runtime:

const font = fs.promises.readFile(
  path.join(fileURLToPath(import.meta.url), '../../assets/TYPEWR__.ttf'),
);
const image = fs.promises.readFile(
  path.join(fileURLToPath(import.meta.url), '../../assets/image.png'),
);

@iamhectorsosa
Copy link

iamhectorsosa commented Jul 25, 2023

Moved around the things based on the suggestions.

import { ImageResponse } from "next/server";

const basePath = "hectorsosa.me/";

const title = "Hector Sosa";
const param = "";

export const runtime = "edge";

export const alt = "Hector Sosa";
export const size = {
  width: 1200,
  height: 630,
};

export const contentType = "image/png";

export default async function Image() {
  const groteskRegular = await fetch(
    new URL("./SchibstedGrotesk-Regular.ttf", import.meta.url)
  ).then((res) => res.arrayBuffer());
  const groteskSemibold = await fetch(
    new URL("./SchibstedGrotesk-Semibold.ttf", import.meta.url)
  ).then((res) => res.arrayBuffer());

  return new ImageResponse(
    (
      <div
        style={{
          fontWeight: 600,
          background: "rgb(250, 250, 250)",
          width: "100%",
          height: "100%",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          padding: 120,
          textAlign: "center",
        }}
      >
        <div
          style={{
            position: "absolute",
            width: size.width / 2,
            height: size.height / 1.5,
            top: "-50%",
            left: "60%",
            opacity: "0.25",
            backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%239C92AC' fill-opacity='0.29'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
          }}
        ></div>
        <h1
          style={{
            fontSize: 128,
            lineHeight: 1,
            letterSpacing: "-6px",
            color: "rgb(23, 23, 23)",
            margin: "6px",
          }}
        >
          {title}
        </h1>
        <p
          style={{
            fontSize: 40,
            lineHeight: 1,
            letterSpacing: "-1.5px",
            color: "rgb(64, 64, 64)",
            margin: 0,
          }}
        >
          {basePath + param}
        </p>
      </div>
    ),
    {
      ...size,
      debug: false,
      fonts: [
        {
          name: "Grotesk",
          data: groteskRegular,
          style: "normal",
          weight: 400,
        },
        {
          name: "Grotesk",
          data: groteskSemibold,
          style: "normal",
          weight: 600,
        },
      ],
    }
  );
}

The build succeeds locally but fails at deployment. I'm running into this issue:

./src/app/opengraph-image.tsx:23:4
13:31:43.419 | Module not found: Can't resolve './SchibstedGrotesk-Semibold.ttf'
13:31:43.419 | 21 \|   ).then((res) => res.arrayBuffer());
13:31:43.419 | 22 \|   const groteskSemibold = await fetch(
13:31:43.419 | > 23 \|     new URL("./SchibstedGrotesk-Semibold.ttf", import.meta.url)
13:31:43.419 | \|    ^
13:31:43.420 | 24 \|   ).then((res) => res.arrayBuffer());
13:31:43.420 | 25 \|
13:31:43.420 | 26 \|   return new ImageResponse(
13:31:43.420

@mrasadatik
Copy link

Guys, I don't know why!!! But using woff font format instead of ttf solve my problem!

@suggy33
Copy link

suggy33 commented Aug 3, 2023

Folks, make sure to use static font. Variable font was making it error for me.

kodiakhq bot pushed a commit that referenced this issue Aug 14, 2023
When implementing `opengraph-image` in my [personal-site-project](https://github.com/kylemcd/personal-site). I was consistently running into issues where custom fonts would either only work locally or only work on vercel. To me it seemed like differences in pathing in `edge` vs `nodejs` runtimes. After digging around I found issue #48081, more specifically [this comment](#48081 (comment)) where moving the `fetch` for the font into the `Image` function solved the issue.

I'm not sure if this is 100% the correct fix, or if this is an issue that needs to be solved in another way. If that's not the case this PR updates the documentation around `opengraph-image` to have the fetch for custom fonts inside of the `Image` function.
@kodiakhq kodiakhq bot closed this as completed in #54274 Aug 19, 2023
kodiakhq bot pushed a commit that referenced this issue Aug 19, 2023
In the past few rounds of improving metadata image routes bundling, we have improved the bundling strategy and also updated [the usage tutorial of using custom fonts in og image routes](https://vercel.com/docs/functions/edge-functions/og-image-generation/og-image-examples) which should load the font in the image route handler.

Adding some tests to ensure custom fonts are working with metadata

Closes #48081
@altano
Copy link

altano commented Aug 20, 2023

I created this helper:

import type { Font } from "satori";

export default async function getFonts(): Promise<Font[]> {
  // This is unfortunate but I can't figure out how to load local font files
  // when deployed to vercel.
  const [interRegular, interMedium, interSemiBold, interBold] =
    await Promise.all([
      fetch(`https://rsms.me/inter/font-files/Inter-Regular.woff`).then((res) =>
        res.arrayBuffer()
      ),
      fetch(`https://rsms.me/inter/font-files/Inter-Medium.woff`).then((res) =>
        res.arrayBuffer()
      ),
      fetch(`https://rsms.me/inter/font-files/Inter-SemiBold.woff`).then(
        (res) => res.arrayBuffer()
      ),
      fetch(`https://rsms.me/inter/font-files/Inter-Bold.woff`).then((res) =>
        res.arrayBuffer()
      ),
    ]);

  return [
    {
      name: "Inter",
      data: interRegular,
      style: "normal",
      weight: 400,
    },
    {
      name: "Inter",
      data: interMedium,
      style: "normal",
      weight: 500,
    },
    {
      name: "Inter",
      data: interSemiBold,
      style: "normal",
      weight: 600,
    },
    {
      name: "Inter",
      data: interBold,
      style: "normal",
      weight: 700,
    },
  ];
}

And then used it whenever I needed fonts in an opengraph-image.tsx file:

return new ImageResponse(..., {
  fonts: await getFonts(),
});

I feel bad fetching from a CDN but I couldn't get anything else to work. Here's what I was seeing:

  1. runtime=edge would not work for me. no matter what I did I was over the 1MB limit, whatever that means.
  2. Once deployed to vercel with runtime=nodejs: the OG image of static routes pre-render fine and the font file is there. but, dynamic routes lazily render the OG image when requested (even with getStaticPaths, see OpenGraph images are not statically generated for dynamic routes #51147) and the font files are NOT IN THE DEPLOYMENT at all in Vercel during this lazy render. I scanned the entire filesystem with globby: the ttf files are gone. I tried putting them in multiple places (including public) and they are definitely just not there.

So fetching from a CDN and using runtime=node is the only solution that works for dynamic routes deployed to vercel.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 4, 2023

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot added the locked label Sep 4, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 4, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: app App directory (appDir: true) locked Metadata Related to Next.js' Metadata API. Pages Router Related to Pages Router.
Projects
None yet
Development

Successfully merging a pull request may close this issue.