Before You Proceed

This article is about building dynamic meta tags, which relies on a solid grasp of Angular SSR (Server-Side Rendering) or SSG (Static Site Generation).

You can write the Angular code shown here, but your SEO and social media previews won't see real improvements without the server-side rendering setup. The key reason is that Angular SSR is what allows the generation of unique meta tags based on the page's content.

If you're just getting started with Angular SSR, a great starting point is Miłosz's guide to Angular SSR.

Angular SSR: Optimize SEO with rendering Meta Tags, OG Tags and Social Media Previews — figure 1

The Influence of Meta and OG Tags on Angular SEO

For many online businesses—think e-commerce, news sites, blogs, or content libraries—traffic driven by social media and messaging platforms is vital. When a link to your app gets shared, you want it to capture attention. Therefore, having control over the link preview's title, description, and image is essential for boosting your CTR (Click-Through Rate).

The standard approach for creating engaging shared links is to use Open Graph (OG) meta tags. Before we go further, check out these examples from different platforms below to see how these tags can turn a simple link into a richer preview.

LinkedIn

Link preview to an Angular web app on Linkedin

Facebook

Link preview to an Angular web app on Facebook

X (Twitter)

Link preview to an Angular web app on X (Twitter)
Discord

Link preview to an Angular web app on Discord

You can see a clear upgrade in each example. When a URL is posted, platforms like Facebook and LinkedIn will crawl that page automatically. They do this to gather the essential information and build a rich preview card that includes a big image, a title, and a summary. This makes your post far more interesting and gives it more presence on the screen.

Now that you've seen what OG tags can do, let's look at the Open Graph standard and see how to put it into practice.

OG Tags: A Definition

The Open Graph protocol is the official standard that lets any webpage be represented as a rich object in a social graph.

In simpler words, OG tags are specific pieces of HTML <meta> code located in the <head> of your webpage. They give social media sites, search engines, and messaging apps structured details about the page. Thanks to OG tags, your shared link looks informative and visually striking, not just a bare URL.

Now, let's see what a basic HTML implementation of OG tags looks like.

Deconstructing the HTML Implementation of OG Tags

At its core, adding OG tags means inserting a few specific <meta> tags into your page's HTML. Each tag relies on property and attributes like content to define a specific data point, such as the title, description, or the image for the preview.

Let's review the OG tags used on an angular.love article. To find these yourself, you can right-click the page and choose "View Page Source".

<!DOCTYPE html>
<html lang="en">
 <head>
   ...
   <meta
     property="og:title"
     content="Why is inject() better than constructor? - Angular.love"
   />
   <meta
     property="og:description"
     content="Angular.love - a place for all Angular enthusiasts created to inspire and educate."
   />


   <meta property="og:type" content="article" />
   <meta
     property="og:url"
     content="https://angular.love/why-is-inject-better-than-constructor"
   />
   <meta
     property="og:image"
     content="https://wp.angular.love/wp-content/uploads/2025/09/Okladki-blog-1920-x-1080-px-28.png"
   />
   <meta property="og:image:width" content="1920" />
   <meta property="og:image:height" content="1080" />
   <!-- Other OG meta tags -->
   ...
 </head>
 ...
</html>

Although many tags are present, almost all social media rich previews are based on these essential properties:

  • og:title – The main heading of your content.
  • og:description – A brief, persuasive summary of the page (usually just one or two sentences).
  • og:type – The nature of the content, whether it's a website, article, book, etc. Check the docs.
  • og:url – The canonical, distinct URL for the page.
  • og:image – The URL for the preview image. This one is critical for getting users' attention!

These basic five tags will generate a rich preview. But there’s no single validation standard; each social network handles the display of this information differently. Thus, it's best to keep your titles and descriptions short and to the point.

For the preview image, follow these important guidelines to ensure it looks perfect:

  • Size: The recommended dimensions are 1200x630 pixels. A different aspect ratio might result in the image being cropped unexpectedly – something you’ll want to avoid.
  • Format: Use JPG or PNG files to ensure compatibility across all social platforms.
  • File Size: Keep the image size small. A very large file could cause the preview to not show up at all.

