Skip to content

Commit

Permalink
✨ Generate dynamic social previews
Browse files Browse the repository at this point in the history
  • Loading branch information
BetaHuhn committed Aug 19, 2021
1 parent d4ccf06 commit 0aaac65
Show file tree
Hide file tree
Showing 13 changed files with 1,451 additions and 403 deletions.
1,437 changes: 1,240 additions & 197 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "webcrate",
"version": "0.10.2",
"version": "0.11.0",
"private": true,
"scripts": {
"dev:server": "tsc-watch -p ./server/tsconfig.json --onSuccess \"node ./build/index.js\"",
Expand All @@ -9,7 +9,9 @@
"start:test": "node test/run.js",
"lint:nuxt": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint:server": "eslint ./server/ --ext .ts",
"copy-templates": "cp ./server/service/image/template.ejs ./build/service/image && cp ./server/service/image/NotoColorEmoji.ttf ./build/service/image",
"clean": "rimraf dist build .nuxt",
"postbuild:server": "npm run copy-templates",
"prebuild": "npm run clean",
"predev": "npm run clean",
"predeploy": "npm run build",
Expand All @@ -35,6 +37,7 @@
"dependencies": {
"body-parser": "^1.19.0",
"cheerio": "^1.0.0-rc.10",
"chrome-aws-lambda": "^10.1.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"deta": "^1.0.0",
Expand All @@ -43,6 +46,7 @@
"express": "^4.17.1",
"metadata-scraper": "^0.1.1",
"nanoid": "^3.1.23",
"puppeteer-core": "^10.2.0",
"running-at": "^0.3.21",
"signale": "^1.4.0"
},
Expand All @@ -57,13 +61,15 @@
"@nuxtjs/pwa": "^3.3.5",
"@types/body-parser": "^1.19.1",
"@types/cors": "^2.8.12",
"@types/ejs": "^3.1.0",
"@types/express": "^4.17.13",
"@types/node": "^15.14.2",
"@types/signale": "^1.4.2",
"babel-eslint": "^10.1.0",
"core-js": "^3.15.2",
"debounce": "^1.2.1",
"drkmd-js": "^1.0.8",
"ejs-serve": "^1.2.28",
"eslint": "^7.30.0",
"eslint-plugin-nuxt": "^2.0.0",
"eslint-plugin-vue": "^7.13.0",
Expand Down
8 changes: 6 additions & 2 deletions server/middleware/index.ts
Expand Up @@ -7,6 +7,7 @@ import log from '../utils/log'
import { messages } from '../utils/status'
import { isSetup } from '../utils/isSetup'
import emojis from '../utils/emojis'
import { domain } from '../utils/variables'

import { Crate } from '../models/crate'
import { Stat } from '../models/stats'
Expand Down Expand Up @@ -62,11 +63,14 @@ export async function renderMetaTags(req: express.Request, res: express.Response
// Replace title
$('title').text(title)
$(`meta[name='title']`).attr('content', title)
$(`meta[name='og:title']`).attr('content', title)
$(`meta[property='og:title']`).attr('content', title)

// Replace description
$(`meta[name='description']`).attr('content', description)
$(`meta[name='og:description']`).attr('content', description)
$(`meta[property='og:description']`).attr('content', description)

// Replace image
$(`meta[property='og:image']`).attr('content', `https://${ domain }/img/preview/${ crate.id }`)

// Render HTML
return res.send($.html())
Expand Down
36 changes: 34 additions & 2 deletions server/router/image.ts
Expand Up @@ -2,10 +2,42 @@ import express from 'express'
import got from 'got'

import { Link } from '../models/link'
import { Crate } from '../models/crate'

import { generateSocialImage } from '../service/image'

import log from '../utils/log'
import emojis from '../utils/emojis'

export const router = express.Router()

router.get('/preview/:id', async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
const id = req.params.id as string
if (!id) {
return res.fail(400, 'Missing crate id')
}

const crate = await Crate.findById(id)
if (!crate) {
return res.fail(404, 'link not found')
}

log.debug(`Generating preview for crate ${ crate.id }`)

const image = await generateSocialImage(crate.name, crate.description, emojis[crate.icon])

/* const sec = 60 * 5 // 5 minutes
res.header('Cache-Control', `public, max-age=${ sec }`) */

res.header('Content-Type', 'image/png')
res.send(image)
} catch (err) {
console.log(err)
return next(err)
}
})

