Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
e4f5ab0
feat: 390 - add images for openGraph generate function
LaraNU May 11, 2025
3e209ec
feat: 390 - implement OG image generation script for all pages
LaraNU May 11, 2025
ce91a92
docs: 390 - add generated OG image folders to .gitignore to prevent c…
LaraNU May 11, 2025
9fe67b0
fix: 390 - add missing image asset for OG generation
LaraNU May 11, 2025
8aab59b
refactor: 390 - convert title to lowercase in generateOgImagePage for…
LaraNU May 12, 2025
0cd0f35
refactor: 390 - improve code structure by splitting generate-og-image…
LaraNU May 14, 2025
f707a8d
feat: 390 - create reusable metadata generator function
LaraNU May 14, 2025
1ec5b1b
feat: 390 - add metadataBase for absolute URL generation for OG URLs
LaraNU May 14, 2025
b8fa55d
feat: 390 - add dynamic metadata to all pages using generatePageMetadata
LaraNU May 14, 2025
ab05327
fix: 390 - validate API_URL before course generation
LaraNU May 14, 2025
fe9c639
chore: 390 - resolve merge conflicts
LaraNU May 15, 2025
704eaac
refactor: 390 - change data fetching approach
LaraNU May 15, 2025
181b67c
refactor: 390 - replace if-else with ternary for date assignment
LaraNU May 15, 2025
5832416
refactor: 390 - move and modularize generate-courses-tree logic
YulikK May 16, 2025
bb487c5
refactor: 390 - move and modularize generate-pages-tree logic
LaraNU May 16, 2025
f93c1ff
chore: 390 - resolve merge conflicts
LaraNU May 16, 2025
d681867
refactor: 390 - move UI components to view folder
LaraNU May 16, 2025
eafbb19
feat: 390 - add PNG image conversion using sharp
LaraNU May 16, 2025
687abec
refactor: 390 - enhance schedule data with name and formatted dates
LaraNU May 16, 2025
43f6443
feat: 390 - implement logo fetching with normalized names
LaraNU May 16, 2025
b701d4c
feat: 390 - add combined data fetching service
LaraNU May 16, 2025
359077a
refactor: 390 - udate OG generate function
LaraNU May 16, 2025
fb908d4
refactor: 390 - relocate open-graph.data.ts to scripts
LaraNU May 16, 2025
67af13b
refactor: 390 - remove unused images
LaraNU May 16, 2025
f6af7df
fix: 390 - fix import order in generate-og-script.ts
LaraNU May 16, 2025
1139574
refactor: 390 - use another link for logo
LaraNU May 17, 2025
c3d0d5d
refactor: 390 - remove link
LaraNU May 17, 2025
9c7d0ec
refactor: 390 - add error handling for file operations
LaraNU May 17, 2025
e7f9bd0
fix: 390 - fix incorrect Buffer generic type usage
LaraNU May 17, 2025
f395a12
refactor: 390 - improve execution flow and error handling
LaraNU May 17, 2025
09d81ab
refactor: 390 - inconsistent function naming
LaraNU May 17, 2025
dcacd53
fix: 390 - fix quotation mark syntax in description
LaraNU May 17, 2025
b129fd5
refactor: 390 - consider adding error handling to the image loading f…
LaraNU May 17, 2025
cad36cb
feat: 390 - consider adding image optimization options
LaraNU May 17, 2025
9ec1aa1
fix: 390 - fix the date formatting fallback logic
LaraNU May 17, 2025
dbd488c
refactor: 390 - add fixed width
LaraNU May 17, 2025
a60d6dd
refactor: 390 - change error message
LaraNU May 17, 2025
e4bb4a2
feat: 390 - add function for generate page metadata
LaraNU May 17, 2025
0a17097
refactor: 390 - update page descriptions
LaraNU May 17, 2025
294cd56
feat: 390 - add standard SEO fields to course pages and main page
LaraNU May 17, 2025
7235dd5
feat: 390 - add standard SEO fields to all pages
LaraNU May 17, 2025
d719bbe
refactor: 390 - rename image paths according to new convention
LaraNU May 17, 2025
6f9fc9e
refactor: 390 - reorganize and consolidate course-related types
YulikK May 17, 2025
5e24bee
refactor: 390 - consolidate OG image paths and constants
YulikK May 17, 2025
56b0857
fix: 390 - restore ru suffix in javascript-preschool image filename
LaraNU May 18, 2025
aa58f2e
chore: 390 - resolve merge conflicts
LaraNU May 18, 2025
d42d1f0
refactor: 390 - reorganize and consolidate page-related types
LaraNU May 18, 2025
a0e48b1
refactor: 390 - change file extension from .tsx to .ts
LaraNU May 18, 2025
76ee539
fix: 390 - eslint errors
LaraNU May 18, 2025
b775830
refactor: 390 - move API-related functions to api directory
LaraNU May 18, 2025
dbe1122
chore: 390 - resolve merge conflicts
LaraNU May 22, 2025
1aecacd
chore: 390 - resolve merge conflicts
LaraNU May 26, 2025
34fb1c9
feat: 390 - integrate OG image generation into src using route handler
LaraNU May 28, 2025
633a8bb
refactor: 390 - remove comments
LaraNU May 28, 2025
be2ef96
feat: 390 - add SEO fields seoDescription and seoKeywords
LaraNU May 28, 2025
80bfca8
feat: 390 - add OG image generation to all pages using route handler
LaraNU May 28, 2025
8c97e49
refactor: 390 - clean up code formatting
LaraNU May 29, 2025
002ab79
chore: 390 - resolve merge conflicts
LaraNU Jun 11, 2025
c8ee8ab
fix: 390 - mark new SEO fields optional and document them
LaraNU Jun 11, 2025
eb366e3
fix: 390 - change never on request
LaraNU Jun 12, 2025
5e00dde
chore: 390 - resolve merge conflicts
LaraNU Jul 1, 2025
7eb4b6e
refactor: 390 - simplify function fetchAndConvertToDataUri
LaraNU Jul 1, 2025
7141e14
chore: 390 - resolve merge conflicts
LaraNU Aug 3, 2025
c48ea6b
refactor: 390 - added constants and moved metadata to separate files
LaraNU Aug 3, 2025
c928ab1
feat: 390 - add og for merch page
LaraNU Aug 5, 2025
8f47891
fix: 390 - danger options
LaraNU Aug 5, 2025
74c5825
refactor: 390 - remove preferredRegion and move var fontsPromis
Aug 16, 2025
b848d0a
refactor: 390 - use constants for metadata
Aug 16, 2025
6a61679
refactor: 390 - move docs metadata and upload only courses data
Aug 16, 2025
8962062
chore: 390 - resolve merge conflicts
Aug 16, 2025
aa34c88
refactor: 390 - remove comments
Aug 16, 2025
f82e387
fix: 390 - add width and height for image
Aug 16, 2025
0ff6e08
Merge branch 'main' into feat/390-add-open-graph-previews
LaraNU Aug 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ src/shared/__tests__/test-results
src/shared/__tests__/report
/blob-report/
/playwright/.cache/
/public/og

