After implementing the basic functionalities — articles, authentication, list display — it's time to move on to serious things: advanced features that improve the user experience and professionalize the application.
In this article we will cover:
-
Comments and relationships between entities (Post ↔ Comments)
-
Pagination, search, and filtering of articles
-
Upload cover image for article (multipart/form-data)
-
SEO: SSR vs SPA – decisions for a modern blog
💬 1. Comments and relationships between entities (Post ↔ Comments)
✅ Backend modeling
public class Post
{
public Guid Id { get; set; }
public string Title { get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public Guid Id { get; set; }
public string AuthorName { get; set; }
public string Text { get; set; }
public DateTime CreatedAt { get; set; }
public Guid PostId { get; set; }
public Post Post { get; set; }
}
-
Comments are linked through PostId
-
Use Include(p => p.Comments) to load the comments
✅ Endpoints
[HttpGet("{id}")]
public async Task<IActionResult> GetPostWithComments(Guid id)
{
var post = await _context.Posts
.Include(p => p.Comments)
.FirstOrDefaultAsync(p => p.Id == id);
return Ok(post);
}
[HttpPost("{postId}/comments")]
public async Task<IActionResult> AddComment(Guid postId, [FromBody] CommentDto dto)
{
var comment = new Comment { Text = dto.Text, PostId = postId, CreatedAt = DateTime.UtcNow };
_context.Comments.Add(comment);
await _context.SaveChangesAsync();
return Ok();
}
✅ React
const PostPage = () => {
const [post, setPost] = useState<Post | null>(null);
useEffect(() => {
fetch(`/api/posts/${id}`).then(res => res.json()).then(setPost);
}, []);
return (
<>
<h1>{post?.title}</h1>
<p>{post?.content}</p>
<h3>Comments</h3>
<ul>
{post?.comments.map(c => (
<li key={c.id}><strong>{c.authorName}</strong>: {c.text}</li>
))}
</ul>
</>
);
};
🔍 2. Pagination, search, and filtering of articles
✅ Backend
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] int page = 1, [FromQuery] string? search = null)
{
var query = _context.Posts.AsQueryable();
if (!string.IsNullOrEmpty(search))
query = query.Where(p => p.Title.Contains(search));
var pageSize = 10;
var items = await query
.OrderByDescending(p => p.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return Ok(items);
}
✅ React – components
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
useEffect(() => {
fetch(`/api/posts?page=${page}&search=${search}`)
.then(res => res.json())
.then(setPosts);
}, [page, search]);
✅ You can add components such as:
-
<Pagination /> (with buttons + page number)
-
<input type="search" /> for title/content
🖼️ 3. Upload cover image for article
✅ Backend (multipart/form-data)
[HttpPost]
public async Task<IActionResult> Upload([FromForm] CreatePostWithImageDto dto)
{
var file = dto.Image;
var fileName = Guid.NewGuid() + Path.GetExtension(file.FileName);
var path = Path.Combine("uploads", fileName);
using var stream = new FileStream(path, FileMode.Create);
await file.CopyToAsync(stream);
var post = new Post { Title = dto.Title, Content = dto.Content, ImageUrl = $"/uploads/{fileName}" };
_context.Posts.Add(post);
await _context.SaveChangesAsync();
return Ok();
}
✅ React + FormData
const handleSubmit = async (e) => {
e.preventDefault();
const data = new FormData();
data.append("title", title);
data.append("content", content);
data.append("image", file);
await fetch("/api/posts", {
method: "POST",
body: data
});
};
🌐 4. SEO and rendering: SSR vs SPA