Having those 5 meta tags is sufficient to create an attractive preview on most networks. But does that mean you've harnessed all the power of the Open Graph protocol? Not quite.

Next, we’ll look at the optional tags that can provide web crawlers with extra valuable information.

Additional OG Metadata Properties

These extra tags are not mandatory but they are strongly encouraged. They offer crawlers more context about your page.

Site Details

  • og:site_name – The name of your whole site, for example, "Angular.love".
  • og:locale – Specifies the language and region of your content. It sits in the language_TERRITORY format, with en_US as the default.
  • og:locale:alternate – An array listing other locales this page is available in.

Take a look at a sample implementation:

<meta property="og:site_name" content="Angular.love" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="pl_PL" />
<meta property="og:locale:alternate" content="de_DE" />

Preview Image Details

  • og:image:secure_url – The HTTPS version of the image's URL.
  • og:image:type – The MIME type of the image (like image/jpeg, image/png).
  • og:image:width – The image's width in pixels.
  • og:image:height – The image's height in pixels.
  • og:image:alt – A text description of the image's content.

The complete list of OG properties can be found in the Official Open Graph documentation.

Using Angular SSR to Handle OG Tags

Now that the HTML side is clear, let's move this to Angular. A top-notch implementation starts with a well-typed structure, so we'll begin by defining a solid data model.

Creating a Data Model

og-image.ts

export interface OgImage {
 url: string;
 alt: string;
}

For the og:type, we'll use a string union. This gives us autocompletion in our IDE, prevents accidental typos, and ensures our SeoService strictly aligns with the official Open Graph specification.

og-type.ts

export type OgType =
 | 'website'
 | 'article'
 | 'book'
 | 'profile'
 | 'payment.link'
 | 'music.song'
 | 'music.album'
 | 'music.playlist'
 | 'music.radio_station'
 | 'video.movie'
 | 'video.episode'
 | 'video.tv_show'
 | 'video.other';

A Note on og:type and "product"

You might see older articles or some AI responses telling you to use product as a value for og:type. However, as we stand in 2025, product is not listed as an official type in the Open Graph documentation. As far as I can tell, the major social platforms do not recognize it either.

For e-commerce product pages, the correct type to use is website. The product value feels like a legacy from the past, created by Facebook at one point and then dropped in favor of the official OG specification.

Combining Types into SeoData

With our types defined, it's time to aggregate them. We'll build a SeoData type which acts as the all-encompassing model for the page's SEO and OG information.

seo-data.ts

import type { OgImage } from './og-tags/og-image';
import type { OgType } from './og-tags/og-type';

export type SeoData = {
 title?: string;
 description?: string;
 ogImage?: OgImage;
 ogType?: OgType;
 ogUrl?: string;
 // Other SEO properties...
};

Developing the Angular Seo Service

Now for the core logic. We'll put all our API logic into an injectable SeoService that manages the document's meta tags.

seo.service.ts

import { DOCUMENT, inject, Injectable } from '@angular/core';
import { Meta } from '@angular/platform-browser';
import { SeoData } from './seo-data.model';

interface ImageParams {
 width: number;
 height: number;
 // See: https://unsplash.com/documentation#supported-parameters
 // All possible types for Unsplash image parameters:
 format: 'jpg' | 'png' | 'webp';
 fit: 'clip' | 'crop' | 'fill' | 'facearea' | 'fit' | 'scale';
}

/*
 Open Graph image requirements:
 - size: 1200x630
 - format: jpg or png
*/
export const OG_IMAGE_PARAMS: ImageParams = {
 width: 1200,
 height: 630,
 format: 'jpg',
 fit: 'crop',
};

@Injectable({ providedIn: 'root' })
export class SeoService {
 // Other SEO logic...
 private readonly _metaService = inject(Meta);

