Skip to content

Commit

Permalink
✨ og image generator
Browse files Browse the repository at this point in the history
  • Loading branch information
SilentDepth committed Apr 11, 2023
1 parent 289f2a3 commit 8f407b9
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 15 deletions.
2 changes: 1 addition & 1 deletion assets/config-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
showAbout: true,

path: '',
ogImageGenerateURL: 'https://og-image-craigary.vercel.app',
ogImageGenerateURL: '',
seo: {
keywords: ['Blog', 'Website', 'Notion'],
googleSiteVerification: '',
Expand Down
Binary file added assets/fonts/NotoSansSC-Bold.otf
Binary file not shown.
Binary file added assets/fonts/NotoSerifSC-Bold.otf
Binary file not shown.
23 changes: 11 additions & 12 deletions components/Container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ const Container = ({ children, layout, fullWidth, ...customMeta }) => {
type: 'website',
...customMeta,
}

const ogImageService = BLOG.ogImageGenerateURL && (
BLOG.ogImageGenerateURL === 'https://og-image-craigary.vercel.app'
// Backward compatibility
// TODO: remove in v2.0
? `${BLOG.ogImageGenerateURL}/${encodeURIComponent(meta.title)}.png?theme=dark&md=1&fontSize=125px&images=https%3A%2F%2Fnobelium.vercel.app%2Flogo-for-dark-bg.svg`
: BLOG.ogImageGenerateURL.replace(/\$title\b/, meta.title)
)

return (
<div>
<Head>
Expand All @@ -39,22 +48,12 @@ const Container = ({ children, layout, fullWidth, ...customMeta }) => {
property="og:url"
content={meta.slug ? `${url}/${meta.slug}` : url}
/>
<meta
property="og:image"
content={`${BLOG.ogImageGenerateURL}/${encodeURIComponent(
meta.title,
)}.png?theme=dark&md=1&fontSize=125px&images=https%3A%2F%2Fnobelium.vercel.app%2Flogo-for-dark-bg.svg`}
/>
{ogImageService && <meta property="og:image" content={ogImageService}/>}
<meta property="og:type" content={meta.type}/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:description" content={meta.description}/>
<meta name="twitter:title" content={meta.title}/>
<meta
name="twitter:image"
content={`${BLOG.ogImageGenerateURL}/${encodeURIComponent(
meta.title,
)}.png?theme=dark&md=1&fontSize=125px&images=https%3A%2F%2Fnobelium.vercel.app%2Flogo-for-dark-bg.svg`}
/>
{ogImageService && <meta name="twitter:image" content={ogImageService}/>}
{meta.type === 'article' && (
<>
<meta
Expand Down
4 changes: 3 additions & 1 deletion lib/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ const applyDefaults = createDefu((obj, key, value) => {
}
})

const raw = JSON.parse(fs.readFileSync(resolve(process.cwd(), 'osmium-config.json'), 'utf-8'))
const config: Osmium.Config = applyDefaults(
JSON.parse(fs.readFileSync(resolve(process.cwd(), 'osmium-config.json'), 'utf-8')),
raw,
{
path: '/',
ogImageGenerateURL: `${raw.link}/api/og-image?title=$title`,
},
)

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@
"notion-client": "^6.16.0",
"notion-utils": "^6.16.0",
"ofetch": "^1.0.1",
"ohash": "^1.0.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-cusdis": "^2.1.3",
"react-dom": "^18.2.0",
"react-notion-x": "^6.16.0",
"react-tweet-embed": "^2.0.0",
"react-use": "^17.4.0",
"satori": "^0.4.7",
"sharp": "^0.32.0",
"use-ackee": "^3.0.1"
},
Expand Down
102 changes: 102 additions & 0 deletions pages/api/og-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import fs from 'node:fs'
import { extname, resolve } from 'node:path'
import { createHash } from 'node:crypto'
import type { NextApiHandler } from 'next'
import satori from 'satori'
import { sha256 } from 'ohash'
import sharp from 'sharp'
import { clientConfig } from '@/lib/server/config'

const NOTO_SANS_SC = fs.readFileSync(resolve(process.cwd(), 'assets/fonts/NotoSansSC-Bold.otf'))
const NOTO_SERIF_SC = fs.readFileSync(resolve(process.cwd(), 'assets/fonts/NotoSerifSC-Bold.otf'))
const fonts = [
clientConfig.font === 'serif'
? { name: 'Noto Serif SC', data: NOTO_SERIF_SC }
: { name: 'Noto Sans SC', data: NOTO_SANS_SC },
]

const WIDTH = 1200
const HEIGHT = 600

type Params = {
title: string
}

export default (async function handler (req, res) {
const { title } = req.query as Partial<Params>

if (!title) return res.status(204).end()

const hash = sha256(title)
const logo = await resolveLogo()
const { author, email } = clientConfig
const emailHash = email && createHash('md5').update(email).digest('hex').trim().toLowerCase()

const svg = await satori(
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
backgroundColor: '#000',
backgroundImage: `linear-gradient(153.4349488229deg, #${hash.slice(0, 6)}80, #${hash.slice(6, 12)}80)`,
backgroundSize: '100% 162%',
}}>
{logo && (
<img
src={logo}
width={HEIGHT / 10}
height={HEIGHT / 10}
style={{ position: 'absolute', top: '10vh', right: '10vw', boxShadow: '1px 2px 4px #0004' }}
/>
)}
{author && (
<div style={{
position: 'absolute',
bottom: '10vh',
left: '10vw',
display: 'flex',
alignItems: 'center',
fontSize: '40px',
lineHeight: '1.2',
color: '#fff',
textShadow: '1px 2px 4px #0004',
}}>
<img
src={`https://gravatar.com/avatar/${emailHash}`}
width={48}
height={48}
style={{ marginRight: '0.25em', borderRadius: 9999, boxShadow: '1px 2px 4px #0004' }}
/>
<div>{author}</div>
</div>
)}
<div style={{
maxWidth: '80vw',
fontSize: '60px',
fontWeight: 'bold',
textAlign: 'center',
color: '#fff',
textShadow: '1px 2px 4px #0004',
}}>{title}</div>
</div>,
{ width: WIDTH, height: HEIGHT, fonts },
)
const png = await sharp(Buffer.from(svg, 'ascii')).toFormat('png').toBuffer()
res.setHeader('Content-Type', 'image/png')
res.send(png)
} as NextApiHandler)

async function resolveLogo () {
const { logo } = clientConfig

if (!logo) return

const raw = fs.readFileSync(resolve(process.cwd(), 'public', logo))
const format = extname(logo).slice(1)
return format === 'svg'
// Nested SVG will not be rendered due to tech limitations
? `data:image/png;base64,${(await sharp(raw).toFormat('png').toBuffer()).toString('base64')}`
: `data:image/${format};base64,${raw.toString('base64')}`
}
Loading

0 comments on commit 8f407b9

Please sign in to comment.