Skip to content

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 Open Graph image generation for Home, Courses (list and per-course), Docs (EN/RU), Mentorship, Community, and Merch pages.
    • Per-course OG images include course name, logo, and start date.
  • Enhancements
    • Upgraded SEO metadata across Home, Courses, Docs, Community, Mentorship, and Merch with titles, descriptions, keywords, canonicals, robots, and social images.
    • Language-aware metadata for Docs and site-wide metadata base set.
  • Chores
    • Ignored public OG assets in version control.
    • Formatting updates to configuration files.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 14, 2025

πŸ“ Walkthrough

Walkthrough

Introduces centralized metadata generation and Open Graph (OG) image routes. Adds per-page metadata configs, updates page-level generateMetadata implementations, sets metadataBase, and implements OG image generation utilities, styles, and routes (including dynamic course-specific OG images with logo fetching/fallback). Extends course page store to surface SEO fields.

Changes

Cohort / File(s) Summary
Metadata helper and constants
src/shared/helpers/generate-page-metadata.ts, src/shared/constants.ts, src/app/layout.tsx
Adds generatePageMetadata helper; exports OG constants and dynamic rendering flag; sets metadataBase in root layout.
Page metadata: static pages
src/app/page.tsx, src/app/community/page.tsx, src/app/courses/page.tsx, src/app/mentorship/page.tsx, src/app/merch/page.tsx
Refactors generateMetadata to use metadata objects and helper; adds description/keywords/canonical/robots and OG image paths.
Page metadata: docs
src/app/docs/[lang]/page.tsx, src/app/docs/[lang]/[...slug]/page.tsx, src/metadata/docs.ts
Adds language-aware metadata and per-doc slug metadata via generator; constructs lang-specific OG image paths.
Page metadata: courses (slug)
src/app/courses/[slug]/page.tsx, src/entities/course-page/model/store.ts
generateMetadata now uses course page/store to include title/description/keywords/canonical and OG image path; store returns seo fields and courseUrl.
Metadata configs
src/metadata/home.ts, src/metadata/community.ts, src/metadata/courses.ts, src/metadata/mentorship.ts, src/metadata/merch.ts
Adds static metadata objects for pages (title, description, keywords, canonical, robots).
OG routes: static pages
src/app/og.png/route.ts, src/app/community/og.png/route.ts, src/app/courses/og.png/route.ts, src/app/docs/[lang]/og.png/route.ts, src/app/mentorship/og.png/route.ts, src/app/merch/og.png/route.ts
Adds GET handlers for generating OG images via createPageTree; docs route includes generateStaticParams for en/ru.
OG route: courses (slug)
src/app/courses/[slug]/og.png/route.ts
Adds dynamic OG generator per course; loads course data, resolves locale, fetches logo with fallback, returns image via createCourseTree; includes generateStaticParams and preferredRegion.
OG utilities
src/shared/og/utils/fetch-and-convert-to-data-uri.ts, src/shared/og/utils/load-image-as-data-uri.ts, src/shared/og/utils/load-fonts.ts
Adds helpers for remote image fetch→PNG data URI, local image→data URI, and font loading.
OG view builders and styles
src/shared/og/view/pages-tree/generate-pages-tree.tsx, src/shared/og/view/pages-tree/generate-pages-tree.styles.ts, src/shared/og/view/courses-tree/generate-courses-tree.tsx, src/shared/og/view/courses-tree/generate-courses-tree.styles.ts
Implements React-based image composition for generic pages and course-specific layouts with associated styles.
Contentful types
src/shared/types/contentful/TypeHomePage.ts
Adds seoDescription and seoKeywords to TypeHomePageFields.
Repo config
.gitignore, tsconfig.json
Ignore /public/og; formatting changes to tsconfig arrays.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as Client
  participant Next as Next.js Route (og.png)
  participant View as createPageTree
  participant Utils as OG Utils (fonts/images)
  User->>Next: GET /{page}/og.png
  Next->>View: createPageTree({ title, description })
  par Load assets
    View->>Utils: load fonts
    View->>Utils: load banner/mascots/map as data URIs
  end
  View-->>Next: ImageResponse (1200x630)
  Next-->>User: 200 PNG
