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

feat: dynamic open graph images #3

Merged
merged 11 commits into from
Aug 8, 2024
10 changes: 10 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss"
],

"unwantedRecommendations": [
"esbenp.prettier-vscode"
]
}
29 changes: 27 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
{
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
],
"css.customData": [".vscode/css.json"]

"tailwindCSS.classAttributes": [
"class",
"className",
"tw"
],

"css.customData": [
".vscode/css.json"
],

"[javascript]": {
"editor.formatOnSave": true
},

"[typescript]": {
"editor.formatOnSave": true
},

"[typescriptreact]": {
"editor.formatOnSave": true
},

"[json]": {
"editor.formatOnSave": true
}
}
71 changes: 71 additions & 0 deletions content/blog/en/dynamic-og-images-is-here.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: Dynamic Open Graph images for your blog posts are now available
date: 2024-08-07 22:33:00
excerpt: With the generation of dynamic Open Graph images, your blog posts can now have featured images generated automatically.
author_id: daltonmenezes
tags: [next.js, open-graph, blog, dynamic-images, seo, social-media]
---

Now it is possible to generate dynamic Open Graph images for your blog posts.
With this, when someone shares a post on social networks, the featured image will be generated automatically from the content of the post. The contents are:

- Site logo
- Post title
- Author's photo and name

You will be able to customize the design of the images according to your project.

**Today, there are two ways** to define which OG image will be used in a blog post:
1. Adding an `og_image` field in the frontmatter of the `.mdx` file of the post
```mdx
---
og_image: introducing-blogs-og.jpg
---
```
the image must be in the `public/blog-og/` folder and the name and extension of the image file must be the same as defined in the frontmatter.

2. If an `og_image` field is not defined, the image will be generated automatically from the content of the post.

For example, the automatically generated image for this post is the following and supports internationalization,
try changing the site language, but maybe you need to refresh the page to see the changes:

<img src="/blog/og/dynamic-og-images-is-here" alt="the generated dynamic og image" />

## Changing the logo
In the `public` folder, you can replace the `logo.svg` file with your logo.

## Changing the background image
In the `public` folder, you can replace the `og-background.jpg` file with the background image you want.

## Changing the font

<Alert variant="warning">
<AlertTitle>⚠️ Warning</AlertTitle>
<AlertDescription>Avoid using `variable` fonts, always use static fonts, as Next.js has been having problems with this type of font!</AlertDescription>
</Alert>

In the `public/fonts` folder, you can add the desired font and change the `src/lib/fonts.ts` file to load the new font:
```ts
export async function getFonts() {
const [bold, regular] = await Promise.all([
fetch(new URL(absoluteUrl('/fonts/Geist-Bold.ttf'), import.meta.url)).then(
(res) => res.arrayBuffer()
),

fetch(
new URL(absoluteUrl('/fonts/Geist-Regular.ttf'), import.meta.url)
).then((res) => res.arrayBuffer()),
])

return {
bold,
regular,
}
}
```

## Changing styles and structures
For a deeper change, edit the `src/app/[locale]/blog/og/[slug]/route.tsx` file



71 changes: 71 additions & 0 deletions content/blog/pt/dynamic-og-images-is-here.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: Imagens dinâmicas de Open Graph para suas postagens de blog já estão disponíveis
date: 2024-08-07 22:33:00
excerpt: Com a geração de imagens dinâmicas de Open Graph, suas postagens de blog agora podem ter imagens de destaque geradas automaticamente.
author_id: daltonmenezes
tags: [next.js, open-graph, blog, dynamic-images, seo, social-media]
---

Agora é possível gerar imagens dinâmicas de Open Graph para suas postagens de blog.
Com isso, quando alguém compartilhar uma postagem nas redes sociais, a imagem de destaque será gerada automaticamente a partir do conteúdo da postagem. Os conteúdos são:

- Logotipo do site
- Título do post
- Foto e nome do autor

Você poderá customizar o design das imagens de acordo com o seu projeto.

**Hoje, existem duas formas** de definir qual OG image será utilizada em uma postagem de blog:
1. Adicionando um campo `og_image` no frontmatter do arquivo `.mdx` da postagem
```mdx
---
og_image: introducing-blogs-og.jpg
---
```
a imagem precisará estar na pasta `public/blog-og/` e o nome e extensão do arquivo da imagem deve ser o mesmo que foi definido no frontmatter.

2. Caso não seja definido um campo `og_image`, a imagem será gerada automaticamente a partir do conteúdo da postagem.

Por exemplo, a imagem automaticamente gerada para esta postagem é a seguinte e tem suporte a internacionalização,
experimente trocar o idioma do site mas talvez seja necessário atualizar a página para ver as mudanças:

<img src="/blog/og/dynamic-og-images-is-here" alt="the generated dynamic og image" />

## Alterando o logotipo
Na pasta `public`, você pode substituir o arquivo `logo.svg` pelo seu logotipo.

## Alterando a imagem de fundo
Na pasta `public`, você pode substituir o arquivo `og-background.jpg` pela imagem de fundo que desejar.

## Alterando a fonte

<Alert variant="warning">
<AlertTitle>⚠️ Atenção</AlertTitle>
<AlertDescription>Evite usar fontes do tipo `variable`, utilize sempre fontes estáticas, pois o Next.js tem dado problemas com esse tipo de fonte!</AlertDescription>
</Alert>

Na pasta `public/fonts`, você pode adicionar a fonte desejada e alterar o arquivo `src/lib/fonts.ts` para carregar a nova fonte:
```ts
export async function getFonts() {
const [bold, regular] = await Promise.all([
fetch(new URL(absoluteUrl('/fonts/Geist-Bold.ttf'), import.meta.url)).then(
(res) => res.arrayBuffer()
),

fetch(
new URL(absoluteUrl('/fonts/Geist-Regular.ttf'), import.meta.url)
).then((res) => res.arrayBuffer()),
])

return {
bold,
regular,
}
}
```

