Skip to content

Blog Platform

Build a modern, full-featured Blog Platform using Angular Signals, standalone components, routing, and Tailwind CSS. This project demonstrates real-world patterns for content management applications!

A complete blog platform with:

  • βœ… Blog post listing with pagination
  • βœ… Individual post detail pages
  • βœ… Create and edit posts
  • βœ… Categories and tags
  • βœ… Search functionality
  • βœ… Responsive design with Tailwind CSS
  • βœ… Routing with lazy loading
  • βœ… Signal-based state management
  • βœ… API integration (assuming APIs are ready)

Before starting this project, make sure you have:

  • Node.js (v18 or higher) installed
  • Angular CLI installed globally
  • Basic understanding of TypeScript and Angular
  • Familiarity with REST APIs

For project setup instructions, please refer to:

Terminal window
ng new blog-platform --standalone --routing --style=css --strict
cd blog-platform
Terminal window
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init

Update tailwind.config.js:

/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,ts}",
],
theme: {
extend: {},
},
plugins: [],
}

Update src/styles.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

Create the following structure:

src/app/
β”œβ”€β”€ models/
β”‚ └── blog.model.ts
β”œβ”€β”€ services/
β”‚ └── blog.service.ts
β”œβ”€β”€ pages/
β”‚ β”œβ”€β”€ home/
β”‚ β”‚ └── home.component.ts
β”‚ β”œβ”€β”€ post-detail/
β”‚ β”‚ └── post-detail.component.ts
β”‚ └── post-form/
β”‚ └── post-form.component.ts
β”œβ”€β”€ components/
β”‚ β”œβ”€β”€ post-card/
β”‚ β”‚ └── post-card.component.ts
β”‚ β”œβ”€β”€ header/
β”‚ β”‚ └── header.component.ts
β”‚ └── search-bar/
β”‚ └── search-bar.component.ts
└── app.routes.ts

Create src/app/models/blog.model.ts:

export interface BlogPost {
id: string;
title: string;
content: string;
excerpt: string;
author: string;
category: string;
tags: string[];
imageUrl: string;
publishedAt: Date;
updatedAt: Date;
}
export interface CreatePostDto {
title: string;
content: string;
excerpt: string;
category: string;
tags: string[];
imageUrl: string;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
}

What we created:

  • BlogPost - Main interface for blog posts
  • CreatePostDto - Data transfer object for creating/updating posts
  • PaginatedResponse - Generic interface for paginated API responses

Create src/app/services/blog.service.ts:

import { Injectable, signal, computed, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, tap } from 'rxjs';
import { BlogPost, CreatePostDto, PaginatedResponse } from '../models/blog.model';
@Injectable({
providedIn: 'root'
})
export class BlogService {
private http = inject(HttpClient);
private apiUrl = 'https://api.example.com/posts'; // Replace with your API
// State
private posts = signal<BlogPost[]>([]);
private currentPost = signal<BlogPost | null>(null);
private loading = signal(false);
private searchQuery = signal('');
private selectedCategory = signal<string>('all');
// Public read-only signals
allPosts = this.posts.asReadonly();
post = this.currentPost.asReadonly();
isLoading = this.loading.asReadonly();
// Computed values
filteredPosts = computed(() => {
const posts = this.posts();
const query = this.searchQuery().toLowerCase();
const category = this.selectedCategory();
let filtered = posts;
if (category !== 'all') {
filtered = filtered.filter(post => post.category === category);
}
if (query) {
filtered = filtered.filter(post =>
post.title.toLowerCase().includes(query) ||
post.excerpt.toLowerCase().includes(query)
);
}
return filtered;
});
categories = computed(() => {
const posts = this.posts();
const categorySet = new Set(posts.map(post => post.category));
return ['all', ...Array.from(categorySet)];
});
// API Methods
getPosts(page: number = 1, pageSize: number = 10): Observable<PaginatedResponse<BlogPost>> {
this.loading.set(true);
const params = new HttpParams()
.set('page', page.toString())
.set('pageSize', pageSize.toString());
return this.http.get<PaginatedResponse<BlogPost>>(this.apiUrl, { params }).pipe(
tap(response => {
this.posts.set(response.data);
this.loading.set(false);
})
);
}
getPostById(id: string): Observable<BlogPost> {
this.loading.set(true);
return this.http.get<BlogPost>(`${this.apiUrl}/${id}`).pipe(
tap(post => {
this.currentPost.set(post);
this.loading.set(false);
})
);
}
createPost(post: CreatePostDto): Observable<BlogPost> {
this.loading.set(true);
return this.http.post<BlogPost>(this.apiUrl, post).pipe(
tap(newPost => {
this.posts.update(posts => [newPost, ...posts]);
this.loading.set(false);
})
);
}
updatePost(id: string, post: Partial<CreatePostDto>): Observable<BlogPost> {
this.loading.set(true);
return this.http.put<BlogPost>(`${this.apiUrl}/${id}`, post).pipe(
tap(updatedPost => {
this.posts.update(posts =>
posts.map(p => p.id === id ? updatedPost : p)
);
this.currentPost.set(updatedPost);
this.loading.set(false);
})
);
}
deletePost(id: string): Observable<void> {
this.loading.set(true);
return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(
tap(() => {
this.posts.update(posts => posts.filter(p => p.id !== id));
this.loading.set(false);
})
);
}
// Filter methods
setSearchQuery(query: string): void {
this.searchQuery.set(query);
}
setCategory(category: string): void {
this.selectedCategory.set(category);
}
}

Key features:

  • βœ… Signal-based state management
  • βœ… Computed values for filtering
  • βœ… CRUD operations (Create, Read, Update, Delete)
  • βœ… Search and category filtering
  • βœ… Loading state management

Continue to Part 2: Components to build the UI components!


What’s in Part 2:

  • Header Component
  • Search Bar Component
  • Post Card Component
  • Home Page
  • And more!