Erstellen Sie ein Django-Nachrichtenportal in 2026: Full-Stack-Tutorial

Kent Hudson

Kent Hudson

·

22 minuten Lesen

Erstellen Sie ein Django-Nachrichtenportal in 2026: Full-Stack-Tutorial

Erstellen Sie ein Django-Nachrichtenportal in 2026: Full-Stack-Tutorial

Ein Django-Nachrichtenportal ist eine Full-Stack-Webanwendung, die Artikel nach einem Zeitplan aus einem Nachrichten-API abruft, sie mit Metadaten (Entitäten, Kategorien, Sentiment) speichert, über gecachte Ansichten mit unendlichem Scrollen bereitstellt und die Suche im Korpus ermöglicht — erstellt mit Django 5, Celery Beat, Redis und HTMX. Diese Anleitung erstellt ein Portal, das Live-Artikel alle 15 Minuten abruft und diese nach dem Caching unter 50 ms bereitstellt.

Dieses Tutorial führt durch den Aufbau des Portals von Ende zu Ende in etwa 350 Zeilen Django Code. Die Daten stammen live von einer echten Nachrichten-API. Das Frontend verwendet HTMX für unendliches Scrollen anstelle einer JavaScript-Build-Pipeline. Der letzte Abschnitt gibt Ihnen ein Entscheidungsframework, wann jedes Komplexitätsteil (Celery, Redis, Postgres) hinzufügenswert ist.

Was Sie bauen werden

Ein funktionierendes Django Nachrichtenportal mit sieben Komponenten:

  1. Modelle – Article, Source, Category, Topic mit JSONFields für Entitäten und Sentiment
  2. Celery beat ingestion — Aufgabe, die alle 15 Minuten neue Artikel mit idempotenter Upsert abruft
  3. Redis Cache – Django Cache-Framework mit pro Artikel TTL, ausgerichtet an der Aktualität der Veröffentlichung
  4. Klassenbasierte Ansichten — ListView + DetailView mit Cache-Dekoratoren
  5. HTMX Endlos-Scrollen — hx-trigger="revealed" Teilvorlage, kein JavaScript Build
  6. Postgres Volltextsuche – SearchVector + GIN-Index über Titel und Body
  7. Entscheidungsrahmen — wann jede Schicht hinzugefügt werden soll im Vergleich zum Start mit SQLite

Für wen das geeignet ist: Django Entwickler, die einen Nachrichtenaggregator, ein Markenüberwachungsportal, ein internes Nachrichtenraum-Dashboard oder eine beliebige Content-Website erstellen, die einen externen Nachrichten-Feed nutzt.

Voraussetzungen

  • Python 3.12+ und Django 5.1+
  • Ein News-API-Schlüssel – dieser Leitfaden verwendet APITube, da jeder Artikel mit bereits angehängten Kategorien, Themen, Entitäten und Stimmungen zurückgegeben wird, was die Modellschicht klein hält. Jeder News-API mit strukturierten Metadaten funktioniert.
  • PostgreSQL 16+ und Redis 7+ (überspringen Sie beide am Anfang; der Entscheidungsrahmen erklärt, wann sie hinzugefügt werden sollen)
  • Pakete:
pip install django celery[redis] redis psycopg httpx django-htmx
export APITUBE_KEY="your_key_here"
export DATABASE_URL="postgres://user:pass@localhost:5432/newsportal"
export CELERY_BROKER_URL="redis://localhost:6379/0"

Schritt 1 – Modelle für Nachrichtenartikel

Ein echtes Nachrichtenportal benötigt mehr als einen title + body. Jeder Artikel stammt von einer Quelle, gehört zu einer oder mehreren Kategorien und Themen, erwähnt benannte Entitäten und trägt eine Stimmung. Verwenden Sie JSONField für die offenen Teile, um eine vorzeitige Normalisierung zu vermeiden:

# news/models.py
from django.db import models
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex

class Source(models.Model):
    domain = models.CharField(max_length=255, unique=True)
    country_code = models.CharField(max_length=2, blank=True)

    def __str__(self):
        return self.domain


class Category(models.Model):
    slug = models.SlugField(max_length=80, unique=True)
    name = models.CharField(max_length=120)


