erudit 4.3.0 → 4.3.1-dev.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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
export type SatoriNode =
|
|
2
|
+
| string
|
|
3
|
+
| number
|
|
4
|
+
| {
|
|
5
|
+
type: string;
|
|
6
|
+
props: {
|
|
7
|
+
style?: Record<string, unknown>;
|
|
8
|
+
children?: SatoriNode | SatoriNode[];
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const OG_WIDTH = 1200;
|
|
14
|
+
export const OG_HEIGHT = 630;
|
|
15
|
+
export const DIM_COLOR = '#555555';
|
|
16
|
+
|
|
17
|
+
export function svgToDataUri(svg: string, fill?: string): string {
|
|
18
|
+
let processed = svg;
|
|
19
|
+
if (fill) {
|
|
20
|
+
processed = processed.replace(/<path /g, `<path fill="${fill}" `);
|
|
21
|
+
}
|
|
22
|
+
return 'data:image/svg+xml,' + encodeURIComponent(processed);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mixColors(hex1: string, hex2: string, ratio: number): string {
|
|
26
|
+
const r1 = parseInt(hex1.slice(1, 3), 16);
|
|
27
|
+
const g1 = parseInt(hex1.slice(3, 5), 16);
|
|
28
|
+
const b1 = parseInt(hex1.slice(5, 7), 16);
|
|
29
|
+
const r2 = parseInt(hex2.slice(1, 3), 16);
|
|
30
|
+
const g2 = parseInt(hex2.slice(3, 5), 16);
|
|
31
|
+
const b2 = parseInt(hex2.slice(5, 7), 16);
|
|
32
|
+
const r = Math.round(r1 * ratio + r2 * (1 - ratio));
|
|
33
|
+
const g = Math.round(g1 * ratio + g2 * (1 - ratio));
|
|
34
|
+
const b = Math.round(b1 * ratio + b2 * (1 - ratio));
|
|
35
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mixWithWhite(hex: string, ratio: number): string {
|
|
39
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
40
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
41
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
42
|
+
const mr = Math.round(r + (255 - r) * (1 - ratio));
|
|
43
|
+
const mg = Math.round(g + (255 - g) * (1 - ratio));
|
|
44
|
+
const mb = Math.round(b + (255 - b) * (1 - ratio));
|
|
45
|
+
return `rgb(${mr}, ${mg}, ${mb})`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function truncate(text: string, maxLen: number): string {
|
|
49
|
+
if (text.length <= maxLen) return text;
|
|
50
|
+
return text.slice(0, maxLen - 1).trimEnd() + '\u2026';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ogBrandGradient(brandColor: string): string {
|
|
54
|
+
const brandMixed = mixWithWhite(brandColor, 0.5);
|
|
55
|
+
return `linear-gradient(to bottom right, ${brandMixed}, #ffffff)`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function ogTitleColor(brandColor: string): string {
|
|
59
|
+
return mixColors(brandColor, '#1a1a1a', 0.6);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function ogRootContainer(
|
|
63
|
+
brandColor: string,
|
|
64
|
+
children: SatoriNode[],
|
|
65
|
+
): SatoriNode {
|
|
66
|
+
return {
|
|
67
|
+
type: 'div',
|
|
68
|
+
props: {
|
|
69
|
+
style: {
|
|
70
|
+
display: 'flex',
|
|
71
|
+
flexDirection: 'column' as const,
|
|
72
|
+
width: OG_WIDTH,
|
|
73
|
+
height: OG_HEIGHT,
|
|
74
|
+
background: ogBrandGradient(brandColor),
|
|
75
|
+
fontFamily: 'Noto Sans',
|
|
76
|
+
position: 'relative' as const,
|
|
77
|
+
},
|
|
78
|
+
children,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function ogTopRow(params: {
|
|
84
|
+
logotypeDataUri?: string;
|
|
85
|
+
siteName?: string;
|
|
86
|
+
formatText?: (text: string) => string;
|
|
87
|
+
paddingRight?: number;
|
|
88
|
+
}): SatoriNode | undefined {
|
|
89
|
+
const fmt = params.formatText ?? ((t: string) => t);
|
|
90
|
+
const items: SatoriNode[] = [];
|
|
91
|
+
|
|
92
|
+
if (params.logotypeDataUri) {
|
|
93
|
+
items.push({
|
|
94
|
+
type: 'div',
|
|
95
|
+
props: {
|
|
96
|
+
style: {
|
|
97
|
+
display: 'flex',
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
justifyContent: 'center',
|
|
100
|
+
width: 64,
|
|
101
|
+
height: 64,
|
|
102
|
+
borderRadius: '50%',
|
|
103
|
+
backgroundColor: 'white',
|
|
104
|
+
border: '2px solid white',
|
|
105
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
106
|
+
overflow: 'hidden',
|
|
107
|
+
},
|
|
108
|
+
children: [
|
|
109
|
+
{
|
|
110
|
+
type: 'img',
|
|
111
|
+
props: {
|
|
112
|
+
src: params.logotypeDataUri,
|
|
113
|
+
width: 54,
|
|
114
|
+
height: 54,
|
|
115
|
+
style: { objectFit: 'contain' as const },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (params.siteName) {
|
|
124
|
+
items.push({
|
|
125
|
+
type: 'span',
|
|
126
|
+
props: {
|
|
127
|
+
style: {
|
|
128
|
+
fontSize: 36,
|
|
129
|
+
color: DIM_COLOR,
|
|
130
|
+
fontWeight: 600,
|
|
131
|
+
},
|
|
132
|
+
children: fmt(params.siteName),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (items.length === 0) return undefined;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
type: 'div',
|
|
141
|
+
props: {
|
|
142
|
+
style: {
|
|
143
|
+
display: 'flex',
|
|
144
|
+
alignItems: 'center',
|
|
145
|
+
gap: 14,
|
|
146
|
+
paddingLeft: 56,
|
|
147
|
+
paddingRight: params.paddingRight ?? 60,
|
|
148
|
+
paddingTop: 48,
|
|
149
|
+
},
|
|
150
|
+
children: items,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function ogDescription(params: {
|
|
156
|
+
description: string;
|
|
157
|
+
formatText?: (text: string) => string;
|
|
158
|
+
maxLen?: number;
|
|
159
|
+
paddingRight?: number;
|
|
160
|
+
}): SatoriNode {
|
|
161
|
+
const fmt = params.formatText ?? ((t: string) => t);
|
|
162
|
+
const trimmed = truncate(params.description, params.maxLen ?? 360);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
type: 'div',
|
|
166
|
+
props: {
|
|
167
|
+
style: {
|
|
168
|
+
display: 'flex',
|
|
169
|
+
paddingLeft: 60,
|
|
170
|
+
paddingRight: params.paddingRight ?? 60,
|
|
171
|
+
paddingBottom: 48,
|
|
172
|
+
},
|
|
173
|
+
children: [
|
|
174
|
+
{
|
|
175
|
+
type: 'div',
|
|
176
|
+
props: {
|
|
177
|
+
style: {
|
|
178
|
+
fontSize: 24,
|
|
179
|
+
fontWeight: 400,
|
|
180
|
+
color: DIM_COLOR,
|
|
181
|
+
textAlign: 'left' as const,
|
|
182
|
+
lineHeight: 1.4,
|
|
183
|
+
wordBreak: 'break-word' as const,
|
|
184
|
+
},
|
|
185
|
+
children: fmt(trimmed),
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const ARROW_RIGHT_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>`;
|
|
194
|
+
|
|
195
|
+
export function ogActionButton(brandColor: string, text: string): SatoriNode {
|
|
196
|
+
return {
|
|
197
|
+
type: 'div',
|
|
198
|
+
props: {
|
|
199
|
+
style: {
|
|
200
|
+
display: 'flex',
|
|
201
|
+
alignItems: 'center',
|
|
202
|
+
gap: 10,
|
|
203
|
+
marginTop: 24,
|
|
204
|
+
backgroundColor: brandColor,
|
|
205
|
+
borderRadius: 32,
|
|
206
|
+
paddingLeft: 32,
|
|
207
|
+
paddingRight: 26,
|
|
208
|
+
paddingTop: 14,
|
|
209
|
+
paddingBottom: 14,
|
|
210
|
+
boxShadow: `0 6px 20px rgba(0,0,0,0.25), 0 2px 6px rgba(0,0,0,0.1)`,
|
|
211
|
+
alignSelf: 'flex-start' as const,
|
|
212
|
+
border: '3px solid rgba(255,255,255,0.4)',
|
|
213
|
+
},
|
|
214
|
+
children: [
|
|
215
|
+
{
|
|
216
|
+
type: 'span',
|
|
217
|
+
props: {
|
|
218
|
+
style: {
|
|
219
|
+
fontSize: 24,
|
|
220
|
+
fontWeight: 700,
|
|
221
|
+
color: '#ffffff',
|
|
222
|
+
letterSpacing: 0.5,
|
|
223
|
+
},
|
|
224
|
+
children: text,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: 'img',
|
|
229
|
+
props: {
|
|
230
|
+
src: 'data:image/svg+xml,' + encodeURIComponent(ARROW_RIGHT_SVG),
|
|
231
|
+
width: 22,
|
|
232
|
+
height: 22,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function ogBottomSpacer(): SatoriNode {
|
|
241
|
+
return {
|
|
242
|
+
type: 'div',
|
|
243
|
+
props: {
|
|
244
|
+
style: { display: 'flex', paddingTop: 80 },
|
|
245
|
+
children: [],
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function sendOgPng(event: Parameters<typeof setHeader>[0], png: Buffer) {
|
|
251
|
+
setHeader(event, 'Content-Type', 'image/png');
|
|
252
|
+
setHeader(event, 'Cache-Control', 'public, max-age=86400, s-maxage=86400');
|
|
253
|
+
return png;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getOgImageConfig() {
|
|
257
|
+
return ERUDIT.config.public.seo?.ogImage;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function checkOgEnabled() {
|
|
261
|
+
const ogImageConfig = getOgImageConfig();
|
|
262
|
+
if (ogImageConfig?.type !== 'auto') {
|
|
263
|
+
throw createError({
|
|
264
|
+
statusCode: 404,
|
|
265
|
+
statusMessage: 'OG image generation is disabled',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function getOgBrandColor(): string {
|
|
271
|
+
const ogImageConfig = getOgImageConfig();
|
|
272
|
+
if (ogImageConfig?.type === 'auto') {
|
|
273
|
+
return ogImageConfig.siteColor;
|
|
274
|
+
}
|
|
275
|
+
return '#4aa44c';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function getOgSiteName(): string | undefined {
|
|
279
|
+
const ogImageConfig = getOgImageConfig();
|
|
280
|
+
if (ogImageConfig?.type === 'auto') {
|
|
281
|
+
return ogImageConfig.siteName;
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function getOgSiteShort(): string | undefined {
|
|
287
|
+
const ogImageConfig = getOgImageConfig();
|
|
288
|
+
if (ogImageConfig?.type === 'auto') {
|
|
289
|
+
return ogImageConfig.siteShort;
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function getOgLearnPhrase(): string {
|
|
295
|
+
const ogImageConfig = getOgImageConfig();
|
|
296
|
+
if (ogImageConfig?.type === 'auto') {
|
|
297
|
+
return typeof ogImageConfig.phrases === 'string'
|
|
298
|
+
? ogImageConfig.phrases
|
|
299
|
+
: ogImageConfig.phrases.learn;
|
|
300
|
+
}
|
|
301
|
+
return 'Learn';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function getOgOpenPhrase(): string {
|
|
305
|
+
const ogImageConfig = getOgImageConfig();
|
|
306
|
+
if (ogImageConfig?.type === 'auto') {
|
|
307
|
+
return typeof ogImageConfig.phrases === 'string'
|
|
308
|
+
? ogImageConfig.phrases
|
|
309
|
+
: ogImageConfig.phrases.open;
|
|
310
|
+
}
|
|
311
|
+
return 'Open';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function getOgLogotypePath(): string | undefined {
|
|
315
|
+
const ogImageConfig = getOgImageConfig();
|
|
316
|
+
if (ogImageConfig?.type === 'auto') {
|
|
317
|
+
return ogImageConfig.logotype;
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type SatoriNode,
|
|
3
|
+
DIM_COLOR,
|
|
4
|
+
svgToDataUri,
|
|
5
|
+
truncate,
|
|
6
|
+
ogTitleColor,
|
|
7
|
+
ogRootContainer,
|
|
8
|
+
ogTopRow,
|
|
9
|
+
ogDescription,
|
|
10
|
+
ogBottomSpacer,
|
|
11
|
+
ogActionButton,
|
|
12
|
+
} from '../shared';
|
|
13
|
+
|
|
14
|
+
export interface ContentOgParams {
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
contentLabel: string;
|
|
18
|
+
contentIconSvg: string;
|
|
19
|
+
bookTitle?: string;
|
|
20
|
+
bookIconSvg?: string;
|
|
21
|
+
isBook: boolean;
|
|
22
|
+
decorationDataUri?: string;
|
|
23
|
+
logotypeDataUri?: string;
|
|
24
|
+
siteName?: string;
|
|
25
|
+
brandColor: string;
|
|
26
|
+
formatText?: (text: string) => string;
|
|
27
|
+
learnButtonText?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildContentOgTemplate(params: ContentOgParams): SatoriNode {
|
|
31
|
+
const titleColor = ogTitleColor(params.brandColor);
|
|
32
|
+
const fmt = params.formatText ?? ((t: string) => t);
|
|
33
|
+
|
|
34
|
+
const children: SatoriNode[] = [];
|
|
35
|
+
|
|
36
|
+
// Top-right: decoration image
|
|
37
|
+
if (params.decorationDataUri) {
|
|
38
|
+
children.push({
|
|
39
|
+
type: 'div',
|
|
40
|
+
props: {
|
|
41
|
+
style: {
|
|
42
|
+
display: 'flex',
|
|
43
|
+
position: 'absolute',
|
|
44
|
+
top: 24,
|
|
45
|
+
right: 32,
|
|
46
|
+
},
|
|
47
|
+
children: [
|
|
48
|
+
{
|
|
49
|
+
type: 'img',
|
|
50
|
+
props: {
|
|
51
|
+
src: params.decorationDataUri,
|
|
52
|
+
width: 180,
|
|
53
|
+
height: 180,
|
|
54
|
+
style: { objectFit: 'contain' as const, opacity: 0.6 },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Top row: site logo + name
|
|
63
|
+
const top = ogTopRow({
|
|
64
|
+
logotypeDataUri: params.logotypeDataUri,
|
|
65
|
+
siteName: params.siteName,
|
|
66
|
+
formatText: params.formatText,
|
|
67
|
+
paddingRight: 240,
|
|
68
|
+
});
|
|
69
|
+
if (top) children.push(top);
|
|
70
|
+
|
|
71
|
+
// Center section: content info + title + learn button
|
|
72
|
+
const centerContent: SatoriNode[] = [];
|
|
73
|
+
|
|
74
|
+
// Info row: book icon+title / separator / content icon+label
|
|
75
|
+
const infoRow: SatoriNode[] = [];
|
|
76
|
+
|
|
77
|
+
if (params.bookTitle && params.bookIconSvg && !params.isBook) {
|
|
78
|
+
const bookTitleTruncated = truncate(params.bookTitle, 40);
|
|
79
|
+
infoRow.push({
|
|
80
|
+
type: 'img',
|
|
81
|
+
props: {
|
|
82
|
+
src: svgToDataUri(params.bookIconSvg, DIM_COLOR),
|
|
83
|
+
width: 26,
|
|
84
|
+
height: 26,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
infoRow.push({
|
|
88
|
+
type: 'span',
|
|
89
|
+
props: {
|
|
90
|
+
style: {
|
|
91
|
+
fontSize: 24,
|
|
92
|
+
color: DIM_COLOR,
|
|
93
|
+
fontWeight: 600,
|
|
94
|
+
maxWidth: 500,
|
|
95
|
+
overflow: 'hidden',
|
|
96
|
+
whiteSpace: 'nowrap' as const,
|
|
97
|
+
},
|
|
98
|
+
children: fmt(bookTitleTruncated),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
infoRow.push({
|
|
102
|
+
type: 'span',
|
|
103
|
+
props: {
|
|
104
|
+
style: {
|
|
105
|
+
fontSize: 24,
|
|
106
|
+
color: DIM_COLOR,
|
|
107
|
+
fontWeight: 400,
|
|
108
|
+
marginLeft: 6,
|
|
109
|
+
marginRight: 6,
|
|
110
|
+
},
|
|
111
|
+
children: '/',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
infoRow.push({
|
|
117
|
+
type: 'img',
|
|
118
|
+
props: {
|
|
119
|
+
src: svgToDataUri(params.contentIconSvg, DIM_COLOR),
|
|
120
|
+
width: 24,
|
|
121
|
+
height: 24,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
infoRow.push({
|
|
125
|
+
type: 'span',
|
|
126
|
+
props: {
|
|
127
|
+
style: {
|
|
128
|
+
fontSize: 24,
|
|
129
|
+
color: DIM_COLOR,
|
|
130
|
+
fontWeight: 600,
|
|
131
|
+
},
|
|
132
|
+
children: fmt(params.contentLabel),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
centerContent.push({
|
|
137
|
+
type: 'div',
|
|
138
|
+
props: {
|
|
139
|
+
style: { display: 'flex', alignItems: 'center', gap: 8 },
|
|
140
|
+
children: infoRow,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Content title
|
|
145
|
+
const titleTruncated = truncate(params.title, 80);
|
|
146
|
+
centerContent.push({
|
|
147
|
+
type: 'div',
|
|
148
|
+
props: {
|
|
149
|
+
style: {
|
|
150
|
+
fontSize: 52,
|
|
151
|
+
fontWeight: 700,
|
|
152
|
+
color: titleColor,
|
|
153
|
+
textAlign: 'left' as const,
|
|
154
|
+
lineHeight: 1.25,
|
|
155
|
+
maxHeight: 200,
|
|
156
|
+
overflow: 'hidden',
|
|
157
|
+
wordBreak: 'break-word' as const,
|
|
158
|
+
marginTop: 12,
|
|
159
|
+
},
|
|
160
|
+
children: fmt(titleTruncated),
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Learn button
|
|
165
|
+
if (params.learnButtonText) {
|
|
166
|
+
centerContent.push(
|
|
167
|
+
ogActionButton(params.brandColor, params.learnButtonText),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
children.push({
|
|
172
|
+
type: 'div',
|
|
173
|
+
props: {
|
|
174
|
+
style: {
|
|
175
|
+
display: 'flex',
|
|
176
|
+
flexDirection: 'column' as const,
|
|
177
|
+
justifyContent: 'center',
|
|
178
|
+
flex: 1,
|
|
179
|
+
paddingLeft: 60,
|
|
180
|
+
paddingRight: 240,
|
|
181
|
+
},
|
|
182
|
+
children: centerContent,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Bottom: description or spacer
|
|
187
|
+
if (params.description) {
|
|
188
|
+
children.push(
|
|
189
|
+
ogDescription({
|
|
190
|
+
description: params.description,
|
|
191
|
+
formatText: params.formatText,
|
|
192
|
+
paddingRight: 240,
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
} else {
|
|
196
|
+
children.push(ogBottomSpacer());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return ogRootContainer(params.brandColor, children);
|
|
200
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type SatoriNode,
|
|
3
|
+
DIM_COLOR,
|
|
4
|
+
truncate,
|
|
5
|
+
ogTitleColor,
|
|
6
|
+
ogRootContainer,
|
|
7
|
+
} from '../shared';
|
|
8
|
+
|
|
9
|
+
export interface IndexOgParams {
|
|
10
|
+
logotypeDataUri?: string;
|
|
11
|
+
siteName?: string;
|
|
12
|
+
short?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
brandColor: string;
|
|
15
|
+
formatText?: (text: string) => string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildIndexOgTemplate(params: IndexOgParams): SatoriNode {
|
|
19
|
+
const titleColor = ogTitleColor(params.brandColor);
|
|
20
|
+
const fmt = params.formatText ?? ((t: string) => t);
|
|
21
|
+
|
|
22
|
+
const children: SatoriNode[] = [];
|
|
23
|
+
|
|
24
|
+
// Centered logotype + site name
|
|
25
|
+
const centerColumn: SatoriNode[] = [];
|
|
26
|
+
|
|
27
|
+
if (params.logotypeDataUri) {
|
|
28
|
+
centerColumn.push({
|
|
29
|
+
type: 'div',
|
|
30
|
+
props: {
|
|
31
|
+
style: {
|
|
32
|
+
display: 'flex',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
justifyContent: 'center',
|
|
35
|
+
width: 96,
|
|
36
|
+
height: 96,
|
|
37
|
+
borderRadius: '50%',
|
|
38
|
+
backgroundColor: 'white',
|
|
39
|
+
border: '3px solid white',
|
|
40
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
|
|
41
|
+
overflow: 'hidden',
|
|
42
|
+
},
|
|
43
|
+
children: [
|
|
44
|
+
{
|
|
45
|
+
type: 'img',
|
|
46
|
+
props: {
|
|
47
|
+
src: params.logotypeDataUri,
|
|
48
|
+
width: 80,
|
|
49
|
+
height: 80,
|
|
50
|
+
style: { objectFit: 'contain' as const },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (params.siteName) {
|
|
59
|
+
centerColumn.push({
|
|
60
|
+
type: 'span',
|
|
61
|
+
props: {
|
|
62
|
+
style: {
|
|
63
|
+
fontSize: 56,
|
|
64
|
+
fontWeight: 700,
|
|
65
|
+
color: titleColor,
|
|
66
|
+
textAlign: 'center' as const,
|
|
67
|
+
},
|
|
68
|
+
children: fmt(params.siteName),
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (params.short) {
|
|
74
|
+
centerColumn.push({
|
|
75
|
+
type: 'span',
|
|
76
|
+
props: {
|
|
77
|
+
style: {
|
|
78
|
+
fontSize: 36,
|
|
79
|
+
fontWeight: 600,
|
|
80
|
+
color: DIM_COLOR,
|
|
81
|
+
lineHeight: 1.3,
|
|
82
|
+
textAlign: 'center' as const,
|
|
83
|
+
},
|
|
84
|
+
children: fmt(params.short),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
children.push({
|
|
90
|
+
type: 'div',
|
|
91
|
+
props: {
|
|
92
|
+
style: {
|
|
93
|
+
display: 'flex',
|
|
94
|
+
flex: 1,
|
|
95
|
+
flexDirection: 'column' as const,
|
|
96
|
+
alignItems: 'center',
|
|
97
|
+
justifyContent: 'center',
|
|
98
|
+
gap: 12,
|
|
99
|
+
paddingTop: params.description ? 40 : 0,
|
|
100
|
+
},
|
|
101
|
+
children: centerColumn,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Bottom description
|
|
106
|
+
if (params.description) {
|
|
107
|
+
const trimmed = truncate(params.description, 200);
|
|
108
|
+
children.push({
|
|
109
|
+
type: 'div',
|
|
110
|
+
props: {
|
|
111
|
+
style: {
|
|
112
|
+
display: 'flex',
|
|
113
|
+
justifyContent: 'center',
|
|
114
|
+
paddingLeft: 80,
|
|
115
|
+
paddingRight: 80,
|
|
116
|
+
paddingBottom: 56,
|
|
117
|
+
},
|
|
118
|
+
children: [
|
|
119
|
+
{
|
|
120
|
+
type: 'div',
|
|
121
|
+
props: {
|
|
122
|
+
style: {
|
|
123
|
+
fontSize: 26,
|
|
124
|
+
fontWeight: 400,
|
|
125
|
+
color: DIM_COLOR,
|
|
126
|
+
textAlign: 'center' as const,
|
|
127
|
+
lineHeight: 1.4,
|
|
128
|
+
},
|
|
129
|
+
children: fmt(trimmed),
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return ogRootContainer(params.brandColor, children);
|
|
138
|
+
}
|