Mit Django eine Business-Website von Grund auf entwickeln
Table of Contents
Dieser Leitfaden beginnt nicht als theoretischer Django-Artikel. Das Ziel ist, zuerst ein echtes Projekt aufzubauen, jeden Schritt zu testen, die Seite im Browser zu pruefen und erst danach den Blogbeitrag daraus zu formen.
Unser Szenario ist ein lokales Reinigungsunternehmen namens Mudos Clean. Die Website soll Services auflisten, Detailseiten fuer einzelne Services anzeigen, Angebotsanfragen sammeln und dem Unternehmen erlauben, Services ueber das Django-Admin zu verwalten.
Wir bauen also zuerst das funktionierende System und schreiben den Artikel auf Basis dieser Beweise.
1. Die Umgebung verifizieren
Das Projekt startete in einem leeren Ordner. Bevor Django-Dateien erzeugt wurden, wurde die Umgebung geprueft:
python --version
python -m django --version
Verifizierte Umgebung:
Python 3.11.9
Django 5.0.2
Das ist wichtig, weil alle spaeteren Schritte auf einer realen Django-Installation beruhen.
2. Das Django-Projekt erstellen
Das Projekt wurde mit folgendem Befehl erstellt:
python -m django startproject config .
Unter Windows ist django-admin nicht immer im PATH verfuegbar. python -m django ist deshalb ein robusterer Startpunkt. Der Befehl erzeugte das Projektpaket config und den Einstiegspunkt manage.py.
Erste Struktur:
config/
manage.py
3. Die Services-App erstellen
Die Business-Logik wird in einer eigenen App gehalten:
python manage.py startapp services
Danach wurde sie in config/settings.py unter INSTALLED_APPS registriert:
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"services",
]
Damit liegen Service-Listen, Angebotsanfragen, Formulare, Views, Templates und Tests sauber in der services-App.
4. Das Business-Modell aufbauen
Fuer diese Business-Website reichen zwei zentrale Datenmodelle:
Service: ein Reinigungsservice, den das Unternehmen anbietetQuoteRequest: eine Angebotsanfrage eines Besuchers
Erstes Modell:
from django.db import models
class Service(models.Model):
title = models.CharField(max_length=120)
slug = models.SlugField(unique=True)
short_description = models.CharField(max_length=240)
description = models.TextField()
starting_price = models.DecimalField(max_digits=10, decimal_places=2)
is_featured = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["title"]
def __str__(self):
return self.title
class QuoteRequest(models.Model):
TIMING_CHOICES = [
("once", "One-time"),
("weekly", "Weekly"),
("monthly", "Monthly"),
]
service = models.ForeignKey(
Service,
on_delete=models.PROTECT,
related_name="quote_requests",
)
full_name = models.CharField(max_length=120)
phone = models.CharField(max_length=40)
email = models.EmailField(blank=True)
address = models.CharField(max_length=255)
timing = models.CharField(max_length=20, choices=TIMING_CHOICES)
note = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
is_contacted = models.BooleanField(default=False)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return f"{self.full_name} - {self.service.title}"
Diese Modellentscheidungen haben praktische Gruende:
sluggibt jedem Service eine stabile URL.is_featurederlaubt hervorgehobene Services auf der Homepage.PROTECTverhindert, dass ein Service geloescht wird, solange Angebotsanfragen daran haengen.is_contactedgibt dem Unternehmen einen einfachen Follow-up-Status.
5. Admin-Verwaltung hinzufuegen
Eine Business-Website braucht frueh eine einfache Inhaltsverwaltung. Deshalb wurde das Django-Admin direkt konfiguriert.
services/admin.py:
from django.contrib import admin
from .models import QuoteRequest, Service
@admin.register(Service)
class ServiceAdmin(admin.ModelAdmin):
list_display = ("title", "starting_price", "is_featured", "created_at")
list_filter = ("is_featured",)
search_fields = ("title", "short_description")
prepopulated_fields = {"slug": ("title",)}
@admin.register(QuoteRequest)
class QuoteRequestAdmin(admin.ModelAdmin):
list_display = (
"full_name",
"service",
"phone",
"timing",
"is_contacted",
"created_at",
)
list_filter = ("service", "timing", "is_contacted")
search_fields = ("full_name", "phone", "email", "address")
Damit kann das Unternehmen Services und eingehende Anfragen verwalten, ohne Code zu aendern.
6. Den oeffentlichen Workflow bauen
Der erste oeffentliche Workflow besteht aus drei Seiten:
- Service-Liste
- Service-Detailseite mit Anfrageformular
- Erfolgsseite nach der Anfrage
App-URLs:
from django.urls import path
from . import views
app_name = "services"
urlpatterns = [
path("", views.service_list, name="service_list"),
path("services/<slug:slug>/", views.service_detail, name="service_detail"),
path("quote-request-received/", views.quote_success, name="quote_success"),
]
Projekt-URLs:
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("services.urls")),
]
Jetzt gibt es echte Routen, die Browser und Tests aufrufen koennen.
7. Das Angebotsformular hinzufuegen
Das Formular basiert auf dem QuoteRequest-Modell:
from django import forms
from .models import QuoteRequest
class QuoteRequestForm(forms.ModelForm):
class Meta:
model = QuoteRequest
fields = ["full_name", "phone", "email", "address", "timing", "note"]
Die Detail-View rendert die Seite und speichert bei POST das Formular:
def service_detail(request, slug):
service = get_object_or_404(Service, slug=slug)
if request.method == "POST":
form = QuoteRequestForm(request.POST)
if form.is_valid():
quote_request = form.save(commit=False)
quote_request.service = service
quote_request.save()
return redirect("services:quote_success")
else:
form = QuoteRequestForm()
return render(
request,
"services/service_detail.html",
{
"service": service,
"form": form,
},
)
Damit entsteht der erste vollstaendige Business-Loop: Ein Besucher sieht einen Service, fuellt ein Formular aus und der Lead wird in der Datenbank gespeichert.
8. Die Database Migration erstellen
Nach den Modellen wurde die Migration erzeugt:
python manage.py makemigrations services
Ausgabe:
Migrations for 'services':
services\migrations\0001_initial.py
- Create model Service
- Create model QuoteRequest
Das beweist, dass die Modellschicht in eine konkrete Datenbankaenderung uebersetzt wurde.
9. Den Systemcheck ausfuehren
Vor den Tests wurde Djangos eingebauter Check ausgefuehrt:
python manage.py check
Ausgabe:
System check identified no issues (0 silenced).
Damit sind Settings, App-Registrierung, URL-Konfiguration und Modelle strukturell gueltig.
10. Den ersten Browser-Check reparieren
Der erste Browser-Check zeigte zwei echte Probleme.
Erstens: Vor den Migrationen fuehrte der Seitenaufruf zu einem Serverfehler. Die Loesung:
python manage.py migrate --noinput
Danach lieferte die Homepage 200 OK.
Zweitens: Die Seite funktionierte technisch, wirkte aber leer, weil die Datenbank noch keine Services enthielt. Das ist fuer ein neues Django-Projekt normal, aber fuer eine Demo kein guter erster Eindruck. Deshalb wurde ein Seed-Command hinzugefuegt:
python manage.py seed_services
Der Command laedt drei Demo-Services:
- Home Cleaning
- Office Cleaning
- Move-Out Cleaning
Ausgabe:
Seeded demo services. Created: 3. Updated: 0.
Auch dieses Verhalten wurde testbar gemacht.
11. Grundlegende Website-Komponenten hinzufuegen
Die erste Version funktionierte, bestand aber nur aus rohen Seiten. Eine reale Business-Website braucht:
- Header mit Marke und Navigation
- einen klaren Call-to-Action
- Footer mit Unternehmenskontext
- responsive Grids und Abstaende fuer mobile Geraete
Um Wiederholung zu vermeiden, wurde ein gemeinsames Template hinzugefuegt:
services/templates/services/base.html
Die Seiten erweitern nun dieses Template:
{% extends "services/base.html" %}
Homepage, Detailseite und Erfolgsseite teilen sich damit Header, Navigation, Footer, Typografie und responsive Regeln.
Auch das wurde in Tests aufgenommen:
self.assertContains(response, "Primary navigation")
self.assertContains(response, "A Django-powered service business website")
12. Ein visuelles Erweiterungsszenario testen
Nach der Grundstruktur stellte sich die Frage: Wie wuerden wir Services besser visualisieren?
Zwei typische Erweiterungen wurden getestet:
- Icons auf Service-Karten
- Hamburger-Menue fuer mobile Navigation
Das Service-Modell erhielt ein kleines icon-Feld:
ICON_CHOICES = [
("home", "Home"),
("briefcase", "Office"),
("sparkles", "Deep cleaning"),
]
icon = models.CharField(max_length=32, choices=ICON_CHOICES, default="sparkles")
Dadurch entstand eine zweite Migration:
services\migrations\0002_service_icon.py
- Add field icon to service
Der Seed-Command wurde aktualisiert, sodass jeder Demo-Service ein passendes Icon bekommt.
Die Homepage rendert Icons ueber ein kleines Partial:
services/templates/services/partials/service_icon.html
Fuer mobile Navigation wurde ein leichter checkbox-basierter Hamburger-Toggle hinzugefuegt. So bleibt die Loesung einfach und testbar, ohne ein Frontend-Framework einzufuehren.
Die Tests wurden erweitert:
self.assertContains(response, "service-icon")
self.assertContains(response, "Toggle navigation menu")
Fuer den Screenshot des geoeffneten Menues gibt es einen deterministischen Zustand:
/?menu=open
13. Visuelle Beweise erfassen
Nach Seed-Daten, Header/Footer, responsive Layout, Icons und mobilem Menue wurden Screenshots der lokalen Website erstellt.
Desktop-Homepage:

