DEV Community

Tmx Dripco
Tmx Dripco

Posted on

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

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

Top comments (0)