Skip to content
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

Closed
justinappler opened this issue Apr 5, 2017 · 31 comments
Closed

Support adding rel=canonical link tags using an included service #15776

justinappler opened this issue Apr 5, 2017 · 31 comments
Labels
area: server Issues related to server-side rendering

Comments

@justinappler
Copy link

I'm submitting a ... (check one with "x")

[ ] bug report => search github for a similar issue or PR before submitting
[ X ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

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

@MarkPieszak
Copy link
Member

MarkPieszak commented Apr 5, 2017

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;
    };

@vikerman
Copy link
Contributor

vikerman commented Apr 5, 2017

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.

@vikerman vikerman added the area: server Issues related to server-side rendering label Apr 5, 2017
@justinappler
Copy link
Author

@MarkPieszak Exactly the example I was looking for in lieu of built-in support. Thanks!

@goelinsights
Copy link

goelinsights commented Apr 8, 2017

@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?

removeCanonicalLink() {
        try {
            const renderer = this.rendererFactory.createRenderer(this.document, {
                id: '-1',
                encapsulation: ViewEncapsulation.None,
                styles: [],
                data: {}
            });
            const canonical = document.querySelector("link[rel='canonical']")
            const head = this.document.head;

            if (head === null) {
                throw new Error('<head> not found within DOCUMENT.');
            }
            if (!!canonical) {
                renderer.removeChild(head, canonical);
            }
        } catch (e) {
            console.error('Error within linkService : ', e);
        }
}

@MarkPieszak
Copy link
Member

@goelinsights Yes that won't work since you're using document which doesn't exist on the server

@goelinsights
Copy link

@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.

@samvloeberghs
Copy link
Contributor

the same thing should be able for the <html> tag putting the lang attribute

@salcam
Copy link

salcam commented May 30, 2017

Hi to all,
thanks @MarkPieszak for script code. I added a workaround that work (apparently) in prod. Not use document directly. but read head children and if there is selector, then remove before add.

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]);
}

@rzwinge
Copy link

rzwinge commented Jun 19, 2017

Hi,
exists a roadmap/plan for this DocumentService?
Is it a topic for 4.3?

@vikerman
Copy link
Contributor

vikerman commented Sep 6, 2017

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 vikerman closed this as completed Sep 6, 2017
@NgxDev
Copy link

NgxDev commented Nov 21, 2017

@vikerman any pointers regarding the DOM API? Looking through the docs, searching the api for dom doesn't say anything (even tried on 5.1.0-beta.1).
Is this DOM API present in 5+ ? If yes, any pointers on how to use it would be appreciated!

Thanks!

P.S. I've tried @salcam 's example above on a small app with a few material components.
It takes about 1.2 seconds until I see the html rendered server-side, as opposed to ~200ms when not traversing all children of the document's head in order to remove all styles/links[rel="stylesheet"].

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 <style> tag with what's needed for the current page (still haven't figured out how to do that, barely struggling to remove style/link tags for now).
And I hope that this DOM Api would come in handy for other things as well, like adding amp on the <html> tag.

@goelinsights
Copy link

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.

@julienR2
Copy link

@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 document is not defined. So I end up using the same workaround as before.

Am I missing something ? Any docs or info we can turn to to get more usecase ?

In the meantime thanks @MarkPieszak for the example 👍

@DominicBoettger
Copy link

I modified the code of @MarkPieszak a bit to be able to remove link tags by attribute.
example:
this.linkService.removeTag('rel=alternate');
this.linkService.removeTag('rel=canonical');

/*
 * -- 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;
  };

@crebuh
Copy link

crebuh commented Jan 9, 2018

@DominicBoettger @MarkPieszak

Adding link tags is possible, but when I want to remove them, I always get this error.

Error while removing tag this.document.querySelectorAll is not a function

@NgxDev
Copy link

NgxDev commented Jan 13, 2018

@crebuh
I've tried this on @MarkPieszak example:

  /**
   * AMP
   * Remove all <style> and <link/> tags from the <head>
   */
  removeStyles() {
    try {
      const renderer = this.rendererFactory.createRenderer(this.document, {
        id: '-1',
        encapsulation: ViewEncapsulation.None,
        styles: [],
        data: {}
      });

      const head = this.document.head;
      const children = head.children;
      let removedStyles = 0;
      for (var idx = 0; idx < children.length; idx++) {
        if (
          children[idx].localName === 'style' ||
          children[idx].localName === 'link' && children[idx].rel === 'stylesheet'
        ) {
          removedStyles++;
          renderer.removeChild(head, children[idx]);
        }
      }
      console.log(`Removed ${removedStyles} styles`);

    } catch (e) {
      console.error('Error within linkService : ', e);
    }
  }

It does remove all <style> and <link rel="stylesheet"/> tags from the head, BUT it takes around 1.5 seconds (I had around 8-9 style tags and 2 external links. It's very slow. And I haven't even added removal of script tags/blocks, which would mean traversing the <body>.
There has to be a way... Or, if there isn't one yet, I think Angular has to implement some support to help us with AMP (especially since there is a whole lot more to AMP than just removing styles, links and scripts). Adding a <style amp-custom> tag with all the styles needed on that page, adding amp attribute to the html tag. Plus, I don't know what to do about duplicate code. Right now, I would need have 2 versions for each component that is also needed on an AMP route. Firstly, because AMP validator errors on all custom tags and attributes (so no <my-component>, no <div _ngcontent-c1>; and what about <my-app-root>? I'd either have to not use that as a tag OR replace it server-side, at the Express level - by parsing the generated html somehow. Such an overkill; also, if I were to use anything else other than tag selectors for components, that is kind of against the style guide, isn't it?). Even for just eliminating the attributes, I would need a different component altogether in order to set ViewEncapsulation.None only when on the AMP route, right? As far as I know, we have no possibility of having logic within our @Component decorator, like viewEncapsulation: isAmpRoute ? ViewEncapsulation.None : ViewEncapsulation.Emulated, right?
I would love to turn our php tube sites to Angular, but unfortunately AMP is a requirement. And that doesn't seem possible right now, unless we'd write a separate app for AMP. In php is easy, we can have logic anywhere and can easily control the entire html output (piece-by-piece, from the "build" process, there's no need to parse the output in order to do major changes), like what styles/scripts to include (or not), conditional html blocks (use our mobile navigation or the amp-sidebar, whichever/when/where is needed) etc.
Since Universal became pretty stable, I've been waiting for some AMP support from Angular. But to this day I can't find anything out there to make it possible.

@cpelikan
Copy link

cpelikan commented Apr 6, 2018

Thank You to @MarkPieszak
I have adapted the code according to my needs, it works very well for me

`
import { Injectable, Optional, RendererFactory2, ViewEncapsulation, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';

@Injectable()
export class LinkTagService {
renderer: any;

constructor(
    private rendererFactory: RendererFactory2,
    @Inject(DOCUMENT) private document
) {

  this.renderer = this.rendererFactory.createRenderer(this.document, {
    id: '-1',
    encapsulation: ViewEncapsulation.None,
    styles: [],
    data: {}
});
}

updateTag (tag: LinkDefinition) {
  this.removeTag(tag);
  this.addTag(tag);
}

 /**
 * Rimuove il link esistente con lo stesso atrtributo rel
 */
removeTag(tag: LinkDefinition) {
  try {

      const selector = this._parseSelector(tag);

      const canonical = this.document.querySelector(selector)
      const head = this.document.head;

      if (head === null) {
          throw new Error('<head> not found within DOCUMENT.');
      }
      if (!!canonical) {
          this.renderer.removeChild(head, canonical);
      }
  } catch (e) {
      console.error('Error within linkService : ', e);
  }
}

/**
 * Inietta il link ocme ultimo child del tag <head>
 */
addTag(tag: LinkDefinition) {

    try {

          const link = this.renderer.createElement('link');
          const head = this.document.head;


          if (head === null) {
              throw new Error('<head> not found within DOCUMENT.');
          }


        Object.keys(tag).forEach((prop: string) => {
            return this.renderer.setAttribute(link, prop, tag[prop]);
        });

        // [TODO]: get them to update the existing one (if it exists) ?
        this.renderer.appendChild(head, link);

    } catch (e) {
        console.error('Error within linkService : ', e);
    }
}

private _parseSelector(tag: LinkDefinition): string {
    const attr: string = tag.rel ? 'rel' : 'hreflang';
    return `link[${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;
};

`

@rafa-suagu
Copy link

Any notice if it's included on release 6 ?

@MarkPieszak
Copy link
Member

So now that there is DOCUMENT (think the Document you already have in your browser / same API) from @angular/common, you can simply query the DOM and add/edit/remove any link items you'd want. You don't even need the Renderer anymore for it. 👍

@rafa-as

@alirezamirian
Copy link

alirezamirian commented May 16, 2018

Another approach to handle link, meta and anything that should be placed in head, without verbose services to cover all options, is a simple moveToHead directive like this:

@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 meta, link, etc., just like any other element in your template.

@m98
Copy link

m98 commented Jun 9, 2018

@MarkPieszak would you please provide an example using DOCUMENT?

@anymos
Copy link

anymos commented Jun 12, 2018

Trying to find any side effect regarding @alirezamirian solution, and I can not find any.

Thank you

@suau
Copy link

suau commented Sep 17, 2018

@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 @angular/common in 5.0 and thus now works with SSR. There is no need to mess with the renderer anymore and using DOCUMENT is recommended.
It mimics the regular browser document api.
Here is a boiled down example:

import {DOCUMENT} from "@angular/common";

...

constructor(@Inject(DOCUMENT) private document: Document) {...}

...

ngOnInit() {
    ...

    // WARNING ngOnInit is actually NOT the right place to do this, as it won't be called on back button presses etc.
    // for more see below

    const link = <HTMLLinkElement> this.document.head.querySelector("link[rel='canonical']")
      || this.document.head.appendChild(this.document.createElement("link"));
    link.rel = "canonical";
    link.href = "https://www.example.com/url";
}

NOTE about ngOnInit: depending on your RouteReuseStrategy this won't be always called and therefore shouldn't be used to update the canonical links, there seem to be no appropriate lifecycle hooks for these cases yet. Here are some alternatives:

  • (recommended) listen for url changes and change the canonicals accordingly
  • (overkill if this is your only issue, my solution) use a customized router-outlet, which adds additional lifecycle hooks onAttach and onDetach

@alexisbmills
Copy link

@MrCroft did you manage to get a final solution for AMP?

@RFreij
Copy link

RFreij commented Nov 8, 2018

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 routerListener

export class AppComponent {

    constructor(
        private readonly linkService: LinkService
    ) {
        linkService.startRouteListener();
    }
}

@Big-Silver
Copy link

@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?

@Big-Silver
Copy link

I just found the way how to get the hostname on server side in the Angular universal.

refer this article

@floodedcodeboy
Copy link

@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 @angular/common in 5.0 and thus now works with SSR. There is no need to mess with the renderer anymore and using DOCUMENT is recommended.
It mimics the regular browser document api.

@suau It looks like DOCUMENT has been deprecated in Angular 7 - doing a quick google I can't seem to find what one would use instead? Do you or anyone have any ideas?

@RFreij
Copy link

RFreij commented Mar 7, 2019

@floodedcodeboy The @angular/platform-browser DOCUMENT is deprecated, you should use the @angular/common DOCUMENT

https://angular.io/api/platform-browser/DOCUMENT << Deprecated
https://angular.io/api/common/DOCUMENT << Correct one

@msp-codes
Copy link

Angular Version: 7
AoT: Yes

import {DOCUMENT} from '@angular/common';
import {Inject, Injectable, inject} from '@angular/core';
import {
ɵDomAdapter as DomAdapter,
ɵgetDOM as getDOM
} from '@angular/platform-browser';

/**

  • Represents a link element.
  • @publicapi
    */
    export 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;
    };

/**

  • Factory to create Link service.
    */
    export function createLink() {
    return new Link(inject(DOCUMENT));
    }

/**

  • A service that can be used to get and add link tags.
  • @publicapi
    */
    @Injectable({providedIn: 'root', useFactory: createLink, deps: []})
    export class Link {
    private _dom: DomAdapter;
    constructor(@Inject(DOCUMENT) private _doc: any) { this._dom = getDOM(); }

addTag(tag: LinkDefinition, forceCreation: boolean = false): HTMLLinkElement|null {
if (!tag) return null;
return this._getOrCreateElement(tag, forceCreation);
}

addTags(tags: LinkDefinition[], forceCreation: boolean = false): HTMLLinkElement[] {
if (!tags) return [];
return tags.reduce((result: HTMLLinkElement[], tag: LinkDefinition) => {
if (tag) {
result.push(this._getOrCreateElement(tag, forceCreation));
}
return result;
}, []);
}

getTag(attrSelector: string): HTMLLinkElement|null {
if (!attrSelector) return null;
return this._dom.querySelector(this._doc, link[${attrSelector}]) || null;
}

getTags(attrSelector: string): HTMLLinkElement[] {
if (!attrSelector) return [];
const list /NodeList/ = this._dom.querySelectorAll(this._doc, link[${attrSelector}]);
return list ? [].slice.call(list) : [];
}

updateTag(tag: LinkDefinition, selector?: string): HTMLLinkElement|null {
if (!tag) return null;
selector = selector || this._parseSelector(tag);
const meta: HTMLLinkElement = this.getTag(selector) !;
if (meta) {
return this._setMetaElementAttributes(tag, meta);
}
return this._getOrCreateElement(tag, true);
}

removeTag(attrSelector: string): void { this.removeTagElement(this.getTag(attrSelector) !); }

removeTagElement(meta: HTMLLinkElement): void {
if (meta) {
this._dom.remove(meta);
}
}

private _getOrCreateElement(meta: LinkDefinition, forceCreation: boolean = false):
HTMLLinkElement {
if (!forceCreation) {
const selector: string = this._parseSelector(meta);
const elem: HTMLLinkElement = this.getTag(selector) !;
// It's allowed to have multiple elements with the same name so it's not enough to
// just check that element with the same name already present on the page. We also need to
// check if element has tag attributes
if (elem && this._containsAttributes(meta, elem)) return elem;
}
const element: HTMLLinkElement = this._dom.createElement('link') as HTMLLinkElement;
this._setMetaElementAttributes(meta, element);
const head = this._dom.getElementsByTagName(this._doc, 'head')[0];
this._dom.appendChild(head, element);
return element;
}

private _setMetaElementAttributes(tag: LinkDefinition, el: HTMLLinkElement): HTMLLinkElement {
Object.keys(tag).forEach((prop: string) => this._dom.setAttribute(el, prop, tag[prop]));
return el;
}

private _parseSelector(tag: LinkDefinition): string {
const attr: string = tag.name ? 'name' : 'property';
return ${attr}="${tag[attr]}";
}

private _containsAttributes(tag: LinkDefinition, elem: HTMLLinkElement): boolean {
return Object.keys(tag).every((key: string) => this._dom.getAttribute(elem, key) === tag[key]);
}
}

Usage:
constructor(private linkService: Link) {}

this.linkService.updateTag({rel: 'canonical', href: data.rel});

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 15, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: server Issues related to server-side rendering
Projects
None yet
Development

No branches or pull requests