router.get('/:id', async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
const id = req.params.id as string
Expand Down Expand Up @@ -51,10 +83,10 @@ router.get('/:id', async (req: express.Request, res: express.Response, next: exp

// Instruct browser to cache image
if (type === 'image') {
const sec = 60 * 2 // 2 hours
const sec = 60 * 60 * 2 // 2 hours
res.header('Cache-Control', `public, max-age=${ sec }`)
} else if (type === 'icon') {
const sec = 60 * 24 * 7 * 4 // 1 month
const sec = 60 * 60 * 24 * 7 * 4 // 1 month
res.header('Cache-Control', `public, max-age=${ sec }`)
}

Expand Down
32 changes: 0 additions & 32 deletions server/run.ts

This file was deleted.

Binary file added server/service/image/NotoColorEmoji.ttf
Binary file not shown.
61 changes: 61 additions & 0 deletions server/service/image/index.ts
@@ -0,0 +1,61 @@

import path from 'path'
import chromium from 'chrome-aws-lambda'
import ejs from 'ejs'

import { isDev, domain } from '../../utils/variables'

// Use different options depending on the environment
const getOptions = async () => {
if (isDev) {
// Path to local chrome executable on different platforms
const chromeExecutables: { [key: string]: string } = {
linux: '/usr/bin/chromium-browser',
win32: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
darwin: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
}

return {
args: [ '--no-sandbox' ],
executablePath: chromeExecutables[process.platform] || chromeExecutables.linux,
headless: true
}
}

// Else, use the path of chrome-aws-lambda and its args
return {
args: chromium.args,
executablePath: await chromium.executablePath,
headless: chromium.headless
}
}

export const generateSocialImage = async (title: string, description: string, icon: string) => {
// Inflate chrome and get options
const opts = await getOptions()

// Load noto-emoji font
await chromium.font(path.join(__dirname, 'NotoColorEmoji.ttf'))

// Launch browser
const browser = await chromium.puppeteer.launch(opts)

// Create new page
const page = await browser.newPage()
await page.setViewport({
width: 1200,
height: 628
})

// Load the template into the page
const data = { title, description, icon, domain }
const html = await ejs.renderFile(path.join(__dirname, 'template.ejs'), data)
await page.setContent(html, { waitUntil: 'domcontentloaded' })

// Take a screenshot of the page
const image = await page.screenshot()

await browser.close()

return image
}
98 changes: 98 additions & 0 deletions server/service/image/template.ejs
@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<main id="wrapper">
<div class="content">
<h1 class="title"><%= icon %> <%= title %></h1>
<p class="description"><%= description %></p>
<p class="domain"><%= domain %></p>
</div>
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53">
<defs>
<linearGradient id="linear-gradient" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f5be6b"/>
<stop offset="1" stop-color="#df9930"/>
</linearGradient>
</defs>
<g id="Rechteck_532" data-name="Rechteck 532" stroke="#f5be6b" stroke-width="10" fill="url(#linear-gradient)">
<rect width="53" height="53" rx="8" stroke="none"/>
<rect x="5" y="5" width="43" height="43" rx="3" fill="none"/>
</g>
</svg>
<span>WebCrate</span>
</div>
</main>

<style>
:root {
/* Colors */
--background: #fff;
--background-2nd: #F7F6F4;
--grey: #E8E8E8;
--grey-light: #EEEEEE;
--grey-2nd: #cac9c9;
--text: #474440;
--text-light: #7B7B7B;
--text-dark:#242221;
--accent: #F5BE6B;
--accent-dark: #efa83d;
--red: rgb(197, 63, 63);
--red-light: rgb(230, 104, 104);
--red-dark: rgb(146, 40, 40);
--green: rgb(123, 217, 147);
/* Values */
--border-radius: 8px;
--click-scale-factor: 0.95;
}
#wrapper {
font-family: "Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
width: 1200px;
height: 628px;
background: var(--background-2nd);
padding: 10rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
font-size: 20px;
}
.title {
margin-bottom: 0.5rem;
}
.description {
color: var(--text-light);
}
.domain {
color: var(--accent-dark);
}
.logo {
position: absolute;
bottom: 5rem;
right: 10rem;
display: flex;
align-items: center;
}
.logo span {
font-size: 2rem;
font-weight: 600;
margin-left: 1rem;
color: #474440;
}
</style>
</body>
</html>
5 changes: 5 additions & 0 deletions server/utils/variables.ts
@@ -0,0 +1,5 @@

export const isDev = process.env.DETA_RUNTIME !== 'True' && process.env.DETA_RUNTIME !== 'true'
export const isSpace = process.env.DETA_SPACE_APP === 'True' || process.env.DETA_SPACE_APP === 'true'
export const subdomain = process.env.DETA_PATH
export const domain = isDev ? 'localhost:3000' : `${ subdomain }.${ isSpace ? 'deta.app' : 'deta.dev' }`
31 changes: 0 additions & 31 deletions test/nuxtSSRTest.js

This file was deleted.

20 changes: 0 additions & 20 deletions test/run.js

This file was deleted.

0 comments on commit 0aaac65

Please sign in to comment.