Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Version 2.0 #154

Merged
merged 8 commits into from
Nov 21, 2022
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
7 changes: 7 additions & 0 deletions .changeset/wise-kings-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'socialify': major
---

Satori and @vercel/og for image generation
Use vercel edge functions
Upgrade to Tailwind CSS and daisyUI
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ yarn-error.log*
.swc

.idea
.vercel
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ __generated__/
package.json
build/
.next/
.vercel
google-fonts.css
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Want your project to stand out? **Socialify** helps you showcase your project to

It includes a ton of options including custom logo, description, badges, and many fonts and background patterns to choose from.

Join [![thousands of repositories](https://socialify-usage-count-pclo66uxqtfh.runkit.sh/)](https://github.com/search?o=desc&q=%22socialify.git.ci%22&s=indexed&type=Code) today!
Join [![thousands of repositories](https://socialify.git.ci/api/stats.svg)](https://github.com/search?o=desc&q=%22socialify.git.ci%22&s=indexed&type=Code) today!

## Usage

Expand Down
23 changes: 23 additions & 0 deletions common/font.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @jest-environment node
*/

import { SatoriOptions } from 'satori'
import { getFont } from './renderCard'
import { Font } from './types/configType'

describe('Verify Fonts', () => {
for (const item in Font) {
const fontName = Font[item as keyof typeof Font]

for (const weight of [200, 400, 500]) {
test(`Check font '${fontName}', ${weight} exists`, async () => {
const { data } = await getFont(
fontName,
weight as SatoriOptions['fonts'][0]['weight']
)
expect(data).toBeTruthy()
})
}
}
})
15 changes: 0 additions & 15 deletions common/fonts/font.test.ts

This file was deleted.

8 changes: 0 additions & 8 deletions common/fonts/fonts.json

This file was deleted.

108 changes: 0 additions & 108 deletions common/fonts/google-fonts.css

This file was deleted.

25 changes: 21 additions & 4 deletions common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const getSimpleIconsImageURI = function (language: string, theme: Theme) {
return `data:image/svg+xml,${encodeURIComponent(iconSvg)}`
}

const getHeroPattern = (pattern: Pattern, theme: Theme): string => {
const getHeroPattern = (pattern: Pattern, theme: Theme) => {
const PATTERN_FUNCTIONS_MAPPING: { [key: string]: any } = {
[Pattern.signal]: signal,
[Pattern.charlieBrown]: charlieBrown,
Expand All @@ -118,16 +118,33 @@ const getHeroPattern = (pattern: Pattern, theme: Theme): string => {
const patternFunction = PATTERN_FUNCTIONS_MAPPING[pattern]
const themedBackgroundColor = theme === Theme.dark ? '#000' : '#fff'

if (!patternFunction) return themedBackgroundColor
if (!patternFunction) {
return {
backgroundColor: themedBackgroundColor
}
}

const darkThemeArgs = ['#eaeaea', 0.2]
const lightThemeArgs = ['#eaeaea', 0.6]
const patternImageUrl = patternFunction.apply(
let patternImageUrl = patternFunction.apply(
null,
theme === Theme.dark ? darkThemeArgs : lightThemeArgs
)

return `${themedBackgroundColor} ${patternImageUrl}`
const width = patternImageUrl.match(/width%3D%22(\d+)%22/)?.[1]
const height = patternImageUrl.match(/height%3D%22(\d+)%22/)?.[1]

// Satori has issues with quotes around data uris, therefore we are stripping the quotes
patternImageUrl = patternImageUrl
.replace(/^url\('/, 'url(')
.replace(/'\)$/, ')')

return {
backgroundColor: themedBackgroundColor,
backgroundImage: patternImageUrl,
backgroundSize: `${width}px ${height}px`,
backgroundRepeat: 'repeat'
}
}

let webpSupport: boolean | undefined
Expand Down
126 changes: 47 additions & 79 deletions common/renderCard.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,64 @@
import { readFileSync } from 'fs'
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { createStyleRegistry, StyleRegistry } from 'styled-jsx'

import Card from '../src/components/preview/card'

import { SatoriOptions } from 'satori'
import { Font } from './types/configType'
import QueryType from './types/queryType'

import { mergeConfig } from './configHelper'
import { getRepoDetails, RepoQueryResponse } from './github/repoQuery'
import { getRepoDetails } from './github/repoQuery'
import getTwemojiMap from './twemoji'

const cwd = process.cwd()
export async function getFont(
font: Font,
weight: SatoriOptions['fonts'][0]['weight']
): Promise<SatoriOptions['fonts'][0]> {
const fontSlug = font.replace(/\s/g, '-').toLowerCase()
const cdnUrl = `https://cdn.jsdelivr.net/npm/@fontsource/${fontSlug}/files/${fontSlug}-all-${weight}-normal.woff`

const getGoogleFontCSS = (font: Font): string => {
const googleFontsCSS = readFileSync(
`${cwd}/common/fonts/google-fonts.css`
).toString('utf-8')
return {
name: font,
data: await fetch(cdnUrl).then((response) => {
if (response.ok) {
return response.arrayBuffer()
}
throw new Error('Failed to fetch font')
}),
weight,
style: 'normal'
}
}

return googleFontsCSS
.replace(/([{;])\n*\s*/g, '$1')
.split('\n')
.filter((f) => f.startsWith(`@font-face {font-family: '${font}'`))
.join('\n')
export function getFonts(font: Font) {
return Promise.all([
getFont(Font.jost, 400),
getFont(font, 200),
getFont(font, 400),
getFont(font, 500)
])
}

const getBase64Image = async (imgUrl: string) => {
const imagePromise = new Promise<string>((resolve) => {
fetch(imgUrl)
.then(async (response) => {
const arrayBuffer = await response.arrayBuffer()
const base64Url =
'data:' +
((response.headers.get('content-type') || 'image/png') +
';base64,' +
Buffer.from(arrayBuffer).toString('base64'))
resolve(base64Url)
})
.catch(() => {
resolve('')
})
})
const timeoutPromise = new Promise<string>((resolve) => {
setTimeout(() => {
resolve('')
}, 1500)
})
return Promise.race([timeoutPromise, imagePromise])
export async function getEmojiSVG(code: string) {
return (
await fetch(`https://twemoji.maxcdn.com/v/13.1.0/svg/${code}.svg`)
).text()
}

const renderCard = async (query: QueryType) => {
const responsePromise = getRepoDetails(query._owner, query._name)
const promises: Promise<RepoQueryResponse | string>[] = [responsePromise]
export async function getGraphemeImages(description: string = '') {
const emojiCodes = getTwemojiMap(description)
const emojis = await Promise.all(Object.values(emojiCodes).map(getEmojiSVG))
const graphemeImages = Object.fromEntries(
Object.entries(emojiCodes).map(([key], index) => [
key,
`data:image/svg+xml;base64,` + btoa(emojis[index])
])
)

if (query.logo) {
if (query.logo.toLowerCase().startsWith('http')) {
const imagePromise = getBase64Image(query.logo)
promises.push(imagePromise)
}
}
return graphemeImages
}

export async function getCardConfig(query: QueryType) {
const { repository } = await getRepoDetails(query._owner, query._name)

const responses = await Promise.all(promises)
const { repository } = responses[0] as RepoQueryResponse
if (responses.length > 1) {
const imageUrl = responses[1] as string
Object.assign(query, { logo: imageUrl })
}
const config = mergeConfig(repository, query)

if (!config) throw Error('Configuration failed to generate')

const registry = createStyleRegistry()
// eslint-disable-next-line react/no-children-prop
const cardComponent = React.createElement(StyleRegistry, {
registry,
children: React.createElement(Card, config)
})
const cardHTMLMarkup = ReactDOMServer.renderToStaticMarkup(cardComponent)
const styles = registry.styles() // access styles
const stylesHTMLMarkup = ReactDOMServer.renderToStaticMarkup(
React.createElement(React.Fragment, {}, styles)
)

return cardHTMLMarkup.replace(
'</foreignObject>',
`
${stylesHTMLMarkup}
</foreignObject>
<defs><style type="text/css">
${getGoogleFontCSS(config.font)}
</style></defs>`
)
return config
}

export default renderCard
21 changes: 21 additions & 0 deletions common/renderPNG.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ImageResponse } from '@vercel/og'
import Card from '../src/components/preview/card'
import { getCardConfig, getFonts } from './renderCard'
import QueryType from './types/queryType'

const renderCardPNG = async (
query: QueryType,
opts: { headers?: Record<string, string> } = {}
) => {
const config = await getCardConfig(query)

return new ImageResponse(<Card {...config} />, {
width: 1280,
height: 640,
fonts: await getFonts(config.font),
emoji: 'twemoji',
...opts
})
}

export default renderCardPNG
26 changes: 26 additions & 0 deletions common/renderSVG.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// @ts-ignore
import satori, { init as initSatori } from 'satori/wasm'
// @ts-ignore
import initYoga from 'yoga-wasm-web'
// @ts-ignore
import yogaWasm from '../public/yoga.wasm?module'

import Card from '../src/components/preview/card'
import { getCardConfig, getFonts, getGraphemeImages } from './renderCard'
import QueryType from './types/queryType'

const renderCardSVG = async (query: QueryType) => {
const yoga = await initYoga(yogaWasm)
initSatori(yoga)

const config = await getCardConfig(query)

return satori(<Card {...config} />, {
width: 1280,
height: 640,
fonts: await getFonts(config.font),
graphemeImages: await getGraphemeImages(config.description?.value)
})
}

export default renderCardSVG
Loading