Xây dựng Website Tin Tức với Django: Hướng dẫn Từng Bước cho Người Mới

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Vấn đề thực tế: Nhận dự án website tin tức nhưng không biết bắt đầu từ đâu

Mình từng nhận yêu cầu từ người quen: xây một website tin tức nội bộ cho công ty. Yêu cầu nghe đơn giản — danh sách bài viết, trang chi tiết, phân loại theo chuyên mục, và quan trọng nhất là phải có admin để người không biết code cũng đăng bài được.

Nhưng khi ngồi vào code, đủ thứ câu hỏi nảy ra: lưu bài viết vào đâu? URL slug xử lý thế nào? Ảnh đại diện upload kiểu gì? Admin panel thì tự viết hay dùng thư viện? Mình mất mấy ngày loanh quanh với Flask trước khi nhận ra mình đang chọn sai công cụ cho bài toán này.

Phân tích: Website tin tức cần gì mà phức tạp đến vậy?

Trông đơn giản vậy thôi, nhưng ngay cả một trang tin nội bộ nhỏ cũng cần đủ thứ phải hoạt động ăn khớp nhau:

  • Database: lưu bài viết, chuyên mục, trạng thái xuất bản
  • Admin panel: để biên tập viên quản lý nội dung mà không cần biết SQL
  • URL slug đẹp: dạng /bai-viet/ten-bai/ thay vì ?id=5
  • Phân trang: khi có hàng trăm bài, không thể đổ hết một màn hình
  • Upload và hiển thị ảnh: thumbnail cho từng bài

Tự code từng thứ này với Flask hay PHP thuần? Dự kiến mất ít nhất 1–2 tuần chỉ để dựng “hạ tầng” trước khi chạm được vào logic thật sự. Mình vấp đúng chỗ đó — cố tự làm mọi thứ thay vì dùng đúng công cụ cho đúng bài toán.

Các cách giải quyết

Cách 1: WordPress hoặc CMS có sẵn

Nhanh và đủ tính năng, nhưng nếu dự án cần custom logic phức tạp — ví dụ tích hợp API nội bộ hoặc quy trình duyệt bài nhiều bước — WordPress sẽ nhanh chóng trở thành mớ hook và filter rối rắm khó debug.

Cách 2: Tự xây với Flask

Flask linh hoạt, nhưng bạn phải tự tích hợp ORM, form validation, authentication, và admin panel từ đầu. Với website tin tức cần admin đầy đủ, đây là con đường dài không cần thiết.

Cách 3: Django — batteries included

Django đi kèm sẵn admin panel, ORM mạnh, authentication, form handling, và URL routing. Phần lớn thứ một website tin tức cần đã có sẵn — bạn chỉ cần cấu hình và kết nối lại.

Cách tốt nhất: Xây website tin tức Django từng bước

Bước 1: Cài đặt môi trường

python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install django pillow python-slugify

pillow xử lý upload ảnh, python-slugify tạo slug đúng cho tiếng Việt có dấu.

Bước 2: Tạo project và app

django-admin startproject newsite .
python manage.py startapp news

Thêm news vào INSTALLED_APPS và cấu hình media trong settings.py:

INSTALLED_APPS = [
    # ...
    'news',
]

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

Bước 3: Định nghĩa Model

File news/models.py:

from django.db import models
from slugify import slugify

class Category(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.name

class Article(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, blank=True, max_length=250)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
    content = models.TextField()
    thumbnail = models.ImageField(upload_to='thumbnails/', blank=True)
    published_at = models.DateTimeField(auto_now_add=True)
    is_published = models.BooleanField(default=False)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['-published_at']
python manage.py makemigrations
python manage.py migrate

Lưu ý về slug tiếng Việt: slugify() mặc định của Django không xử lý tốt dấu — “Tin tức hôm nay” sẽ ra tin-tc-hm-nay. Dùng python-slugify thay thế, kết quả sẽ là tin-tuc-hom-nay đúng hơn nhiều. Mình hay test nhanh regex pattern tại toolcraft.app/vi/tools/developer/regex-tester trước khi đưa vào code — chạy ngay trên trình duyệt, không cần cài gì, tiện cho các pattern như ^[a-z0-9-]+$ để validate slug.

Bước 4: Cấu hình Admin Panel

File news/admin.py:

from django.contrib import admin
from .models import Article, Category

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ('name',)}

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'category', 'is_published', 'published_at']
    list_filter = ['is_published', 'category']
    search_fields = ['title', 'content']
    prepopulated_fields = {'slug': ('title',)}
python manage.py createsuperuser

Bước 5: Views và URLs

File news/views.py:

from django.shortcuts import render, get_object_or_404
from django.core.paginator import Paginator
from .models import Article, Category

def article_list(request):
    articles = Article.objects.filter(is_published=True)
    paginator = Paginator(articles, 10)
    page_obj = paginator.get_page(request.GET.get('page'))
    return render(request, 'news/list.html', {'page_obj': page_obj})

def article_detail(request, slug):
    article = get_object_or_404(Article, slug=slug, is_published=True)
    return render(request, 'news/detail.html', {'article': article})

def category_view(request, slug):
    category = get_object_or_404(Category, slug=slug)
    articles = Article.objects.filter(category=category, is_published=True)
    return render(request, 'news/category.html', {
        'category': category,
        'articles': articles,
    })

File news/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.article_list, name='article-list'),
    path('bai-viet/<slug:slug>/', views.article_detail, name='article-detail'),
    path('chuyen-muc/<slug:slug>/', views.category_view, name='category'),
]

Cập nhật newsite/urls.py:

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('news.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Bước 6: Template cơ bản

Tạo thư mục news/templates/news/. File list.html:

<!DOCTYPE html>
<html lang="vi">
<head><meta charset="UTF-8"><title>Tin tức</title></head>
<body>
  <h1>Tin tức mới nhất</h1>
  {% for article in page_obj %}
  <div>
    {% if article.thumbnail %}
      <img src="{{ article.thumbnail.url }}" alt="{{ article.title }}">
    {% endif %}
    <h2><a href="{% url 'article-detail' article.slug %}">{{ article.title }}</a></h2>
    <p>{{ article.published_at|date:"d/m/Y" }} — {{ article.category }}</p>
  </div>
  {% endfor %}

  <nav>
    {% if page_obj.has_previous %}
      <a href="?page={{ page_obj.previous_page_number }}">← Trước</a>
    {% endif %}
    {% if page_obj.has_next %}
      <a href="?page={{ page_obj.next_page_number }}">Tiếp →</a>
    {% endif %}
  </nav>
</body>
</html>

Bước 7: Chạy và kiểm tra

python manage.py runserver

Truy cập http://127.0.0.1:8000/admin/, đăng nhập bằng superuser vừa tạo, thêm vài chuyên mục và bài viết. Sau đó vào http://127.0.0.1:8000/ để xem danh sách tin tức.

Kết quả đạt được

Chỉ khoảng 2–3 tiếng setup, bạn đã có website tin tức chạy được với đủ tính năng cơ bản:

  • Admin panel đầy đủ — biên tập viên không biết code vẫn dùng được
  • URL slug sạch theo từng bài, hỗ trợ tiếng Việt đúng
  • Phân trang tự động khi số bài tăng lên
  • Upload và hiển thị ảnh đại diện
  • Lọc bài theo chuyên mục

Muốn mở rộng thêm? Full-text search thì dùng django-haystack, RSS feed Django đã hỗ trợ sẵn qua django.contrib.syndication, headless CMS cho Next.js hay Nuxt thì thêm Django REST Framework vào là xong. Cái hay là scope dự án có lớn đến đâu, bạn vẫn bolt-on từng phần — không cần viết lại từ đầu như khi outgrow Flask.

Share: