How to Add Open Graph Tags to an Angular App
Add route-specific Open Graph tags with Angular’s Meta service, choose CSR, SSR, or prerendering, and verify what sharing crawlers can fetch.
To add Open Graph tags to an Angular app, use Angular’s Meta service to set og:title, og:type, og:image, and og:url for each route. For route-specific sharing previews, those values must also appear in the HTML a sharing crawler can fetch. A client-side DOM update can look correct in a browser without guaranteeing the crawler receives it.
This guide, “How to add Open Graph tags to an Angular app,” covers the metadata fields, a reusable route-level implementation, rendering choices, validation, and common failures.
1. Know which Open Graph tags to set
The Open Graph protocol defines four required properties. Use an absolute, publicly fetchable URL for the canonical route and image. Add a useful description and image alternative text where they help readers and consumers.
| Tag | Purpose | Guidance |
|---|---|---|
og:title |
The page or object title. | Use the route-specific title you want represented in the preview. |
og:type |
The kind of object. | Use an appropriate protocol type; website is a reasonable default for ordinary site pages. |
og:image |
The preview image URL. | Use an absolute URL that the crawler can fetch without a login. |
og:url |
The canonical URL and permanent graph ID. | Use the intended canonical route, not a transient or unrelated URL. |
og:description |
A short page description. | Optional in the protocol and generally recommended. |
og:site_name |
The name of the overall site. | Optional; often shared across routes. |
og:locale |
The page locale. | Optional; set it when relevant to your content. |
og:image:alt |
A description of the image’s content. | Describe the image itself rather than using this field as a caption. |
Open Graph permits repeated values for array-like properties, and consumers may prefer the first value when values conflict. For a page’s title and canonical URL, avoid accidental duplicate tags: two conflicting values make the result dependent on consumer behavior. [Open Graph Protocol reference]
2. Add route-level metadata with Angular’s Meta service
Angular provides Meta in @angular/platform-browser. Its methods include addTag, addTags, getTag, getTags, updateTag, removeTag, and removeTagElement. A MetaDefinition maps its properties to HTML attributes: use property for Open Graph fields and content for each value. [Angular Meta API]
The following service updates existing tags when present and adds them otherwise. This avoids relying on a particular starter template to have already created every tag. It also updates the ordinary document title and description, which are separate from Open Graph metadata.
import { Injectable, inject } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';
@Injectable({ providedIn: 'root' })
export class SocialMetaService {
private readonly meta = inject(Meta);
private readonly title = inject(Title);
setPage(data: {
title: string;
description: string;
url: string;
image: string;
imageAlt?: string;
type?: string;
siteName?: string;
locale?: string;
}): void {
this.title.setTitle(data.title);
this.setTag({ name: 'description', content: data.description });
this.setTag({ property: 'og:title', content: data.title });
this.setTag({ property: 'og:type', content: data.type ?? 'website' });
this.setTag({ property: 'og:url', content: data.url });
this.setTag({ property: 'og:description', content: data.description });
this.setTag({ property: 'og:image', content: data.image });
if (data.imageAlt) {
this.setTag({ property: 'og:image:alt', content: data.imageAlt });
}
if (data.siteName) {
this.setTag({ property: 'og:site_name', content: data.siteName });
}
if (data.locale) {
this.setTag({ property: 'og:locale', content: data.locale });
}
}
private setTag(tag: { name?: string; property?: string; content: string }): void {
const key = tag.name ? `name="${tag.name}"` : `property="${tag.property}"`;
if (this.meta.getTag(key)) {
this.meta.updateTag(tag, key);
} else {
this.meta.addTag(tag);
}
}
}
Save the service in a file such as social-meta.service.ts, then call it from the component that owns the route data. For example:
import { Component, OnInit, inject } from '@angular/core';
import { SocialMetaService } from './social-meta.service';
@Component({
selector: 'app-article-page',
standalone: true,
template: '<article>Article content</article>',
})
export class ArticlePageComponent implements OnInit {
private readonly socialMeta = inject(SocialMetaService);
ngOnInit(): void {
this.socialMeta.setPage({
title: 'Example article | Example site',
description: 'A concise description of this article.',
url: 'https://example.com/articles/example',
image: 'https://example.com/assets/example-share.jpg',
imageAlt: 'A diagram illustrating the article topic',
type: 'article',
siteName: 'Example site',
});
}
}
Replace the example strings with values from your route or content model. Use the Angular injection and component patterns supported by your app version. If the route’s data arrives asynchronously, call setPage after it is available; otherwise the initial values may be empty or stale. Angular’s updateTag can locate an existing tag by its name or property when no selector is supplied, which is useful for updating a value as navigation changes. [Angular Meta API]
3. Choose CSR, SSR, or prerendering
Angular supports client-side rendering (CSR), server-side rendering (SSR), and build-time prerendering (SSG). The key question is when the route-specific metadata is available.
| Approach | When it fits | HTML returned | Tradeoff |
|---|---|---|---|
| CSR | Metadata is updated in the browser after the app runs. | The initial response may not contain route-specific values. | Simple browser runtime; client-side changes do not guarantee a scraper reading the response sees those values. |
| SSR | Route data needs to be resolved for each request. | Angular renders HTML for the request. | Per-request rendering can increase hosting costs and requires server-compatible code. |
| Prerendering (SSG) | Route data is known at build time. | Build output includes rendered static HTML. | Static files can be served from a CDN or static server; content must be available at build time and builds may take longer. |
If public route metadata is available during a build, prerender those routes. Choose SSR when each request needs fresh route data. A CSR-only app can still update tags for browser behavior, and a shared static head can describe pages that do not need route-specific metadata. But a browser’s hydrated Elements panel is not proof that the initial response contained the correct tags. Crawler JavaScript behavior varies, so verify the services that matter to your site rather than assuming all crawlers behave alike. [Angular server-side rendering guide] [Prerender.io Open Graph guide]
SSR and prerendered code must not strictly depend on browser-only APIs. Angular recommends injecting DOCUMENT for document access in SSR contexts and points developers to Meta for metadata operations. [Angular SSR guide] [Angular Meta API]
4. Validate the rendered metadata and share card
- Pick a representative deep route, not just the home page.
- Inspect the initial server response or prerendered HTML. Confirm it contains the route’s
property="og:..."elements with the expectedcontentvalues. Do not rely only on the browser’s post-hydration Elements panel. - Check that
og:urlis the intended canonical route and thatog:imageis an absolute, publicly accessible image URL. Confirm the image itself can be fetched without a logged-in session. - Use Facebook’s Sharing Debugger as one way to inspect fetched values and warnings. Recheck after edits; cached previews can make it hard to tell whether the current response has been picked up.
- Repeat for routes whose title, description, URL, or image differs. Check the resulting preview on the actual services your audience uses.
Metadata describes the page; each social platform controls how it renders and caches the preview. Correct tags help provide the intended information but cannot guarantee a particular card layout.
5. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser DOM is correct, but a share preview is missing route data. | The tags were added only after client-side JavaScript ran, while the fetcher read initial HTML or did not run the app as expected. | Inspect the initial response. Use SSR or prerendering when route metadata must be present before browser execution. |
| Every route shares the same title or image. | Metadata is static, or route data has not loaded when the update runs. | Call the metadata service with the resolved data for each route, and verify a deep route’s returned HTML. |
| Duplicate or conflicting tags appear. | Code adds tags on every navigation without checking for existing entries, or the document shell already contains tags. | Use updateTag for existing values and add only when absent. Inspect the response and remove stale duplicates from the shell or route logic. |
| Preview uses the wrong canonical page. | og:url points to the root, a redirect, a query variant, or a different canonical route. |
Set an absolute URL for the intended canonical route and compare it with the route’s canonical policy. |
| Preview image is missing. | The URL is relative, inaccessible to the fetcher, requires authentication, or does not return the intended image. | Use an absolute public image URL and check that the resource can be fetched directly. |
| Debugger or platform still shows old values. | A previously fetched preview may be cached. | Confirm current HTML first, then request a fresh fetch in the relevant debugger or platform workflow and allow for its cache behavior. |
SSR throws because of window or document. |
Server-rendered code uses browser globals unavailable during server rendering. | Remove the browser-only dependency from metadata handling; use Angular’s server-compatible services and inject DOCUMENT when document access is needed. |
6. Performance, reliability, and cost
Updating a few metadata fields is small compared with rendering a page, but the rendering strategy affects delivery. CSR adds no per-request Angular server rendering, though its initial HTML may not contain route-specific tags. SSR renders on each request and can increase hosting costs. Prerendered pages shift work to the build and can be served as static files through a CDN or static file server; their tradeoff is that route data must be available at build time and builds can take longer. [Angular SSR guide]
For reliability, derive title, description, canonical URL, and image from one route data source so they stay aligned. Ensure metadata is set for not-found and redirect routes too, where appropriate. Validate representative routes after deployment because generated output, routing, and public asset accessibility all affect what a fetcher sees. No metadata setup can force a platform to display a specific card or refresh it immediately.
7. Capture and inspect a route while debugging
A screenshot can help compare a rendered route with its expected appearance, but it cannot prove what metadata was present in the initial HTML. Inspect the response source for that. If you also need a visual record of the page, a browser automation setup can capture it, or a screenshot API can do so from a URL.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server by Yorker Media. It is useful for capturing and reviewing how a public route renders while you debug; use response HTML inspection separately to verify Open Graph tags. Its request and rendering options are documented at ScreenshotNeo docs.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/articles/example -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/articles/example"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/articles/example' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Cookie and consent banners are accepted like a visitor and removed before the shot, along with supported newsletter popups and chat widgets; each step can be turned off. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See ScreenshotNeo, then sign up for 1,000 free screenshots a month with no card.
FAQ
Do Open Graph tags replace the regular page title and description?
No. Set the document title and ordinary description separately when you want both browser/search metadata and Open Graph data.
Should I use relative URLs for og:image?
Use an absolute, publicly fetchable URL so a scraper can locate the image independently of the page context.
Will correct tags guarantee the same preview everywhere?
No. Platforms decide how to display and cache the metadata. Check the preview on the services relevant to your audience.
Can I use Angular’s Meta service in SSR?
Angular provides the service for metadata operations, but your surrounding code must also be server-compatible and avoid assumptions about browser globals.


