skip to Main Content

I’ve spent more than 2 months but could not found a clear solution of how to proceed with angular universal.
I’ve already spent about 6 months in implementing angular universal on a project for which I don’t get a much time and now I’m stuck with this issue. Can anyone help me with this as it seems the whole world want to know a solution for Angular SSR.

Here’s my code(Meta tag service):

import {Injectable} from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';
import {commonMetas} from './meta-data.model';

@Injectable()
export class SeoService {
    public commonMetas = commonMetas;
    constructor(public meta: Meta, public title: Title) {}

    setAutomatically (key = 'none') {
        const detail = this.commonMetas[key];
        /** setting defaults */
        this.setTitle(detail.title);
        this.setAuthor();
        this.setDescription(detail.description);
        this.setKeywords(detail.keyword);
    }
    setFromJson(key: {
        title: any,
        description: any,
        image: any,
        keyword: any,
        author: any
    }) {
        key.title = key.title ? key.title : this.commonMetas['none'].title;
        key.description = key.description ? key.description : this.commonMetas['none'].description;

    }
    setTitle(titleToSet = '') {
        this.title.setTitle(titleToSet);
    }
    setAuthor (nameToSet = '') {
        this.meta.addTag({ name: 'author',   content: 'havemybooks.com'});
    }
    setKeywords (keyword = '') {
        this.meta.addTag({ name: 'keywords', content: keyword});
    }
    }
}

And my component.ts

  ngOnInit() {
    this.sub = this.route.params.subscribe(params => {
      this.id = +params['id'];
      this.api.getParticular({id: this.id}).subscribe((response) => {
        this.content = response.blog_data[0];
        this.content.main_image = this.getImage(this.content.main_image);
        this.content.metaCreatedAt = moment(this.content.created_at).format('YYYY-MM-DD');
        this.content.displayCreatedAt = moment.utc(new Date(this.content.created_at)).fromNow();
        this.content.name = this.handleName(this.content.name);
        this.seo.setFromJson({
          title: this.content.title,
          image: this.content.main_image,
          description: this.content.blog,
          author: this.content.name,
          keyword: ''
        });
      });
   });
  }

Here are some linked StackOverflow questions and GitHub issues:

Angular universal Server Side Rendering, Meta tags

Updating meta tags for SEO using angular universal

Angular Universal + External API

https://github.com/fulls1z3/ngx-meta/issues/101

Angular Universal – OG meta tags not working with child routes

https://github.com/fulls1z3/ngx-meta/issues/118(I tried to get help here from someone who successfully implemented but got no help)

https://github.com/maciejtreder/ng-toolkit/issues/460 (I opened it)

The list goes on I’ve seen a number of discussions which were never concluded. Anyone who can suggest how to make an API call before rendering in ng-express.

I have implemented SSR and use ngx-meta tag but still, fb crawler shows the default meta tags I used in the head tag.

UPDATE: I’m able to get the source updated with view source option in
chrome but the Facebook and Google crawler show the default meta tags
which are there by default. It’s very tough to launch my website with
it remaining any help is appreciated. @Brandon guided me a lot I spent
quite some time on implementing node with Jade and Nunchucks but since
angular universal use angular-express by default so I wasn’t able to
use the above-mentioned render engines.

So is there a way to render meta tags using ng express engine.

Like this <title>{{meta.title}}</title>...

5