class Article(models.Model):
    external_id = models.CharField(max_length=64, unique=True, db_index=True)
    title = models.CharField(max_length=500)
    description = models.TextField(blank=True)
    body = models.TextField(blank=True)
    href = models.URLField(max_length=2000)
    image = models.URLField(max_length=2000, blank=True)
    published_at = models.DateTimeField(db_index=True)
    source = models.ForeignKey(Source, on_delete=models.CASCADE, related_name="articles")
    categories = models.ManyToManyField(Category, related_name="articles", blank=True)

    # Open-ended metadata — APITube returns rich nested structures here
    entities = models.JSONField(default=list, blank=True)
    topics = models.JSONField(default=list, blank=True)
    sentiment = models.JSONField(default=dict, blank=True)

    search_vector = SearchVectorField(null=True, editable=False)

    class Meta:
        ordering = ("-published_at",)
        indexes = [
            GinIndex(fields=["search_vector"], name="article_search_idx"),
            models.Index(fields=["-published_at"], name="article_pub_idx"),
        ]

    def __str__(self):
        return self.title

Zwei Designhinweise. Erstens ist external_id einzigartig und indiziert – das macht das Upsert in der Celery-Aufgabe idempotent gegenüber wiederholten Abfragen. Zweitens ist search_vector ein Postgres-spezifisches Feld mit einem GIN-Index; wir werden es in der Ingestionsaufgabe befüllen, damit die Suche schnell bleibt, wenn der Korpus wächst.

Führen Sie Migrationen aus und registrieren Sie einen minimalen Admin, damit Sie die Aufnahme visuell überprüfen können:

python manage.py makemigrations news && python manage.py migrate
# news/admin.py
from django.contrib import admin
from .models import Article, Source, Category

admin.site.register([Source, Category])
admin.site.register(Article, list_display=("title", "source", "published_at"))

Schritt 2 – Celery Beat für Live-Nachrichten-Ingestion

Ein echtes Nachrichtenportal ruft Artikel automatisch ab, nicht über das Admin-Formular. Celery Beat ist der bevorzugte Scheduler von Django.

Konfigurieren Sie Celery in Ihrem Projekt:

# project/celery.py
import os
from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
app = Celery("project")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
# project/__init__.py
from .celery import app as celery_app
__all__ = ("celery_app",)
# project/settings.py — relevant pieces
CELERY_BROKER_URL = os.environ["CELERY_BROKER_URL"]
CELERY_TIMEZONE = "UTC"
CELERY_BEAT_SCHEDULE = {
    "fetch-news-every-15-min": {
        "task": "news.tasks.fetch_news",
        "schedule": 900.0,  # 15 minutes
    },
}

Die Fetcher-Aufgabe erledigt drei Dinge: aktuelle Artikel abrufen, sie auf external_id einfügen und den Suchvektor für neue Zeilen aktualisieren.

# news/tasks.py
import os
import httpx
from celery import shared_task
from datetime import datetime, timedelta, timezone
from django.contrib.postgres.search import SearchVector
from django.db import transaction
from .models import Article, Source, Category

APITUBE_KEY = os.environ["APITUBE_KEY"]
BASE = "https://api.apitube.io/v1/news/everything"

@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def fetch_news(self, category="medtop:13000000", per_page=50):  # IPTC media-topic id (e.g. technology)
    since = (datetime.now(timezone.utc) - timedelta(minutes=30)).isoformat()
    params = {
        "language.code": "en",
        "category.id": category,
        "published_at.start": since,
        "per_page": per_page,
    }
    try:
        r = httpx.get(BASE, params=params, headers={"X-API-Key": APITUBE_KEY}, timeout=15)
        r.raise_for_status()
    except httpx.HTTPError as exc:
        raise self.retry(exc=exc)

    new_ids = []
    for item in r.json().get("results", []):
        source, _ = Source.objects.get_or_create(
            domain=item["source"]["domain"],
            defaults={"country_code": item["source"].get("location", {}).get("country_code", "")[:2]},
        )
        with transaction.atomic():
            article, created = Article.objects.update_or_create(
                external_id=str(item["id"]),
                defaults={
                    "title": item["title"][:500],
                    "description": item.get("description", ""),
                    "body": item.get("body", ""),
                    "href": item["href"],
                    "image": item.get("image", "") or "",
                    "published_at": item["published_at"],
                    "source": source,
                    "entities": item.get("entities", []),
                    "topics": item.get("topics", []),
                    "sentiment": item.get("sentiment", {}),
                },
            )
            if created:
                new_ids.append(article.pk)
            cats = [Category.objects.get_or_create(slug=c["id"], defaults={"name": c.get("name", c["id"])})[0]
                    for c in item.get("categories", [])]
            article.categories.set(cats)

    if new_ids:
        Article.objects.filter(pk__in=new_ids).update(
            search_vector=SearchVector("title", weight="A") + SearchVector("body", weight="B")
        )
    return {"fetched": len(r.json().get("results", [])), "new": len(new_ids)}

