Skip to content

Commit b2fedc8

Browse files
Merge pull request #69 from deariary/feat/index-og-image
feat: add default OG image for index page
2 parents b476677 + 5c90bd5 commit b2fedc8

4 files changed

Lines changed: 154 additions & 6 deletions

File tree

src/cli/commands/render.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { renderReport } from "../../renderer/index.js";
88
import { renderIndexPage, buildReportEntry, type ReportEntry } from "../../deployer/index-page.js";
99
import { getWeekId } from "../../deployer/week.js";
1010
import { parseLocalDate } from "../../collector/date-range.js";
11-
import { generateOGImage } from "../../renderer/og-image.js";
11+
import { generateOGImage, generateIndexOGImage } from "../../renderer/og-image.js";
1212
import type { WeeklyReportData, AIContent, Language } from "../../types.js";
1313

1414
const env = (key: string): string | undefined => process.env[key];
@@ -164,12 +164,25 @@ const run = async (options: RenderOptions): Promise<void> => {
164164
{ username: githubData.username, avatarUrl: githubData.avatarUrl, profile: githubData.profile },
165165
options.language,
166166
options.siteTitle,
167+
base,
167168
);
168169
const indexPath = join(options.outputDir, "index.html");
169170
await mkdir(options.outputDir, { recursive: true });
170171
await writeFile(indexPath, indexHtml, "utf-8");
171172
console.log(`Index written to ${indexPath}`);
172173

174+
// Generate index OG image
175+
const resolvedSiteTitle = (options.siteTitle ?? "Dev Pulse").replace(/\\n/g, " ");
176+
const indexOGImage = await generateIndexOGImage({
177+
siteTitle: resolvedSiteTitle,
178+
username: githubData.username,
179+
language: options.language,
180+
reportCount: allPaths.length,
181+
});
182+
const indexOGPath = join(options.outputDir, "og.png");
183+
await writeFile(indexOGPath, indexOGImage);
184+
console.log(`Index OG image written to ${indexOGPath}`);
185+
173186
// Generate sitemap.xml
174187
const sitemapEntries = allPaths
175188
.map((p) => ` <url><loc>${base}/${p}/</loc></url>`)

