
Koji problem rešavamo?
Angular je neverovatno napredovao u poslednjih nekoliko verzija.
S obzirom da je Server-Side Rendering (SSR) sada zreo i ugrađen, Angular je postao odličan izbor za pravljenje SEO prijateljskih aplikacija.
Za lokalizaciju, integrisali smo ngx-translate za rukovanje prevodima na čist i poznat način. Taj deo je radio odlično.
Međutim, SEO uvodi dodatni zahtev.
Bili su nam potrebni lokalizovani URL-ovi koje pretraživači mogu da indeksiraju — što znači rute kao što su:
/en/sr
Na prvi pogled, ovo zvuči jednostavno.
Ali kada počnete da povezujete rutiranje, lokalizaciju, čuvare (guards), početnu navigaciju i SSR zajedno, stvari postaju interesantne.
Tu na scenu stupa Angular Ruter — i još važnije, Dependency Injection (DI).
O čemu će biti reči u ovom članku
U ovom članku istražićemo:
- zašto je rutiranje svesno lokalizacije važno za SEO
- gde podrazumevani Angular Ruter ima nedostatke
- kako proširiti ponašanje rutiranja pomoću DI-ja
- kako održati rešenje čistim, pogodnim za testiranje i SSR
Krenimo redom.
Zašto je rutiranje svesno lokalizacije važno za SEO
Pretraživači indeksiraju URL-ove, a ne stanje korisničkog interfejsa (UI).
Ako jezik nije prikazan u URL-u, pretraživači ne mogu pravilno da razumeju, rangiraju ili ciljaju vaš sadržaj po lokaciji.
Pretraživači pretpostavljaju da su:
/en/about/sr/about
dva različita dokumenta namenjena za dve različite publike.
Ako umesto toga uradite ovo:
/about // jezik odlučuju JS, kolačići ili zaglavlja
onda iz perspektive pretraživača:
- postoji samo jedna stranica
- promena jezika je nejasna
- SEO signali se mešaju
- indeksiranje postaje nepouzdano
👉 Rezultat: slabije rangiranje i pogrešno ciljanje jezika.
Korak 1: Inicijalizacija jezika na početku
Prva stvar koja nam je potrebna je da inicijalizujemo jezik koji koristi ngx-translate.
Ovo radimo koristeći App Initializer, obezbeđujući da se tačan jezik postavi pre rutiranja i renderovanja, što je posebno važno za SSR.
App Config:
import { ApplicationConfig, provideAppInitializer } from '@angular/core';
import { languageInitializer } from './core/language';
export const appConfig: ApplicationConfig = {
providers: [
provideTranslateService({
lang: 'en',
fallbackLang: 'en',
loader: provideTranslateHttpLoader({
prefix: '/i18n/',
suffix: '.json'
})
}),
provideAppInitializer(() => languageInitializer())
],
};
languageInitializer:
export type Language = 'sr' | 'en';
export const SUPPORTED_LANGUAGES = new InjectionToken<Language[]>(
'SUPPORTED_LANGUAGES',
{
providedIn: 'root',
factory: () => ['en', 'sr'] as const,
}
);
export function languageInitializer() {
const translate = inject(TranslateService);
const platformLocation = inject(PlatformLocation);
const supportedLanguages = inject(SUPPORTED_LANGUAGES);
const path = platformLocation.pathname;
const segments = path.split('/').filter(Boolean);
const language = segments[0] || 'en';
if (supportedLanguages.includes(language as Language)) {
translate.use(language);
}
}
Ovo osigurava da je tačan jezik aktivan na osnovu URL-a, pre nego što se bilo šta drugo dogodi.
Korak 2: Rute svesne jezika
Sada definišemo naše rute sa dinamičnim :language segmentom.
import { Routes } from '@angular/router';
import { canMatchLanguageRoute } from './core/language';
export const routes: Routes = [
{
path: ':language',
canMatch: [canMatchLanguageRoute],
children: [
{
path: '',
pathMatch: 'full',
loadComponent: () =>
import('./features/home/home').then(m => m.Home),
},
{
path: 'about',
loadComponent: () =>
import('./features/about/about').then(m => m.About),
},
],
},
];
Čuvanje nepodržanih jezika:
export function canMatchLanguageRoute(
_route: Route,
segments: UrlSegment[]
): boolean {
const supportedLanguages = inject(SUPPORTED_LANGUAGES);
const router = inject(Router);
const language = segments[0]?.path as Language || 'en';
if (!supportedLanguages.includes(language)) {
router.navigateByUrl('/en');
return false;
}
return true;
}
Ovo sprečava korisnike i pretraživače da pristupe rutama nepodržanog jezika, dok održava definiciju rute čistom.
Korak 3: Proširenje Rutera pomoću DI-ja
Sada dolazi onaj zanimljiv deo.
Želeli smo da svaka navigacija — uključujući router.navigateByUrl() i routerLink — automatski uključuje trenutni jezik bez ručnog prosleđivanja.
Umesto da posipamo logiku jezika svuda po aplikaciji, proširili smo sam Angular Router.
LanguageRouter
@Injectable()
class LanguageRouter extends Router {
private readonly translate = inject(TranslateService);
private readonly supportedLanguages = inject<Language[]>(SUPPORTED_LANGUAGES);
override navigateByUrl(
url: string | UrlTree,
extras?: NavigationBehaviorOptions,
): Promise<boolean> {
const currentLang = this.translate.getCurrentLang() || 'en';
const urlString = typeof url === 'string' ? url : this.serializeUrl(url);
const { path, query, fragment } = this.parseUrlParts(urlString);
const finalPath = this.addLanguagePrefix(path, currentLang);
const finalUrlString = this.reconstructUrl(finalPath, query, fragment);
const finalUrl: string | UrlTree =
typeof url === 'string' ? finalUrlString : this.parseUrl(finalUrlString);
const currentUrl = this.url;
const finalUrlSerialized =
typeof finalUrl === 'string' ? finalUrl : this.serializeUrl(finalUrl);
if (currentUrl === finalUrlSerialized) {
return Promise.resolve(false);
}
return super.navigateByUrl(finalUrl, extras);
}
private isLanguagePath(segment: string): boolean {
return this.supportedLanguages.includes(segment as Language);
}
private parseUrlParts(urlString: string): { path: string; query?: string; fragment?: string } {
const questionMarkIndex = urlString.indexOf('?');
const hashIndex = urlString.indexOf('#');
let path: string;
let query: string | undefined;
let fragment: string | undefined;
if (questionMarkIndex !== -1) {
path = urlString.substring(0, questionMarkIndex);
const afterQuestion = urlString.substring(questionMarkIndex + 1);
if (hashIndex !== -1 && hashIndex > questionMarkIndex) {
query = urlString.substring(questionMarkIndex + 1, hashIndex);
fragment = urlString.substring(hashIndex + 1);
} else {
query = afterQuestion;
}
} else if (hashIndex !== -1) {
path = urlString.substring(0, hashIndex);
fragment = urlString.substring(hashIndex + 1);
} else {
path = urlString;
}
return { path, query, fragment };
}
private addLanguagePrefix(path: string, language: string): string {
const pathSegments = path.split('/').filter((s) => s.length > 0);
const firstSegment = pathSegments[0];
const hasLanguagePrefix = firstSegment && this.isLanguagePath(firstSegment);
if (hasLanguagePrefix) {
if (firstSegment === language) {
return path;
}
pathSegments[0] = language;
return '/' + pathSegments.join('/');
}
const trimmedPath = path.startsWith('/') ? path.substring(1) : path;
return `/${language}${trimmedPath ? '/' + trimmedPath : ''}`;
}
private reconstructUrl(path: string, query?: string, fragment?: string): string {
let url = path;
if (query) {
url += '?' + query;
}
if (fragment) {
url += '#' + fragment;
}
return url;
}
}
Korak 4: Zamena Rutera preko DI-ja
Sada govorimo Angularu da koristi naš ruter umesto podrazumevanog.
export function provideLanguageRouter(): Provider[] {
return [{ provide: Router, useClass: LanguageRouter }];
}
I registrujemo ga nakon provideRouter:
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideAppInitializer(() => languageInitializer()),
provideRouter(routes),
provideClientHydration(withEventReplay()),
provideLanguageRouter(), // mora doći nakon provideRouter
],
};
Rezultat
Sada:
- svaki
routerLink - svaki
navigateByUrl - svaka programska navigacija
automatski poštuje aktivni jezik.
Nema koda koji se ponavlja.
Nema ručnog dodavanja prefiksa.
Nema "curenja" logike lokalizacije u komponente.
Završne reči
Ovaj pristup održava:
- rutiranje deklarativnim
- lokalizaciju eksplicitnom
- SEO čvrstim
- SSR srećnim
I što je najvažnije, koristi Angular-ov sistem Dependency Injection onako kako je i zamišljeno — proširivanjem ponašanja bez borbe sa framework-om.
Ako pravite lokalizovane Angular aplikacije prilagođene SEO-u, ovaj obrazac se iznenađujuće dobro skalira.
Srećno rutiranje! 🚀