Skip to content

Commit a0e4323

Browse files
authored
Fix blog SEO meta tags and error handling (#17)
1 parent 50ca3f9 commit a0e4323

File tree

9 files changed

+94
-33
lines changed

9 files changed

+94
-33
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "personal-website-ui",
3-
"version": "1.8.1",
3+
"version": "1.8.2",
44
"type": "module",
55
"scripts": {
66
"ng": "ng",

src/app/app.routes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export const routes: Routes = [
4747
{
4848
path: 'blogs/:slug',
4949
loadComponent: () => import('./pages/blog-detail/blog-detail.component').then(m => m.BlogDetailComponent),
50-
title: 'Blog Post - Kapil Garg',
5150
resolve: { blog: blogDetailResolver }
5251
},
5352
{

src/app/pages/blog-detail/blog-detail.component.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,20 +154,24 @@ export class BlogDetailComponent implements OnInit, OnDestroy {
154154
.getBlogBySlug(slug)
155155
.pipe(takeUntilDestroyed(this.destroyRef))
156156
.subscribe({
157-
next: (blog) => {
158-
if (blog) {
159-
this.currentBlogSignal.set(blog);
157+
next: (result) => {
158+
if (result.blog) {
159+
this.currentBlogSignal.set(result.blog);
160160
this.apiErrorSignal.set(false);
161-
this.blogService.addBlogToList(blog);
162-
this.seoService.setBlogPostMetaTags(blog);
161+
this.blogService.addBlogToList(result.blog);
162+
this.seoService.setBlogPostMetaTags(result.blog);
163163
if (isPlatformBrowser(this.platformId)) {
164-
this.incrementViewCount(blog.id);
164+
this.incrementViewCount(result.blog.id);
165165
}
166-
} else {
166+
} else if (result.error === 'not_found') {
167167
this.router.navigate(['/blogs']);
168+
} else {
169+
this.seoService.setBlogSlugFallbackMetaTags(slug);
170+
this.apiErrorSignal.set(true);
168171
}
169172
},
170173
error: () => {
174+
this.seoService.setBlogSlugFallbackMetaTags(slug);
171175
this.apiErrorSignal.set(true);
172176
},
173177
});

src/app/services/blog.service.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Injectable, signal, inject } from '@angular/core';
2-
import { HttpClient } from '@angular/common/http';
2+
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
33
import { Observable, of, catchError, finalize, map, timeout, TimeoutError } from 'rxjs';
4-
import { Blog, BlogFilters } from '../models/blog.interface';
4+
import { Blog, BlogFilters, BlogDetailResult } from '../models/blog.interface';
55
import { ApiResponse } from '../models/api-response.interface';
66
import { APP_CONSTANTS, SortOption, SortOrder } from '../shared/constants/app.constants';
77
import { StringUtils } from '../shared/utils/string.utils';
@@ -15,8 +15,8 @@ export class BlogService {
1515

1616
private readonly API_BASE_URL = `${environment.apiUrl}/blogs`;
1717

18-
private readonly CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
19-
private readonly REQUEST_TIMEOUT_MS = 15 * 1000; // 15 seconds timeout
18+
private readonly CACHE_TTL_MS = 5 * 60 * 1000;
19+
private readonly REQUEST_TIMEOUT_MS = 15 * 1000;
2020

2121
private http = inject(HttpClient);
2222
private environmentService = inject(EnvironmentService);
@@ -72,17 +72,21 @@ export class BlogService {
7272
);
7373
}
7474

75-
getBlogBySlug(slug: string): Observable<Blog | null> {
75+
getBlogBySlug(slug: string): Observable<BlogDetailResult> {
7676
return this.http.get<ApiResponse<Blog>>(`${this.API_BASE_URL}/published/${slug}`).pipe(
7777
timeout(this.REQUEST_TIMEOUT_MS),
78-
map(response => response.data),
79-
catchError(error => {
78+
map(response => ({ blog: response.data, error: null }) as BlogDetailResult),
79+
catchError((error: HttpErrorResponse | TimeoutError) => {
8080
if (error instanceof TimeoutError) {
8181
this.environmentService.warn('Blog detail request timed out after', this.REQUEST_TIMEOUT_MS, 'ms');
82-
} else {
83-
this.environmentService.warn('Error fetching blog:', error);
82+
return of({ blog: null, error: 'api_error' } as BlogDetailResult);
8483
}
85-
return of(null);
84+
if (error instanceof HttpErrorResponse && error.status === 404) {
85+
this.environmentService.warn('Blog not found:', slug);
86+
return of({ blog: null, error: 'not_found' } as BlogDetailResult);
87+
}
88+
this.environmentService.warn('Error fetching blog:', error);
89+
return of({ blog: null, error: 'api_error' } as BlogDetailResult);
8690
})
8791
);
8892
}

src/app/shared/resolvers/blog-detail.resolver.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { inject } from '@angular/core';
22
import { ResolveFn } from '@angular/router';
33
import { of } from 'rxjs';
4-
import { catchError, map } from 'rxjs/operators';
4+
import { map } from 'rxjs/operators';
55
import { BlogDetailResult } from '../../models/blog.interface';
66
import { BlogService } from '../../services/blog.service';
77
import { SeoService } from '../services/seo.service';
@@ -17,16 +17,14 @@ export const blogDetailResolver: ResolveFn<BlogDetailResult> = (route) => {
1717
const blogService = inject(BlogService);
1818

1919
return blogService.getBlogBySlug(slug).pipe(
20-
map((blog) => {
21-
if (blog) {
22-
seoService.setBlogPostMetaTags(blog);
23-
blogService.addBlogToList(blog);
24-
return { blog, error: null };
20+
map((result) => {
21+
if (result.blog) {
22+
seoService.setBlogPostMetaTags(result.blog);
23+
blogService.addBlogToList(result.blog);
24+
return { blog: result.blog, error: null };
2525
}
26-
return { blog: null, error: 'not_found' as const };
27-
}),
28-
catchError(() => {
29-
return of({ blog: null, error: 'api_error' as const });
26+
seoService.setBlogSlugFallbackMetaTags(slug);
27+
return result;
3028
}),
3129
);
3230

src/app/shared/services/seo.service.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,57 @@ export class SeoService {
8383

8484
}
8585

86+
/**
87+
* Sets fallback meta tags when a blog post cannot be loaded.
88+
* This ensures social platforms show something useful instead of default home page tags.
89+
*/
90+
setBlogSlugFallbackMetaTags(slug: string): void {
91+
92+
const readableTitle = this.slugToReadableTitle(slug);
93+
const title = `${readableTitle} - Kapil Garg`;
94+
const url = `${this.baseUrl}/blogs/${slug}`;
95+
const description = `Read "${readableTitle}" by Kapil Garg - Java Full Stack Developer sharing insights on technology, career, and software development.`;
96+
97+
this.title.setTitle(title);
98+
99+
this.updateOrCreateMetaTag('name', 'description', description);
100+
this.updateOrCreateMetaTag('name', 'author', this.author);
101+
this.updateOrCreateMetaTag('name', 'keywords', this.generateFallbackKeywords(readableTitle));
102+
103+
this.updateOrCreateMetaTag('property', 'og:type', 'article');
104+
this.updateOrCreateMetaTag('property', 'og:title', title);
105+
this.updateOrCreateMetaTag('property', 'og:description', description);
106+
this.updateOrCreateMetaTag('property', 'og:image', this.defaultImage);
107+
this.updateOrCreateMetaTag('property', 'og:image:width', '1200');
108+
this.updateOrCreateMetaTag('property', 'og:image:height', '630');
109+
this.updateOrCreateMetaTag('property', 'og:image:alt', title);
110+
this.updateOrCreateMetaTag('property', 'og:image:type', this.getImageType(this.defaultImage));
111+
this.updateOrCreateMetaTag('property', 'og:url', url);
112+
this.updateOrCreateMetaTag('property', 'og:site_name', 'Kapil Garg');
113+
this.updateOrCreateMetaTag('property', 'og:locale', 'en_US');
114+
115+
this.updateOrCreateMetaTag('name', 'twitter:card', 'summary_large_image');
116+
this.updateOrCreateMetaTag('name', 'twitter:title', title);
117+
this.updateOrCreateMetaTag('name', 'twitter:description', description);
118+
this.updateOrCreateMetaTag('name', 'twitter:image', this.defaultImage);
119+
this.updateOrCreateMetaTag('name', 'twitter:site', '@KappilGarg');
120+
this.updateOrCreateMetaTag('name', 'twitter:creator', '@KappilGarg');
121+
122+
this.updateOrCreateLinkTag('canonical', url);
123+
this.updateOrCreateMetaTag('property', 'article:author', this.author);
124+
125+
}
126+
127+
/**
128+
* Converts a URL slug to a readable title.
129+
*/
130+
private slugToReadableTitle(slug: string): string {
131+
return slug
132+
.split('-')
133+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
134+
.join(' ');
135+
}
136+
86137
setBlogsListingMetaTags(): void {
87138
const title = 'Blogs - Kapil Garg';
88139
const url = `${this.baseUrl}/blogs`;
@@ -282,6 +333,11 @@ export class SeoService {
282333
return `${categoryKeywords}${baseKeywords}`;
283334
}
284335

336+
private generateFallbackKeywords(readableTitle: string): string {
337+
const baseKeywords = 'Kapil Garg, Java Developer, Full Stack Developer, Spring Boot, Angular, Blog';
338+
return `${readableTitle}, ${baseKeywords}`;
339+
}
340+
285341
private getImageType(imageUrl: string): string {
286342
const sanitizedUrl = imageUrl.split('?')[0];
287343
const extension = sanitizedUrl.split('.').pop()?.toLowerCase() || 'png';

src/environments/environment.prod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const environment = {
22
production: true,
33
apiUrl: 'https://api.kappilgarg.dev/api',
44
appName: 'Personal Website',
5-
version: '1.8.1',
5+
version: '1.8.2',
66
features: {
77
enableAnalytics: true,
88
enableDebugMode: false,

src/environments/environment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const environment = {
22
production: false,
33
apiUrl: 'http://localhost:8080/api',
44
appName: 'Personal Website',
5-
version: '1.8.1',
5+
version: '1.8.2',
66
features: {
77
enableAnalytics: false,
88
enableDebugMode: true,

0 commit comments

Comments
 (0)