I Built My Personal Website with Django, Wagtail, Render and Supabase


Introduction

I’ve been using Django for years, but building and deploying my personal website with Wagtail, Render, and Supabase still forced me to learn an entirely new set of problems.

In this article, I’ll walk you through how I built and deployed my own "Digital Atelier" from the ground up. I’ll show you the exact steps I took to move from a local setup to a fully functional, custom-owned site.

This article will cover:

homepage

Tools and stack

Here’s the stack I'll use:

Technology Purpose
Django Backend framework
Wagtail CMS for content management
Render Hosting platform
Supabase PostgreSQL database + storage
cron-job.org Keeps free Render instance awake

Why did I choose this stack?

Screenshot location: Place a screenshot of the tech stack or dashboard after this section.

I chose this stack to push beyond my usual setup while still building on tools I trust. Django and Wagtail are familiar to me, and they offer the stability and structure I want for managing content long-term.

At the same time, this project is an opportunity to explore new tools like Render and Supabase, which I haven’t used before. Combining the familiar with the unfamiliar lets me stay productive while deliberately expanding my stack.

Planning the structure

Before writing a single line of code, I mapped out the structural blueprint to ensure a seamless flow between the user and the backend.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
User Browser
     ↓
Render Web Service
(Django + Wagtail)
     ↓
Supabase PostgreSQL
(Database)
     ↓
Supabase Storage
(Media Files)

The Stack & Data Flow

  1. Client-Side: The user interacts with the browser.
  2. Application Layer: A Django + Wagtail web service handles the rendering and logic.
  3. Persistence Layer: Supabase (PostgreSQL) manages all relational data.
  4. Asset Management: Supabase Storage hosts all media and static files.
1
2
3
4
5
6
7
8
9
Hierarchy of Pages:
    HomePage
        MusingsPage
              MusingsPostPage
        TechPage
              MusingsPostPage
        AtelierPage
              PhotoPage
        AboutPage

Inspiration of the Design

I moved away from "quick-build" platforms because I wanted complete creative sovereignty. This site is more than a portfolio; it is an Digital Atelier, a custom-coded workshop where my creativity, technical passion, and teaching spirit converge.

Visualizing the Site Hierarchy:

Inspiration Note: I took the visual polish of my previous Squarespace site and used it as a prototype to build a faster, more personal engine that I can call my own.


1. Setting up the LOCAL/DEV environment

1.1. Create virtual environment

1
2
3
> python | -m venv env
> source venv/bin/activate  # Mac/Linux
> venv\\\\Scripts\\\\activate     # Windows

1.2. Fork then Clone Repository

1
2
> git clone [email protected]:anjlapastora/anjlapastora.github.io.git
> cd anjlapastora.github.io/anj_lapastora/

1.3. Install Dependencies

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
requirements.txt:

Django>=5.2,<5.3
wagtail>=7.3,<7.4
gunicorn
psycopg2-binary
whitenoise
dj-database-url
wagtail-favicon
wagtail-markdown
pillow
pygments
django-storages
boto3
1
> pip | install -r requirements.txt

1.4. Local file setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import os
from .base import *
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

print("🔥 USING LOCAL SETTINGS 🔥")

# -------------------------
# Core
# -------------------------
DEBUG = True

SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key")

ALLOWED_HOSTS = ["*"]

# -------------------------
# Database
# -------------------------
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

# -------------------------
# Wagtail
# -------------------------
WAGTAILADMIN_BASE_URL = "<http://127.0.0.1:8000>"

# -------------------------
# Supabase Storage (S3)
# -------------------------
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"

# -------------------------
# Django 4+ storage config
# -------------------------
STORAGES = {
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
    },
}
1
2
3
4
> python manage.py makemimgrations
> python manage.py migrate
> python manage.py createsuperuser
> python manage.py runserver

1.5. Access Homepage

1
<http://127.0.0.1:8000/admin>

1.6. Configure Wagtail Site in Admin

