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.