 setSeoData(seoData: SeoData): void {
   const title = seoData.title ? `${seoData.title} | SSRmart` : 'SSRmart';

   // Other SEO logic...
   this._updateMetaTag('og:locale', 'en_US');
   this._updateMetaTag('og:site_name', 'SSRmart');
   this._updateMetaTag('og:title', title);
   this._updateMetaTag('og:description', seoData.description);

   this._updateMetaTag(
     'og:image',
     this._getImageParamsUrl(seoData.ogImage?.url, OG_IMAGE_PARAMS)
   );
   this._updateMetaTag('og:image:width', '1200');
   this._updateMetaTag('og:image:height', '630');
   this._updateMetaTag('og:image:type', 'image/jpeg');
   this._updateMetaTag('og:image:alt', seoData.ogImage?.alt);
   this._updateMetaTag('og:url', seoData.ogUrl);
   this._updateMetaTag('og:type', seoData.ogType);

   // Other SEO logic...
 }

 private _updateMetaTag(name: string, content?: string): void {
   if (!content) {
     this._metaService.removeTag(`property="${name}"`);
     return;
   }

   if (this._metaService.getTag(`property="${name}"`)) {
     this._metaService.updateTag({ property: name, content });
   } else {
     this._metaService.addTag({ property: name, content });
   }
 }

 private _getImageParamsUrl(
   imageUrl: string | undefined,
   imageParams: ImageParams
 ): string | undefined {
   if (!imageUrl) return undefined;

   const url = new URL(imageUrl);
   url.search = '';
   url.searchParams.set('w', imageParams.width.toString());
   url.searchParams.set('h', imageParams.height.toString());
   url.searchParams.set('fm', imageParams.format);
   url.searchParams.set('fit', imageParams.fit);

   return url.toString();
 }
}

1. Image Settings

At the file's start, I've declared an ImageParams interface and an OG_IMAGE_PARAMS constant. This constant represents the preferred image settings for Open Graph (1200x630 jpg).

2. Under-the-Hood Helpers

_updateMetaTag() is our utility that handles the document's <head>. It leverages Angular's built-in Meta service to add, update, or remove tags without creating duplicates in the HTML.

Then, _getImageParamsUrl() calculates the final URL. It takes a base image URL and an ImageParams object to craft the correctly formatted URL for Unsplash. The logic here is specific to the Unsplash API, but keeping it isolated in this method makes it easier to swap in another CDN if needed. Note: the images used in my app all come from Unsplash's CDN.

3. The Public Interface: setSeoData()

This is the main method the Application will call. It handles the SeoData object we defined, generating the proper OG tags in the page's <head>. It also takes care of default values for things like og:site_name and og:locale.

Implementing the SeoService for a Static Page

Let's see our SeoService in action on a HomePageComponent. Initially, we create a function that provides the static SEO configuration:

home-page-seo.ts

import { inject } from '@angular/core';
import { SeoData } from '@ssrmart/client/utils';
import { ConfigService } from '@ssrmart/shared/config';

export const getHomePageSeo = (): SeoData => {
 const baseUrl = inject(ConfigService).get('baseUrl');

 return {
   title: 'Welcome to SSRmart - Your Online Shopping Destination',
   description:
     'Discover amazing products at great prices. Shop the latest trends in electronics. Fast shipping and excellent customer service.',
   ogType: 'website',
   ogUrl: baseUrl,
   ogImage: {
     url: 'https://images.unsplash.com/photo-1498049794561-7780e7231661',
     alt: 'Desk with laptop, headphones, smartphone, and smartwatch',
   },
 };
};

We then use this function inside the HomePageComponent:

home-page.component.ts

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { SeoService } from '@ssrmart/client/utils';
import { getHomePageSeo } from './home-page-seo';

@Component({
 selector: 'ssrmart-home-page',
 templateUrl: './home-page.component.html',
 changeDetection: ChangeDetectionStrategy.OnPush,
 imports: [
   // Component imports...
 ],
})
export class HomePageComponent {
 // Component logic...

 constructor() {
   inject(SeoService).setSeoData(getHomePageSeo());
 }
}

For static pages, the HomePageComponent is a straightforward case. With dynamic pages, like a product or an article, you need to get the data first before setting the OG tags. A route resolver or a reactive effect are the best methods to employ here. Continue on for that implementation guide.

