diff --git a/package-lock.json b/package-lock.json index 1c75e0b..712309b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2143,9 +2143,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", - "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, "peer": true, "dependencies": { diff --git a/src/content/blog/dynamic-og-images.md b/src/content/blog/dynamic-og-images.md index 8a60bb9..349b78f 100644 --- a/src/content/blog/dynamic-og-images.md +++ b/src/content/blog/dynamic-og-images.md @@ -41,10 +41,48 @@ Dynamic OG images will be generated at build time for blog posts that Dynamic OG image of AstroPaper includes _the blog post title_, _author name_ and _site title_. Author name and site title will be retrieved via `SITE.author` and `SITE.title` of **"src/config.ts"** file. The title is generated from the blog post frontmatter `title`. ![Example Dynamic OG Image link](https://user-images.githubusercontent.com/53733092/209704501-e9c2236a-3f4d-4c67-bab3-025aebd63382.png) +### Issue Non-Latin Characters + +Titles with non-latin characters won't display properly out of the box. To resolve this, we have to replace `fontsConfig` inside `loadGoogleFont.ts` with your preferred font. + +```ts +// file: loadGoogleFont.ts + +async function loadGoogleFonts( + text: string +): Promise< + Array<{ name: string; data: ArrayBuffer; weight: number; style: string }> +> { + const fontsConfig = [ + { + name: "Noto Sans JP", + font: "Noto+Sans+JP", + weight: 400, + style: "normal", + }, + { + name: "Noto Sans JP", + font: "Noto+Sans+JP:wght@700", + weight: 700, + style: "normal", + }, + { name: "Noto Sans", font: "Noto+Sans", weight: 400, style: "normal" }, + { + name: "Noto Sans", + font: "Noto+Sans:wght@700", + weight: 700, + style: "normal", + }, + ]; + // other codes +} +``` + +> Check out [this PR](https://github.com/satnaing/astro-paper/pull/318) for more info. + ## Limitations At the time of writing this, [Satori](https://github.com/vercel/satori) is fairly new and has not reached major release yet. So, there are still some limitations to this dynamic OG image feature. -- If you have Blog posts with non-English titles, you have to set `embedFonts` option to `false` (file: `src/utils/generateOgImage.tsx`). Even after this, the OG image might not be displayed very well. - Besides, RTL languages are not supported yet. - [Using emoji](https://github.com/vercel/satori#emojis) in the title might be a little bit tricky. diff --git a/src/utils/generateOgImages.tsx b/src/utils/generateOgImages.tsx index c6752b5..7694718 100644 --- a/src/utils/generateOgImages.tsx +++ b/src/utils/generateOgImages.tsx @@ -1,47 +1,8 @@ -import satori, { type SatoriOptions } from "satori"; import { Resvg } from "@resvg/resvg-js"; import { type CollectionEntry } from "astro:content"; import postOgImage from "./og-templates/post"; import siteOgImage from "./og-templates/site"; -const fetchFonts = async () => { - // Regular Font - const fontFileRegular = await fetch( - "https://www.1001fonts.com/download/font/ibm-plex-mono.regular.ttf" - ); - const fontRegular: ArrayBuffer = await fontFileRegular.arrayBuffer(); - - // Bold Font - const fontFileBold = await fetch( - "https://www.1001fonts.com/download/font/ibm-plex-mono.bold.ttf" - ); - const fontBold: ArrayBuffer = await fontFileBold.arrayBuffer(); - - return { fontRegular, fontBold }; -}; - -const { fontRegular, fontBold } = await fetchFonts(); - -const options: SatoriOptions = { - width: 1200, - height: 630, - embedFont: true, - fonts: [ - { - name: "IBM Plex Mono", - data: fontRegular, - weight: 400, - style: "normal", - }, - { - name: "IBM Plex Mono", - data: fontBold, - weight: 600, - style: "normal", - }, - ], -}; - function svgBufferToPngBuffer(svg: string) { const resvg = new Resvg(svg); const pngData = resvg.render(); @@ -49,11 +10,11 @@ function svgBufferToPngBuffer(svg: string) { } export async function generateOgImageForPost(post: CollectionEntry<"blog">) { - const svg = await satori(postOgImage(post), options); + const svg = await postOgImage(post); return svgBufferToPngBuffer(svg); } export async function generateOgImageForSite() { - const svg = await satori(siteOgImage(), options); + const svg = await siteOgImage(); return svgBufferToPngBuffer(svg); } diff --git a/src/utils/loadGoogleFont.ts b/src/utils/loadGoogleFont.ts new file mode 100644 index 0000000..cfd5c56 --- /dev/null +++ b/src/utils/loadGoogleFont.ts @@ -0,0 +1,71 @@ +import type { FontStyle, FontWeight } from "satori"; + +export type FontOptions = { + name: string; + data: ArrayBuffer; + weight: FontWeight | undefined; + style: FontStyle | undefined; +}; + +async function loadGoogleFont( + font: string, + text: string +): Promise { + const API = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`; + + const css = await ( + await fetch(API, { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1", + }, + }) + ).text(); + + const resource = css.match( + /src: url\((.+)\) format\('(opentype|truetype)'\)/ + ); + + if (!resource) throw new Error("Failed to download dynamic font"); + + const res = await fetch(resource[1]); + + if (!res.ok) { + throw new Error("Failed to download dynamic font. Status: " + res.status); + } + + const fonts: ArrayBuffer = await res.arrayBuffer(); + return fonts; +} + +async function loadGoogleFonts( + text: string +): Promise< + Array<{ name: string; data: ArrayBuffer; weight: number; style: string }> +> { + const fontsConfig = [ + { + name: "IBM Plex Mono", + font: "IBM+Plex+Mono", + weight: 400, + style: "normal", + }, + { + name: "IBM Plex Mono", + font: "IBM+Plex+Mono:wght@700", + weight: 700, + style: "bold", + }, + ]; + + const fonts = await Promise.all( + fontsConfig.map(async ({ name, font, weight, style }) => { + const data = await loadGoogleFont(font, text); + return { name, data, weight, style }; + }) + ); + + return fonts; +} + +export default loadGoogleFonts; diff --git a/src/utils/og-templates/post.tsx b/src/utils/og-templates/post.tsx index a0dcf60..5ed69cf 100644 --- a/src/utils/og-templates/post.tsx +++ b/src/utils/og-templates/post.tsx @@ -1,8 +1,10 @@ -import { SITE } from "@config"; +import satori from "satori"; import type { CollectionEntry } from "astro:content"; +import { SITE } from "@config"; +import loadGoogleFonts, { type FontOptions } from "../loadGoogleFont"; -export default (post: CollectionEntry<"blog">) => { - return ( +export default async (post: CollectionEntry<"blog">) => { + return satori(
) => {
- + , + { + width: 1200, + height: 630, + embedFont: true, + fonts: (await loadGoogleFonts( + post.data.title + post.data.author + SITE.title + "by" + )) as FontOptions[], + } ); }; diff --git a/src/utils/og-templates/site.tsx b/src/utils/og-templates/site.tsx index 33bddc6..190955c 100644 --- a/src/utils/og-templates/site.tsx +++ b/src/utils/og-templates/site.tsx @@ -1,7 +1,9 @@ +import satori from "satori"; import { SITE } from "@config"; +import loadGoogleFonts, { type FontOptions } from "../loadGoogleFont"; -export default () => { - return ( +export default async () => { + return satori(
{
- + , + { + width: 1200, + height: 630, + embedFont: true, + fonts: (await loadGoogleFonts(SITE.title + SITE.desc)) as FontOptions[], + } ); };