Loading
sequenceDiagram
  autonumber
  actor User as Client
  participant Next as Next.js Route (/courses/[slug]/og.png)
  participant Store as coursePageStore/courseStore
  participant Utils as fetch/load image utils
  participant View as createCourseTree
  User->>Next: GET /courses/{slug}/og.png
  Next->>Store: resolveCoursePageLocale(slug)
  Next->>Store: loadCoursePage(slug, locale)
  Next->>Store: loadCourse(courseId)
  alt Logo available remotely
    Next->>Utils: fetchAndConvertToDataUri(course.iconSrc.src)
  else Fetch fails
    Next->>Utils: loadImageAsDataUri(fallback SVG)
  end
  Next->>View: createCourseTree({ name, logo, startDate })
  View-->>Next: ImageResponse
  Next-->>User: 200 PNG
Loading
sequenceDiagram
  autonumber
  participant Route as Page generateMetadata()
  participant Meta as page-specific metadata object
  participant Helper as generatePageMetadata
  Route->>Meta: import {title, desc, keywords, canonical, robots}
  Route->>Helper: generatePageMetadata({ ... , imagePath, alternates:{canonical}})
  Helper-->>Route: Metadata (OG/Twitter/robots/etc.)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Implement Open Graph previews for pages and dynamic course/docs routes (#390) βœ…
Add proper meta descriptions, keywords, canonical URLs, and robots across pages (#390) βœ…
Centralize metadata generation to ensure consistency (#390) βœ…
Ensure absolute URL handling for metadata (metadataBase) (#390) βœ…

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Reformat arrays and exclude only node_modules (tsconfig.json) Build config formatting not required by the OG/meta objective; functionally unrelated.
Add /public/og to .gitignore (.gitignore) VCS ignore rule not specified in the objective; not functionally tied to OG/meta implementation.

Possibly related PRs

Suggested labels

feature

Suggested reviewers

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

Tip

πŸ”Œ Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


πŸ“œ Recent review details

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

πŸ’‘ Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between f82e387 and 0ff6e08.

β›” Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
πŸ“’ Files selected for processing (1)
  • src/app/layout.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/layout.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI
✨ Finishing Touches
  • πŸ“ Generate Docstrings
πŸ§ͺ Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/390-add-open-graph-previews

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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

@github-actions
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.

@github-actions
Copy link

github-actions bot commented Aug 3, 2025

Lighthouse Report:

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

View detailed report

@coderabbitai coderabbitai bot mentioned this pull request Aug 5, 2025
8 tasks
@LaraNU
Copy link
Collaborator Author

LaraNU commented Aug 5, 2025

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.
https://nextjs.org/docs/app/api-reference/file-conventions/dynamic-routes - await use in new version

🧹 Nitpick comments (9)
πŸ“œ Review details

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)
src/shared/og/utils/fetch-and-convert-to-data-uri.ts (3)

3-3: Consider cache size management.

The unbounded Map cache could lead to memory leaks in long-running processes. Consider implementing cache eviction based on size or TTL.

Add cache size management:

-const cache = new Map<string, string>();
+const MAX_CACHE_SIZE = 100;
+const cache = new Map<string, string>();
+
+const evictOldestEntry = () => {
+  if (cache.size >= MAX_CACHE_SIZE) {
+    const firstKey = cache.keys().next().value;
+    cache.delete(firstKey);
+  }
+};

24-29: Optimize compression settings.

The current settings (compression level 9, quality 90) prioritize file size over processing speed. Consider if this trade-off is appropriate for OG image generation during build time.

For faster build times, consider:

 const pngBuffer = await sharp(buffer)
   .png({
-    compressionLevel: 9,
-    quality: 90,
+    compressionLevel: 6,
+    quality: 85,
   })
   .toBuffer();

33-33: Apply cache eviction if implemented.

If cache size management is added, insert eviction logic before setting new entries.

+    evictOldestEntry();
     cache.set(url, dataUri);
πŸ“œ Review details

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

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between c48ea6b and 8f47891.

πŸ“’ Files selected for processing (18)
  • src/app/courses/[slug]/og.png/route.ts (1 hunks)
  • src/app/courses/[slug]/page.tsx (2 hunks)
  • src/app/docs/[lang]/[...slug]/page.tsx (2 hunks)
  • src/app/merch/og.png/route.ts (1 hunks)
  • src/app/merch/page.tsx (1 hunks)
  • src/metadata/community.ts (1 hunks)
  • src/metadata/courses.ts (1 hunks)
  • src/metadata/docs.ts (1 hunks)
  • src/metadata/home.ts (1 hunks)
  • src/metadata/mentorship.ts (1 hunks)
  • src/metadata/merch.ts (1 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.tsx (1 hunks)
  • src/shared/og/view/pages-tree/generate-pages-tree.tsx (1 hunks)
  • src/views/course/model/store.ts (2 hunks)
βœ… Files skipped from review due to trivial changes (4)
  • src/metadata/merch.ts
  • src/metadata/docs.ts
  • src/metadata/home.ts
  • src/app/merch/og.png/route.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/app/docs/[lang]/[...slug]/page.tsx
  • src/metadata/community.ts
  • src/shared/og/utils/load-image-as-data-uri.ts
  • src/metadata/courses.ts
  • src/app/courses/[slug]/page.tsx
  • src/views/course/model/store.ts
  • src/metadata/mentorship.ts
  • src/shared/og/view/pages-tree/generate-pages-tree.tsx
  • src/shared/helpers/generate-page-metadata.ts
  • src/shared/og/view/courses-tree/generate-courses-tree.tsx
  • src/app/courses/[slug]/og.png/route.ts
🧰 Additional context used
🧠 Learnings (3)
πŸ““ Common learnings
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: scripts/generate-og-script.ts:0-0
Timestamp: 2025-08-04T19:51:21.905Z
Learning: In PR #894, the scripts/generate-og-script.ts file contains old/legacy code that is not part of the current OG image generation implementation. The current approach uses Next.js route handlers for OG image generation instead of separate scripts.
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: src/app/docs/[lang]/page.tsx:9-15
Timestamp: 2025-06-11T12:39:10.902Z
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.
πŸ“š Learning: in pr #894, the scripts/generate-og-script.ts file contains old/legacy code that is not part of the ...
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: scripts/generate-og-script.ts:0-0
Timestamp: 2025-08-04T19:51:21.905Z
Learning: In PR #894, the scripts/generate-og-script.ts file contains old/legacy code that is not part of the current OG image generation implementation. The current approach uses Next.js route handlers for OG image generation instead of separate scripts.

Applied to files:

  • src/app/merch/page.tsx
  • src/shared/og/utils/fetch-and-convert-to-data-uri.ts
πŸ“š Learning: in next.js v15, app router helpers such as `generatemetadata` now receive context properties (`param...
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: src/app/docs/[lang]/page.tsx:9-15
Timestamp: 2025-06-11T12:39:10.902Z
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.

Applied to files:

  • src/app/merch/page.tsx
🧬 Code Graph Analysis (1)
src/app/merch/page.tsx (2)
src/metadata/merch.ts (1)
  • merchMetadata (1-12)
src/shared/helpers/generate-page-metadata.ts (1)
  • generatePageMetadata (15-51)
πŸ”‡ Additional comments (6)
src/app/merch/page.tsx (2)

3-4: LGTM! Imports follow the established pattern.

The imports correctly bring in the standardized metadata constants and helper function, consistent with the broader metadata enhancement effort across the application.


7-20: Well-implemented metadata generation following the established pattern.

The function correctly uses the shared generatePageMetadata helper with all necessary SEO fields from the imported constants. The OG image path (/merch/og.png) aligns with the route-based OG image generation approach.

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

4-9: LGTM!

Clean type definition with proper weight constraints and optional style parameter.


35-42: LGTM!

Proper singleton pattern implementation prevents redundant font loading. The caching mechanism is efficient and thread-safe.


18-21: ArrayBuffer slice is required

The review suggestion to drop the slice() call would cause the returned ArrayBuffer to include any bytes outside the intended segment. In Node.js, buffer.buffer references the full backing store, whereas

buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)

extracts exactly the bytes for this Buffer. You can safely ignore the proposed simplification here.

Likely an incorrect or invalid review comment.

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

20-22: LGTM!

Good security practice with the 2MB size limit to prevent resource exhaustion attacks.

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: 6

🧹 Nitpick comments (11)
src/app/merch/og.png/route.ts (3)

1-1: Add an explicit ImageResponse type import for stronger typing

Minor nit: importing ImageResponse enables an explicit return type on GET, catching signature drift early.

Apply this diff near the existing import:

 import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree';
+import type { ImageResponse } from 'next/og';

5-13: GET handler is clean; optionally annotate the return type

Logic is straightforward and matches other OG routes. As a tiny improvement, add an explicit return type for clarity.

Apply this diff:

-export async function GET() {
+export async function GET(): Promise<ImageResponse> {

If you want, I can help add a smoke test asserting the route returns 200 with image/png and that the body is an ImageResponse.


3-3: Prefer inlining dynamic = 'force-static
We’ve confirmed in src/shared/constants.tsx that

export const DYNAMIC = 'force-static';

To avoid brittle re-exports, consider inlining:

File: src/app/merch/og.png/route.ts (line 3)

-export { DYNAMIC as dynamic } from '@/shared/constants';
+export const dynamic = 'force-static';

[optional_refactors_recommended]

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

21-29: LGTM; consider avoiding double data fetch for perf.

You’re loading full course data in both generateMetadata and the page. It’s fine for static export, but consider a lightweight loadCourseMeta that returns only {courseName, description, keywords, courseUrl} to trim fetch cost and coupling.

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

35-41: Make caching effective across calls (hoist the promise to module scope).

fontsPromise is recreated on every invocation, so nothing is cached. Hoist it to the module scope to dedupe FS reads during builds.

Apply within these lines:

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

Add this outside the selected range (module scope, e.g., right after the type Font):

let fontsPromise: Promise<Font[]> | undefined;

29-32: Preserve original error (use error cause) and avoid double logging.

Throwing with a cause keeps the original stack and avoids duplicate logs downstream.

-  } catch (error) {
-    console.error(`Error loading font (${fileName}):`, error);
-    throw new Error(`Failed to load font: ${fileName}`);
-  }
+  } catch (error) {
+    throw new Error(`Failed to load font: ${fileName}`, { cause: error });
+  }

Note: Ensure the project targets a Node/TS lib that supports ErrorOptions (Node 16+/TS ES2022+). If not, keep the console.error and rethrow the original error.


4-9: Export the Font type for reuse by callers.

Call sites that prepare fonts for ImageResponse often need this type. Export it for consistency.

-type Font = {
+export type Font = {
   name: string;
   data: ArrayBuffer;
   weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
   style?: 'normal' | 'italic';
 };

11-14: Resolve font path using module-relative lookup
Switching from process.cwd() to import.meta.url ensures the correct asset path across dev, build, and deploy:

+import { fileURLToPath } from 'node:url';

-  const fontPath = path.join(process.cwd(), 'src', 'shared', 'assets', 'fonts', fileName);
+  const fontPath = path.join(
+    path.dirname(fileURLToPath(import.meta.url)),
+    '..',
+    '..',
+    'assets',
+    'fonts',
+    fileName,
+  );

β€’ βœ… Fonts are present: Inter_28pt-Regular.ttf & Inter_28pt-Bold.ttf in src/shared/assets/fonts.
β€’ ⚠️ I didn’t locate any OG image route handlers to confirm their runtime. Please verify that routes invoking loadFont run under Node.js (e.g., export const runtime = 'nodejs'), since fs isn’t available in the Edge runtime.

src/metadata/mentorship.ts (2)

9-14: Prefer relative canonicals to leverage metadataBase and avoid hardcoding the host

Hardcoding https://rs.school here bypasses the metadataBase you set in the root layout, complicating local dev and preview deploys. Using relative paths keeps environments consistent.

-  canonical: `https://rs.school/${ROUTES.MENTORSHIP}`,
+  canonical: `/${ROUTES.MENTORSHIP}`,

And (if keeping a constant for course pages despite the earlier suggestion):

-  canonical: `https://rs.school/${ROUTES.MENTORSHIP}`,
+  canonical: `/${ROUTES.MENTORSHIP}`,

Also applies to: 22-27


3-15: DRY up shared fields between the two objects

Titles, robots, and canonical base repeat. Extract a base object or a small factory to reduce drift.

Example:

const base = {
  title: `Mentorship Β· ${OG_SITE_NAME}`,
  robots: { index: true, follow: true },
};

export const mentorshipMetadata = {
  ...base,
  description: '...',
  keywords: '...',
  canonical: `/${ROUTES.MENTORSHIP}`,
};

Also applies to: 16-27

src/metadata/docs.ts (1)

10-15: Make canonicals relative to honor metadataBase across environments

These absolute URLs hardcode production. Switch to relative paths so Next computes absolutes using metadataBase.

   canonical: 'https://rs.school/docs',
+  // Better as:
+  // canonical: '/docs',
-  const canonical = `https://rs.school/docs/${lang}/${slugPath}`;
+  const canonical = `/docs/${lang}/${slugPath}`;

Also applies to: 23-27

πŸ“œ Review details

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

πŸ’‘ Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 8f47891 and 6a61679.

πŸ“’ Files selected for processing (17)
  • src/app/community/og.png/route.ts (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/docs/[lang]/[...slug]/page.tsx (2 hunks)
  • src/app/docs/[lang]/og.png/route.ts (1 hunks)
  • src/app/mentorship/og.png/route.ts (1 hunks)
  • src/app/merch/og.png/route.ts (1 hunks)
  • src/app/og.png/route.ts (1 hunks)
  • src/metadata/community.ts (1 hunks)
  • src/metadata/courses.ts (1 hunks)
  • src/metadata/docs.ts (1 hunks)
  • src/metadata/home.ts (1 hunks)
  • src/metadata/mentorship.ts (1 hunks)
  • src/metadata/merch.ts (1 hunks)
  • src/shared/constants.tsx (1 hunks)
  • src/shared/og/utils/load-fonts.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/app/mentorship/og.png/route.ts
  • src/metadata/courses.ts
  • src/app/community/og.png/route.ts
  • src/metadata/community.ts
  • src/app/og.png/route.ts
  • src/shared/constants.tsx
  • src/app/courses/og.png/route.ts
  • src/metadata/home.ts
  • src/metadata/merch.ts
  • src/app/courses/[slug]/og.png/route.ts
  • src/app/docs/[lang]/og.png/route.ts
🧰 Additional context used
🧠 Learnings (3)
πŸ““ Common learnings
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: scripts/generate-og-script.ts:0-0
Timestamp: 2025-08-04T19:51:21.922Z
Learning: In PR #894, the scripts/generate-og-script.ts file contains old/legacy code that is not part of the current OG image generation implementation. The current approach uses Next.js route handlers for OG image generation instead of separate scripts.
πŸ“š Learning: 2025-08-04T19:51:21.922Z
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: scripts/generate-og-script.ts:0-0
Timestamp: 2025-08-04T19:51:21.922Z
Learning: In PR #894, the scripts/generate-og-script.ts file contains old/legacy code that is not part of the current OG image generation implementation. The current approach uses Next.js route handlers for OG image generation instead of separate scripts.

Applied to files:

  • src/app/merch/og.png/route.ts
πŸ“š Learning: 2025-06-11T12:39:10.902Z
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: src/app/docs/[lang]/page.tsx:9-15
Timestamp: 2025-06-11T12:39:10.902Z
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.

Applied to files:

  • src/app/courses/[slug]/page.tsx
  • src/app/docs/[lang]/[...slug]/page.tsx
🧬 Code Graph Analysis (5)
src/app/merch/og.png/route.ts (3)
src/app/og.png/route.ts (1)
  • GET (5-13)
src/app/docs/[lang]/og.png/route.ts (1)
  • GET (20-27)
src/shared/og/view/pages-tree/generate-pages-tree.tsx (1)
  • createPageTree (28-64)
src/metadata/mentorship.ts (3)
src/shared/constants.tsx (2)
  • OG_SITE_NAME (180-180)
  • ROUTES (117-136)
src/app/mentorship/[course]/page.tsx (2)
  • generateMetadata (6-10)
  • MentorshipRoute (19-32)
src/app/courses/page.tsx (1)
  • generateMetadata (5-9)
src/app/courses/[slug]/page.tsx (2)
src/views/course/model/store.ts (1)
  • coursePageStore (75-75)
src/shared/helpers/generate-page-metadata.ts (1)
  • generatePageMetadata (15-51)
src/app/docs/[lang]/[...slug]/page.tsx (4)
src/metadata/docs.ts (1)
  • generateDocsMetadata (17-35)
src/shared/helpers/generate-page-metadata.ts (1)
  • generatePageMetadata (15-51)
src/app/docs/constants.ts (1)
  • TITLE_POSTFIX (2-2)
src/app/docs/[lang]/page.tsx (1)
  • generateMetadata (8-10)
src/metadata/docs.ts (2)
src/app/docs/constants.ts (1)
  • TITLE_POSTFIX (2-2)
src/app/docs/[lang]/page.tsx (1)
  • generateMetadata (8-10)
πŸ”‡ Additional comments (5)
src/app/merch/og.png/route.ts (1)

1-1: Centralized OG generator import looks good

Reusing createPageTree keeps OG visuals consistent across routes. No issues here.

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

5-5: Centralized metadata helper import looks good.

Good move towards consistency and DRY metadata generation.

src/metadata/docs.ts (1)

17-35: Good separation of concerns for per-page docs metadata

generateDocsMetadata cleanly centralizes description/keywords/robots and composes a correct per-language canonical. This will scale well with additional fields.

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

17-23: Correct: awaiting params aligns with Next.js v15 App Router API

Per earlier learnings for this codebase, receiving params as a Promise and awaiting it is the right pattern here.


53-61: Nice integration with the shared metadata helper

This composes Open Graph/Twitter fields consistently and centralizes image sizing/siteName via the helper. Once the imagePath and title fallback tweaks are applied, this block is solid.

@github-actions
Copy link

Lighthouse Report:

  • Performance: 64 / 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

♻️ Duplicate comments (3)
src/shared/types/contentful/TypeHomePage.ts (1)

36-49: Make SEO fields optional for backward compatibility (repeat of earlier suggestion)

Older entries may not have these fields populated. Declare them optional and add a short JSDoc for seoKeywords to match conventions.

   /**
    * Field type definition for field 'seoDescription' (SEO Description)
    * @name SEO Description
    * @localized false
    * @summary Enter a description of the page using sentence casing, remaining between 100 and 150 characters. The format should include the page's topic and value proposition (if relevant), followed by a call-to-action
    */
-  seoDescription: EntryFieldTypes.Symbol;
+  /**
+   * Short description shown in search snippets and social previews
+   */
+  seoDescription?: EntryFieldTypes.Symbol;
   /**
    * Field type definition for field 'seoKeywords' (SEO Keywords)
    * @name SEO Keywords
    * @localized false
    */
-  seoKeywords: EntryFieldTypes.Symbol;
+  /**
+   * Comma-separated keywords for SEO
+   */
+  seoKeywords?: EntryFieldTypes.Symbol;

Note: This aligns with how other content types handle SEO fields and prevents runtime nullable checks from diverging from strict types.

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

2-2: Don’t use Node’s path.join for URLs

path.join can introduce backslashes on Windows, producing invalid URLs in meta tags. Use a POSIX-safe join or a template string. Also drop the unused Node import afterwards.

-import path from 'path';

And below (see separate diff) switch to a template string for imagePath.


31-38: Fix OG image URL construction and guard canonical

  • Build the OG image URL with a template string to keep it URL-safe.
  • Only set alternates.canonical when courseUrl is truthy; otherwise Next may emit invalid metadata.
   return generatePageMetadata({
     title,
     description,
-    imagePath: path.join('courses', slug, 'og.png'),
+    imagePath: `courses/${slug}/og.png`,
     keywords,
-    alternates: { canonical: courseUrl },
+    alternates: courseUrl ? { canonical: courseUrl } : undefined,
     robots,
   });
🧹 Nitpick comments (4)
src/widgets/hero/ui/hero.tsx (1)

41-50: Guard width/height; fall back to intrinsic StaticImageData dimensions

If image.width/image.height are not provided, Next/Image will error at build-time. Prefer falling back to the intrinsic dimensions on image.src (when it's a StaticImageData).

Apply within the selected lines:

-            width={image.width}
-            height={image.height}
+            width={typeof image.width === 'number'
+              ? image.width
+              : (typeof image.src === 'object' && 'width' in image.src ? (image.src as any).width : undefined)}
+            height={typeof image.height === 'number'
+              ? image.height
+              : (typeof image.src === 'object' && 'height' in image.src ? (image.src as any).height : undefined)}

Optionally, extract a small resolver to keep JSX clean:

function resolveDims(img: { src: unknown; width?: number; height?: number }) {
  const src = img.src as { width?: number; height?: number } | null;
  return {
    width: typeof img.width === 'number' ? img.width : src?.width,
    height: typeof img.height === 'number' ? img.height : src?.height,
  };
}

Then:

const { width, height } = resolveDims(image);
<Image width={width} height={height} ... />

Please verify that HeroSectionData['image'] actually guarantees numeric width/height. If not, the fallback above prevents runtime failures.

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

25-26: Prefer OG_SITE_NAME constant to avoid repeated literals

Minor consistency tweakβ€”reuse the shared constant.

-  const title = `${courseName} Β· The Rolling Scopes School`;
+  const title = `${courseName} Β· ${OG_SITE_NAME}`;

Outside the selected range, add:

import { OG_SITE_NAME } from '@/shared/constants';
src/entities/course-page/model/store.ts (2)

15-23: Avoid defaulting SEO fields to empty strings; prefer undefined to enable fallbacks

Defaulting to '' can result in an empty <meta name="description">. Let these be undefined so downstream can decide to omit or fallback. Also make the fields.url access fully optional.

-      const {
-        title = '',
-        seoDescription = '',
-        seoKeywords = '',
-        sections: coursePageSections,
-        course,
-      } = preparedData.at(0)?.fields ?? {};
+      const {
+        title = '',
+        seoDescription,
+        seoKeywords,
+        sections: coursePageSections,
+        course,
+      } = preparedData.at(0)?.fields ?? {};
       const courseId = course?.sys?.id;
-      const courseUrl = course?.fields.url || '';
+      const courseUrl = course?.fields?.url || '';

Optionally, if transformPageSections tolerates undefined, keep as is; otherwise default coursePageSections ?? [] before transforming.


30-36: Return shape looks good; consider omitting undefined fields instead of empty strings

If you adopt the change above (no empty-string defaults), this return stays the same but will propagate undefined for absent SEO fields, allowing generatePageMetadata to conditionally include them.

πŸ“œ Review details

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

πŸ’‘ Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 6a61679 and f82e387.

β›” Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
πŸ“’ Files selected for processing (7)
  • src/app/courses/[slug]/og.png/route.ts (1 hunks)
  • src/app/courses/[slug]/page.tsx (2 hunks)
  • src/app/page.tsx (1 hunks)
  • src/entities/course-page/model/store.ts (3 hunks)
  • src/shared/constants.ts (1 hunks)
  • src/shared/types/contentful/TypeHomePage.ts (1 hunks)
  • src/widgets/hero/ui/hero.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/page.tsx
  • src/app/courses/[slug]/og.png/route.ts
🧰 Additional context used
🧠 Learnings (3)
πŸ““ Common learnings
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: scripts/generate-og-script.ts:0-0
Timestamp: 2025-08-04T19:51:21.922Z
Learning: In PR #894, the scripts/generate-og-script.ts file contains old/legacy code that is not part of the current OG image generation implementation. The current approach uses Next.js route handlers for OG image generation instead of separate scripts.
πŸ“š Learning: 2025-08-04T19:51:21.922Z
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: scripts/generate-og-script.ts:0-0
Timestamp: 2025-08-04T19:51:21.922Z
Learning: In PR #894, the scripts/generate-og-script.ts file contains old/legacy code that is not part of the current OG image generation implementation. The current approach uses Next.js route handlers for OG image generation instead of separate scripts.

Applied to files:

  • src/app/courses/[slug]/page.tsx
πŸ“š Learning: 2025-06-11T12:39:10.902Z
Learnt from: LaraNU
PR: rolling-scopes/site#894
File: src/app/docs/[lang]/page.tsx:9-15
Timestamp: 2025-06-11T12:39:10.902Z
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.

Applied to files:

  • src/app/courses/[slug]/page.tsx
🧬 Code Graph Analysis (4)
src/widgets/hero/ui/hero.tsx (7)
src/shared/icons/boosty-icon.tsx (1)
  • Image (5-15)
src/shared/__tests__/setup-tests.tsx (2)
  • default (37-48)
  • props (38-47)
src/shared/icons/jetbrains.tsx (1)
  • Image (5-14)
src/widgets/hero-course/ui/hero-course.test.tsx (1)
  • imageElement (58-64)
src/shared/icons/opencollective-icon.tsx (1)
  • Image (5-15)
src/shared/icons/facebook.tsx (1)
  • figure (5-11)
src/shared/icons/aws.tsx (1)
  • Image (5-9)
src/app/courses/[slug]/page.tsx (2)
src/shared/helpers/generate-page-metadata.ts (1)
  • generatePageMetadata (15-51)
src/app/courses/page.tsx (1)
  • generateMetadata (5-9)
src/shared/types/contentful/TypeHomePage.ts (1)
src/shared/types/contentful/TypeLandingPage.ts (1)
  • TypeLandingPageFields (26-73)
src/entities/course-page/model/store.ts (2)
src/entities/course-page/api/course-page-api.ts (3)
  • CoursePageApi (9-43)
  • queryCoursePage (12-21)
  • queryCoursePageTitle (23-33)
src/views/course.tsx (1)
  • CourseProps (18-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build rs.school
  • GitHub Check: CI
πŸ”‡ Additional comments (2)
src/shared/constants.ts (1)

131-136: OG constants look good and match common defaults

1200x630 is standard for OG. The DYNAMIC = 'force-static' helper is a nice touch for route modules.

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

13-19: No changes needed: consistent params typing across the app

Next.js version is 15.4.5 and all App Router handlers (generateMetadata, page defaults, GET, etc.) use params: Promise<…> followed by await params. Your src/app/courses/[slug]/page.tsx matches the established pattern and is already aligned with the rest of the codebase.

@github-actions
Copy link

Lighthouse Report:

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

View detailed report

@LaraNU LaraNU merged commit 42a7e4c into main Aug 21, 2025
3 checks passed
@LaraNU LaraNU deleted the feat/390-add-open-graph-previews branch August 21, 2025 18:10
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

8 participants