diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e8031ca7f20e688b56f8d6c8fbda081c24875844..40e238594fe5656aab3efddc72875775a32ce78e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -6,6 +6,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { environment } from '../environments/environment'; import { NavigationHistoryService } from './core/services'; import { AppRoutes } from './routes'; +import { SeoSErvice } from './editorialisation/services'; @Component({ selector: 'app-root', @@ -19,6 +20,7 @@ export class AppComponent implements OnInit { private _activatedRoute: ActivatedRoute, private _titleService: Title, private _angulartics2Piwik: Angulartics2Piwik, + private _seoSErvice: SeoSErvice, ) { this._angulartics2Piwik.startTracking(); } @@ -47,8 +49,14 @@ export class AppComponent implements OnInit { return titles; }), ).subscribe((titles) => { - const title = titles.join(' - '); - this._titleService.setTitle(title); + if (titles.length > 1) { + const title = titles.join(' - '); + this._seoSErvice.setRoutingTitle(title); + } + else { + this._seoSErvice.setRoutingTitle(''); + } + }); } diff --git a/src/app/dataset-detail/components/dataset-detail/dataset-detail.component.ts b/src/app/dataset-detail/components/dataset-detail/dataset-detail.component.ts index 80972adc0444f40597db2cd20ce689c58e616349..a37b0c61e3f173e3e22b1fe1a617ba7450f02b12 100644 --- a/src/app/dataset-detail/components/dataset-detail/dataset-detail.component.ts +++ b/src/app/dataset-detail/components/dataset-detail/dataset-detail.component.ts @@ -1,5 +1,4 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Meta } from '@angular/platform-browser'; import { ActivatedRoute, Router, Scroll } from '@angular/router'; import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; @@ -11,6 +10,7 @@ import { AppRoutes } from '../../../routes'; import { IPageHeaderInfo, Metadata, typesMetadata } from '../../../shared/models'; import { isRentertron } from '../../../shared/variables'; import { DatasetDetailService } from '../../services'; +import { SeoSErvice } from '../../../editorialisation/services'; @Component({ @@ -41,7 +41,7 @@ export class DatasetDetailComponent implements OnInit, OnDestroy { private _router: Router, private _scroller: ViewportScroller, private _navigationHistoryService: NavigationHistoryService, - private _meta: Meta, + private _seoService: SeoSErvice, ) { } ngOnInit() { @@ -58,8 +58,6 @@ export class DatasetDetailComponent implements OnInit, OnDestroy { this._route.params.subscribe((params) => { // Set the title and description for the research page - this._meta.updateTag({ name: 'description', content: this.metadata.abstract }); - this.isLoading = true; this.initDatasetInfo(); }); @@ -224,6 +222,8 @@ export class DatasetDetailComponent implements OnInit, OnDestroy { this.isLoading = false; + this._seoService.setDatasetSEO(this.metadata); + // If a tab (info, data...) is not specified in the url then redirect to the data tab if the dataset // as a table or a map or to the info tab in the failing case if (!this._route.snapshot.firstChild) { diff --git a/src/app/dataset-detail/dataset-detail-routing.module.ts b/src/app/dataset-detail/dataset-detail-routing.module.ts index d9db43de1586b463cb7b3a5e293f1b3cd570e4d9..5144f73961d2e807b3268b7dde071bb43ffa4b37 100644 --- a/src/app/dataset-detail/dataset-detail-routing.module.ts +++ b/src/app/dataset-detail/dataset-detail-routing.module.ts @@ -17,44 +17,26 @@ export const routes: Routes = [ { path: AppRoutes.info.uri, component: DatasetInfoComponent, - data: { - title: AppRoutes.info.title, - }, }, { path: AppRoutes.data.uri, component: DatasetTableMapComponent, - data: { - title: AppRoutes.data.title, - }, }, { path: AppRoutes.resources.uri, component: DatasetAPIComponent, - data: { - title: AppRoutes.resources.title, - }, }, { path: AppRoutes.downloads.uri, component: DatasetDownloadsComponent, - data: { - title: AppRoutes.downloads.title, - }, }, { path: AppRoutes.otherResources.uri, component: DatasetDownloadsComponent, - data: { - title: AppRoutes.otherResources.title, - }, }, { path: AppRoutes.dataReuses.uri, component: DatasetReusesComponent, - data: { - title: AppRoutes.dataReuses.title, - }, }, ], }, diff --git a/src/app/editorialisation/components/cms-page/cms-page.component.ts b/src/app/editorialisation/components/cms-page/cms-page.component.ts index 75aadb548578aadcbe88af5962ed6acb2f9bb519..6a7f533c037cd072a6e1ac953ce2de2683776bfa 100644 --- a/src/app/editorialisation/components/cms-page/cms-page.component.ts +++ b/src/app/editorialisation/components/cms-page/cms-page.component.ts @@ -1,12 +1,12 @@ import { Component, OnInit } from '@angular/core'; -import { DomSanitizer, Meta, SafeHtml } from '@angular/platform-browser'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { environment } from '../../../../environments/environment'; import { ToolsService } from '../../../core/services/tools.service'; import { AppRoutes } from '../../../routes'; import { IPageHeaderInfo } from '../../../shared/models'; import { CMSContent } from '../../models'; -import { EditorialisationService } from '../../services'; +import { EditorialisationService, SeoSErvice } from '../../services'; @Component({ selector: 'app-cms-page', @@ -26,7 +26,7 @@ export class CMSPageComponent implements OnInit { private sanitizer: DomSanitizer, private _toolsService: ToolsService, private _router: Router, - private _meta: Meta, + private _seoService: SeoSErvice, ) { } @@ -42,14 +42,14 @@ export class CMSPageComponent implements OnInit { this._editorialisationService.getPage(page).subscribe((page) => { this.page = page; - // Set the title and description for a CMS page - this._meta.updateTag({ name: 'description', content: this.page.content.excerpt }); if (!(this.page instanceof CMSContent)) { this._router.navigate(['/', AppRoutes.page404.uri]); } else { this.safePageContent = this.sanitizer.bypassSecurityTrustHtml(this._toolsService.decorateRichText(this.page.content.html)); this.pageHeaderInfo.title = this.page.content.title; + + this._seoService.setPostSEO(this.page); } }); }); diff --git a/src/app/editorialisation/components/cms-post-detail/cms-post-detail.component.ts b/src/app/editorialisation/components/cms-post-detail/cms-post-detail.component.ts index a5fd8378d8d59532cf4b9cfef94be28bc77ec287..310ac417c79eca8f113de644b1a24b796b9bea81 100644 --- a/src/app/editorialisation/components/cms-post-detail/cms-post-detail.component.ts +++ b/src/app/editorialisation/components/cms-post-detail/cms-post-detail.component.ts @@ -1,13 +1,15 @@ import { DatePipe } from '@angular/common'; import { Component, OnInit } from '@angular/core'; -import { DomSanitizer, Meta, SafeHtml } from '@angular/platform-browser'; +import { DomSanitizer, Meta, SafeHtml, Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; -import { datasetStatistics, notificationMessages } from '../../../../i18n/traductions'; +import { notificationMessages } from '../../../../i18n/traductions'; import { ToolsService } from '../../../core/services/tools.service'; import { ElasticsearchService } from '../../../elasticsearch/services/elasticsearch.service'; +import { SeoSErvice } from '../../services/seo.service' import { AppRoutes } from '../../../routes'; import { IPageHeaderInfo, Metadata, typesMetadata } from '../../../shared/models'; import { CMSContent } from '../../models/cms-content.model'; +import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-cms-post-detail', @@ -36,7 +38,9 @@ export class CMSPostDetailComponent implements OnInit { private _datePipe: DatePipe, private _sanitizer: DomSanitizer, private _elasticSearchService: ElasticsearchService, + private _seoService: SeoSErvice, private _toolsService: ToolsService, + private _titleService: Title, private _meta: Meta, ) { } @@ -47,7 +51,7 @@ export class CMSPostDetailComponent implements OnInit { if (this.post) { // Set the title and description for a CMS post - this._meta.updateTag({ name: 'description', content: this.post.content.excerpt }); + this._seoService.setPostSEO(this.post); this.safePostContent = this._sanitizer.bypassSecurityTrustHtml(this._toolsService.decorateRichText(this.post.content.html)); diff --git a/src/app/editorialisation/components/contribution/contribution.component.ts b/src/app/editorialisation/components/contribution/contribution.component.ts index 99ec5897a524bc0a2ca64ade2f1fc71978115dcb..a14c8b7d9cd9d21fb2b0c6503567b65d94deb570 100644 --- a/src/app/editorialisation/components/contribution/contribution.component.ts +++ b/src/app/editorialisation/components/contribution/contribution.component.ts @@ -3,6 +3,7 @@ import { IPageHeaderInfo } from '../../../shared/models'; import { ActivatedRoute } from '@angular/router'; import { pageTitles, contactTrad } from '../../../../i18n/traductions'; import { AppRoutes } from '../../../routes'; +import { SeoSErvice } from '../../services'; @Component({ selector: 'app-contribution', @@ -19,10 +20,12 @@ export class ContributionComponent implements OnInit { constructor( private _route: ActivatedRoute, + private _seoService: SeoSErvice, ) { } ngOnInit() { + this._seoService.setPostSEO() this._route.data.subscribe((data) => { this.pageHeaderInfo.hasBetaStyle = data.hasBetaStyle; this.pageHeaderInfo.title = pageTitles.contribution; diff --git a/src/app/editorialisation/components/reuse-detail/reuse-detail.component.ts b/src/app/editorialisation/components/reuse-detail/reuse-detail.component.ts index 880a48e6fd25a51f8f427586ac978a6da8ccabab..294601f8c3af8eccdf5d9def735887e73d8587b1 100644 --- a/src/app/editorialisation/components/reuse-detail/reuse-detail.component.ts +++ b/src/app/editorialisation/components/reuse-detail/reuse-detail.component.ts @@ -6,7 +6,7 @@ import { ElasticsearchService } from '../../../elasticsearch/services/elasticsea import { AppRoutes } from '../../../routes'; import { IPageHeaderInfo, Metadata, typesMetadata } from '../../../shared/models'; import { Reuse } from '../../models'; -import { ReusesService } from '../../services'; +import { ReusesService, SeoSErvice } from '../../services'; @Component({ selector: 'app-reuse-detail', @@ -26,6 +26,7 @@ export class ReuseDetailComponent implements OnInit { private _reusesService: ReusesService, private _route: ActivatedRoute, private _elasticSearchService: ElasticsearchService, + private _seoSErvice: SeoSErvice, ) { } ngOnInit() { @@ -40,6 +41,8 @@ export class ReuseDetailComponent implements OnInit { return this._elasticSearchService.getDatasetMetadata(slug); }); + this._seoSErvice.setReuseSEO(this.reuse); + forkJoin(calls).pipe( map((response) => { return response.map((e, i) => { diff --git a/src/app/editorialisation/models/cms-content.model.ts b/src/app/editorialisation/models/cms-content.model.ts index a3e135feecf7bb66d397051c3ba00c2acf962344..59096100277aee29808cd9cfa1a1bdafffb4a62f 100644 --- a/src/app/editorialisation/models/cms-content.model.ts +++ b/src/app/editorialisation/models/cms-content.model.ts @@ -34,6 +34,11 @@ export interface IGhostContentResponse { featured: boolean; tags: IGhostTag[]; highlight?: IHighlight[]; + meta_description: string; + meta_title?: string; + og_title?: string; + og_description?: string; + og_image?: string; } export interface IHighlight { @@ -100,6 +105,11 @@ export class GhostContentResponse { modificationDate: number; highlight?: string[]; titleHighlight?: string; + meta_description?: string; + meta_title?: string; + og_title?: string; + og_description?: string; + og_image?: string; constructor(data: IGhostContentResponse) { this.id = data.id; @@ -107,7 +117,15 @@ export class GhostContentResponse { this.title = (data.title != null) ? data.title : ''; this.html = (data.html != null) ? data.html : ''; this.excerpt = (data.excerpt != null) ? data.excerpt : ''; + + this.meta_title = (data.meta_title != null) ? data.meta_title: this.title; + this.meta_description = (data.meta_description != null) ? data.meta_description : this.excerpt; + this.og_title=(data.og_title != null) ? data.og_title : this.title; + this.og_description=(data.og_description != null) ? data.og_description : this.excerpt; + this.featureImage = data.feature_image; + this.og_image = this.featureImage; + this.publicationDate = (data.published_at != null) ? Date.parse(data.published_at) : 0; this.modificationDate = (data.updated_at != null) ? Date.parse(data.updated_at) : 0; diff --git a/src/app/editorialisation/services/index.ts b/src/app/editorialisation/services/index.ts index 2577fdec76f895098e5e50cee52b32557930b256..d1461e62fcfce8d5a969aa6f0b92e7476a189227 100644 --- a/src/app/editorialisation/services/index.ts +++ b/src/app/editorialisation/services/index.ts @@ -3,6 +3,7 @@ import { OrganizationsService } from './organizations.service'; import { CreditsService } from './credits.service'; import { ChangelogsService } from './changelog.service'; import { ReusesService } from './reuses.service'; +import { SeoSErvice } from './seo.service'; export { EditorialisationService, @@ -10,6 +11,7 @@ export { CreditsService, ChangelogsService, ReusesService, + SeoSErvice, }; // tslint:disable-next-line:variable-name @@ -19,4 +21,5 @@ export const EditorialisationServices = [ CreditsService, ChangelogsService, ReusesService, + SeoSErvice, ]; diff --git a/src/app/editorialisation/services/seo.service.ts b/src/app/editorialisation/services/seo.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b62ff426d09712f4c88e17d16f10233392c850c --- /dev/null +++ b/src/app/editorialisation/services/seo.service.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@angular/core'; +import { Meta, Title } from '@angular/platform-browser'; +import { environment } from '../../../environments/environment'; +import { AppRoutes } from '../../routes'; +import { Metadata } from '../../shared/models'; +import { CMSContent, Reuse } from '../models'; + + +@Injectable() +export class SeoSErvice { + + private _lang: string; + private _defaultImg: string; + private _defaultDesc: string; + private _defaultTitle: string; + + constructor( + private _titleService: Title, + private _metaService: Meta, + ) { + this._lang = window.location.href.includes(environment.angularAppHost.en) ? 'en' : 'fr'; + + this._defaultImg = this._metaService.getTag("property='og:image'").content; + this._defaultDesc = this._metaService.getTag("name='description'").content; + this._defaultTitle = this._titleService.getTitle(); + } + + setPostSEO(post:CMSContent) + { + const imageUrl = (post.content.og_image != null) ? + post.content.og_image : post.content.featureImage; + + const meta = [ + { name: 'description', content: post.content.excerpt }, + { property: 'og:title', content: post.content.og_title }, + { property: 'og:description', content: post.content.og_description }, + { property: 'og:image', content: imageUrl }, + ]; + + this._setMeta(meta); + this._setTitle(post.content.title); + } + + setDatasetSEO(data: Metadata) + { + const imageUrl = (data.image != null) ? + data.image.url : this._defaultImg; + + const meta = [ + { name: 'description', content: data.abstractTroncated }, + { property: 'og:title', content: data.title }, + { property: 'og:description', content: data.abstractTroncated }, + { property: 'og:image', content: imageUrl }, + ]; + + this._setMeta(meta); + this._setTitle(data.title); + } + + setRoutingTitle(title:string) + { + if (title=='') { + title=this._defaultTitle; + } + + const meta = [ + { name: 'description', content: this._defaultDesc }, + { property: 'og:title', content: title }, + { property: 'og:description', content: this._defaultDesc }, + { property: 'og:image', content: this._defaultImg } + ]; + this._setMeta(meta); + this._titleService.setTitle(title); + } + + setReuseSEO (reuse: Reuse) + { + const title:string = reuse.name; + const meta = [ + { name: 'description', content: '' }, + { property: 'og:title', content: title }, + { property: 'og:description', content: '' }, + { property: 'og:image', content: reuse.logo } + ]; + + this._setTitle(title); + this._setMeta(meta); + } + + private _setTitle(title:string) { + this._titleService.setTitle(`${title} - ${AppRoutes.root.title[this._lang]}` ); + } + + private _setMeta(metatags:{name?: string, property?:string, content:string}[]) { + + metatags.forEach((meta) => { + if (meta.content!= null) { + this._metaService.updateTag(meta); + } + }); + } + +} diff --git a/src/index.html b/src/index.html index 81f0aa2ec41c92ca20a320c6936fbb105e331abd..a5771b3ef548b54ddb4b69b7a7c6e7728b83414c 100644 --- a/src/index.html +++ b/src/index.html @@ -3,8 +3,8 @@ <head> <meta charset="utf-8"> - <title>Accueil - data.grandlyon.com</title> - <meta name="description" content="Les données des acteurs du territoire de la Métropole de Lyon"> + <title>data.grandlyon.com : plateforme open data de la métropole de Lyon</title> + <meta name="description" content="Basé entièrement sur des briques logicielles Open Source, ce portail vous invite à découvrir les données des acteurs du territoire de la Métropole de Lyon"> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1">