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;
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 { promises as fsPromises } from "fs";
import { resolve } from "path";
import { getAllPosts } from "utils/get-all-posts";
import { getPosts } from "utils/get-all-posts";
import { PostInfo } from "types/index";
import {
layouts,
heightWidth,
renderPostPreviewToString,
} from "./shared-post-preview-png";
import { Layout } from "./base";
let browser: puppeteer.Browser;
let page: puppeteer.Page;
import { layouts, renderPostPreviewToString } from "./shared-post-preview-png";
import { Layout, PAGE_HEIGHT, PAGE_WIDTH } from "./base";
const browser_args = [
"--autoplay-policy=user-gesture-required",
@@ -53,27 +46,33 @@ const browser_args = [
"--disable-web-security",
];
const createPostSocialPreviewPng = async (layout: Layout, post: PostInfo) => {
if (!browser) {
browser = await chromium.puppeteer.launch({
const browser: Promise<puppeteer.Browser> = chromium.puppeteer.launch({
args: [...chromium.args, ...browser_args],
defaultViewport: chromium.defaultViewport,
defaultViewport: {
width: PAGE_WIDTH,
height: PAGE_HEIGHT,
},
executablePath: await chromium.executablePath,
headless: true,
ignoreHTTPSErrors: true,
userDataDir: "./.puppeteer",
});
page = await browser.newPage();
await page.setViewport(heightWidth);
const page: Promise<puppeteer.Page> = browser.then((b) => b.newPage());
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;
}
await page.setContent(await renderPostPreviewToString(layout, post));
return (await page.screenshot({ type: "jpeg" })) as Buffer;
};
const build = async () => {
const posts = getAllPosts("en");
// Relative to root
const outDir = resolve(process.cwd(), "./public/generated");
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
* 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) {
const png = await createPostSocialPreviewPng(layout, post);
const buffer = await renderPostImage(layout, post);
await fsPromises.writeFile(
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

View File

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

View File

@@ -3,7 +3,6 @@ import { PostInfo } from "types/index";
import * as fs from "fs";
import { render } from "preact-render-to-string";
import { createElement } from "preact";
import { COLORS } from "constants/theme";
import { unified } from "unified";
import remarkParse from "remark-parse";
@@ -14,7 +13,7 @@ import rehypeStringify from "rehype-stringify";
import banner from "./layouts/banner";
import twitterPreview from "./layouts/twitter-preview";
import { Layout } from "./base";
import { Layout, PAGE_HEIGHT, PAGE_WIDTH } from "./base";
export const layouts: Layout[] = [banner, twitterPreview];
@@ -63,17 +62,6 @@ async function markdownToHtml(content: string) {
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");
export const renderPostPreviewToString = async (
@@ -95,19 +83,14 @@ export const renderPostPreviewToString = async (
<head>
<style>
${shikiSCSS}
</style>
<style>
${colorsCSS}
</style>
<style>
${layout.css}
</style>
<style>
html, body {
margin: 0;
padding: 0;
width: ${heightWidth.width}px;
height: ${heightWidth.height}px;
width: ${PAGE_WIDTH}px;
height: ${PAGE_HEIGHT}px;
position: relative;
overflow: hidden;
}
@@ -118,7 +101,8 @@ export const renderPostPreviewToString = async (
createElement(layout.Component, {
post,
postHtml,
...heightWidth,
width: PAGE_WIDTH,
height: PAGE_HEIGHT,
authorImageMap,
})
)}

View File

@@ -17,10 +17,10 @@ const extTypeMap = {
};
export function readFileAsBase64(file: string) {
const image = fs.readFileSync(file);
const image = fs.readFileSync(file, { encoding: "base64" });
const contentType =
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) {

View File

@@ -9,6 +9,7 @@ import {
} from "types/index";
import * as fs from "fs";
import { join } from "path";
import { isNotJunk } from "junk";
import { getImageSize } from "../utils/get-image-size";
import { getFullRelativePath } from "./url-paths";
import matter from "gray-matter";
@@ -66,8 +67,8 @@ const fullUnicorns: UnicornInfo[] = unicornsRaw.map((unicorn) => {
return newUnicorn;
});
function getPosts(): Array<RawPostInfo> {
const slugs = fs.readdirSync(postsDirectory);
function getPosts(): Array<RawPostInfo & { slug: string }> {
const slugs = fs.readdirSync(postsDirectory).filter(isNotJunk);
return slugs.map((slug) => {
const fileContents = fs.readFileSync(
join(postsDirectory, slug, "index.md"),
@@ -76,7 +77,10 @@ function getPosts(): Array<RawPostInfo> {
const frontmatter = matter(fileContents).data as RawPostInfo;
return frontmatter;
return {
...frontmatter,
slug,
};
});
}
@@ -85,7 +89,7 @@ const posts = getPosts();
function getCollections(): Array<
RawCollectionInfo & Pick<CollectionInfo, "slug" | "coverImgMeta">
> {
const slugs = fs.readdirSync(collectionsDirectory);
const slugs = fs.readdirSync(collectionsDirectory).filter(isNotJunk);
const collections = slugs.map((slug) => {
const fileContents = fs.readFileSync(
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.
*/
import { rehypeUnicornPopulatePost } from "./markdown/rehype-unicorn-populate-post";
import { isNotJunk } from "junk";
import { postsDirectory } from "./data";
import { postsDirectory, posts } from "./data";
import { Languages, PostInfo } from "types/index";
import * as fs from "fs";
import * as path from "path";
@@ -17,20 +16,20 @@ const getIndexPath = (lang: Languages) => {
};
export function getPostSlugs(lang: Languages) {
// Avoid errors trying to read from `.DS_Store` files
return fs
.readdirSync(postsDirectory)
.filter(isNotJunk)
.filter((dir) =>
fs.existsSync(path.resolve(postsDirectory, dir, getIndexPath(lang)))
);
return [...getPosts(lang)].map((post) => post.slug);
}
export const getAllPosts = (lang: Languages): PostInfo[] => {
const slugs = getPostSlugs(lang);
return slugs.map((slug) => {
export function* getPosts(lang: Languages) {
for (const post of posts) {
const indexFile = path.resolve(
postsDirectory,
post.slug,
getIndexPath(lang)
);
if (!fs.existsSync(indexFile)) continue;
const file = {
path: path.join(postsDirectory, slug, getIndexPath(lang)),
path: indexFile,
data: {
astro: {
frontmatter: {},
@@ -40,9 +39,13 @@ export const getAllPosts = (lang: Languages): PostInfo[] => {
(rehypeUnicornPopulatePost as any)()(undefined, file);
return {
yield {
...((file.data.astro.frontmatter as any) || {}).frontmatterBackup,
...file.data.astro.frontmatter,
};
});
}
}
export const getAllPosts = (lang: Languages): PostInfo[] => {
return [...getPosts(lang)];
};