Dynamically generate Opengraph images for each market (#21)
* Start customizing opengraph image * Fix cover image size to 1200x630 * Design a text-based, dynamic preview card * Load designed html into template.ts Remove twemoji for now too * Take in params to configure Manifold card * Switch back to hardcoded png for default case
This commit is contained in:
parent
179fa8c608
commit
ed37d93868
|
@ -19,7 +19,7 @@ export async function getScreenshot(
|
||||||
isDev: boolean
|
isDev: boolean
|
||||||
) {
|
) {
|
||||||
const page = await getPage(isDev);
|
const page = await getPage(isDev);
|
||||||
await page.setViewport({ width: 2048, height: 1170 });
|
await page.setViewport({ width: 1200, height: 630 });
|
||||||
await page.setContent(html);
|
await page.setContent(html);
|
||||||
const file = await page.screenshot({ type });
|
const file = await page.screenshot({ type });
|
||||||
return file;
|
return file;
|
||||||
|
|
|
@ -5,7 +5,22 @@ import { ParsedRequest } from "./types";
|
||||||
export function parseRequest(req: IncomingMessage) {
|
export function parseRequest(req: IncomingMessage) {
|
||||||
console.log("HTTP " + req.url);
|
console.log("HTTP " + req.url);
|
||||||
const { pathname, query } = parse(req.url || "/", true);
|
const { pathname, query } = parse(req.url || "/", true);
|
||||||
const { fontSize, images, widths, heights, theme, md } = query || {};
|
const {
|
||||||
|
fontSize,
|
||||||
|
images,
|
||||||
|
widths,
|
||||||
|
heights,
|
||||||
|
theme,
|
||||||
|
md,
|
||||||
|
|
||||||
|
// Attributes for Manifold card:
|
||||||
|
question,
|
||||||
|
probability,
|
||||||
|
metadata,
|
||||||
|
creatorName,
|
||||||
|
creatorUsername,
|
||||||
|
creatorAvatarUrl,
|
||||||
|
} = query || {};
|
||||||
|
|
||||||
if (Array.isArray(fontSize)) {
|
if (Array.isArray(fontSize)) {
|
||||||
throw new Error("Expected a single fontSize");
|
throw new Error("Expected a single fontSize");
|
||||||
|
@ -26,6 +41,15 @@ export function parseRequest(req: IncomingMessage) {
|
||||||
text = arr.join(".");
|
text = arr.join(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Take a url query param and return a single string
|
||||||
|
const getString = (stringOrArray: string[] | string | undefined): string => {
|
||||||
|
if (Array.isArray(stringOrArray)) {
|
||||||
|
// If the query param is an array, return the first element
|
||||||
|
return stringOrArray[0];
|
||||||
|
}
|
||||||
|
return stringOrArray || "";
|
||||||
|
};
|
||||||
|
|
||||||
const parsedRequest: ParsedRequest = {
|
const parsedRequest: ParsedRequest = {
|
||||||
fileType: extension === "jpeg" ? extension : "png",
|
fileType: extension === "jpeg" ? extension : "png",
|
||||||
text: decodeURIComponent(text),
|
text: decodeURIComponent(text),
|
||||||
|
@ -35,6 +59,15 @@ export function parseRequest(req: IncomingMessage) {
|
||||||
images: getArray(images),
|
images: getArray(images),
|
||||||
widths: getArray(widths),
|
widths: getArray(widths),
|
||||||
heights: getArray(heights),
|
heights: getArray(heights),
|
||||||
|
|
||||||
|
question:
|
||||||
|
getString(question) || "Will you create a prediction market on Manifold?",
|
||||||
|
probability: getString(probability) || "85",
|
||||||
|
metadata: getString(metadata) || "Jan 1 • M$ 123 pool",
|
||||||
|
creatorName: getString(creatorName) || "Manifold Markets",
|
||||||
|
creatorUsername: getString(creatorUsername) || "ManifoldMarkets",
|
||||||
|
creatorAvatarUrl:
|
||||||
|
getString(creatorAvatarUrl) || "https://manifold.markets/logo.png",
|
||||||
};
|
};
|
||||||
parsedRequest.images = getDefaultImages(parsedRequest.images);
|
parsedRequest.images = getDefaultImages(parsedRequest.images);
|
||||||
return parsedRequest;
|
return parsedRequest;
|
||||||
|
|
|
@ -1,20 +1,5 @@
|
||||||
import { readFileSync } from "fs";
|
|
||||||
import marked from "marked";
|
|
||||||
import { sanitizeHtml } from "./sanitizer";
|
import { sanitizeHtml } from "./sanitizer";
|
||||||
import { ParsedRequest } from "./types";
|
import { ParsedRequest } from "./types";
|
||||||
const twemoji = require("twemoji");
|
|
||||||
const twOptions = { folder: "svg", ext: ".svg" };
|
|
||||||
const emojify = (text: string) => twemoji.parse(text, twOptions);
|
|
||||||
|
|
||||||
const rglr = readFileSync(
|
|
||||||
`${__dirname}/../_fonts/Inter-Regular.woff2`
|
|
||||||
).toString("base64");
|
|
||||||
const bold = readFileSync(`${__dirname}/../_fonts/Inter-Bold.woff2`).toString(
|
|
||||||
"base64"
|
|
||||||
);
|
|
||||||
const mono = readFileSync(`${__dirname}/../_fonts/Vera-Mono.woff2`).toString(
|
|
||||||
"base64"
|
|
||||||
);
|
|
||||||
|
|
||||||
function getCss(theme: string, fontSize: string) {
|
function getCss(theme: string, fontSize: string) {
|
||||||
let background = "white";
|
let background = "white";
|
||||||
|
@ -30,36 +15,12 @@ function getCss(theme: string, fontSize: string) {
|
||||||
return `
|
return `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Readex+Pro:wght@400;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Readex+Pro:wght@400;700&display=swap');
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
src: url(data:font/woff2;charset=utf-8;base64,${rglr}) format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: bold;
|
|
||||||
src: url(data:font/woff2;charset=utf-8;base64,${bold}) format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Vera';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
src: url(data:font/woff2;charset=utf-8;base64,${mono}) format("woff2");
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: ${background};
|
background: ${background};
|
||||||
background-image: radial-gradient(circle at 25px 25px, ${radial} 2%, transparent 0%), radial-gradient(circle at 75px 75px, ${radial} 2%, transparent 0%);
|
background-image: radial-gradient(circle at 25px 25px, ${radial} 2%, transparent 0%), radial-gradient(circle at 75px 75px, ${radial} 2%, transparent 0%);
|
||||||
background-size: 100px 100px;
|
background-size: 100px 100px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
font-family: "Readex Pro", sans-serif;
|
||||||
text-align: center;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
@ -108,50 +69,92 @@ function getCss(theme: string, fontSize: string) {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
color: ${foreground};
|
color: ${foreground};
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
}`;
|
}
|
||||||
|
|
||||||
|
.font-major-mono {
|
||||||
|
font-family: "Major Mono Display", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: #11b981;
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHtml(parsedReq: ParsedRequest) {
|
export function getHtml(parsedReq: ParsedRequest) {
|
||||||
const { text, theme, md, fontSize, images, widths, heights } = parsedReq;
|
const {
|
||||||
|
theme,
|
||||||
|
fontSize,
|
||||||
|
|
||||||
|
question,
|
||||||
|
probability,
|
||||||
|
metadata,
|
||||||
|
creatorName,
|
||||||
|
creatorUsername,
|
||||||
|
creatorAvatarUrl,
|
||||||
|
} = parsedReq;
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Generated Image</title>
|
<title>Generated Image</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
<style>
|
<style>
|
||||||
${getCss(theme, fontSize)}
|
${getCss(theme, fontSize)}
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div class="px-24">
|
||||||
<div class="spacer">
|
<!-- Profile image -->
|
||||||
<div class="logo-wrapper">
|
<div class="absolute left-24 top-8">
|
||||||
${images
|
<div class="flex flex-row align-bottom gap-6">
|
||||||
.map(
|
<img
|
||||||
(img, i) =>
|
class="h-24 w-24 rounded-full bg-white flex items-center justify-center"
|
||||||
getPlusSign(i) + getImage(img, widths[i], heights[i])
|
src="${creatorAvatarUrl}"
|
||||||
)
|
alt=""
|
||||||
.join("")}
|
/>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<p class="text-gray-900 text-3xl">${creatorName}</p>
|
||||||
|
<p class="text-gray-500 text-3xl">@${creatorUsername}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mantic logo -->
|
||||||
|
<div class="absolute right-24 top-8">
|
||||||
|
<a class="flex flex-row gap-3" href="/"
|
||||||
|
><img
|
||||||
|
class="sm:h-12 sm:w-12"
|
||||||
|
src="https://manifold.markets/logo.png"
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="hidden sm:flex font-major-mono lowercase mt-1 sm:text-3xl md:whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Manifold Markets
|
||||||
|
</div></a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between gap-12 pt-36">
|
||||||
|
<div class="text-indigo-700 text-6xl leading-snug">
|
||||||
|
${question}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col text-primary">
|
||||||
|
<div class="text-8xl">${probability}%</div>
|
||||||
|
<div class="text-4xl">chance</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Metadata -->
|
||||||
|
<div class="absolute bottom-16">
|
||||||
|
<div class="text-gray-500 text-3xl">
|
||||||
|
${metadata}
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer">
|
|
||||||
<div class="heading">${emojify(
|
|
||||||
md ? marked(text) : sanitizeHtml(text)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImage(src: string, width = "auto", height = "225") {
|
|
||||||
return `<img
|
|
||||||
class="logo"
|
|
||||||
alt="Generated Image"
|
|
||||||
src="${sanitizeHtml(src)}"
|
|
||||||
width="${sanitizeHtml(width)}"
|
|
||||||
height="${sanitizeHtml(height)}"
|
|
||||||
/>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlusSign(i: number) {
|
|
||||||
return i === 0 ? "" : '<div class="plus">+</div>';
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,4 +10,12 @@ export interface ParsedRequest {
|
||||||
images: string[];
|
images: string[];
|
||||||
widths: string[];
|
widths: string[];
|
||||||
heights: string[];
|
heights: string[];
|
||||||
|
|
||||||
|
// Attributes for Manifold card:
|
||||||
|
question: string;
|
||||||
|
probability: string;
|
||||||
|
metadata: string;
|
||||||
|
creatorName: string;
|
||||||
|
creatorUsername: string;
|
||||||
|
creatorAvatarUrl: string;
|
||||||
}
|
}
|
||||||
|
|
135
og-image/public/og-cover.html
Normal file
135
og-image/public/og-cover.html
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Generated Image</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<style>
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Readex+Pro:wght@400;700&display=swap");
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
background-image: radial-gradient(
|
||||||
|
circle at 25px 25px,
|
||||||
|
lightgray 2%,
|
||||||
|
transparent 0%
|
||||||
|
),
|
||||||
|
radial-gradient(circle at 75px 75px, lightgray 2%, transparent 0%);
|
||||||
|
background-size: 100px 100px;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: "Readex Pro", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: #d400ff;
|
||||||
|
font-family: "Vera";
|
||||||
|
white-space: pre-wrap;
|
||||||
|
letter-spacing: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code:before,
|
||||||
|
code:after {
|
||||||
|
content: "`";
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
margin: 0 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus {
|
||||||
|
color: #bbb;
|
||||||
|
font-family: Times New Roman, Verdana;
|
||||||
|
font-size: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
margin: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
margin: 0 0.05em 0 0.1em;
|
||||||
|
vertical-align: -0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
font-family: "Major Mono Display", monospace;
|
||||||
|
font-size: 100px;
|
||||||
|
font-style: normal;
|
||||||
|
color: black;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-major-mono {
|
||||||
|
font-family: "Major Mono Display", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: #11b981;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<div class="px-24">
|
||||||
|
<!-- Profile image -->
|
||||||
|
<div class="absolute left-24 top-8">
|
||||||
|
<div class="flex flex-row align-bottom gap-6">
|
||||||
|
<img
|
||||||
|
class="h-24 w-24 rounded-full bg-gray-400 flex items-center justify-center"
|
||||||
|
src="https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<p class="text-gray-900 text-3xl">Austin Chen</p>
|
||||||
|
<p class="text-gray-500 text-3xl">@AustinChen</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mantic logo -->
|
||||||
|
<div class="absolute right-24 top-8">
|
||||||
|
<a class="flex flex-row gap-3" href="/"
|
||||||
|
><img
|
||||||
|
class="sm:h-12 sm:w-12"
|
||||||
|
src="https://manifold.markets/logo.png"
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="hidden sm:flex font-major-mono lowercase mt-1 sm:text-3xl md:whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Manifold Markets
|
||||||
|
</div></a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between gap-12 pt-36">
|
||||||
|
<div class="text-indigo-700 text-6xl leading-snug">
|
||||||
|
Will Manifold switch its logo to a manatee by April?
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col text-primary">
|
||||||
|
<div class="text-8xl">30%</div>
|
||||||
|
<div class="text-4xl">chance</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Metadata -->
|
||||||
|
<div class="absolute bottom-16">
|
||||||
|
<div class="text-gray-500 text-3xl">
|
||||||
|
Jan 7 • Closes Mar 31, 9:59pm • M$ 448 pool
|
||||||
|
• #ManifoldMarkets #fun
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -30,7 +30,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||||
<meta name="twitter:site" content="@manifoldmarkets" />
|
<meta name="twitter:site" content="@manifoldmarkets" />
|
||||||
<meta
|
<meta
|
||||||
property="og:image"
|
property="og:image"
|
||||||
content="https://manifold-og-image.vercel.app/manifold%20markets.png"
|
content="https://manifold.markets/logo-cover.png"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="twitter:image"
|
name="twitter:image"
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 11 KiB |
Loading…
Reference in New Issue
Block a user