Answers


  1. Chosen as BEST ANSWER

    Couldn't find an easy way to do it but here's a temporary/hacky solution provided by a GitHub user here and I'm quoting his answer directly:

    //all imports
    enableProdMode();
    
    export const app = express();
    
    const mysql = require('mysql');
    const httpRequest = require("request");
    // all other consts
    
    app.engine('html', ngExpressEngine({
      bootstrap: AppServerModuleNgFactory,
      providers: [
        provideModuleMap(LAZY_MODULE_MAP)
      ]
    }));
    //all other piece of code in server.ts
    
    app.get('/*', (req, res) => {
      res.render('index', {req, res}, (err, html) => {
        if (html) {
            
            //console.log(html);
            // This is where you get hold of HTML which "is about to be rendered"
        
            // after some conditional checks make a HTTP call
            let url = 'http://....';
            httpRequest.get(url, (error, response, body) => {
                            if(error) {
                                return console.log(error);
                            }
                            const respBody = JSON.parse(body);
                            if(respBody){
                                  html = html.replace(/$TITLE/g, respBody.title);
                                  html = html.replace(/$DESCRIPTION/g, respBody.description);
                                  html = html.replace(/$OG_META_KEYWORDS/g, respBody.metaKeywords);
                                  html = html.replace(/$OG_META_DESCRIPTION/g, respBody.metaDescription);
                                  html = html.replace(/$OG_DESCRIPTION/g, respBody.ogDescription);
                                  html = html.replace(/$OG_TITLE/g, respBody.ogTitle);
                                  html = html.replace(/$OG_IMAGE/g, respBody.img);
                                  html = html.replace(/$OG_SITE/g, respBody.ogSite);
                            }
                            res.send(html);
                });
         }
      }
    }
    

    And in index.html, create template values as below:

        <title>$TITLE</title>
    
        <meta name="description" content="$DESCRIPTION"/> 
        <meta name="keywords" content="$OG_META_KEYWORDS" />
        <meta name="metaDescription" content="$OG_META_DESCRIPTION"/> 
        <meta name="metaKeywords" content="$OG_META_KEYWORDS" />
        <meta property="og:title" content="$OG_TITLE" />
        <meta property="og:description" content="$OG_DESCRIPTION" />
        <meta property="og:site_name" content="$OG_SITE" /> 
        <meta property="og:type" content="website" />   
        <meta property="og:image" content="$OG_IMAGE" />
    

  2. First Add all required meta tags in index.html file like this

    index file

    Then update the meta tags in each required component, use meta.updateTag() as below

    component file

    This one worked for me.

    I try to create service to update but that not worked, its working only when meta update added in the same component.

    Login or Signup to reply.
  3. I implemented setting title on Angular 2 Universal. It should be done using Renderer, like this:

    1.Import Renderer inside of component:

    import {Renderer } from '@angular/core';
    

    2.Add dependency inside of constructor:

    constructor
        (
            ...
            private renderer: Renderer
        ) {}
    

    3.Use it now for setting title:

    renderer.setText(renderer.selectRootElement('title'), value);
    
    Login or Signup to reply.
  4. Stumbled upon this looking for same solution. Using a setTimeout worked for me. Make sure to refresh cache or use incognito mode or you’ll miss seeing the metadata (I made this mistake). My research suggests that subscribe for API data must be waited upon before SSR renders page so observable must stay open (but I’m not an expert). https://angular.io/guide/observables. Also, the only way to make it work is to put the response from subscription right into metaService. I’ll show what I mean in my code.

    setTimeout( () =>
                  this.seo.setFromJson({
                  title: this.content.title,
                  image: this.content.main_image,
                  description: this.content.blog,
                  author: this.content.name,
                  keyword: ''
                });
        ,0);
    

    My code with solution:

    this.publicFeedService.getPublicProfile(data).subscribe((response:any) => {
            this.memberProfile = response[this.userId];
            setTimeout( () => 
            //Define the meta title using profile data
            this.titleService.setTitle('The best investment ideas from ' + response[this.userId].firstName + ' ' + response[this.userId].lastName), 0);
            setTimeout( () =>         
            //Define the meta tags using the user profile data
            this.metaService.addTags([
              {name: 'keywords', content: 'investing ideas, stock ideas, financial profile'},
              {name: 'description', content: response[this.userId].about},
              {name: 'robots', content: 'index, follow'}
            ]), 0);
    
    Login or Signup to reply.
  5. Solution you can call it!

    A yoda master recipe

    As someone who tried to achieve a SEO friendly angular using SSR and utilities angular has to offer regarding meta tags, I would like to share an idea about this question.

    I was so focused on this topic that I failed to see the obvious and how simple it was to solve it!

    As you may know HTML would be rendered first, then CSS and only THEN JavaScript.

    In addition, index bots mostly care about raw HTML!

    There for whether bots should be made to make decisions after they rendered JS
    which by the way is not efficient and cost for companies like google and they have their own reasons for doing so, or …

    We can embed single page apps inside specific backend programs compatible of changing the html before sending it to client-side.

    Think about it …

    single page application was introduced for a specific new problem and is not opt to be analyzed by only the raw HTML!

    So the specific answer to "Update meta tags in angular universal with external API call" has already been answered.

    How ever, if you would like a real SEO friendly angular app, you must embed them together.


    I like doing it in Spring boot + Thymeleaf, If you use any other backend, well you know what to do …

    Approach

    1. Simply build your angular/react/vue projects or in your case project.
      You are not required to build it with angular universal, it’s primary idea is to distribute the same index.html page to replicase with folder structure based on your routing with the help of node runners so that you can change the meta tag of each page. I’m sure with enough information you can find a way to embed the app with even that but does it worth it? I leave it up to you.

    2. Make strategies for your routes in your backend. Make them all return the index but not before setting proper meta tags and specification you want to apply to it. The client-app it self will do the rest of the work on rendering in the browser.
    3. Customize the backend and/or front end for the maximum performance, hell you can do a lot with a devOps as well. Then coffee break.

    Tip: with this approach you can have Thymeleaf templates and also manage multiple single page apps together with only routing!


    I will provide a simple example as this is only an idea not a tutorial. Feel free to leave comments, I respond shortly afterward.

    Implementation

    • Index.html let’s say I want this meta tag to be dynamic based on a path I consider.
    ...
    
      <meta th:if="${post}" property="og:site_name" 
    th:content="${post} ? ${post.getName()}: 'Your sit name'">
    
    ...
    
    
    • Template Controller and there goes the paths
    
    import lombok.AllArgsConstructor;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.servlet.ModelAndView;
    
    
    @RestController
    @AllArgsConstructor
    public class TemplateController {
    
        @GetMapping("/")
        public ModelAndView HomePage() {
    
            // replace the path you aim for the index.html
            ModelAndView home = new ModelAndView("app/index");
            return home;
    
        }
    
    
        @GetMapping("/post/{name}")
        public ModelAndView Post(@PathVariable String name) {
    
            // replace the path you aim for the index.html
            ModelAndView home = new ModelAndView("app/index");
            PostResponse postResponse = new PostResponse();
            postResponse.setName(name);
            home.addObject("post", mock);
            return home;
        }
    
    }
    
    
    • PostResponse just a dummy class
    
    import lombok.Data;
    
    import java.util.Date;
    import java.util.UUID;
    
    @Data
    public class PostResponse {
    
        private UUID id;
        private Date created;
        private String path;
        private String name;
    
    }
    
    

    Now if you simply go to the route page localhost:port you can find the meta with "your site name". You go to the page localhost:port/post/some name , you’ll get the meta with the content, "some name".

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search