Skip to content

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

Open
wants to merge 60 commits into
base: main
Choose a base branch
from

Conversation

LaraNU
Copy link
Collaborator

@LaraNU LaraNU commented May 14, 2025

What type of PR is this? (select all that apply)

  • πŸ• Feature
  • πŸ› Bug Fix
  • 🚧 Breaking Change
  • πŸ§‘β€πŸ’» Code Refactor
  • πŸ“ Documentation Update

Description

  • Created reusable generatePageMetadata utility function for consistent metadata generation
  • Added metadataBase to root layout for proper absolute URL resolution
  • Removed the separate image generation script
  • Implemented a static route handler (/app/courses/[slug]/og.png/route.ts) for generating OG images.
  • The images are now generated during build time as part of the static export process

Problem
Project uses output: 'export' which limits dynamic image generation capabilities Based on the solution described in:

When running the project in development mode (npm run dev), developers need to:

Modify metadataBase in the root layout:

- metadataBase: new URL('https://rs.school'),
+ metadataBase: new URL('http://localhost:3000'),
  1. OG images require absolute URLs for proper display in social previews
  2. The current implementation links images to their deployment environment
  3. This ensures local development matches the production behavior while using correct local URLs
  4. Ensures image metadata (width, height, fallback handling) is correctly handled to avoid build failures
  5. Uses ImageResponse from next/og directly in a route file inside the app/ directory

Related Tickets & Documents

Screenshots, Recordings

I've deployed a test version with the new metadata system: Preview Deployment

For courses
image

For pages
image

Added/updated tests?

  • πŸ‘Œ Yes
  • πŸ™…β€β™‚οΈ No, because they aren't needed
  • πŸ™‹β€β™‚οΈ No, because I need help

[optional] Are there any post deployment tasks we need to perform?

[optional] What gif best describes this PR or how it makes you feel?

Summary by CodeRabbit

  • New Features
    • Added dynamic Open Graph image generation for home, courses, community, mentorship, and documentation pages, including language and course-specific images.
    • Enhanced page metadata across the site with richer SEO and social sharing details, such as descriptions, keywords, canonical URLs, and robots directives.
    • Introduced utilities for loading fonts and images as data URIs to support Open Graph image generation.
  • Bug Fixes
    • Improved handling of missing or fallback images for Open Graph generation.
  • Chores
    • Added new dev dependency for improved development workflow.
    • Updated configuration files for formatting and inclusion of new scripts.
    • Updated .gitignore to exclude generated Open Graph image assets.
  • Documentation
    • Improved and centralized metadata helper functions for consistent SEO data across pages.
  • Style
    • Introduced consistent styling for Open Graph image layouts and components.

Copy link
Contributor

coderabbitai bot commented May 14, 2025

πŸ“ Walkthrough

Walkthrough

This change implements comprehensive Open Graph image generation and enhanced SEO metadata across multiple pages. It introduces dynamic OG image routes, centralizes metadata creation via a helper, enriches metadata with descriptions, keywords, and canonical URLs, and adds utility functions for image and font processing. Type definitions and store logic are updated to support new SEO fields.

Changes

Files / Grouped Files Change Summary
.gitignore, package.json, tsconfig.json Ignore /public/og, add tsx dev dependency, reformat arrays and include OG script in TypeScript config.
src/app/layout.tsx Adds metadataBase property to exported metadata object.
src/app/page.tsx, src/app/community/page.tsx, src/app/courses/page.tsx, src/app/mentorship/page.tsx Enhance generateMetadata to use a helper for richer metadata (description, keywords, canonical, robots, image).
src/app/courses/[slug]/page.tsx, src/app/docs/[lang]/page.tsx Update generateMetadata to async, use helper for detailed SEO metadata, accept route params where needed.
src/app/docs/[lang]/[...slug]/page.tsx, src/app/mentorship/[course]/page.tsx Expand generateMetadata to provide full SEO metadata using the helper.
src/shared/helpers/generate-page-metadata.ts Add generatePageMetadata function to centralize metadata object creation.
src/app/og.png/route.ts, src/app/community/og.png/route.ts, src/app/courses/og.png/route.ts, src/app/mentorship/og.png/route.ts, src/app/docs/[lang]/og.png/route.ts, src/app/courses/[slug]/og.png/route.ts Add new OG image route handlers with static/dynamic settings, generating images with page/course data.
src/shared/og/view/pages-tree/generate-pages-tree.tsx, src/shared/og/view/pages-tree/generate-pages-tree.styles.ts Add components and styles for page tree OG images.
src/shared/og/view/courses-tree/generate-courses-tree.tsx, src/shared/og/view/courses-tree/generate-courses-tree.styles.ts Add components and styles for course tree OG images.
src/shared/og/utils/fetch-and-convert-to-data-uri.ts, src/shared/og/utils/load-fonts.ts, src/shared/og/utils/load-image-as-data-uri.ts Add utility functions for image and font processing (fetch, convert, load).
src/shared/types/contentful/TypeHomePage.ts Add seoDescription and seoKeywords fields to home page type.
src/views/course/model/store.ts Extend loadCoursePage to extract SEO fields and course URL, update returned object.
src/shared/constants.tsx Remove trailing blank lines (no logic change).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant RouteHandler
    participant Helper
    participant Utils

    User->>RouteHandler: Request OG image (e.g., /courses/[slug]/og.png)
    RouteHandler->>Utils: Load course/page data, images, fonts
    RouteHandler->>Helper: Call createCourseTree/createPageTree with data
    Helper->>Utils: Fetch and process images/fonts as needed
    Helper-->>RouteHandler: Return ImageResponse
    RouteHandler-->>User: Respond with OG image
