mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-09 12:57:45 +00:00
more performance improvements to social preview generation
This commit is contained in:
@@ -14,3 +14,6 @@ export type Layout = {
|
||||
css: string;
|
||||
Component: React.FunctionComponent<ComponentProps>;
|
||||
};
|
||||
|
||||
export const PAGE_WIDTH = 1280;
|
||||
export const PAGE_HEIGHT = 640;
|
||||
|
||||
@@ -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({
|
||||
args: [...chromium.args, ...browser_args],
|
||||
defaultViewport: chromium.defaultViewport,
|
||||
executablePath: await chromium.executablePath,
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: true,
|
||||
userDataDir: "./.puppeteer",
|
||||
});
|
||||
page = await browser.newPage();
|
||||
await page.setViewport(heightWidth);
|
||||
}
|
||||
const browser: Promise<puppeteer.Browser> = chromium.puppeteer.launch({
|
||||
args: [...chromium.args, ...browser_args],
|
||||
defaultViewport: {
|
||||
width: PAGE_WIDTH,
|
||||
height: PAGE_HEIGHT,
|
||||
},
|
||||
executablePath: await chromium.executablePath,
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: true,
|
||||
userDataDir: "./.puppeteer",
|
||||
});
|
||||
|
||||
await page.setContent(await renderPostPreviewToString(layout, post));
|
||||
return (await page.screenshot({ type: "jpeg" })) as Buffer;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@ export default `
|
||||
|
||||
* {
|
||||
font-family: "Work Sans";
|
||||
color: var(--white);
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.codeScreenOverlay, .codeScreenBg {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
)}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user