To level up this pattern even more, you could build a custom injector function that takes SeoData as an argument. This function would then inject the SeoService and execute the setup. For a pattern reference, look at the injectNavigationEnd utility from the ngxtension platform.

Verifying Your OG Tags

With the core implementation complete, let's confirm everything is functioning as expected.

Manual HTML Inspection

First, we can check whether our tags are being rendered in the HTML response:

  • Right-click the page and choose View Page Source.
  • The HTML response appears in a new tab. Select all with ctrl + a, then copy using ctrl + c.
  • Paste the copied content into a fresh file in your code editor, switch the language mode to HTML, and format the document for readability.

For my application, the screenshot below shows correctly generated tags. Your HTML response should look similar.

<!DOCTYPE html>
<html lang="en">
 <head>
   <!-- Other head tags ... -->
   <meta property="og:locale" content="en_US" />
   <meta property="og:site_name" content="SSRmart" />
   <meta
     property="og:title"
     content="Welcome to SSRmart - Your Online Shopping Destination | SSRmart"
   />
   <meta
     property="og:description"
     content="Discover amazing products at great prices. Shop the latest trends in electronics. Fast shipping and excellent customer service."
   />
   <meta
     property="og:image"
     content="https://images.unsplash.com/photo-1498049794561-7780e7231661?w=1200&amp;h=630&amp;fm=jpg&amp;fit=crop"
   />
   <meta property="og:image:width" content="1200" />
   <meta property="og:image:height" content="630" />
   <meta property="og:image:type" content="image/jpeg" />
   <meta
     property="og:image:alt"
     content="Desk with laptop, headphones, smartphone, and smartwatch"
   />
   <meta property="og:url" content="https://ssrmart.vercel.app" />
   <meta property="og:type" content="website" />
   <!-- Styles ... -->
 </head>
 <body class="mat-typography">
   <!-- Body Content ... -->
 </body>
</html>

Validating Angular SSR Output with Online Tools

Manual inspection doesn’t scale well when you have many pages to validate. Fortunately, several platforms offer complimentary tools that crawl your live URL and display the exact preview they’ll generate. These “post inspectors” are an excellent starting point for debugging social preview issues.

Useful tools include:

You could also try out the SEO META in 1 CLICK Chrome extension for on-the-fly checks.

Simply paste your page’s URL into one of these tools. They’ll crawl your HTML response and flag any missing tags or incorrectly formatted images. If rich social media previews aren’t showing up, these validators should be your first stop in the debugging process.

LinkedIn Post Inspector

Here’s an example of the LinkedIn Post Inspector analyzing the SSRmart application’s Home Page:

a screenshot of the LinkedIn Post Inspector analyzing a SSRmart application Home Page

a screenshot of the LinkedIn Post Inspector analyzing a SSRmart application Home Page

One thing you’ll notice is that LinkedIn reports the page’s og:type as Article, even though we explicitly set it to website. This looks like a bug in LinkedIn’s crawler, as it often displays Article for nearly any page I test.

Aside from showing the validated data and redirect trail, the LinkedIn validator also suggests alternative titles, descriptions, or images we might use for our og tags.

Meta Post Inspector

Below are the results from the Meta Validator:

Meta validator results for an Angular project

Meta validator results for an Angular project

As demonstrated, the Facebook sharing debugger reports no errors for the SSRmart site.

Keep in mind: even when these validators give the green light, always test the preview yourself. The most reliable method is pasting your link into a draft message on platforms like LinkedIn, Discord, or Facebook. This way, you see exactly what users will encounter. For larger projects, consider automating this check whenever your code is updated; various services can catch issues before a release.

Additional Standard – Twitter Tags (Twitter Cards)

Ideally, we’d rely on a single standard for all social previews. However, that’s not the reality. Alongside OG tags, you should implement Twitter Tags, also called Twitter Cards.

X (formerly Twitter) uses these tags to create rich, enhanced card previews. An example of a large Twitter card is shown below.

A social media preview on twitter created with Angular

