PageSourceSearch

https://english-step-by-step.com/js/seo.js

js english-step-by-step.com collected 2026-10-02 00:49:07 UTC 11,324 bytes, 274 lines download raw bytes

1import { htmlLanguage, localizedPublicPath, lessonPath, sectionPath } from './routes.js';
2
3const SITE_NAME = 'Learn English Easily';
4const SITE_URL = 'https://english-step-by-step.com';
5
6const PAGE_COPY = {
7  home: {
8    title: {
9      ru: 'Learn English Easily — учите английский шаг за шагом',
10      ua: 'Learn English Easily — вивчайте англійську крок за кроком',
11    },
12    description: {
13      ru: 'Учите английский бесплатно: понятная грамматика, новые слова, упражнения и разговорная практика. Уроки для повседневного общения и работы.',
14      ua: 'Вивчайте англійську безкоштовно: зрозуміла граматика, нові слова, вправи та розмовна практика. Уроки для повсякденного спілкування й роботи.',
15    },
16  },
17  grammar: {
18    title: {
19      ru: 'Грамматика английского языка — уроки и практика | Learn English Easily',
20      ua: 'Граматика англійської мови — уроки та практика | Learn English Easily',
21    },
22    description: {
23      ru: 'Пошаговые бесплатные уроки английской грамматики: короткая теория, примеры и упражнения с проверкой ответов.',
24      ua: 'Покрокові безкоштовні уроки англійської граматики: коротка теорія, приклади та вправи з перевіркою відповідей.',
25    },
26  },
27  vocabulary: {
28    title: {
29      ru: 'Лексика английского языка — слова и упражнения | Learn English Easily',
30      ua: 'Лексика англійської мови — слова та вправи | Learn English Easily',
31    },
32    description: {
33      ru: 'Бесплатные уроки английской лексики со словами, примерами употребления и практическими заданиями.',
34      ua: 'Безкоштовні уроки англійської лексики зі словами, прикладами вживання та практичними завданнями.',
35    },
36  },
37  professions: {
38    title: {
39      ru: 'Английский для работы и профессий | Learn English Easily',
40      ua: 'Англійська для роботи та професій | Learn English Easily',
41    },
42    description: {
43      ru: 'Профессиональный английский для моряков, IT, медицины, сервиса, логистики и других рабочих ситуаций.',
44      ua: 'Професійна англійська для моряків, IT, медицини, сервісу, логістики та інших робочих ситуацій.',
45    },
46  },
47  trainer: {
48    title: {
49      ru: 'Тренажёр английских слов и личный словарь | Learn English Easily',
50      ua: 'Тренажер англійських слів та особистий словник | Learn English Easily',
51    },
52    description: {
53      ru: 'Создавайте личный словарь английских слов и тренируйтесь с карточками, тестами и упражнениями.',
54      ua: 'Створюйте особистий словник англійських слів і тренуйтеся з картками, тестами та вправами.',
55    },
56  },
57  placement: {
58    title: {
59      ru: 'Тест уровня английского языка онлайн | Learn English Easily',
60      ua: 'Тест рівня англійської мови онлайн | Learn English Easily',
61    },
62    description: {
63      ru: 'Пройдите бесплатный тест по грамматике и лексике и получите рекомендацию, с какого урока начать обучение.',
64      ua: 'Пройдіть безкоштовний тест із граматики та лексики й отримайте рекомендацію, з якого уроку почати навчання.',
65    },
66  },
67};
68
69export function applyPageSeo(pageKey, language) {
70  const page = PAGE_COPY[pageKey];
71  if (!page) return;
72
73  const htmlLang = htmlLanguage(language);
74  const title = page.title[language] || page.title.ru;
75  const description = page.description[language] || page.description.ru;
76  const path = localizedPublicPath(window.location.pathname, language, window.location.search);
77  const canonicalUrl = absoluteUrl(path);
78
79  document.documentElement.lang = htmlLang;
80  document.title = title;
81  setMeta('description', description);
82  setMeta('og:title', title, 'property');
83  setMeta('og:description', description, 'property');
84  setMeta('og:type', 'website', 'property');
85  setMeta('og:url', canonicalUrl, 'property');
86  setMeta('og:image', `${SITE_URL}/og.png`, 'property');
87  setMeta('twitter:card', 'summary_large_image');
88  setMeta('twitter:title', title);
89  setMeta('twitter:description', description);
90  setMeta('twitter:image', `${SITE_URL}/og.png`);
91  setCanonical(canonicalUrl);
92  setLanguageAlternates(window.location.pathname, window.location.search);
93  setStructuredData(pageKey === 'home'
94    ? websiteStructuredData(language)
95    : pageStructuredData(title, description, canonicalUrl, language));
96}
97
98export function applyLessonSeo(lesson, category, language) {
99  if (!lesson?.title) return;
100
101  const path = lessonPath(lesson.id, language);
102  const canonicalUrl = absoluteUrl(path);
103  const description = lessonDescription(lesson, language);
104  const title = `${lesson.title} | ${SITE_NAME}`;
105
106  document.documentElement.lang = htmlLanguage(language);
107  document.title = title;
108  setMeta('description', description);
109  setMeta('og:title', title, 'property');
110  setMeta('og:description', description, 'property');
111  setMeta('og:type', 'article', 'property');
112  setMeta('og:url', canonicalUrl, 'property');
113  setMeta('og:image', `${SITE_URL}/og.png`, 'property');
114  setMeta('twitter:card', 'summary_large_image');
115  setMeta('twitter:title', title);
116  setMeta('twitter:description', description);
117  setMeta('twitter:image', `${SITE_URL}/og.png`);
118  setCanonical(canonicalUrl);
119  setAlternate('ru', absoluteUrl(lessonPath(lesson.id, 'ru')));
120  setAlternate('uk', absoluteUrl(lessonPath(lesson.id, 'ua')));
121  setAlternate('x-default', absoluteUrl(lessonPath(lesson.id, 'ru')));
122  setStructuredData({
123    '@context': 'https://schema.org',
124    '@type': 'LearningResource',
125    name: lesson.title,
126    description,
127    url: canonicalUrl,
128    inLanguage: htmlLanguage(language),
129    educationalLevel: 'beginner to intermediate',
130    isPartOf: {
131      '@type': 'Course',
132      name: category === 'professions'
133        ? (language === 'ua' ? 'Англійська для професій' : 'Английский для профессий')
134        : (language === 'ua' ? 'Курс англійської мови' : 'Курс английского языка'),
135      url: absoluteUrl(sectionPath(category, language)),
136      provider: organizationReference(),
137    },
138    author: personReference(),
139  });
140}
141
142function lessonDescription(lesson, language) {
143  const firstSection = lesson.theory?.sections?.[0];
144  const candidates = [
145    ...(firstSection?.blocks || []),
146    ...(firstSection?.bullets || []),
147  ];
148  const text = candidates
149    .map((item) => typeof item === 'string' ? item : item?.text)
150    .find(Boolean);
151
152  if (text) return truncate(cleanText(text), 158);
153  return language === 'ua'
154    ? `Безкоштовний урок англійської: ${lesson.title}. Теорія, приклади та практичні вправи.`
155    : `Бесплатный урок английского: ${lesson.title}. Теория, примеры и практические упражнения.`;
156}
157
158function setLanguageAlternates(pathname, search) {
159  setAlternate('ru', absoluteUrl(localizedPublicPath(pathname, 'ru', search)));
160  setAlternate('uk', absoluteUrl(localizedPublicPath(pathname, 'ua', search)));
161  setAlternate('x-default', absoluteUrl(localizedPublicPath(pathname, 'ru', search)));
162}
163
164function setMeta(name, content, attribute = 'name') {
165  let node = document.head.querySelector(`meta[${attribute}="${name}"]`);
166  if (!node) {
167    node = document.createElement('meta');
168    node.setAttribute(attribute, name);
169    document.head.appendChild(node);
170  }
171  node.content = content;
172}
173
174function setCanonical(href) {
175  let node = document.head.querySelector('link[rel="canonical"]');
176  if (!node) {
177    node = document.createElement('link');
178    node.rel = 'canonical';
179    document.head.appendChild(node);
180  }
181  node.href = href;
182}
183
184function setAlternate(hreflang, href) {
185  let node = document.head.querySelector(`link[rel="alternate"][hreflang="${hreflang}"]`);
186  if (!node) {
187    node = document.createElement('link');
188    node.rel = 'alternate';
189    node.hreflang = hreflang;
190    document.head.appendChild(node);
191  }
192  node.href = href;
193}
194
195function setStructuredData(value) {
196  let node = document.getElementById('site-structured-data');
197  if (!node) {
198    node = document.createElement('script');
199    node.id = 'site-structured-data';
200    node.type = 'application/ld+json';
201    document.head.appendChild(node);
202  }
203  node.textContent = JSON.stringify(value);
204}
205
206function websiteStructuredData(language) {
207  return {
208    '@context': 'https://schema.org',
209    '@graph': [
210      {
211        '@type': 'WebSite',
212        '@id': `${SITE_URL}/#website`,
213        url: `${SITE_URL}/`,
214        name: SITE_NAME,
215        inLanguage: [htmlLanguage(language), language === 'ua' ? 'ru' : 'uk'],
216        publisher: organizationReference(),
217      },
218      {
219        ...organizationReference(),
220        name: SITE_NAME,
221        url: `${SITE_URL}/`,
222        logo: `${SITE_URL}/favicon.svg`,
223        founder: personReference(),
224        contactPoint: {
225          '@type': 'ContactPoint',
226          email: '[email protected]',
227          contactType: 'customer support',
228        },
229      },
230    ],
231  };
232}
233
234function pageStructuredData(title, description, url, language) {
235  return {
236    '@context': 'https://schema.org',
237    '@type': 'WebPage',
238    name: title,
239    description,
240    url,
241    inLanguage: htmlLanguage(language),
242    isPartOf: { '@id': `${SITE_URL}/#website` },
243    author: personReference(),
244  };
245}
246
247function organizationReference() {
248  return {
249    '@type': 'Organization',
250    '@id': `${SITE_URL}/#organization`,
251  };
252}
253
254function personReference() {
255  return {
256    '@type': 'Person',
257    name: 'Oleksii Klein',
258    url: `${SITE_URL}/about`,
259    sameAs: ['https://www.linkedin.com/in/oleksii-klein-84b3741a7/'],
260  };
261}
262
263function absoluteUrl(path) {
264  return new URL(path, SITE_URL).href;
265}
266
267function cleanText(value) {
268  return String(value || '').replace(/\s+/g, ' ').trim();
269}
270
271function truncate(value, maxLength) {
272  if (value.length <= maxLength) return value;
273  return `${value.slice(0, maxLength - 1).trimEnd()}…`;
274}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.