---
title: "Angular SEO Guide: SSR, Prerendering and Meta Tags"
url: https://encited.com/blog/angular-seo
description: "Is Angular SEO friendly? What a client-rendered Angular app sends crawlers, and how to fix it with @angular/ssr, prerendering, meta tags or no SSR at all."
lang: en
---

Image: Angular SEO: How to Make an Angular App SEO Friendly, With or Without SSR (https://encited.com/blog/thumbnails/angular-seo.webp)

# Angular SEO: How to Make an Angular App SEO Friendly, With or Without SSR

Oct 16, 2026 October 16, 2026 · Updated Oct 29, 2026 October 29, 2026 · by Aki from Encited

Is Angular SEO friendly? What a client-rendered Angular app sends crawlers, and how to fix it with @angular/ssr, prerendering, meta tags or no SSR at all.

If ChatGPT can't quote your Angular pages, or every link you share shows the same title, check the HTML your server sends when a crawler asks for a route. A default Angular CLI build sends every route the same `index.html`: an empty `<app-root>` tag, a script, and the title from `src/index.html`. Googlebot fills the page in when it renders it later. AI crawlers and link previews keep the empty shell. Angular is SEO friendly once crawlers get rendered HTML, and you can make an Angular app SEO friendly in three ways: server-side rendering with `@angular/ssr`, build-time prerendering, or a pre-render layer in front of the app, which is how you do Angular SEO without SSR. The framework-neutral side is in JavaScript SEO (https://encited.com/blog/javascript-seo), and the single-page app pitfalls that apply whatever you choose are in SPA SEO (https://encited.com/blog/spa-seo).

**TL;DR**

- A client-rendered Angular app sends crawlers `<app-root></app-root>` and one title for every route.
- Googlebot runs the scripts later. GPTBot, ClaudeBot, PerplexityBot and link previews don't run them.
- `@angular/ssr` replaced Angular Universal in Angular 17. Since Angular 19 you pick server rendering, prerendering or client rendering per route.
- Even with SSR, `@defer` blocks without a `hydrate` trigger and `RenderMode.Client` routes reach crawlers empty.
- Set titles with the route `title`, tags with `Meta.updateTag()`, and JSON-LD with a script in the head.
- Without SSR, a pre-render layer serves crawlers a snapshot of each finished page and leaves your Angular code alone.

## Is Angular SEO friendly?

Angular builds a client-rendered app unless you add server rendering. Angular's own docs say client-side rendering may hurt SEO "as search crawlers have limits to how much JavaScript they execute when indexing a page."

A crawler that asks for any route of a client-rendered app gets the same `index.html`. Apart from the script and stylesheet tags, its content is two lines:

html

Copy Download

< title > YourStore </ title >

< app-root > </ app-root >

`/`, `/products/blue-mug` and `/about` all return it: one title, no description, no canonical, no links, no text. The titles and meta tags your components set appear only after `main.js` runs.

What happens next depends on the crawler:

- **Googlebot** queues the page and renders it later. Google's JavaScript SEO guide says the wait can be a few seconds or longer. Content from scripts or API calls that fail or time out is missing from what it indexes.
- **AI crawlers** such as GPTBot, ClaudeBot and PerplexityBot fetch the HTML and don't run the scripts, so they read the shell. Do AI crawlers render JavaScript (https://encited.com/blog/do-ai-crawlers-render-javascript) goes through each one.
- **Link previews** in Slack, LinkedIn, X and Facebook read the raw HTML too, so every shared link shows the `index.html` title.

React and Vue apps have the same problem. React SEO (https://encited.com/blog/react-seo) and Vue SEO (https://encited.com/blog/vue-seo) cover their fixes, and JavaScript SEO (https://encited.com/blog/javascript-seo) covers what else goes wrong on JavaScript sites.

## Angular SEO issues in a client-rendered SPA

- **One title for every route** until JavaScript runs. Route titles and the `Meta` service change the page in the browser only.
- **Canonical tags added by JavaScript.** Google's JavaScript SEO guide says the best place for the canonical is the HTML, and that JavaScript shouldn't change it to a different URL.
- **Soft 404s.** The `**` route shows your not-found page, but the server answers 200.
- **Hash URLs.** `withHashLocation()` puts every route after a `#`, which Google reads as one URL.
- **Links built from click handlers.** Crawlers follow `<a href>` elements, which `routerLink` renders. A `(click)` on a button or a `div` gives them nothing to follow.

The last three have framework fixes in dynamic rendering in React, Vue and Angular (https://encited.com/blog/dynamic-rendering-react-vue-angular), along with a table of which crawlers run JavaScript.

## How to make Angular SEO friendly: SSR, prerendering or no SSR

Choose per route, by who needs to read it and how often it changes. SSR and prerendering run your components before the HTML leaves your server, so the first response holds the text, links, titles and meta tags. Angular's docs rate both as excellent for SEO. Client rendering costs the least to host.

| Option | What crawlers get | What you run | Code changes |
| --- | --- | --- | --- |
| Client rendering (default) | The `<app-root>` shell. Googlebot renders it later | Any static host | None |
| SSR with `RenderMode.Server` | Full HTML on every request | A Node.js server, or another runtime that runs Angular | Browser-only code kept out of server paths |
| Prerendering with `RenderMode.Prerender` | Full HTML built at deploy time | A static host, or the SSR server | Same as SSR, plus the list of routes at build time |
| Encited (pre-render layer, no SSR) | A snapshot of the finished page, or Markdown for AI crawlers that ask | A DNS change or an edge snippet | None |

How that maps to a typical site:

- Product, listing and article pages whose data changes often: SSR, or a pre-render layer that refreshes those paths daily.
- Marketing pages, docs and a blog: prerendering.
- Account, checkout and dashboard routes: client rendering. Keep them out of your sitemap.
- An app built on browser-only libraries, a host with no Node.js runtime, or no time to refactor for hydration: a pre-render layer.

SSR vs SSG vs pre-rendering (https://encited.com/blog/ssr-vs-pre-rendering) compares the three approaches outside Angular.

## Angular SSR SEO: set up @angular/ssr and hydration

### Add server rendering

bash

Copy Download

ng add @angular/ssr # an existing app

ng new your-store --ssr # a new app

The schematic adds `src/main.server.ts`, an Express server in `src/server.ts`, `app.config.server.ts`, the server route config `app.routes.server.ts`, and `provideClientHydration()` in `app.config.ts`. One `ng build` then builds the browser bundle, the server bundle and the prerendered pages.

### Choose a render mode per route

The generated `app.routes.server.ts` prerenders every route (`path: '**'` with `RenderMode.Prerender`). Most apps need a mix:

ts

Copy Download

import { inject } from '@angular/core';

import { RenderMode, ServerRoute } from '@angular/ssr';

import { PostService } from './post.service';

export const serverRoutes: ServerRoute [ ] = [

{ path: '', renderMode: RenderMode. Prerender },

{

path: 'blog/:slug',

renderMode: RenderMode. Prerender,

async getPrerenderParams () {

const posts = inject (PostService);

const slugs = await posts. getSlugs ();

return slugs. map ((slug) => ({ slug }));

},

},

{ path: 'products/:id', renderMode: RenderMode. Server },

{ path: 'account/**', renderMode: RenderMode. Client },

{ path: '**', renderMode: RenderMode. Server },

];

- A prerendered route with a parameter fails the build until it has `getPrerenderParams`. Call `inject()` before the first `await`, since Angular requires it to run synchronously there.
- `RenderMode.Client` routes still send the empty shell, even from an SSR server.

### Hydration

`provideClientHydration()` lets the browser reuse the server's HTML. Without it, Angular discards the server DOM and renders the page again, which Angular's docs say can cause a visible flicker and hurt Core Web Vitals. Add `withI18nSupport()` only if you use Angular's i18n; without it, Angular skips hydration for i18n blocks.

Hydration needs the same DOM on the server and in the browser. Components that edit their own DOM with `innerHTML` or `appendChild`, invalid HTML such as a `<div>` inside a `<p>`, and third-party scripts that change the page early cause mismatch errors. Angular DevTools highlights the component at fault, and `ngSkipHydration` on its host is the documented last resort.

### Keep deferred content in the server HTML

On the server, a `@defer` block renders only its `@placeholder`, in SSR and prerendering alike. Reviews, specs or FAQs inside a plain `@defer` never reach the HTML crawlers read.

To see whether that happens on your site, run a product route through the Googlebot simulator (https://encited.com/free-tools/crawler-simulator/googlebot-simulator) and search the response for a sentence from the reviews.

Add a `hydrate` trigger and Angular renders the main content on the server, then hydrates it in the browser when the trigger fires:

html

Copy Download

@defer (hydrate on viewport) {

< app-product-reviews [productId] = " id() " />

} @placeholder {

< p > Customer reviews </ p >

}

`hydrate` triggers need incremental hydration, which `provideClientHydration()` turns on by default since Angular 22. On Angular 19 to 21, add `withIncrementalHydration()`.

### Browser-only code during server rendering

Angular's docs list `window`, `document`, `navigator` and `location` as unavailable on the server. Constructors and class hooks such as `ngOnInit` and `ngOnChanges` run during server rendering too, so code in them has to work on both sides. Angular skips only the render callbacks, `afterNextRender` and `afterEveryRender`, on the server. Put browser-only code there.

New apps have been zoneless by default since Angular 21. In a zoneless app, the server waits for router navigations and `HttpClient` requests before it sends the page. Data from `fetch()` or a client SDK such as Firebase's needs `PendingTasks`, or the server sends the page before the data arrives:

ts

Copy Download

import { Component, PendingTasks, afterNextRender, inject, signal } from '@angular/core';

@ Component ({

selector: 'app-store-hours',

template: ` <ul>@for (line of hours(); track line) { <li>{{ line }}</li> }</ul> `,

})

export class StoreHours {

private tasks = inject (PendingTasks);

hours = signal < string [ ] > ([ ]);

constructor () {

this. tasks. run (async () => {

const res = await fetch ('https://api.yourstore.com/hours');

this. hours. set (await res. json ());

});

afterNextRender (() => {

const preferred = localStorage. getItem ('preferredStore');

// browser only: personalize after the page is on screen

});

}

}

Don't branch a template on `isPlatformBrowser` to show different content on the server. Angular's docs warn it causes hydration mismatches and layout shifts.

### Return a real 404

ts

Copy Download

import { Component, RESPONSE_INIT, inject } from '@angular/core';

@ Component ({

selector: 'app-not-found',

template: ` <h1>Page not found</h1> `,

})

export class NotFound {

constructor () {

const response = inject (RESPONSE_INIT);

if (response) {

response. status = 404;

}

}

}

Put `{ path: '**', component: NotFound }` last in your routes and keep `**` on `RenderMode.Server`. `RESPONSE_INIT` is `null` during prerendering and in the browser, so only a server-rendered route can set the status. Redirects from `redirectTo` go out as real HTTP redirects from SSR.

### Angular Universal SEO: moving to @angular/ssr

Angular Universal served Angular 16 and earlier, with `ng add @nguniversal/express-engine` and scripts such as `npm run build:ssr` and `serve:ssr`. In Angular 17 it moved into the Angular CLI as `@angular/ssr`, and `ng update` to 17 included a migration from `@nguniversal`. Angular Universal SEO tutorials that use those commands target old versions. On Angular 17 or later, use `@angular/ssr`.

Angular 22 deprecated `CommonEngine` in favor of `AngularNodeAppEngine`. A `server.ts` that still imports `CommonEngine` predates the current server API.

## Build-time prerendering (SSG) in the Angular CLI

`RenderMode.Prerender` routes become HTML files at build time. Crawlers get the fastest response, and nothing runs per request. It fits pages that look the same for every visitor and change when you deploy.

For a site with no server at all, set `outputMode` to `static` in `angular.json`:

json

Copy Download

{

"projects": {

"your-store": {

"architect": {

"build": {

"options": {

"outputMode": "static"

}

}

}

}

}

}

Angular then writes one HTML file per prerendered route and no server bundle. Deploy `dist/your-store/browser` to Firebase Hosting, Netlify, Cloudflare Pages or S3. After each build, `dist/your-store/prerendered-routes.json` lists every route that got a file, so you can confirm new pages made it in.

Limits:

- Data is fixed at build time. A price or stock change waits for the next deploy.
- Angular's docs warn that prerendering many pages through `getPrerenderParams` adds build time and deploy size.
- Redirects on prerendered routes become `<meta http-equiv="refresh">` tags. Put permanent redirects in your host's config so crawlers get a 301.

## Angular SEO meta tags: titles, descriptions, canonicals and JSON-LD

With SSR, prerendering or a pre-render layer, your Angular SEO meta tags are in the HTML every crawler reads. In a client-rendered app, only Googlebot sees them, after rendering.

### Page titles

Set a `title` on each route, or a resolver when the title comes from data:

ts

Copy Download

import { inject } from '@angular/core';

import { ResolveFn, Routes } from '@angular/router';

import { ProductService } from './product.service';

const productTitle: ResolveFn < string > = (route) =>

inject (ProductService). getName (route. paramMap. get ('id')!);

export const routes: Routes = [

{ path: '', component: Home, title: 'Handmade mugs and bowls | YourStore' },

{ path: 'products/:id', component: ProductPage, title: productTitle },

];

To add your site name to every title in one place, provide a custom `TitleStrategy`.

### Description, Open Graph, canonical and JSON-LD

`Meta.updateTag()` updates a tag, or creates it when it's missing, so running it on the server and again in the browser leaves one tag. Canonical links have no Angular service, and Angular doesn't support `<script>` elements in templates, so both go in through the `DOCUMENT` token. Angular's SSR guide uses the same approach for canonicals.

ts

Copy Download

import { DOCUMENT, Injectable, inject } from '@angular/core';

import { Meta } from '@angular/platform-browser';

@ Injectable ({ providedIn: 'root' })

export class Seo {

private meta = inject (Meta);

private doc = inject (DOCUMENT);

setPage (page: { title: string; description: string; url: string; image: string }) {

this. meta. updateTag ({ name: 'description', content: page. description });

this. meta. updateTag ({ property: 'og:title', content: page. title });

this. meta. updateTag ({ property: 'og:description', content: page. description });

this. meta. updateTag ({ property: 'og:url', content: page. url });

this. meta. updateTag ({ property: 'og:image', content: page. image });

this. headElement ('link', 'canonical-link', { rel: 'canonical', href: page. url });

}

setJsonLd (data: object) {

const script = this. headElement ('script', 'page-jsonld', { type: 'application/ld+json' });

script. textContent = JSON. stringify (data). replace (/ < / g, '\\u003c');

}

private headElement (tag: string, id: string, attrs: Record < string, string >) {

let el = this. doc. getElementById (id);

if (! el) {

el = this. doc. createElement (tag);

el. id = id;

this. doc. head. appendChild (el);

}

for (const [ name, value ] of Object. entries (attrs)) el. setAttribute (name, value);

return el;

}

}

The fixed `id` lets the browser run find the server's tag and update it, so the page never ends up with two canonicals or two JSON-LD blocks. On Angular 19, import `DOCUMENT` from `@angular/common`.

Call it when the page's data arrives:

ts

Copy Download

this. products. get (id). subscribe ((p) => {

this. seo. setPage ({

title: p. name,

description: p. summary,

url: ` https://yourstore.com/products/ ${ p . id } `,

image: p. imageUrl,

});

this. seo. setJsonLd ({

'@context': 'https://schema.org',

'@type': 'Product',

name: p. name,

image: p. imageUrl,

offers: { '@type': 'Offer', price: p. price, priceCurrency: 'USD', availability: 'https://schema.org/InStock' },

});

});

Google's structured data docs say Product markup generated with JavaScript can make Shopping crawls less frequent and less reliable. On a store, get JSON-LD into the HTML the server sends.

## Angular SEO without SSR

Some apps stay client-rendered: they depend on browser-only libraries such as maps, charts and editors, their host has no Node.js runtime, or the team can't refactor for hydration yet. Smaller bundles, lazy routes with `loadComponent` and compressed images help visitors and Core Web Vitals. Crawlers that don't run JavaScript still get the shell.

A pre-render layer takes care of crawlers. It sits at your DNS, CDN or host edge and checks each request's user agent. Crawlers get a snapshot of the page, saved from a headless browser after Angular finished rendering. People get your app as before. Google calls this approach dynamic rendering, and dynamic rendering SEO (https://encited.com/blog/dynamic-rendering-seo) covers how Google treats it.

With Encited in front of your Angular app, each route reaches search engines, link previews and AI crawlers as finished HTML, and AI crawlers that ask for Markdown get Markdown. You set refresh rules per path, such as daily for products and monthly for the about page, and broken pages are never cached or served.

Setup is a DNS change (https://encited.com/docs/pre-rendering/setup-no-code-via-dns) or a snippet for your host: Netlify (https://encited.com/docs/domain-setup/setup-with-netlify-edge-functions), Vercel (https://encited.com/docs/domain-setup/setup-with-vercel), Cloudflare Workers (https://encited.com/docs/domain-setup/setup-with-cloudflare-workers) or Express (https://encited.com/docs/domain-setup/express-js), which also fits the `server.ts` that `@angular/ssr` generates. Plans start at $19 a month.

Encited waits for the network and the page to go quiet before it saves a snapshot. For routes with slow or chained API calls, set `window.prerenderReady = false` in `src/index.html`, so it's in place before Angular starts:

html

Copy Download

<!-- src/index.html, inside <head> -->

< script > window. prerenderReady = false; </ script >

Then set it to `true` once Angular is done. Encited waits up to 30 seconds for `true` before it saves the snapshot. `ApplicationRef.whenStable()` resolves when Angular has no pending work, which includes router navigations and `HttpClient` requests:

ts

Copy Download

// src/main.ts

bootstrapApplication (App, appConfig). then ((appRef) =>

appRef. whenStable (). then (() => Object. assign (window, { prerenderReady: true })),

);

For unknown routes, have the not-found component set `<meta name="prerender-status-code" content="404">`, and crawlers get a real 404. Real 404s in Angular (https://encited.com/blog/dynamic-rendering-react-vue-angular) shows where it goes.

A pre-render layer also works on an SSR app, for the parts SSR leaves out: `@defer` blocks without `hydrate` triggers and `RenderMode.Client` routes.

## How to check what Google and AI crawlers get

On an SSR or prerendered app, the root tag in the raw HTML records how Angular rendered the route:

bash

Copy Download

curl -s https://yourstore.com/products/blue-mug | grep -o 'ng-server-context="[a-z]*"'

`ssr` means server-rendered and `ssg` means prerendered. No match means the route went out as the client shell. Then:

1. **Check the head.** In the same raw HTML, each route needs its own `<title>`, description, canonical and JSON-LD. If every route shows the `index.html` title, the tags are set in the browser only.
2. **Check deferred and late content.** Search the raw HTML for a sentence from a `@defer` block, or from data the page loads after an API call.
3. **Compare crawlers.** Run the route through the Googlebot simulator (https://encited.com/free-tools/crawler-simulator/googlebot-simulator) and the ChatGPT crawler simulator (https://encited.com/free-tools/crawler-simulator/chatgpt-crawler-simulator). URL Inspection in Search Console shows the HTML Googlebot rendered.
4. **Watch real visits.** Your crawl log (https://encited.com/docs/crawl-logs/read-your-crawl-log) shows which bots fetched each route and what they got back.
5. **Audit every route.** One missing title resolver leaves a whole section on the default title. A site audit (https://encited.com/site-audit) finds missing titles, duplicate descriptions and canonical problems across the site.

For a single route, the free SEO audit (https://encited.com/free-tools/seo-audit) runs the head checks from step 1 for you: title, description, canonical, schema and Open Graph.

## Angular 19 SEO and what changed through Angular 22

| Version | Released | What changed for SEO |
| --- | --- | --- |
| 17 | November 2023 | Angular Universal moved into the CLI as `@angular/ssr`. `ng new --ssr` added |
| 19 | November 2024 | Per-route render modes and `getPrerenderParams` in developer preview, the `outputMode` option, incremental hydration in developer preview |
| 20 | May 2025 | Server routing and incremental hydration stable. Routes registered with `provideServerRendering(withRoutes(...))` |
| 21 | November 2025 | New apps zoneless by default |
| 22 | June 2026 | `provideClientHydration()` turns on incremental hydration. `CommonEngine` deprecated |

Angular 19 SEO works the same way, with three differences: the server routing API is a developer preview, registered with `provideServerRouting(serverRoutes)` on 19.2 and `provideServerRoutesConfig(serverRoutes)` on 19.0 and 19.1; `DOCUMENT` comes from `@angular/common`; and `hydrate` triggers need `withIncrementalHydration()`. Angular's release page lists versions 2 through 19 as no longer supported, so an update to 20 or later also brings stable server routing and security fixes.

## FAQ

**Is Angular SEO friendly?** Yes, once crawlers get rendered HTML. A default client-rendered build sends every route an empty `<app-root>`, which Googlebot renders later and AI crawlers and link previews never render. `@angular/ssr`, build-time prerendering or a pre-render layer puts the content in the first response.

**Can you do Angular SEO without SSR?** Yes. Prerender routes that are the same for every visitor with `outputMode: "static"`, which needs server-compatible code but no server. Or put a pre-render layer in front of the client-rendered app, which needs neither.

**Which is better for SEO, Angular SSR or CSR?** SSR. Crawlers get the full page in the first response and don't depend on running your JavaScript. CSR suits pages behind a login that you don't want indexed, and Angular 19 and later let you mix both per route.

**What is the difference between Angular Universal and @angular/ssr?** `@angular/ssr` is Angular Universal's successor. Universal (`@nguniversal/*`) served Angular 16 and earlier. In Angular 17 it moved into the Angular CLI as `@angular/ssr`, set up with `ng add @angular/ssr` or `ng new --ssr`.

**How do I add meta tags in Angular?** Use the route `title` for titles and `Meta.updateTag()` from `@angular/platform-browser` for descriptions and Open Graph tags. Add canonical links and JSON-LD through the `DOCUMENT` token, with fixed ids so the server and browser runs don't duplicate them.

**Do @defer blocks hurt Angular SEO?** They can. During SSR and prerendering, a `@defer` block renders only its placeholder, so its content isn't in the HTML. A `hydrate` trigger such as `hydrate on viewport` renders it on the server.

**Is Angular end of life?** No. Angular 22 came out on June 3, 2026, with active support until June 2027 and long-term support until June 2028, according to Angular's release page. Versions 2 through 19 are no longer supported.

Image: Google (https://encited.com/crawler-logos/google-search.svg)

Image: Gemini (https://encited.com/crawler-logos/gemini.png)

Image: ChatGPT (https://encited.com/crawler-logos/chatgpt.svg)

Image: Claude (https://encited.com/crawler-logos/claude.svg)

Image: Grok (https://encited.com/crawler-logos/xai.svg)

Image: Meta (https://encited.com/crawler-logos/meta.svg)

Image: Perplexity (https://encited.com/crawler-logos/perplexity.png)

## Get discovered anywhere search happens

Readable, citable, outranking pages.

Talk to sales: https://encited.com/contact-sales

Image: Avatar (https://replymaven.com/api/uploads/Su6Q0nK63iZW0ZFJBRNWpmRuppVDLuab/98df5262-f992-45fa-a750-00a6c919baf9.png)

How can we help?

Get instant answers to your questions or leave a message for an engineer will reach out

Ask AI about Encited

See our docs (https://lovablehtml.com/docs)

Contact support

Leave a message

We'll get back to you soon

Ask AI about Encited

Product questions, troubleshooting and

Thinking

Image: Preview

Drop an image to attach

Powered by ReplyMaven (https://replymaven.com/)

## Structured data

```json
[
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      {
        "@type": "ListItem",
        "position": 1,
        "name": "Home",
        "item": "https://encited.com/"
      },
      {
        "@type": "ListItem",
        "position": 2,
        "name": "Blog",
        "item": "https://encited.com/blog"
      },
      {
        "@type": "ListItem",
        "position": 3,
        "name": "Angular SEO: How to Make an Angular App SEO Friendly, With or Without SSR",
        "item": "https://encited.com/blog/angular-seo"
      }
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "Angular SEO: How to Make an Angular App SEO Friendly, With or Without SSR",
    "description": "Is Angular SEO friendly? What a client-rendered Angular app sends crawlers, and how to fix it with @angular/ssr, prerendering, meta tags or no SSR at all.",
    "image": "https://encited.com/blog/thumbnails/angular-seo.webp",
    "datePublished": "2026-10-16",
    "dateModified": "2026-10-29",
    "author": {
      "@type": "Person",
      "name": "Aki from Encited"
    },
    "publisher": {
      "@type": "Organization",
      "name": "Encited.com",
      "alternateName": "LovableHTML.com",
      "logo": {
        "@type": "ImageObject",
        "url": "https://encited.com/brand/logo.svg"
      }
    },
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": "https://encited.com/blog/angular-seo"
    }
  }
]
```