mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
monograph: speed up og image generation
this gets rid of satori and uses custom logic for drawing using @napi-rs/canvas
This commit is contained in:
@@ -62,7 +62,7 @@ export function MonographChat({ sx }: SxProp) {
|
||||
borderRadius: 10,
|
||||
width: "100%"
|
||||
}}
|
||||
src={`${PUBLIC_URL}/api/og.png?title=Open+sourcing&description=VGhpcyBtb25vZ3JhcGggaXMgZW5jcnlwdGVkLiBFbnRlciBwYXNzd29yZCB0byB2aWV3IGNvbnRlbnRzLg%3D%3D&date=Saturday%2C+February+18%2C+2023`}
|
||||
src={`${PUBLIC_URL}/api/og.jpg?title=Open+sourcing&description=VGhpcyBtb25vZ3JhcGggaXMgZW5jcnlwdGVkLiBFbnRlciBwYXNzd29yZCB0byB2aWV3IGNvbnRlbnRzLg%3D%3D&date=Saturday%2C+February+18%2C+2023`}
|
||||
/>
|
||||
<Text
|
||||
mt="12px"
|
||||
|
||||
@@ -48,7 +48,7 @@ type MonographResponse = Omit<Monograph, "content"> & { content: string };
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
if (!data || !data.metadata || !data.monograph) return [];
|
||||
|
||||
const imageUrl = `${PUBLIC_URL}/api/og.png?${new URLSearchParams({
|
||||
const imageUrl = `${PUBLIC_URL}/api/og.jpg?${new URLSearchParams({
|
||||
title: data?.metadata?.title || "",
|
||||
description: data?.metadata?.fullDescription
|
||||
? Buffer.from(data.metadata.fullDescription, "utf-8").toString("base64")
|
||||
|
||||
@@ -21,6 +21,6 @@ import { LoaderFunctionArgs } from "@remix-run/node";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
url.pathname += ".png";
|
||||
url.pathname += ".jpg";
|
||||
return Response.redirect(url, 308);
|
||||
}
|
||||
|
||||
52
apps/monograph/app/routes/api.og[.jpg].ts
Normal file
52
apps/monograph/app/routes/api.og[.jpg].ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { makeImage } from "../utils/generate-og-image.server";
|
||||
import { formatDate } from "@notesnook/core";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const title = url.searchParams.get("title") || "Not found";
|
||||
const description = url.searchParams.get("description") || "";
|
||||
const date =
|
||||
url.searchParams.get("date") ||
|
||||
formatDate(new Date(), {
|
||||
type: "date-time",
|
||||
dateFormat: "YYYY-MM-DD",
|
||||
timeFormat: "24-hour"
|
||||
});
|
||||
|
||||
return new Response(
|
||||
await makeImage(
|
||||
{
|
||||
date,
|
||||
description,
|
||||
title
|
||||
},
|
||||
url.search
|
||||
),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "image/jpeg"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -18,35 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { makeImage } from "../utils/generate-og-image.server";
|
||||
import { formatDate } from "@notesnook/core";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const title = url.searchParams.get("title") || "Not found";
|
||||
const description = url.searchParams.get("description") || "";
|
||||
const date =
|
||||
url.searchParams.get("date") ||
|
||||
formatDate(new Date(), {
|
||||
type: "date-time",
|
||||
dateFormat: "YYYY-MM-DD",
|
||||
timeFormat: "24-hour"
|
||||
});
|
||||
|
||||
return new Response(
|
||||
await makeImage(
|
||||
{
|
||||
date,
|
||||
description,
|
||||
title
|
||||
},
|
||||
url.search
|
||||
),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "image/png"
|
||||
}
|
||||
}
|
||||
);
|
||||
url.pathname = url.pathname.replace(/.png/, ".jpg");
|
||||
return Response.redirect(url, 308);
|
||||
}
|
||||
|
||||
147
apps/monograph/app/utils/generate-og-image.server.ts
Normal file
147
apps/monograph/app/utils/generate-og-image.server.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { createCanvas, GlobalFonts, loadImage } from "@napi-rs/canvas";
|
||||
import { LRUCache } from "lru-cache";
|
||||
import { ThemeDark } from "@notesnook/theme";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { split } from "canvas-hypertxt";
|
||||
|
||||
export type OGMetadata = { title: string; description: string; date: string };
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Register fonts
|
||||
const OpenSans = path.join(
|
||||
__dirname,
|
||||
import.meta.env.DEV
|
||||
? "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf"
|
||||
: "../../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf"
|
||||
);
|
||||
const OpenSansBold = path.join(
|
||||
__dirname,
|
||||
import.meta.env.DEV
|
||||
? "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf"
|
||||
: "../../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf"
|
||||
);
|
||||
|
||||
console.log("OpenSans", GlobalFonts.registerFromPath(OpenSans, "OpenSans"));
|
||||
console.log(
|
||||
"OpenSansBold",
|
||||
GlobalFonts.registerFromPath(OpenSansBold, "OpenSansBold")
|
||||
);
|
||||
|
||||
const cache = new LRUCache<string, Buffer>({
|
||||
ttl: 1000 * 60 * 60 * 24,
|
||||
ttlAutopurge: true
|
||||
});
|
||||
|
||||
const WIDTH = 1200;
|
||||
const HEIGHT = 630;
|
||||
const PADDING = 50;
|
||||
const QUALITY = 80;
|
||||
const logo = loadImage(
|
||||
import.meta.env.DEV
|
||||
? path.resolve(__dirname, "../../public/logo.svg")
|
||||
: path.resolve(__dirname, "../../client/logo.svg")
|
||||
);
|
||||
|
||||
export async function makeImage(metadata: OGMetadata, cacheKey: string) {
|
||||
if (cache.has(cacheKey)) {
|
||||
return cache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
console.time("canvas");
|
||||
const theme = ThemeDark.scopes.base;
|
||||
|
||||
const canvas = createCanvas(WIDTH, HEIGHT);
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = theme.primary.background;
|
||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
// Bottom border
|
||||
ctx.fillStyle = "#008837";
|
||||
ctx.fillRect(0, HEIGHT - 10, WIDTH, 10);
|
||||
|
||||
// Draw logo
|
||||
ctx.drawImage(await logo, PADDING, HEIGHT - PADDING - 85, 80, 80);
|
||||
|
||||
// Draw bottom text
|
||||
ctx.fillStyle = theme.primary.heading;
|
||||
ctx.font = "600 32px OpenSansBold";
|
||||
ctx.fillText("Notesnook Monograph", PADDING + 95, HEIGHT - PADDING - 55);
|
||||
|
||||
ctx.fillStyle = theme.secondary.paragraph;
|
||||
ctx.font = "25px OpenSans";
|
||||
ctx.fillText(
|
||||
"Anonymous, secure, and encrypted note sharing with password protection.",
|
||||
PADDING + 95,
|
||||
HEIGHT - PADDING - 19
|
||||
);
|
||||
|
||||
// Draw date
|
||||
ctx.fillStyle = theme.secondary.paragraph;
|
||||
ctx.font = "25px OpenSans";
|
||||
ctx.fillText(metadata.date, PADDING, PADDING + 25);
|
||||
|
||||
// Draw title
|
||||
ctx.fillStyle = theme.primary.heading;
|
||||
ctx.font = "600 64px OpenSansBold";
|
||||
let y = PADDING + 105;
|
||||
const titleLines = split(
|
||||
ctx as any,
|
||||
metadata.title,
|
||||
"600 64px OpenSansBold",
|
||||
WIDTH - PADDING * 2,
|
||||
true
|
||||
);
|
||||
for (const line of titleLines) {
|
||||
ctx.fillText(line, PADDING, y);
|
||||
y += 60;
|
||||
}
|
||||
|
||||
// Draw description
|
||||
ctx.fillStyle = theme.primary.paragraph;
|
||||
ctx.font = "30px OpenSans";
|
||||
const description = Buffer.from(
|
||||
metadata.description || "",
|
||||
"base64"
|
||||
).toString("utf-8");
|
||||
const descLines = split(
|
||||
ctx as any,
|
||||
description,
|
||||
"30px OpenSans",
|
||||
WIDTH - PADDING * 2,
|
||||
true
|
||||
).slice(0, 4);
|
||||
for (const line of descLines) {
|
||||
ctx.fillText(line, PADDING, y);
|
||||
y += 40;
|
||||
}
|
||||
|
||||
const buffer = canvas.toBuffer("image/jpeg", QUALITY);
|
||||
console.timeEnd("canvas");
|
||||
|
||||
cache.set(cacheKey, buffer);
|
||||
return buffer;
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import satori from "satori";
|
||||
import { ThemeDark } from "@notesnook/theme";
|
||||
import sharp from "sharp";
|
||||
import fontRegular from "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf?arraybuffer";
|
||||
import fontBold from "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf?arraybuffer";
|
||||
import {} from "lru-cache";
|
||||
import { Readable } from "node:stream";
|
||||
import { LRUCache } from "lru-cache";
|
||||
|
||||
export type OGMetadata = { title: string; description: string; date: string };
|
||||
|
||||
const cache = new LRUCache<string, string>({
|
||||
ttl: 1000 * 60 * 60 * 24,
|
||||
ttlAutopurge: true
|
||||
});
|
||||
|
||||
export async function makeImage(metadata: OGMetadata, cacheKey: string) {
|
||||
const theme = ThemeDark.scopes.base;
|
||||
|
||||
console.time("satori");
|
||||
let svg = cache.get(cacheKey);
|
||||
|
||||
if (!svg) {
|
||||
svg = await satori(
|
||||
<div
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
borderBottom: "10px solid #008837",
|
||||
backgroundColor: theme.primary.background,
|
||||
width: "100%",
|
||||
padding: 50,
|
||||
margin: 0
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 25,
|
||||
margin: 0,
|
||||
color: theme.secondary.paragraph
|
||||
}}
|
||||
>
|
||||
{metadata.date}
|
||||
</p>
|
||||
<h1
|
||||
style={{
|
||||
margin: 0,
|
||||
marginTop: 5,
|
||||
fontSize: 64,
|
||||
fontWeight: 600,
|
||||
color: theme.primary.heading
|
||||
}}
|
||||
>
|
||||
{metadata.title}
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
marginTop: 5,
|
||||
fontSize: 30,
|
||||
color: theme.primary.paragraph
|
||||
}}
|
||||
>
|
||||
{Buffer.from(metadata.description || "", "base64").toString(
|
||||
"utf-8"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "row", flexShrink: 0 }}>
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_25_12)">
|
||||
<path d="M1024 0H0V1024H1024V0Z" fill="white" />
|
||||
<path
|
||||
d="M736.652 699.667C719.397 750.078 684.817 792.732 639.064 820.039C593.311 847.346 539.354 857.535 486.794 848.792C434.234 840.049 386.481 812.942 352.032 772.294C317.583 731.646 298.673 680.095 298.667 626.812V516.562L377.788 549.615V626.767C377.78 647.546 382.221 668.085 390.812 687.005C399.402 705.924 411.943 722.785 427.592 736.455C430.562 739.042 433.644 741.562 436.828 743.914C460.184 761.302 488.23 771.266 517.322 772.511C518.312 772.511 519.268 772.59 520.247 772.612C521.225 772.635 522.497 772.612 523.622 772.612C524.747 772.612 525.872 772.612 526.997 772.612C528.122 772.612 528.932 772.612 529.922 772.511C559.002 771.263 587.038 761.308 610.393 743.936C613.565 741.585 616.648 739.076 619.629 736.489C640.186 718.51 655.286 695.123 663.212 668.989L736.652 699.667Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M748.667 430.748V626.813C748.667 629.344 748.667 631.887 748.509 634.418L669.545 601.399V430.748C669.533 393.064 654.939 356.847 628.82 329.682C602.702 302.518 567.086 286.514 529.431 285.022C491.777 283.53 455.006 296.666 426.82 321.679C398.634 346.692 381.221 381.641 378.227 419.206C377.945 423.008 377.788 426.867 377.788 430.748V479.461L298.667 446.386V205.748H523.667C583.34 205.748 640.57 229.453 682.766 271.649C724.961 313.845 748.667 371.074 748.667 430.748Z"
|
||||
fill="black"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_25_12">
|
||||
<rect width="1024" height="1024" rx="200" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", marginLeft: 15 }}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
color: theme.primary.heading
|
||||
}}
|
||||
>
|
||||
Notesnook Monograph
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 25,
|
||||
margin: 0,
|
||||
color: theme.secondary.paragraph
|
||||
}}
|
||||
>
|
||||
Anonymous, secure, and encrypted note sharing with password
|
||||
protection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Open Sans",
|
||||
data: fontRegular!,
|
||||
weight: 400,
|
||||
style: "normal"
|
||||
},
|
||||
{
|
||||
name: "Open Sans",
|
||||
data: fontBold!,
|
||||
weight: 600,
|
||||
style: "normal"
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
cache.set(cacheKey, svg);
|
||||
}
|
||||
console.timeEnd("satori");
|
||||
|
||||
return sharp(Buffer.from(svg)).png().toBuffer();
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
"@lingui/core": "4.11.4",
|
||||
"@lingui/react": "4.11.4",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@napi-rs/canvas": "0.1.59",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@notesnook/editor": "file:../../packages/editor",
|
||||
@@ -28,7 +29,7 @@
|
||||
"@sagi.io/workers-kv": "^0.0.14",
|
||||
"@theme-ui/components": "^0.16.2",
|
||||
"@theme-ui/core": "^0.16.2",
|
||||
"buffer": "^6.0.3",
|
||||
"canvas-hypertxt": "^1.0.3",
|
||||
"comlink": "^4.4.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
@@ -42,10 +43,7 @@
|
||||
"react-turnstile": "^1.1.4",
|
||||
"refractor": "^4.8.1",
|
||||
"remix-utils": "^7.7.0",
|
||||
"satori": "^0.11.1",
|
||||
"sharp": "^0.33.5",
|
||||
"slugify": "^1.6.6",
|
||||
"svg2png-wasm": "^1.4.1",
|
||||
"zustand": "^4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
32
apps/monograph/public/logo.svg
Normal file
32
apps/monograph/public/logo.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_25_12)">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="1024"
|
||||
height="1024"
|
||||
rx="130"
|
||||
ry="130"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M736.652 699.667C719.397 750.078 684.817 792.732 639.064 820.039C593.311 847.346 539.354 857.535 486.794 848.792C434.234 840.049 386.481 812.942 352.032 772.294C317.583 731.646 298.673 680.095 298.667 626.812V516.562L377.788 549.615V626.767C377.78 647.546 382.221 668.085 390.812 687.005C399.402 705.924 411.943 722.785 427.592 736.455C430.562 739.042 433.644 741.562 436.828 743.914C460.184 761.302 488.23 771.266 517.322 772.511C518.312 772.511 519.268 772.59 520.247 772.612C521.225 772.635 522.497 772.612 523.622 772.612C524.747 772.612 525.872 772.612 526.997 772.612C528.122 772.612 528.932 772.612 529.922 772.511C559.002 771.263 587.038 761.308 610.393 743.936C613.565 741.585 616.648 739.076 619.629 736.489C640.186 718.51 655.286 695.123 663.212 668.989L736.652 699.667Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M748.667 430.748V626.813C748.667 629.344 748.667 631.887 748.509 634.418L669.545 601.399V430.748C669.533 393.064 654.939 356.847 628.82 329.682C602.702 302.518 567.086 286.514 529.431 285.022C491.777 283.53 455.006 296.666 426.82 321.679C398.634 346.692 381.221 381.641 378.227 419.206C377.945 423.008 377.788 426.867 377.788 430.748V479.461L298.667 446.386V205.748H523.667C583.34 205.748 640.57 229.453 682.766 271.649C724.961 313.845 748.667 371.074 748.667 430.748Z"
|
||||
fill="black"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_25_12">
|
||||
<rect width="1024" height="1024" rx="200" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
Reference in New Issue
Block a user