After creating the superuser and accessing the Wagtail admin panel, the next step is configuring the website structure properly.

1.6.1. Add a Home Page:

a. Inside the left sidebar:

1
2
Pages
└── Welcome to new Wagtail Site! <-- delete this

b. After deleting the page, create a new page called “Home Page”:

image.png

1.6.2 Go to Settings, then Sites:

a. Click “Add a site”:

image.png

b. Set the following:

Hostname localhost
Port 8000
Root Page Home Page
Is default site yes

c. Go to “Pages” → “Home Page”, then add the following:

Page Name Slug
About Page about
Tech Page tech
Musings Page musings
Atelier Page atelier

1.7. Upload favicon

The favicon is the small icon displayed in the browser tab beside the website name.

To upload it,

1.7.1. Go to Settings → Favicon.

1.7.2. Configure the following:

a. App Name — the name of the website.

b. App Theme Color (optional) — mainly used for supported mobile browsers.

c. Base Favicon Image — upload the favicon image here

1.8. Upload images

These images will be used in the cover photo of the posts.

1.8.1. Go to Images.

1.8.2. Click “Add an image”.

1.9 Check the website:

1
localhost:8000/

Screenshot.png

1.10 Write custom articles in Markdown format

Instead of using standard rich text editors, I built a Markdown-based pipeline to manage my content. This approach treats my writing like code.

If you'd like to dive deeper into how Markdown works, I’ve put together a dedicated Markdown Cheat Sheet. Feel free to skim through it for a quick reference on syntax and formatting tips.


2. Deploy the Website:

2.1. Push to git

Before deploying the website to GitHub and Render, I first needed to push the latest version of the project repository.

The repository already contained the previous version of my website, so instead of creating a new one, I decided to continue using the same repository and simply push the new changes to the master branch.

1
2
3
> git add .
> git commit -m "Update website layout and deployment configuration"
> git push origin master

I also try to be more intentional when writing commit messages. At first, it’s tempting to write vague commits like update, fix, changes. Over time, messy notes become useless when I'm trying to look back at the project history. Keeping the commit messages clear makes it much easier to see exactly what changed at a glance and track down bugs faster.

2.2 Create Supabase Account

image.png

I chose Supabase for this build because I wanted to explore it firsthand. It's a developer-friendly, open-source alternative to Firebase that handles the heavy lifting of backend infrastructure. Since it's built on PostgreSQL, it provides everything I need, like instant APIs, file storage, and authentication.

2.2.1 Create a project in Supabase.

2.2.2 Once created:

  1. Go to Project Settings
  2. Open Database
  3. Copy the connection string

The connection string follows this format: postgresql://postgres:[YOUR_PASSWORD]@db.xxx.supabase.co:5432/postgres

I added this string to my environment variables so it can be securely pulled into the project’s settings file.

1
2
3
4
5
6
7
8
import dj_database_url
import os

DATABASES = {
    'default': dj_database_url.parse(
        os.environ.get('DATABASE_URL')
    )
}

2.3: Create Render Account

image.png

2.3.1 Create a Render account.

2.3.2 Create a New Web Service.

2.3.4 Connect the GitHub repository.

2.3.5 I configured the following:

1
2
3
4
5
6
7
build.sh

#!/usr/bin/env bash
pip install -r requirements.txt
python manage.py collectstatic --noinput
python manage.py migrate
python manage.py bootstrap_site
1
2
3
start.sh

gunicorn anj_lapastora.wsgi:application --workers 2 --threads 2 --timeout 30

Common Challenges & Solutions:

1. Root Directory

The root directory must align with the location of manage.py.

If the build configuration contains pathing errors, the deployment will fail. Specifically, Render must be able to locate manage.py, requirements.txt, and the Django project root to initialize the build process.

If the repository uses a nested structure, such as:

1
2
3
portfolio/
└── mysite/
        ├── manage.py

The Root Directory setting in the Render dashboard must be explicitly set to mysite rather than the top-level portfolio directory. This ensures the environment correctly maps the working directory to the application entry point, a common configuration oversight that leads to failed deployments.

