După ce ai implementat funcționalitățile de bază — articole, autentificare, afișare listă — e timpul să trecem la lucruri serioase: funcționalități avansate care îmbunătățesc experiența utilizatorului și profesionalizează aplicația.
În acest articol vom acoperi:
-
Comentarii și relații între entități (Post ↔ Comments)
-
Paginare, căutare și filtrare articole
-
Upload imagine cover pentru articol (multipart/form-data)
-
SEO: SSR vs SPA – decizii pentru un blog modern
💬 1. Comentarii și relații între entități (Post ↔ Comments)
✅ Modelare în backend
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; }
}
[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>Comentarii</h3>
<ul>
{post?.comments.map(c => (
<li key={c.id}><strong>{c.authorName}</strong>: {c.text}</li>
))}
</ul>
</>
);
};
🔍 2. Paginare, căutare și filtrare articole
✅ 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);
}
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]);
✅ Poți adăuga componente ca:
🖼️ 3. Upload imagine cover pentru articol
✅ 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();
}
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 și rendering: SSR vs SPA
SPA (Single Page Application)
Avantaje:
Dezavantaje:
-
Nu e ideal pentru crawlere (Googlebot etc.)
-
Fără prerendering, titlurile și meta tag-urile nu sunt vizibile
SSR (Server-Side Rendering) – opțional în viitor
Poți migra la:
✅ Concluzie
Aceste funcționalități transformă un blog simplu într-o aplicație completă și scalabilă:
✅ Comentarii și entități conectate
✅ Paginare și filtre pentru UX optim
✅ Upload imagini cover în mod elegant
✅ Gândire SEO pentru vizibilitate pe web