Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

feat: Automatic OG images for Garden posts #70

Merged
merged 3 commits into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ module.exports = {
`src/templates/*.tsx`,
`**/gatsby-config.ts`,
`content/**/*.mdx`,
`netlify/edge-functions/*.tsx`,
],
rules: {
"import/no-default-export": 0,
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ cypress/e2e/build
playwright/test-results
dist
test-results
.netlify
Binary file added netlify/edge-functions/inter-500.ttf
Binary file not shown.
Binary file added netlify/edge-functions/inter-700.ttf
Binary file not shown.
161 changes: 161 additions & 0 deletions netlify/edge-functions/og.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import React from "https://esm.sh/[email protected]"
import type { Config } from "@netlify/edge-functions"
import { ImageResponse } from "https://deno.land/x/og_edge/mod.ts"

const WIDTH = 1600
const HEIGHT = 836

type Weight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
type Style = "normal" | "italic"

const customFonts: Array<{ name: string; weight: Weight; style: Style; fileName: string }> = [
{
name: `Inter`,
weight: 500,
style: `normal`,
fileName: `inter-500.ttf`,
},
{
name: `Inter`,
weight: 700,
style: `normal`,
fileName: `inter-700.ttf`,
},
]

const fonts = Promise.all(customFonts.map((font) => fetch(new URL(`./${font.fileName}`, import.meta.url))))

export default async function handler(req: Request) {
const { searchParams } = new URL(req.url)

const fontsDatas = await fonts

const fontsOptions = fontsDatas.map((fontData, i) => ({
name: customFonts[i].name,
data: fontData,
style: customFonts[i].style,
weight: customFonts[i].weight,
}))

const hasTitle = searchParams.has(`title`)
const title = hasTitle ? (searchParams.get(`title`) as string) : `Digital Garden`
const subTitle = hasTitle ? `Digital Garden` : `Lennart Jörgens`
const lastUpdated = searchParams.get(`lastUpdated`) ?? null
const tags = searchParams.get(`tags`) ?? null

return new ImageResponse(
(
<div
className="parent"
style={{
display: `flex`,
fontFamily: `"Inter", sans-serif`,
width: `${WIDTH}px`,
height: `${HEIGHT}px`,
flexDirection: `column`,
justifyContent: `center`,
alignItems: `center`,
position: `relative`,
background: `url(https://www.lekoarts.de/social/digital-garden-template.png?v1)`,
}}
>
<div
className="text"
style={{
display: `flex`,
flexDirection: `column`,
textAlign: `center`,
alignItems: `center`,
maxWidth: `1400px`,
}}
>
<h2
className="subtitle"
style={{
display: `flex`,
backgroundImage: `linear-gradient(to bottom, #FFFFFF 0%, #B8C6E9 100%)`,
backgroundClip: `text`,
WebkitBackgroundClip: `text`,
WebkitTextFillColor: `transparent`,
color: `transparent`,
fontSize: `57.33px`,
fontWeight: 500,
marginTop: `0`,
marginBottom: `16px`,
letterSpacing: `0.025em`,
}}
>
{subTitle}
</h2>
<h1
className="title"
style={{
display: `flex`,
wordBreak: `break-word`,
background: `linear-gradient(to bottom, #7AD28D 0%, #1B9C68 100%)`,
backgroundClip: `text`,
WebkitBackgroundClip: `text`,
WebkitTextFillColor: `transparent`,
color: `transparent`,
fontSize: `68.80px`,
fontWeight: 700,
margin: `0`,
lineHeight: 1.15,
}}
>
{title}
</h1>
</div>
{lastUpdated ? (
<div
className="date"
style={{
display: `flex`,
position: `absolute`,
left: `80px`,
bottom: `80px`,
background: `linear-gradient(to bottom, #FFFFFF 0%, #C3F1C3 100%)`,
backgroundClip: `text`,
WebkitBackgroundClip: `text`,
WebkitTextFillColor: `transparent`,
fontSize: `27.65px`,
color: `transparent`,
textAlign: `left`,
}}
>
Last updated: {lastUpdated}
</div>
) : null}
{tags ? (
<div
className="tags"
style={{
display: `flex`,
position: `absolute`,
right: `80px`,
bottom: `80px`,
background: `linear-gradient(to bottom, #FFFFFF 0%, #C3F1C3 100%)`,
backgroundClip: `text`,
WebkitBackgroundClip: `text`,
WebkitTextFillColor: `transparent`,
fontSize: `27.65px`,
color: `transparent`,
textAlign: `left`,
}}
>
Tags: {tags.split(`,`).join(`, `)}
</div>
) : null}
</div>
),
{
width: WIDTH,
height: HEIGHT,
fonts: fontsOptions,
}
)
}

export const config: Config = {
path: `/og/garden`,
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"remark-smartypants": "^2.0.0"
},
"devDependencies": {
"@netlify/edge-functions": "^2.2.0",
"@playwright/test": "^1.37.1",
"@testing-library/jest-dom": "^6.1.2",
"@testing-library/react": "^14.0.0",
Expand Down
2 changes: 1 addition & 1 deletion playwright/meta.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const metaTagAssertions = [
},
{
key: `og:image`,
value: `https://www.lekoarts.de${site.defaultGardenOgImage}`,
value: `https://www.lekoarts.de/og/garden?title=How+to+Add+Plausible+Analytics+to+Gatsby&lastUpdated=Aug+22%2C+2023&tags=Gatsby`,
},
{
key: `twitter:label2`,
Expand Down
1 change: 1 addition & 0 deletions src/constants/meta.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const site = {
defaultOgImage: `/social/default-og-image.png?v=1`,
twitter: `@lekoarts_de`,
defaultGardenOgImage: `/social/digital-garden.png`,
gardenOgEdge: `/og/garden`,
}
103 changes: 55 additions & 48 deletions src/templates/garden.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type DataProps = {
}
}

const GardenTemplate: React.FC<PageProps<DataProps>> = ({ data: { garden }, location: { pathname }, children }) => {
const GardenTemplate: React.FC<PageProps<DataProps>> = ({ data: { garden }, children }) => {
const [hasShareApi, setHasShareApi] = React.useState(false)

React.useEffect(() => {
Expand All @@ -51,16 +51,9 @@ const GardenTemplate: React.FC<PageProps<DataProps>> = ({ data: { garden }, loca
<Spacer size="4" axis="vertical" />
<Box className={metaStyle} fontSize={[`sm`, `md`, null, null, `lg`]}>
<Text>
Created {garden.date} – Last Updated {garden.lastUpdated}
Created: {garden.date} – Last Updated: {garden.lastUpdated}
</Text>
<Box display="flex" flexWrap="wrap" justifyContent={[`flex-start`, null, `flex-end`]}>
{garden.tags.map((tag, index) => (
<React.Fragment key={tag}>
<Box as="span">{tag}</Box>
{index !== garden.tags.length - 1 && <Spacer axis="horizontal" size="2" />}
</React.Fragment>
))}
</Box>
<Text>Tags: {garden.tags.map((tag) => tag).join(`, `)}</Text>
<Tag colorScheme="green" style={{ justifySelf: `flex-start` }}>
<Link to="/garden">Digital Garden</Link>
</Tag>
Expand Down Expand Up @@ -123,44 +116,58 @@ const GardenTemplate: React.FC<PageProps<DataProps>> = ({ data: { garden }, loca

export default GardenTemplate

export const Head: HeadFC<DataProps> = ({ data: { garden } }) => (
<SEO
title={garden.title}
pathname={garden.slug}
description={garden.description ? garden.description : garden.excerpt}
image={garden.image}
>
<meta name="twitter:label1" value="Time To Read" />
<meta name="twitter:data1" value={`${garden.timeToRead} Minutes`} />
<meta name="twitter:label2" value="Tags" />
<meta name="twitter:data2" value={garden.tags.join(`, `)} />
<meta name="article:published_time" content={garden.seoDate} />
<meta name="article:modified_time" content={garden.seoLastUpdated} />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
article({
isGarden: true,
post: {
title: garden.title,
description: garden.description ? garden.description : garden.excerpt,
slug: garden.slug,
image: garden.image,
date: garden.seoDate,
lastUpdated: garden.seoLastUpdated,
year: garden.yearDate,
},
category: {
name: `Digital Garden`,
slug: `/garden`,
},
})
),
}}
/>
</SEO>
)
export const Head: HeadFC<DataProps> = ({ data: { garden } }) => {
// You can set image in frontmatter to overwrite the default image
// The OG Edge image should not be used in those cases
const hasDefaultOgImage = garden.image === site.defaultGardenOgImage

const ogURL = new URL(site.gardenOgEdge, site.url)
ogURL.searchParams.set(`title`, garden.title)
ogURL.searchParams.set(`lastUpdated`, garden.lastUpdated)
ogURL.searchParams.set(`tags`, garden.tags.join(`,`))

// The image link passed to <SEO /> has to be a relative path as it will prepend the site URL
const ogImage = hasDefaultOgImage ? `${ogURL.pathname}${ogURL.search}` : garden.image

return (
<SEO
title={garden.title}
pathname={garden.slug}
description={garden.description ? garden.description : garden.excerpt}
image={ogImage}
>
<meta name="twitter:label1" value="Time To Read" />
<meta name="twitter:data1" value={`${garden.timeToRead} Minutes`} />
<meta name="twitter:label2" value="Tags" />
<meta name="twitter:data2" value={garden.tags.join(`, `)} />
<meta name="article:published_time" content={garden.seoDate} />
<meta name="article:modified_time" content={garden.seoLastUpdated} />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
article({
isGarden: true,
post: {
title: garden.title,
description: garden.description ? garden.description : garden.excerpt,
slug: garden.slug,
image: ogImage,
date: garden.seoDate,
lastUpdated: garden.seoLastUpdated,
year: garden.yearDate,
},
category: {
name: `Digital Garden`,
slug: `/garden`,
},
})
),
}}
/>
</SEO>
)
}

export const query = graphql`
query ($id: String!) {
Expand Down
2 changes: 1 addition & 1 deletion src/templates/tutorial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const TutorialTemplate: React.FC<PageProps<WritingViewDataProps>> = ({
<Spacer size="4" axis="vertical" />
<Box display="flex" justifyContent="space-between" flexDirection={[`column`, null, null, `row`]}>
<Text marginBottom="2">
Created {post.date} – Last Updated {post.lastUpdated}
Created: {post.date} – Last Updated: {post.lastUpdated}
</Text>
<Tag marginBottom="2" colorScheme={tagColorSwitch(post.category.name)} style={{ alignSelf: `flex-start` }}>
{post.category.name}
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3482,6 +3482,13 @@ __metadata:
languageName: node
linkType: hard

"@netlify/edge-functions@npm:^2.2.0":
version: 2.2.0
resolution: "@netlify/edge-functions@npm:2.2.0"
checksum: 14c4d72c15737a8c4b49d5306040b8ccebba706b9e07b06de08970d3bbc838f978759bef9047d43337606b46c08af5bd0fb62a78883bd4ac0ee197e28b249080
languageName: node
linkType: hard

"@netlify/functions@npm:^1.6.0":
version: 1.6.0
resolution: "@netlify/functions@npm:1.6.0"
Expand Down Expand Up @@ -16330,6 +16337,7 @@ __metadata:
"@lekoarts/gatsby-source-flickr": ^0.1.1
"@lekoarts/rehype-meta-as-attributes": ^3.0.0
"@mdx-js/react": ^2.3.0
"@netlify/edge-functions": ^2.2.0
"@playwright/test": ^1.37.1
"@react-aria/button": ^3.8.1
"@react-stately/toggle": ^3.6.1
Expand Down
Loading