Vandaag stel ik een service samen die mijn SEO-tags, metatags, paginatitels voor zowel Angular SSR als SPA afhandelt. (Te veel acroniemen! Server-side rendering en single-page applicatie - dat wil zeggen, client-side rendering). Dit wordt gedaan voor op inhoud gebaseerde websites , die al dan niet statisch kan zijn (zoals in een JAM-stack).
Dit is het gewenste resultaat:
De vereiste HTML-tags
De volgende tags moeten voor elke pagina worden weergegeven.
<title>Page title - site title</title>
<!-- open graph -->
<meta property="og:site_name" content="Sekrab Garage">
<meta property="og.type" content="website">
<meta property="og:url" content="pageUrl"/>
<meta name="description" property="og:description" content="description is optional">
<meta name="title" property="og:title" content="Page title">
<meta name="image" property="og:image" content="imageurl">
<!-- twitter related -->
<meta property="twitter:site" content="@sekrabbin">
<meta property="twitter:card" content="summary_large_image"/>
<meta preoprty="twitter:creator" content="@sekrabbin">
<meta property="twitter:image" content="imageurl">
<meta property="twitter:title" content="title">
<meta property="twitter:description" content="description">
<!-- general and for compatibility purposes -->
<meta name="author" content="Ayyash">
<!-- cononical, if you have multiple languages, point to default -->
<link rel="canonical" href="https://elmota.com"/>
<!-- alternate links, languages -->
<link rel="alternate" hreflang="ar-jo" href="ar link">
<meta property="og:locale" content="en_GB" />
We zullen een service maken, geleverd in de root, geïnjecteerd in de root-component. Dan hebben we een manier nodig om tags voor verschillende routes bij te werken. Dus uiteindelijk hebben we een "Tags toevoegen . nodig " en "Tags bijwerken " openbare methoden. Met behulp van de twee services van Angular:Meta en Title.
@Injectable({
providedIn: 'root'
})
export class SeoService {
// inject title and meta from @angular/platform-browser
constructor(
private title: Title,
private meta: Meta
) {
// in constructor, need to add fixed tags only
}
AddTags() {
// TODO: implement
}
UpdateTags() {
// TODO: implement
}
}
We hebben ook het DOCUMENT-injectietoken nodig om de link toe te voegen. De service ziet er nu zo uit
@Injectable({
providedIn: 'root',
})
export class SeoService {
constructor(
private title: Title,
private meta: Meta,
@Inject(DOCUMENT) private doc: Document
) {}
AddTags() {
const tags = [
{ property: 'og:site_name', content: 'Sekrab Garage' },
{ property: 'og.type', content: 'website' },
{ property: 'og:url', content: 'pageUrl' },
{ property: 'twitter:site', content: '@sekrabbin' },
{ property: 'twitter:card', content: 'summary_large_image' },
{ property: 'twitter:creator', content: '@sekrabbin' },
{ property: 'twitter:image', content: 'imageurl' },
{ property: 'twitter:title', content: '[title]' },
{ property: 'twitter:description', content: '[description]' },
{ property: 'og:locale', content: 'en_GB' },
{
name: 'description',
property: 'og:description',
content: '[description]',
},
{ name: 'title', property: 'og:title', content: '[title]' },
{ name: 'image', property: 'og:image', content: 'imageurl' },
{ name: 'author', content: 'Ayyash' },
];
// add tags
this.meta.addTags(tags);
// add title
this.title.setTitle('[Title] - Sekrab Garage');
// add canonical and alternate links
this.createCanonicalLink();
this.createAlternateLink();
}
private createAlternateLink() {
// append alternate link to body, TODO: url and hreflang
const _link = this.doc.createElement('link');
_link.setAttribute('rel', 'alternate');
_link.setAttribute('hreflang', 'en');
_link.setAttribute('href', '[url]');
this.doc.head.appendChild(_link);
}
private createCanonicalLink() {
// append canonical to body, TODO: url
const _canonicalLink = this.doc.createElement('link');
_canonicalLink.setAttribute('rel', 'canonical');
_canonicalLink.setAttribute('href', '[url]');
this.doc.head.appendChild(_canonicalLink);
}
UpdateTags() {
// TOOD: find out what we need to update
}
}
Niet alle metatags hoeven te worden bijgewerkt, dus degenen die niet worden bijgewerkt, zullen we in de serviceconstructor injecteren. Maar voordat ik dat doe, wil ik de tags buiten mijn service plaatsen , zullen later nadenken over waar ze moeten worden geplaatst. Voor nu wil ik twee arrays maken, één voor fixedTags:
// outside service class
const tags = [
{ property: "og:url", content: "pageUrl" },
{ property: "twitter:image", content: "imageurl" },
{ property: "twitter:title", content: "[title]" },
{ property: "twitter:description", content: "[description]" },
{ name: "description", property: "og:description", content: "[description]" },
{ name: "title", property: "og:title", content: "[title]" },
{ name: "image", property: "og:image", content: "imageurl" }
]
const fixedTags = [
{ property: "og:site_name", content: "Sekrab Garage", dataAttr:'ayyash' },
{ property: "og.type", content: "website" },
{ property: "twitter:site", content: "@sekrabbin" },
{ property: "twitter:card", content: "summary_large_image" },
{ property: "twitter:creator", content: "@sekrabbin" },
{ property: "og:locale", content: "en_GB" },
{ name: "author", content: "Ayyash" }
]
Het andere uiteinde
De simplistische manier om SEO te implementeren, gaat als volgt:in elke route, na het ophalen van details van de server, update titel, beschrijving, afbeelding... etc.
@Component({
templateUrl: './view.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectViewComponent implements OnInit {
project$: Observable<any>;
constructor(private route: ActivatedRoute,
private projectService: ProjectService,
private seoService: SeoService) {
}
ngOnInit(): void {
this.project$ = this.route.paramMap.pipe(
switchMap(params => {
// get project from service by params
return this.projectService.GetProject(params.get('id'));
}),
tap(project => {
// I want to get to this
this.seoService.UpdateTags({
// some pages don't have it from server
title: project.title,
// optional
description: project.description,
// out of context in some pages
url: this.route.snapshot.url,
// may not be known
image: project.image
});
})
)
}
}
Het doorgeven van parameters is voor mij niet voldoende:sommige pagina's hebben geen afbeelding, zoals een lijstpagina, andere hebben misschien een afbeelding of een titel die niet door de server wordt gevoed. Sommige pagina's kunnen pagineringsinformatie bevatten. De url is een stukje eigen werk, aangezien niet alle componenten van route afhankelijk zijn. Wat ik wil, is een centrale plek om voor alle bits te zorgen , zoiets als dit
this.seoService.setProject(project)
ngOnInit(): void {
this.project$ = this.route.paramMap.pipe(
switchMap(params => {
// get project from service by params
return this.projectService.GetProject(params.get('id'));
}),
// map or tap
tap(project => {
// do magic away from here
if (project) {
this.seoService.setProject(project);
}
})
);
}
De magie zit in de SEO-service:
setProject(project: IProject) {
// set title
const title = `${project.title} - Sekrab Garage`;
this.title.setTitle(title);
this.meta.updateTag({ property: 'og:title', content: title });
this.meta.updateTag({ property: 'twitter:title', content: title});
// set url, from doc injection token (next week we'll cover url in details)
this.meta.updateTag({ property: 'og:url', content: this.doc.URL });
// set description
this.meta.updateTag({ name: 'description', property: 'og:description', content: project.description });
// set image
this.meta.updateTag({ name: 'image', property: 'og:image', content: project.image });
this.meta.updateTag({ property: "twitter:image", content: project.image});
}
Dit zal een gebruikspatroon zijn, dus laat me aparte methoden maken voor het instellen van de metatags.
setProject(project: any) {
// set title
this.setTitle(project.title);
// set url
this.setUrl();
// set description
this.setDescription(project.description);
// set image
this.setImage(project.image);
}
private setTitle(title: string) {
const _title = `${ title } - Sekrab Garage`;
this.title.setTitle(_title);
this.meta.updateTag({ name: 'title', property: 'og:title', content: _title });
this.meta.updateTag({ property: 'twitter:title', content: _title });
}
private setDescription(description: string) {
this.meta.updateTag({ name: 'description', property: 'og:description', content: description });
}
private setImage(imageUrl: string) {
this.meta.updateTag({ name: 'image', property: 'og:image', content: imageUrl });
this.meta.updateTag({ property: "twitter:image", content: imageUrl });
}
private setUrl() {
// next week we'll dive into other links
this.meta.updateTag({ property: 'og:url', content: this.doc.URL });
}
Pagina met vermeldingen
Wat betreft de projectenlijst, vandaag is het vrij eenvoudig, maar in de toekomst zal dit een pagina met zoekresultaten zijn. Het benodigde resultaat is een beetje slimmer dan een simpele "lijst met projecten".** Bijvoorbeeld bij het opzoeken van een restaurant:**
Title: 345 Restaurants, Chinese Food in San Francisco
Description: Found 345 Restaurants of Chinese food, with delivery, in San Francisco
De afbeelding is ook onbekend, we kunnen ofwel terugvallen op de standaard, of een categoriespecifieke afbeelding opzoeken. Ik wil klaar zijn voor zoekresultaten:
setSearchResults(total: number, category?: string) {
// Title: 34 projects in Turtles.
// Desc: Found 34 projects categorized under Turtles.
// TODO: localization and transalation...
this.setTitle(`${ total } projects in ${ category }`);
this.setDescription(`Found ${ total } projects categorized under ${ category }`);
this.setUrl();
this.setImage(); // rewrite service to allow null
}
private setImage(imageUrl?: string) {
// prepare image, either passed or defaultImage
// TODO: move defaultImage to config
const _imageUrl = imageUrl || defaultImage;
this.meta.updateTag({ name: 'image', property: 'og:image', content: _imageUrl });
this.meta.updateTag({ property: 'twitter:image', content: _imageUrl });
}
Titel structureren
Titel bestaat uit de volgende delen:
project title, extra info - Site name
Het eerste deel wordt aangestuurd door de server. Maar sommige pagina's kunnen statisch zijn , zoals "contacteer ons", "Registreer" of "Pagina niet gevonden." Het tweede deel is erg contextueel, in sommige apps, zoals een restaurantzoeker-app, is het beter SEO om op deze manier extra informatie over het restaurant toe te voegen
Turtle Restaurant, 5 stars in San Francisco - Site name
In ons eenvoudige project is de categorie de enige extra informatie:
setProject(project: IProject) {
// set title
this.setTitle(`${project.title}, in ${project.category}`);
// ... the rest
}
Statische paginatitels met routegegevens
In plaats van de SEO-setter in elk onderdeel aan te roepen, ga ik voor statische pagina's de root gebruiken app.component
constructor, en de routes
zich. Tonen, niet vertellen:
In een routedefinitie
{
path: 'contact',
component: ProjectContactComponent,
data: {
// add an optional parameter. TODO: translation
title: 'Contact us about a project',
},
}
In root app.component
, bekijk gebeurteniswijzigingen en filter NavigationEnd
eruit evenementen
export class AppComponent {
constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
private seoService: SeoService
) {
this.router.events
.pipe(filter((e) => e instanceof NavigationEnd))
.subscribe((event) => {
// get the route, right from the root child
// this allows a title to be set at any level
// but for this to work, the routing module should be set with paramsInheritanceStrategy=always
let route = this.activatedRoute.snapshot;
while (route.firstChild) {
route = route.firstChild;
}
// create a function with a fallback title
this.seoService.setPageTitle(route.data?.title);
});
}
}
In SeoService:
setPageTitle(title: string) {
// set to title if found, else fall back to default
this.setTitle(title || 'Welcome to Turtles and Lizards');
}
Om de titel op elk routeringsniveau op te halen, moeten we de rootroutingmodule aanpassen om op elk niveau te lezen (paramsInheritanceStrategy), is de titelwaarde die wordt opgehaald het diepste kind in de beoogde route , die een titelwaarde heeft, ongeacht hoe ondiep het is (het kan de wortel zijn).
@NgModule({
imports: [
RouterModule.forRoot(routes, {
// this is important if you are going to use "data:title" from any level
paramsInheritanceStrategy: 'always',
}),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
Dit lost ook een ander probleem op. Dat is standaard voor alle routes zorgen . Als we geen standaard fallback doen, kunnen de titels te lang blijven hangen in meerdere navigaties.
Kanttekening over volgorde van gebeurtenissen
Aangezien we de titel van meerdere locaties instellen, moet u in de gaten houden wat het laatst voorkomt, is dit wat u van plan was? Omdat feature-componenten meestal API-ophaalacties inhouden, zijn ze gegarandeerd de laatste, maar als u een constante paginatitel instelt, weet dan wat er eerst gebeurt, is het NavigationEnd, componet constructor of OnInit ?
Refactoren
Tijd om de kleine stukjes bij elkaar te brengen op één plek. We moeten 'vaste tags', 'defaults' en constante strings naar een mooiere plek verplaatsen.
Kanttekening:lokalisatie en vertaling
Ik gebruik een resourceklasse om mijn strings klaar te houden voor vertaling, maar u gebruikt waarschijnlijk het i18n-pakket van Angular , en ik vergeef het je, je zou alle strings moeten lokaliseren met dat pakket.
// Config.ts
export const Config = {
Seo: {
tags: [
{ property: 'og:site_name', content: 'Sekrab Garage' },
{ property: 'og.type', content: 'website' },
{ property: 'twitter:site', content: '@sekrabbin' },
{ property: 'twitter:card', content: 'summary_large_image' },
{ property: 'twitter:creator', content: '@sekrabbin' },
{ property: 'og:locale', content: 'en_GB' },
{ name: 'author', content: 'Ayyash' }
],
defaultImage: 'http://garage.sekrab.com/assets/images/sekrab0813.jpg'
}
}
// in SEO service, use Config.Seo.tags and Config.Seo.defaultImage
Zet de strings samen in een bronnenbestand, vergeet niet om later te vertalen. Het eindresultaat zou er als volgt uit moeten zien:
this.setTitle(SomeRes[title] || SomeRes.DEFAULT_PAGE_TITLE);
En voor opgemaakte titels, een manier om eenvoudige tekenreeksen te vervangen door werkelijke waarden, zoals deze:
this.setTitle(SomeRes.PROJECT_TITLE.replace('$0',project.title).replace('$1',project.description));
Dus eerst de strings, en laten we ze groeperen zodat we ze sneller kunnen vinden :
// A resources.ts file, need to be localized
export const RES = {
SITE_NAME: 'Sekrab Garage',
DEFAULT_PAGE_TITLE: 'Welcome to Turtles and Lizards',
// group static titles together
PAGE_TITLES: {
NOT_FOUND: 'Page no longer exists',
ERROR: 'Oh oh! Something went wrong.',
PROJECT_CONTACT: 'Contact us about a project',
HOME: 'Homepage',
},
// group other formatted strings together
SEO_CONTENT: {
PROJECT_TITLE: '$0, in $1',
PROJECT_RESULTS_TITLE: '$0 projects in $1',
PROJECT_RESULTS_DESC: 'Found $0 projects categorized under $1',
}
};
De routegegevens bevatten nu "sleutel" in plaats van de exacte titel:
// the project route
{
path: 'contact',
component: ProjectContactComponent,
data: {
title: 'PROJECT_CONTACT', // this is a key
},
},
En nog iets waar we gebruik van kunnen maken, JavaScript Replace
functie:
// take a string with $0 $1 ... etc, and replace with arguments passed
export const toFormat = (s:string, ...args: any) => {
const regExp = /\$(\d+)/gi;
// match $1 $2 ...
return s.replace(regExp, (match, index) => {
return args[index] ? args[index] : match;
});
}
Nu terug naar onze SEO-service
// the changes in the SEOService are:
private setTitle(title: string) {
// change this:
// const _title = `${title} - Sekrab Garage`;
const _title = `${ title } - ${RES.SITE_NAME}`;
// ... rest
}
setPageTitle(title: string) {
// this
// this.setTitle(title || 'Welcome to Turtles and Lizards');
this.setTitle(RES.PAGE_TITLES[title] || RES.DEFAULT_PAGE_TITLE);
}
setProject(project: any) {
// this
// this.setTitle(`${project.title}, in ${project.category}`);
this.setTitle(
toFormat(RES.SEO_CONTENT.PROJECT_TITLE, project.title, project.category)
);
// ...rest
}
setSearchResults(total: number, category?: string) {
// these
// this.setTitle(`${total} projects in ${category}`);
// this.setDescription(
// `Found ${total} projects categorized under ${category}`
// );
this.setTitle(
toFormat(RES.SEO_CONTENT.PROJECT_RESULTS_TITLE, total, category)
);
this.setDescription(
toFormat(RES.SEO_CONTENT.PROJECT_RESULTS_DESC, total, category)
);
// ... rest
}
Om te vertalen, raken we nu één bestand aan. Het toevoegen van een nieuwe functie brengt een nieuwe methode met zich mee, om titel en beschrijving en eventueel afbeelding aan te passen.
Volgende...
Links in metatags zijn de document-URL, canonieke links en alternatieve links. Volgende week duiken we er in. Bedankt voor het afstemmen. Laat het me weten in de reacties als je vragen hebt.
BRONNEN
- Metatags die Google begrijpt
- Metatag-generator
- Open grafiekprotocol