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