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:
@ -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}`,
|
||||
|
BIN
app/me.jpg
BIN
app/me.jpg
Binary file not shown.
Before Width: | Height: | Size: 128 KiB |
135
app/notes/[slug]/opengraph-image.tsx
Normal file
135
app/notes/[slug]/opengraph-image.tsx
Normal 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;
|
@ -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: {
|
||||
|
18
app/page.tsx
18
app/page.tsx
@ -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,
|
||||
|
@ -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";
|
||||
|
||||
|
Reference in New Issue
Block a user