Zwei Produktionsaspekte. Das 30-Minuten-Lookback-Fenster mit 15-Minuten-Planung garantiert Überlappungen, sodass ein verpasster Lauf keine Artikel verliert. Der update_or_create-Schlüssel, der auf external_id basiert, macht erneute Abrufe idempotent – derselbe Artikel kann bei drei aufeinanderfolgenden Abfragen eintreffen und Sie werden niemals Duplikate erhalten.

Starten Sie die Worker:

celery -A project worker -l info
celery -A project beat -l info

Schritt 3 – Redis Cache für heiße Artikel

Die Artikeldetailansicht führt für jeden beliebten Artikel tausende Male pro Stunde dieselbe Datenbankabfrage aus. Das Django-Cache-Framework vor Redis behebt dies.

Konfigurieren Sie den Cache:

# settings.py
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/1"),
    }
}

Zwischenspeichern Sie Artikelobjekte mit einem TTL, das mit dem Alter des Artikels verfällt — eine 5 Minuten alte Eilmeldung sollte häufiger aktualisiert werden als ein Woche altes Archivstück:

# news/cache.py
from django.core.cache import cache
from datetime import datetime, timezone
from .models import Article

def article_ttl(published_at):
    age_hours = (datetime.now(timezone.utc) - published_at).total_seconds() / 3600
    if age_hours < 1: return 60          # 1 min — breaking
    if age_hours < 24: return 300        # 5 min — recent
    return 3600                          # 1 hour — older

def get_article_cached(external_id):
    key = f"article:{external_id}"
    article = cache.get(key)
    if article is None:
        article = Article.objects.select_related("source").prefetch_related("categories").get(external_id=external_id)
        cache.set(key, article, timeout=article_ttl(article.published_at))
    return article

Cache-Invalidierung bei Aktualisierung ist eine Einzeiler über Signal:

# news/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.cache import cache
from .models import Article

@receiver(post_save, sender=Article)
def invalidate_article_cache(sender, instance, **kwargs):
    cache.delete(f"article:{instance.external_id}")

Verdrahten Sie Signale in apps.py, damit sie beim Start geladen werden:

class NewsConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "news"
    def ready(self):
        from . import signals  # noqa

Schritt 4 – Klassenbasierte Ansichten

Djangos ListView und DetailView decken die beiden benötigten Seiten ab. Halten Sie diese schlank – die Hauptarbeit gehört in Modelle und Manager.

# news/views.py
from django.views.generic import ListView, DetailView
from django.shortcuts import render
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from .cache import get_article_cached
from .models import Article

PAGE_SIZE = 20

@method_decorator(cache_page(60), name="dispatch")  # 60s page cache for the list
class ArticleListView(ListView):
    model = Article
    template_name = "news/list.html"
    context_object_name = "articles"
    paginate_by = PAGE_SIZE

    def get_queryset(self):
        return Article.objects.select_related("source").only(
            "id", "external_id", "title", "description", "image", "published_at", "source"
        )


class ArticleDetailView(DetailView):
    template_name = "news/detail.html"
    context_object_name = "article"

    def get_object(self, queryset=None):
        return get_article_cached(self.kwargs["external_id"])

URL-Konfiguration:

# news/urls.py
from django.urls import path
from .views import ArticleListView, ArticleDetailView, ArticleListPartial, ArticleSearchView

urlpatterns = [
    path("", ArticleListView.as_view(), name="article-list"),
    path("page/", ArticleListPartial.as_view(), name="article-list-partial"),
    path("search/", ArticleSearchView.as_view(), name="article-search"),
    path("<str:external_id>/", ArticleDetailView.as_view(), name="article-detail"),
]

Schritt 5 – HTMX Endlos-Scroll (kein JavaScript Build)

HTMX fügt unendliches Scrollen in 15 Zeilen von HTML hinzu, ohne React, Webpack oder einen Build-Schritt.

Installieren Sie HTMX in Ihrer Basisschablone:

<!-- templates/base.html -->
<head>
  <script src="https://unpkg.com/[email protected]" defer></script>
</head>

Die Listenansicht-Vorlage rendert die erste Seite und fordert dann HTMX auf, die nächste Seite abzurufen, wenn der Sentinel in Sichtweite scrollt:

<!-- templates/news/list.html -->
{% extends "base.html" %}
{% block content %}
<h1>Latest News</h1>
<div id="articles">
  {% include "news/_articles.html" %}
</div>
{% endblock %}
<!-- templates/news/_articles.html -->
{% for article in articles %}
  <article class="card">
    <h2><a href="{% url 'article-detail' article.external_id %}">{{ article.title }}</a></h2>
    <p>{{ article.description|truncatechars:160 }}</p>
    <small>{{ article.source.domain }} — {{ article.published_at|date:"j M, H:i" }}</small>
  </article>
{% endfor %}

{% if page_obj.has_next %}
<div hx-get="{% url 'article-list-partial' %}?page={{ page_obj.next_page_number }}"
     hx-trigger="revealed"
     hx-swap="outerHTML">
  Loading...
</div>
{% endif %}

Die partielle Ansicht gibt nur den Artikelblock plus den nächsten Begrenzer zurück:

# news/views.py — append
from django.views.generic import ListView

class ArticleListPartial(ArticleListView):
    template_name = "news/_articles.html"

Das ist das vollständige Muster. Wenn der Sentinel-div in den Viewport eintritt, löst HTMX einen GET aus, tauscht die Antwort in seinen eigenen Slot und die neue Antwort bringt ihren eigenen Nächste-Seite-Sentinel mit. Kein von Ihnen geschriebener JavaScript.

Schritt 6 — Postgres Volltextsuche

Einfaches Article.objects.filter(title__icontains=q) bricht bei ~50,000 Artikeln zusammen. Postgres Volltextsuche mit dem GIN-Index aus Schritt 1 bleibt auch bei über einer Million Zeilen unter -100m.

# news/views.py — append
from django.views.generic import ListView
from django.contrib.postgres.search import SearchQuery, SearchRank
from django.db.models import F

class ArticleSearchView(ListView):
    template_name = "news/search.html"
    context_object_name = "articles"
    paginate_by = PAGE_SIZE

    def get_queryset(self):
        q = self.request.GET.get("q", "").strip()
        if not q:
            return Article.objects.none()
        query = SearchQuery(q, search_type="websearch")
        return (
            Article.objects.annotate(rank=SearchRank(F("search_vector"), query))
            .filter(search_vector=query)
            .order_by("-rank", "-published_at")
            .select_related("source")
        )

search_type="websearch" akzeptiert Operatoren im Google-Stil ("exact phrase", -exclude, OR), ohne dass Sie etwas parsen müssen. Die SearchRank-Sortierung zeigt zuerst die relevantesten Artikel an; die sekundäre published_at-Sortierung bevorzugt Aktualität.

Die Suchvorlage ist nur ein Formular, das auf diese Ansicht zeigt, und rendert dann denselben _articles.html-Teil – Paginierung und HTMX Infinite Scroll funktionieren auch für Suchergebnisse kostenlos.

Entscheidungsrahmen: Wann jede Ebene hinzufügen?

Ein häufiger Django-Fehler besteht darin, am ersten Tag für ein Portal mit 12 Artikeln und 3 Benutzern Postgres + Redis + Celery bereitzustellen. Die richtige Reihenfolge:

StufeArtikelStackWarum
Prototyp< 1,000SQLite + Synchronisierung fetch_news über CronNull Ops; Versand an einem Tag
Frühe Produktion1k – 50kPostgres + synchrone Abfrage über CronFTS rechtfertigt Postgres; noch keine Redis
Echter Traffic> 50k Artikel oder > 1k DAU+ Celery schlägt + Redis CacheAsynchroner Abruf schützt die Latenz der Anfrage; Cache absorbiert Lesevorgänge
Mehrere Quellen> 5 Fetcher+ Celery Worker-Pool, separater Beat-ContainerFetcher-Fehler vom Web-Tier isolieren

Zwei Regeln hinter der Tabelle. Fügen Sie Celery erst hinzu, wenn Ihr synchroner Abruf Webanfragen blockiert – bis dahin ist ein cron-Job, der einen Management-Befehl aufruft, einfacher und genauso effektiv. Fügen Sie Redis-Caching erst hinzu, wenn Sie wiederholte Lesevorgänge desselben Artikels in Ihren Protokollen messen können; für Seiten mit geringem Traffic kostet der Cache mehr an betrieblicher Komplexität, als er an Datenbanklast spart.

Häufig gestellte Fragen