Adding Twitter Tags to HTML

Adding Twitter Tags is as straightforward as OG tags – just use <meta> tags with different property names.

The most important ones are:

  • twitter:site – the @username of the website on Twitter.
  • twitter:site:id – the id of that username. Note: you need either twitter:site or twitter:site:id – only one is required.
  • twitter:card – the card type. Available types are summary, summary_large_image, player, app.
  • twitter:title – the title displayed in the tweet preview.

  • twitter:description – a brief description shown under the title.
  • twitter:image – the URL for the preview image.
    • The recommended image size is 1200×600. Be careful: this is a 2:1 ratio, unlike the OG standard 1200×630. Using the same image for both tags may lead to cropping!
    • Minimum dimensions: 300×157
    • Maximum dimensions: 4096px x 4096px
    • Maximum image size is 5MB
  • twitter:image:alt – alt text for the image.

For a full list of optional Twitter tags, refer to X's developer documentation.

Below is an example of how these tags appear in the HTML response.

<!DOCTYPE html>
<html lang="en">
 <head>
   <!-- Other head tags ... -->
   <meta property="twitter:site" content="@ssrmart" />
   <meta property="twitter:card" content="summary_large_image" />
   <meta
     property="twitter:title"
     content="Welcome to SSRmart - Your Online Shopping Destination | SSRmart"
   />
   <meta
     property="twitter:description"
     content="Discover amazing products at great prices. Shop the latest trends in electronics. Fast shipping and excellent customer service."
   />
   <meta
     property="twitter:image"
     content="https://images.unsplash.com/photo-1498049794561-7780e7231661?w=1200&amp;h=600&amp;fm=jpg&amp;fit=crop"
   />
   <meta
     property="twitter:image:alt"
     content="Desk with laptop, headphones, smartphone, and smartwatch"
   />
   <!-- Styles ... -->
 </head>
 <body class="mat-typography">
   <!-- Body Content ... -->
 </body>
</html>

Twitter previously supported extra key-value pairs for its Product Card, which has since been retired. Even though X no longer uses these tags, rendering them is still useful – platforms like Slack employ them to build rich, two-column previews.

The tags work as pairs:

  • twitter:label1 – the label for the first row
  • twitter:data1 – the value for the first row
  • twitter:label2 – the label for the second row
  • twitter:data2 – the value for the second row

These tags are a great way to add context at a glance, such as Article Reading Time, Author Name, Product Price, or Product Availability.

Here’s how they look in HTML:

<!DOCTYPE html>
<html lang="en">
 <head>
   <!-- Other head tags ... -->
   <meta property="twitter:label1" content="Reading Time" />
   <meta property="twitter:data1" content="8 min" />
   <meta property="twitter:label2" content="Published At" />
   <meta property="twitter:data2" content="January 15, 2024" />
   <!-- Styles ... -->
 </head>
 <body class="mat-typography">
   <!-- Body Content ... -->
 </body>
</html>

This setup generates a neat two-column preview in Slack, as shown below.

A two-column preview in Slack

Rendering Twitter/X Tags in Angular via SSR

Now that we know the HTML for Twitter tags and their impact, we can integrate them into our SeoService.

Defining the Twitter Data Model

First, we’ll create a TwitterCardType to ensure strong typing.

twitter-card-type.ts

export type TwitterCardType =
 | 'summary'
 | 'summary_large_image'
 | 'app'
 | 'player';

Next, we extend the data model to include the new configuration properties.

seo-data.model.ts

import type { KeyValue } from '@angular/common';
import type { OgImage } from './og-tags/og-image';
import type { OgType } from './og-tags/og-type';
import type { TwitterCardType } from './twitter-tags/twitter-card-type';

export type SeoData = {
 title?: string;
 description?: string;
 ogImage?: OgImage;
 ogType?: OgType;
 ogUrl?: string;
 twitterCardType?: TwitterCardType;
 twitterLabel?: KeyValue<string, string>;
 twitterLabel2?: KeyValue<string, string>;

 // Other SEO properties...
};

Then, we update the SeoService to set these new tags.

