Building CommunityForge: A Full-Stack Django Community Blog with PWA Support
Introduction
In this article, I'll walk you through how I built CommunityForge — a full-stack community blog and forum platform built with Django and Tailwind CSS. The app has a complete user flow, two user types, profile images, and Progressive Web App (PWA) features.
Live Demo: https://communityforge.onrender.com
GitHub: https://github.com/blackmurithi/communityforge
🛠️ Tech Stack
- Backend: Django 6.0
- Frontend: Tailwind CSS (CDN)
- Database: PostgreSQL (Render) / SQLite (local)
- Image Storage: Cloudinary
- Email: Gmail SMTP
- Deployment: Render.com
📋 Features
- Two user types: Author and Reader
- Full user flow: Register, Login, Password Reset, Password Change
- Profile images powered by Cloudinary
- Authors can create, edit and delete posts
- Readers can comment on posts
- Categories and filtering
- Dark theme UI with hover animations
- Progressive Web App (PWA) — installable and offline support
- Animated monkey on login 🐒
🚀 Project Setup
Step 1: Create Project Structure
mkdir communityforge
cd communityforge
python -m venv venv
venv\Scripts\activate # Windows
pip install django pillow python-decouple whitenoise gunicorn django-crispy-forms crispy-tailwind cloudinary django-cloudinary-storage psycopg2-binary dj-database-url
django-admin startproject core .
python manage.py startapp accounts
python manage.py startapp blog
Step 2: Configure Settings
We used python-decouple to manage environment variables and configured Django to use PostgreSQL on production and SQLite locally.
DATABASES = {
'default': dj_database_url.config(
default=config('DATABASE_URL', default=f'sqlite:///{BASE_DIR}/db.sqlite3')
)
}
Step 3: Custom User Model
We created a custom user model with two user types — Author and Reader:
class CustomUser(AbstractUser):
USER_TYPE_CHOICES = (
('author', 'Author'),
('reader', 'Reader'),
)
user_type = models.CharField(max_length=10, choices=USER_TYPE_CHOICES, default='reader')
bio = models.TextField(blank=True, null=True)
profile_image = models.ImageField(upload_to='profiles/', blank=True, null=True)
def is_author(self):
return self.user_type == 'author'
def is_reader(self):
return self.user_type == 'reader'
Step 4: Blog Models
class Post(models.Model):
STATUS_CHOICES = (
('draft', 'Draft'),
('published', 'Published'),
)
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
author = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
content = models.TextField()
cover_image = models.ImageField(upload_to='posts/', blank=True, null=True)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
created_at = models.DateTimeField(auto_now_add=True)
🎨 UI Design
We went with a dark theme throughout the app using pure CSS with Tailwind CDN. Key design decisions:
- Dark background:
#0a0a0f - Cyan accent:
#00ffff - Purple accent:
#cc00ff - Glowing borders on hover
- Smooth transitions and animations
Animated Monkey Login 🐒
One of the coolest features is the animated monkey on the login page. When you focus on the password field, the monkey covers its eyes with its hands!
passwordInput.addEventListener('focus', () => {
monkeyHands.classList.add('peek');
monkeyEyes.classList.add('peek');
});
passwordInput.addEventListener('blur', () => {
monkeyHands.classList.remove('peek');
monkeyEyes.classList.remove('peek');
});
📱 PWA Implementation
We implemented two PWA features:
1. Service Worker (Offline Support)
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
if (response) return response;
return fetch(event.request);
})
);
});
2. Web App Manifest (Installable)
{
"name": "CommunityForge",
"short_name": "CForge",
"display": "standalone",
"theme_color": "#00ffff",
"icons": [
{
"src": "/static/icons/icon-192.png",
"sizes": "192x192"
}
]
}
☁️ Cloudinary Integration
We used Cloudinary for storing profile images and post cover images so they persist across deployments:
CLOUDINARY_STORAGE = {
'CLOUD_NAME': config('CLOUDINARY_CLOUD_NAME'),
'API_KEY': config('CLOUDINARY_API_KEY'),
'API_SECRET': config('CLOUDINARY_API_SECRET'),
}
DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage'
🚀 Deployment on Render
build.sh
#!/usr/bin/env bash
set -o errexit
pip install -r requirements.txt
python manage.py collectstatic --no-input
python manage.py migrate
Environment Variables on Render
| Variable | Description |
|---|---|
SECRET_KEY |
Django secret key |
DEBUG |
False in production |
DATABASE_URL |
PostgreSQL URL from Render |
CLOUDINARY_CLOUD_NAME |
Cloudinary credentials |
EMAIL_HOST_USER |
Gmail address |
EMAIL_HOST_PASSWORD |
Gmail app password |
🔐 Complete User Flow
- Register — Choose username, email, password and user type
- Login — With animated monkey password field
- Password Reset — Via Gmail SMTP email link
- Password Change — From profile page
- Profile Update — Change bio, avatar, user type
🎯 Challenges & Lessons Learned
Custom User Model — Always create a custom user model at the start of a Django project before any migrations.
PostgreSQL on Render — SQLite doesn't work on Render's free tier because the filesystem is ephemeral. Always use PostgreSQL for production.
Cloudinary for Media — Same issue with media files — they disappear on every deploy without cloud storage.
PWA on Django — Service workers need to be served from the root path. Make sure your static files are configured correctly.
Environment Variables — Never commit
.envto GitHub. Usepython-decoupleto manage secrets.
🏁 Conclusion
Building CommunityForge was a great exercise in full-stack Django development. The combination of Django's powerful backend, Tailwind's utility-first CSS, and Render's free hosting makes it easy to build and deploy production-ready apps.
Live Demo: https://communityforge.onrender.com
GitHub: https://github.com/blackmurithi/communityforge
Built with ❤️ using Django, Tailwind CSS, Cloudinary and Render
Top comments (0)