Mit Django eine Business-Website von Grund auf entwickeln

12 min read
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 anbietet
  • QuoteRequest: 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:

  • slug gibt jedem Service eine stabile URL.
  • is_featured erlaubt hervorgehobene Services auf der Homepage.
  • PROTECT verhindert, dass ein Service geloescht wird, solange Angebotsanfragen daran haengen.
  • is_contacted gibt 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:

Homepage screenshot

Desktop-Service-Detailseite:

Service detail screenshot

Mobile Homepage:

Mobile homepage screenshot

Mobile Service-Detailseite:

Mobile service detail screenshot

Mobile Homepage mit geoeffnetem Hamburger-Menue:

Mobile open menu screenshot

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 VERSION und CHANGELOG.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:

  1. Kleine Aenderung umsetzen.
  2. Tests hinzufuegen oder aktualisieren.
  3. python manage.py test ausfuehren.
  4. Mit python manage.py prepare_release ein Release vorbereiten.
  5. CHANGELOG.md pruefen.
  6. 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:

  1. Lokales Release mit prepare_release vorbereiten.
  2. Tests lokal ausfuehren.
  3. VERSION, CHANGELOG.md und Code-Aenderungen committen.
  4. Git-Tag wie v0.4.0 erstellen.
  5. Tag pushen.
  6. CI-Tests abwarten.
  7. Deploy Hook die neue Version veroeffentlichen lassen.
  8. Health Check pruefen.
  9. 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_KEY in Environment Variables auslagern.
  • DEBUG = False setzen.
  • ALLOWED_HOSTS konfigurieren.
  • PostgreSQL statt lokaler SQLite-Datenbank verwenden.
  • Migrationen auf der Production-Datenbank ausfuehren.
  • Statische Dateien mit python manage.py collectstatic sammeln.
  • 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:

  1. Neues Verhalten hinzufuegen.
  2. Test schreiben oder aktualisieren.
  3. Tests ausfuehren.
  4. 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

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

Share this Post

Lassen Sie uns gemeinsam etwas Grossartiges bauen

Haben Sie ein Projekt im Kopf? Fuellen Sie das Formular aus, wir melden uns schnell zurueck.

Schnelle Rueckmeldung
Kostenlose Projektberatung

Oder direkt kontaktieren