# env
.env
38 changes: 38 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src/app/community/og.png/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree';

export { DYNAMIC as dynamic } from '@/shared/constants';

export async function GET() {
const title = 'Community';
const description = 'Join the RS School developer community.';

return createPageTree({
title,
description,
});
}
15 changes: 13 additions & 2 deletions src/app/community/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { Metadata } from 'next';

import { communityMetadata } from '@/metadata/community';
import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata';
import Community from '@/views/community';

export async function generateMetadata(): Promise<Metadata> {
const title = 'Community Β· The Rolling Scopes School';
const { title, description, keywords, canonical, robots } = communityMetadata;

return { title };
const metadata = generatePageMetadata({
title,
description,
imagePath: `/community/og.png`,
keywords,
alternates: { canonical },
robots,
});

return metadata;
}

export default function CommunityRoute() {
Expand Down
53 changes: 53 additions & 0 deletions src/app/courses/[slug]/og.png/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { courseStore } from '@/entities/course';
import { resolveCoursePageLocale } from '@/entities/course/helpers/resolve-course-page-locale';
import { coursePageStore } from '@/entities/course-page';
import { fetchAndConvertToDataUri } from '@/shared/og/utils/fetch-and-convert-to-data-uri';
import { loadImageAsDataUri } from '@/shared/og/utils/load-image-as-data-uri';
import { createCourseTree } from '@/shared/og/view/courses-tree/generate-courses-tree';

export const preferredRegion = 'auto';
const fallbackPath = 'src/shared/assets/svg/rss-logo.svg';
const logoFallbackSize = 250;

export async function generateStaticParams() {
const pages = await coursePageStore.loadCoursePages();

return pages.map(({ slug }) => ({ slug }));
}

export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const locale = resolveCoursePageLocale(slug);

const { courseName, courseId } = await coursePageStore.loadCoursePage(slug, locale);

const course = await courseStore.loadCourse(courseId);

if (!course) {
throw new Error(`Course metadata not found for id="${courseId}"`);
}

const logoWidth = course.iconSrc.width ?? logoFallbackSize;
const logoHeight = course.iconSrc.height ?? logoFallbackSize;
const logoCache = new Map<string, string>();

let logoDataUri: string | undefined = logoCache.get(course.iconSrc.src);

try {
logoDataUri = await fetchAndConvertToDataUri(course.iconSrc.src);
logoCache.set(course.iconSrc.src, logoDataUri);
} catch (err) {
console.warn('Failed to load remote logo, using fallback', err);
logoDataUri = await loadImageAsDataUri(fallbackPath);
}

return createCourseTree({
name: courseName,
logo: {
src: logoDataUri,
width: logoWidth,
height: logoHeight,
},
startDate: course.startDate,
});
}
24 changes: 20 additions & 4 deletions src/app/courses/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Metadata } from 'next';
import path from 'path';

