erudit 4.3.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/composables/formatText.ts +9 -100
- package/app/composables/og.ts +58 -16
- package/app/pages/contributor/[contributorId].vue +1 -0
- package/app/pages/contributors.vue +1 -0
- package/app/pages/sponsors.vue +1 -0
- package/app/plugins/prerender.server.ts +1 -0
- package/nuxt.config.ts +1 -1
- package/package.json +9 -7
- package/server/api/prerender/ogImages.ts +46 -0
- package/server/erudit/ogImage/fonts/NotoSans-Bold.ttf +0 -0
- package/server/erudit/ogImage/fonts/NotoSans-Regular.ttf +0 -0
- package/server/erudit/ogImage/formatText.ts +12 -0
- package/server/erudit/ogImage/icons.ts +22 -0
- package/server/erudit/ogImage/logotype.ts +51 -0
- package/server/erudit/ogImage/render.ts +90 -0
- package/server/erudit/ogImage/shared.ts +320 -0
- package/server/erudit/ogImage/templates/content.ts +200 -0
- package/server/erudit/ogImage/templates/index.ts +138 -0
- package/server/erudit/ogImage/templates/sitePage.ts +110 -0
- package/server/erudit/staticFile.ts +3 -0
- package/server/routes/og/content/[...contentTypePath].ts +126 -0
- package/server/routes/og/site/contributor/[contributorId].ts +60 -0
- package/server/routes/og/site/contributors.png.ts +38 -0
- package/server/routes/og/site/index.png.ts +51 -0
- package/server/routes/og/site/sponsors.png.ts +38 -0
- package/shared/utils/formatText.ts +73 -0
- package/app/formatters/ru.ts +0 -14
- package/public/og.png +0 -0
|
@@ -1,100 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export let formatText: FormatText;
|
|
12
|
-
|
|
13
|
-
export async function initFormatText() {
|
|
14
|
-
const languageCode = ERUDIT.config.language.current;
|
|
15
|
-
|
|
16
|
-
const formatTextLoader =
|
|
17
|
-
languageCode in formatTextLoaders
|
|
18
|
-
? formatTextLoaders[languageCode]
|
|
19
|
-
: undefined;
|
|
20
|
-
|
|
21
|
-
let languageFormatText: LanguageFormatText = (text) => text;
|
|
22
|
-
if (formatTextLoader) {
|
|
23
|
-
languageFormatText = (await formatTextLoader()).default;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function _formatText(text: string, state?: FormatTextState): string;
|
|
27
|
-
function _formatText(text: undefined, state?: FormatTextState): undefined;
|
|
28
|
-
function _formatText(
|
|
29
|
-
text?: string,
|
|
30
|
-
state?: FormatTextState,
|
|
31
|
-
): string | undefined;
|
|
32
|
-
function _formatText(
|
|
33
|
-
text?: string,
|
|
34
|
-
state?: FormatTextState,
|
|
35
|
-
): string | undefined {
|
|
36
|
-
if (text === undefined) {
|
|
37
|
-
return text;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
// Normalize spacing (new lines, spaces)
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
text = text
|
|
46
|
-
.trim()
|
|
47
|
-
.replace(/\r\n/gm, '\n')
|
|
48
|
-
.replace(/\n{3,}/gm, '\n\n')
|
|
49
|
-
.replace(/[ \t]+/gm, ' ');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
// Normalize dashes
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
text = text.replace(/(^| )--($| )/gm, '$1—$2');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
// Normalize quotes
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
const quoteSymbols: [string, string] = (() => {
|
|
66
|
-
switch (languageCode) {
|
|
67
|
-
case 'ru':
|
|
68
|
-
return ['«', '»'];
|
|
69
|
-
default:
|
|
70
|
-
return ['“', '”'];
|
|
71
|
-
}
|
|
72
|
-
})();
|
|
73
|
-
|
|
74
|
-
let quoteOpen = state?.quote === 'opened';
|
|
75
|
-
text = text.replaceAll(/"/gm, () => {
|
|
76
|
-
quoteOpen = !quoteOpen;
|
|
77
|
-
if (state) {
|
|
78
|
-
state.quote = quoteOpen ? 'opened' : 'closed';
|
|
79
|
-
}
|
|
80
|
-
return quoteOpen ? quoteSymbols[0] : quoteSymbols[1];
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
// Normalize ellipsis
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
text = text.replace(/\.{3}/gm, '…');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
// Language-specific formatting
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
return languageFormatText(text);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
formatText = _formatText;
|
|
100
|
-
}
|
|
1
|
+
import type { FormatText } from '@erudit-js/core/formatText';
|
|
2
|
+
import { createFormatTextFn } from '../../shared/utils/formatText';
|
|
3
|
+
|
|
4
|
+
export let formatText: FormatText;
|
|
5
|
+
|
|
6
|
+
export async function initFormatText() {
|
|
7
|
+
const languageCode = ERUDIT.config.language.current;
|
|
8
|
+
formatText = createFormatTextFn(languageCode);
|
|
9
|
+
}
|
package/app/composables/og.ts
CHANGED
|
@@ -15,22 +15,18 @@ export function initOgSiteName() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function initOgImage() {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
width: ogImage.width,
|
|
31
|
-
height: ogImage.height,
|
|
32
|
-
},
|
|
33
|
-
});
|
|
18
|
+
const ogImageConfig = ERUDIT.config.seo?.ogImage;
|
|
19
|
+
|
|
20
|
+
if (ogImageConfig?.type === 'manual') {
|
|
21
|
+
const withSiteUrl = useSiteUrl();
|
|
22
|
+
useSeoMeta({
|
|
23
|
+
ogImage: {
|
|
24
|
+
url: withSiteUrl(ogImageConfig.src),
|
|
25
|
+
width: ogImageConfig.width,
|
|
26
|
+
height: ogImageConfig.height,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
export function useIndexSeo(indexPage: IndexPage) {
|
|
@@ -39,12 +35,25 @@ export function useIndexSeo(indexPage: IndexPage) {
|
|
|
39
35
|
description: indexPage.seo?.description || indexPage.description,
|
|
40
36
|
urlPath: '/',
|
|
41
37
|
});
|
|
38
|
+
|
|
39
|
+
const ogImageConfig = ERUDIT.config.seo?.ogImage;
|
|
40
|
+
if (ogImageConfig?.type === 'auto') {
|
|
41
|
+
const withSiteUrl = useSiteUrl();
|
|
42
|
+
useSeoMeta({
|
|
43
|
+
ogImage: {
|
|
44
|
+
url: withSiteUrl('/og/site/index.png'),
|
|
45
|
+
width: 1200,
|
|
46
|
+
height: 630,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
export function useStandartSeo(args: {
|
|
45
53
|
title: string;
|
|
46
54
|
description?: string;
|
|
47
55
|
urlPath: string;
|
|
56
|
+
ogImagePath?: string;
|
|
48
57
|
}) {
|
|
49
58
|
const seoSiteTitle =
|
|
50
59
|
ERUDIT.config.seo?.siteTitle ||
|
|
@@ -58,6 +67,20 @@ export function useStandartSeo(args: {
|
|
|
58
67
|
description: args.description,
|
|
59
68
|
urlPath: args.urlPath,
|
|
60
69
|
});
|
|
70
|
+
|
|
71
|
+
if (args.ogImagePath) {
|
|
72
|
+
const ogImageConfig = ERUDIT.config.seo?.ogImage;
|
|
73
|
+
if (ogImageConfig?.type === 'auto') {
|
|
74
|
+
const withSiteUrl = useSiteUrl();
|
|
75
|
+
useSeoMeta({
|
|
76
|
+
ogImage: {
|
|
77
|
+
url: withSiteUrl(args.ogImagePath),
|
|
78
|
+
width: 1200,
|
|
79
|
+
height: 630,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
61
84
|
}
|
|
62
85
|
|
|
63
86
|
export async function useContentSeo(args: {
|
|
@@ -100,6 +123,25 @@ export async function useContentSeo(args: {
|
|
|
100
123
|
|
|
101
124
|
setupSeo(baseSeo);
|
|
102
125
|
|
|
126
|
+
// Auto-generated OG image for content
|
|
127
|
+
// Manual OG image is handled globally by initOgImage() in app.vue
|
|
128
|
+
const ogImageConfig = ERUDIT.config.seo?.ogImage;
|
|
129
|
+
if (ogImageConfig?.type === 'auto') {
|
|
130
|
+
const withSiteUrl = useSiteUrl();
|
|
131
|
+
const ogTypePart =
|
|
132
|
+
args.contentTypePath.type === 'topic'
|
|
133
|
+
? args.contentTypePath.topicPart
|
|
134
|
+
: args.contentTypePath.type;
|
|
135
|
+
const ogPath = `/og/content/${ogTypePart}/${args.contentTypePath.contentId}.png`;
|
|
136
|
+
useSeoMeta({
|
|
137
|
+
ogImage: {
|
|
138
|
+
url: withSiteUrl(ogPath),
|
|
139
|
+
width: 1200,
|
|
140
|
+
height: 630,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
103
145
|
//
|
|
104
146
|
// SEO snippets
|
|
105
147
|
//
|
package/app/pages/sponsors.vue
CHANGED
package/nuxt.config.ts
CHANGED
|
@@ -53,7 +53,7 @@ export default defineNuxtConfig({
|
|
|
53
53
|
rollupConfig: {
|
|
54
54
|
// Prevent inlining some packages
|
|
55
55
|
external(source) {
|
|
56
|
-
const ignore = ['jiti', 'tsprose'];
|
|
56
|
+
const ignore = ['jiti', 'tsprose', '@resvg/resvg-js'];
|
|
57
57
|
|
|
58
58
|
for (const ignoreItem of ignore) {
|
|
59
59
|
if (source.includes(ignoreItem)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "erudit",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "🤓 CMS for perfect educational sites.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,24 +24,26 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@erudit-js/cli": "4.3.
|
|
28
|
-
"@erudit-js/core": "4.3.
|
|
29
|
-
"@erudit-js/prose": "4.3.
|
|
27
|
+
"@erudit-js/cli": "4.3.1",
|
|
28
|
+
"@erudit-js/core": "4.3.1",
|
|
29
|
+
"@erudit-js/prose": "4.3.1",
|
|
30
30
|
"unslash": "^2.0.0",
|
|
31
31
|
"@floating-ui/vue": "^1.1.11",
|
|
32
32
|
"tsprose": "^1.0.1",
|
|
33
33
|
"@tailwindcss/vite": "^4.2.1",
|
|
34
|
-
"better-sqlite3": "^12.
|
|
34
|
+
"better-sqlite3": "^12.8.0",
|
|
35
35
|
"chokidar": "^5.0.0",
|
|
36
36
|
"consola": "^3.4.2",
|
|
37
37
|
"drizzle-kit": "^0.31.9",
|
|
38
38
|
"drizzle-orm": "^0.45.1",
|
|
39
|
-
"esbuild": "^0.27.
|
|
39
|
+
"esbuild": "^0.27.4",
|
|
40
40
|
"flexsearch": "^0.8.212",
|
|
41
41
|
"glob": "^13.0.6",
|
|
42
42
|
"image-size": "^2.0.2",
|
|
43
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
44
|
+
"satori": "^0.25.0",
|
|
43
45
|
"jiti": "^2.6.1",
|
|
44
|
-
"nuxt": "4.
|
|
46
|
+
"nuxt": "4.4.2",
|
|
45
47
|
"nuxt-my-icons": "1.2.2",
|
|
46
48
|
"perfect-debounce": "^2.1.0",
|
|
47
49
|
"tailwindcss": "^4.2.1",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export default defineEventHandler(async () => {
|
|
2
|
+
const ogImageConfig = ERUDIT.config.public.seo?.ogImage;
|
|
3
|
+
if (ogImageConfig?.type !== 'auto') {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const routes: string[] = [];
|
|
8
|
+
|
|
9
|
+
// Index page
|
|
10
|
+
routes.push('/og/site/index.png');
|
|
11
|
+
|
|
12
|
+
// Contributors page
|
|
13
|
+
if (ERUDIT.config.public.contributors?.enabled) {
|
|
14
|
+
routes.push('/og/site/contributors.png');
|
|
15
|
+
|
|
16
|
+
const dbContributors = await ERUDIT.db.query.contributors.findMany({
|
|
17
|
+
columns: { contributorId: true },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
for (const dbContributor of dbContributors) {
|
|
21
|
+
routes.push(`/og/site/contributor/${dbContributor.contributorId}.png`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Sponsors page
|
|
26
|
+
if (ERUDIT.config.public.sponsors?.enabled) {
|
|
27
|
+
routes.push('/og/site/sponsors.png');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Content pages
|
|
31
|
+
for (const navNode of ERUDIT.contentNav.id2Node.values()) {
|
|
32
|
+
if (navNode.type === 'topic') {
|
|
33
|
+
const topicParts = await ERUDIT.repository.content.topicParts(
|
|
34
|
+
navNode.fullId,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
for (const part of topicParts) {
|
|
38
|
+
routes.push(`/og/content/${part}/${navNode.shortId}.png`);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
routes.push(`/og/content/${navNode.type}/${navNode.shortId}.png`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return routes;
|
|
46
|
+
});
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FormatText } from '@erudit-js/core/formatText';
|
|
2
|
+
import { createFormatTextFn } from '../../../shared/utils/formatText';
|
|
3
|
+
|
|
4
|
+
let _ogFormatText: FormatText | undefined;
|
|
5
|
+
|
|
6
|
+
export function ogFormatText(text: string): string {
|
|
7
|
+
if (!_ogFormatText) {
|
|
8
|
+
const languageCode = ERUDIT.config.public.language.current;
|
|
9
|
+
_ogFormatText = createFormatTextFn(languageCode);
|
|
10
|
+
}
|
|
11
|
+
return _ogFormatText(text);
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { ICONS } from '../../../shared/utils/icons';
|
|
3
|
+
|
|
4
|
+
const ICON_MAP: Record<string, string> = {
|
|
5
|
+
...ICONS,
|
|
6
|
+
contributors: 'users',
|
|
7
|
+
contributor: 'user',
|
|
8
|
+
sponsors: 'diamond',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const iconCache = new Map<string, string>();
|
|
12
|
+
|
|
13
|
+
export function getIconSvg(contentType: string): string {
|
|
14
|
+
const cached = iconCache.get(contentType);
|
|
15
|
+
if (cached) return cached;
|
|
16
|
+
|
|
17
|
+
const iconName = ICON_MAP[contentType] || 'lines';
|
|
18
|
+
const iconPath = ERUDIT.paths.erudit('app/assets/icons', iconName + '.svg');
|
|
19
|
+
const svg = readFileSync(iconPath, 'utf-8');
|
|
20
|
+
iconCache.set(contentType, svg);
|
|
21
|
+
return svg;
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
|
|
4
|
+
let cachedLogotypeDataUri: string | undefined | null;
|
|
5
|
+
|
|
6
|
+
export async function loadLogotypeDataUri(
|
|
7
|
+
explicitPath?: string,
|
|
8
|
+
): Promise<string | undefined> {
|
|
9
|
+
if (cachedLogotypeDataUri !== undefined) {
|
|
10
|
+
return cachedLogotypeDataUri ?? undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const logotypeUrl =
|
|
14
|
+
explicitPath || ERUDIT.config.public.asideMajor?.siteInfo?.logotype;
|
|
15
|
+
if (!logotypeUrl || logotypeUrl === '') {
|
|
16
|
+
cachedLogotypeDataUri = null;
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (logotypeUrl.startsWith('http://') || logotypeUrl.startsWith('https://')) {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(logotypeUrl);
|
|
23
|
+
if (response.ok) {
|
|
24
|
+
const contentType =
|
|
25
|
+
response.headers.get('content-type') || 'image/svg+xml';
|
|
26
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
27
|
+
cachedLogotypeDataUri = `data:${contentType};base64,${buffer.toString('base64')}`;
|
|
28
|
+
return cachedLogotypeDataUri;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Ignore fetch errors
|
|
32
|
+
}
|
|
33
|
+
cachedLogotypeDataUri = null;
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Local path — resolve relative to project root, or use as-is if absolute
|
|
38
|
+
const localPath = isAbsolute(logotypeUrl)
|
|
39
|
+
? logotypeUrl
|
|
40
|
+
: ERUDIT.paths.project(logotypeUrl.replace(/^\/+/, ''));
|
|
41
|
+
if (existsSync(localPath)) {
|
|
42
|
+
const buffer = readFileSync(localPath);
|
|
43
|
+
const ext = logotypeUrl.split('.').pop()?.toLowerCase();
|
|
44
|
+
const mime = ext === 'svg' ? 'image/svg+xml' : `image/${ext}`;
|
|
45
|
+
cachedLogotypeDataUri = `data:${mime};base64,${buffer.toString('base64')}`;
|
|
46
|
+
return cachedLogotypeDataUri;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
cachedLogotypeDataUri = null;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import satori from 'satori';
|
|
3
|
+
import { Resvg } from '@resvg/resvg-js';
|
|
4
|
+
import { type SatoriNode, OG_WIDTH, OG_HEIGHT } from './shared';
|
|
5
|
+
|
|
6
|
+
let regularFontData: ArrayBuffer | undefined;
|
|
7
|
+
let boldFontData: ArrayBuffer | undefined;
|
|
8
|
+
|
|
9
|
+
function loadFontFile(filename: string): ArrayBuffer {
|
|
10
|
+
const fontPath = ERUDIT.paths.erudit('server/erudit/ogImage/fonts', filename);
|
|
11
|
+
const buffer = readFileSync(fontPath);
|
|
12
|
+
return buffer.buffer.slice(
|
|
13
|
+
buffer.byteOffset,
|
|
14
|
+
buffer.byteOffset + buffer.byteLength,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadFonts(): { regular: ArrayBuffer; bold: ArrayBuffer } {
|
|
19
|
+
if (!regularFontData) {
|
|
20
|
+
regularFontData = loadFontFile('NotoSans-Regular.ttf');
|
|
21
|
+
}
|
|
22
|
+
if (!boldFontData) {
|
|
23
|
+
boldFontData = loadFontFile('NotoSans-Bold.ttf');
|
|
24
|
+
}
|
|
25
|
+
return { regular: regularFontData, bold: boldFontData };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CACHE_CAPACITY = 50;
|
|
29
|
+
const cacheSlotKeys: (string | undefined)[] = new Array(CACHE_CAPACITY);
|
|
30
|
+
const cacheSlotValues: (Buffer | undefined)[] = new Array(CACHE_CAPACITY);
|
|
31
|
+
const cacheKeyToSlot = new Map<string, number>();
|
|
32
|
+
let cachePointer = 0;
|
|
33
|
+
|
|
34
|
+
export async function renderOgImage(
|
|
35
|
+
cacheKey: string,
|
|
36
|
+
template: SatoriNode,
|
|
37
|
+
): Promise<Buffer> {
|
|
38
|
+
const slot = cacheKeyToSlot.get(cacheKey);
|
|
39
|
+
if (slot !== undefined) return cacheSlotValues[slot]!;
|
|
40
|
+
|
|
41
|
+
const fonts = loadFonts();
|
|
42
|
+
|
|
43
|
+
const svg = await satori(template as any, {
|
|
44
|
+
width: OG_WIDTH,
|
|
45
|
+
height: OG_HEIGHT,
|
|
46
|
+
fonts: [
|
|
47
|
+
{
|
|
48
|
+
name: 'Noto Sans',
|
|
49
|
+
data: fonts.regular,
|
|
50
|
+
weight: 400,
|
|
51
|
+
style: 'normal',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Noto Sans',
|
|
55
|
+
data: fonts.bold,
|
|
56
|
+
weight: 600,
|
|
57
|
+
style: 'normal',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Noto Sans',
|
|
61
|
+
data: fonts.bold,
|
|
62
|
+
weight: 700,
|
|
63
|
+
style: 'normal',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const resvg = new Resvg(svg, {
|
|
69
|
+
fitTo: {
|
|
70
|
+
mode: 'width',
|
|
71
|
+
value: OG_WIDTH,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const pngData = resvg.render();
|
|
76
|
+
const pngBuffer = Buffer.from(pngData.asPng());
|
|
77
|
+
|
|
78
|
+
// Evict oldest entry when the circular buffer is full
|
|
79
|
+
const evictedKey = cacheSlotKeys[cachePointer];
|
|
80
|
+
if (evictedKey !== undefined) {
|
|
81
|
+
cacheKeyToSlot.delete(evictedKey);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
cacheSlotKeys[cachePointer] = cacheKey;
|
|
85
|
+
cacheSlotValues[cachePointer] = pngBuffer;
|
|
86
|
+
cacheKeyToSlot.set(cacheKey, cachePointer);
|
|
87
|
+
cachePointer = (cachePointer + 1) % CACHE_CAPACITY;
|
|
88
|
+
|
|
89
|
+
return pngBuffer;
|
|
90
|
+
}
|