Desktop-Service-Detailseite:

Mobile Homepage:

Mobile Service-Detailseite:

Mobile Homepage mit geoeffnetem Hamburger-Menue:

Tests beweisen Verhalten; Screenshots beweisen, dass die Anleitung nicht zu einer leeren oder kaputten ersten Ansicht fuehrt.
Beim mobilen Screenshot-Review zeigte sich, dass CTA und lange Textzeilen in engem Viewport abgeschnitten wurden. Das CSS wurde angepasst: sichere linke Ausrichtung, einspaltige Karten und umbrechende Ueberschriften fuer kleine Screens.
14. Verhalten mit Tests beweisen
Die Testsuite deckt diese Kernverhalten ab:
- Die String-Repräsentation eines Services gibt den Titel zurueck.
- Die Service-Liste zeigt Services an.
- Die Service-Detailseite zeigt das Anfrageformular.
- Eine Angebotsanfrage kann gespeichert werden.
- Der Seed-Command erstellt Demo-Services.
- Header und Footer werden gerendert.
- Service-Icons erscheinen auf der Homepage.
- Das mobile Menue kann geoeffnet gerendert werden.
- Die Versionsdatei wird gelesen.
- Der Release-Command aktualisiert
VERSIONundCHANGELOG.md. - Remote Release Manifest und Workflow-Schritte werden verifiziert.
Testbefehl:
python manage.py test
Ergebnis:
Found 11 test(s).
System check identified no issues (0 silenced).
...........
----------------------------------------------------------------------
Ran 11 tests in 0.151s
OK
Die Website wird also nicht nur beschrieben; ihr Verhalten wird getestet.
15. Maintenance-Releases automatisieren
Nach der ersten funktionierenden Version kommt die reale Maintenance-Frage. Eine Business-Website lebt nach dem Launch weiter: Texte aendern sich, Services werden aktualisiert, Design wird verbessert, Bugs werden behoben, Abhaengigkeiten werden aktualisiert und spaeter kommen vielleicht weitere Sprachversionen hinzu.
Um diesen Prozess wiederholbar zu machen, bekam das Projekt ein einfaches Versioning-System:
VERSION
CHANGELOG.md
Die VERSION-Datei haelt die aktive Website-Version:
0.4.0
Der Footer liest diese Datei ueber einen Context Processor und zeigt sie auf jeder Seite an:
Version
v0.4.0
Fuer Maintenance-Releases wurde ein Management Command erstellt:
python manage.py prepare_release 0.4.0 \
--note "Added remote tag-based release workflow." \
--note "Added release manifest artifact for deployment automation." \
--note "Added remote release runbook with deploy and rollback steps."
Der Command aktualisiert VERSION und fuegt oben in CHANGELOG.md neue Release Notes ein:
## 0.4.0
- Added remote tag-based release workflow.
- Added release manifest artifact for deployment automation.
- Added remote release runbook with deploy and rollback steps.
Lokaler Maintenance-Ablauf:
- Kleine Aenderung umsetzen.
- Tests hinzufuegen oder aktualisieren.
python manage.py testausfuehren.- Mit
python manage.py prepare_releaseein Release vorbereiten. CHANGELOG.mdpruefen.- Deployen.
Diese Struktur hilft auch bei kuenftigen Sprachversionen. Jede Lokalisierung kann einer kontrollierten Version zugeordnet werden.
16. Remote Releases automatisieren
Lokale Release-Vorbereitung ist hilfreich, aber nicht genug. Es muss auch klar sein, was passiert, wenn eine neue Version remote gepusht wird.
Fuer dieses Projekt wurde ein tagbasierter Release-Workflow hinzugefuegt:
.github/workflows/release.yml
Remote Releases werden durch Semantic-Version-Tags ausgeloest:
git tag v0.4.0
git push origin v0.4.0
Workflow-Logik:
Git tag -> CI tests -> release manifest -> deploy hook -> health check
Der remote Workflow fuehrt dieselben Checks aus wie lokal:
python manage.py check
python manage.py test
Danach erzeugt er ein Release Manifest:
python manage.py build_release_manifest --environment production --deploy-target managed-paas
release-manifest.json enthaelt:
{
"version": "0.4.0",
"tag": "v0.4.0",
"environment": "production",
"deploy_target": "managed-paas",
"required_checks": [
"python manage.py test",
"python manage.py check"
],
"remote_release_steps": [
"push git tag",
"run CI checks",
"deploy on successful checks",
"run post-deploy health check"
],
"rollback": {
"strategy": "redeploy previous successful tag"
}
}
Dieses Manifest macht jedes Release nachvollziehbar: Version, Tag, Umgebung, Deploy-Ziel, erforderliche Checks und Rollback-Strategie.
Der Workflow nutzt zwei Secrets:
DEPLOY_HOOK_URL
PRODUCTION_HEALTHCHECK_URL
DEPLOYHOOKURL zeigt auf den Deploy Hook des Hosting-Providers. Nach erfolgreichen Tests kann so das Production Deployment gestartet werden.
PRODUCTIONHEALTHCHECKURL zeigt auf eine oeffentliche URL, die nach dem Deployment erfolgreich antworten soll.
Zusätzlich gibt es ein Remote-Release-Runbook:
docs/remote-release-runbook.md
Remote-Release-Ablauf:
- Lokales Release mit
prepare_releasevorbereiten. - Tests lokal ausfuehren.
VERSION,CHANGELOG.mdund Code-Aenderungen committen.- Git-Tag wie
v0.4.0erstellen. - Tag pushen.
- CI-Tests abwarten.
- Deploy Hook die neue Version veroeffentlichen lassen.
- Health Check pruefen.
- Bei Problemen den letzten erfolgreichen Tag erneut deployen.
Dieser Ablauf kann spaeter auch fuer die deutsche, tuerkische oder weitere Sprachversionen verwendet werden.
17. Wohin deployen?
Dieses Projekt ist noch eine lokale Tutorial-Anwendung. Vor Production muss die Django Deployment Checklist abgearbeitet werden:
SECRET_KEYin Environment Variables auslagern.DEBUG = Falsesetzen.ALLOWED_HOSTSkonfigurieren.- PostgreSQL statt lokaler SQLite-Datenbank verwenden.
- Migrationen auf der Production-Datenbank ausfuehren.
- Statische Dateien mit
python manage.py collectstaticsammeln. - Django in Production mit einem WSGI-Server wie Gunicorn betreiben.
- Bei VPS-Deployment Nginx davorschalten.
- HTTPS aktivieren.
Fuer dieses Projekt gibt es zwei sinnvolle Wege.
Einfachster Weg: Managed PaaS
Eine Plattform nutzen, die Python Web Services und PostgreSQL anbietet:
Git repository -> Python web service -> PostgreSQL -> environment variables -> custom domain
Render, Fly.io, Railway oder aehnliche Plattformen passen gut. Wichtiger als der konkrete Name sind diese Eigenschaften: Python Runtime, persistentes PostgreSQL, Environment Variables, HTTPS, Deploy Logs und vorhersehbare Build-/Deploy-Kommandos.
Kontrollierter Weg: VPS
Wenn Serverbetrieb mit erklaert werden soll, kann ein Ubuntu VPS genutzt werden:
Ubuntu VPS
Nginx
Gunicorn
Django
PostgreSQL
systemd
HTTPS
Das gibt mehr Kontrolle, bringt aber auch Verantwortung fuer Firewall, Updates, Process Manager, Logs, Backups und SSL-Erneuerung.
Fuer diese Blogserie ist zuerst ein Managed-PaaS-Deployment sinnvoller. Ein spaeterer Artikel kann denselben Code auf einem VPS deployen und Serverbetrieb separat erklaeren.
18. Was kommt als Naechstes?
Der weitere Ablauf sollte dieselbe Disziplin behalten:
- Neues Verhalten hinzufuegen.
- Test schreiben oder aktualisieren.
- Tests ausfuehren.
- Erst danach den Blogartikel aktualisieren.
Gute naechste Schritte:
- Production-ready static files und Styling
- Service-Area-Modell
- E-Mail-Benachrichtigung bei Angebotsanfragen
- SEO-Felder fuer Services
- Deployment in einer echten Umgebung abschliessen
- Deutsche, tuerkische und weitere Sprachversionen als versionierte Releases veroeffentlichen
Fazit
Die zentrale Idee ist der Prozess. Ein Django-Tutorial wird staerker, wenn es nicht nur gut erklaert ist, sondern durch laufenden Code und bestandene Tests belegt wird. In dieser Iteration wurde das Projekt erstellt, das Business-Modell gebaut, oeffentliche Seiten wurden verdrahtet, Angebotsanfragen gespeichert, Migrationen erzeugt, der leere erste Zustand mit Seed-Daten geloest, Header/Footer und responsives Layout ergaenzt, Service-Icons und Hamburger-Navigation getestet, lokale und remote Release-Automatisierung eingerichtet, Desktop- und Mobile-Screenshots aufgenommen und 11 Tests erfolgreich ausgefuehrt.
Damit gibt es eine belastbare Grundlage fuer weitere Sprachversionen: Jeder Abschnitt basiert auf echtem Code, echten Tests und echten Ergebnissen.
References
- Django Documentation
- Django Testing Tools
- Django Deployment Checklist
- Django How to Deploy with WSGI
Evidence
- Die Umgebung wurde mit Python 3.11.9 und Django 5.0.2 verifiziert
- Das Projekt wurde mit python -m django startproject config . erstellt
- Die services-App wurde mit python manage.py startapp services angelegt
- Der erste Browser-Check lieferte vor den Migrationen einen 500-Fehler
- Der erste erfolgreiche Seitenaufruf wirkte leer, bis Demo-Services eingespielt wurden
- Der Django-Systemcheck lief ohne Probleme durch
- Demo-Services wurden mit python manage.py seed_services eingespielt
- Die Testsuite war erfolgreich: 11 Tests bestanden
- Ein gemeinsames base template fuer Header, Navigation, Footer und responsive Layout wurde hinzugefuegt
- Ein Service-Icon-Feld wurde ergaenzt und ueber Seed-Daten verifiziert
- Mobile Hamburger-Navigation wurde hinzugefuegt und mit einem Screenshot des geoeffneten Menues getestet
- Maintenance-Automatisierung wurde mit VERSION, CHANGELOG.md und prepare_release ergaenzt
- Der Footer zeigt nun die aktive Version aus der VERSION-Datei an
- Remote-Release-Automatisierung wurde mit einem tagbasierten GitHub-Actions-Workflow ergaenzt
- Ein Release-Manifest-Artefakt wurde fuer Deployment- und Rollback-Nachvollziehbarkeit erzeugt
- Desktop-Screenshots wurden fuer Homepage und Service-Detailseite erstellt
- Mobile Screenshots fuer Homepage und Service-Detailseite wurden mit 390×844 erstellt