src/deployer/index-page.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,19 @@ const TEMPLATE = `<!DOCTYPE html>
3838
<title>{{siteTitle}}</title>
3939
<meta charset="utf-8" />
4040
<meta name="viewport" content="width=device-width, initial-scale=1" />
41-
<meta name="description" content="{{siteTitle}}" />
41+
<meta name="description" content="{{description}}" />
4242
<meta name="view-transition" content="same-origin" />
43+
<meta property="og:title" content="{{siteTitleInline}}" />
44+
<meta property="og:description" content="{{description}}" />
45+
<meta property="og:type" content="website" />
46+
{{#if ogImageUrl}}<meta property="og:image" content="{{ogImageUrl}}" />
47+
<meta property="og:image:width" content="1200" />
48+
<meta property="og:image:height" content="630" />{{/if}}
49+
{{#if baseUrl}}<meta property="og:url" content="{{baseUrl}}/" />{{/if}}
50+
<meta name="twitter:card" content="summary_large_image" />
51+
<meta name="twitter:title" content="{{siteTitleInline}}" />
52+
<meta name="twitter:description" content="{{description}}" />
53+
{{#if ogImageUrl}}<meta name="twitter:image" content="{{ogImageUrl}}" />{{/if}}
4354
<style>{{{css}}}</style>
4455
<style>
4556
body { background: #050505; color: #e8e8e8; overflow-x: hidden; }
@@ -449,21 +460,28 @@ export const renderIndexPage = (
449460
pageData?: IndexPageData,
450461
language: Language = "en",
451462
siteTitle?: string,
463+
baseUrl?: string,
452464
): string => {
453465
const locale = getLocale(language);
454466
const fontConfig = getFontConfig(language);
455467
const resolvedSiteTitle = (siteTitle ?? "Dev\nPulse").replace(/\\n/g, "\n");
456468
const siteTitleInline = resolvedSiteTitle.replace(/\n/g, " ");
469+
const username = pageData?.username ?? "";
470+
const description = `Weekly reports by @${username}`;
471+
const ogImageUrl = baseUrl ? `${baseUrl}/og.png` : "og.png";
457472
const template = Handlebars.compile(TEMPLATE);
458473
return template({
459474
yearGroups: groupByYear(reports),
460475
css: buildCSS(language),
461-
username: pageData?.username,
476+
username,
462477
avatarUrl: pageData?.avatarUrl,
463478
profile: pageData?.profile,
464-
displayName: pageData?.profile?.name ?? pageData?.username,
479+
displayName: pageData?.profile?.name ?? username,
465480
siteTitle: resolvedSiteTitle,
466481
siteTitleInline,
482+
description,
483+
ogImageUrl,
484+
baseUrl,
467485
lang: language,
468486
weeklyReports: locale.weeklyReports,
469487
poweredBy: locale.poweredBy,

src/renderer/og-image.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,120 @@ export const generateOGImage = async (data: OGImageData): Promise<Buffer> => {
170170
const svg = await buildSVG(data, font);
171171
return sharp(Buffer.from(svg)).png().toBuffer();
172172
};
173+
174+
export type IndexOGImageData = {
175+
siteTitle: string;
176+
username: string;
177+
language: Language;
178+
reportCount: number;
179+
};
180+
181+
const buildIndexSVG = async (
182+
data: IndexOGImageData,
183+
font: ArrayBuffer,
184+
): Promise<string> => {
185+
const element = {
186+
type: "div",
187+
props: {
188+
style: {
189+
display: "flex",
190+
flexDirection: "column",
191+
justifyContent: "space-between",
192+
width: "100%",
193+
height: "100%",
194+
padding: "80px 90px",
195+
background: "linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%)",
196+
color: "#e0e0e0",
197+
fontFamily: "OGFont",
198+
},
199+
children: [
200+
{
201+
type: "div",
202+
props: {
203+
style: { display: "flex", flexDirection: "column", gap: "24px" },
204+
children: [
205+
{
206+
type: "div",
207+
props: {
208+
style: {
209+
fontSize: "96px",
210+
fontWeight: 600,
211+
lineHeight: 1.1,
212+
letterSpacing: "-0.03em",
213+
color: "#ffffff",
214+
},
215+
children: data.siteTitle.replace(/\n/g, " "),
216+
},
217+
},
218+
{
219+
type: "div",
220+
props: {
221+
style: {
222+
fontSize: "40px",
223+
color: "rgba(255,255,255,0.5)",
224+
},
225+
children: `Weekly reports by @${data.username}`,
226+
},
227+
},
228+
],
229+
},
230+
},
231+
{
232+
type: "div",
233+
props: {
234+
style: {
235+
display: "flex",
236+
justifyContent: "space-between",
237+
alignItems: "flex-end",
238+
},
239+
children: [
240+
{
241+
type: "div",
242+
props: {
243+
style: { display: "flex", gap: "8px", alignItems: "baseline" },
244+
children: [
245+
{
246+
type: "span",
247+
props: {
248+
style: { color: "#39d353", fontWeight: 600, fontSize: "36px" },
249+
children: String(data.reportCount),
250+
},
251+
},
252+
{
253+
type: "span",
254+
props: {
255+
style: { color: "rgba(255,255,255,0.4)", fontSize: "30px" },
256+
children: data.reportCount === 1 ? "report" : "reports",
257+
},
258+
},
259+
],
260+
},
261+
},
262+
{
263+
type: "div",
264+
props: {
265+
style: { fontSize: "28px", color: "rgba(255,255,255,0.3)" },
266+
children: "github-weekly-reporter",
267+
},
268+
},
269+
],
270+
},
271+
},
272+
],
273+
},
274+
};
275+
276+
return satori(element as Parameters<typeof satori>[0], {
277+
width: 1200,
278+
height: 630,
279+
fonts: [{ name: "OGFont", data: font, weight: 600, style: "normal" as const }],
280+
});
281+
};
282+
283+
export const generateIndexOGImage = async (
284+
data: IndexOGImageData,
285+
): Promise<Buffer> => {
286+
const font = loadFont(data.language);
287+
const svg = await buildIndexSVG(data, font);
288+
return sharp(Buffer.from(svg)).png().toBuffer();
289+
};

video/src/project.meta

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
null
88
],
99
"size": {
10-
"x": 1920,
11-
"y": 1080
10+
"x": 1280,
11+
"y": 640
1212
},
1313
"audioOffset": 0
1414
},

0 commit comments

Comments
 (0)