more performance improvements to social preview generation

This commit is contained in:
James Fenn
2023-04-04 21:33:45 -04:00
parent f95dc983d1
commit 20e69a93db
7 changed files with 70 additions and 79 deletions

View File

@@ -14,3 +14,6 @@ export type Layout = {
css: string; css: string;
Component: React.FunctionComponent<ComponentProps>; Component: React.FunctionComponent<ComponentProps>;
}; };
export const PAGE_WIDTH = 1280;
export const PAGE_HEIGHT = 640;

View File

@@ -2,17 +2,10 @@ import chromium from "chrome-aws-lambda";
import puppeteer from "puppeteer-core"; import puppeteer from "puppeteer-core";
import { promises as fsPromises } from "fs"; import { promises as fsPromises } from "fs";
import { resolve } from "path"; import { resolve } from "path";
import { getAllPosts } from "utils/get-all-posts"; import { getPosts } from "utils/get-all-posts";
import { PostInfo } from "types/index"; import { PostInfo } from "types/index";
import { import { layouts, renderPostPreviewToString } from "./shared-post-preview-png";
layouts, import { Layout, PAGE_HEIGHT, PAGE_WIDTH } from "./base";
heightWidth,
renderPostPreviewToString,
} from "./shared-post-preview-png";
import { Layout } from "./base";
let browser: puppeteer.Browser;
let page: puppeteer.Page;
const browser_args = [ const browser_args = [
"--autoplay-policy=user-gesture-required", "--autoplay-policy=user-gesture-required",
@@ -53,27 +46,33 @@ const browser_args = [
"--disable-web-security", "--disable-web-security",
]; ];
const createPostSocialPreviewPng = async (layout: Layout, post: PostInfo) => { const browser: Promise<puppeteer.Browser> = chromium.puppeteer.launch({
if (!browser) { args: [...chromium.args, ...browser_args],
browser = await chromium.puppeteer.launch({ defaultViewport: {
args: [...chromium.args, ...browser_args], width: PAGE_WIDTH,
defaultViewport: chromium.defaultViewport, height: PAGE_HEIGHT,
executablePath: await chromium.executablePath, },
headless: true, executablePath: await chromium.executablePath,
ignoreHTTPSErrors: true, headless: true,
userDataDir: "./.puppeteer", ignoreHTTPSErrors: true,
}); userDataDir: "./.puppeteer",
page = await browser.newPage(); });
await page.setViewport(heightWidth);
}
await page.setContent(await renderPostPreviewToString(layout, post)); const page: Promise<puppeteer.Page> = browser.then((b) => b.newPage());
return (await page.screenshot({ type: "jpeg" })) as Buffer;
}; async function renderPostImage(layout: Layout, post: PostInfo) {
const label = `${post.slug} (${layout.name})`;
console.time(label);
const browserPage = await page;
await browserPage.setContent(await renderPostPreviewToString(layout, post));
const buffer = (await browserPage.screenshot({ type: "jpeg" })) as Buffer;
console.timeEnd(label);
return buffer;
}
const build = async () => { const build = async () => {
const posts = getAllPosts("en");
// Relative to root // Relative to root
const outDir = resolve(process.cwd(), "./public/generated"); const outDir = resolve(process.cwd(), "./public/generated");
await fsPromises.mkdir(outDir, { recursive: true }); await fsPromises.mkdir(outDir, { recursive: true });
@@ -82,19 +81,17 @@ const build = async () => {
* This is done synchronously, in order to prevent more than a single instance * This is done synchronously, in order to prevent more than a single instance
* of the browser from running at the same time. * of the browser from running at the same time.
*/ */
for (const post of posts) { for (const post of getPosts("en")) {
for (const layout of layouts) { for (const layout of layouts) {
const png = await createPostSocialPreviewPng(layout, post); const buffer = await renderPostImage(layout, post);
await fsPromises.writeFile( await fsPromises.writeFile(
resolve(outDir, `${post.slug}.${layout.name}.jpg`), resolve(outDir, `${post.slug}.${layout.name}.jpg`),
png buffer
); );
} }
console.log(post.slug);
} }
await browser.close(); await (await browser).close();
}; };
// For non-prod builds, this isn't needed // For non-prod builds, this isn't needed

View File

@@ -3,7 +3,7 @@ export default `
* { * {
font-family: "Work Sans"; font-family: "Work Sans";
color: var(--white); color: #FFF;
} }
.codeScreenOverlay, .codeScreenBg { .codeScreenOverlay, .codeScreenBg {

View File

@@ -3,7 +3,6 @@ import { PostInfo } from "types/index";
import * as fs from "fs"; import * as fs from "fs";
import { render } from "preact-render-to-string"; import { render } from "preact-render-to-string";
import { createElement } from "preact"; import { createElement } from "preact";
import { COLORS } from "constants/theme";
import { unified } from "unified"; import { unified } from "unified";
import remarkParse from "remark-parse"; import remarkParse from "remark-parse";
@@ -14,7 +13,7 @@ import rehypeStringify from "rehype-stringify";
import banner from "./layouts/banner"; import banner from "./layouts/banner";
import twitterPreview from "./layouts/twitter-preview"; import twitterPreview from "./layouts/twitter-preview";
import { Layout } from "./base"; import { Layout, PAGE_HEIGHT, PAGE_WIDTH } from "./base";
export const layouts: Layout[] = [banner, twitterPreview]; export const layouts: Layout[] = [banner, twitterPreview];
@@ -63,17 +62,6 @@ async function markdownToHtml(content: string) {
return await (await unifiedChain().process(content)).toString(); return await (await unifiedChain().process(content)).toString();
} }
const colorsCSS = (Object.keys(COLORS) as Array<keyof typeof COLORS>).reduce(
(stylesheetStr, colorKey, i, arr) => {
let str = stylesheetStr + `\n--${colorKey}: ${COLORS[colorKey].light};`;
if (i === arr.length - 1) str += "\n}";
return str;
},
":root {\n"
);
export const heightWidth = { width: 1280, height: 640 };
const shikiSCSS = fs.readFileSync("src/styles/shiki.scss", "utf8"); const shikiSCSS = fs.readFileSync("src/styles/shiki.scss", "utf8");
export const renderPostPreviewToString = async ( export const renderPostPreviewToString = async (
@@ -95,19 +83,14 @@ export const renderPostPreviewToString = async (
<head> <head>
<style> <style>
${shikiSCSS} ${shikiSCSS}
</style>
<style>
${colorsCSS}
</style>
<style>
${layout.css} ${layout.css}
</style>
<style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: ${heightWidth.width}px; width: ${PAGE_WIDTH}px;
height: ${heightWidth.height}px; height: ${PAGE_HEIGHT}px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@@ -118,7 +101,8 @@ export const renderPostPreviewToString = async (
createElement(layout.Component, { createElement(layout.Component, {
post, post,
postHtml, postHtml,
...heightWidth, width: PAGE_WIDTH,
height: PAGE_HEIGHT,
authorImageMap, authorImageMap,
}) })
)} )}

View File

@@ -17,10 +17,10 @@ const extTypeMap = {
}; };
export function readFileAsBase64(file: string) { export function readFileAsBase64(file: string) {
const image = fs.readFileSync(file); const image = fs.readFileSync(file, { encoding: "base64" });
const contentType = const contentType =
extTypeMap[path.extname(file) as keyof typeof extTypeMap] || "image/jpeg"; extTypeMap[path.extname(file) as keyof typeof extTypeMap] || "image/jpeg";
return `data:${contentType};base64,${image.toString("base64")}`; return `data:${contentType};base64,${image}`;
} }
export function ensureDirectoryExistence(filePath: string) { export function ensureDirectoryExistence(filePath: string) {

View File

@@ -9,6 +9,7 @@ import {
} from "types/index"; } from "types/index";
import * as fs from "fs"; import * as fs from "fs";
import { join } from "path"; import { join } from "path";
import { isNotJunk } from "junk";
import { getImageSize } from "../utils/get-image-size"; import { getImageSize } from "../utils/get-image-size";
import { getFullRelativePath } from "./url-paths"; import { getFullRelativePath } from "./url-paths";
import matter from "gray-matter"; import matter from "gray-matter";
@@ -66,8 +67,8 @@ const fullUnicorns: UnicornInfo[] = unicornsRaw.map((unicorn) => {
return newUnicorn; return newUnicorn;
}); });
function getPosts(): Array<RawPostInfo> { function getPosts(): Array<RawPostInfo & { slug: string }> {
const slugs = fs.readdirSync(postsDirectory); const slugs = fs.readdirSync(postsDirectory).filter(isNotJunk);
return slugs.map((slug) => { return slugs.map((slug) => {
const fileContents = fs.readFileSync( const fileContents = fs.readFileSync(
join(postsDirectory, slug, "index.md"), join(postsDirectory, slug, "index.md"),
@@ -76,7 +77,10 @@ function getPosts(): Array<RawPostInfo> {
const frontmatter = matter(fileContents).data as RawPostInfo; const frontmatter = matter(fileContents).data as RawPostInfo;
return frontmatter; return {
...frontmatter,
slug,
};
}); });
} }
@@ -85,7 +89,7 @@ const posts = getPosts();
function getCollections(): Array< function getCollections(): Array<
RawCollectionInfo & Pick<CollectionInfo, "slug" | "coverImgMeta"> RawCollectionInfo & Pick<CollectionInfo, "slug" | "coverImgMeta">
> { > {
const slugs = fs.readdirSync(collectionsDirectory); const slugs = fs.readdirSync(collectionsDirectory).filter(isNotJunk);
const collections = slugs.map((slug) => { const collections = slugs.map((slug) => {
const fileContents = fs.readFileSync( const fileContents = fs.readFileSync(
join(collectionsDirectory, slug, "index.md"), join(collectionsDirectory, slug, "index.md"),

View File

@@ -5,8 +5,7 @@
* when the Astro runtime isn't available, such as getting suggested articles and other instances. * when the Astro runtime isn't available, such as getting suggested articles and other instances.
*/ */
import { rehypeUnicornPopulatePost } from "./markdown/rehype-unicorn-populate-post"; import { rehypeUnicornPopulatePost } from "./markdown/rehype-unicorn-populate-post";
import { isNotJunk } from "junk"; import { postsDirectory, posts } from "./data";
import { postsDirectory } from "./data";
import { Languages, PostInfo } from "types/index"; import { Languages, PostInfo } from "types/index";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
@@ -17,20 +16,20 @@ const getIndexPath = (lang: Languages) => {
}; };
export function getPostSlugs(lang: Languages) { export function getPostSlugs(lang: Languages) {
// Avoid errors trying to read from `.DS_Store` files return [...getPosts(lang)].map((post) => post.slug);
return fs
.readdirSync(postsDirectory)
.filter(isNotJunk)
.filter((dir) =>
fs.existsSync(path.resolve(postsDirectory, dir, getIndexPath(lang)))
);
} }
export const getAllPosts = (lang: Languages): PostInfo[] => { export function* getPosts(lang: Languages) {
const slugs = getPostSlugs(lang); for (const post of posts) {
return slugs.map((slug) => { const indexFile = path.resolve(
postsDirectory,
post.slug,
getIndexPath(lang)
);
if (!fs.existsSync(indexFile)) continue;
const file = { const file = {
path: path.join(postsDirectory, slug, getIndexPath(lang)), path: indexFile,
data: { data: {
astro: { astro: {
frontmatter: {}, frontmatter: {},
@@ -40,9 +39,13 @@ export const getAllPosts = (lang: Languages): PostInfo[] => {
(rehypeUnicornPopulatePost as any)()(undefined, file); (rehypeUnicornPopulatePost as any)()(undefined, file);
return { yield {
...((file.data.astro.frontmatter as any) || {}).frontmatterBackup, ...((file.data.astro.frontmatter as any) || {}).frontmatterBackup,
...file.data.astro.frontmatter, ...file.data.astro.frontmatter,
}; };
}); }
}
export const getAllPosts = (lang: Languages): PostInfo[] => {
return [...getPosts(lang)];
}; };