import { resolveCoursePageLocale } from '@/entities/course';
import { coursePageStore } from '@/entities/course-page';
import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata';
import { Course } from '@/views/course';

type Params = {
Expand All @@ -16,10 +18,24 @@ export async function generateMetadata({ params }: CourseRouteParams): Promise<M
const { slug } = await params;
const locale = resolveCoursePageLocale(slug);

const pageTitle = await coursePageStore.loadCoursePageTitle(slug, locale);
const title = `${pageTitle} Β· The Rolling Scopes School`;

return { title };
const { courseName, description, keywords, courseUrl } = await coursePageStore.loadCoursePage(
slug,
locale,
);
const title = `${courseName} Β· The Rolling Scopes School`;
const robots = {
index: true,
follow: true,
};

return generatePageMetadata({
title,
description,
imagePath: path.join('courses', slug, 'og.png'),
keywords,
alternates: { canonical: courseUrl },
robots,
});
}

export async function generateStaticParams() {
Expand Down
13 changes: 13 additions & 0 deletions src/app/courses/og.png/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree';

export { DYNAMIC as dynamic } from '@/shared/constants';

export async function GET() {
const title = 'Courses';
const description = 'Free RS School courses: JavaScript, React, Node.js, AWS, and more.';

return createPageTree({
title,
description,
});
}
15 changes: 13 additions & 2 deletions src/app/courses/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { Metadata } from 'next';

import { coursesMetadata } from '@/metadata/courses';
import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata';
import { Courses } from '@/views/courses';

export async function generateMetadata(): Promise<Metadata> {
const title = 'Courses Β· The Rolling Scopes School';
const { title, description, keywords, canonical, robots } = coursesMetadata;

return { title };
const metadata = generatePageMetadata({
title,
description,
imagePath: `/courses/og.png`,
keywords,
alternates: { canonical },
robots,
});

return metadata;
}

export default function CoursesRoute() {
Expand Down
16 changes: 15 additions & 1 deletion src/app/docs/[lang]/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import path from 'path';

import { DocsContent } from '../../components/docs-content/docs-content';
import { TITLE_POSTFIX } from '../../constants';
import { Menu } from '../../types';
import { fetchMarkdownContent } from '../../utils/fetch-markdown-content';
import { fetchMenu } from '../../utils/fetch-menu';
import { generateDocsMetadata } from '@/metadata/docs';
import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata';
import { Language } from '@/shared/types';

type RouteParams = { lang: Language;
Expand Down Expand Up @@ -45,7 +48,18 @@ export async function generateMetadata({

const title = titles.find((el) => el.slug.join('/') === slugPath)?.title;

return { title: `${title} ${TITLE_POSTFIX}` };
const { description, keywords, canonical, robots } = generateDocsMetadata(lang, slugPath);

const metadata = generatePageMetadata({
title: `${title} ${TITLE_POSTFIX}`,
description,
imagePath: path.join('docs', lang, 'og.png'),
keywords,
alternates: { canonical },
robots,
});

return metadata;
}

export async function generateStaticParams(): Promise<RouteParams[]> {
Expand Down
27 changes: 27 additions & 0 deletions src/app/docs/[lang]/og.png/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree';
import { Language } from '@/shared/types';

export { DYNAMIC as dynamic } from '@/shared/constants';

export function generateStaticParams() {
return [{ lang: 'en' }, { lang: 'ru' }];
}

const titleMap: Record<Language, string> = {
en: 'RS School Docs',
ru: 'RS ДокумСнтация',
};

const descriptionMap: Record<Language, string> = {
en: 'RS School Docs: rules, guides, FAQs.',
ru: 'ДокумСнтация RS: ΠΏΡ€Π°Π²ΠΈΠ»Π°, Π³Π°ΠΉΠ΄Ρ‹, часто Π·Π°Π΄Π°Π²Π°Π΅ΠΌΡ‹Π΅ вопросы.',
};

export async function GET(_request: Request, { params }: { params: Promise<{ lang: Language }> }) {
const lang = (await params).lang;

return createPageTree({
title: titleMap[lang],
description: descriptionMap[lang],
});
}
20 changes: 17 additions & 3 deletions src/app/docs/[lang]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { DocsContent } from '../components/docs-content/docs-content';
import { TITLE_POSTFIX } from '../constants';
import { fetchMarkdownContent } from '../utils/fetch-markdown-content';
import { docsLangMetadata } from '@/metadata/docs';
import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata';
import { Language } from '@/shared/types';

type RouteParams = { lang: Language };

export async function generateMetadata() {
return { title: `RS School Overview ${TITLE_POSTFIX}` };
export async function generateMetadata({ params }: { params: Promise<RouteParams> }) {
const { lang } = await params;

const { title, description, keywords, canonical, robots } = docsLangMetadata;

const metadata = generatePageMetadata({
title,
description,
imagePath: `/docs/${lang}/og.png`,
keywords,
alternates: { canonical },
robots,
});

return metadata;
}

export async function generateStaticParams(): Promise<RouteParams[]> {
Expand Down
1 change: 1 addition & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { Metadata } from 'next';
import '@/core/styles/index.scss';

export const metadata: Metadata = {
metadataBase: new URL('https://rs.school'),
title: 'RS Site',
description:
'RS School offers free, community-driven education courses run by The Rolling Scopes developer community since 2013. Enhance your web development, JavaScript, and front-end skills with us.',
Expand Down
15 changes: 13 additions & 2 deletions src/app/mentorship/[course]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { Metadata } from 'next';

import { mentorshipCourseMetadata } from '@/metadata/mentorship';
import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata';
import { Mentorship } from '@/views/mentorship/mentorship';
import { MentorshipCourseRouteKeys, mentorshipCourses, mentorshipCoursesDefault } from 'data';

export async function generateMetadata(): Promise<Metadata> {
const title = `Mentorship Β· The Rolling Scopes School`;
const { title, description, keywords, canonical, robots } = mentorshipCourseMetadata;

return { title };
const metadata = generatePageMetadata({
title,
description,
imagePath: `/mentorship/og.png`,
keywords,
alternates: { canonical },
robots,
});

return metadata;
}
export async function generateStaticParams(): Promise<{ course: MentorshipCourseRouteKeys }[]> {
return [
Expand Down
13 changes: 13 additions & 0 deletions src/app/mentorship/og.png/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree';

export { DYNAMIC as dynamic } from '@/shared/constants';

export async function GET() {
const title = 'Mentorship';
const description = 'Mentor at RS School and help others grow.';

return createPageTree({
title,
description,
});
}
15 changes: 13 additions & 2 deletions src/app/mentorship/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { Metadata } from 'next';

import { mentorshipMetadata } from '@/metadata/mentorship';
import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata';
import { Mentorship } from '@/views/mentorship/mentorship';
import { mentorshipCoursesDefault } from 'data';

export async function generateMetadata(): Promise<Metadata> {
const title = `Mentorship Β· The Rolling Scopes School`;
const { title, description, keywords, canonical, robots } = mentorshipMetadata;

return { title };
const metadata = generatePageMetadata({
title,
description,
imagePath: `/mentorship/og.png`,
keywords,
alternates: { canonical },
robots,
});

return metadata;
}

export default async function MentorshipRoute() {
Expand Down
Loading
Loading