Building CommunityForge: A Full-Stack Django Community Blog with PWA Support

python dev.to

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
Enter fullscreen mode Exit fullscreen mode

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')
    )
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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);
        })
    );
});
Enter fullscreen mode Exit fullscreen mode

2. Web App Manifest (Installable)

{"name":"CommunityForge","short_name":"CForge","display":"standalone","theme_color":"#00ffff","icons":[{"src":"/static/icons/icon-192.png","sizes":"192x192"}]}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Register — Choose username, email, password and user type
  2. Login — With animated monkey password field
  3. Password Reset — Via Gmail SMTP email link
  4. Password Change — From profile page
  5. Profile Update — Change bio, avatar, user type

Challenges & Lessons Learned

  1. Custom User Model — Always create a custom user model at the start of a Django project before any migrations.

  2. PostgreSQL on Render — SQLite doesn't work on Render's free tier because the filesystem is ephemeral. Always use PostgreSQL for production.

  3. Cloudinary for Media — Same issue with media files — they disappear on every deploy without cloud storage.

  4. PWA on Django — Service workers need to be served from the root path. Make sure your static files are configured correctly.

  5. Environment Variables — Never commit .env to GitHub. Use python-decouple to 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

Source: dev.to

arrow_back Back to Tutorials