import { DOCUMENT, inject, Injectable } from '@angular/core';
import { SeoData } from './seo-data.model';
import type { TwitterCardType } from './twitter-tags/twitter-card-type';
import { Meta } from '@angular/platform-browser';

interface ImageParams {
 width: number;
 height: number;
 // See: https://unsplash.com/documentation#supported-parameters
 // All possible types for Unsplash image parameters:
 format: 'jpg' | 'png' | 'webp';
 fit: 'clip' | 'crop' | 'fill' | 'facearea' | 'fit' | 'scale';
}

/*
 Open Graph image requirements:
 - size: 1200x630
 - format: jpg or png
*/
export const OG_IMAGE_PARAMS: ImageParams = {
 width: 1200,
 height: 630,
 format: 'jpg',
 fit: 'crop',
};

/*
 Twitter image requirements:
 - min size: 300x157, max size: 4096x4096
 - aspect ratio: 2:1
 - format: jpg, png, webp, gic
*/
export const TWITTER_IMAGE_PARAMS: ImageParams = {
 width: 1200,
 height: 600,
 format: 'jpg',
 fit: 'crop',
};

@Injectable({ providedIn: 'root' })
export class SeoService {
 // Other SEO logic...
 private readonly _metaService = inject(Meta);

 setSeoData(seoData: SeoData): void {
   const title = seoData.title ? `${seoData.title} | SSRmart` : 'SSRmart';

   // Other SEO logic...
   this._updateMetaTag('og:locale', 'en_US');
   this._updateMetaTag('og:site_name', 'SSRmart');
   this._updateMetaTag('og:title', title);
   this._updateMetaTag('og:description', seoData.description);

   this._updateMetaTag(
     'og:image',
     this._getImageParamsUrl(seoData.ogImage?.url, OG_IMAGE_PARAMS)
   );
   this._updateMetaTag('og:image:width', '1200');
   this._updateMetaTag('og:image:height', '630');
   this._updateMetaTag('og:image:type', 'image/jpeg');
   this._updateMetaTag('og:image:alt', seoData.ogImage?.alt);
   this._updateMetaTag('og:url', seoData.ogUrl);
   this._updateMetaTag('og:type', seoData.ogType);

   // The Twitter @username the card should be attributed to.
   this._updateMetaTag('twitter:site', '@ssrmart');
   const cardType: TwitterCardType =
     seoData.twitterCardType ?? 'summary_large_image';
   this._updateMetaTag('twitter:card', cardType);
   this._updateMetaTag('twitter:title', title);
   this._updateMetaTag('twitter:description', seoData.description);

   this._updateMetaTag(
     'twitter:image',
     this._getImageParamsUrl(seoData.ogImage?.url, TWITTER_IMAGE_PARAMS)
   );
   this._updateMetaTag('twitter:image:alt', seoData.ogImage?.alt);

   this._updateMetaTag('twitter:label1', seoData.twitterLabel?.key);
   this._updateMetaTag('twitter:data1', seoData.twitterLabel?.value);
   this._updateMetaTag('twitter:label2', seoData.twitterLabel2?.key);
   this._updateMetaTag('twitter:data2', seoData.twitterLabel2?.value);

   // Other SEO logic...
 }

 private _updateMetaTag(name: string, content?: string): void {
   if (!content) {
     this._metaService.removeTag(`property="${name}"`);
     return;
   }

   if (this._metaService.getTag(`property="${name}"`)) {
     this._metaService.updateTag({ property: name, content });
   } else {
     this._metaService.addTag({ property: name, content });
   }
 }

 private _getImageParamsUrl(
   imageUrl: string | undefined,
   imageParams: ImageParams
 ): string | undefined {
   if (!imageUrl) return undefined;

   const url = new URL(imageUrl);
   url.search = '';
   url.searchParams.set('w', imageParams.width.toString());
   url.searchParams.set('h', imageParams.height.toString());
   url.searchParams.set('fm', imageParams.format);
   url.searchParams.set('fit', imageParams.fit);

   return url.toString();
 }
}