2: Configure Environment Variables

Inside Render, I needed to add the following environment variables:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ALLOWED_HOSTS
AWS_ACCESS_KEY_ID
AWS_DEFAULT_ACL
AWS_S3_ENDPOINT_URL
AWS_S3_REGION_NAME
AWS_S3_SIGNATURE_VERSION
AWS_STORAGE_BUCKET_NAME
DATABASE_URL
DJANGO_SETTINGS_MODULE
DOMAIN
SUPABASE_PROJECT_ID

3: Static Files Configuration

To optimize the production environment for static asset delivery, I needed to implement WhiteNoise. This allows the web service to serve its own static files without relying on a dedicated CDN or external server.

1
> pip install whitenoise

Register the middleware in settings.py. Make sure it is positioned immediately following the Django SecurityMiddleware:

1
2
3
4
MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'whitenoise.middleware.WhiteNoiseMiddleware',
]

Define the collection directory and URL prefix:

1
2
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATIC_URL = '/static/'

Why this matters:

In a local development environment, Django’s runserver automatically manages static file discovery. However, in a production environment, Django is not designed to serve these files efficiently.

Without a library like WhiteNoise to intercept these requests, the application will fail to resolve paths for CSS, images, and JavaScript—resulting in a broken UI and unstyled admin dashboard. This remains one of the most frequent configuration errors during initial deployment cycles.

4: Media Storage and Supabase Storage

In production environments, it is critical to distinguish between Static and Media assets, as they require different handling within the architecture.

Storing media files directly on the local filesystem of Render instances. Because Render’s free tier utilizes ephemeral storage, any files saved locally will be lost upon a service restart or redeployment.

To solve this, I implemented Supabase Storage as an external object storage solution. This integration involves specific configuration logic, which warrants a dedicated article to explain the system fully.

5: Storage Policies and RLS

Integrating Supabase Storage requires a precise configuration of its security layer. If media assets fail to resolve on the front end, the root cause is rarely found in the Django configuration; it typically resides within the Storage Access Policies.

Understanding Row-Level Security (RLS) Supabase implements Row-Level Security (RLS) to govern interactions with the storage.objects table. I must explicitly define granular policies to authorize specific actions:

Implementation Example To enable public read access for a specific bucket, I must execute a SQL policy similar to the following:

1
2
3
4
CREATE POLICY "Public Read Access"
ON storage.objects
FOR SELECT
USING ( bucket_id = 'media' );

Security Best Practices

It is critical to avoid blindly implementing wide-open policies found in documentation. While disabling RLS or allowing ALL

actions for ANON roles might resolve immediate upload failures, it creates significant security vulnerabilities. A common beginner pitfall is bypassing the security layer entirely to fix a "broken" configuration—always prioritize the Principle of Least Privilege by granting only the minimum permissions necessary for the application to function.

6: Migrations in Production

One common issue I encountered involved manual migration management. When I modify models or page structures in Wagtail, Django may not always auto-detect those changes across all apps. I had to explicitly target the home app to generate the necessary files:

1
2
> python manage.py makemigrations home
> python manage.py migrate

Never assume the migration state is identical between environments. If the production site behaves differently than the local build, always verify the migration files first. Ensuring the database schema is in sync is a critical step in any deployment workflow.

7: Using Free-tier

image.png

Since I am using Render’s free tier, the service is subject to "spinning down" after periods of inactivity. This results in cold starts, where the first request to the site can be significantly delayed while the instance reboots.

I implemented a workaround using cron-job.org. This service sends periodic HTTP pings to the site, effectively keeping the instance "awake" and responsive.

Use Cases & Tradeoffs


Final Result

After resolving a few initial technical hurdles, here is the final deployment of the site.

The process demonstrated that building from scratch provides the flexibility to implement any specific feature or vision. My next steps involve refining the UI and optimizing the codebase for better performance and scalability.

fin.gif