Loading
sequenceDiagram
    participant Page
    participant generateMetadata
    participant generatePageMetadata

    Page->>generateMetadata: On page load
    generateMetadata->>generatePageMetadata: Pass title, description, keywords, etc.
    generatePageMetadata-->>generateMetadata: Return metadata object
    generateMetadata-->>Page: Return enriched metadata
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement Open Graph Previews for pages and courses (#390) βœ…
Add proper meta descriptions, keywords, canonical URLs, and robots directives (#390) βœ…
Centralize and standardize metadata generation (#390) βœ…
Support dynamic OG images for localized/docs/course pages (#390) βœ…

Assessment against linked issues: Out-of-scope changes

Code Change (file_path) Explanation

Suggested labels

feature

Suggested reviewers

  • andron13
  • dzmitry-varabei
  • natanchik
  • SpaNb4
  • ansivgit

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

πŸ”§ ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

npm error Exit handler never called!
npm error This is an error with npm itself. Please report this error at:
npm error https://github.com/npm/cli/issues
npm error A complete log of this run can be found in: /.npm/_logs/2025-06-12T07_19_35_599Z-debug-0.log


πŸ“œ Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between c8ee8ab and eb366e3.

πŸ“’ Files selected for processing (2)
  • src/app/courses/[slug]/og.png/route.ts (1 hunks)
  • src/app/docs/[lang]/og.png/route.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/docs/[lang]/og.png/route.ts
  • src/app/courses/[slug]/og.png/route.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build rs.school
  • GitHub Check: CI
✨ Finishing Touches
  • πŸ“ Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share
πŸͺ§ Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (11)
scripts/utils/fetch-courses-list.ts (1)

3-10: Good implementation with proper error handling

The function correctly handles API errors and type casting. Consider adding a timeout to the fetch request for better error handling in case of slow network conditions.

 export async function fetchCoursesList(url: string): Promise<ApiCourse[]> {
-  const res = await fetch(url);
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 10000);
+  
+  try {
+    const res = await fetch(url, { 
+      signal: controller.signal 
+    });
+    
+    if (!res.ok) {
+      throw new Error(`API error ${res.status}`);
+    }
+    return (await res.json()) as ApiCourse[];
+  } finally {
+    clearTimeout(timeoutId);
+  }

-  if (!res.ok) {
-    throw new Error(`API error ${res.status}`);
-  }
-  return (await res.json()) as ApiCourse[];
 }
src/app/mentorship/[course]/page.tsx (1)

9-9: Consider course-specific metadata for each course page

Currently, this course-specific page uses the same generic mentorship description and image as the main mentorship page. For better SEO and social media sharing, consider generating unique descriptions and OG images for each course.

Also applies to: 11-16

src/app/courses/reactjs/page.tsx (1)

11-12: Consider updating the React course description.

While the title is correctly fetched, the description doesn't seem specific to the React course - it appears to be a generic RS School description rather than React-specific content.

  const title = await getCourseTitle(courseName);
- const description = 'Everyone can study at RS School, regardless of age, professional employment, or place of residence';
+ const description = 'Learn modern React, hooks, state management, and build production-ready applications with our comprehensive React course';
src/app/courses/javascript/page.tsx (1)

3-3: Fix extra quotation mark in description

The description contains an erroneous closing quotation mark at the end of the string.

- const description = 'Everyone can study at RS School, regardless of age, professional employment, or place of residence".';
+ const description = 'Everyone can study at RS School, regardless of age, professional employment, or place of residence.';

Also applies to: 11-20

scripts/utils/course-info.ts (2)

5-19: The getCourseInfo function looks good but could be simplified

The function correctly extracts and formats course dates from course information, but the conditional logic in lines 8-14 could be simplified.

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;
-  }
-
-  const formattedDate: string = rawDate ? dayjs(rawDate).format('MMM DD, YYYY') : 'TBD';
+  const rawDate = courseInfo?.startDate || '';
+  const formattedDate: string = rawDate ? dayjs(rawDate).format('MMM DD, YYYY') : 'TBD';

  return formattedDate;
};

5-6: Consider adding null check for coursesList parameter

The function assumes coursesList is always a valid array, but doesn't validate this input.

-export const getCourseInfo = (coursesList: ApiCourse[], slug: string): string => {
+export const getCourseInfo = (coursesList: ApiCourse[] | null | undefined, slug: string): string => {
+  if (!coursesList || !Array.isArray(coursesList)) {
+    return 'TBD';
+  }
  const courseInfo: ApiCourse | undefined = coursesList.find((c) => c.descriptionUrl.toLowerCase().endsWith(slug));
src/app/courses/javascript-ru/page.tsx (1)

12-12: Consider moving description to constants

The description is hardcoded in this file but may be reused across course pages.

If this description is common across courses, consider moving it to a constants file.

src/app/docs/[lang]/[...slug]/page.tsx (1)

48-48: Consider moving description to constants

Similar to course pages, this hardcoded description could be moved to a constants file.

If this description appears in multiple docs-related pages, consider extracting it to a constants file.

dev-data/open-graph.data.ts (1)

21-27: Naming convention inconsistency.

Consider using consistent naming conventions across the constants.

-export const Descriptions = {
+export const DESCRIPTIONS = {

This would match the uppercase naming pattern used for COURSE_SLUGS and RS_PAGES.

scripts/utils/generate-courses-tree.ts (1)

1-122: Extract common style values as constants

The component has multiple hardcoded style values (colors, sizes, fonts) that are repeated throughout. Consider extracting these as constants for better maintainability.

import { JSX, createElement } from 'react';

+// Design constants
+const COLORS = {
+  dark: '#000',
+  light: '#f0f2f5',
+  accent: '#ffda1f'
+};
+
+const FONTS = {
+  family: 'Inter',
+  sizes: {
+    large: 72,
+    medium: 50,
+    small: 38,
+    body: 36
+  }
+};
+
+const IMAGE_DIMENSIONS = {
+  width: 1200,
+  height: 630,
+  logo: 250
+};

export function createCourseTree(
  /* ... parameters remain the same ... */
) {
  return createElement(
    'div',
    {
      style: {
        display: 'flex',
        width: IMAGE_DIMENSIONS.width,
        height: IMAGE_DIMENSIONS.height,
        fontFamily: FONTS.family,
        fontWeight: 400,
      },
    },
    /* ... remainder of the function using these constants ... */
  );
}
scripts/utils/generate-page-tree.ts (1)

30-40: Alt text doesn’t match the image content.

The map background is declared with alt: 'RS Logo', which is misleading and hurts accessibility. Use a description that actually represents the image purpose (e.g. "World map background" or empty alt if it’s purely decorative).

-  alt: 'RS Logo',
+  alt: 'World map background',
πŸ“œ Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between e3a4b26 and b8fa55d.

β›” Files ignored due to path filters (18)
  • package-lock.json is excluded by !**/package-lock.json
  • public/fonts/Inter_28pt-Bold.ttf is excluded by !**/*.ttf
  • public/fonts/Inter_28pt-Regular.ttf is excluded by !**/*.ttf
  • src/shared/assets/og-logos/angular.svg is excluded by !**/*.svg
  • src/shared/assets/og-logos/aws-cloud-developer.svg is excluded by !**/*.svg
  • src/shared/assets/og-logos/aws-devops.svg is excluded by !**/*.svg
  • src/shared/assets/og-logos/aws-fundamentals.svg is excluded by !**/*.svg
  • src/shared/assets/og-logos/javascript-preschool-ru.svg is excluded by !**/*.svg
  • src/shared/assets/og-logos/javascript-ru.svg is excluded by !**/*.svg
  • src/shared/assets/og-logos/javascript.svg is excluded by !**/*.svg
  • src/shared/assets/og-logos/map.png is excluded by !**/*.png
  • src/shared/assets/og-logos/mentor-with-his-students.png is excluded by !**/*.png
  • src/shared/assets/og-logos/nodejs.svg is excluded by !**/*.svg
  • src/shared/assets/og-logos/reactjs.svg is excluded by !**/*.svg
  • src/shared/assets/og-logos/rs-banner.png is excluded by !**/*.png
  • src/shared/assets/og-logos/rs-banner.svg is excluded by !**/*.svg
  • src/shared/assets/og-logos/rs-school.png is excluded by !**/*.png
  • src/shared/assets/og-logos/rss-logo.svg is excluded by !**/*.svg
πŸ“’ Files selected for processing (32)
  • .gitignore (1 hunks)
  • dev-data/open-graph.data.ts (1 hunks)
  • package.json (2 hunks)
  • scripts/generate-og-script.ts (1 hunks)
  • scripts/types.ts (1 hunks)
  • scripts/utils/course-info.ts (1 hunks)
  • scripts/utils/ensure-dir-exists.ts (1 hunks)
  • scripts/utils/fetch-courses-list.ts (1 hunks)
  • scripts/utils/generate-courses-tree.ts (1 hunks)
  • scripts/utils/generate-image.tsx (1 hunks)
  • scripts/utils/generate-page-tree.ts (1 hunks)
  • scripts/utils/load-fonts.ts (1 hunks)
  • scripts/utils/load-image-as-data-uri.ts (1 hunks)
  • src/app/community/page.tsx (1 hunks)
  • src/app/courses/angular/page.tsx (1 hunks)
  • src/app/courses/aws-cloud-developer/page.tsx (1 hunks)
  • src/app/courses/aws-devops/page.tsx (1 hunks)
  • src/app/courses/aws-fundamentals/page.tsx (1 hunks)
  • src/app/courses/javascript-preschool-ru/page.tsx (1 hunks)
  • src/app/courses/javascript-ru/page.tsx (1 hunks)
  • src/app/courses/javascript/page.tsx (1 hunks)
  • src/app/courses/nodejs/page.tsx (1 hunks)
  • src/app/courses/page.tsx (1 hunks)
  • src/app/courses/reactjs/page.tsx (1 hunks)
  • src/app/docs/[lang]/[...slug]/page.tsx (2 hunks)
  • src/app/docs/[lang]/page.tsx (1 hunks)
  • src/app/layout.tsx (1 hunks)
  • src/app/mentorship/[course]/page.tsx (1 hunks)
  • src/app/mentorship/page.tsx (1 hunks)
  • src/app/page.tsx (1 hunks)
  • src/shared/helpers/generate-page-metadata.ts (1 hunks)
  • tsconfig.json (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
src/app/courses/aws-cloud-developer/page.tsx (4)
dev-data/index.ts (1)
  • COURSE_TITLES (36-36)
src/app/courses/reactjs/page.tsx (1)
  • generateMetadata (10-21)
src/shared/helpers/get-course-title.ts (1)
  • getCourseTitle (4-10)
src/shared/helpers/generate-page-metadata.ts (1)
  • generatePageMetadata (1-26)
scripts/utils/fetch-courses-list.ts (1)
scripts/types.ts (1)
  • ApiCourse (1-4)
scripts/utils/course-info.ts (1)
scripts/types.ts (1)
  • ApiCourse (1-4)
src/app/courses/page.tsx (7)
src/app/community/page.tsx (1)
  • generateMetadata (6-17)
src/app/courses/reactjs/page.tsx (1)
  • generateMetadata (10-21)
src/app/page.tsx (1)
  • generateMetadata (6-17)
src/app/mentorship/page.tsx (1)
  • generateMetadata (7-18)
src/app/docs/[lang]/page.tsx (1)
  • generateMetadata (9-20)
src/app/layout.tsx (1)
  • metadata (11-36)
src/shared/helpers/generate-page-metadata.ts (1)
  • generatePageMetadata (1-26)
scripts/generate-og-script.ts (10)
scripts/utils/load-fonts.ts (2)
  • Font (4-9)
  • loadFont (11-23)
dev-data/open-graph.data.ts (3)
  • COURSE_SLUGS (1-11)
  • RS_PAGES (13-19)
  • Descriptions (21-27)
scripts/types.ts (1)
  • ApiCourse (1-4)
scripts/utils/fetch-courses-list.ts (1)
  • fetchCoursesList (3-10)
scripts/utils/ensure-dir-exists.ts (1)
  • ensureDirExists (3-14)
scripts/utils/load-image-as-data-uri.ts (1)
  • loadImageAsDataUri (4-8)
scripts/utils/course-info.ts (1)
  • getCourseInfo (5-19)
scripts/utils/generate-courses-tree.ts (1)
  • createCourseTree (3-122)
scripts/utils/generate-image.tsx (1)
  • generateImage (6-32)
scripts/utils/generate-page-tree.ts (1)
  • createPageTree (3-130)
scripts/utils/generate-image.tsx (1)
scripts/utils/load-fonts.ts (1)
  • Font (4-9)
src/app/docs/[lang]/[...slug]/page.tsx (3)
src/app/layout.tsx (1)
  • metadata (11-36)
src/shared/helpers/generate-page-metadata.ts (1)
  • generatePageMetadata (1-26)
src/app/docs/constants.ts (1)
  • TITLE_POSTFIX (2-2)
πŸͺ› GitHub Actions: Pull Request
scripts/generate-og-script.ts

[error] 28-28: TypeError: Failed to parse URL from undefined. The error originates from an invalid URL input in fetchCoursesList function called at line 4 in fetch-courses-list.ts. This caused the build to fail with exit code 1.

πŸ”‡ Additional comments (40)
tsconfig.json (1)

56-58: Correctly updated the TypeScript configuration to include new script

The inclusion of "scripts/generate-og-script.ts" in the TypeScript configuration ensures proper type checking and compilation for the new OG image generation script.

scripts/types.ts (1)

1-4: Type definition looks good

The ApiCourse type correctly defines the expected structure with the necessary properties for course data handling.

.gitignore (1)

35-36: Good practice: ignoring generated OG image directories

The addition of generated OG image directories to .gitignore prevents unwanted version control of dynamically generated assets.

src/app/layout.tsx (1)

12-12: Remember to change this URL during local development

The metadataBase URL is set to the production URL. As mentioned in the PR description, developers will need to modify this to 'http://localhost:3000' during local development to ensure proper OG image rendering.

Ensure this is documented in the project README or development guide to help other developers.

src/app/courses/page.tsx (3)

3-3: Proper import added for metadata helper function

The import of generatePageMetadata enables consistent metadata generation across the site.


8-8: Great descriptive text for SEO

The description provides a concise summary that effectively captures the value proposition of the courses.


10-15: Good implementation of centralized metadata generation

Using the helper function ensures consistency across all pages and properly includes Open Graph image data for social sharing.

src/app/mentorship/page.tsx (3)

3-3: Proper import added for metadata helper function

The import aligns with the overall approach to centralize metadata generation.


9-9: Concise and engaging mentorship description

The description effectively communicates the reciprocal benefit of mentorship.


11-16: Consistent implementation of metadata generation

The helper function is properly used with all required parameters, maintaining consistent structure.

src/app/mentorship/[course]/page.tsx (1)

3-3: Proper import added for metadata helper function

The import matches the pattern used across other page components.

src/app/page.tsx (3)

3-3: Proper import added for metadata helper function

The import enables consistent metadata generation for the home page.


8-8: Effective and concise home page description

The description captures the community spirit of the school in a memorable way.


10-16: Proper implementation of metadata generation

The helper function is correctly used with all required parameters to generate complete metadata including Open Graph image data.

src/app/community/page.tsx (3)

3-3: New import added correctly.

The import for generatePageMetadata helper is properly added.


8-8: Good description for community page.

Clear, concise description that accurately represents the community page content.


10-16: Metadata implementation looks good.

The implementation correctly uses the new helper function with appropriate parameters:

  • Title from existing code
  • New description
  • Proper OG image path that follows the convention

This ensures consistent metadata structure across the site.

src/app/courses/nodejs/page.tsx (3)

3-3: New import added correctly.

The import for generatePageMetadata helper is properly added.


11-12: Good metadata implementation.

The title is correctly fetched asynchronously, and the Node.js course has a detailed, specific description.


14-20: Metadata generation looks good.

The implementation correctly uses the helper function with appropriate parameters:

  • Dynamic title
  • Specific course description
  • Proper OG image path following the naming convention

This ensures consistent metadata across course pages.

src/app/courses/reactjs/page.tsx (2)

3-3: New import added correctly.

The import for generatePageMetadata helper is properly added.


14-20: Metadata implementation is correct.

The helper function is used correctly with the appropriate parameters, ensuring consistent metadata structure.

src/app/docs/[lang]/page.tsx (2)

4-4: New import added correctly.

The import for generatePageMetadata helper is properly added.


10-19: Metadata implementation looks good.

The implementation correctly:

  • Uses the existing title with postfix
  • Adds a clear and concise description
  • References the appropriate OG image path
  • Uses the helper function consistently

This ensures standardized metadata structure across the site.

src/app/courses/javascript-preschool-ru/page.tsx (1)

3-3: Properly implemented Open Graph metadata

The metadata generation is correctly implemented using the new helper function. The course-specific description and image path provide good context for social sharing.

Also applies to: 11-20

src/app/courses/angular/page.tsx (1)

3-3: Well-implemented OG metadata

The metadata implementation follows the project's standard pattern and provides a clear description that accurately represents the Angular course.

Also applies to: 11-20

src/app/courses/aws-fundamentals/page.tsx (1)

3-3: Correctly implemented metadata

The AWS Fundamentals course metadata is properly implemented with a relevant description and appropriate image path.

Also applies to: 11-20

src/app/courses/javascript-ru/page.tsx (1)

10-21: Metadata implementation looks good

The implementation correctly uses the central generatePageMetadata helper to create consistent metadata across the site.

src/app/docs/[lang]/[...slug]/page.tsx (1)

48-56: Well-implemented metadata enhancement

The metadata generation follows the standardized pattern using the shared helper function.

scripts/utils/ensure-dir-exists.ts (1)

3-14: Well-implemented directory existence check

This utility function correctly handles directory creation with proper error handling.

The implementation:

  1. Properly checks if the directory exists
  2. Creates it recursively if needed
  3. Handles errors appropriately, distinguishing between missing directories and other errors
src/app/courses/aws-cloud-developer/page.tsx (2)

3-3: New import for metadata generation utility.

Good addition of the generatePageMetadata helper to standardize metadata generation.


11-20: Metadata implementation looks good.

The enhanced metadata implementation properly includes title, description, and OG image path, following the project's new metadata standard.

package.json (2)

7-9: Script integration for OG image generation.

Good implementation of pre-build OG image generation script that runs before both development and build processes.


95-95: Added tsx dependency.

Appropriate addition of the tsx package as a dev dependency to support the new OG image generation script.

dev-data/open-graph.data.ts (2)

1-11: Course slugs mapping looks good.

The course slugs mapping provides a clean way to reference course URLs.


13-20: Page mapping implementation.

Clear mapping of page identifiers to their display names.

src/app/courses/aws-devops/page.tsx (2)

3-3: New import for metadata generation utility.

Good addition of the generatePageMetadata helper to standardize metadata generation.


11-20: Metadata implementation looks good.

The enhanced metadata implementation properly includes title, description, and OG image path, following the project's new metadata standard.

src/shared/helpers/generate-page-metadata.ts (1)

1-26: Well-structured metadata generator

This function provides a clean, standardized way to generate page metadata including Open Graph properties. The structure matches Next.js metadata requirements and includes all essential fields for proper social media sharing.

scripts/utils/generate-image.tsx (1)

1-32: Robust image generation with proper error handling

The function correctly handles JSX-to-image conversion with appropriate input validation and error catching. The use of Buffer for the output makes it versatile for file system operations.

Comment on lines 14 to 21
style: {
display: 'flex',
width: 1200,
height: 630,
fontFamily: 'Inter',
weight: 400,
},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix invalid CSS property in root element styles

The root div uses an invalid CSS property weight which should be fontWeight.

style: {
  display: 'flex',
  width: 1200,
  height: 630,
  fontFamily: 'Inter',
-  weight: 400,
+  fontWeight: 400,
},
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
style: {
display: 'flex',
width: 1200,
height: 630,
fontFamily: 'Inter',
weight: 400,
},
},
style: {
display: 'flex',
width: 1200,
height: 630,
fontFamily: 'Inter',
fontWeight: 400,
},
},

Copy link

Lighthouse Report:

  • Performance: 79 / 100
  • Accessibility: 96 / 100
  • Best Practices: 100 / 100
  • SEO: 100 / 100

View detailed report

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
scripts/utils/get-courses-schedule.ts (3)

4-7: Consider adding JSDoc documentation.

This utility function would benefit from JSDoc documentation explaining its purpose, return type, and potential errors.

+/**
+ * Fetches course schedule data from the API
+ * @returns Array of course objects with startDate and descriptionUrl properties
+ * @throws Logs error and returns empty array if fetch fails
+ */
 export async function getCoursesSchedule() {
   try {
     const response = await api.course.queryCoursesSchedule();

7-8: Type assertion should be validated.

The type assertion (as CoursesScheduleResponse) doesn't validate the actual shape of the data. Consider using runtime validation or a more type-safe approach.

-    const courses = response.result as CoursesScheduleResponse;
+    // Option 1: Basic validation
+    const result = response.result;
+    if (!Array.isArray(result)) {
+      throw new Error('Invalid API response format');
+    }
+    const courses = result as CoursesScheduleResponse;
+    
+    // Option 2: With a validation library like zod, ajv, etc.
+    // import { courseScheduleSchema } from '../../path/to/validation';
+    // const courses = courseScheduleSchema.parse(response.result);

4-17: Consider dependency injection for better testability.

The current implementation directly imports the API client, making it difficult to test in isolation. Consider using dependency injection for better testability.

-export async function getCoursesSchedule() {
+export async function getCoursesSchedule(apiClient = api) {
   try {
-    const response = await api.course.queryCoursesSchedule();
+    const response = await apiClient.course.queryCoursesSchedule();
     const courses = response.result as CoursesScheduleResponse;
πŸ“œ Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between ab05327 and 181b67c.

πŸ“’ Files selected for processing (3)
  • scripts/generate-og-script.ts (1 hunks)
  • scripts/utils/course-info.ts (1 hunks)
  • scripts/utils/get-courses-schedule.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/utils/course-info.ts
  • scripts/generate-og-script.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
scripts/utils/get-courses-schedule.ts (2)
src/shared/api/api.ts (1)
  • api (3-3)
src/entities/course/types.ts (1)
  • CoursesScheduleResponse (13-32)
πŸ”‡ Additional comments (3)
scripts/utils/get-courses-schedule.ts (3)

1-3: Appropriate imports for the utility.

The imports correctly reference the required type and API client needed for this utility function.


9-12: Appropriate data extraction logic.

The mapping logic correctly extracts the needed fields with fallbacks for missing values.


13-16: Error handling looks good.

The function appropriately catches errors, logs them, and returns a sensible fallback value.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
scripts/generate-og-script.ts (1)

133-141: πŸ› οΈ Refactor suggestion

Use Promise.all for better error handling

Running tasks in parallel with separate error handlers can terminate the process before all tasks complete.

- generateOgImagePages().catch((err) => {
-   console.error(err);
-   process.exit(1);
- });
-
- generateOGCourses().catch((err) => {
-   console.error(err);
-   process.exit(1);
- });
+ Promise.all([generateOgImagePages(), generateOGCourses()])
+   .then(() => console.log('OG image generation finished βœ…'))
+   .catch((err) => {
+     console.error('OG image generation failed ❌', err);
+     process.exit(1);
+   });
🧹 Nitpick comments (3)
scripts/utils/courses-tree/generate-courses-tree.tsx (2)

5-12: Parameter naming could be clearer

The parameters rsLogoDataUriPromise and logoCourseUriPromise suggest they're Promises, but they're actually string values (already resolved data URIs).

export function createCourseTree(
  title: string,
  leftTitle: string,
  leftSubtitle: string,
  formattedDate: string,
-  rsLogoDataUriPromise: string,
-  logoCourseUriPromise: string,
+  rsLogoDataUri: string,
+  logoCourseDataUri: string,
): React.JSX.Element {

Corresponding changes should be made in the function body:

        <img
-          src={rsLogoDataUriPromise}
+          src={rsLogoDataUri}
          style={stylesCourseTree.logo}
          alt="RS School Logo"
        />
        
        <img
-          src={logoCourseUriPromise}
+          src={logoCourseDataUri}
          style={stylesCourseTree.courseLogo}
          alt={`${title} logo`}
        />

16-20: Improve accessibility with more descriptive alt text

The current alt text is generic. Consider adding more context for better accessibility.

        <img
          src={rsLogoDataUriPromise}
          style={stylesCourseTree.logo}
-          alt="RS School Logo"
+          alt="Rolling Scopes School Logo"
        />
scripts/generate-og-script.ts (1)

24-80: Inconsistent function naming

Function naming has inconsistent capitalization: generateOGCourses vs generateOgImagePages.

- async function generateOGCourses(): Promise<void> {
+ async function generateOgCourses(): Promise<void> {

Update references too:

- Promise.all([generateOgImagePages(), generateOGCourses()])
+ Promise.all([generateOgImagePages(), generateOgCourses()])
🧰 Tools
πŸͺ› GitHub Actions: Preview

[error] 57-57: Error: ENOENT: no such file or directory, open '/home/runner/work/site/site/src/shared/assets/og-logos/undefined.svg'. The file 'undefined.svg' is missing, causing the build script to fail.

πŸͺ› GitHub Actions: Pull Request

[error] 57-57: Error: ENOENT: no such file or directory, open '/home/runner/work/site/site/src/shared/assets/og-logos/undefined.svg'. The file 'undefined.svg' is missing, causing the build script to fail.

πŸ“œ Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 181b67c and 5832416.

πŸ“’ Files selected for processing (3)
  • scripts/generate-og-script.ts (1 hunks)
  • scripts/utils/courses-tree/generate-courses-tree.styles.ts (1 hunks)
  • scripts/utils/courses-tree/generate-courses-tree.tsx (1 hunks)
βœ… Files skipped from review due to trivial changes (1)
  • scripts/utils/courses-tree/generate-courses-tree.styles.ts
🧰 Additional context used
πŸͺ› GitHub Actions: Preview
scripts/generate-og-script.ts

[error] 57-57: Error: ENOENT: no such file or directory, open '/home/runner/work/site/site/src/shared/assets/og-logos/undefined.svg'. The file 'undefined.svg' is missing, causing the build script to fail.

πŸͺ› GitHub Actions: Pull Request
scripts/generate-og-script.ts

[error] 57-57: Error: ENOENT: no such file or directory, open '/home/runner/work/site/site/src/shared/assets/og-logos/undefined.svg'. The file 'undefined.svg' is missing, causing the build script to fail.

πŸ”‡ Additional comments (2)
scripts/utils/courses-tree/generate-courses-tree.tsx (1)

1-35: Component looks good overall

The JSX structure is clean and well-organized. The component correctly uses the imported styles and creates a visually balanced layout for the Open Graph image.

scripts/generate-og-script.ts (1)

1-23: Script setup looks well-organized

The imports, font loading, and type definitions are well-structured and follow good practices.

Comment on lines 51 to 61
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 = getCourseInfo(coursesSchedule, slug);

const logoDataUri: string = await loadImageAsDataUri(
`src/shared/assets/og-logos/${slug}.svg`,
'image/svg+xml',
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle undefined slug to prevent build failure

Pipeline failures show that the script is trying to access undefined.svg. You need to validate course slugs before using them.

  for (const key of Object.keys(COURSE_TITLES) as CourseKey[]) {
    const slug: string = COURSE_SLUGS[key];
+   if (!slug) {
+     console.warn(`Skipping OG image generation for ${key} - slug is undefined`);
+     continue;
+   }
    const title: string = COURSE_TITLES[key];

    const formattedDate: string = getCourseInfo(coursesSchedule, slug);

    const logoDataUri: string = await loadImageAsDataUri(
      `src/shared/assets/og-logos/${slug}.svg`,
      'image/svg+xml',
    );
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 = getCourseInfo(coursesSchedule, slug);
const logoDataUri: string = await loadImageAsDataUri(
`src/shared/assets/og-logos/${slug}.svg`,
'image/svg+xml',
);
for (const key of Object.keys(COURSE_TITLES) as CourseKey[]) {
const slug: string = COURSE_SLUGS[key];
if (!slug) {
console.warn(`Skipping OG image generation for ${key} - slug is undefined`);
continue;
}
const title: string = COURSE_TITLES[key];
const formattedDate: string = getCourseInfo(coursesSchedule, slug);
const logoDataUri: string = await loadImageAsDataUri(
`src/shared/assets/og-logos/${slug}.svg`,
'image/svg+xml',
);
// …rest of your loop body…
}
🧰 Tools
πŸͺ› GitHub Actions: Preview

[error] 57-57: Error: ENOENT: no such file or directory, open '/home/runner/work/site/site/src/shared/assets/og-logos/undefined.svg'. The file 'undefined.svg' is missing, causing the build script to fail.

πŸͺ› GitHub Actions: Pull Request

[error] 57-57: Error: ENOENT: no such file or directory, open '/home/runner/work/site/site/src/shared/assets/og-logos/undefined.svg'. The file 'undefined.svg' is missing, causing the build script to fail.

πŸ€– Prompt for AI Agents
In scripts/generate-og-script.ts around lines 51 to 61, the code uses
COURSE_SLUGS[key] without checking if the slug is defined, causing build
failures when slug is undefined. Add a validation step after retrieving the slug
to check if it is defined; if not, skip processing that key or handle the error
gracefully to prevent accessing undefined.svg files.

Comment on lines 71 to 78
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`);
Copy link
Contributor

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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`);
const buffer: Buffer<ArrayBufferLike> | null = await generateImage(tree, fonts);
if (!buffer) {
throw new Error(`Doesn't generate image for "${slug}"`);
}
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;
}
πŸ€– Prompt for AI Agents
In scripts/generate-og-script.ts around lines 71 to 78, the image generation and
file writing operations lack error handling, which can cause unhandled
exceptions. Wrap the code that generates the image buffer and writes the file in
a try-catch block. In the catch block, log or handle the error appropriately to
prevent the script from crashing unexpectedly.

Comment on lines 122 to 130
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`);
}
Copy link
Contributor

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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`);
}
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}"`);
}
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;
}
}
πŸ€– Prompt for AI Agents
In scripts/generate-og-script.ts around lines 122 to 130, the error handling for
the image generation is missing proper checks similar to those used in course
image generation. Add a check to verify if the buffer is valid before attempting
to write the file, and if not, throw an error with a descriptive message
including the title. This ensures consistent and clear error handling for image
generation failures.

Copy link

Lighthouse Report:

  • Performance: 63 / 100
  • Accessibility: 96 / 100
  • Best Practices: 100 / 100
  • SEO: 100 / 100

View detailed report

Comment on lines 11 to 15
// type CourseData = {
// name: string;
// logo: StaticImageData;
// startDate: string;
// };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented code

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted comments

// };

type CourseLogo = {
src: string; // Data URI ΠΈΠ»ΠΈ URL
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use english

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted comments

@SpaNb4
Copy link
Collaborator

SpaNb4 commented May 28, 2025

Please pull the latest changes from main so we don't have to see all these changes
image

Copy link

Lighthouse Report:

  • Performance: 82 / 100
  • Accessibility: 96 / 100
  • Best Practices: 100 / 100
  • SEO: 100 / 100

View detailed report

Copy link

Lighthouse Report:

  • Performance: 98 / 100
  • Accessibility: 96 / 100
  • Best Practices: 100 / 100
  • SEO: 100 / 100

View detailed report

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

πŸ”­ Outside diff range comments (1)
src/app/courses/[slug]/page.tsx (1)

12-18: πŸ› οΈ Refactor suggestion

params isn’t a Promiseβ€”tighten the type.
generateMetadata receives { params: Params }, not a promise. The current typing silently widens everything to any and the extra await is a no-op.

-type CourseRouteParams = {
-  params: Promise<Params>;
-};
+type CourseRouteParams = {
+  params: Params;
+};
 …
-const { slug } = await params;
+const { slug } = params;

This keeps the types honest and removes an unnecessary await.

🧹 Nitpick comments (9)
src/shared/og/utils/fetch-and-convert-to-data-uri.ts (1)

3-28: Add basic caching & size-guard to cut repeated fetch + sharp cost

The helper works, but each OG-image build now performs an unconditional network fetch + full PNG re-encode – expensive when dozens of pages hit the same logo.

Consider:

+// naive in-memory cache – good enough for build-time
+const cache = new Map<string, string>();
+
 export async function fetchAndConvertToDataUri(url: string): Promise<string> {
+  if (cache.has(url)) return cache.get(url)!;
   try {
     const res = await fetch(url);
@@
-    const pngBuffer = await sharp(buffer)
+    // Guard against unexpectedly large assets (>2 MB)
+    if (buffer.byteLength > 2 * 1024 * 1024) {
+      throw new Error(`Image ${url} is too large (${buffer.byteLength} bytes)`);
+    }
+
+    const pngBuffer = await sharp(buffer)
       .png({
         compressionLevel: 9,
         quality: 90,
       })
       .toBuffer();
@@
-    return `data:image/png;base64,${base64}`;
+    const dataUri = `data:image/png;base64,${base64}`;
+    cache.set(url, dataUri);
+    return dataUri;
   } catch (err) {

Cuts duplicate work and prevents bloated builds.

src/shared/og/utils/load-image-as-data-uri.ts (1)

5-21: Prefer path.resolve and bubble original error

Minor, but improves clarity:

-    const buf: Buffer = await fs.readFile(path.join(process.cwd(), relativePath));
+    const absPath = path.resolve(process.cwd(), relativePath);
+    const buf = await fs.readFile(absPath);
@@
-  } catch (error) {
-    console.error(`Error loading image from ${relativePath}:`, error);
-    throw new Error(`Failed to load image as data URI: ${error}`);
+  } catch (error) {
+    console.error(`Error loading image from ${relativePath}:`, error);
+    throw error; // preserves stack & type
   }

Keeps error stacks intact and avoids surprises if relativePath already starts with /.

src/views/course/model/store.ts (1)

30-34: Tiny optimisation: bail early instead of mutating empty array

-let sections: Section[] = [];
-
-if (coursePageSections && coursePageSections.length > 0) {
-  sections = transformCourseSections(coursePageSections);
-}
+const sections: Section[] =
+  coursePageSections?.length ? transformCourseSections(coursePageSections) : [];

Not critical, but terser & side-effect-free.

src/shared/og/utils/load-fonts.ts (1)

16-18: Avoid the extra memory copy when converting Buffer β†’ ArrayBuffer.
new Uint8Array(buffer).buffer allocates a fresh Uint8Array. You can get the underlying ArrayBuffer for free with:

-const arrayBuffer = new Uint8Array(buffer).buffer;
+const arrayBuffer = buffer.buffer.slice(
+  buffer.byteOffset,
+  buffer.byteOffset + buffer.byteLength,
+);

This shaves an additional allocation and keeps GC pressure lower when the util is imported in many edge routes.

src/app/courses/[slug]/page.tsx (1)

24-26: robots should follow Next.js type, not a raw string.
Metadata.robots expects Robots or string | boolean. A plain string passes type-checking but loses semantic richness (e.g., noarchive). Prefer the object form:

-const robots = 'index, follow';
+const robots = { index: true, follow: true };
src/shared/og/view/pages-tree/generate-pages-tree.tsx (1)

27-35: Incorrect alt text for purely decorative imagery.
The background map and mascot images are decorative in the OG card and should carry empty alt to avoid noisy screen-reader output. Conversely, "RS Logo" is misleading for a map.

-<img src={mapBgUri} alt="RS Logo" …
+<img src={mapBgUri} alt="" aria-hidden="true" …
 …
-<img src={rsMascotsUri} alt="RS mascots" …
+<img src={rsMascotsUri} alt="" aria-hidden="true" …

Keeps accessibility lint quiet even though OG images aren’t rendered in browsers.

src/app/docs/[lang]/og.png/route.ts (1)

12-20: Russian title is still English.
titleMap.ru repeats the English string; probably meant something like "ДокумСнтация RS School". Quick fix:

-ru: 'RS School Docs',
+ru: 'ДокумСнтация RS School',
src/app/courses/[slug]/og.png/route.ts (2)

41-46: Uncached remote logo fetch = NΓ— network hits at build.
fetchAndConvertToDataUri will run for every course during next export, potentially hammering the remote CDN. Cache per-process:

-let logoDataUri: string;
+let logoDataUri: string | undefined = logoCache.get(meta.iconSrc.src);
+
 if (!logoDataUri) {
   try {
-    logoDataUri = await fetchAndConvertToDataUri(meta.iconSrc.src);
+    logoDataUri = await fetchAndConvertToDataUri(meta.iconSrc.src);
+    logoCache.set(meta.iconSrc.src, logoDataUri);
   } catch (err) {

A simple Map<string,string> at module scope suffices.


34-38: || collapses legitimate zero dimensions.
If width is explicitly 0, || will fall back to 250. Use nullish-coalescing:

-const logoWidth = meta.iconSrc.width || LOGO_FALLBACK_SIZE;
-const logoHeight = meta.iconSrc.height || LOGO_FALLBACK_SIZE;
+const logoWidth = meta.iconSrc.width ?? LOGO_FALLBACK_SIZE;
+const logoHeight = meta.iconSrc.height ?? LOGO_FALLBACK_SIZE;
πŸ“œ Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between b775830 and 002ab79.

β›” Files ignored due to path filters (3)
  • package-lock.json is excluded by !**/package-lock.json
  • src/shared/assets/fonts/Inter_28pt-Bold.ttf is excluded by !**/*.ttf
  • src/shared/assets/fonts/Inter_28pt-Regular.ttf is excluded by !**/*.ttf
πŸ“’ Files selected for processing (26)
  • package.json (1 hunks)
  • src/app/community/og.png/route.ts (1 hunks)
  • src/app/community/page.tsx (1 hunks)
  • src/app/courses/[slug]/og.png/route.ts (1 hunks)
  • src/app/courses/[slug]/page.tsx (2 hunks)
  • src/app/courses/og.png/route.ts (1 hunks)
  • src/app/courses/page.tsx (1 hunks)
  • src/app/docs/[lang]/[...slug]/page.tsx (2 hunks)
  • src/app/docs/[lang]/og.png/route.ts (1 hunks)
  • src/app/docs/[lang]/page.tsx (1 hunks)
  • src/app/mentorship/[course]/page.tsx (1 hunks)
  • src/app/mentorship/og.png/route.ts (1 hunks)
  • src/app/mentorship/page.tsx (1 hunks)
  • src/app/og.png/route.ts (1 hunks)
  • src/app/page.tsx (1 hunks)
  • src/shared/constants.tsx (0 hunks)
  • src/shared/helpers/generate-page-metadata.ts (1 hunks)
  • src/shared/og/utils/fetch-and-convert-to-data-uri.ts (1 hunks)
  • src/shared/og/utils/load-fonts.ts (1 hunks)
  • src/shared/og/utils/load-image-as-data-uri.ts (1 hunks)
  • src/shared/og/view/courses-tree/generate-courses-tree.styles.ts (1 hunks)
  • src/shared/og/view/courses-tree/generate-courses-tree.tsx (1 hunks)
  • src/shared/og/view/pages-tree/generate-pages-tree.styles.ts (1 hunks)
  • src/shared/og/view/pages-tree/generate-pages-tree.tsx (1 hunks)
  • src/shared/types/contentful/TypeHomePage.ts (1 hunks)
  • src/views/course/model/store.ts (2 hunks)
πŸ’€ Files with no reviewable changes (1)
  • src/shared/constants.tsx
βœ… Files skipped from review due to trivial changes (7)
  • src/shared/og/view/pages-tree/generate-pages-tree.styles.ts
  • src/app/community/og.png/route.ts
  • src/app/og.png/route.ts
  • src/app/courses/og.png/route.ts
  • src/app/mentorship/og.png/route.ts
  • src/shared/og/view/courses-tree/generate-courses-tree.tsx
  • src/shared/og/view/courses-tree/generate-courses-tree.styles.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • package.json
  • src/app/courses/page.tsx
  • src/app/mentorship/[course]/page.tsx
  • src/app/docs/[lang]/[...slug]/page.tsx
  • src/app/community/page.tsx
  • src/app/mentorship/page.tsx
  • src/app/page.tsx
  • src/shared/helpers/generate-page-metadata.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/app/docs/[lang]/og.png/route.ts (7)
src/app/community/og.png/route.ts (3)
  • dynamic (4-4)
  • preferredRegion (3-3)
  • GET (6-14)
src/app/courses/og.png/route.ts (3)
  • dynamic (4-4)
  • preferredRegion (3-3)
  • GET (6-14)
src/app/mentorship/og.png/route.ts (3)
  • dynamic (4-4)
  • preferredRegion (3-3)
  • GET (6-14)
src/app/og.png/route.ts (3)
  • dynamic (4-4)
  • preferredRegion (3-3)
  • GET (6-14)
src/app/docs/[lang]/page.tsx (1)
  • generateStaticParams (34-36)
src/app/docs/[lang]/[...slug]/page.tsx (1)
  • generateStaticParams (68-108)
src/shared/og/view/pages-tree/generate-pages-tree.tsx (1)
  • createPageTree (16-47)
src/app/courses/[slug]/page.tsx (2)
src/views/course/model/store.ts (1)
  • coursePageStore (77-77)
src/shared/helpers/generate-page-metadata.ts (1)
  • generatePageMetadata (12-48)
src/app/courses/[slug]/og.png/route.ts (6)
src/app/courses/[slug]/page.tsx (1)
  • generateStaticParams (37-39)
src/views/course/model/store.ts (1)
  • coursePageStore (77-77)
src/entities/course/helpers/resolve-course-page-locale.ts (1)
  • resolveCoursePageLocale (9-13)
src/shared/og/utils/fetch-and-convert-to-data-uri.ts (1)
  • fetchAndConvertToDataUri (3-28)
src/shared/og/utils/load-image-as-data-uri.ts (1)
  • loadImageAsDataUri (5-21)
src/shared/og/view/courses-tree/generate-courses-tree.tsx (1)
  • createCourseTree (22-57)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build rs.school
  • GitHub Check: CI
πŸ”‡ Additional comments (4)
src/views/course/model/store.ts (1)

16-22: seoDescription / seoKeywords may be undefined – keep defaults in sync

Good defensive defaults (''), but downstream code treats keywords as string; later metadata helper may expect string | undefined | string[]. If you make fields optional in Contentful types (see comment above) you can simplify:

-  const {
-    title = '',
-    seoDescription = '',
-    seoKeywords = '',
+  const {
+    title = '',
+    seoDescription,
+    seoKeywords,

…and pass them through untouched; helper can decide fallbacks.

src/shared/og/utils/load-fonts.ts (1)

31-37: Top-level await can break older toolchains / Jest.
While Next.js (Edge / Node 18+) happily supports TLA, any consumer that still runs Jest under CommonJS ("type": "commonjs") will explode. Consider lazily exporting getFonts() that internally memoises the promise:

-const [fontRegular, fontBold] = await Promise.all([ … ]);
-export const fonts: Font[] = [fontRegular, fontBold];
+let fontsPromise: Promise<Font[]> | null = null;
+export const getFonts = (): Promise<Font[]> => {
+  if (!fontsPromise) {
+    fontsPromise = Promise.all([loadFont(400), loadFont(700)]);
+  }
+  return fontsPromise;
+};

This sidesteps the TLA requirement entirely.

src/shared/og/view/pages-tree/generate-pages-tree.tsx (1)

12-15: Eager asset loading blocks every route import.
Declaring the three Promises at module scope means any failed read kills all pages that import this helper. Wrap in a try/catch inside createPageTree or memoise with a safe fallback to prevent a single missing asset from taking the whole OG generation offline.

src/app/docs/[lang]/og.png/route.ts (1)

22-33: Missing dynamic = 'force-static' consistency.
Other OG routes export dynamic = 'force-static' to guarantee SSG. Omitting it here may leave the docs OG images in β€œauto” mode, breaking next export.

Comment on lines +9 to +15
export async function generateMetadata({
params,
}: {
params: Promise<RouteParams>;
}) {
const { lang } = await params;
const title = `RS School Overview ${TITLE_POSTFIX}`;
Copy link
Contributor

@coderabbitai coderabbitai bot Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

params isn’t a Promise – remove the await pattern

generateMetadata receives { params: RouteParams }, not a Promise.
Awaiting a non-promise silently succeeds but is misleading and trips up tooling.

-export async function generateMetadata({
-  params,
-}: {
-  params: Promise<RouteParams>;
-}) {
-  const { lang } = await params;
+export async function generateMetadata({ params }: { params: RouteParams }) {
+  const { lang } = params;

Apply the same correction to DocsIndex below for consistency.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function generateMetadata({
params,
}: {
params: Promise<RouteParams>;
}) {
const { lang } = await params;
const title = `RS School Overview ${TITLE_POSTFIX}`;
export async function generateMetadata({ params }: { params: RouteParams }) {
const { lang } = params;
const title = `RS School Overview ${TITLE_POSTFIX}`;
// …
}
πŸ€– Prompt for AI Agents
In src/app/docs/[lang]/page.tsx around lines 9 to 15, the function
generateMetadata incorrectly treats params as a Promise and uses await on it,
but params is actually a plain object of type RouteParams. Remove the await
keyword before params and adjust the type annotation to reflect that params is
not a Promise. Also, apply the same fix to the DocsIndex function below by
removing any await usage on params and correcting its type accordingly for
consistency.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the breaking changes that came with v15

vercel/next.js#71997

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LaraNU Got itβ€”thanks for the heads-up. Next.js v15 indeed changed the App Router helpers so that params (and other context values) are now delivered as Promises, which makes the existing await usage correct. I’ll keep that in mind for future reviews and retract the previous suggestion. πŸ‘


✏️ Learnings added
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: src/app/docs/[lang]/page.tsx:9-15
Timestamp: 2025-06-11T12:39:10.882Z
Learning: In Next.js v15, App Router helpers such as `generateMetadata` now receive context properties (`params`, `searchParams`, etc.) as Promises, so awaiting them is required and correct.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link

Lighthouse Report:

  • Performance: 77 / 100
  • Accessibility: 96 / 100
  • Best Practices: 100 / 100
  • SEO: 100 / 100

View detailed report

Copy link

Lighthouse Report:

  • Performance: 82 / 100
  • Accessibility: 96 / 100
  • Best Practices: 100 / 100
  • SEO: 100 / 100

View detailed report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Open Graph Previews and Proper Meta Descriptions
5 participants