Hi there, this won't be your regular blog application using django for simple CRUD operations. NO!
Let's build a feature-rich industrial standard blog application using django and react. To enhance the content creation process, we will use django's CKEditor, a powerful rich-text editor that helps you with all the tools to write engaging and well formatted articles with ease.
Whether you're a beginner or an experienced developer, this guide will provide you with a comprehensive understanding of full-stack web development. Let's dive right to it!
THE BACKEND
Set Up Django Project
In your terminal, navigate to your preferred directory and begin your project. I will call mine robin's blog. Then create your django app, let's call it "blog" for simplicity.
django-admin startproject robins_blog
cd robins_blog
python manage.py startapp blog
Create and Activate Virtual Environment (Optional)
It is always advisable to create virtual environment for your django projects so you have more freedom on the packages you wish to install. It ensures local packages don't conflict with global packages. Let's say you want to use a specific older python version, your virtual environment will let you do it for that project alone, without affecting other projects.
python -m venv venv
venv/Scripts/activate
So now we have created our virtual environments, I will just list out all the packages we need to install for our app to work perfectly.
Django==5.1.4
djangorestframework==3.15.2
django-filter==24.3
django-cors-headers==4.6.0
django-ckeditor==6.7.2
django-ckeditor-5==0.2.15
django-taggit==6.1.0
dj-rest-auth==7.0.0
django-oauth-toolkit==3.0.1
djangorestframework-simplejwt==5.3.1
PyJWT==2.10.1
oauthlib==3.2.2
pillow==11.0.0
bleach==6.2.0
Create Our Model
Let's create our model, go to the blog folder, locate model.py and define the model for Articles. From this model, we set title, slug, content, image, tags, meta title, meta description and published date.
from django.db import models
from ckeditor.fields import RichTextField
from taggit.managers import TaggableManager
from django.utils.text import slugify
class Article(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, blank=True)
content = RichTextField()
image = models.ImageField(upload_to='articles/', blank=True, null=True)
tags = TaggableManager(blank=True)
published_date = models.DateTimeField(auto_now_add=True)
meta_title = models.CharField(max_length=255, blank=True, null=True)
meta_description = models.CharField(max_length=255, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
def __str__(self):
return self.title
Modify Django Settings
Go to the project folder, we named ours "robin's blog" and make the following modifications.
At the top, import os. set Allowed_Hosts to all "[*]", add your app along with some of the packages we installed to the INSTALLED_APPS field, which should look like this;
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'oauth2_provider',
'corsheaders',
'dj_rest_auth',
'rest_framework.authtoken',
'blog',
'ckeditor',
'ckeditor_uploader',
'taggit',
]
Add the CKEditor config to settings and add corsheaders to middleware. It should look like this;
CKEDITOR_CONFIGS = {
'default': {
'toolbar': 'full',
'height': 300,
'width': '100%',
},
}
CKEDITOR_UPLOAD_PATH = 'uploads/'
CKEDITOR_IMAGE_BACKEND = "pillow"
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Add restframework configurations to settings which is just a cool way in django you define how you want your api call to behave; I added comments to tell what each line does, you can modify it to whatever suits you. The restframework section in settings should look like this;
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication', # Add token authentication
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny', # Allow access to all users so it doesnt require a token
],
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle', #Throttle is a rate limiting technique that limits number of calls on the api
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour', #100 calls for anonymous users
'user': '1500/hour', #1000 calls per hour for authenticated users
},
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'SEARCH_PARAM': 'search', # This should match the query parameter you're using in the frontend
}
Finally, set media url and media root, usually at the bottom of the settings page under statuc_url, you will have something like this; make sure you imported os at the top
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
Crate Serializers.py File
In your app folder, same folder you have your views and models, create a file and name it serializers.py. We have to build the serializer for the model we made earlier, It should look like this;
from rest_framework import serializers
import bleach
from bleach import ALLOWED_TAGS
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
clean_content = serializers.SerializerMethodField()
tags = serializers.StringRelatedField(many=True)
author = serializers.SerializerMethodField()
class Meta:
model = Article
fields = [
'id',
'title',
'slug',
'clean_content',
'image',
'tags',
'published_date',
'author',
'meta_title',
'meta_description',
# Add other fields if necessary
]
def get_clean_content(self, obj):
allowed_tags = list(ALLOWED_TAGS) + [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'blockquote', 'strong', 'em', 'u', 's', 'ol', 'ul', 'li',
'figure', 'figcaption', 'img',
]
allowed_attrs = {
'*': ['class', 'style'],
'a': ['href', 'title'],
'img': ['src', 'alt', 'title', 'width', 'height'],
}
return bleach.clean(obj.content, tags=allowed_tags, attributes=allowed_attrs, strip=True)
def get_author(self, obj):
return obj.author.first_name + ' ' + obj.author.last_name
Define Your Apis in Views.py
Now, let's write the API views for the Article model. First, we will import the necessary packages, including the Article model and its serializer. We will create API classes to handle the following functionalities: retrieving all articles, fetching specific articles based on a unique identifier like the slug, and searching articles by title, content, or tags.
# from django.shortcuts import render
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions, status, viewsets, filters
from rest_framework.response import Response
from .models import Article
from .serializers import ArticleSerializer
# Create your views here.
class ArticleList(generics.ListAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
class ArticleListView(generics.ListAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
filter_backends = [filters.SearchFilter]
search_fields = ['title', 'content', 'tags__name'] # adjust this based on your model
class ArticleDetailView(generics.RetrieveAPIView):
lookup_field = 'slug'
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def get_object(self, slug):
return get_object_or_404(Article, slug=slug)
def get(self, request, slug, format=None):
article = self.get_object(slug)
serializer = ArticleSerializer(article)
return Response(serializer.data)
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
filter_backends = [filters.SearchFilter]
search_fields = ['title', 'content', 'tags__name']
Create Urls for our Views
Now we have created our views, we need a way to access them, and we cannot do so without modifying our url.py file.
First in the url.py file in your project folder, add these urls which are necessary for ckeditor to function.
"""
URL configuration for robins_blog project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
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('api-auth/', include('rest_framework.urls')),
path('rest-auth/', include('dj_rest_auth.urls')),
path('api/', include('blog.urls')),
path('ckeditor5/', include('ckeditor_uploader.urls')),
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Then in your app folder we named "blog" you have to create a url.py file and define the urls for fetching all articles and fetching articles based on slug or id. It should look like this;
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
urlpatterns = [
path('', include(router.urls)),
path('articles/', views.ArticleListView.as_view(), name='article-list'),
path('articles/<slug:slug>/', views.ArticleDetailView.as_view(), name='article-detail')
]
Define Admin, Migrate then Create Super USer
Now we need to modify our admin.py page so when we create superuser we will be able to see our model in our django backend;
from django.contrib import admin
from .models import Article
# Register your models here.
class ArticleAdmin(admin.ModelAdmin):
list_display = ('title', 'published_date', 'author',)
search_fields = ('title', 'author',)
admin.site.register(Article, ArticleAdmin)
Now we have made all necessary changes, we have to makemigrations and migrate. Run these commands in the terminal.
python manage.py makemigrations blog
python manage.py migrate
Once they are successful, we can create a super user so we can access our django admin panel. Run this command in the terminal and follow the prompt to create a super admin.
python manage.py createsuperuser
Once all this has been completed successfully, we can now run our server. Run this code in your terminal;
python manage.py runserver
If your setup was correct, you should see the development server running at port 8000 in your terminal. Now simply visiting localhost:8000 or 127.0.0.1:8000 in your browser will launch the application.
You can visit the django admin panel by going to the url 127.0.0.1:8000/admin or localhost:8000/admin.
And there you have it for the backend. If successful, you should see this page on your browser;
Then when you click on articles and try to add articles, you should see the beautiful and powerful text editor below;
Remember, we made two api urls for fetching articles;
localhost:8000/api/articles/
loaclhost:8000/api/articles/<slug:slug>/
THE FRONTEND
Setup React Project
Now, let's dive right to it, We are done with the backend and our apis are functional, Let's create our react project.
Open your terminal and navigate to your preferred directory then run the command;
npx create-react-app blog-frontend
We are naming ours "blog-frontend", you can name it whatever you like.
We will keep it simple and use plain css and javascript components. No ui-framework, no component overload. But the outcome will still look as nice as you want.
Once your react app has been created you can simply set up the folder structure and create necessary components. Proper structuring of components is crucial when working with react, especially when working in a team. Your folder structure should look like this;
src/
├── components/
│ ├── Header.jsx // Header component
│ ├── Footer.jsx // Footer component
│ ├── ArticleList.jsx // Displays the list of articles
│ ├── ArticleDetail.jsx // Displays details of a specific article
│ ├── HeaderFooter.css // Shared CSS for Header and Footer
│ ├── ArticleList.css // CSS for the ArticleList component
│ ├── ArticleDetail.css // CSS for the ArticleDetail component
│
├── services/
│ ├── AppService.jsx // Handles API requests
│
├── App.js // Main app entry point
├── index.js // ReactDOM rendering
Now Let's Write the Code for each component.
First, the Article Detail Page
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { fetchArticle } from '../services/AppService';
import Header from '../components/Header';
import Footer from '../components/Footer';
import './ArticleDetail.css';
function ArticleDetail() {
const { slug } = useParams();
const [article, setArticle] = useState(null);
useEffect(() => {
fetchArticle(slug)
.then(response => setArticle(response.data))
.catch(error => console.error('Error fetching article:', error));
}, [slug]);
if (!article) {
return <p>Loading...</p>;
}
// Resolve full image URL
const imageUrl = article.image.startsWith('/media/')
? `http://192.168.0.180:8000${article.image}` // Replace with your base API URL or Localhost Ip
: article.image;
return (
<div>
<Header />
<div className="article-detail-container">
<div className="article-card">
<img
src={imageUrl}
alt={article.title}
className="article-detail-image"
/>
<h1 className="article-title">{article.title}</h1>
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.clean_content }}
/>
<div className="article-meta">
<p>
<strong>Tags:</strong>{' '}
{article.tags.map(tag => (
<span className="tag" key={tag}>
{tag}
</span>
))}
</p>
<p>
<strong>Author:</strong> {article.author}
</p>
</div>
</div>
</div>
<Footer />
</div>
);
}
export default ArticleDetail;
ArticleList
import React, { useEffect, useState } from 'react';
import { fetchArticles } from '../services/AppService';
import Header from './Header';
import Footer from './Footer';
import './ArticleList.css';
function ArticleList() {
const [articles, setArticles] = useState([]);
useEffect(() => {
fetchArticles()
.then(response => {
setArticles(response.data.results || response.data); // Adjust based on your API response
})
.catch(error => console.error('Error fetching articles:', error));
}, []);
return (
<div>
<Header />
<div className="article-list-container">
<div className="articles-grid">
{articles.map(article => (
<div className="article-card" key={article.id}>
<img
src={article.image || 'https://via.placeholder.com/150'}
alt={article.title}
className="article-image"
/>
<div className="article-content">
<h2 className="article-title">{article.title}</h2>
<p className="article-meta">{article.meta_description}</p>
<a href={`/article/${article.slug}`} className="read-more">
Read More
</a>
</div>
</div>
))}
</div>
</div>
<Footer />
</div>
);
}
export default ArticleList;
Footer
import React from 'react';
import './HeaderFooter.css'; // Shared styles for Header and Footer
function Footer() {
return (
<footer className="footer">
<p>Designed with 💗 by Robin Okwanma</p>
</footer>
);
}
export default Footer;
Header
import React from 'react';
import './HeaderFooter.css'; // Shared styles for Header and Footer
function Header() {
return (
<header className="header">
<h1 className='title'>Robin's 😁 Blog</h1>
</header>
);
}
export default Header;
AppService File Where we are making our API calls
import axios from 'axios';
const API = axios.create({ baseURL: 'http://192.168.0.180:8000/api/' });
// Fetch all articles
export const fetchArticles = () => API.get('articles/');
// Fetch a single article by slug
export const fetchArticle = slug => API.get(`articles/${slug}/`);
App.js file
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import ArticleList from './components/ArticleList';
import ArticleDetail from './components/ArticleDetail';
// import './App.css';
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<ArticleList />} />
<Route path="/article/:slug" element={<ArticleDetail />} />
</Routes>
</Router>
);
}
export default App;
Now, Let's Add Styling to the page we created, You can play around with the colors and style to whatever you like;
ArticleDetail.css
/* Container */
.article-detail-container {
max-width: 800px;
margin: 40px auto;
padding: 20px;
display: flex;
/* Enable Flexbox */
justify-content: center;
/* Center horizontally */
align-items: center;
/* Center vertically */
text-align: center;
/* background-color: #333; */
/* Dark background for contrast */
min-height: 100vh;
/* Make it take the full viewport height */
}
/* Image Styling */
.article-detail-image {
width: 100%;
height: auto;
border-radius: 8px 8px 0 0;
/* Rounded corners at the top */
margin-bottom: 20px;
/* Space between the image and the title */
object-fit: cover;
/* Ensures the image maintains its aspect ratio */
}
/* Card styling */
.article-card {
background-color: #007bff;
/* Blue background */
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
width: 100%;
/* Full width within the container */
max-width: 600px;
/* Limit the maximum width */
}
/* Title */
.article-title {
font-size: 2rem;
color: #fff;
/* White color for title */
margin-bottom: 20px;
}
/* Content */
.article-content {
text-align: left;
font-size: 1rem;
color: #f0f0f0;
/* Light gray for text */
margin-bottom: 20px;
}
/* Metadata (Tags and Author) */
.article-meta {
text-align: left;
font-size: 0.9rem;
color: #ccc;
/* Light gray for metadata */
}
.tag {
display: inline-block;
margin: 0 5px;
padding: 5px 10px;
background-color: #091a7c;
border-radius: 15px;
font-size: 0.8rem;
}
ArticleList.css
/* General styling */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f9f9f9;
}
.article-list-container {
text-align: center;
}
/* Responsive grid */
.articles-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 50px;
margin: 40px;
/* Adds space between grid items */
justify-items: center;
}
/* Article card */
.article-card {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
width: 100%;
max-width: 300px;
transition: transform 0.2s;
margin: 15px;
/* Adds additional margin between cards */
}
.article-card:hover {
transform: translateY(-5px);
}
.article-image {
width: 100%;
height: 150px;
object-fit: cover;
}
.article-content {
padding: 15px;
}
.article-title {
font-size: 1.2rem;
margin: 0 0 10px;
color: #222;
}
.article-meta {
font-size: 0.9rem;
color: #666;
margin: 0 0 15px;
}
/* Updated Read More button */
.read-more {
display: inline-block;
padding: 8px 12px;
background-color: #222;
/* Matches footer background */
color: #fff;
text-decoration: none;
border-radius: 5px;
font-size: 0.9rem;
}
.read-more:hover {
background-color: #444;
/* Slightly lighter on hover */
}
/* Footer styling */
.footer {
margin-top: 50px;
background-color: #222;
color: #fff;
padding: 15px 0;
text-align: center;
font-size: 0.9rem;
}
HeaderFooter.css
.header {
background-color: #007bff;
color: #fff;
padding: 5px 0;
text-align: center;
font-size: 1.5rem;
position: sticky;
margin-bottom: 30px;
}
.title {
font-size: 2rem;
}
.footer {
margin-top: 50px;
background-color: #222;
color: #fff;
padding: 15px 0;
text-align: center;
font-size: 0.9rem;
}
We have now completely set up our react frontend, you can run the server by the command npm start
in your terminal. And you should see your frontend running. Here is what it looks like;
Clicking on read more goes to the article detail page just as we programmed in the App.js page.... Whatever text formatting you set on your backend reflects perfectly in the front.
Here is a live blog I built using the same technology(django and react), https://techhive.ng/blog
Few Things To Look Out For
When working with localhost, you should use your local ip instead of localhost or 127.0.0.1:8000. You can do this by simply adding it when running your django server. Like this,
python manage.py runserver 198.168.1.56:8000
replace the ip with whatever your network ip is. This will help you call your django APIs from react.In the django settings we added throttling, you can reduce or increase this at will
Here is the link to the github repo for the backend and frontend of this project, If you run into any errors or have any questions, please let me know in the comments.
Top comments (0)