Django Blog Project
Complete Step-by-Step Guide for Beginners
Stack: Django • SQLite • Tailwind CSS • Dark Mode • Profile Pictures
1. Project Setup
1.1 Create Project Structure
Open your terminal in Kali and run these commands one by one:
# Navigate to your desired folder (e.g. Desktop)
cd ~/Desktop
# Create the Django project
django-admin startproject myblog
cd myblog
# Create the blog app
python3 [Link] startapp blog
# Create the users app
python3 [Link] startapp users
1.2 Install Required Packages
Install Pillow for image uploads (profile pictures):
pip3 install Pillow
1.3 Configure [Link]
Open myblog/[Link] and make these changes:
# In INSTALLED_APPS, add your apps:
INSTALLED_APPS = [
'[Link]',
'[Link]',
'[Link]',
'[Link]',
'[Link]',
'[Link]',
'blog', # <-- add this
'users', # <-- add this
]
# At the bottom of [Link], add:
import os
MEDIA_URL = '/media/'
MEDIA_ROOT = [Link](BASE_DIR, 'media')
LOGIN_REDIRECT_URL = 'blog-home'
LOGIN_URL = 'login'
💡 Note: BASE_DIR is already defined near the top of [Link] — don't re-define it.
2. Database Models
2.1 Blog Post Model — blog/[Link]
Replace the contents of blog/[Link] with:
from [Link] import models
from [Link] import User
from [Link] import timezone
class Post([Link]):
title = [Link](max_length=200)
content = [Link]()
image = [Link](upload_to='post_pics/', blank=True, null=True)
author = [Link](User, on_delete=[Link])
created = [Link](default=[Link])
def __str__(self):
return [Link]
class Meta:
ordering = ['-created']
2.2 User Profile Model — users/[Link]
Replace the contents of users/[Link] with:
from [Link] import models
from [Link] import User
class Profile([Link]):
user = [Link](User, on_delete=[Link])
image = [Link](default='[Link]', upload_to='profile_pics/')
def __str__(self):
return f'{[Link]} Profile'
2.3 Auto-create Profile — users/[Link]
Create a new file users/[Link]:
from [Link] import post_save
from [Link] import User
from [Link] import receiver
from .models import Profile
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
[Link](user=instance)
@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
[Link]()
2.4 Register Signals — users/[Link]
Edit users/[Link] so signals load automatically:
from [Link] import AppConfig
class UsersConfig(AppConfig):
default_auto_field = '[Link]'
name = 'users'
def ready(self):
import [Link]
2.5 Run Migrations
python3 [Link] makemigrations
python3 [Link] migrate
3. Forms
3.1 User Forms — users/[Link]
Create users/[Link]:
from django import forms
from [Link] import User
from [Link] import UserCreationForm
from .models import Profile
class RegisterForm(UserCreationForm):
email = [Link]()
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
class UserUpdateForm([Link]):
email = [Link]()
class Meta:
model = User
fields = ['username', 'email']
class ProfileUpdateForm([Link]):
class Meta:
model = Profile
fields = ['image']
3.2 Post Form — blog/[Link]
Create blog/[Link]:
from django import forms
from .models import Post
class PostForm([Link]):
class Meta:
model = Post
fields = ['title', 'content', 'image']
4. Views
4.1 Blog Views — blog/[Link]
from [Link] import render, get_object_or_404, redirect
from [Link] import login_required
from [Link] import User
from .models import Post
from .forms import PostForm
def home(request):
posts = [Link]()
return render(request, 'blog/[Link]', {'posts': posts})
@login_required
def post_create(request):
if [Link] == 'POST':
form = PostForm([Link], [Link])
if form.is_valid():
post = [Link](commit=False)
[Link] = [Link]
[Link]()
return redirect('blog-home')
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form, 'title': 'New
Post'})
@login_required
def post_edit(request, pk):
post = get_object_or_404(Post, pk=pk)
if [Link] != [Link]:
return redirect('blog-home')
if [Link] == 'POST':
form = PostForm([Link], [Link], instance=post)
if form.is_valid():
[Link]()
return redirect('blog-home')
else:
form = PostForm(instance=post)
return render(request, 'blog/post_form.html', {'form': form, 'title': 'Edit
Post'})
@login_required
def post_delete(request, pk):
post = get_object_or_404(Post, pk=pk)
if [Link] == [Link]:
[Link]()
return redirect('blog-home')
4.2 User Views — users/[Link]
from [Link] import render, redirect
from [Link] import login_required
from [Link] import messages
from .forms import RegisterForm, UserUpdateForm, ProfileUpdateForm
def register(request):
if [Link] == 'POST':
form = RegisterForm([Link])
if form.is_valid():
[Link]()
[Link](request, 'Account created! You can now log in.')
return redirect('login')
else:
form = RegisterForm()
return render(request, 'users/[Link]', {'form': form})
@login_required
def profile(request):
if [Link] == 'POST':
u_form = UserUpdateForm([Link], instance=[Link])
p_form = ProfileUpdateForm([Link], [Link],
instance=[Link])
if u_form.is_valid() and p_form.is_valid():
u_form.save()
p_form.save()
[Link](request, 'Profile updated!')
return redirect('profile')
else:
u_form = UserUpdateForm(instance=[Link])
p_form = ProfileUpdateForm(instance=[Link])
return render(request, 'users/[Link]', {'u_form': u_form, 'p_form':
p_form})
5. URL Configuration
5.1 Main URLs — myblog/[Link]
from [Link] import admin
from [Link] import path, include
from [Link] import views as auth_views
from [Link] import settings
from [Link] import static
urlpatterns = [
path('admin/', [Link]),
path('', include('[Link]')),
path('users/', include('[Link]')),
path('login/', auth_views.LoginView.as_view(template_name='users/[Link]'),
name='login'),
path('logout/',
auth_views.LogoutView.as_view(template_name='users/[Link]'), name='logout'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
5.2 Blog URLs — blog/[Link]
Create blog/[Link]:
from [Link] import path
from . import views
urlpatterns = [
path('', [Link], name='blog-home'),
path('post/new/', views.post_create, name='post-create'),
path('post/<int:pk>/edit/', views.post_edit, name='post-edit'),
path('post/<int:pk>/delete/', views.post_delete, name='post-delete'),
]
5.3 Users URLs — users/[Link]
Create users/[Link]:
from [Link] import path
from . import views
urlpatterns = [
path('register/', [Link], name='register'),
path('profile/', [Link], name='profile'),
]
6. Templates
6.1 Folder Structure
Create these folders first:
mkdir -p blog/templates/blog
mkdir -p users/templates/users
mkdir -p media/profile_pics
💡 Note: You also need a default profile image. Download any image and save it as
media/[Link]
6.2 Base Template — blog/templates/blog/[Link]
This is the master layout with Tailwind CSS via CDN + dark mode toggle:
<!DOCTYPE html>
<html lang='en' id='html-root'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>MyBlog</title>
<script src='[Link]
<script>
[Link] = { darkMode: 'class' }
</script>
</head>
<body class='bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-
screen transition-colors duration-300'>
<!-- NAVBAR -->
<nav class='bg-blue-700 dark:bg-blue-900 text-white px-6 py-3 flex items-center
justify-between shadow-lg'>
<a href='{% url "blog-home" %}' class='text-2xl font-bold tracking-
tight'>MyBlog</a>
<div class='flex items-center gap-4 text-sm'>
<a href='{% url "blog-home" %}' class='hover:underline'>Home</a>
{% if user.is_authenticated %}
<a href='{% url "post-create" %}' class='hover:underline'>New Post</a>
<a href='{% url "profile" %}' class='hover:underline'>Profile</a>
<a href='{% url "logout" %}' class='hover:underline'>Logout</a>
{% else %}
<a href='{% url "register" %}' class='hover:underline'>Register</a>
<a href='{% url "login" %}' class='hover:underline'>Login</a>
{% endif %}
<!-- Dark Mode Toggle -->
<button onclick='toggleDark()' class='ml-2 bg-white text-blue-700 dark:bg-gray-
700 dark:text-white px-3 py-1 rounded-full text-xs font-bold'>
🌙 Dark
</button>
</div>
</nav>
<!-- FLASH MESSAGES -->
<div class='max-w-4xl mx-auto mt-4 px-4'>
{% for message in messages %}
<div class='bg-green-100 border border-green-400 text-green-800 px-4 py-2
rounded mb-2'>
{{ message }}
</div>
{% endfor %}
</div>
<!-- MAIN CONTENT -->
<main class='max-w-4xl mx-auto px-4 py-8'>
{% block content %}{% endblock %}
</main>
<script>
function toggleDark() {
[Link]('dark');
[Link]('dark',
[Link]('dark'));
}
if ([Link]('dark') === 'true') {
[Link]('dark');
}
</script>
</body>
</html>
6.3 Home Page — blog/templates/blog/[Link]
{% extends 'blog/[Link]' %}
{% block content %}
<h1 class='text-3xl font-bold mb-6'>All Posts</h1>
{% for post in posts %}
<div class='bg-white dark:bg-gray-800 rounded-xl shadow p-6 mb-4'>
{% if [Link] %}
<img src='{{ [Link] }}' class='w-full h-48 object-cover rounded-lg
mb-4'>
{% endif %}
<h2 class='text-xl font-semibold'>{{ [Link] }}</h2>
<p class='text-sm text-gray-500 dark:text-gray-400 mb-2'>
By {{ [Link] }} • {{ [Link]|date:'M d, Y' }}
</p>
<p class='text-gray-700 dark:text-gray-300'>{{ [Link]|
truncatewords:30 }}</p>
{% if user == [Link] %}
<div class='mt-3 flex gap-3'>
<a href='{% url "post-edit" [Link] %}' class='text-blue-500
hover:underline text-sm'>Edit</a>
<a href='{% url "post-delete" [Link] %}' class='text-red-500
hover:underline text-sm'
onclick='return confirm("Delete this post?")'>Delete</a>
</div>
{% endif %}
</div>
{% empty %}
<p class='text-gray-500'>No posts yet. <a href='{% url "post-create" %}'
class='text-blue-500 hover:underline'>Create the first one!</a></p>
{% endfor %}
{% endblock %}
6.4 Post Form — blog/templates/blog/post_form.html
{% extends 'blog/[Link]' %}
{% block content %}
<div class='bg-white dark:bg-gray-800 rounded-xl shadow p-8 max-w-2xl mx-auto'>
<h1 class='text-2xl font-bold mb-6'>{{ title }}</h1>
<form method='POST' enctype='multipart/form-data'>
{% csrf_token %}
{% for field in form %}
<div class='mb-4'>
<label class='block font-semibold mb-1 text-sm'>{{ [Link] }}</label>
{{ field }}
{% if [Link] %}
<p class='text-red-500 text-xs mt-1'>{{ [Link]|join:', ' }}</p>
{% endif %}
</div>
{% endfor %}
<button type='submit' class='bg-blue-600 hover:bg-blue-700 text-white px-6 py-2
rounded-lg font-semibold'>
Save Post
</button>
</form>
</div>
{% endblock %}
6.5 Register — users/templates/users/[Link]
{% extends 'blog/[Link]' %}
{% block content %}
<div class='bg-white dark:bg-gray-800 rounded-xl shadow p-8 max-w-md mx-auto'>
<h1 class='text-2xl font-bold mb-6'>Create Account</h1>
<form method='POST'>
{% csrf_token %}
{% for field in form %}
<div class='mb-4'>
<label class='block font-semibold mb-1 text-sm'>{{ [Link] }}</label>
{{ field }}
{% if [Link] %}
<p class='text-red-500 text-xs mt-1'>{{ [Link]|join:', ' }}</p>
{% endif %}
{% if field.help_text %}
<p class='text-gray-400 text-xs mt-1'>{{ field.help_text }}</p>
{% endif %}
</div>
{% endfor %}
<button type='submit' class='w-full bg-blue-600 hover:bg-blue-700 text-white
py-2 rounded-lg font-semibold'>
Register
</button>
</form>
<p class='mt-4 text-sm text-center'>Already have an account? <a href='{% url
"login" %}' class='text-blue-500 hover:underline'>Login</a></p>
</div>
{% endblock %}
6.6 Login — users/templates/users/[Link]
{% extends 'blog/[Link]' %}
{% block content %}
<div class='bg-white dark:bg-gray-800 rounded-xl shadow p-8 max-w-md mx-auto'>
<h1 class='text-2xl font-bold mb-6'>Login</h1>
<form method='POST'>
{% csrf_token %}
{% for field in form %}
<div class='mb-4'>
<label class='block font-semibold mb-1 text-sm'>{{ [Link] }}</label>
{{ field }}
{% if [Link] %}
<p class='text-red-500 text-xs mt-1'>{{ [Link]|join:', ' }}</p>
{% endif %}
</div>
{% endfor %}
<button type='submit' class='w-full bg-blue-600 hover:bg-blue-700 text-white
py-2 rounded-lg font-semibold'>
Login
</button>
</form>
<p class='mt-4 text-sm text-center'>No account? <a href='{% url "register" %}'
class='text-blue-500 hover:underline'>Register</a></p>
</div>
{% endblock %}
6.7 Logout — users/templates/users/[Link]
{% extends 'blog/[Link]' %}
{% block content %}
<div class='text-center mt-20'>
<h1 class='text-3xl font-bold mb-4'>You have been logged out.</h1>
<a href='{% url "login" %}' class='text-blue-500 hover:underline text-lg'>Login
again</a>
</div>
{% endblock %}
6.8 Profile — users/templates/users/[Link]
{% extends 'blog/[Link]' %}
{% block content %}
<div class='bg-white dark:bg-gray-800 rounded-xl shadow p-8 max-w-lg mx-auto'>
<div class='flex items-center gap-6 mb-6'>
<img src='{{ [Link] }}' class='w-24 h-24 rounded-full object-
cover border-4 border-blue-400'>
<div>
<h1 class='text-2xl font-bold'>{{ [Link] }}</h1>
<p class='text-gray-500'>{{ [Link] }}</p>
</div>
</div>
<form method='POST' enctype='multipart/form-data'>
{% csrf_token %}
<div class='mb-4'>
<label class='block font-semibold mb-1 text-sm'>Username</label>
{{ u_form.username }}
</div>
<div class='mb-4'>
<label class='block font-semibold mb-1 text-sm'>Email</label>
{{ u_form.email }}
</div>
<div class='mb-4'>
<label class='block font-semibold mb-1 text-sm'>Profile Picture</label>
{{ p_form.image }}
</div>
<button type='submit' class='bg-blue-600 hover:bg-blue-700 text-white px-6 py-2
rounded-lg font-semibold'>
Update Profile
</button>
</form>
</div>
{% endblock %}
7. Style Form Inputs with Tailwind
Django renders form inputs as plain HTML. To apply Tailwind classes to them, add this to both
[Link] files. Edit blog/[Link]:
from django import forms
from .models import Post
INPUT_CLASS = 'w-full border border-gray-300 dark:border-gray-600 rounded-lg px-3
py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none
focus:ring-2 focus:ring-blue-500'
class PostForm([Link]):
class Meta:
model = Post
fields = ['title', 'content', 'image']
widgets = {
'title': [Link](attrs={'class': INPUT_CLASS}),
'content': [Link](attrs={'class': INPUT_CLASS, 'rows': 6}),
}
Edit users/[Link] — add the same INPUT_CLASS and widgets to RegisterForm,
UserUpdateForm, and ProfileUpdateForm:
INPUT_CLASS = 'w-full border border-gray-300 dark:border-gray-600 rounded-lg px-3
py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none
focus:ring-2 focus:ring-blue-500'
class RegisterForm(UserCreationForm):
email = [Link](widget=[Link](attrs={'class': INPUT_CLASS}))
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
widgets = {
'username': [Link](attrs={'class': INPUT_CLASS}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
[Link]['password1'].[Link]({'class': INPUT_CLASS})
[Link]['password2'].[Link]({'class': INPUT_CLASS})
class UserUpdateForm([Link]):
email = [Link](widget=[Link](attrs={'class': INPUT_CLASS}))
class Meta:
model = User
fields = ['username', 'email']
widgets = { 'username': [Link](attrs={'class': INPUT_CLASS}) }
class ProfileUpdateForm([Link]):
class Meta:
model = Profile
fields = ['image']
8. Create Superuser & Run
# Create an admin account
python3 [Link] createsuperuser
# Start the development server
python3 [Link] runserver
Open your browser and go to: [Link]
Admin panel: [Link]
9. Final File Structure
Your project should look like this:
myblog/
├── [Link]
├── myblog/
│ ├── [Link]
│ └── [Link]
├── blog/
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ └── templates/blog/
│ ├── [Link]
│ ├── [Link]
│ └── post_form.html
├── users/
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ └── templates/users/
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ └── [Link]
└── media/
└── [Link]
10. Push to GitHub
Step 1: Create a new repository on [Link] (no README, no .gitignore)
Step 2: In your project folder, run:
# Create .gitignore first
echo '__pycache__/\n*.pyc\nmedia/\ndb.sqlite3\[Link]' > .gitignore
git init
git add .
git commit -m 'Initial Django blog project'
git branch -M main
git remote add origin [Link]
git push -u origin main
💡 Note: Replace YOUR_USERNAME and YOUR_REPO with your actual GitHub username and
repo name.
11. Common Errors & Fixes
'No module named Pillow'
pip3 install Pillow
Profile has no image / RelatedObjectDoesNotExist
This means the signals didn't fire for an existing user. Fix by running:
python3 [Link] shell
>>> from [Link] import User
>>> from [Link] import Profile
>>> for u in [Link]():
... [Link].get_or_create(user=u)
>>> exit()
TemplateDoesNotExist
Double-check your folder path. Django is case-sensitive. Verify: blog/templates/blog/[Link]
(not Blog or BLOG).
CSRF verification failed
Always include {% csrf_token %} inside every <form> tag.
Good luck with your project!