Skip to content

Commit

Permalink
feat(SEO): handle meta description, keywords & og:image in website pages
Browse files Browse the repository at this point in the history
  • Loading branch information
juamber-rgs committed Jan 14, 2025
1 parent 1bf9ff5 commit 0d88083
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 55 deletions.
3 changes: 3 additions & 0 deletions src/app/shared/models/page.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface Page {
handleSEO(): void;
}
33 changes: 25 additions & 8 deletions src/app/website/pages/certificates/certificates.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import {
inject,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ResolveFn } from '@angular/router';
import { Meta, Title } from '@angular/platform-browser';
import { faGithub, faGooglePlay, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
import { faDownload, faGlobe, faLink } from '@fortawesome/free-solid-svg-icons';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable, Subject, filter, map, pairwise, startWith, takeUntil, zip } from 'rxjs';
import { appRootTitle } from 'src/app/app.component';
import { CertificateGroup } from 'src/app/backoffice/tables/certificate-group/models/certificate-group.model';
import { certificateGroupActions } from 'src/app/backoffice/tables/certificate-group/state/certificate-group.actions';
import { certificateGroupReducer } from 'src/app/backoffice/tables/certificate-group/state/certificate-group.reducer';
import { Certificate } from 'src/app/backoffice/tables/certificate/models/certificate.model';
import { Page } from 'src/app/shared/models/page.model';
import { TranslationProvider } from 'src/app/shared/models/translation-provider.model';
import { ActionStatus, ActionType } from 'src/app/shared/state/common/common-state';
import { addActionId } from 'src/app/shared/state/common/common.actions';
Expand All @@ -31,11 +31,6 @@ import Swiper, { A11y, Autoplay, Navigation, Pagination, Scrollbar, SwiperOption

Swiper.use([Navigation, A11y, Pagination, Scrollbar, Autoplay]);

export const certificatesTitleResolver: ResolveFn<string> = () => {
const translateSrv = inject(TranslateService);
return translateSrv.get('pages.certificates.title').pipe(map((title) => `${appRootTitle} | ${title}`));
};

@Component({
selector: 'app-certificates',
templateUrl: './certificates.component.html',
Expand All @@ -49,9 +44,12 @@ export const certificatesTitleResolver: ResolveFn<string> = () => {
]),
],
})
export class CertificatesComponent extends TranslationProvider implements OnInit, AfterViewChecked, OnDestroy {
export class CertificatesComponent extends TranslationProvider implements OnInit, AfterViewChecked, OnDestroy, Page {
private store = inject(Store);
private ref = inject(ChangeDetectorRef);
private title = inject(Title);
private meta = inject(Meta);
private translateSrv = inject(TranslateService);

destroy$ = new Subject<void>();

Expand Down Expand Up @@ -120,6 +118,7 @@ export class CertificatesComponent extends TranslationProvider implements OnInit
certificateElementAnimationsDone: string[] = [];

ngOnInit() {
this.handleSEO();
zip([this.certificateGroups$.pipe(startWith([]), pairwise()), this.certificateGroupCount$])
.pipe(takeUntil(this.destroy$))
.subscribe(([[previousCertificateGroups, currentCertificateGroups], count]) => {
Expand Down Expand Up @@ -184,6 +183,24 @@ export class CertificatesComponent extends TranslationProvider implements OnInit
this.store.dispatch(certificateGroupActions.unloadAll());
}

handleSEO() {
this.language$.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.title.setTitle(this.translateSrv.instant('pages.certificates.title'));
this.meta.updateTag({
name: 'description',
content: this.translateSrv.instant('pages.certificates.meta.description'),
});
this.meta.updateTag({
name: 'keywords',
content: this.translateSrv.instant('pages.certificates.meta.keywords'),
});
this.meta.updateTag({
name: 'og:image',
content: 'assets/images/meta-image.png',
});
});
}

orderByDate(certificates: Certificate[]): Certificate[] {
if (certificates) {
return [...certificates].sort((a, b) => {
Expand Down
44 changes: 34 additions & 10 deletions src/app/website/pages/home/home.component.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit, inject } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject } from '@angular/core';

import { animate, state, style, transition, trigger } from '@angular/animations';
import { toSignal } from '@angular/core/rxjs-interop';
import { ResolveFn } from '@angular/router';
import { Meta, Title } from '@angular/platform-browser';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { filter, map } from 'rxjs/operators';
import { appRootTitle } from 'src/app/app.component';
import { Subject } from 'rxjs';
import { filter, map, takeUntil } from 'rxjs/operators';
import { skillTypeActions } from 'src/app/backoffice/tables/skill-type/state/skill-type.actions';
import { skillTypeReducer } from 'src/app/backoffice/tables/skill-type/state/skill-type.reducer';
import { Page } from 'src/app/shared/models/page.model';
import { TranslationProvider } from 'src/app/shared/models/translation-provider.model';
import { ActionStatus, ActionType } from 'src/app/shared/state/common/common-state';
import { addActionId } from 'src/app/shared/state/common/common.actions';
import { publicLanguageReducer } from 'src/app/shared/state/languages/public-language.reducer';
import { SocialNetwork } from '../../components/social-networks/models/social-network.model';

export const homeTitleResolver: ResolveFn<string> = () => {
const translateSrv = inject(TranslateService);
return translateSrv.get('pages.home.title').pipe(map((title) => `${appRootTitle} | ${title}`));
};

@Component({
selector: 'app-home',
templateUrl: './home.component.html',
Expand All @@ -45,9 +41,13 @@ export const homeTitleResolver: ResolveFn<string> = () => {
]),
],
})
export class HomeComponent extends TranslationProvider implements OnInit, AfterViewInit {
export class HomeComponent extends TranslationProvider implements OnInit, OnDestroy, AfterViewInit, Page {
private store = inject(Store);
private translateSrv = inject(TranslateService);
private title = inject(Title);
private meta = inject(Meta);

destroy$ = new Subject<void>();

language$ = this.store.select(publicLanguageReducer.getOne);
language = toSignal(this.language$);
Expand Down Expand Up @@ -94,6 +94,7 @@ export class HomeComponent extends TranslationProvider implements OnInit, AfterV
}

ngOnInit() {
this.handleSEO();
this.skillTypes$.subscribe((skillTypes) => {
if (!skillTypes.length) {
this.store.dispatch(
Expand All @@ -107,6 +108,29 @@ export class HomeComponent extends TranslationProvider implements OnInit, AfterV
});
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}

handleSEO() {
this.language$.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.title.setTitle(this.translateSrv.instant('pages.home.title'));
this.meta.updateTag({
name: 'description',
content: this.translateSrv.instant('pages.home.meta.description'),
});
this.meta.updateTag({
name: 'keywords',
content: this.translateSrv.instant('pages.home.meta.keywords'),
});
this.meta.updateTag({
name: 'og:image',
content: 'assets/images/meta-image.png',
});
});
}

get ActionStatus() {
return ActionStatus;
}
Expand Down
43 changes: 34 additions & 9 deletions src/app/website/pages/projects/projects.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,29 @@ import {
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
QueryList,
ViewChildren,
inject,
} from '@angular/core';

import { toSignal } from '@angular/core/rxjs-interop';
import { ResolveFn } from '@angular/router';
import { Meta, Title } from '@angular/platform-browser';
import { faAppStore, faGithub, faGooglePlay, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
import { faBriefcase, faGlobe, faRocket } from '@fortawesome/free-solid-svg-icons';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { map } from 'rxjs/operators';
import { appRootTitle } from 'src/app/app.component';
import { Subject, takeUntil } from 'rxjs';
import { projectActions } from 'src/app/backoffice/tables/project/state/project.actions';
import { projectReducer } from 'src/app/backoffice/tables/project/state/project.reducer';
import { Page } from 'src/app/shared/models/page.model';
import { TranslationProvider } from 'src/app/shared/models/translation-provider.model';
import { ActionStatus } from 'src/app/shared/state/common/common-state';
import { addActionId } from 'src/app/shared/state/common/common.actions';
import { publicLanguageReducer } from 'src/app/shared/state/languages/public-language.reducer';
import { generateTechnologyShield } from 'src/app/shared/utils/shield.utils';

export const projectsTitleResolver: ResolveFn<string> = () => {
const translateSrv = inject(TranslateService);
return translateSrv.get('pages.projects.title').pipe(map((title) => `${appRootTitle} | ${title}`));
};

@Component({
selector: 'app-projects',
templateUrl: './projects.component.html',
Expand All @@ -45,9 +41,14 @@ export const projectsTitleResolver: ResolveFn<string> = () => {
]),
],
})
export class ProjectsComponent extends TranslationProvider implements OnInit, AfterViewChecked {
export class ProjectsComponent extends TranslationProvider implements OnInit, OnDestroy, AfterViewChecked, Page {
private store = inject(Store);
private ref = inject(ChangeDetectorRef);
private title = inject(Title);
private meta = inject(Meta);
private translateSrv = inject(TranslateService);

destroy$ = new Subject<void>();

projects$ = this.store.select(projectReducer.getAll);
projects = toSignal(this.projects$, {
Expand All @@ -62,6 +63,7 @@ export class ProjectsComponent extends TranslationProvider implements OnInit, Af
projectElementAnimationsDone: string[] = [];

ngOnInit(): void {
this.handleSEO();
this.store.dispatch(
projectActions.loadAll(
addActionId({
Expand All @@ -80,6 +82,11 @@ export class ProjectsComponent extends TranslationProvider implements OnInit, Af
});
}

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}

ngAfterViewChecked() {
if (typeof window === 'undefined' || !('IntersectionObserver' in window)) {
return;
Expand All @@ -105,6 +112,24 @@ export class ProjectsComponent extends TranslationProvider implements OnInit, Af
});
}

handleSEO() {
this.language$.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.title.setTitle(this.translateSrv.instant('pages.projects.title'));
this.meta.updateTag({
name: 'description',
content: this.translateSrv.instant('pages.projects.meta.description'),
});
this.meta.updateTag({
name: 'keywords',
content: this.translateSrv.instant('pages.projects.meta.keywords'),
});
this.meta.updateTag({
name: 'og:image',
content: 'assets/images/meta-image.png',
});
});
}

getProjectEnterAnimationState(projectId: string | undefined): 'inViewport' | 'notInViewport' {
if (!projectId) return 'notInViewport';
return this.projectElementStates.get(projectId) || 'notInViewport';
Expand Down
6 changes: 0 additions & 6 deletions src/app/website/website-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LayoutComponent } from './components/layout/layout.component';
import { certificatesTitleResolver } from './pages/certificates/certificates.component';
import { homeTitleResolver } from './pages/home/home.component';
import { projectsTitleResolver } from './pages/projects/projects.component';

const routes: Routes = [
{
Expand All @@ -13,17 +10,14 @@ const routes: Routes = [
{
path: 'home',
loadChildren: () => import('./pages/home/home.module').then((m) => m.HomeModule),
title: homeTitleResolver,
},
{
path: 'projects',
loadChildren: () => import('./pages/projects/projects.module').then((m) => m.ProjectsModule),
title: projectsTitleResolver,
},
{
path: 'certificates',
loadChildren: () => import('./pages/certificates/certificates.module').then((m) => m.CertificatesModule),
title: certificatesTitleResolver,
},

{
Expand Down
6 changes: 5 additions & 1 deletion src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
},
"pages": {
"home": {
"title": "Home"
"title": "Home",
"meta": {
"description": "Juamber - Webanwendungsentwickler",
"keywords": "Juamber, Webanwendungsentwickler, Entwickler, Web, Anwendung, Entwickler"
}
},
"certificates": {
"title": "Zertifikate"
Expand Down
18 changes: 15 additions & 3 deletions src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,25 @@
},
"pages": {
"home": {
"title": "Home"
"title": "Home",
"meta": {
"description": "Personal website of Juan Sáez García, web application developer. Here you can find information about his professional career, projects and certificates.",
"keywords": "Juan Sáez García, JuamBer, Web Application Developer, Full Stack, Frontend, Backend"
}
},
"certificates": {
"title": "Certificates"
"title": "Certificates",
"meta": {
"description": "Certificates of Juan Sáez García, web application developer. Here you can find information about his certificates.",
"keywords": "Juan Sáez García, JuamBer, Web Application Developer, Full Stack, Frontend, Backend, Certificates"
}
},
"projects": {
"title": "Projects"
"title": "Projects",
"meta": {
"description": "Projects of Juan Sáez García, web application developer. Here you can find information about his projects.",
"keywords": "Juan Sáez García, JuamBer, Web Application Developer, Full Stack, Frontend, Backend, Projects"
}
},
"logIn": {
"title": "Log In",
Expand Down
18 changes: 15 additions & 3 deletions src/assets/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,25 @@
},
"pages": {
"home": {
"title": "Resumen"
"title": "Resumen",
"meta": {
"description": "Web personal de Juan Sáez García, desarrollador de aplicaciones web. Aquí podrás encontrar información sobre su trayectoria profesional, proyectos y certificados.",
"keywords": "Juan Sáez García, JuamBer, Desarrollador de aplicaciones web, Full Stack, Frontend, Backend"
}
},
"certificates": {
"title": "Certificados"
"title": "Certificados",
"meta": {
"description": "Certificados de Juan Sáez García, desarrollador de aplicaciones web. Aquí podrás encontrar información sobre sus certificados.",
"keywords": "Juan Sáez García, JuamBer, Desarrollador de aplicaciones web, Full Stack, Frontend, Backend, Certificados"
}
},
"projects": {
"title": "Proyectos"
"title": "Proyectos",
"meta": {
"description": "Proyectos de Juan Sáez García, desarrollador de aplicaciones web. Aquí podrás encontrar información sobre sus proyectos.",
"keywords": "Juan Sáez García, JuamBer, Desarrollador de aplicaciones web, Full Stack, Frontend, Backend, Proyectos"
}
},
"logIn": {
"title": "Inicio de sesión",
Expand Down
18 changes: 15 additions & 3 deletions src/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,25 @@
},
"pages": {
"home": {
"title": "Résumé"
"title": "Résumé",
"meta": {
"description": "Site Web personnel de Juan Sáez García, développeur d'applications Web. Ici, vous trouverez des informations sur son parcours professionnel, ses projets et ses certificats.",
"keywords": "Juan Sáez García, JuamBer, Développeur d'applications Web, Full Stack, Frontend, Backend"
}
},
"certificates": {
"title": "Certificats"
"title": "Certificats",
"meta": {
"description": "Certificats de Juan Sáez García, développeur d'applications Web. Ici, vous trouverez des informations sur ses certificats.",
"keywords": "Juan Sáez García, JuamBer, Développeur d'applications Web, Full Stack, Frontend, Backend, Certificats"
}
},
"projects": {
"title": "Projets"
"title": "Projets",
"meta": {
"description": "Projets de Juan Sáez García, développeur d'applications Web. Ici, vous trouverez des informations sur ses projets.",
"keywords": "Juan Sáez García, JuamBer, Développeur d'applications Web, Full Stack, Frontend, Backend, Projets"
}
},
"logIn": {
"title": "Connexion",
Expand Down
Loading

0 comments on commit 0d88083

Please sign in to comment.