Skip to content

Commit fa9d24e

Browse files
committed
feat: strengthen SEO with html lang, richer JSON-LD and OG tags
1 parent bf62290 commit fa9d24e

5 files changed

Lines changed: 78 additions & 36 deletions

File tree

src/app/core/services/seo.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,32 @@ export class SeoService {
3636
const ogAlternate = current === 'ru' ? 'en_US' : 'ru_RU';
3737
const ogImage = `${PROFILE.siteUrl}/og-image.png`;
3838

39+
// Reflect the build locale on <html lang> — search engines (especially
40+
// Yandex) weight it heavily for language targeting; it ships baked into
41+
// each prerendered locale instead of the static "en" from index.html.
42+
this.#doc.documentElement.lang = current;
43+
3944
this.#title.setTitle(copy.title);
4045
this.#meta.updateTag({ name: 'description', content: copy.description });
4146
this.#meta.updateTag({ name: 'author', content: PROFILE.name });
4247

4348
this.#meta.updateTag({ property: 'og:type', content: 'website' });
49+
this.#meta.updateTag({ property: 'og:site_name', content: PROFILE.name });
4450
this.#meta.updateTag({ property: 'og:title', content: copy.title });
4551
this.#meta.updateTag({ property: 'og:description', content: copy.description });
4652
this.#meta.updateTag({ property: 'og:url', content: this.#locale.localeUrl(current) });
4753
this.#meta.updateTag({ property: 'og:image', content: ogImage });
54+
this.#meta.updateTag({ property: 'og:image:width', content: '1200' });
55+
this.#meta.updateTag({ property: 'og:image:height', content: '630' });
56+
this.#meta.updateTag({ property: 'og:image:alt', content: copy.title });
4857
this.#meta.updateTag({ property: 'og:locale', content: ogLocale });
4958
this.#meta.updateTag({ property: 'og:locale:alternate', content: ogAlternate });
5059

5160
this.#meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
5261
this.#meta.updateTag({ name: 'twitter:title', content: copy.title });
5362
this.#meta.updateTag({ name: 'twitter:description', content: copy.description });
5463
this.#meta.updateTag({ name: 'twitter:image', content: ogImage });
64+
this.#meta.updateTag({ name: 'twitter:image:alt', content: copy.title });
5565

5666
this.#setLink('canonical', null, this.#locale.localeUrl(current));
5767
this.#setLink('alternate', 'en', this.#locale.localeUrl('en'));

src/app/core/services/structured-data.service.ts

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@ import { PROFILE } from '@shared/data/portfolio.data';
66

77
const SCRIPT_ID = 'ld-json';
88

9+
const KNOWS_ABOUT = [
10+
'Angular',
11+
'TypeScript',
12+
'RxJS',
13+
'NgRx',
14+
'Angular Signals',
15+
'Nx monorepo',
16+
'Micro-frontends',
17+
'Server-Side Rendering',
18+
'Smart TV',
19+
'Ionic',
20+
'Tauri',
21+
'Rust',
22+
'GitLab CI/CD',
23+
'Docker',
24+
'Unit testing',
25+
'AI-assisted development',
26+
] as const;
27+
28+
const OCCUPATION_SKILLS = 'Angular, TypeScript, RxJS, NgRx, Signals, Nx, SSR, GitLab CI/CD';
29+
930
@Injectable({ providedIn: 'root' })
1031
export class StructuredDataService {
1132
readonly #doc = inject(DOCUMENT);
@@ -14,52 +35,47 @@ export class StructuredDataService {
1435
init(): void {
1536
const lang = this.#locale.current;
1637
const url = this.#locale.localeUrl(lang);
17-
const jobTitle = $localize`:@@ld.jobTitle:Tech Lead / Senior Angular Developer`;
1838

19-
const graph = {
39+
this.#inject({
2040
'@context': 'https://schema.org',
2141
'@graph': [
22-
{
23-
'@type': 'Person',
24-
name: PROFILE.name,
25-
url,
26-
jobTitle,
27-
image: `${PROFILE.siteUrl}/og-image.png`,
28-
email: `mailto:${PROFILE.email}`,
29-
sameAs: [PROFILE.github, PROFILE.linkedin, PROFILE.telegram],
30-
knowsAbout: [
31-
'Angular',
32-
'TypeScript',
33-
'RxJS',
34-
'NgRx',
35-
'Angular Signals',
36-
'Nx monorepo',
37-
'Micro-frontends',
38-
'Server-Side Rendering',
39-
'Smart TV',
40-
'Ionic',
41-
'Tauri',
42-
'Rust',
43-
'GitLab CI/CD',
44-
'Docker',
45-
'Unit testing',
46-
'AI-assisted development',
47-
],
48-
knowsLanguage: ['Russian', 'English'],
49-
address: { '@type': 'PostalAddress', addressLocality: 'Taganrog', addressCountry: 'RU' },
50-
},
51-
{ '@type': 'WebSite', name: PROFILE.name, url, inLanguage: lang },
42+
this.#person(url),
43+
{ '@type': 'WebSite', '@id': `${url}#website`, name: PROFILE.name, url, inLanguage: lang },
5244
{
5345
'@type': 'ProfilePage',
54-
name: PROFILE.name,
5546
url,
5647
inLanguage: lang,
57-
about: { '@type': 'Person', name: PROFILE.name },
48+
isPartOf: { '@id': `${url}#website` },
49+
about: { '@id': `${url}#person` },
50+
mainEntity: { '@id': `${url}#person` },
5851
},
5952
],
60-
};
53+
});
54+
}
6155

62-
this.#inject(graph);
56+
#person(url: string): Record<string, unknown> {
57+
const jobTitle = $localize`:@@ld.jobTitle:Tech Lead / Senior Angular Developer`;
58+
const description = $localize`:@@ld.description:Tech Lead and Senior Angular Developer with 14+ years of experience, shipping Angular on web (SSR), Smart TV, mobile (Ionic) and desktop (Tauri/Rust).`;
59+
60+
return {
61+
'@type': 'Person',
62+
'@id': `${url}#person`,
63+
name: PROFILE.name,
64+
// Both spellings on every locale so Google and Yandex resolve the
65+
// Latin and Cyrillic name to one entity.
66+
alternateName: ['Alexey Popov', 'Алексей Попов'],
67+
url,
68+
jobTitle,
69+
description,
70+
image: `${PROFILE.siteUrl}/og-image.png`,
71+
email: `mailto:${PROFILE.email}`,
72+
nationality: { '@type': 'Country', name: 'Russia' },
73+
sameAs: [PROFILE.github, PROFILE.linkedin, PROFILE.telegram],
74+
hasOccupation: { '@type': 'Occupation', name: jobTitle, skills: OCCUPATION_SKILLS },
75+
knowsAbout: KNOWS_ABOUT,
76+
knowsLanguage: ['Russian', 'English'],
77+
address: { '@type': 'PostalAddress', addressLocality: 'Taganrog', addressCountry: 'RU' },
78+
};
6379
}
6480