## Alterando estilos e estruturas
Para uma alteração mais profunda, edite o arquivo `src/app/[locale]/blog/og/[slug]/route.tsx`



Binary file added public/fonts/Geist-Bold.ttf
Binary file not shown.
Binary file added public/fonts/Geist-Regular.ttf
Binary file not shown.
6 changes: 6 additions & 0 deletions public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/og-background.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 3 additions & 5 deletions src/app/[locale]/blog/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,7 @@ export async function generateMetadata({

images: [
{
width: 1200,
height: 630,
...siteConfig.og.size,
url: ogImage,
alt: siteConfig.name,
},
Expand Down Expand Up @@ -98,7 +97,7 @@ export async function generateMetadata({

const postOgImage = blogPost.og_image
? absoluteUrl(`/blog-og/${blogPost.og_image}`)
: siteConfig.ogImage
: absoluteUrl(`/blog/og/${blogSlug}`)

return {
title: blogPost.title,
Expand All @@ -119,8 +118,7 @@ export async function generateMetadata({

images: [
{
width: 1200,
height: 630,
...siteConfig.og.size,
url: postOgImage,
alt: blogPost.title,
},
Expand Down
133 changes: 133 additions & 0 deletions src/app/[locale]/blog/og/[slug]/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* eslint-disable @next/next/no-img-element */

import { ImageResponse } from 'next/og'

import type { LocaleOptions } from '@/lib/opendocs/types/i18n'
import type { NextRequest } from 'next/server'

import { allBlogs, type Blog } from 'contentlayer/generated'
import { absoluteUrl, truncateText } from '@/lib/utils'
import { siteConfig } from '@/config/site'
import { getFonts } from '@/lib/fonts'

interface BlogOgProps {
params: { slug: string; locale: LocaleOptions }
}

export const runtime = 'edge'
export const dynamicParams = true

export async function GET(_: NextRequest, { params }: BlogOgProps) {
const post = getBlogPostBySlugAndLocale(params.slug, params.locale)

if (!post) {
return new ImageResponse(<Fallback src="/og.jpg" />, {
...siteConfig.og.size,
})
}

const { bold, regular } = await getFonts()

return new ImageResponse(
(
<div
tw={`bg-black flex flex-col min-w-full h-[${siteConfig.og.size.height}px] relative`}
>
<Background src="/og-background.jpg" />

<div tw="my-10 mx-14 flex flex-col">
<Logo src="/logo.svg" />

<div tw="flex flex-col h-full max-h-[300px]">
<Title>{post.title}</Title>
<Author post={post} />
</div>
</div>
</div>
),
{
...siteConfig.og.size,
fonts: [
{
name: 'Geist',
data: regular,
style: 'normal',
weight: 400,
},
{
name: 'Geist',
data: bold,
style: 'normal',
weight: 700,
},
],
}
)
}

function Author({ post }: { post: Blog }) {
return (
<div tw="flex items-center pt-10">
{post.author?.image && (
<img
tw="w-20 h-20 rounded-full border-gray-800 border-4"
src={absoluteUrl(post.author?.image)}
alt=""
/>
)}

<span tw="ml-3 text-gray-400 text-3xl">{post.author?.name}</span>
</div>
)
}

function Background({ src }: { src: string }) {
return (
<img
alt=""
src={absoluteUrl(src)}
tw="w-full h-full absolute left-0 top-0 opacity-70"
/>
)
}

function Logo({ src }: { src: string }) {
return <img tw="w-28 h-28 rounded-full" src={absoluteUrl(src)} alt="" />
}

function Title({ children }: { children: string }) {
return (
<div tw="pt-4 flex flex-col h-full justify-center">
<h1 tw="text-white text-7xl w-full">{truncateText(children)}</h1>
</div>
)
}

function Fallback({ src }: { src: string }) {
return (
<div tw="flex w-full h-full">
<img src={absoluteUrl(src)} tw="w-full h-full" alt="" />
</div>
)
}

function getBlogPostBySlugAndLocale(slug: string, locale: LocaleOptions) {
return allBlogs.find((post) => {
const [postLocale, ...slugs] = post.slugAsParams.split('/')

return slugs.join('/') === slug && postLocale === locale
})
}

export async function generateStaticParams(): Promise<BlogOgProps['params'][]> {
const blog = allBlogs.map((blog) => {
const [locale, ...slugs] = blog.slugAsParams.split('/')

return {
slug: slugs.join('/'),
locale: locale as LocaleOptions,
}
})

return blog
}
7 changes: 3 additions & 4 deletions src/app/[locale]/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ export async function generateMetadata({

images: [
{
url: siteConfig.ogImage,
width: 1200,
height: 630,
...siteConfig.og.size,
url: siteConfig.og.image,
alt: siteConfig.name,
},
],
Expand All @@ -63,7 +62,7 @@ export async function generateMetadata({
card: 'summary_large_image',
title: doc.title,
description: doc.description,
images: [siteConfig.ogImage],
images: [siteConfig.og.image],
creator: siteConfig.links.twitter.username,
},
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/[locale]/feed/[feed]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function generateWebsiteFeeds({
id: file,
generator: siteConfig.name,
copyright: siteConfig.name,
image: absoluteUrl('/og.jpg'),
image: siteConfig.og.image,
language: locale || defaultLocale,
title: `Blog - ${siteConfig.name}`,
favicon: absoluteUrl('/favicon.ico'),
Expand Down
Loading