-
-
Notifications
You must be signed in to change notification settings - Fork 11
390-feat: Add open graph previews #894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 9 commits
e4f5ab0
3e209ec
ce91a92
9fe67b0
8aab59b
0cd0f35
f707a8d
1ec5b1b
b8fa55d
ab05327
fe9c639
704eaac
181b67c
5832416
bb487c5
f93c1ff
d681867
eafbb19
687abec
43f6443
b701d4c
359077a
fb908d4
67af13b
f6af7df
1139574
c3d0d5d
9c7d0ec
e7f9bd0
f395a12
09d81ab
dcacd53
b129fd5
cad36cb
9ec1aa1
dbd488c
a60d6dd
e4bb4a2
0a17097
294cd56
7235dd5
d719bbe
6f9fc9e
5e24bee
56b0857
aa58f2e
d42d1f0
a0e48b1
76ee539
b775830
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
export const COURSE_SLUGS = { | ||
JS_PRESCHOOL_RU: 'javascript-preschool-ru', | ||
JS_EN: 'javascript', | ||
JS_RU: 'javascript-ru', | ||
REACT: 'reactjs', | ||
ANGULAR: 'angular', | ||
NODE: 'nodejs', | ||
AWS_FUNDAMENTALS: 'aws-fundamentals', | ||
AWS_CLOUD_DEVELOPER: 'aws-cloud-developer', | ||
AWS_DEVOPS: 'aws-devops', | ||
} as const; | ||
|
||
export const RS_PAGES = { | ||
MAIN: 'Home', | ||
MENTORSHIP: 'Mentorship', | ||
COMMUNITY: 'Community', | ||
DOCS: 'Docs', | ||
COURSES: 'Courses', | ||
} as const; | ||
|
||
export const Descriptions = { | ||
MAIN: 'Connecting people, growing together, having fun', | ||
MENTORSHIP: 'By teaching others, you learn yourself', | ||
COMMUNITY: 'An international community of developers since 2013', | ||
DOCS: 'School docs hub: rules, guides, and FAQs', | ||
COURSES: 'Community driven. 100% free of charge. Journey to full stack mastery', | ||
} as const; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,120 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
#!/usr/bin/env tsx | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
import { JSX } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import 'dotenv/config'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import fs from 'fs/promises'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import path from 'path'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
import { ApiCourse } from './types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { getCourseInfo } from './utils/course-info'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { ensureDirExists } from './utils/ensure-dir-exists'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { fetchCoursesList } from './utils/fetch-courses-list'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { createCourseTree } from './utils/generate-courses-tree'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { generateImage } from './utils/generate-image'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { createPageTree } from './utils/generate-page-tree'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { type Font, loadFont } from './utils/load-fonts'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { loadImageAsDataUri } from './utils/load-image-as-data-uri'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { COURSE_TITLES } from '../dev-data/course-titles.data'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { COURSE_SLUGS, Descriptions, RS_PAGES } from '../dev-data/open-graph.data'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const fontRegularPromise: Promise<Font> = loadFont(400); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const fontBoldPromise: Promise<Font> = loadFont(700); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
type CourseKey = keyof typeof COURSE_SLUGS; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const COURSES_JSON_URL = process.env.API_URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
async function generateOGCourses(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
const coursesList: ApiCourse[] = await fetchCoursesList(COURSES_JSON_URL); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const ogDir: string = path.join(process.cwd(), 'public', 'og-images'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
await ensureDirExists(ogDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const rsLogoDataUriPromise: Promise<string> = loadImageAsDataUri('src/shared/assets/og-logos/rs-school.png', 'image/png'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const [fontRegular, fontBold, rsLogoDataUri] = | ||||||||||||||||||||||||||||||||||||||||||||||||||
await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||||
fontRegularPromise, | ||||||||||||||||||||||||||||||||||||||||||||||||||
fontBoldPromise, | ||||||||||||||||||||||||||||||||||||||||||||||||||
rsLogoDataUriPromise, | ||||||||||||||||||||||||||||||||||||||||||||||||||
]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const fonts: Font[] = [fontRegular, fontBold]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const leftTitle: string = 'RS School'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
const leftSubtitle: string = 'Free courses. High motivation'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
for (const key of Object.keys(COURSE_TITLES) as CourseKey[]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
const slug: string = COURSE_SLUGS[key]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
const title: string = COURSE_TITLES[key]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const formattedDate: string = await getCourseInfo(coursesList, slug); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const logoDataUri: string = await loadImageAsDataUri(`src/shared/assets/og-logos/${slug}.svg`, 'image/svg+xml'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const tree: JSX.Element = createCourseTree( | ||||||||||||||||||||||||||||||||||||||||||||||||||
title, | ||||||||||||||||||||||||||||||||||||||||||||||||||
leftTitle, | ||||||||||||||||||||||||||||||||||||||||||||||||||
leftSubtitle, | ||||||||||||||||||||||||||||||||||||||||||||||||||
formattedDate, | ||||||||||||||||||||||||||||||||||||||||||||||||||
rsLogoDataUri, | ||||||||||||||||||||||||||||||||||||||||||||||||||
logoDataUri, | ||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const buffer: Buffer<ArrayBufferLike> | null = await generateImage(tree, fonts); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
if (!buffer) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
throw new Error(`Doesn't generate image for "${slug}"`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
await fs.writeFile(path.join(ogDir, `${slug}.png`), buffer); | ||||||||||||||||||||||||||||||||||||||||||||||||||
console.log(`${slug} created`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Add try-catch for image file operations Missing error handling for image file operations could lead to unhandled exceptions. const buffer: Buffer<ArrayBufferLike> | null = await generateImage(tree, fonts);
if (!buffer) {
throw new Error(`Doesn't generate image for "${slug}"`);
}
- await fs.writeFile(path.join(ogDir, `${slug}.png`), buffer);
- console.log(`${slug} created`);
+ try {
+ await fs.writeFile(path.join(ogDir, `${slug}.png`), buffer);
+ console.log(`${slug} created`);
+ } catch (error) {
+ console.error(`Error writing image file for ${slug}:`, error);
+ throw error;
+ } π Committable suggestion
Suggested change
π€ Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
async function generateOgImagePages(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
const rsBannerPromise: Promise<string> = loadImageAsDataUri('src/shared/assets/og-logos/rs-banner.png', 'image/png'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const rsMascotsPromise: Promise<string> = loadImageAsDataUri('src/shared/assets/og-logos/mentor-with-his-students.png', 'image/png'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const mapBgPromise: Promise<string> = loadImageAsDataUri('src/shared/assets/og-logos/map.png', 'image/png'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const ogDir: string = path.join(process.cwd(), 'public', 'og-images-pages'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
await ensureDirExists(ogDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const [fontRegular, fontBold, rsBannerUri, rsMascotsUri, mapBgUri] = | ||||||||||||||||||||||||||||||||||||||||||||||||||
await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||||
fontRegularPromise, | ||||||||||||||||||||||||||||||||||||||||||||||||||
fontBoldPromise, | ||||||||||||||||||||||||||||||||||||||||||||||||||
rsBannerPromise, | ||||||||||||||||||||||||||||||||||||||||||||||||||
rsMascotsPromise, | ||||||||||||||||||||||||||||||||||||||||||||||||||
mapBgPromise, | ||||||||||||||||||||||||||||||||||||||||||||||||||
]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const fonts: Font[] = [fontRegular, fontBold]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
for (const key of Object.keys(RS_PAGES) as Array<keyof typeof RS_PAGES>) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
const title: string = RS_PAGES[key]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
const description: string = Descriptions[key]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const tree: JSX.Element = createPageTree(title, description, rsBannerUri, rsMascotsUri, mapBgUri); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const buffer: Buffer<ArrayBufferLike> | null = await generateImage(tree, fonts); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const fileName: string = `${title.toLowerCase()}.png`.replace(/\s+/g, '-'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
if (!buffer) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
throw new Error(`Doesn't generate image for "${title}"`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
await fs.writeFile(path.join(ogDir, fileName), buffer); | ||||||||||||||||||||||||||||||||||||||||||||||||||
console.log(`${fileName} created`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Similar error handling needed in page image generation Apply the same error handling pattern here as suggested for course image generation. const buffer: Buffer<ArrayBufferLike> | null = await generateImage(tree, fonts);
const fileName: string = `${title.toLowerCase()}.png`.replace(/\s+/g, '-');
if (!buffer) {
throw new Error(`Doesn't generate image for "${title}"`);
}
- await fs.writeFile(path.join(ogDir, fileName), buffer);
- console.log(`${fileName} created`);
+ try {
+ await fs.writeFile(path.join(ogDir, fileName), buffer);
+ console.log(`${fileName} created`);
+ } catch (error) {
+ console.error(`Error writing image file for ${fileName}:`, error);
+ throw error;
+ } π Committable suggestion
Suggested change
π€ Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
generateOgImagePages().catch((err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
console.error(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||
process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
generateOGCourses().catch((err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
console.error(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||
process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export type ApiCourse = { | ||
startDate: string; | ||
descriptionUrl: string; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import dayjs from 'dayjs'; | ||
|
||
import type { ApiCourse } from '../types'; | ||
|
||
export const getCourseInfo = (coursesList: ApiCourse[], slug: string): string => { | ||
const courseInfo: ApiCourse | undefined = coursesList.find((c) => c.descriptionUrl.toLowerCase().endsWith(slug)); | ||
|
||
let rawDate: string; | ||
|
||
if (!courseInfo) { | ||
rawDate = ''; | ||
} else { | ||
rawDate = courseInfo.startDate; | ||
} | ||
natanchik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const formattedDate: string = rawDate ? dayjs(rawDate).format('MMM DD, YYYY') : 'TBD'; | ||
|
||
return formattedDate; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import fs from 'node:fs/promises'; | ||
|
||
export const ensureDirExists = async (dirPath: string): Promise<void> => { | ||
const stat = await fs.stat(dirPath).catch((err) => { | ||
if (err.code === 'ENOENT') { | ||
return null; | ||
} | ||
throw err; | ||
}); | ||
|
||
if (!stat || !stat.isDirectory()) { | ||
await fs.mkdir(dirPath, { recursive: true }); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import type { ApiCourse } from '../types'; | ||
|
||
export async function fetchCoursesList(url: string): Promise<ApiCourse[]> { | ||
const res = await fetch(url); | ||
|
||
if (!res.ok) { | ||
throw new Error(`API error ${res.status}`); | ||
} | ||
return (await res.json()) as ApiCourse[]; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.