{"id":53,"date":"2026-05-23T23:16:25","date_gmt":"2026-05-23T23:16:25","guid":{"rendered":"https:\/\/mudosdigital.com\/de\/?p=53"},"modified":"2026-05-23T23:16:26","modified_gmt":"2026-05-23T23:16:26","slug":"mit-django-eine-business-website-von-grund-auf-entwickeln","status":"publish","type":"post","link":"https:\/\/mudosdigital.com\/de\/mit-django-eine-business-website-von-grund-auf-entwickeln\/","title":{"rendered":"Mit Django eine Business-Website von Grund auf entwickeln"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Unser Szenario ist ein lokales Reinigungsunternehmen namens <strong>Mudos Clean<\/strong>. Die Website soll Services auflisten, Detailseiten fuer einzelne Services anzeigen, Angebotsanfragen sammeln und dem Unternehmen erlauben, Services ueber das Django-Admin zu verwalten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wir bauen also zuerst das funktionierende System und schreiben den Artikel auf Basis dieser Beweise.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Die Umgebung verifizieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Das Projekt startete in einem leeren Ordner. Bevor Django-Dateien erzeugt wurden, wurde die Umgebung geprueft:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python --version\npython -m django --version<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Verifizierte Umgebung:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Python 3.11.9\nDjango 5.0.2<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Das ist wichtig, weil alle spaeteren Schritte auf einer realen Django-Installation beruhen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Das Django-Projekt erstellen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Das Projekt wurde mit folgendem Befehl erstellt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python -m django startproject config .<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Unter Windows ist <code>django-admin<\/code> nicht immer im PATH verfuegbar. <code>python -m django<\/code> ist deshalb ein robusterer Startpunkt. Der Befehl erzeugte das Projektpaket <code>config<\/code> und den Einstiegspunkt <code>manage.py<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Erste Struktur:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>config\/\nmanage.py<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">3. Die Services-App erstellen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die Business-Logik wird in einer eigenen App gehalten:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py startapp services<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Danach wurde sie in <code>config\/settings.py<\/code> unter <code>INSTALLED_APPS<\/code> registriert:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>INSTALLED_APPS = &#91;\n    \"django.contrib.admin\",\n    \"django.contrib.auth\",\n    \"django.contrib.contenttypes\",\n    \"django.contrib.sessions\",\n    \"django.contrib.messages\",\n    \"django.contrib.staticfiles\",\n    \"services\",\n]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Damit liegen Service-Listen, Angebotsanfragen, Formulare, Views, Templates und Tests sauber in der <code>services<\/code>-App.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Das Business-Modell aufbauen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Fuer diese Business-Website reichen zwei zentrale Datenmodelle:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Service<\/code>: ein Reinigungsservice, den das Unternehmen anbietet<\/li>\n\n\n\n<li><code>QuoteRequest<\/code>: eine Angebotsanfrage eines Besuchers<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Erstes Modell:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from django.db import models\n\n\nclass Service(models.Model):\n    title = models.CharField(max_length=120)\n    slug = models.SlugField(unique=True)\n    short_description = models.CharField(max_length=240)\n    description = models.TextField()\n    starting_price = models.DecimalField(max_digits=10, decimal_places=2)\n    is_featured = models.BooleanField(default=False)\n    created_at = models.DateTimeField(auto_now_add=True)\n\n    class Meta:\n        ordering = &#91;\"title\"]\n\n    def __str__(self):\n        return self.title\n\n\nclass QuoteRequest(models.Model):\n    TIMING_CHOICES = &#91;\n        (\"once\", \"One-time\"),\n        (\"weekly\", \"Weekly\"),\n        (\"monthly\", \"Monthly\"),\n    ]\n\n    service = models.ForeignKey(\n        Service,\n        on_delete=models.PROTECT,\n        related_name=\"quote_requests\",\n    )\n    full_name = models.CharField(max_length=120)\n    phone = models.CharField(max_length=40)\n    email = models.EmailField(blank=True)\n    address = models.CharField(max_length=255)\n    timing = models.CharField(max_length=20, choices=TIMING_CHOICES)\n    note = models.TextField(blank=True)\n    created_at = models.DateTimeField(auto_now_add=True)\n    is_contacted = models.BooleanField(default=False)\n\n    class Meta:\n        ordering = &#91;\"-created_at\"]\n\n    def __str__(self):\n        return f\"{self.full_name} - {self.service.title}\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Diese Modellentscheidungen haben praktische Gruende:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>slug<\/code> gibt jedem Service eine stabile URL.<\/li>\n\n\n\n<li><code>is_featured<\/code> erlaubt hervorgehobene Services auf der Homepage.<\/li>\n\n\n\n<li><code>PROTECT<\/code> verhindert, dass ein Service geloescht wird, solange Angebotsanfragen daran haengen.<\/li>\n\n\n\n<li><code>is_contacted<\/code> gibt dem Unternehmen einen einfachen Follow-up-Status.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">5. Admin-Verwaltung hinzufuegen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Eine Business-Website braucht frueh eine einfache Inhaltsverwaltung. Deshalb wurde das Django-Admin direkt konfiguriert.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>services\/admin.py<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from django.contrib import admin\n\nfrom .models import QuoteRequest, Service\n\n\n@admin.register(Service)\nclass ServiceAdmin(admin.ModelAdmin):\n    list_display = (\"title\", \"starting_price\", \"is_featured\", \"created_at\")\n    list_filter = (\"is_featured\",)\n    search_fields = (\"title\", \"short_description\")\n    prepopulated_fields = {\"slug\": (\"title\",)}\n\n\n@admin.register(QuoteRequest)\nclass QuoteRequestAdmin(admin.ModelAdmin):\n    list_display = (\n        \"full_name\",\n        \"service\",\n        \"phone\",\n        \"timing\",\n        \"is_contacted\",\n        \"created_at\",\n    )\n    list_filter = (\"service\", \"timing\", \"is_contacted\")\n    search_fields = (\"full_name\", \"phone\", \"email\", \"address\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Damit kann das Unternehmen Services und eingehende Anfragen verwalten, ohne Code zu aendern.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Den oeffentlichen Workflow bauen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Der erste oeffentliche Workflow besteht aus drei Seiten:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Service-Liste<\/li>\n\n\n\n<li>Service-Detailseite mit Anfrageformular<\/li>\n\n\n\n<li>Erfolgsseite nach der Anfrage<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">App-URLs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from django.urls import path\n\nfrom . import views\n\napp_name = \"services\"\n\nurlpatterns = &#91;\n    path(\"\", views.service_list, name=\"service_list\"),\n    path(\"services\/&lt;slug:slug&gt;\/\", views.service_detail, name=\"service_detail\"),\n    path(\"quote-request-received\/\", views.quote_success, name=\"quote_success\"),\n]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Projekt-URLs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from django.contrib import admin\nfrom django.urls import include, path\n\nurlpatterns = &#91;\n    path(\"admin\/\", admin.site.urls),\n    path(\"\", include(\"services.urls\")),\n]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Jetzt gibt es echte Routen, die Browser und Tests aufrufen koennen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Das Angebotsformular hinzufuegen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Das Formular basiert auf dem <code>QuoteRequest<\/code>-Modell:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from django import forms\n\nfrom .models import QuoteRequest\n\n\nclass QuoteRequestForm(forms.ModelForm):\n    class Meta:\n        model = QuoteRequest\n        fields = &#91;\"full_name\", \"phone\", \"email\", \"address\", \"timing\", \"note\"]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Detail-View rendert die Seite und speichert bei POST das Formular:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def service_detail(request, slug):\n    service = get_object_or_404(Service, slug=slug)\n\n    if request.method == \"POST\":\n        form = QuoteRequestForm(request.POST)\n        if form.is_valid():\n            quote_request = form.save(commit=False)\n            quote_request.service = service\n            quote_request.save()\n            return redirect(\"services:quote_success\")\n    else:\n        form = QuoteRequestForm()\n\n    return render(\n        request,\n        \"services\/service_detail.html\",\n        {\n            \"service\": service,\n            \"form\": form,\n        },\n    )<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Damit entsteht der erste vollstaendige Business-Loop: Ein Besucher sieht einen Service, fuellt ein Formular aus und der Lead wird in der Datenbank gespeichert.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">8. Die Database Migration erstellen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nach den Modellen wurde die Migration erzeugt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py makemigrations services<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ausgabe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Migrations for 'services':\n  services\\migrations\\0001_initial.py\n    - Create model Service\n    - Create model QuoteRequest<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Das beweist, dass die Modellschicht in eine konkrete Datenbankaenderung uebersetzt wurde.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">9. Den Systemcheck ausfuehren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Vor den Tests wurde Djangos eingebauter Check ausgefuehrt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py check<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ausgabe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>System check identified no issues (0 silenced).<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Damit sind Settings, App-Registrierung, URL-Konfiguration und Modelle strukturell gueltig.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">10. Den ersten Browser-Check reparieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Der erste Browser-Check zeigte zwei echte Probleme.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Erstens: Vor den Migrationen fuehrte der Seitenaufruf zu einem Serverfehler. Die Loesung:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py migrate --noinput<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Danach lieferte die Homepage <code>200 OK<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py seed_services<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Command laedt drei Demo-Services:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Home Cleaning<\/li>\n\n\n\n<li>Office Cleaning<\/li>\n\n\n\n<li>Move-Out Cleaning<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Ausgabe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Seeded demo services. Created: 3. Updated: 0.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Auch dieses Verhalten wurde testbar gemacht.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">11. Grundlegende Website-Komponenten hinzufuegen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die erste Version funktionierte, bestand aber nur aus rohen Seiten. Eine reale Business-Website braucht:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Header mit Marke und Navigation<\/li>\n\n\n\n<li>einen klaren Call-to-Action<\/li>\n\n\n\n<li>Footer mit Unternehmenskontext<\/li>\n\n\n\n<li>responsive Grids und Abstaende fuer mobile Geraete<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Um Wiederholung zu vermeiden, wurde ein gemeinsames Template hinzugefuegt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services\/templates\/services\/base.html<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Seiten erweitern nun dieses Template:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{% extends \"services\/base.html\" %}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Homepage, Detailseite und Erfolgsseite teilen sich damit Header, Navigation, Footer, Typografie und responsive Regeln.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Auch das wurde in Tests aufgenommen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>self.assertContains(response, \"Primary navigation\")\nself.assertContains(response, \"A Django-powered service business website\")<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">12. Ein visuelles Erweiterungsszenario testen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nach der Grundstruktur stellte sich die Frage: Wie wuerden wir Services besser visualisieren?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zwei typische Erweiterungen wurden getestet:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Icons auf Service-Karten<\/li>\n\n\n\n<li>Hamburger-Menue fuer mobile Navigation<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Das <code>Service<\/code>-Modell erhielt ein kleines <code>icon<\/code>-Feld:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ICON_CHOICES = &#91;\n    (\"home\", \"Home\"),\n    (\"briefcase\", \"Office\"),\n    (\"sparkles\", \"Deep cleaning\"),\n]\n\nicon = models.CharField(max_length=32, choices=ICON_CHOICES, default=\"sparkles\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dadurch entstand eine zweite Migration:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services\\migrations\\0002_service_icon.py\n  - Add field icon to service<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Seed-Command wurde aktualisiert, sodass jeder Demo-Service ein passendes Icon bekommt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Homepage rendert Icons ueber ein kleines Partial:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services\/templates\/services\/partials\/service_icon.html<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Fuer mobile Navigation wurde ein leichter checkbox-basierter Hamburger-Toggle hinzugefuegt. So bleibt die Loesung einfach und testbar, ohne ein Frontend-Framework einzufuehren.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Tests wurden erweitert:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>self.assertContains(response, \"service-icon\")\nself.assertContains(response, \"Toggle navigation menu\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Fuer den Screenshot des geoeffneten Menues gibt es einen deterministischen Zustand:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/?menu=open<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">13. Visuelle Beweise erfassen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nach Seed-Daten, Header\/Footer, responsive Layout, Icons und mobilem Menue wurden Screenshots der lokalen Website erstellt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Desktop-Homepage:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/mudosdigital.com\/wp-content\/uploads\/2026\/05\/home-page-4.png\" alt=\"Homepage screenshot\"\/ loading=\"lazy\"><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Desktop-Service-Detailseite:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/mudosdigital.com\/wp-content\/uploads\/2026\/05\/service-detail-4.png\" alt=\"Service detail screenshot\"\/ loading=\"lazy\"><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Mobile Homepage:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/mudosdigital.com\/wp-content\/uploads\/2026\/05\/home-page-mobile-4.png\" alt=\"Mobile homepage screenshot\"\/ loading=\"lazy\"><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Mobile Service-Detailseite:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/mudosdigital.com\/wp-content\/uploads\/2026\/05\/service-detail-mobile-4.png\" alt=\"Mobile service detail screenshot\"\/ loading=\"lazy\"><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Mobile Homepage mit geoeffnetem Hamburger-Menue:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/mudosdigital.com\/wp-content\/uploads\/2026\/05\/home-page-mobile-menu-open-4.png\" alt=\"Mobile open menu screenshot\"\/ loading=\"lazy\"><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Tests beweisen Verhalten; Screenshots beweisen, dass die Anleitung nicht zu einer leeren oder kaputten ersten Ansicht fuehrt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">14. Verhalten mit Tests beweisen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die Testsuite deckt diese Kernverhalten ab:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Die String-Repr\u00e4sentation eines Services gibt den Titel zurueck.<\/li>\n\n\n\n<li>Die Service-Liste zeigt Services an.<\/li>\n\n\n\n<li>Die Service-Detailseite zeigt das Anfrageformular.<\/li>\n\n\n\n<li>Eine Angebotsanfrage kann gespeichert werden.<\/li>\n\n\n\n<li>Der Seed-Command erstellt Demo-Services.<\/li>\n\n\n\n<li>Header und Footer werden gerendert.<\/li>\n\n\n\n<li>Service-Icons erscheinen auf der Homepage.<\/li>\n\n\n\n<li>Das mobile Menue kann geoeffnet gerendert werden.<\/li>\n\n\n\n<li>Die Versionsdatei wird gelesen.<\/li>\n\n\n\n<li>Der Release-Command aktualisiert <code>VERSION<\/code> und <code>CHANGELOG.md<\/code>.<\/li>\n\n\n\n<li>Remote Release Manifest und Workflow-Schritte werden verifiziert.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Testbefehl:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py test<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ergebnis:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Found 11 test(s).\nSystem check identified no issues (0 silenced).\n...........\n----------------------------------------------------------------------\nRan 11 tests in 0.151s\n\nOK<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Website wird also nicht nur beschrieben; ihr Verhalten wird getestet.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">15. Maintenance-Releases automatisieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um diesen Prozess wiederholbar zu machen, bekam das Projekt ein einfaches Versioning-System:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>VERSION\nCHANGELOG.md<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die <code>VERSION<\/code>-Datei haelt die aktive Website-Version:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>0.4.0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Footer liest diese Datei ueber einen Context Processor und zeigt sie auf jeder Seite an:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Version\nv0.4.0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Fuer Maintenance-Releases wurde ein Management Command erstellt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py prepare_release 0.4.0 \\\n  --note \"Added remote tag-based release workflow.\" \\\n  --note \"Added release manifest artifact for deployment automation.\" \\\n  --note \"Added remote release runbook with deploy and rollback steps.\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Command aktualisiert <code>VERSION<\/code> und fuegt oben in <code>CHANGELOG.md<\/code> neue Release Notes ein:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>## 0.4.0\n\n- Added remote tag-based release workflow.\n- Added release manifest artifact for deployment automation.\n- Added remote release runbook with deploy and rollback steps.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lokaler Maintenance-Ablauf:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Kleine Aenderung umsetzen.<\/li>\n\n\n\n<li>Tests hinzufuegen oder aktualisieren.<\/li>\n\n\n\n<li><code>python manage.py test<\/code> ausfuehren.<\/li>\n\n\n\n<li>Mit <code>python manage.py prepare_release<\/code> ein Release vorbereiten.<\/li>\n\n\n\n<li><code>CHANGELOG.md<\/code> pruefen.<\/li>\n\n\n\n<li>Deployen.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Diese Struktur hilft auch bei kuenftigen Sprachversionen. Jede Lokalisierung kann einer kontrollierten Version zugeordnet werden.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">16. Remote Releases automatisieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Lokale Release-Vorbereitung ist hilfreich, aber nicht genug. Es muss auch klar sein, was passiert, wenn eine neue Version remote gepusht wird.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Fuer dieses Projekt wurde ein tagbasierter Release-Workflow hinzugefuegt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.github\/workflows\/release.yml<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Remote Releases werden durch Semantic-Version-Tags ausgeloest:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git tag v0.4.0\ngit push origin v0.4.0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Workflow-Logik:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Git tag -&gt; CI tests -&gt; release manifest -&gt; deploy hook -&gt; health check<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der remote Workflow fuehrt dieselben Checks aus wie lokal:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py check\npython manage.py test<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Danach erzeugt er ein Release Manifest:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py build_release_manifest --environment production --deploy-target managed-paas<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>release-manifest.json<\/code> enthaelt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"version\": \"0.4.0\",\n  \"tag\": \"v0.4.0\",\n  \"environment\": \"production\",\n  \"deploy_target\": \"managed-paas\",\n  \"required_checks\": &#91;\n    \"python manage.py test\",\n    \"python manage.py check\"\n  ],\n  \"remote_release_steps\": &#91;\n    \"push git tag\",\n    \"run CI checks\",\n    \"deploy on successful checks\",\n    \"run post-deploy health check\"\n  ],\n  \"rollback\": {\n    \"strategy\": \"redeploy previous successful tag\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dieses Manifest macht jedes Release nachvollziehbar: Version, Tag, Umgebung, Deploy-Ziel, erforderliche Checks und Rollback-Strategie.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der Workflow nutzt zwei Secrets:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DEPLOY_HOOK_URL\nPRODUCTION_HEALTHCHECK_URL<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>DEPLOY<em>HOOK<\/em>URL<\/code> zeigt auf den Deploy Hook des Hosting-Providers. Nach erfolgreichen Tests kann so das Production Deployment gestartet werden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>PRODUCTION<em>HEALTHCHECK<\/em>URL<\/code> zeigt auf eine oeffentliche URL, die nach dem Deployment erfolgreich antworten soll.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zus\u00e4tzlich gibt es ein Remote-Release-Runbook:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docs\/remote-release-runbook.md<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Remote-Release-Ablauf:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Lokales Release mit <code>prepare_release<\/code> vorbereiten.<\/li>\n\n\n\n<li>Tests lokal ausfuehren.<\/li>\n\n\n\n<li><code>VERSION<\/code>, <code>CHANGELOG.md<\/code> und Code-Aenderungen committen.<\/li>\n\n\n\n<li>Git-Tag wie <code>v0.4.0<\/code> erstellen.<\/li>\n\n\n\n<li>Tag pushen.<\/li>\n\n\n\n<li>CI-Tests abwarten.<\/li>\n\n\n\n<li>Deploy Hook die neue Version veroeffentlichen lassen.<\/li>\n\n\n\n<li>Health Check pruefen.<\/li>\n\n\n\n<li>Bei Problemen den letzten erfolgreichen Tag erneut deployen.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Dieser Ablauf kann spaeter auch fuer die deutsche, tuerkische oder weitere Sprachversionen verwendet werden.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">17. Wohin deployen?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dieses Projekt ist noch eine lokale Tutorial-Anwendung. Vor Production muss die Django Deployment Checklist abgearbeitet werden:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>SECRET_KEY<\/code> in Environment Variables auslagern.<\/li>\n\n\n\n<li><code>DEBUG = False<\/code> setzen.<\/li>\n\n\n\n<li><code>ALLOWED_HOSTS<\/code> konfigurieren.<\/li>\n\n\n\n<li>PostgreSQL statt lokaler SQLite-Datenbank verwenden.<\/li>\n\n\n\n<li>Migrationen auf der Production-Datenbank ausfuehren.<\/li>\n\n\n\n<li>Statische Dateien mit <code>python manage.py collectstatic<\/code> sammeln.<\/li>\n\n\n\n<li>Django in Production mit einem WSGI-Server wie Gunicorn betreiben.<\/li>\n\n\n\n<li>Bei VPS-Deployment Nginx davorschalten.<\/li>\n\n\n\n<li>HTTPS aktivieren.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Fuer dieses Projekt gibt es zwei sinnvolle Wege.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Einfachster Weg: Managed PaaS<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Eine Plattform nutzen, die Python Web Services und PostgreSQL anbietet:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Git repository -&gt; Python web service -&gt; PostgreSQL -&gt; environment variables -&gt; custom domain<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Kontrollierter Weg: VPS<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn Serverbetrieb mit erklaert werden soll, kann ein Ubuntu VPS genutzt werden:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Ubuntu VPS\nNginx\nGunicorn\nDjango\nPostgreSQL\nsystemd\nHTTPS<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Das gibt mehr Kontrolle, bringt aber auch Verantwortung fuer Firewall, Updates, Process Manager, Logs, Backups und SSL-Erneuerung.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Fuer diese Blogserie ist zuerst ein Managed-PaaS-Deployment sinnvoller. Ein spaeterer Artikel kann denselben Code auf einem VPS deployen und Serverbetrieb separat erklaeren.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">18. Was kommt als Naechstes?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Der weitere Ablauf sollte dieselbe Disziplin behalten:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Neues Verhalten hinzufuegen.<\/li>\n\n\n\n<li>Test schreiben oder aktualisieren.<\/li>\n\n\n\n<li>Tests ausfuehren.<\/li>\n\n\n\n<li>Erst danach den Blogartikel aktualisieren.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Gute naechste Schritte:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Production-ready static files und Styling<\/li>\n\n\n\n<li>Service-Area-Modell<\/li>\n\n\n\n<li>E-Mail-Benachrichtigung bei Angebotsanfragen<\/li>\n\n\n\n<li>SEO-Felder fuer Services<\/li>\n\n\n\n<li>Deployment in einer echten Umgebung abschliessen<\/li>\n\n\n\n<li>Deutsche, tuerkische und weitere Sprachversionen als versionierte Releases veroeffentlichen<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Fazit<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Damit gibt es eine belastbare Grundlage fuer weitere Sprachversionen: Jeder Abschnitt basiert auf echtem Code, echten Tests und echten Ergebnissen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">References<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/docs.djangoproject.com\/\">Django Documentation<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.djangoproject.com\/en\/stable\/topics\/testing\/tools\/\">Django Testing Tools<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.djangoproject.com\/en\/5.0\/howto\/deployment\/checklist\/\">Django Deployment Checklist<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.djangoproject.com\/en\/5.0\/howto\/deployment\/wsgi\/\">Django How to Deploy with WSGI<\/a><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Evidence<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Die Umgebung wurde mit Python 3.11.9 und Django 5.0.2 verifiziert<\/li>\n\n\n\n<li>Das Projekt wurde mit python -m django startproject config . erstellt<\/li>\n\n\n\n<li>Die services-App wurde mit python manage.py startapp services angelegt<\/li>\n\n\n\n<li>Der erste Browser-Check lieferte vor den Migrationen einen 500-Fehler<\/li>\n\n\n\n<li>Der erste erfolgreiche Seitenaufruf wirkte leer, bis Demo-Services eingespielt wurden<\/li>\n\n\n\n<li>Der Django-Systemcheck lief ohne Probleme durch<\/li>\n\n\n\n<li>Demo-Services wurden mit python manage.py seed_services eingespielt<\/li>\n\n\n\n<li>Die Testsuite war erfolgreich: 11 Tests bestanden<\/li>\n\n\n\n<li>Ein gemeinsames base template fuer Header, Navigation, Footer und responsive Layout wurde hinzugefuegt<\/li>\n\n\n\n<li>Ein Service-Icon-Feld wurde ergaenzt und ueber Seed-Daten verifiziert<\/li>\n\n\n\n<li>Mobile Hamburger-Navigation wurde hinzugefuegt und mit einem Screenshot des geoeffneten Menues getestet<\/li>\n\n\n\n<li>Maintenance-Automatisierung wurde mit VERSION, CHANGELOG.md und prepare_release ergaenzt<\/li>\n\n\n\n<li>Der Footer zeigt nun die aktive Version aus der VERSION-Datei an<\/li>\n\n\n\n<li>Remote-Release-Automatisierung wurde mit einem tagbasierten GitHub-Actions-Workflow ergaenzt<\/li>\n\n\n\n<li>Ein Release-Manifest-Artefakt wurde fuer Deployment- und Rollback-Nachvollziehbarkeit erzeugt<\/li>\n\n\n\n<li>Desktop-Screenshots wurden fuer Homepage und Service-Detailseite erstellt<\/li>\n\n\n\n<li>Mobile Screenshots fuer Homepage und Service-Detailseite wurden mit 390&#215;844 erstellt<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>A proof-driven Django walkthrough where we build and test a business website from scratch before turning the process into a blog post.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10],"tags":[],"class_list":["post-53","post","type-post","status-publish","format-standard","hentry","category-django"],"_links":{"self":[{"href":"https:\/\/mudosdigital.com\/de\/wp-json\/wp\/v2\/posts\/53","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mudosdigital.com\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mudosdigital.com\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mudosdigital.com\/de\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mudosdigital.com\/de\/wp-json\/wp\/v2\/comments?post=53"}],"version-history":[{"count":1,"href":"https:\/\/mudosdigital.com\/de\/wp-json\/wp\/v2\/posts\/53\/revisions"}],"predecessor-version":[{"id":55,"href":"https:\/\/mudosdigital.com\/de\/wp-json\/wp\/v2\/posts\/53\/revisions\/55"}],"wp:attachment":[{"href":"https:\/\/mudosdigital.com\/de\/wp-json\/wp\/v2\/media?parent=53"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mudosdigital.com\/de\/wp-json\/wp\/v2\/categories?post=53"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mudosdigital.com\/de\/wp-json\/wp\/v2\/tags?post=53"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}