angularionic-frameworkserver-side-renderingangular-universal

Angular Universal Metatags not updating at server when loading from service


When navigating to this route user/:username I load the user data from a service to render it, but when updating the metatags with that data for page's SEO, it does not update it at the server just the client.

user.page.ts

ngOnInit(): void {
  this.route.params.subscribe( (params: ProfileParams) => this.getUserProfile(params.username) );
}

private getUserProfile(username: string): void {
this.profiles.getUserProfile(username).subscribe({
  next: profile => {
    this.profile = profile;
    this.updateMetas();
  }, error: error => {
    this.router.navigate(['404']);
    console.error(error);
  }
});

private updateMetas(): void {
const PROFILE_NAME: string = `${this.profile?.name || 'Profile'} ${this.profile?.last_name || ''}`.trim();
this.title.setTitle(`${PROFILE_NAME} | ${environment.appName}`);
this.seo.updateUrlMetas({
  title: PROFILE_NAME,
  description: this.profile?.description,
  image: this.profile?.profile_picture,
  keywords: [PROFILE_NAME, this.profile?.profession, this.profile?.locale]
});

}

seo.service.ts

private platformId = inject(PLATFORM_ID);
constructor(
    private title: Title,
    private meta: Meta,
    private router: Router
) {}

private sanitizeText(text?: string): string {
    return text ? text.replace(/(\r\n|\n|\r)/gm, ' ') : '';
}

public updateUrlMetas(seoData: SeoData): void {
    //this.title.setTitle(seoData.title);
    this.meta.updateTag({ name: 'title', content: seoData.title }, `name='title'`);
    this.meta.updateTag({ property: 'og:title', content: seoData.title }, `property='og:title'`);
    this.meta.updateTag({ name: 'twitter:title', content: seoData.title }, `name='twitter:title'`);

    if (seoData.description) {
        const cleanDescription = this.sanitizeText(seoData.description);
        this.meta.updateTag({ name: 'description', content: cleanDescription }, `name='description'`);
        this.meta.updateTag({ property: 'og:description', content: cleanDescription }, `property='og:description'`);
        this.meta.updateTag({ name: 'twitter:description', content: cleanDescription }, `name='twitter:description'`);
    }

    if (seoData.image) {
        this.meta.updateTag({ property: 'og:image', content: seoData.image }, `property='og:image'`);
        this.meta.updateTag({ name: 'twitter:image', content: seoData.image }, `name='twitter:image'`);
    }

    if (seoData.keywords?.length) {
        this.meta.updateTag({ name: 'keywords', content: seoData.keywords.join(', ') }, `name='keywords'`);
    }

    const fullUrl = isPlatformServer(this.platformId) 
        ? `https://konfii.com${this.router.url}` 
        : window.location.href;
    this.meta.updateTag({ property: 'og:url', content: fullUrl }, `property='og:url'`);
    this.meta.updateTag({ property: 'al:web:url', content: fullUrl }, `property='al:web:url'`);
    this.meta.updateTag({ property: 'al:android:url', content: `com.konfii.app:${this.router.url}` }, `property='al:android:url'`);

    this.meta.updateTag({ property: 'og:type', content: 'website' }, `property='og:type'`);
    this.meta.updateTag({ name: 'twitter:card', content: seoData.image ? 'summary_large_image' : 'summary' }, `name='twitter:card'`);
}

I have other component using that service function and it works fine, all meta tags update and the SEO aswell.

service-detail.component.ts

ngOnInit(): void {
if(!this.uuid) { this.router.navigate(['404']); }
this.refreshFunction.subscribe( event => this.handleRefresh(event) );
this.services.getService(this.uuid, this.auth?.user?.unique_code).subscribe({
  next: res => {
    this.serviceDetail = res;
    this.setMetaTags(res);
    this.reviews.getReviewsPerService(this.uuid, 1).subscribe(reviews => this.serviceReviews = reviews);
    this.reviews.getReviewScorePerService(this.uuid).subscribe(score => this.serviceScore = score);
  }, error: error => {
    console.error(error);
    if(error.status === 404) { return this.router.navigate(['404']); }
  }
});

}

I'm using Angular 16.1.7, NgUniversal 16.1.1 and Ionic 8.


Solution

  • I eventually discovered that the error was caused because my geolocation service was being called in the constructor of my Profile component without checking whether the code was running on the server or the browser. During server-side rendering, there is no navigator.geolocation (and no document or window), so when the code tried to access getCurrentPosition, it threw a TypeError.

    How I Diagnosed the Issue:

    Solution:

    I updated my localization service so that the geolocation code is only executed when running in the browser. For example:

    import { isPlatformBrowser } from '@angular/common';
    import { Inject, PLATFORM_ID } from '@angular/core';
    
    export class LocalizationService {
      constructor(
        @Inject(PLATFORM_ID) private platformId: Object,
        private language: LanguageService,
        private alert: AlertService
      ) {}
    
      private getWebLocalization(): void {
        if (isPlatformBrowser(this.platformId)) {
          navigator.geolocation.getCurrentPosition(
            position => this.getReverseGeocodingPosition({
              coords: {
                latitude: position.coords.latitude,
                longitude: position.coords.longitude
              }
            }),
            { enableHighAccuracy: true }
          );
        } else {
          console.warn('Geolocation is not available on the server.');
        }
      }
    
      private getReverseGeocodingPosition(data: any): void {
        // Reverse geocoding logic...
      }
    }
    

    Key Takeaways:

    This approach ensured that the geolocation code only runs on the client, preventing the “document is not defined” or “getCurrentPosition” errors on the server side.