diff --git a/.babelrc.js b/.babelrc.js
new file mode 100644
index 000000000..52ad7bdd6
--- /dev/null
+++ b/.babelrc.js
@@ -0,0 +1,4 @@
+module.exports = {
+ presets: [["next/babel", { "preset-react": { runtime: "automatic" } }]],
+ plugins: ["babel-plugin-macros", ["styled-components", { ssr: true }]],
+};
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..cd39d7059
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @styfle
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..1437c53f7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,34 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# vercel
+.vercel
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000..9aa793a05
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "bracketSpacing": true,
+ "singleQuote": false,
+ "trailingComma": "all",
+ "arrowParens": "avoid"
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..9e455646d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,20 @@
+# Contributing
+
+There are two pieces to `og-image` that are worth noting before you begin development.
+
+1. The backend image generator located in [/api/index.ts](https://github.com/vercel/og-image/blob/main/api/index.ts)
+2. The frontend inputs located in [/web/index.ts](https://github.com/vercel/og-image/blob/main/web/index.ts)
+
+Vercel handles [routing](https://github.com/vercel/og-image/blob/main/vercel.json#L6) in an elegant way for us so deployment is easy.
+
+To start hacking, do the following:
+
+1. Clone this repo with `git clone https://github.com/vercel/og-image`
+2. Change directory with `cd og-image`
+3. Run `yarn` or `npm install` to install all dependencies
+4. Run locally with `vercel dev` and visit [localhost:3000](http://localhost:3000) (if nothing happens, run `npm install -g vercel`)
+5. If necessary, edit the `exePath` in [options.ts](https://github.com/vercel/og-image/blob/main/api/_lib/options.ts) to point to your local Chrome executable
+
+Now you're ready to start local development!
+
+You can set an environment variable to assist with debugging `export OG_HTML_DEBUG=1`. This will render the image as HTML so you can play around with your browser's dev tools before committing changes to the template.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..b3a62cd25
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019-2020 Vercel, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..9e28a853a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,68 @@
+
+
+# [Open Graph Image as a Service](https://og-image.vercel.app)
+
+
+
+
+
+Serverless service that generates dynamic Open Graph images that you can embed in your `` tags.
+
+For each keystroke, headless chromium is used to render an HTML page and take a screenshot of the result which gets cached.
+
+See the image embedded in the tweet for a real use case.
+
+
+## What is an Open Graph Image?
+
+Have you ever posted a hyperlink to Twitter, Facebook, or Slack and seen an image popup?
+How did your social network know how to "unfurl" the URL and get an image?
+The answer is in your `
This is a service that generates dynamic Open Graph images that you can embed in your <meta>
tags.
For each keystroke, headless chromium is used to render an HTML page and take a screenshot of the result which gets cached.
+Find out how this works and deploy your own image generator by visiting GitHub.
+ +Sorry, there was a problem
"); + console.error(e); + } +}; + +export default handler; diff --git a/src/pages/api/_fonts/Inter-Bold.woff2 b/src/pages/api/_fonts/Inter-Bold.woff2 new file mode 100644 index 000000000..b26180b16 Binary files /dev/null and b/src/pages/api/_fonts/Inter-Bold.woff2 differ diff --git a/src/pages/api/_fonts/Inter-License.txt b/src/pages/api/_fonts/Inter-License.txt new file mode 100644 index 000000000..39830805d --- /dev/null +++ b/src/pages/api/_fonts/Inter-License.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016-2018 The Inter Project Authors (me@rsms.me) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/pages/api/_fonts/Inter-Regular.woff2 b/src/pages/api/_fonts/Inter-Regular.woff2 new file mode 100644 index 000000000..46568fdcd Binary files /dev/null and b/src/pages/api/_fonts/Inter-Regular.woff2 differ diff --git a/src/pages/api/_fonts/Vera-License.txt b/src/pages/api/_fonts/Vera-License.txt new file mode 100644 index 000000000..e651be1c4 --- /dev/null +++ b/src/pages/api/_fonts/Vera-License.txt @@ -0,0 +1,124 @@ +Bitstream Vera Fonts Copyright + +The fonts have a generous copyright, allowing derivative works (as +long as "Bitstream" or "Vera" are not in the names), and full +redistribution (so long as they are not *sold* by themselves). They +can be be bundled, redistributed and sold with any software. + +The fonts are distributed under the following copyright: + +Copyright +========= + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream +Vera is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute +the Font Software, including without limitation the rights to use, +copy, merge, publish, distribute, and/or sell copies of the Font +Software, and to permit persons to whom the Font Software is furnished +to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Bitstream" or the word "Vera". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Bitstream Vera" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, +OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT +SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font +Software without prior written authorization from the Gnome Foundation +or Bitstream Inc., respectively. For further information, contact: +fonts at gnome dot org. + +Copyright FAQ +============= + + 1. I don't understand the resale restriction... What gives? + + Bitstream is giving away these fonts, but wishes to ensure its + competitors can't just drop the fonts as is into a font sale system + and sell them as is. It seems fair that if Bitstream can't make money + from the Bitstream Vera fonts, their competitors should not be able to + do so either. You can sell the fonts as part of any software package, + however. + + 2. I want to package these fonts separately for distribution and + sale as part of a larger software package or system. Can I do so? + + Yes. A RPM or Debian package is a "larger software package" to begin + with, and you aren't selling them independently by themselves. + See 1. above. + + 3. Are derivative works allowed? + Yes! + + 4. Can I change or add to the font(s)? + Yes, but you must change the name(s) of the font(s). + + 5. Under what terms are derivative works allowed? + + You must change the name(s) of the fonts. This is to ensure the + quality of the fonts, both to protect Bitstream and Gnome. We want to + ensure that if an application has opened a font specifically of these + names, it gets what it expects (though of course, using fontconfig, + substitutions could still could have occurred during font + opening). You must include the Bitstream copyright. Additional + copyrights can be added, as per copyright law. Happy Font Hacking! + + 6. If I have improvements for Bitstream Vera, is it possible they might get + adopted in future versions? + + Yes. The contract between the Gnome Foundation and Bitstream has + provisions for working with Bitstream to ensure quality additions to + the Bitstream Vera font family. Please contact us if you have such + additions. Note, that in general, we will want such additions for the + entire family, not just a single font, and that you'll have to keep + both Gnome and Jim Lyles, Vera's designer, happy! To make sense to add + glyphs to the font, they must be stylistically in keeping with Vera's + design. Vera cannot become a "ransom note" font. Jim Lyles will be + providing a document describing the design elements used in Vera, as a + guide and aid for people interested in contributing to Vera. + + 7. I want to sell a software package that uses these fonts: Can I do so? + + Sure. Bundle the fonts with your software and sell your software + with the fonts. That is the intent of the copyright. + + 8. If applications have built the names "Bitstream Vera" into them, + can I override this somehow to use fonts of my choosing? + + This depends on exact details of the software. Most open source + systems and software (e.g., Gnome, KDE, etc.) are now converting to + use fontconfig (see www.fontconfig.org) to handle font configuration, + selection and substitution; it has provisions for overriding font + names and subsituting alternatives. An example is provided by the + supplied local.conf file, which chooses the family Bitstream Vera for + "sans", "serif" and "monospace". Other software (e.g., the XFree86 + core server) has other mechanisms for font substitution. + diff --git a/src/pages/api/_fonts/Vera-Mono.woff2 b/src/pages/api/_fonts/Vera-Mono.woff2 new file mode 100644 index 000000000..4993a0f40 Binary files /dev/null and b/src/pages/api/_fonts/Vera-Mono.woff2 differ diff --git a/src/pages/api/_lib/chromium.ts b/src/pages/api/_lib/chromium.ts new file mode 100644 index 000000000..847d2517b --- /dev/null +++ b/src/pages/api/_lib/chromium.ts @@ -0,0 +1,26 @@ +import core from "puppeteer-core"; +import { getOptions } from "./options"; +import { FileType } from "./types"; +let _page: core.Page | null; + +async function getPage(isDev: boolean) { + if (_page) { + return _page; + } + const options = await getOptions(isDev); + const browser = await core.launch(options); + _page = await browser.newPage(); + return _page; +} + +export async function getScreenshot( + html: string, + type: FileType, + isDev: boolean, +) { + const page = await getPage(isDev); + await page.setViewport({ width: 2048, height: 1170 }); + await page.setContent(html); + const file = await page.screenshot({ type }); + return file; +} diff --git a/src/pages/api/_lib/options.ts b/src/pages/api/_lib/options.ts new file mode 100644 index 000000000..4b403a2f2 --- /dev/null +++ b/src/pages/api/_lib/options.ts @@ -0,0 +1,31 @@ +import chrome from "chrome-aws-lambda"; +const exePath = + process.platform === "win32" + ? "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" + : process.platform === "linux" + ? "/usr/bin/google-chrome" + : "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; + +interface Options { + args: string[]; + executablePath: string; + headless: boolean; +} + +export async function getOptions(isDev: boolean) { + let options: Options; + if (isDev) { + options = { + args: [], + executablePath: exePath, + headless: true, + }; + } else { + options = { + args: chrome.args, + executablePath: await chrome.executablePath, + headless: chrome.headless, + }; + } + return options; +} diff --git a/src/pages/api/_lib/parser.ts b/src/pages/api/_lib/parser.ts new file mode 100644 index 000000000..eca3a612c --- /dev/null +++ b/src/pages/api/_lib/parser.ts @@ -0,0 +1,72 @@ +import { IncomingMessage } from "http"; +import { parse } from "url"; +import { ParsedRequest, Theme } from "./types"; + +export function parseRequest(req: IncomingMessage) { + console.log("HTTP " + req.url); + const { pathname, query } = parse(req.url || "/", true); + const { fontSize, images, widths, heights, theme, md } = query || {}; + + if (Array.isArray(fontSize)) { + throw new Error("Expected a single fontSize"); + } + if (Array.isArray(theme)) { + throw new Error("Expected a single theme"); + } + + const arr = (pathname || "/").slice(1).split("."); + let extension = ""; + let text = ""; + if (arr.length === 0) { + text = ""; + } else if (arr.length === 1) { + text = arr[0]; + } else { + extension = arr.pop() as string; + text = arr.join("."); + } + + const parsedRequest: ParsedRequest = { + fileType: extension === "jpeg" ? extension : "png", + text: decodeURIComponent(text), + theme: theme === "dark" ? "dark" : "light", + md: md === "1" || md === "true", + fontSize: fontSize || "96px", + images: getArray(images), + widths: getArray(widths), + heights: getArray(heights), + }; + parsedRequest.images = getDefaultImages( + parsedRequest.images, + parsedRequest.theme, + ); + return parsedRequest; +} + +function getArray(stringOrArray: string[] | string | undefined): string[] { + if (typeof stringOrArray === "undefined") { + return []; + } else if (Array.isArray(stringOrArray)) { + return stringOrArray; + } else { + return [stringOrArray]; + } +} + +function getDefaultImages(images: string[], theme: Theme): string[] { + const defaultImage = + theme === "light" + ? "https://assets.vercel.com/image/upload/front/assets/design/vercel-triangle-black.svg" + : "https://assets.vercel.com/image/upload/front/assets/design/vercel-triangle-white.svg"; + + if (!images || !images[0]) { + return [defaultImage]; + } + if ( + !images[0].startsWith("https://assets.vercel.com/") && + !images[0].startsWith("https://assets.zeit.co/") + ) { + images[0] = defaultImage; + } + return images; +} diff --git a/src/pages/api/_lib/sanitizer.ts b/src/pages/api/_lib/sanitizer.ts new file mode 100644 index 000000000..f6f084285 --- /dev/null +++ b/src/pages/api/_lib/sanitizer.ts @@ -0,0 +1,12 @@ +const entityMap: { [key: string]: string } = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "/": "/", +}; + +export function sanitizeHtml(html: string) { + return String(html).replace(/[&<>"'\/]/g, key => entityMap[key]); +} diff --git a/src/pages/api/_lib/template.ts b/src/pages/api/_lib/template.ts new file mode 100644 index 000000000..edf004126 --- /dev/null +++ b/src/pages/api/_lib/template.ts @@ -0,0 +1,138 @@ +import { readFileSync } from "fs"; +import marked from "marked"; +import { sanitizeHtml } from "./sanitizer"; +import { ParsedRequest } from "./types"; +const twemoji = require("twemoji"); +const twOptions = { folder: "svg", ext: ".svg" }; +const emojify = (text: string) => twemoji.parse(text, twOptions); + +function getCss(theme: string, fontSize: string) { + console.log("DIRNAME", __dirname); + + // const rglr = readFileSync(`../_fonts/Inter-Regular.woff2`).toString("base64"); + + // console.log("REGULAR", rglr); + + // const bold = readFileSync(`${__dirname}/../_fonts/Inter-Bold.woff2`).toString( + // "base64", + // ); + // const mono = readFileSync(`${__dirname}/../_fonts/Vera-Mono.woff2`).toString( + // "base64", + // ); + + let background = "white"; + let foreground = "black"; + let radial = "lightgray"; + + if (theme === "dark") { + background = "black"; + foreground = "white"; + radial = "dimgray"; + } + return ` + @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'); + + body { + 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-size: 100px 100px; + height: 100vh; + display: flex; + text-align: center; + align-items: center; + justify-content: center; + } + + 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 .05em 0 .1em; + vertical-align: -0.1em; + } + + .heading { + font-family: 'Inter', sans-serif; + font-size: ${sanitizeHtml(fontSize)}; + font-style: normal; + color: ${foreground}; + line-height: 1.8; + }`; +} + +export function getHtml(parsedReq: ParsedRequest) { + const { text, theme, md, fontSize, images, widths, heights } = parsedReq; + return ` + + +