Before You Continue
This guide focuses on creating dynamic meta tags, a technique that requires a basic understanding of Angular SSR (Server-Side Rendering) or SSG (Static Site Generation).
While you can implement the angular code from this article, it won’t improve your application’s SEO in drastic ways or add social media previews unless you are using Angular SSR or Angular SSG. That’s because the Angular SSR is required to generate unique meta tags based on the page content.
If you’re new to Server-Side Rendering in Angular, I recommend starting with Miłosz’s complete guide to Angular SSR first.
How Meta and OG Tags affect Angular SEO
Gaining traffic from social media and messaging apps is crucial for many online businesses, including e-commerce platforms, news outlets, blogs, wikis, content libraries and many more. For this kind of app, when a link to your application is shared, you want it to stand out. Therefore, controlling the link preview’s title, description, and image is an absolute necessity to maximize CTR (Click-Through Rate).
Using Open Graph (OG) meta tags is the gold standard for making shared links more engaging. Before we continue, take a look at the examples below from different social media platforms and messaging apps to see how these tags can enhance your link previews with images and additional meta information.
X (Twitter)
In each example, you’re seeing a simple link get a major upgrade. When a URL is shared, platforms like Facebook and LinkedIn automatically crawl that page for key information. They use this to generate a rich preview card with a large image, title, and description. This makes the entire post far more engaging and results in it taking up more space on the screen.
Now that you’ve seen the benefits of OG tags, let’s dive into the Open Graph standard and learn how to implement them.
What are OG Tags?
The Open Graph protocol enables any webpage to become a rich object within a social graph. This is the official definition.
In simple terms, OG tags are snippets of HTML <meta> code placed in the <head> of your webpage. They provide structured data to social media platforms, search engines, and messaging apps, defining key meta elements of the page. Thanks to OG tags your shared link appears informative and visually engaging, rather than just a plain URL.
Let’s see an example HTML implementation of the OG tags.
OG Tags in Action: The HTML Implementation
On a basic level, adding OG tags to a page means adding a few special <meta> tags to its HTML. Each tag uses property and content attributes to specify a piece of information, such as the title, description, or preview image.
Let’s look at the OG tags on an angular.love article as an example. You can find this information yourself by right-clicking on the page and selecting the „View Page Source” option.
<!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>
While there are many tags you can see, almost every social media rich preview is built with these core properties:
og:title– The headline of your content.og:description– A short, compelling summary of the page (usually 1-2 sentences).og:type– The type of content, such as website, article, or book. See docs.og:url– The permanent, unique URL of the page.og:image– The URL for the preview image. This is the most critical tag for grabbing attention!
These core tags create a rich preview for your page. However, there isn’t a single validation standard; each social media platform has its own rules for displaying information. For this reason keep your titles and descriptions concise.
For the preview image, follow these key guidelines for the best results:
Size: The preferred size is1200x630pixels. Using a different aspect ratio can cause your image to be awkwardly cropped – you don’t want that.Format: Stick to JPG or PNG file types for the best compatibility across all platforms.File Size: Keep the image’s file size small. A large file may fail to appear in the preview at all.
Implementing those 5 meta tags is enough to get a rich preview on most platforms. But does that mean you’ve fully tapped into the power of the Open Graph standard? Not yet!
Now, let’s explore the optional tags that are certainly useful for web crawlers.
Optional OG Metadata Properties
While the adding optional tags aren’t strictly required, they are highly recommended. They provide crawlers with extra context.
Site Information
og:site_name– the name of your overall website (e.g., „Angular.love”)og:locale– defines the locale (language and territory) of your content. It follows the language_TERRITORY format. The default is en_US.og:locale:alternate– An array of other locales this page is available in.
For example:
<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 Metadata
og:image:secure_url– HTTPS version of the image URL.og:image:type– MIME type of the image (e.g., image/jpeg, image/png)og:image:width– width of image in pxog:image:height– height of image in pxog:image:alt– description of what is in the image
For a full list of the OG tags see the Open Graph documentation.
Use Angular SSR to Implement OG Tags
Now that you know the HTML implementation, it’s time to bring this into Angular. A great implementation starts with a strongly typed foundation, so our first step is to define a clear data model.
Define Data Model
og-image.ts
export interface OgImage {
url: string;
alt: string;
}
For OG type, let’s leverage string union in order to get string autocompletion, prevent typos, and ensure our SeoService is always compatible with the official Open Graph standard.
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 Quick Note on og:type and „product”
You may have seen older articles or AI-generated responses suggest using og:type with a value of product. However, as of 2025, product value is not a defined type in the official Open Graph documentation and, as far as I know, not recognized by major social platforms.
For e-commerce pages or product listings, the recommended type is website. It’s best to consider the product type a thing of the past that was once created by Facebook and then abandoned in favor of official OG specification.
The Complete SeoData Type
Now that we’ve defined our core types, let’s bring them all together. Create a SeoData type that will serve as the complete type for all the SEO and OG tags information for the page.
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...
};
Building Angular Seo Service
With our data model ready, it’s time to build the engine that will manage our meta tags. We’ll encapsulate all of our business logic within an injectable SeoService.
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 Configuration
At the top of the file, I defined an ImageParams interface and an OG_IMAGE_PARAMS constant. The constant represents the ideal OG image settings (1200×630, jpg).
2. The Helper Methods
_updateMetaTag() – core utility for interacting with the document’s <head>. It uses Angular’s built-in Meta service to add, update, or remove a tag. This prevents duplicate meta tags from appearing in your HTML.
_getImageParamsUrl() – takes a base image URL and ImageParams object to construct a final, perfectly formatted unsplash image URL. In this example, the logic is tailored for the Unsplash API, but by keeping it in its own method, you could easily adapt it to any other image CDN. Note: in my app, all images come from unsplash image CDN.
3. The Public API: setSeoData()
This is the public API that the rest of our application will interact with. It accepts our strongly-typed SeoData object and sets proper OG tags in the document’s <head>. This method also handles setting default values for tags like og:site_name and og:locale.
Implementing the SeoService on a Static Page
Now that our SeoService is built, let’s put it to use on the HomePageComponent. First, we’ll define a function that returns our static SEO configuration object:
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',
},
};
};
Next, we’ll utilize this function within 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());
}
}
The HomePageComponent is a simple example for a static page. For dynamic pages, like a product or article, you’ll need to fetch data first, before setting OG tags. The best way to do this is with a route resolver or an effect. If you’re interested in that implementation, continue reading.
To further improve this code, you could create a custom util injector that accepts SeoData as a parameter. This injector would automatically inject the SeoService and set the data. For a pattern example, refer to the injectNavigationEnd utility from the ngxtension library.
How to See If Your OG Tags Are Working
As we finish our core code implementation, let’s test if everything works correctly.
Manual Inspection
Let’s start by inspecting if the tags are rendering in our HTML response. To do it:
- Right click on the page, click
View Page Sourceoption. - It’ll open the page with HTML response in a new tab. Copy it all by using
ctrl + aand thenctrl + c. - Paste previously copied code to a new untitled file in your IDE, select the HTML language mode, and format it for a better readability of the HTML response.
In the case of my application, as you can see below, the tags have been generated correctly. Your HTML response should be 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&h=630&fm=jpg&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>
Using Online Tools to Validate Angular SSR Response
As you can imagine, checking the HTML manually for every page isn’t efficient for validation of OG tags. Fortunately, there are many platforms that provide free tools to do this for you. These „post inspectors” will crawl your live page and show you exactly what preview they will generate.
Example web apps are:
- Official LinkedIn Post Validator
- Official Meta Post Validator
- Metatags.io – shows OG tag previews for all major platforms
You can also try inspecting the OG tags using the SEO META in 1 CLICK Chrome extension.
To validate your page just paste your page’s URL into the tool. They will validate your HTML response and provide errors if a tag is missing or an image is formatted incorrectly. These validators should be the first step of debugging why a rich social media preview of your page isn’t working.
Linkedin Post Inspector
Here’s a screenshot of the LinkedIn Post Inspector analyzing our SSRmart application Home Page:
You might notice the LinkedIn inspector reports the page og:type as Article, even though we explicitly set it to website. This appears to be a bug in LinkedIn’s crawler – at the time of writing, it displays Article type for almost any page I try to crawl.
In addition to showing validated data and the redirect trail, the linkedin post validator provides suggestions for alternate titles, descriptions, or images found that we can use in the og tags.
Meta Post Inspector
Here are the results from the Meta Validator:
As you can see, the facebook sharing debugger doesn’t indicate any error for the SSRmart site.
Note: even after the tools give you the green light, always test the social media preview yourself. The only way to be 100% sure is to copy your link and paste it directly into a draft post or message on LinkedIn, Discord, or Facebook. This lets you see exactly what your users will see before you share it widely.
For bigger projects, you can even automate this process. There are services that can automatically check your OG tags every time you update your code, catching any issues before your changes go live.
Additional Tags – Twitter Tags Standard
In a perfect world, we’d only have one standard for social previews. But, as you may expect, that’s not the case. Alongside OG standard, you also need to implement Twitter Tags (also known as Twitter Cards).
These tags are used by X (formerly Twitter) to generate its own rich, enhanced card previews. You can see an example of a large twitter card below.
Twitter Tags Implementation in HTML
Implementing Twitter Tags in HTML is just as simple as implementing the OG tags – you use <meta> tags with different property names.
Here are the most important ones:
twitter:site– twitter @username of website.twitter:site:id– id of the username. Note: either twitter:site or twitter:site:id is required – just implement onetwitter:card– This defines the card type. The available values are summary, summary_large_image, player, app.twitter:title– The title shown in the tweet preview
twitter:description– A short description that appears under the titletwitter:image– The URL for the preview image- The recommended image size is 1200×600. Be careful: it’s a 2:1 aspect ratio compared to the OG image standard 1200×630 image. If you reuse the same image for both tags, the content may be cropped!
- Min dimensions: 300×157
- Max dimensions: 4096px x 4096px
- Max image size is 5MB
twitter:image:alt– Alt text for image
For a list of other available twitter tags, see X documentation for developers.
Below, you can see an example of how the tags may look like in 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&h=600&fm=jpg&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>
Adding Slack Rich Link Preview
In the past, Twitter supported extra key-value tags for its now-retired Product Card. While X no longer uses these tags, rendering them is still valuable because platforms like Slack use them to create rich, two-column previews.
The tags work in pairs:
twitter:label1– The label for the first rowtwitter:data1– The value for the first rowtwitter:label2– The label for the second rowtwitter:data2– The value for the second row
This is a great way to add extra context at a glance. Common examples include: Article Reading Time, Author Name, Product Price and Product Availability (e.g., „In Stock”).
Here’s how you’d implement them in your 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 creates a clean, two-column preview in Slack, as you can see below:
Use SSR to render Twitter/X Tags in Angular
Now that we understand the HTML for Twitter tags and their impact on previews, let’s integrate them into our SeoService.
Define the Twitter Data Model
First, we create TwitterCardType to have a strongly typed implementation
twitter-card-type.ts
export type TwitterCardType =
| 'summary'
| 'summary_large_image'
| 'app'
| 'player';
Then, we’ll extend our data model with 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...
};
Next, we’ll update the SeoService logic 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();
}
}
To support Twitter Cards, I created a new TWITTER_IMAGE_PARAMS constant, which handles unsplash image configuration of Twitter’s unique 2:1 image aspect ratio.
I also updated the setSeoData method to set all the necessary twitter tags. This was a straightforward change, as we could reuse most of the information from our existing SEO configuration data model.
Update the Page’s SEO Configuration
Finally, let’s update our page’s SEO configuration object with the Twitter data.
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 our home page will display the Contact Us and Follow Us labels while our website is shared on slack.
For most websites, the standard OG and Twitter tags we’ve covered are all you need. However, the Open Graph protocol offers powerful, specific „types” for certain kinds of content.
Advanced OG Metadata for Payments, Articles, Music and More
If your page isn’t just a generic website but is an article, book, or video/movie, you can unlock a whole new set of metadata properties which you can define to boost SEO. This allows you to provide much richer, more detailed information to social platforms and search engines.
For example, if you set your og:type to article, you can then specify its exact publish time, author, and more:
<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" />
Here is a quick reference table for the most common advanced types and the special properties they unlock. For full documentation see 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 |
It would be a lot to implement every scenario, so let’s focus on a common one: the article page. This is the perfect example to show how to add optional OG metadata. In this example we’ll also use an effect to set the article tags dynamically, based on the received API response.
Specific OG Metadata Implementation in Angular
Let’s walk through a practical example of implementing advanced OG tags for an article page.
Define the Article Data Model
First, let’s define our data model. In this case, I’ll reuse my existing Article API model, as it already contains all the properties we need, 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
};
Extend the SeoService for Articles
Next, we’ll open seo.service.ts and add a new public method, setArticleMetadata, to handle these new 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 create a route resolver for fetching 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, let’s add it to the resolve block of our article page’s 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 the ArticlePageComponent, we inject the SeoService and use 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);
}
});
}
}
And just like that, we’ve implemented specific OG tags for our article page! If we inspect the HTML response, we’ll now see 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 for implementation, it would be valuable to refactor this article related SEO logic into a separate ArticleSeoService. This would keep your main SeoService clean and make your code more tree-shakable for production build.
Summary
Adding OG tags and Twitter Cards is a powerful way to improve your application’s presence on social media. While it takes some effort to set up (especially with aggregating all dynamic data from an API) it’s worth it. This will benefit your app with richer, more professional-looking links that can significantly boost your click-through rates on social and messaging platforms.
I hope this guide saved you hours of research and gave you a clear path forward. If you’re looking for inspiration on what to put in your og:image tags, see Open Graph Examples, as they have a fantastic gallery of various examples.
If you have any questions, feel free to drop them in the comments below!
Here’s a link to the GitHub repository and SeoService implementation.









