From a81117972d39df35574bbab809bb590abc874761 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 14 Jun 2025 19:25:16 +0000 Subject: feat: implement Open Graph image generation and enhance configuration - Added ogImages integration to generate Open Graph images for blog posts. - Updated configuration to include Open Graph settings and default preview image. - Refactored Head component to utilize new preview property for Open Graph meta tags. - Enhanced blog post schema to include preview image for structured data representation. - Introduced utility functions for creating Open Graph images with dynamic content. --- src/utils/createOgImage.ts | 52 +++++++++++++++++++++++++++++++++++++ src/utils/ogResources.ts | 15 +++++++++++ src/utils/schemas/blogPostSchema.ts | 5 ++-- 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/utils/createOgImage.ts create mode 100644 src/utils/ogResources.ts (limited to 'src/utils') diff --git a/src/utils/createOgImage.ts b/src/utils/createOgImage.ts new file mode 100644 index 0000000..da2cece --- /dev/null +++ b/src/utils/createOgImage.ts @@ -0,0 +1,52 @@ +import { config } from "../config"; +import { html } from "satori-html"; +import { resources } from "./ogResources"; +import { Resvg } from "@resvg/resvg-js"; +import dayjs from "dayjs"; +import satori from "satori"; + +export async function createOgImage(title: string, datePublished: Date): Promise { + const formattedDate = dayjs(datePublished).format("MMMM DD, YYYY"); + + const markup = await satori( + html(` +
+
+
${formattedDate}
+
${title}
+
+
+
+ ${config.og.website.toLocaleUpperCase()} +
+
+ +
+ ${config.author.name} + ${config.author.email} +
+
+
+
+`), + { + width: config.og.dimensions.width, + height: config.og.dimensions.height, + fonts: [ + { + name: "Inter", + data: resources.fonts.regular, + weight: 400, + }, + { + name: "Inter", + data: resources.fonts.bold, + weight: 700, + }, + ], + } + ); + + const image = new Resvg(markup, { fitTo: { mode: "width", value: config.og.dimensions.width } }); + return image.render().asPng(); +} diff --git a/src/utils/ogResources.ts b/src/utils/ogResources.ts new file mode 100644 index 0000000..8049fb2 --- /dev/null +++ b/src/utils/ogResources.ts @@ -0,0 +1,15 @@ +import { config } from "../config"; +import fs from "fs/promises"; +import path from "path"; +import sharp from "sharp"; + +export const resources = { + fonts: { + regular: await fs.readFile(path.resolve(config.og.fonts.regular)), + bold: await fs.readFile(path.resolve(config.og.fonts.bold)), + }, + photoBase64: await (async () => { + const buf = await fs.readFile(path.resolve(config.og.photo)); + return "data:image/png;base64," + (await sharp(buf).resize(120, 120).png({ quality: 95 }).toBuffer()).toString("base64"); + })(), +}; diff --git a/src/utils/schemas/blogPostSchema.ts b/src/utils/schemas/blogPostSchema.ts index 9e43478..87e1bf2 100644 --- a/src/utils/schemas/blogPostSchema.ts +++ b/src/utils/schemas/blogPostSchema.ts @@ -7,18 +7,19 @@ export type BlogPostSchemaParams = { readonly description: string; readonly isBasedOn?: string; readonly lang: string; + readonly preview: string; readonly siteUrl: string; readonly slug: string; readonly title: string; }; -export default ({ siteUrl, slug, title, description, datePublished, dateModified, lang, isBasedOn }: BlogPostSchemaParams): WithContext => ({ +export default ({ siteUrl, slug, title, description, preview, datePublished, dateModified, lang, isBasedOn }: BlogPostSchemaParams): WithContext => ({ "@context": "https://schema.org", "@type": "BlogPosting", "url": new URL(`/blog/${slug}`, siteUrl).toString(), "headline": title, "description": description, - "image": new URL(config.posts.defaultImage, siteUrl).toString(), + "image": new URL(preview, siteUrl).toString(), "datePublished": datePublished, "dateModified": dateModified, "inLanguage": lang, -- cgit v1.2.3