Back to Dynamic Webapps
Angular Integration

Angular Integration Guide

Integrate SEO Sniper articles into your Angular application using services and components with full TypeScript support.

Articles Service

typescript
// services/articles.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, map, shareReplay } from 'rxjs';
import { environment } from '../environments/environment';

export interface Article {
  id: string;
  slug: string;
  title: string;
  metaTitle: string;
  metaDescription: string;
  contentHtml: string;
  publishedAt: string;
  featuredImageUrl?: string;
}

interface ArticlesResponse {
  articles: Article[];
}

@Injectable({
  providedIn: 'root'
})
export class ArticlesService {
  private readonly apiUrl = environment.seoSniperApiUrl;
  private articlesCache$?: Observable<Article[]>;

  private headers = new HttpHeaders({
    'Authorization': `Bearer ${environment.seoSniperApiToken}`
  });

  constructor(private http: HttpClient) {}

  getArticles(): Observable<Article[]> {
    if (!this.articlesCache$) {
      this.articlesCache$ = this.http
        .get<ArticlesResponse>(
          `${this.apiUrl}/api/v1/articles`,
          { headers: this.headers }
        )
        .pipe(
          map(response => response.articles),
          shareReplay(1)
        );
    }
    return this.articlesCache$;
  }

  getArticleBySlug(slug: string): Observable<Article | undefined> {
    return this.getArticles().pipe(
      map(articles => articles.find(a => a.slug === slug))
    );
  }

  clearCache(): void {
    this.articlesCache$ = undefined;
  }
}

Blog List Component

typescript
// components/blog-list/blog-list.component.ts
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { Observable } from 'rxjs';
import { ArticlesService, Article } from '../../services/articles.service';

@Component({
  selector: 'app-blog-list',
  standalone: true,
  imports: [CommonModule, RouterLink],
  template: `
    <div class="max-w-4xl mx-auto py-12 px-4">
      <h1 class="text-4xl font-bold mb-8">Blog</h1>

      <div class="grid gap-8" *ngIf="articles$ | async as articles">
        <article
          *ngFor="let article of articles"
          class="border rounded-lg overflow-hidden"
        >
          <img
            *ngIf="article.featuredImageUrl"
            [src]="article.featuredImageUrl"
            [alt]="article.title"
            class="w-full h-48 object-cover"
          />
          <div class="p-6">
            <a [routerLink]="['/blog', article.slug]">
              <h2 class="text-2xl font-semibold hover:text-blue-600">
                {{ article.title }}
              </h2>
            </a>
            <p class="text-gray-600 mt-2">
              {{ article.metaDescription }}
            </p>
            <time class="text-sm text-gray-500 mt-4 block">
              {{ article.publishedAt | date:'longDate' }}
            </time>
          </div>
        </article>
      </div>
    </div>
  `
})
export class BlogListComponent implements OnInit {
  articles$!: Observable<Article[]>;

  constructor(private articlesService: ArticlesService) {}

  ngOnInit(): void {
    this.articles$ = this.articlesService.getArticles();
  }
}

Article Component

typescript
// components/article/article.component.ts
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { Meta, Title } from '@angular/platform-browser';
import { Observable, switchMap, tap } from 'rxjs';
import { ArticlesService, Article } from '../../services/articles.service';

@Component({
  selector: 'app-article',
  standalone: true,
  imports: [CommonModule],
  template: `
    <article
      *ngIf="article$ | async as article"
      class="max-w-3xl mx-auto py-12 px-4"
    >
      <header class="mb-8">
        <h1 class="text-4xl font-bold mb-4">{{ article.title }}</h1>
        <time class="text-gray-500">
          {{ article.publishedAt | date:'longDate' }}
        </time>
      </header>

      <!-- Render HTML content -->
      <div
        class="prose prose-lg max-w-none"
        [innerHTML]="article.contentHtml"
      ></div>
    </article>
  `
})
export class ArticleComponent implements OnInit {
  article$!: Observable<Article | undefined>;

  constructor(
    private route: ActivatedRoute,
    private articlesService: ArticlesService,
    private title: Title,
    private meta: Meta
  ) {}

  ngOnInit(): void {
    this.article$ = this.route.paramMap.pipe(
      switchMap(params => {
        const slug = params.get('slug')!;
        return this.articlesService.getArticleBySlug(slug);
      }),
      tap(article => {
        if (article) {
          this.updateMeta(article);
        }
      })
    );
  }

  private updateMeta(article: Article): void {
    this.title.setTitle(article.metaTitle);
    this.meta.updateTag({ name: 'description', content: article.metaDescription });
    this.meta.updateTag({ property: 'og:title', content: article.metaTitle });
    this.meta.updateTag({ property: 'og:description', content: article.metaDescription });

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

Routing Configuration

typescript
// app.routes.ts
import { Routes } from '@angular/router';
import { BlogListComponent } from './components/blog-list/blog-list.component';
import { ArticleComponent } from './components/article/article.component';

export const routes: Routes = [
  { path: 'blog', component: BlogListComponent },
  { path: 'blog/:slug', component: ArticleComponent },
  { path: '', redirectTo: 'blog', pathMatch: 'full' }
];

Angular Universal for SSR

For better SEO, consider using Angular Universal for server-side rendering. This ensures search engines can crawl your content properly.

SEO Sniper