Hướng dẫn Angular 19 SSR trong 10 phút: Tối ưu SEO và tìm kiếm
Bạn muốn có thời gian tải trang nhanh, nội dung dễ lập chỉ mục và trải nghiệm tìm kiếm mượt mà mà không cần tái cấu trúc lớn? Hãy tham khảo hướng dẫn từng bước này để thiết lập Angular 19 SSR, thêm dữ liệu SEO theo cấp độ route (với JSON-LD) và tìm kiếm debounced sử dụng Signals.
Chúng ta sẽ giả định bạn đã có một ứng dụng Angular 19 mới. Các đoạn mã sẽ tối giản và hướng tới sản xuất.
1) Thêm Angular Universal (SSR)
Để bắt đầu, bạn cần thêm Angular Universal vào dự án của mình:
bash
ng add @nguniversal/express-engine
Lệnh này sẽ tạo ra một máy chủ Express và cập nhật các mục tiêu build của bạn. Bạn có thể chạy nó bằng cách:
bash
npm run dev:ssr
# hoặc build & serve
npm run build:ssr && npm run serve:ssr
Việc sử dụng SSR giúp cải thiện Core Web Vitals, khả năng lập chỉ mục và các trang đích—đặc biệt cho các sản phẩm SaaS dựa vào lưu lượng truy cập tự nhiên (xem thêm về việc xây dựng ứng dụng SaaS tại đây).
2) Dữ liệu SEO theo cấp độ route + JSON-LD
a) Đặt dữ liệu SEO trên các route
Để thêm dữ liệu SEO, bạn cần chỉnh sửa tệp app.routes.ts như sau:
typescript
// app.routes.ts
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./home.component').then(m => m.HomeComponent),
data: {
title: 'Trang Chủ — Nhanh và Dễ Tìm',
description: 'Nội dung được render từ server với Angular 19 và Signals.',
},
},
];
b) Áp dụng Title/Meta khi điều hướng
Trong tệp seo.service.ts, bạn có thể cập nhật tiêu đề và meta cho các route:
typescript
// 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'] });
}
});
}
}
Hãy gọi seoService.init() một lần trong constructor của AppComponent.
c) Thêm JSON-LD (thân thiện với SSR)
Bạn có thể tạo một dịch vụ để thêm JSON-LD vào trang của mình:
typescript
// 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);
}
}
Ví dụ sử dụng trong một component trang:
typescript
jsonLd.set({
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Ứng dụng Mẫu',
applicationCategory: 'BusinessApplication'
});
Bạn có cần chuyên môn về SSR cho các trang đích Angular hoặc các module ERP/CRM lớn hơn không? (tham khảo thêm về Angular và ERP/CRM).
3) Tìm kiếm debounced với Signals
Một tìm kiếm nhỏ gọn, an toàn với kiểu dữ liệu mà không làm quá tải API của bạn.
typescript
// 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="Tìm kiếm…"
[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 }[]>([]);
// Kết nối Signal -> RxJS để 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 : []));
}
}
Điểm cuối Express tối thiểu (Node 18+):
typescript
// 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);
Mô hình này giữ cho giao diện người dùng phản hồi nhanh, tránh việc lấy dữ liệu quá mức và hoạt động liền mạch với SSR/hydration.
Điều gì tiếp theo?
- Thêm bộ nhớ cache HTTP (ETag) cho kết quả tìm kiếm.
- Theo dõi các thuật ngữ tìm kiếm để thông báo quyết định sản phẩm.
- Nếu ngăn xếp của bạn bao gồm các backend Node.js/Express hoặc tích hợp AI (phân loại, tóm tắt), hãy giữ cho các mối quan tâm tách biệt sau các bộ điều hợp rõ ràng.
Các tài liệu hữu ích (đọc thêm)
- Tìm hiểu thêm về các nguyên tắc cơ bản của Angular SSR.
- Hướng dẫn cho các dịch vụ sản xuất Node.js.
- Xây dựng nền tảng SaaS vững chắc.
- Khi nào nên thiết kế các module ERP/CRM tùy chỉnh.
- Xử lý nhu cầu cao với tăng cường nhân lực.