For Twitter Cards, I introduced a TWITTER_IMAGE_PARAMS constant to handle Unsplash image settings for Twitter's distinguishing 2:1 aspect ratio.

The setSeoData method was also updated to assign all necessary Twitter tags. Most of the information was reused from our existing SEO configuration data model, making this a straightforward change.

Revising the Page’s SEO Configuration

Finally, we add the Twitter data to our page’s SEO configuration object.

import { inject } from '@angular/core';
import { SeoData } from '@ssrmart/client/utils';
import { ConfigService } from '@ssrmart/shared/config';

export const getHomePageSeo = (): SeoData => {
 const baseUrl = inject(ConfigService).get('baseUrl');

 return {
   title: 'Welcome to SSRmart - Your Online Shopping Destination',
   description:
     'Discover amazing products at great prices. Shop the latest trends in electronics. Fast shipping and excellent customer service.',
   ogType: 'website',
   ogUrl: baseUrl,
   ogImage: {
     url: 'https://images.unsplash.com/photo-1498049794561-7780e7231661',
     alt: 'Desk with laptop, headphones, smartphone, and smartwatch',
   },
   twitterCardType: 'summary_large_image',
   twitterLabel: {
     key: 'Contact Us',
     value: 'support@ssrmart.com',
   },
   twitterLabel2: {
     key: 'Follow Us',
     value: 'https://www.facebook.com/ssrmart',
   },
 };
};

Now, when our website is shared on Slack, the home page will display Contact Us and Follow Us labels.

The standard OG and Twitter tags we've covered suffice for most websites. However, Open Graph offers specialized “types” for specific content forms.

Advanced OG Metadata – Articles, Payments, Music, and Beyond

If your page is an article, book, or video/movie (instead of a generic website), a whole set of additional metadata becomes available. This lets you provide much richer details to social platforms and search engines.

For instance, setting og:type to article enables you to specify the exact publish time, author, and similar data:

<meta property="og:type" content="article" />
<meta property="article:published_time" content="2025-09-16T17:34:00" />
<meta property="article:author" content="https://angular.love/author/dawid" />

The table below outlines common advanced types and the special properties they unlock. The full documentation is available at https://ogp.me/#optional.

Top Level Property Metadata Properties
article published_time, modified_time, expiration_time, author, section, tag
payment description, currency, amount, expires_at, status, id, success_url
profile first_name. last_name, username, gender
book author, isbn, release_date, tag
music song, album, playlist
video.movie actor, actor:role, director, writer, duration, release-date, tag
video.episode actor, actor:role, director, writer, duration, release_date, tag, series

Implementing every scenario would be extensive, so let’s concentrate on a common one: the article page. This example demonstrates adding optional OG metadata using an effect to set article tags dynamically from an API response.

Implementing Specific OG Metadata in Angular

Let’s walk through a practical example of adding advanced OG tags to an article page.

Defining the Article Data Model

We’ll start by defining the data model. I’ll reuse my existing Article API model, as it already includes properties like publishedAt, author, and tags.

article.model.ts

export type ArticleImage = {
 url: string;
 alt: string;
};

export type Article = {
 id: string;
 title: string;
 excerpt: string;
 content: string;
 image: ArticleImage;
 author: string;
 publishedAt: string;
 modifiedAt?: string;
 expirationTime?: string;
 category: string;
 tags: string[];
 readTime: number; // in minutes
};

Extending SeoService for Articles

Next, we open seo.service.ts and add a new public method, setArticleMetadata, which handles these article-specific properties.

seo.service.ts

import { inject, Injectable } from '@angular/core';
import { Meta } from '@angular/platform-browser';
import { Article } from '@ssrmart/shared/types';

// Image params configuration

@Injectable({ providedIn: 'root' })
export class SeoService {
 private readonly _metaService = inject(Meta);
 // Other SEO logic