6581
#inject(data: unknown): void {

src/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
/>
1919
<link href="favicon.svg" rel="icon" type="image/svg+xml" />
2020
<!-- JSON-LD structured data is injected per-locale by StructuredDataService (Person + WebSite + ProfilePage). -->
21+
<!-- Search-engine ownership verification — paste the codes from the consoles, then redeploy:
22+
Google Search Console (https://search.google.com/search-console) and
23+
Yandex.Webmaster (https://webmaster.yandex.ru). Submit /sitemap.xml in both afterwards. -->
24+
<!-- <meta content="GOOGLE_VERIFICATION_CODE" name="google-site-verification" /> -->
25+
<!-- <meta content="YANDEX_VERIFICATION_CODE" name="yandex-verification" /> -->
2126
</head>
2227
<body>
2328
<app-root></app-root>

src/locale/messages.ru.xlf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
<source>Tech Lead / Senior Angular Developer</source>
1919
<target>Tech Lead / Senior Angular Developer</target>
2020
</trans-unit>
21+
<trans-unit id="ld.description" datatype="html">
22+
<source>Tech Lead and Senior Angular Developer with 14+ years of experience, shipping Angular on web (SSR), Smart TV, mobile (Ionic) and desktop (Tauri/Rust).</source>
23+
<target>Tech Lead и Senior Angular-разработчик с опытом 14+ лет — Angular на web (SSR), Smart TV, mobile (Ionic) и desktop (Tauri/Rust).</target>
24+
</trans-unit>
2125
<trans-unit id="nav.about" datatype="html">
2226
<source>About</source>
2327
<target>Обо мне</target>

src/locale/messages.xlf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838
<context context-type="linenumber">17</context>
3939
</context-group>
4040
</trans-unit>
41+
<trans-unit id="ld.description" datatype="html">
42+
<source>Tech Lead and Senior Angular Developer with 14+ years of experience, shipping Angular on web (SSR), Smart TV, mobile (Ionic) and desktop (Tauri/Rust).</source>
43+
<context-group purpose="location">
44+
<context context-type="sourcefile">src/app/core/services/structured-data.service.ts</context>
45+
<context context-type="linenumber">18</context>
46+
</context-group>
47+
</trans-unit>
4148
<trans-unit id="nav.about" datatype="html">
4249
<source>About</source>
4350
<context-group purpose="location">

0 commit comments

Comments
 (0)