feat: add Open Graph image generation and SEO metadata

- Introduced `PageSeo` component for managing page metadata for social sharing.
- Created Open Graph image templates using SVG and static fonts for better visual representation.
- Implemented service alert management with functions to check active alerts and format route numbers.
- Added utility functions for generating and watching alert validity.
- Developed a script to generate Open Graph images dynamically based on page content.
- Included tests for service alert functionalities to ensure correctness and reliability.
This commit is contained in:
2026-10-05 13:02:06 +03:00
parent 6460846134
commit 142ec97959
35 changed files with 8226 additions and 4690 deletions

114
lib/social.mjs Normal file
View File

@@ -0,0 +1,114 @@
import { legalCopy } from '../modules/legal/content.ts';
export const siteUrl = 'https://ridebus.by';
export const socialLocales = ['en', 'ru'];
export const ogImageSize = { width: 1200, height: 630 };
// Shared by the page metadata and the build-time image generator.
export const socialPages = {
home: {
path: '/',
illustration: 'schedule',
en: {
title: 'RideBus',
description:
'RideBus — An app that helps you conveniently view public transportation schedules',
label: 'Public transport, at hand',
heading: ['Public transport', 'made simple'],
caption: 'Schedules, stops and routes. All in one small app.',
tags: ['Schedules', 'Routes', 'Favourites'],
},
ru: {
title: 'RideBus',
description:
'RideBus — Приложение, которое поможет вам удобно смотреть расписание общественного транспорта',
label: 'Транспорт под рукой',
heading: ['Расписание', 'транспорта'],
caption: 'Расписания, остановки и маршруты. В одном приложении.',
tags: ['Расписания', 'Маршруты', 'Избранное'],
},
},
contribute: {
path: '/contribute',
illustration: 'community',
en: {
title: 'RideBus - Contribute',
description:
'Help RideBus grow: add your city, share the app or leave a review.',
label: 'Made with love',
heading: ['Help RideBus', 'grow with you'],
caption: 'Add your city, share the app or leave a review.',
tags: ['Open source', 'Community'],
},
ru: {
title: 'RideBus - Вклад',
description:
'Помогите развитию RideBus: добавьте свой город, расскажите о приложении или оставьте отзыв.',
label: 'Сделано с любовью',
heading: ['Помогите RideBus', 'стать лучше'],
caption:
'Добавьте свой город, расскажите о приложении или оставьте отзыв.',
tags: ['Открытый код', 'Сообщество'],
},
},
privacy: {
path: '/privacy',
illustration: 'privacy',
en: {
title: `${legalCopy.en.documents.privacy.title} — RideBus`,
description: legalCopy.en.documents.privacy.description,
label: legalCopy.en.label,
heading: ['Privacy', 'policy'],
caption: legalCopy.en.documents.privacy.description,
tags: ['Data', 'Permissions'],
},
ru: {
title: `${legalCopy.ru.documents.privacy.title} — RideBus`,
description: legalCopy.ru.documents.privacy.description,
label: legalCopy.ru.label,
heading: ['Политика', 'конфиденциальности'],
caption: legalCopy.ru.documents.privacy.description,
tags: ['Данные', 'Разрешения'],
},
},
terms: {
path: '/terms',
illustration: 'terms',
en: {
title: `${legalCopy.en.documents.terms.title} — RideBus`,
description: legalCopy.en.documents.terms.description,
label: legalCopy.en.label,
heading: ['Terms', 'of use'],
caption: legalCopy.en.documents.terms.description,
tags: ['The service', 'Guidelines'],
},
ru: {
title: `${legalCopy.ru.documents.terms.title} — RideBus`,
description: legalCopy.ru.documents.terms.description,
label: legalCopy.ru.label,
heading: ['Условия', 'использования'],
caption: legalCopy.ru.documents.terms.description,
tags: ['О сервисе', 'Правила'],
},
},
};
/** @param {keyof typeof socialPages} page @param {string} locale */
export function getSocialMetadata(page, locale) {
const language = locale === 'ru' ? 'ru' : 'en';
const definition = socialPages[page];
const localizedPath =
language === 'ru'
? definition.path === '/'
? '/ru'
: `/ru${definition.path}`
: definition.path;
return {
...definition[language],
url: `${siteUrl}${localizedPath}`,
image: `${siteUrl}/images/og/${page}-${language}.png`,
locale: language === 'ru' ? 'ru_RU' : 'en_US',
alternateLocale: language === 'ru' ? 'en_US' : 'ru_RU',
};
}