-
Notifications
You must be signed in to change notification settings - Fork 25k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support adding rel=canonical link tags using an included service #15776
Comments
Eventually there will be some DocumentService part of Core that will handle both Meta/Link elements, for now you can use this one below, which should get you going! (This works with Universal/platform-server as well of course) Modeled after the Meta service, can use it the same way. constructor(linkService: LinkService) {
this.linkService.addTag( { rel: 'canonical', href: 'http://blogs.example.com/blah/nice' } );
this.linkService.addTag( { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/' } );
} LinkService:/*
* -- LinkService -- [Temporary]
* @MarkPieszak
*
* Similar to Meta service but made to handle <link> creation for SEO purposes
* -- NOTE: Soon there will be an overall DocumentService within Angular that handles Meta/Link everything
*/
import { Injectable, Optional, RendererFactory2, ViewEncapsulation, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';
@Injectable()
export class LinkService {
constructor(
private rendererFactory: RendererFactory2,
@Inject(DOCUMENT) private document
) {
}
/**
* Inject the State into the bottom of the <head>
*/
addTag(tag: LinkDefinition, forceCreation?: boolean) {
try {
const renderer = this.rendererFactory.createRenderer(this.document, {
id: '-1',
encapsulation: ViewEncapsulation.None,
styles: [],
data: {}
});
const link = renderer.createElement('link');
const head = this.document.head;
const selector = this._parseSelector(tag);
if (head === null) {
throw new Error('<head> not found within DOCUMENT.');
}
Object.keys(tag).forEach((prop: string) => {
return renderer.setAttribute(link, prop, tag[prop]);
});
// [TODO]: get them to update the existing one (if it exists) ?
renderer.appendChild(head, link);
} catch (e) {
console.error('Error within linkService : ', e);
}
}
private _parseSelector(tag: LinkDefinition): string {
// Possibly re-work this
const attr: string = tag.rel ? 'rel' : 'hreflang';
return `${attr}="${tag[attr]}"`;
}
}
export declare type LinkDefinition = {
charset?: string;
crossorigin?: string;
href?: string;
hreflang?: string;
media?: string;
rel?: string;
rev?: string;
sizes?: string;
target?: string;
type?: string;
} & {
[prop: string]: string;
}; |
Thanks @MarkPieszak ! The plan is to fold all this into a unified DocumentService that you can use to manipulate different tags in head. I expect this to land in 4.1 time frame. |
@MarkPieszak Exactly the example I was looking for in lieu of built-in support. Thanks! |
@MarkPieszak Thanks for this! I had some trouble with the addTag function creating multiple rel canonicals as navigation occurred. I created a removeCanonicalLink function to pair with it (I just run it ahead of calling the addTag({ rel: 'canonical', href: 'http://blogs.example.com/blah/nice' }) in my function (for me it's in ngOnChanges). Do you see any issues with this approach I should be aware of/ testing for?
|
@goelinsights Yes that won't work since you're using |
@MarkPieszak do you mean for a universal site? Whats a better workaround? I haven't deployed Universal yet, so it seems to be working in the browser for a normal static site. |
the same thing should be able for the |
Hi to all, Then replace your [TODO] with: const children = head.children;
for (var idx = 0; idx < children.length; idx++) {
if (children[idx].localName === 'link' && children[idx].rel === tag.rel)
renderer.removeChild(head, children[idx]);
} |
Hi, |
We started out with a DocumentService but realized we don't want to special case every single meta tag like this. So we went down the route of just provding a good subset of DOM API on the server using domino - 2f2d5f3 So the answer in 5.0 is just use the DOM API. |
@vikerman any pointers regarding the DOM API? Looking through the docs, searching the api for dom doesn't say anything (even tried on Thanks! P.S. I've tried @salcam 's example above on a small app with a few material components. My use case is AMP: need to remove all style/link[rel="stylesheet"] + all script tags and only insert amp specific scripts (sidebar etc.) and one |
Agree that rel=canonical example would be super helpful (and a strange omission for a product supported by google). Rob wormald highlighted early progress toward AMP last year at Ng-conf. Between universal, meta/ canonical, and AMP it would seem google webmaster guidance for content ranking would be something that would be a common enough use case that development path and documentation would be super helpful. |
@vikerman, as @MrCroft I didn't find any docs or details about the new server side rendering life saver Domino. All the news about angular 5 highlight the fact that Dom manipulations will be available out of the box. However even after our recent migration to angular 5, I still have issues like Am I missing something ? Any docs or info we can turn to to get more usecase ? In the meantime thanks @MarkPieszak for the example 👍 |
I modified the code of @MarkPieszak a bit to be able to remove link tags by attribute. /*
* -- LinkService -- [Temporary]
* @MarkPieszak
* Added removeTag by @DominicBoettger
* Similar to Meta service but made to handle <link> creation for SEO purposes
* -- NOTE: Soon there will be an overall DocumentService within Angular that handles Meta/Link everything
*/
import { Injectable, Optional, RendererFactory2, ViewEncapsulation, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';
@Injectable()
export class LinkService {
constructor(
private rendererFactory: RendererFactory2,
@Inject(DOCUMENT) private document
) {
}
/**
* Inject the State into the bottom of the <head>
*/
addTag(tag: LinkDefinition, forceCreation?: boolean) {
try {
const renderer = this.rendererFactory.createRenderer(this.document, {
id: '-1',
encapsulation: ViewEncapsulation.None,
styles: [],
data: {}
});
const link = renderer.createElement('link');
const head = this.document.head;
const selector = this._parseSelector(tag);
if (head === null) {
throw new Error('<head> not found within DOCUMENT.');
}
Object.keys(tag).forEach((prop: string) => {
return renderer.setAttribute(link, prop, tag[prop]);
});
// [TODO]: get them to update the existing one (if it exists) ?
renderer.appendChild(head, link);
} catch (e) {
console.error('Error within linkService : ', e);
}
}
removeTag(attrSelector: string) {
if (attrSelector) {
try {
const renderer = this.rendererFactory.createRenderer(this.document, {
id: '-1',
encapsulation: ViewEncapsulation.None,
styles: [],
data: {}
});
const head = this.document.head;
if (head === null) {
throw new Error('<head> not found within DOCUMENT.');
}
const linkTags = this.document.querySelectorAll('link[' + attrSelector + ']');
for (const link of linkTags) {
renderer.removeChild(head, link);
}
} catch (e) {
console.log('Error while removing tag ' + e.message);
}
}
}
private _parseSelector(tag: LinkDefinition): string {
// Possibly re-work this
const attr: string = tag.rel ? 'rel' : 'hreflang';
return `${attr}="${tag[attr]}"`;
}
}
export declare type LinkDefinition = {
charset?: string;
crossorigin?: string;
href?: string;
hreflang?: string;
media?: string;
rel?: string;
rev?: string;
sizes?: string;
target?: string;
type?: string;
} & {
[prop: string]: string;
}; |
Adding link tags is possible, but when I want to remove them, I always get this error.
|
@crebuh
It does remove all |
Thank You to @MarkPieszak ` @Injectable()
} export declare type LinkDefinition = { ` |
Any notice if it's included on release 6 ? |
So now that there is @rafa-as |
Another approach to handle @Directive({
selector: '[moveToHead]'
})
export class MoveToHeadDirective implements OnDestroy, OnInit {
constructor(private renderer: Renderer2, private elRef: ElementRef, @Inject(DOCUMENT) private document: Document) {
}
ngOnInit(): void {
this.renderer.appendChild(this.document.head, this.elRef.nativeElement);
this.renderer.removeAttribute(this.elRef.nativeElement, 'movetohead');
}
ngOnDestroy(): void {
this.renderer.removeChild(this.document.head, this.elRef.nativeElement);
}
} Usage: <link rel="amphtml" moveToHead [attr.href]="ampUrl"> This way you can use data bindings and all other stuff, and treat that |
@MarkPieszak would you please provide an example using DOCUMENT? |
Trying to find any side effect regarding @alirezamirian solution, and I can not find any. Thank you |
@m98 seems like a lot of people are still having issues with this. As mentioned by @vikerman above already, just use the DOCUMENT / DOM API wrapper, which has been moved to
NOTE about
|
@MrCroft did you manage to get a final solution for AMP? |
I needed a service that adds a canonical tag on every page. However the ability to change/remove the canonical tag in certain components was also required. I combined all the examples into one single LinkService with a option to start a route listener. Thank you @suau @MarkPieszak @DominicBoettger for your examples If you see something that can be improved please tell me. import { Injectable, OnDestroy, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Router, NavigationEnd } from '@angular/router';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import { LinkDefinition } from './link-definition';
@Injectable({
providedIn: 'root'
})
export class LinkService implements OnDestroy {
private routeListener: Subscription;
constructor(
@Inject(DOCUMENT) private readonly document: Document,
private readonly router: Router,
) { }
/**
* Start listening on NavigationEnd router events
*/
public startRouteListener(): void {
this.routeListener = this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(
() => {
let url = '';
const urlTree = this.router.parseUrl(this.router.url);
if (urlTree.root.hasChildren()) {
const segments = urlTree.root.children['primary'].segments;
if (segments && segments.length > 0) {
url = segments.map(segment => segment.path).join('/');
}
}
this.updateTag({
rel: 'canonical',
href: `/${url}`
});
}
);
}
/**
* Create or update a link tag
* @param {LinkDefinition} tag
*/
public updateTag(tag: LinkDefinition): void {
const selector = this._parseSelector(tag);
const linkElement = <HTMLLinkElement> this.document.head.querySelector(selector)
|| this.document.head.appendChild(this.document.createElement('link'));
if (linkElement) {
Object.keys(tag).forEach((prop: string) => {
linkElement[prop] = tag[prop];
});
}
}
/**
* Remove a link tag from DOM
* @param tag
*/
public removeTag(tag: LinkDefinition): void {
const selector = this._parseSelector(tag);
const linkElement = <HTMLLinkElement> this.document.head.querySelector(selector);
if (linkElement) {
this.document.head.removeChild(linkElement);
}
}
/**
* Get link tag
* @param tag
* @return {HTMLLinkElement}
*/
public getTag(tag: LinkDefinition): HTMLLinkElement {
const selector = this._parseSelector(tag);
return this.document.head.querySelector(selector);
}
/**
* Get all link tags
* @return {NodeListOf<HTMLLinkElement>}
*/
public getTags(): NodeListOf<HTMLLinkElement> {
return this.document.head.querySelectorAll('link');
}
/**
* Parse tag to create a selector
* @param tag
* @return {string} selector to use in querySelector
*/
private _parseSelector(tag: LinkDefinition): string {
const attr: string = tag.rel ? 'rel' : 'hreflang';
return `link[${attr}="${tag[attr]}"]`;
}
/**
* Destroy route listener when service is destroyed
*/
ngOnDestroy(): void {
this.routeListener.unsubscribe();
}
} export declare type LinkDefinition = {
charset?: string;
crossorigin?: string;
href?: string;
hreflang?: string;
media?: string;
rel?: string;
rev?: string;
sizes?: string;
target?: string;
type?: string;
} & {
[prop: string]: string;
}; Starting routerListenerexport class AppComponent {
constructor(
private readonly linkService: LinkService
) {
linkService.startRouteListener();
}
} |
@MarkPieszak I followed your guide and something is broken. When I check the header through chrome browser developer console, it looks good but when I check the header using View page source, the href of link went wrong. The reason was server didn't recognize window.location in the Angular universal, is there any way to know the hostname on the server side in the Angular universal? |
I just found the way how to get the hostname on server side in the Angular universal. |
@suau It looks like |
@floodedcodeboy The @angular/platform-browser DOCUMENT is deprecated, you should use the @angular/common DOCUMENT https://angular.io/api/platform-browser/DOCUMENT << Deprecated |
Angular Version: 7 import {DOCUMENT} from '@angular/common'; /**
/**
/**
addTag(tag: LinkDefinition, forceCreation: boolean = false): HTMLLinkElement|null { addTags(tags: LinkDefinition[], forceCreation: boolean = false): HTMLLinkElement[] { getTag(attrSelector: string): HTMLLinkElement|null { getTags(attrSelector: string): HTMLLinkElement[] { updateTag(tag: LinkDefinition, selector?: string): HTMLLinkElement|null { removeTag(attrSelector: string): void { this.removeTagElement(this.getTag(attrSelector) !); } removeTagElement(meta: HTMLLinkElement): void { private _getOrCreateElement(meta: LinkDefinition, forceCreation: boolean = false): private _setMetaElementAttributes(tag: LinkDefinition, el: HTMLLinkElement): HTMLLinkElement { private _parseSelector(tag: LinkDefinition): string { private _containsAttributes(tag: LinkDefinition, elem: HTMLLinkElement): boolean { Usage: this.linkService.updateTag({rel: 'canonical', href: data.rel}); |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
I'm submitting a ... (check one with "x")
Current behavior
MetaService, or any other service, doesn't support setting/adding
<link rel="canonical" href="...">
tags.Expected behavior
Like the HEAD services that allow addition of
<title>
and<meta>
tags, adding<link>
tags should be supported as well.Minimal reproduction of the problem with instructions
N/A
What is the motivation / use case for changing the behavior?
In the universal/server context, it is often useful to be able to set a rel="canonical" link element to prevent search engines from duplicating results. See Canonical Link Element for more details.
Please tell us about your environment:
Angular version: 2.0.X
4.0.1
Browser:
All
Language: [all | TypeScript X.X | ES6/7 | ES5]
All
Node (for AoT issues):
node --version
=N/A
The text was updated successfully, but these errors were encountered: