Search code examples
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:

    • Manual Console Review: I carefully inspected the server console logs and saw the error “Cannot read properties of undefined (reading 'getCurrentPosition')”. This immediately indicated that some code was trying to access browser-specific APIs on the server.
    • Flow Inspection of ServerStateInterceptor: By reviewing the flow in my ServerStateInterceptor, I tracked how my dependencies and services were being initialized and discovered that my localization service (invoked in the Profile component’s constructor) was executing on both server and client.
    • Reviewing Dependencies and Service Implementations: I advised myself (and now you) to check the dependencies and services implemented in your components. Make sure that any code which relies on browser-only objects (like navigator, document, or window) is conditionally executed only on the client side. One reliable way to do this is by injecting PLATFORM_ID from @angular/core and using Angular's isPlatformBrowser function from @angular/common.

    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:

    • Review Your Dependencies: Always check the services and code executed in your component constructors or lifecycle hooks. Make sure that any functionality depending on browser-specific APIs is guarded by a platform check (using isPlatformBrowser).

    • Manual Debugging: If none of the existing solutions on the site work, try reviewing the server console for errors and tracing through your interceptor’s flow (like the ServerStateInterceptor in my case) to pinpoint where the browser-only code is being executed on the server.

    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.