npm package: @omnidyon/ngx-seo-meta
Description: Lightweight SEO meta management for Angular (standalone & router-aware).
Author: Slavko Mihajlovic
Latest version: 1.0.0

![npm version](https://img.shields.io/npm/v/@omnidyon/ngx-seo-meta)
![npm downloads](https://img.shields.io/npm/dm/@omnidyon/ngx-seo-meta)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
![types](https://img.shields.io/npm/types/@omnidyon/ngx-seo-meta)
![Angular](https://img.shields.io/badge/angular-19%2B-green)
![angular compatibility](https://img.shields.io/badge/angular-standalone%20%7C%20modules-green)

<p align="center">
  <img src="./imgs/ngx-seo-meta.svg" alt="ngx-seo-meta" width="400px" height="400px"/>
</p>

A lightweight, declarative SEO meta-tag management library for Angular **19+**.

It gives you:
- Path-based SEO, driven by a central config.
- Route-driven SEO, driven by `route.data.seo`.
- Automatic updates on every navigation.
- A single, focused service that applies `<title>` and `<meta>` tags.

Works with **standalone apps** and classic NgModule setups.

---

## Features

- ✅ Angular **19+** compatible
- ✅ Standalone & NgModule friendly
- ✅ Auto-update SEO on `NavigationEnd`
- ✅ Two parallel SEO systems:
  - **Path-based SEO** via `SEO_META_CONFIG`
  - **Route-based SEO** via `route.data.seo`
- ✅ Central `SeoMetaService` with:
  - `updateForPath(path: string)`
  - `applyRouteMeta(entry: SeoMetaDefinition)`
- ✅ Open Graph meta integration (documented via `SeoMetaDefinition`)
- 🚧 Roadmap for:
  - Social meta helpers (Twitter, richer OG)
  - JSON-LD helpers
  - Canonical + hreflang utilities
  - SSR-aware tweaks
  - Typed config builder & dev-time diagnostics

---

## Installation

```bash
npm install @omnidyon/ngx-seo-meta
```

Peer dependencies: Angular 19+.

---

## Core Concepts

`ngx-seo-meta` is intentionally simple:

- **One service**: `SeoMetaService`
- **One config token**: `SEO_META_CONFIG`
- **Two ways to supply SEO:**
  1. A central **path-based config**
  2. **Route metadata** using `route.data.seo`

The **service API is stable** and must not be broken:

```ts
class SeoMetaService {
  updateForPath(path: string): void;
  applyRouteMeta(entry: SeoMetaDefinition): void;
}
```

Internally it uses Angular's `Title` and `Meta` services and, if available, `Router` to react to navigation.

---

## SEO Meta Definition

All SEO definitions (config-based or route-based) share the same shape:

```ts
export interface SeoMetaDefinition {
  title: string;
  description: string;

  ogTitle?: string;
  ogDescription?: string;
  ogUrl?: string;

  ogImage?: string;
  ogType?: string;
  ogSiteName?: string;
  twitterTitle?: string;
  twitterDescription?: string;
  twitterImage?: string;
  twitterCard?: string;
  twitterSite?: string;
  twitterCreator?: string;
}
```

The library:
- Always sets `<title>` from `title`.
- Updates the `<meta name="description">`.
- Applies OG fields when present, with sensible fallbacks (e.g. `ogTitle ?? title`).

---

## Path-Based SEO (Config-Driven)

This is the **original** mechanism: map normalized URL paths to SEO definitions.

### 1. Define the config

```ts
// seo-config.ts
import type { SeoMetaConfig } from '@omnidyon/ngx-seo-meta';

export const SEO_CONFIG: SeoMetaConfig = {
  '/': {
    title: 'Home – My App',
    description: 'Welcome to my Angular application.',
    ogUrl: 'https://example.com/',
  },
  '/about': {
    title: 'About Us',
    description: 'Learn more about the team behind this project.',
  },
  '/blog': {
    title: 'Blog',
    description: 'Articles, tutorials and release notes.',
  },
};
```

`SeoMetaConfig` is just:

```ts
export type SeoMetaConfig = Record<string, SeoMetaDefinition>;
```

### 2. Provide the config

Using standalone bootstrap:

```ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { provideSeoMeta } from '@omnidyon/ngx-seo-meta';
import { SEO_CONFIG } from './seo-config';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideSeoMeta(SEO_CONFIG),
  ],
});
```

### 3. How it works at runtime

- `SeoMetaService` listens to `Router.events` (if `Router` is present).
- On every `NavigationEnd`:
  - It normalizes the URL:
    - strips query & hash
    - trims trailing slashes (except `/`)
  - Looks up the path in `SEO_CONFIG`.
  - Applies the `SeoMetaDefinition` via the internal `apply()` method.

You can also trigger it manually:

```ts
constructor(private readonly seo: SeoMetaService) {}

ngOnInit() {
  this.seo.updateForPath('/blog');
}
```

---

## Route-Driven SEO (data.seo)

This is the **new** mechanism: put SEO definitions directly on your routes.

### 1. Add `seo` to your route data

```ts
// app.routes.ts
import { Routes } from '@angular/router';
import type { SeoMetaDefinition } from '@omnidyon/ngx-seo-meta';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./home/home.component'),
    data: {
      seo: {
        title: 'Home – My App',
        description: 'Welcome to the homepage.',
      } satisfies SeoMetaDefinition,
    },
  },
  {
    path: 'blog/:slug',
    loadComponent: () => import('./blog/blog.component'),
    data: {
      seo: (route): SeoMetaDefinition => {
        const slug = route.paramMap.get('slug') ?? 'article';
        return {
          title: `Blog – ${slug}`,
          description: `Reading article: ${slug}`,
        };
      },
    },
  },
];
```

The `seo` data supports:

```ts
type RouteSeoMeta = SeoMetaDefinition;

type RouteSeoMetaConfig =
  | RouteSeoMeta
  | ((route: ActivatedRouteSnapshot) => RouteSeoMeta);
```

### 2. Enable route-driven SEO

```ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { provideSeoMetaRouting } from '@omnidyon/ngx-seo-meta';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideSeoMetaRouting(),
  ],
});
```

### 3. What `provideSeoMetaRouting()` does

- Uses `provideEnvironmentInitializer` (Angular 16+/19+ style).
- On app init:
  - Injects `EnvironmentInjector`.
  - Resolves `Router` and `SeoMetaService`.
  - Subscribes to `router.events`.
- On every `NavigationEnd`:
  - Walks to the deepest `ActivatedRouteSnapshot`.
  - Reads `data['seo']`.
  - If present:
    - If it's a function: calls it with the snapshot.
    - If it's an object: uses it directly.
    - Calls `seo.applyRouteMeta(definition)`.

### 4. Coexisting with path-based SEO

You can use both providers together:

```ts
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideSeoMeta(SEO_CONFIG),       // path-based
    provideSeoMetaRouting(),          // route-based
  ],
});
```

They do **not** override each other:

- Path-based SEO still responds to `updateForPath()` and config entries.
- Route-based SEO calls `applyRouteMeta()` with route-specific metadata.

---

## Manual Usage

You can also use the service directly if you need full control.

### Imperative route-style application

```ts
constructor(private readonly seo: SeoMetaService) {}

setDynamicSeoForProduct(product: Product) {
  this.seo.applyRouteMeta({
    title: `Buy ${product.name} – My Store`,
    description: product.shortDescription,
    ogUrl: `https://example.com/products/${product.slug}`,
  });
}
```

### Imperative path-based lookup

```ts
constructor(private readonly seo: SeoMetaService) {}

ngOnInit() {
  // Forces the service to use the config entry for /special
  this.seo.updateForPath('/special');
}
```

---

## Angular 19+ & Standalone

This library is designed with **Angular 19+** and *standalone APIs* in mind:

- Uses `provideEnvironmentInitializer`.
- Plays well with `bootstrapApplication`.
- Still compatible with NgModule apps via a simple module wrapper if needed.

Example (NgModule-style):

```ts
import { NgModule } from '@angular/core';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { provideSeoMeta, provideSeoMetaRouting } from '@omnidyon/ngx-seo-meta';

@NgModule({
  imports: [BrowserModule],
  bootstrap: [AppComponent],
  providers: [
    provideSeoMeta(SEO_CONFIG),
    provideSeoMetaRouting(),
  ],
})
export class AppModule {}
```

---

## Testing

The library is tested with **Vitest**, but:

- We **do not mock** `@angular/core` internals.
- `SeoMetaService` is tested in isolation with spies for `Title` and `Meta`.
- Providers (`provideSeoMeta`, `provideSeoMetaRouting`) are tested via Angular's `TestBed` and simple fakes (for `Router` etc.) where needed.

If you’re testing in your own app:

- Use `TestBed.configureTestingModule`.
- Provide `SeoMetaService`, `Title`, `Meta`, and a fake or real `Router` depending on the scenario.
- Avoid mocking Angular’s core module directly.

---

## Roadmap

Planned / in-progress features:

1. **Social meta helper**
   - Better ergonomics for OG + Twitter tags.
2. **JSON-LD helper**
   - Quick helpers for common schemas (e.g. Article, BreadcrumbList).
3. **Canonical + hreflang utilities**
4. **Guards / directives**
   - Small helpers to glue SEO configuration to templates & guards.
5. **Environment / SSR-aware behavior**
   - Avoid touching `document` on the server.
6. **Dev-time diagnostics**
   - Warnings for missing descriptions, duplicate titles, etc.
7. **Typed configuration & builder**
   - Fluent, fully typed config building API.

---

## License

MIT. Use it, fork it, break it, fix it.

---

## Credits

Built for people who are tired of rewriting the same `<title>` and `<meta>` boilerplate across Angular apps.