1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-07-03 12:46:38 -04:00

dynamic opengraph images

This commit is contained in:
2025-03-14 08:22:32 -04:00
parent 4d2febd262
commit e162d6a46c
35 changed files with 310 additions and 208 deletions

View File

@ -15,7 +15,7 @@ import "./global.css";
import styles from "./layout.module.css";
import meJpg from "./me.jpg";
import meJpg from "../public/static/me.jpg";
export const metadata: Metadata = {
metadataBase: new URL(config.baseUrl),
@ -33,14 +33,6 @@ export const metadata: Metadata = {
url: "/",
locale: config.siteLocale?.replace("-", "_"),
type: "website",
images: [
{
url: meJpg.src,
alt: `${config.siteName} ${config.shortDescription}`,
width: meJpg.width,
height: meJpg.height,
},
],
},
twitter: {
creator: `@${config.authorSocial?.twitter}`,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

View File

@ -0,0 +1,135 @@
/* eslint-disable jsx-a11y/alt-text */
import { ImageResponse } from "next/og";
import { notFound } from "next/navigation";
import path from "path";
import fs from "fs/promises";
import glob from "fast-glob";
import { getPostSlugs, getFrontMatter } from "../../../lib/helpers/posts";
export const dynamicParams = false;
export const contentType = "image/png";
export const size = {
// https://developers.facebook.com/docs/sharing/webmasters/images/
width: 1200,
height: 630,
};
export const generateStaticParams = async () => {
const slugs = await getPostSlugs();
// map slugs into a static paths object required by next.js
return slugs.map((slug) => ({
slug,
}));
};
const getLocalImage = async (src: string) => {
const imagePath = await glob(src);
if (imagePath.length > 0) {
const imageData = await fs.readFile(path.join(process.cwd(), imagePath[0]));
return Uint8Array.from(imageData).buffer;
}
// image doesn't exist
return null;
};
const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
try {
const { slug } = await params;
// get the note's title and image filename from its frontmatter
const { title, image } = await getFrontMatter(slug);
// load the image specified in the note's frontmatter from its directory
const imageSrc = await getLocalImage(`notes/${slug}/${image}`);
// load the author avatar
const avatarSrc = await getLocalImage("public/static/me.jpg");
return new ImageResponse(
(
<div
style={{
display: "flex",
flexDirection: "column",
height: "100%",
width: "100%",
background: "linear-gradient(0deg, hsla(197, 14%, 57%, 1) 0%, hsla(192, 17%, 94%, 1) 100%)",
}}
>
{imageSrc && (
<div
style={{
display: "flex",
height: "100%",
width: "100%",
}}
>
<img
// @ts-expect-error
src={imageSrc}
style={{ objectFit: "cover", height: "100%", width: "100%" }}
/>
</div>
)}
{avatarSrc && (
<div
style={{
display: "flex",
position: "absolute",
left: 42,
top: 42,
}}
>
<img
// @ts-expect-error
src={avatarSrc}
style={{ height: 96, width: 96, borderRadius: "100%" }}
/>
</div>
)}
<div
style={{
display: "flex",
position: "absolute",
left: 0,
bottom: 42,
padding: "12px 20px",
margin: "0 42px",
backgroundColor: "rgba(16, 16, 16, 0.85)",
fontFamily: "Geist",
fontSize: 40,
fontWeight: 600,
lineHeight: 1.4,
letterSpacing: -0.5,
color: "#fefefe",
}}
>
{title}
</div>
</div>
),
{
...size,
fonts: [
{
name: "Geist",
// load the Geist font directly from its npm package
data: await fs.readFile(
path.join(process.cwd(), "node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf")
),
style: "normal",
weight: 600,
},
],
}
);
} catch (error) {
console.error("[og-image] Error generating image:", error);
notFound();
}
};
export default Image;

View File

@ -45,9 +45,10 @@ export const generateMetadata = async ({ params }: { params: Promise<{ slug: str
tags: frontmatter.tags,
publishedTime: frontmatter.date,
modifiedTime: frontmatter.date,
images: frontmatter.image
? [{ url: frontmatter.image, alt: frontmatter.title }]
: defaultMetadata.openGraph?.images,
},
twitter: {
...defaultMetadata.twitter,
card: "summary_large_image",
},
alternates: {
...defaultMetadata.alternates,
@ -66,7 +67,6 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
name: frontmatter.title,
description: frontmatter.description || config.longDescription,
url: frontmatter.permalink,
image: frontmatter.image,
datePublished: frontmatter.date,
dateModified: frontmatter.date,
author: {

View File

@ -2,11 +2,27 @@ import hash from "@emotion/hash";
import { rgba } from "polished";
import { LockIcon } from "lucide-react";
import UnstyledLink from "../components/Link";
import { metadata as defaultMetadata } from "./layout";
import type { ComponentPropsWithoutRef } from "react";
import type { Route } from "next";
import type { Metadata, Route } from "next";
import styles from "./page.module.css";
import meJpg from "../public/static/me.jpg";
export const metadata: Metadata = {
openGraph: {
...defaultMetadata.openGraph,
images: [
{
url: meJpg.src,
width: meJpg.width,
height: meJpg.height,
},
],
},
};
const Link = ({
lightColor,
darkColor,

View File

@ -1,11 +1,11 @@
import { graphql } from "@octokit/graphql";
import commaNumber from "comma-number";
import { GitForkIcon, StarIcon } from "lucide-react";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import RelativeTime from "../../components/RelativeTime";
import commaNumber from "comma-number";
import config from "../../lib/config";
import { metadata as defaultMetadata } from "../layout";
import config from "../../lib/config";
import type { Metadata } from "next";
import type { User, Repository } from "@octokit/graphql-schema";