Skip to content
This repository has been archived by the owner on Jan 2, 2025. It is now read-only.

Commit

Permalink
OG Images for Groups, Doc content
Browse files Browse the repository at this point in the history
  • Loading branch information
ericvicenti committed Oct 17, 2023
1 parent 24ae8f2 commit 0678d97
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 80 deletions.
Binary file added frontend/apps/site/font/Georgia Bold Italic.ttf
Binary file not shown.
Binary file added frontend/apps/site/font/Georgia Bold.ttf
Binary file not shown.
Binary file added frontend/apps/site/font/Georgia Italic.ttf
Binary file not shown.
Binary file added frontend/apps/site/font/Georgia.ttf
Binary file not shown.
12 changes: 12 additions & 0 deletions frontend/apps/site/head.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {OG_IMAGE_SIZE} from 'server/content-image-meta'

export function OGImageMeta({url}: {url: string}) {
return (
<>
<meta property="og:image" content={url} />
<meta property="og:image:width" content={`${OG_IMAGE_SIZE.width}`} />
<meta property="og:image:height" content={`${OG_IMAGE_SIZE.height}`} />
<meta property="og:image:type" content="image/png" />
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,149 @@ import satori from 'satori'
import {readFileSync} from 'fs'
import {join} from 'path'
import {serverHelpers} from 'server/ssr-helpers'
import {createHmId} from '@mintter/shared'
import {OG_IMAGE_SIZE} from 'server/content-image-meta'
import {
HMBlockChildrenType,
InlineContent,
createHmId,
serverBlockToEditorInline,
} from '@mintter/shared'
import {
HMAccount,
HMBlock,
HMBlockNode,
HMGroup,
HMPublication,
} from 'server/json-hm'
import {ReactElement} from 'react'

const robotoBoldPath = join(process.cwd(), 'font/Roboto-Bold.ttf')
const robotoArrayBuffer = readFileSync(robotoBoldPath)
const robotoRegularPath = join(process.cwd(), 'font/Roboto-Regular.ttf')
const robotoRegularArrayBuffer = readFileSync(robotoRegularPath)
function loadFont(fileName: string) {
const path = join(process.cwd(), 'font', fileName)
return readFileSync(path)
}

const AVATAR_SIZE = 100

const avatarLayout: React.CSSProperties = {
margin: 10,
}

export default async function mediaHandler(
req: NextApiRequest,
res: NextApiResponse,
) {
const {entityType, entityId, versionId} = req.query
if (entityType !== 'd') throw new Error('Only supports Document types')
if (!entityId) throw new Error('Missing entityId')
if (!versionId) throw new Error('Missing versionId')
const helpers = serverHelpers({})
const documentId = createHmId('d', String(entityId))
const pub = await helpers.publication.get.fetch({
documentId,
versionId: String(versionId),
})
if (!pub?.publication?.document) throw new Error('Publication not found')
const {publication} = pub
const editors = await Promise.all(
(publication.document?.editors || []).map(async (editorId) => {
return helpers.account.get.fetch({accountId: editorId})
}),
function InlineContent({
content,
fontWeight = 'normal',
fontSize = 24,
}: {
content: InlineContent[]
fontWeight?: 'bold' | 'normal'
fontSize?: number
}) {
return (
<span style={{fontSize: 24}}>
{content.map((item, index) => {
// if (item.type === 'link')
// return (
// <span key={index} style={{color: 'blue'}}>
// <InlineContent content={item.content} />
// </span>
// )
if (item.type === 'text') {
let content: ReactElement = <>{item.text}</>
if (item.styles.bold) content = <b>{content}</b>
if (item.styles.italic) content = <i>{content}</i>
return content
}
return
})}
</span>
)
}

function ParagraphBlockDisplay({
block,
childrenType,
}: {
block: HMBlock
childrenType: HMBlockChildrenType
}) {
const inlineContent = serverBlockToEditorInline(block)
return (
<div
style={{
display: 'flex',
marginTop: 8,
}}
>
<InlineContent content={inlineContent} fontSize={24} />
</div>
)
}

function HeadingBlockDisplay({
block,
childrenType,
}: {
block: HMBlock
childrenType: HMBlockChildrenType
}) {
const inlineContent = serverBlockToEditorInline(block)
return (
<div
style={{
display: 'flex',
marginTop: 8,
}}
>
<InlineContent content={inlineContent} fontSize={64} fontWeight="bold" />
</div>
)
}

function BlockDisplay({
block,
childrenType,
}: {
block: HMBlock
childrenType: HMBlockChildrenType
}) {
if (block.type === 'paragraph')
return <ParagraphBlockDisplay block={block} childrenType={childrenType} />
if (block.type === 'heading')
return <HeadingBlockDisplay block={block} childrenType={childrenType} />

if (block.type === 'image') return <span>{block.ref}</span>

return null
}

function BlockNodeDisplay({blockNode}: {blockNode: HMBlockNode}) {
return (
<div style={{display: 'flex'}}>
{blockNode.block && (
<BlockDisplay
block={blockNode.block}
childrenType={blockNode.childrenType}
/>
)}
<div style={{display: 'flex', marginLeft: 20, flexDirection: 'column'}}>
{blockNode.children?.map((child) => {
if (!child.block) return null
return <BlockNodeDisplay key={child.block.id} blockNode={child} />
})}
</div>
</div>
)
console.log(pub)
console.log(editors)
const svg = await satori(
}

function TitleMembersCard({
title,
accounts,
children,
}: {
title: string
accounts: {account: HMAccount | null}[]
children: React.ReactNode
}) {
return (
<div
style={{
color: 'black',
Expand All @@ -51,10 +156,13 @@ export default async function mediaHandler(
width: '100%',
}}
>
<div style={{padding: 60, display: 'flex'}}>
<span style={{fontSize: 64, fontWeight: 'bold'}}>
{publication.document?.title}
</span>
<div style={{padding: 60, display: 'flex', flexDirection: 'column'}}>
{title && (
<span style={{fontSize: 64, fontWeight: 'bold', marginBottom: 100}}>
{title}
</span>
)}
{children}
</div>
<div
style={{
Expand All @@ -66,6 +174,7 @@ export default async function mediaHandler(
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
background: 'linear-gradient(#ffffff11, #ffffff11, white)',
}}
>
<div
Expand All @@ -75,19 +184,30 @@ export default async function mediaHandler(
padding: 40,
}}
>
{editors.map((editor) => {
const account = editor.account
{accounts.map((item) => {
const account = item.account
const accountLetter =
account?.profile?.alias?.slice(0, 1) ||
account?.id?.slice(4, 5) ||
''
if (!account?.profile?.avatar)
return (
<div
style={{
backgroundColor: '#aac2bd', // mintty, yum!
display: 'flex',
width: AVATAR_SIZE,
height: AVATAR_SIZE,
borderRadius: AVATAR_SIZE / 2,
justifyContent: 'center',
alignItems: 'center',
...avatarLayout,
}}
/>
>
<span style={{fontSize: 50, position: 'relative', bottom: 6}}>
{accountLetter}
</span>
</div>
)
const src = `${process.env.GRPC_HOST}/ipfs/${account.profile.avatar}`
return (
Expand All @@ -106,26 +226,116 @@ export default async function mediaHandler(
})}
</div>
</div>
</div>,
{
width: OG_IMAGE_SIZE.width,
height: OG_IMAGE_SIZE.height,
fonts: [
{
name: 'Roboto',
data: robotoArrayBuffer,
weight: 700,
style: 'normal',
},
{
name: 'Roboto',
data: robotoRegularArrayBuffer,
weight: 400,
style: 'normal',
},
],
},
</div>
)
}

function GroupCard({
group,
members,
}: {
group: HMGroup
members: {account: HMAccount | null}[]
}) {
return (
<TitleMembersCard title={group.title || ''} accounts={members}>
<span>{group.description}</span>
</TitleMembersCard>
)
}

function PublicationCard({
publication,
editors,
}: {
publication: HMPublication
editors: {account: HMAccount | null}[]
}) {
return (
<TitleMembersCard
title={publication.document?.title || ''}
accounts={editors}
>
{publication.document?.children?.map((child, index) => {
if (index === 0) return null // hide title because we have already shown it
return (
<BlockNodeDisplay
key={child.block.id}
blockNode={child}
index={index}
/>
)
})}
</TitleMembersCard>
)
}

export default async function mediaHandler(
req: NextApiRequest,
res: NextApiResponse,
) {
const {entityType, entityId, versionId} = req.query
if (!entityId) throw new Error('Missing entityId')
if (!versionId) throw new Error('Missing versionId')
const helpers = serverHelpers({})
let content: null | JSX.Element = null
if (entityType === 'd') {
const documentId = createHmId(entityType, String(entityId))
const pub = await helpers.publication.get.fetch({
documentId,
versionId: String(versionId),
})
if (!pub?.publication?.document) throw new Error('Publication not found')
const {publication} = pub
const editors = await Promise.all(
(publication.document?.editors || []).map(async (editorId) => {
return await helpers.account.get.fetch({accountId: editorId})
}),
)
content = <PublicationCard publication={publication} editors={editors} />
} else if (entityType === 'g') {
const groupId = createHmId(entityType, String(entityId))
const group = await helpers.group.get.fetch({groupId})
const groupMembers = await helpers.group.listMembers.fetch({groupId})
const groupMembersAccounts = await Promise.all(
groupMembers.map(async (membership) => {
return await helpers.account.get.fetch({accountId: membership.account})
}),
)

content = <GroupCard group={group.group} members={groupMembersAccounts} />
}
if (!content) throw new Error('Invalid content')
const svg = await satori(content, {
width: OG_IMAGE_SIZE.width,
height: OG_IMAGE_SIZE.height,
fonts: [
{
name: 'Georgia',
data: loadFont('Georgia.ttf'),
weight: 400,
style: 'normal',
},
{
name: 'Georgia',
data: loadFont('Georgia Bold.ttf'),
weight: 700,
style: 'normal',
},
{
name: 'Georgia',
data: loadFont('Georgia Italic.ttf'),
weight: 400,
style: 'italic',
},
{
name: 'Georgia',
data: loadFont('Georgia Bold Italic.ttf'),
weight: 700,
style: 'italic',
},
],
})
const png = await new Promise<Buffer>((resolve, reject) =>
svg2img(svg, function (error, buffer) {
if (error) reject(error)
Expand Down
Loading

0 comments on commit 0678d97

Please sign in to comment.