Wie baut man eine Nachrichten-Website mit Django?

Um eine Nachrichten-Website mit Django zu erstellen, definieren Sie ein Article-Modell mit Feldern für Titel, Text, Quelle, Veröffentlichungsdatum, Kategorien und Metadaten. Planen Sie eine Celery-Beat-Aufgabe, die alle 15 Minuten einen Nachrichten-API abfragt und Artikel anhand der externen ID einfügt oder aktualisiert. Rendern Sie Listen mit ListView, cachen Sie heiße Artikel in Redis über das Cache-Framework von Django, fügen Sie unendliches Scrollen für den Feed hinzu, das von HTMX gesteuert wird, und aktivieren Sie die Postgres Volltextsuche über Titel und Texte.

Kann Django Daten von einem REST API abrufen?

Ja – Django kann Daten aus jeder REST API mithilfe von httpx oder requests innerhalb eines Management-Befehls oder einer Celery-Aufgabe abrufen. Das Standardmuster ist ein Celery-Beat-Zeitplan, der den Fetcher alle N Minuten auslöst, Model.objects.update_or_create verwendet, das auf der stabilen Kennung des Upstream-API basiert, um Upserts idempotent zu halten, und jede Einfügung in eine Transaktion einschließt. Versuchen Sie, transiente Fehler mit @shared_task(bind=True, max_retries=3) zu beheben.

Wie plant man periodische Aufgaben in Django?

Planen Sie periodische Django-Aufgaben mit Celery beat, dem Scheduler, der mit Celery gebündelt ist. Fügen Sie ein CELERY_BEAT_SCHEDULE-Dict zu den Einstellungen mit dem Aufgabenspeicherort und schedule (in Sekunden oder einer crontab-Instanz) hinzu, und führen Sie dann celery -A project beat neben Ihrem Worker-Prozess aus. Für sehr einfache Fälle ohne Celery ist ein System-cron-Job, der python manage.py custom_command aufruft, ebenfalls gültig.

Was ist der beste Weg, Nachrichten-Inhalte in Django zu cachen?

Der beste Weg, Nachrichten-Inhalte in Django zu cachen, ist die Cache-Schicht des Frameworks, unterstützt von Redis, mit einem pro-Artikel-TTL, der basierend auf dem Alter des Artikels verfällt. Ein 1-Minuten-TTL für Eilmeldungen unter einer Stunde, 5 Minuten für aktuelle Artikel bis zu 24 Stunden und 1 Stunden für Archive. Invalidieren Sie über ein post_save-Signal, damit ein aktualisierter Artikel seinen Cache-Eintrag sofort löscht.

APITube - News API

Verwandte Artikel

10 Nachrichten API Filter-Muster, die Sie kennen sollten (2026)
Developer Guides

10 Nachrichten API Filter-Muster, die Sie kennen sollten (2026)

10 ausführbare Nachrichten API Filter-Muster — Titelsuche, Daten, Entitäten, Stimmungsanalyse, Quellen und zusammengesetzte Abfragen — mit Curl-, Python- und JSON-Beispielen sowie dem Stolperstein, der Ihren ersten Tag verschwendet.

Nachrichten API Schnelleinstieg: Ihre erste Anfrage in 5 Minuten
Developer Guides

Nachrichten API Schnelleinstieg: Ihre erste Anfrage in 5 Minuten

Rufen Sie in 5 Minuten einen echten Nachrichten-API-Endpunkt auf – curl, JavaScript und Python nebeneinander, mit einer kommentierten JSON-Antwort und Korrekturen für die 401/429-Fehler, auf die Sie tatsächlich stoßen werden.

Telegram News Bot in Python (2026): aiogram + APScheduler
Developer Guides

Telegram News Bot in Python (2026): aiogram + APScheduler

Erstellen Sie einen Telegramm-Nachrichtenbot in Python mit aiogram 3.27, APScheduler und APITube. Async, sentiment-gefiltert, automatische Kanalübertragung — vollständiger Code + Docker.

Real-time news alerts with WebSockets (Socket.IO 2026)
Developer Guides

Real-time news alerts with WebSockets (Socket.IO 2026)

Build real-time news alerts with WebSockets in Node.js using Socket.IO 4.8 and APITube SSE. Room filters, reconnect, Redis adapter, full code.

Wir verwenden Cookies

Durch Klicken auf "Akzeptieren" stimmen Sie der Verwendung von Cookies auf Ihrem Gerät zu, damit die Website funktioniert und für Analysen.