 setArticleMetadata(article: Article): void {
   this._updateMetaTag('article:published_time', article.publishedAt);

   if (article.modifiedAt) {
     this._updateMetaTag('article:modified_time', article.modifiedAt);
   }

   if (article.expirationTime) {
     this._updateMetaTag('article:expiration_time', article.expirationTime);
   }

   this._updateMetaTag('article:author', article.author);
   this._updateMetaTag('article:section', article.category);

   // Set article tags
   article.tags.forEach((tag) => {
     this._updateMetaTag('article:tag', tag);
   });
 }

 private _updateMetaTag(name: string, content?: string): void {
   if (!content) {
     this._metaService.removeTag(`property="${name}"`);
     return;
   }

   if (this._metaService.getTag(`property="${name}"`)) {
     this._metaService.updateTag({ property: name, content });
   } else {
     this._metaService.addTag({ property: name, content });
   }
 }
}

Fetching Data with a Route Resolver

Let’s also set up a route resolver to fetch the article data.

article.resolver.ts

export const articleResolver: ResolveFn<Article> = (
 route: ActivatedRouteSnapshot
) => {
 const router = inject(Router);

 return inject(ArticleService)
   .getArticle(route.params['id'])
   .pipe(
     catchError(() => of(new RedirectCommand(router.createUrlTree(['/blog']))))
   );
};

With the resolver created, we add it to the resolve block of our article route configuration.

routes.ts

import { Routes } from '@angular/router';
import {
 articleResolver,
 articleSeoResolver,
} from '@ssrmart/client/data-access';

export const ROUTES: Routes = [
 // Other routes
 {
   path: ':id',
   loadComponent: () =>
     import('@ssrmart/client/feature-article-page').then(
       (m) => m.ArticlePageComponent
     ),
   resolve: {
     article: articleResolver,
     seo: articleSeoResolver,
   },
 },
];

Applying Tags in the Component

Finally, in ArticlePageComponent, we inject SeoService and leverage an effect to pass the article data to our new setArticleMetadata method.

article-page.ts

import { DatePipe, NgOptimizedImage } from '@angular/common';
import {
 ChangeDetectionStrategy,
 Component,
 effect,
 inject,
 input,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { RouterLink } from '@angular/router';
import { ImageSizePipe, SeoService } from '@ssrmart/client/utils';
import { Article } from '@ssrmart/shared/types';

@Component({
 selector: 'ssrmart-article-page',
 imports: [
   NgOptimizedImage,
   DatePipe,
   ImageSizePipe,
   MatButtonModule,
   MatCardModule,
   MatChipsModule,
   RouterLink,
 ],
 templateUrl: './article-page.component.html',
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArticlePageComponent {
 private readonly _seoService = inject(SeoService);

 readonly article = input<Article>(); // resolver binding

 constructor() {
   effect(() => {
     const article = this.article();
     if (article) {
       this._seoService.setArticleMetadata(article);
     }
   });
 }
}

That’s all it takes to implement specific OG tags for our article page! Inspecting the HTML response should now reveal the new article:* tags are rendered:

<meta property="article:published_time" content="2024-01-15T10:00:00Z" />
<meta property="article:modified_time" content="2024-01-15T10:00:00Z" />
<meta property="article:author" content="Sarah Johnson" />
<meta property="article:section" content="Audio" />
<meta property="article:tag" content="2024" />

As a final note, extracting the article-specific SEO logic into a dedicated ArticleSeoService is a worthwhile refactor. It keeps the main SeoService clean and improves tree-shaking in the production build.

Wrapping Up

Integrating Open Graph tags and Twitter Card metadata is a straightforward yet impactful method for enhancing your site’s social footprint. The initial configuration—especially when you need to consolidate dynamic data from multiple API calls—can be a bit tedious, but the payoff is substantial. You’ll end up with polished, credible link previews that are far more likely to earn clicks across social networks and chat apps.

This guide should give you a solid foundation without sending you down a research rabbit hole. If you’re stuck on creative ideas for your og:image assets, check out the showcase at Open Graph Examples—it’s packed with excellent visual references.

Got questions or run into any snags? Don’t hesitate to leave a comment below.

For your convenience, here is the complete GitHub repository and, for a direct look at the logic, the SeoService implementation.

References & Official Documentation