RO EN
Advanced Features in a Modern Blog: Comments, Pagination, Upload, and SEO
Doru Bulubasa
23 June 2025

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

Distribuie: