Django: Blog Website

9 minute read

Published:

Bab ini membahas pembuatan aplikasi Blog berbasis database dengan fitur inti membaca daftar post dan melihat halaman detail per post, sekaligus menyiapkan fondasi untuk create, read, update, dan delete pada bab berikutnya.

Pada bab ini kita akan mempelajari:

  • Setup proyek project6 dengan virtual environment.
  • Membuat model Post dengan field judul, author, dan body.
  • Memahami konsep primary key dan foreign key dalam Django ORM.
  • Mengelola data menggunakan Django Admin.
  • Menyusun function-based views, URL routing, dan template inheritance.
  • Menambahkan static files (CSS) agar tampilan blog lebih rapi.
  • Membuat halaman detail post dan menerapkan get_absolute_url().
  • Menulis pengujian model, URL, template, dan konten.
  • Workflow Git untuk commit awal proyek.

1. Gambaran Blog Website

Di bab ini kita mulai membangun aplikasi blog yang lebih realistis. Jika pada bab sebelumnya kita baru menampilkan data sederhana, kini kita bergerak ke pola yang umum di hampir semua website CRUD.

Fokus utama bagian ini:

  1. Menyusun struktur data post secara benar.
  2. Menghubungkan post dengan user melalui relasi database.
  3. Menampilkan daftar dan detail post di halaman web.
  4. Menambahkan gaya visual dasar dengan static files.
  5. Menyiapkan test agar perubahan tetap aman.

Sebelum masuk ke langkah setup, berikut gambaran struktur direktori akhir yang akan kita capai pada proyek project6:

project6/
├── .venv/
├── db.sqlite3
├── manage.py
├── django_project/
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── blog/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations/
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── static/
│   └── css/
│       └── base.css
└── templates/
  ├── base.html
  ├── home.html
  └── post_detail.html

2. Setup Awal Proyek project6

Langkah setup mirip dengan bab sebelumnya: buat folder proyek, aktifkan virtual environment, instal dependensi, lalu buat project dan app.

# Windows
cd onedrive\desktop\pawf\django
mkdir project6
cd project6
python -m venv .venv
.venv\Scripts\Activate.ps1

# macOS
cd ~/desktop/pawf/django
mkdir -p project6
cd project6
python3 -m venv .venv
source .venv/bin/activate

Instal paket, buat project, buat app, dan migrasi awal:

python -m pip install django~=6.0.4
python -m pip install black
django-admin startproject django_project .
python manage.py startapp blog
python manage.py migrate
python manage.py runserver

Tambahkan app blog ke INSTALLED_APPS di django_project/settings.py:

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",  # new
]

3. Model Blog Post

Kita ingin tabel Post dengan tiga field utama:

  • title
  • author
  • body

Dalam Django, model adalah representasi tabel database dalam bentuk class Python. Setiap atribut pada class akan dipetakan menjadi kolom pada tabel.

Awalnya kita bisa mendefinisikan model seperti berikut di blog/models.py:

from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=200)
    body = models.TextField()

    def __str__(self):
        return self.title

Kemudian buat migrasi dan terapkan ke database:

python manage.py makemigrations blog
python manage.py migrate

Catatan penting:

  • CharField cocok untuk teks pendek dengan batas panjang.
  • TextField cocok untuk konten panjang.
  • Method __str__() membantu tampilan objek lebih manusiawi di admin dan shell.

django output1

4. Primary Key dan Foreign Key

Django otomatis menambahkan primary key bertipe auto-increment ke model, biasanya bernama id.

Primary key berfungsi sebagai identitas unik untuk setiap baris data. Foreign key digunakan untuk menghubungkan satu tabel dengan tabel lain sehingga relasi antar data tetap konsisten.

Karena kita ingin setiap post terkait user yang valid, field author sebaiknya diubah menjadi foreign key ke model user bawaan Django seperti berikut di blog/models.py:

from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(
        "auth.User",
        on_delete=models.CASCADE,
    )
    body = models.TextField()

    def __str__(self):
        return self.title

Setelah mengubah skema model, jalankan migrasi lagi:

python manage.py makemigrations blog
python manage.py migrate

Makna relasi ini:

  • Satu user dapat memiliki banyak post (many-to-one dari post ke user).
  • Jika user dihapus, post terkait ikut dihapus karena on_delete=models.CASCADE.

django output2

5. Django Admin

Buat akun superuser:

python manage.py createsuperuser

Daftarkan model pada blog/admin.py:

from django.contrib import admin

from .models import Post


admin.site.register(Post)

Agar tampilan list admin menampilkan beberapa kolom sekaligus, gunakan ModelAdmin perbaiki model pada blog/admin.py:

from django.contrib import admin

from .models import Post


class PostAdmin(admin.ModelAdmin):
    list_display = (
        "title",
        "author",
        "body",
    )


admin.site.register(Post, PostAdmin)

Setelah itu, buka http://127.0.0.1:8000/admin/, login, lalu tambahkan minimal dua post contoh.

Admin Homepage django admin

Admin Create Post django create post

Admin Post List django postlist

6. Views, URLs, dan Templates

Tiga komponen ini adalah alur utama request-response di Django: URL menentukan rute, view memproses logika, dan template merender tampilan HTML ke pengguna.

6.1 Function-Based View

Di blog/views.py, buat view daftar post:

from django.shortcuts import render

from .models import Post


def post_list(request):
    posts = Post.objects.all()
    return render(request, "home.html", {"posts": posts})

6.2 Routing URL

Buat file urls.py pada blog isi dengan kode berikut:

from django.urls import path

from .views import post_list

urlpatterns = [
    path("", post_list, name="home"),
]

Perbarui django_project/urls.py:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("blog.urls")),
]

6.3 Template Inheritance

Buat direktori template:

mkdir templates

Perbarui django_project/settings.py:

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"], # update
        "APP_DIRS": True,
        ...
    },
]

Buat templates/base.html:

<html>
<head>
  <title>Django blog</title>
</head>
<body>
  <header>
    <h1><a href="{% url 'home' %}">Django blog</a></h1>
  </header>
  <div>
    {% block content %}
    {% endblock content %}
  </div>
</body>
</html>

Buat templates/home.html:

{% extends "base.html" %}

{% block content %}
{% for post in posts %}
<div class="post-entry">
  <h2><a href="">{{ post.title }}</a></h2>
  <p>Author: {{ post.author }}</p>
  <p>{{ post.body }}</p>
</div>
{% endfor %}
{% endblock content %}

django postlist

7. Static Files dan Styling

Static files adalah aset frontend seperti CSS, JavaScript, dan gambar yang tidak dirender sebagai data dinamis. Pada bab ini kita gunakan CSS untuk meningkatkan keterbacaan halaman blog.

Tambahkan folder static tingkat proyek:

mkdir static
mkdir static/css

Atur di django_project/settings.py:

STATIC_URL = "/static/" # update
STATICFILES_DIRS = [BASE_DIR / "static"] # add

Buat file static/css/base.css:

body {
  font-family: "Source Sans Pro", sans-serif;
  font-size: 18px;
}

header {
  border-bottom: 1px solid #999;
  margin-bottom: 2rem;
  display: flex;
}

header h1 a {
  color: red;
  text-decoration: none;
}

.post-entry {
  margin-bottom: 2rem;
}

.post-entry h2 {
  margin: 0.5rem 0;
}

.post-entry h2 a,
.post-entry h2 a:visited {
  color: blue;
  text-decoration: none;
}

.post-entry h2 a:hover {
  color: red;
}

.post-entry p {
  margin: 0;
  font-weight: 400;
}

Lalu muat static files pada templates/base.html:

{% load static %} # add
<html>
<head>
  <title>Django blog</title>
  <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400" rel="stylesheet"> # add
  <link href="{% static 'css/base.css' %}" rel="stylesheet"> # add
</head>
...

django postlist fe

8. Halaman Detail dan get_absolute_url()

8.1 View dan URL detail

Perbarui blog/views.py:

from django.shortcuts import get_object_or_404, render # upadate

from .models import Post


def post_list(request):
    posts = Post.objects.all()
    return render(request, "home.html", {"posts": posts})

# add
def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    return render(request, "post_detail.html", {"post": post})

Perbarui blog/urls.py:

from django.urls import path

from .views import post_detail, post_list

urlpatterns = [
    path("post/<int:pk>/", post_detail, name="post_detail"),
    path("", post_list, name="home"),
]

Buat templates/post_detail.html:

{% extends "base.html" %}

{% block content %}
<div class="post-entry">
  <h2>{{ post.title }}</h2>
  <p>{{ post.body }}</p>
</div>
{% endblock content %}

8.2 Tautan dari homepage ke halaman detail

Pada templates/home.html, ganti tautan judul post:

<h2><a href="{% url 'post_detail' post.pk %}">{{ post.title }}</a></h2>

django postdetail

8.3 Best practice get_absolute_url()

Supaya URL canonical tidak perlu diulang di banyak template, tambahkan method berikut pada blog/models.py:

get_absolute_url() membantu setiap objek model mengetahui URL detail-nya sendiri. Pendekatan ini membuat template lebih bersih dan mengurangi hardcode path.

from django.db import models
from django.urls import reverse # add

# update
class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    body = models.TextField()

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("post_detail", kwargs={"pk": self.pk})

Lalu pakai di template templates/home.html:

<h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>

9. Testing

Berikut contoh pengujian yang memeriksa model, URL, template, dan konten untuk list maupun detail view.

Pengujian dilakukan dengan django.test.TestCase, yang secara otomatis membuat database sementara agar data uji terisolasi dari database pengembangan.

blog/tests.py:

from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse

from .models import Post


class BlogTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = get_user_model().objects.create_user(
            username="testuser", email="test@email.com", password="secret"
        )
        cls.post = Post.objects.create(
            title="A good title",
            body="Nice body content",
            author=cls.user,
        )

    def test_post_model(self):
        self.assertEqual(self.post.title, "A good title")
        self.assertEqual(self.post.body, "Nice body content")
        self.assertEqual(self.post.author.username, "testuser")
        self.assertEqual(str(self.post), "A good title")
        self.assertEqual(self.post.get_absolute_url(), "/post/1/")

    def test_url_exists_at_correct_location_listview(self):
        response = self.client.get("/")
        self.assertEqual(response.status_code, 200)

    def test_url_exists_at_correct_location_detailview(self):
        response = self.client.get("/post/1/")
        self.assertEqual(response.status_code, 200)

    def test_post_listview(self):
        response = self.client.get(reverse("home"))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Nice body content")
        self.assertTemplateUsed(response, "home.html")

    def test_post_detailview(self):
        response = self.client.get(reverse("post_detail", kwargs={"pk": self.post.pk}))
        no_response = self.client.get("/post/100000/")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(no_response.status_code, 404)
        self.assertContains(response, "A good title")
        self.assertTemplateUsed(response, "post_detail.html")

Jalankan test:

python manage.py test

Tips penting saat mengetes URL detail: jangan lupa slash di depan dan belakang (/post/1/).

10. Git dan Commit Awal

Inisialisasi Git dan cek perubahan:

git init
git status

Buat .gitignore agar folder environment, cache, dan database lokal tidak ikut ter-commit:

.venv/
__pycache__/
db.sqlite3

Lalu lakukan commit:

git add .
git commit -am "Add project blog website with model, admin, views, urls, templates, static files, detail page, get_absolute_url, and tests"

Connect to GitHub and push:

git push -u origin main

11. Kesimpulan

Pada chapter ini kita berhasil membangun fondasi Blog Website dari nol:

  • Model Post dengan relasi user.
  • Manajemen data melalui Django Admin.
  • Halaman list dan detail post berbasis URL dinamis.
  • Styling awal dengan static files.
  • Best practice URL model dengan get_absolute_url().
  • Test suite yang mencakup model, URL, template, dan konten.

Bab berikutnya akan melanjutkan ke generic class-based views dan form agar operasi create, update, delete tidak hanya melalui admin, tetapi langsung dari antarmuka aplikasi.