Skip to main content

Small Django blog/news app with multi-site support (django.contrib.sites).

Project description

django-site-blog

Tests Python Django License

Small Django blog / news app with multi-site support (django.contrib.sites).

Lets editors restrict an article to one or more Site objects, or leave the assignment empty so the article appears on every site that runs the same Django project.

Why?

Many Django projects host multiple sites — a corporate front page, a research portal, project subsites — from a single codebase using django.contrib.sites. Most blog/news apps either ignore that scenario or expect a separate deployment per site.

django-site-blog keeps one article store and lets editors say, per article, whether it should appear on every site (the default) or be restricted to a chosen subset. One schema, one query, no middleware gymnastics.

Features

  • Per-article site assignment via M2M to django.contrib.sites.models.Site.
  • "Empty M2M = visible everywhere" default — restrict only when you need to.
  • draft/published status via model_utils.StatusModel; admin-aware get_absolute_url so drafts link back into the admin.
  • Split-marker excerpts via model_utils.SplitField (marker configurable through the SPLIT_MARKER setting).
  • Multi-site-aware ArticleDetailView (slug-based, resolves the active site via django.contrib.sites.shortcuts.get_current_site(request) — honors CurrentSiteMiddleware for per-Host deployments and falls back to settings.SITE_ID for single-site projects).
  • Article.on_site CurrentSiteManager for pure "current-site only" semantics when the empty-equals-all default isn't what you want.
  • Polish translation included; rest of the UI is gettext_lazy-ready.
  • Admin with filter_horizontal, list_filter, slug prepopulation.

Supported versions

Django × Python

Django 3.10 3.11 3.12 3.13 3.14 Status
5.2 LTS Active LTS (extended support Apr 2028)
6.0 Mainstream Aug 2026, extended Apr 2027

Verified against the CI matrix in .github/workflows/tests.yml. Also requires django-model-utils >=5,<6 (for SplitField, TimeStampedModel, StatusModel). The 5.x release changed how SplitField auto-injects its _article_body_excerpt column at runtime; the shipped 0001_initial migration uses SeparateDatabaseAndState to keep the migration state and the database in sync without emitting the column twice on a fresh migrate.

Installation

uv add django-site-blog

or with pip:

pip install django-site-blog

Add the app and django.contrib.sites to INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    "django.contrib.sites",
    "siteblog",
]

SITE_ID = 1  # required by django.contrib.sites

Include the URLs in your project's urls.py:

urlpatterns = [
    # ...
    path("articles/", include("siteblog.urls")),
]

Run migrations:

python manage.py migrate

How site assignment works

Each Article has a sites M2M to django.contrib.sites.models.Site.

  • Empty M2M → article visible on every site (the default).
  • One or more sites set → article only visible on the listed sites.

The included ArticleDetailView resolves the active site via django.contrib.sites.shortcuts.get_current_site(request) and then runs the inverted-default query through the chainable ArticleQuerySet:

from django.contrib.sites.shortcuts import get_current_site

site = get_current_site(request)
Article.objects.published().visible_on_site(site)

get_current_site() honors request.site set by CurrentSiteMiddleware (per-Host multi-site deployments) and transparently falls back to settings.SITE_ID for single-site projects — so the same view works in both modes without configuration. visible_on_site() accepts either a Site instance or its primary key.

For list / feed views in your own project, prefer this method over Article.on_site — the latter is a plain CurrentSiteManager and treats empty-M2M articles as invisible everywhere, which silently drops your "visible on every site" default.

Settings

Setting Default Purpose
SITE_ID Required by django.contrib.sites. Drives the per-site filtering.
SPLIT_MARKER "<!-- split -->" Marker shown in the admin help text for Article.article_body to indicate the split-point between excerpt and full article. The actual splitting is performed by model_utils.fields.SplitField per its own settings.

Templates

The package ships two minimal templates:

  • siteblog/article_detail.html — uses {% extends "siteblog/base.html" %}.
  • siteblog/base.html — a bare HTML skeleton; override it in your project by placing a siteblog/base.html ahead of the package's in your TEMPLATES DIRS.

Security — trust boundary on article_body

siteblog/article_detail.html renders article.article_body.content through Django's |safe filter, so HTML written by editors is emitted verbatim. This is deliberate — the field is intended for authoring formatted content (<strong>, <h2>, <a>, <blockquote>, etc.) — but it makes the admin a trusted authoring surface.

If everyone with change_article permission is trusted to author HTML (the default Django-admin assumption), no extra work is required. If that is not the case in your deployment, either:

  • sanitize on save (e.g. a clean_article_body that runs bleach over the value), or
  • override siteblog/article_detail.html in your project and drop |safe (renders the raw markup as text).

The same caveat applies to any list / excerpt view you build on top of article_body.excerpt.

Example / demo project

The repository ships with a runnable demo at example/ that wires siteblog together with four django.contrib.sites rows (example.com, mac-mini, localhost, 127.0.0.1) so you can see per-host filtering in action from a single dev server. The seeded articles use the <!-- split --> marker, so the homepage shows just the excerpt with a "Read more →" link, while the detail page renders the full body with <strong> / <em> / <h2> / lists / blockquotes / <code> to illustrate the package's |safe rendering path.

Quick start:

cd example
DJANGO_SETTINGS_MODULE=example_project.settings uv run python manage.py migrate
DJANGO_SETTINGS_MODULE=example_project.settings uv run python manage.py seed_demo
DJANGO_SETTINGS_MODULE=example_project.settings uv run python manage.py createsuperuser
DJANGO_SETTINGS_MODULE=example_project.settings uv run python manage.py runserver

Then visit http://localhost:8000/. The header shows a "Sign in to admin" link pointing at /admin/login/, which switches to "Admin (username)" once you sign in.

Optional: TinyMCE rich text editor in the admin

The package ships with a plain <textarea> for article_body. The example project can swap that for a TinyMCE editor via the rich-editor extras dependency:

# from the repository root:
uv sync --extra rich-editor                   # installs django-tinymce

cd example
RICH_EDITOR=1 DJANGO_SETTINGS_MODULE=example_project.settings \
    uv run python manage.py runserver

When RICH_EDITOR=1 is set, the example's settings.py adds tinymce to INSTALLED_APPS, the URLconf mounts tinymce.urls, and example_project/admin.py (auto-loaded by Django's admin autodiscover) unregisters the package's ArticleAdmin and re-registers it with a TinyMCE widget on article_body. The model, migrations, and public view are unchanged — only the admin form widget differs, so the rich editor is purely a demo-side overlay you can opt into in your own project.

If RICH_EDITOR=1 is set but django-tinymce is missing, settings.py raises a clear ImproperlyConfigured at startup with the exact install command, instead of a ModuleNotFoundError from deep in Django's app registry.

See example/README.md for the multi-host testing recipe (/etc/hosts vs. curl --resolve) and a full breakdown of which articles are visible on which hostnames.

Development

git clone https://github.com/iplweb/django-site-blog.git
cd django-site-blog
uv sync --all-extras --all-groups
DJANGO_SETTINGS_MODULE=tests.settings uv run pytest

Dependency layout: the user-facing rich-editor opt-in lives under [project.optional-dependencies] (published in PyPI metadata), while the local-only tooling (pre-commit, ruff, pytest, pytest-django) is declared under PEP 735 [dependency-groups] so it never reaches published metadata. CI installs them targeted (uv sync --group test for the matrix, uv sync --group dev for lint).

pre-commit install to wire ruff + pyupgrade + django-upgrade.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_site_blog-0.2.1.tar.gz (13.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_site_blog-0.2.1-py3-none-any.whl (13.8 kB view details)

Uploaded Python 3

File details

Details for the file django_site_blog-0.2.1.tar.gz.

File metadata

  • Download URL: django_site_blog-0.2.1.tar.gz
  • Upload date:
  • Size: 13.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for django_site_blog-0.2.1.tar.gz
Algorithm Hash digest
SHA256 b5d47cb50c7c2994100a548aee603e92dcf91d8f19d172c5914fe079b5e583f8
MD5 59c5dedf181168a121ca6ae7a6417cb8
BLAKE2b-256 eb3c3eae4a5043fdf5516fe67df690ebf863916d2e97be924d781eec32451fd9

See more details on using hashes here.

File details

Details for the file django_site_blog-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: django_site_blog-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 13.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for django_site_blog-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 51372c548ff5d70135b0548d87a92e3c2508bde9ebd6a9db19dd45c2391a1d05
MD5 d39f062a10be8640fa77648ec2264ab6
BLAKE2b-256 8d3e0e93d33039fd0b022a37d6ef838ddfee64357bc02c2c818002c012d2662e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page