Skip to content

Commit 2878683

Browse files
committed
dynamic opengraph images
1 parent 4d2febd commit 2878683

File tree

50 files changed

+260
-183
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+260
-183
lines changed

app/layout.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,6 @@ export const metadata: Metadata = {
3333
url: "/",
3434
locale: config.siteLocale?.replace("-", "_"),
3535
type: "website",
36-
images: [
37-
{
38-
url: meJpg.src,
39-
alt: `${config.siteName}${config.shortDescription}`,
40-
width: meJpg.width,
41-
height: meJpg.height,
42-
},
43-
],
4436
},
4537
twitter: {
4638
creator: `@${config.authorSocial?.twitter}`,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/* eslint-disable jsx-a11y/alt-text */
2+
3+
import { ImageResponse } from "next/og";
4+
// import { notFound } from "next/navigation";
5+
import path from "path";
6+
import fs from "fs/promises";
7+
import glob from "fast-glob";
8+
import { getFrontMatter } from "../../../lib/helpers/posts";
9+
10+
export const contentType = "image/png";
11+
export const size = {
12+
// https://developers.facebook.com/docs/sharing/webmasters/images/
13+
width: 1200,
14+
height: 630,
15+
};
16+
17+
const Image = async ({ params }: { params: { slug: string } }) => {
18+
const { slug } = params;
19+
20+
// check for invalid slug
21+
// const validSlugs = await getPostSlugs();
22+
// if (!validSlugs.includes(slug)) {
23+
// notFound();
24+
// }
25+
26+
// search for an opengraph-image.* file next to the note's index.mdx and load it
27+
const imagePath = await glob(`notes/${slug}/opengraph-image.(png|jpg|jpeg)`);
28+
let imageSrc = null;
29+
if (imagePath.length > 0) {
30+
const imageData = await fs.readFile(path.join(process.cwd(), imagePath[0]));
31+
imageSrc = Uint8Array.from(imageData).buffer;
32+
}
33+
34+
// load the author avatar
35+
const avatarData = await fs.readFile(path.join(process.cwd(), "app", "me.jpg"));
36+
const avatarSrc = Uint8Array.from(avatarData).buffer;
37+
38+
// load the Geist font from its npm package
39+
const geistFont = await fs.readFile(
40+
path.join(process.cwd(), "node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf")
41+
);
42+
43+
// load the note's front matter
44+
const { title } = await getFrontMatter(slug);
45+
46+
return new ImageResponse(
47+
(
48+
<div
49+
style={{
50+
display: "flex",
51+
height: "100%",
52+
width: "100%",
53+
flexDirection: "column",
54+
fontFamily: "Geist",
55+
fontSize: 20,
56+
fontWeight: 600,
57+
letterSpacing: -0.5,
58+
background: "linear-gradient(0deg, hsla(197, 14%, 57%, 1) 0%, hsla(192, 17%, 94%, 1) 100%)",
59+
}}
60+
>
61+
{imageSrc && (
62+
<div
63+
style={{
64+
display: "flex",
65+
alignItems: "center",
66+
justifyContent: "center",
67+
width: "100%",
68+
height: "100%",
69+
}}
70+
>
71+
<img
72+
// @ts-expect-error
73+
src={imageSrc}
74+
style={{ objectFit: "cover", width: "100%", height: "100%" }}
75+
/>
76+
</div>
77+
)}
78+
<div
79+
style={{
80+
display: "flex",
81+
alignItems: "center",
82+
position: "absolute",
83+
left: 42,
84+
top: 42,
85+
}}
86+
>
87+
<img
88+
// @ts-expect-error
89+
src={avatarSrc}
90+
style={{ width: 64, height: 64, borderRadius: "100%" }}
91+
/>
92+
</div>
93+
<div
94+
style={{
95+
display: "flex",
96+
flexWrap: "wrap",
97+
position: "absolute",
98+
left: 0,
99+
bottom: 42,
100+
padding: "12px 20px",
101+
margin: "0 42px",
102+
width: "auto",
103+
maxWidth: "100%",
104+
backgroundColor: "rgba(16, 16, 16, 0.85)",
105+
fontSize: 40,
106+
lineHeight: 1.4,
107+
color: "#fefefe",
108+
}}
109+
>
110+
{title}
111+
</div>
112+
</div>
113+
),
114+
{
115+
...size,
116+
fonts: [
117+
{
118+
name: "Geist",
119+
data: geistFont,
120+
style: "normal",
121+
weight: 700,
122+
},
123+
],
124+
}
125+
);
126+
};
127+
128+
export default Image;

app/notes/[slug]/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ export const generateMetadata = async ({ params }: { params: Promise<{ slug: str
4545
tags: frontmatter.tags,
4646
publishedTime: frontmatter.date,
4747
modifiedTime: frontmatter.date,
48-
images: frontmatter.image
49-
? [{ url: frontmatter.image, alt: frontmatter.title }]
50-
: defaultMetadata.openGraph?.images,
48+
},
49+
twitter: {
50+
...defaultMetadata.twitter,
51+
card: "summary_large_image",
5152
},
5253
alternates: {
5354
...defaultMetadata.alternates,
@@ -66,7 +67,6 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
6667
name: frontmatter.title,
6768
description: frontmatter.description || config.longDescription,
6869
url: frontmatter.permalink,
69-
image: frontmatter.image,
7070
datePublished: frontmatter.date,
7171
dateModified: frontmatter.date,
7272
author: {

app/page.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,27 @@ import hash from "@emotion/hash";
22
import { rgba } from "polished";
33
import { LockIcon } from "lucide-react";
44
import UnstyledLink from "../components/Link";
5+
import { metadata as defaultMetadata } from "./layout";
56
import type { ComponentPropsWithoutRef } from "react";
6-
import type { Route } from "next";
7+
import type { Route, Metadata } from "next";
78

89
import styles from "./page.module.css";
910

11+
import meJpg from "./me.jpg";
12+
13+
export const metadata: Metadata = {
14+
openGraph: {
15+
...defaultMetadata.openGraph,
16+
images: [
17+
{
18+
url: meJpg.src,
19+
width: meJpg.width,
20+
height: meJpg.height,
21+
},
22+
],
23+
},
24+
};
25+
1026
const Link = ({
1127
lightColor,
1228
darkColor,

app/projects/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { graphql } from "@octokit/graphql";
2+
import commaNumber from "comma-number";
23
import { GitForkIcon, StarIcon } from "lucide-react";
34
import PageTitle from "../../components/PageTitle";
45
import Link from "../../components/Link";
56
import RelativeTime from "../../components/RelativeTime";
6-
import commaNumber from "comma-number";
7-
import config from "../../lib/config";
87
import { metadata as defaultMetadata } from "../layout";
8+
import config from "../../lib/config";
99
import type { Metadata } from "next";
1010
import type { User, Repository } from "@octokit/graphql-schema";
1111

lib/helpers/build-feed.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export const buildFeed = async (): Promise<Feed> => {
3232
link: post.permalink,
3333
title: post.title,
3434
description: post.description,
35-
image: post.image || undefined,
3635
author: [
3736
{
3837
name: config.authorName,

lib/helpers/posts.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export type FrontMatter = {
1515
title: string;
1616
htmlTitle?: string;
1717
description?: string;
18-
image?: string;
1918
tags?: string[];
2019
noComments?: boolean;
2120
};
@@ -62,7 +61,6 @@ export const getFrontMatter = async (slug: string): Promise<FrontMatter> => {
6261
slug,
6362
date: formatDate(frontmatter.date), // validate/normalize the date string provided from front matter
6463
permalink: `${config.baseUrl}/${POSTS_DIR}/${slug}`,
65-
image: frontmatter.image ? `${config.baseUrl}${frontmatter.image}` : undefined,
6664
};
6765
};
6866

middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@ export const middleware = (request: NextRequest) => {
6666
export const config = {
6767
// save compute time by skipping middleware for static and metadata files
6868
matcher: [
69-
"/((?!_next/static|_next/image|_vercel|static|.well-known|favicon.ico|icon.png|apple-icon.png|manifest.webmanifest).*)",
69+
"/((?!_next/static|_next/image|_vercel|static|\\.well-known|favicon.ico|icon.png|apple-icon.png|sitemap.xml|robots.txt|manifest.webmanifest|feed.xml|feed.atom).*)",
7070
],
7171
};

notes/bernie-sanders-bern-app-data/index.mdx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import featuredImage from "./sad-bernie.jpg";
2-
31
export const frontmatter = {
42
title: 'Bernie Sanders\' Creepy "BERN" App Wants Your Data...From Your Best Friends',
53
date: "2019-05-08 10:31:02-0400",
64
description:
75
"The team behind Bernie's campaign has a new app named BERN. It's undoubtedly a smart move, but also a concerning one for privacy advocates.",
86
tags: ["Privacy", "Data", "Bernie Sanders", "2020 Presidential Campaign", "Politics"],
9-
image: featuredImage.src,
107
};
118

129
The team behind Bernie Sanders' 2020 campaign [released a new web app](https://www.nbcnews.com/politics/2020-election/bernie-sanders-2020-campaign-unveils-app-increase-its-voter-database-n999206) last month named [BERN](https://app.berniesanders.com/). The goal of BERN is simple: to gather as much information as they can on as many voters in the United States as they can, and make their grassroots army of enthusiastic supporters do the work. It's undoubtedly a smart strategy, but also a concerning one for myself and other privacy advocates.
359 KB
Loading

0 commit comments

Comments
 (0)