Skip to main content

django-freeletter

A small Django newsletter engine: subscribers with double opt-in, issues made of ordered content blocks, a web-viewable archive, and a plain management-command send queue. No e-commerce framework, no CMS, no particular project's assumptions baked in — just Django.

What this package does, and doesn't, do

  • Subscriber: email + optional name, double opt-in (confirm via a tokenized email link before ever receiving a send), a separate unsubscribe token.
  • Issue: a draft → queued → sent newsletter, made of ordered IssueBlocks.
  • One built-in block type: "html" — free-form content (rich text + an optional image). That's the only content type this package itself knows about.
  • A pluggable block-type registry (freeletter.blocks) — anything else (a blog post teaser, a product card, whatever a host project wants to feature in an issue) is registered by a different package or the host project itself, pointing at its own template and its own model, via a generic (content_type, object_id) pointer. freeletter never imports or knows about that other model — see "Registering a block type" below.
  • A public archive (freeletter/) of sent issues, subscribe/confirm/ unsubscribe views, and a send_freeletter_issues management command meant to run off a plain cron/systemd timer, not a task queue.
  • It does not ship any dashboard/admin UI beyond Django's own admin.py registration, any e-commerce integration, or any CTA placement — all host-project scope.

Installation

uv add django-freeletter
INSTALLED_APPS = [
    ...,
    "django.contrib.contenttypes",  # required by IssueBlock's generic FK
    "freeletter",
]

FREELETTER_FROM_EMAIL = "newsletter@example.com"  # defaults to DEFAULT_FROM_EMAIL
FREELETTER_BASE_URL = "https://example.com"  # required for links in emails sent outside a request (the send command)
# urls.py
urlpatterns = [
    path("newsletter/", include("freeletter.urls")),
]

Run manage.py migrate.

Registering a block type

# yourapp/apps.py
from django.apps import AppConfig

class YourAppConfig(AppConfig):
    def ready(self):
        from freeletter.blocks import BlockType, register_block_type

        register_block_type(
            BlockType(
                slug="product",
                label="Product",
                template_name="yourapp/freeletter_blocks/product.html",
            )
        )
{# yourapp/freeletter_blocks/product.html — `object` is the resolved content_object #}
<a href="{{ object.get_absolute_url }}">{{ object.title }} — {{ object.price }}</a>

An editor then creates an IssueBlock with block_type="product" and content_object=<some product instance> (e.g. via Django admin, or a host project's own dashboard) — freeletter resolves and renders it without ever importing the product model.

Sending

manage.py send_freeletter_issues

Sends every Issue with status="queued" to every active (is_confirmed and not unsubscribed_at) subscriber, then marks it sent. A single recipient's send failure is logged and skipped, not fatal to the run. Point a systemd timer or cron entry at this command — there's no daemon or task queue involved by design, matching the "prepare for sending in an agnostic way" requirement this package was built for: swapping the mail transport later (a different SMTP relay, an ESP used as an SMTP backend) only ever means changing EMAIL_BACKEND, nothing here.

Optional batching, if a mail relay needs it:

FREELETTER_BATCH_SIZE = 50
FREELETTER_BATCH_DELAY = 2  # seconds, after every FREELETTER_BATCH_SIZE sends

Translations

The package ships a Spanish translation (src/freeletter/locale/es/). Both the .po source and the compiled .mo are committed, so the wheel includes the .mo and nothing needs compiling on install. After editing the .po, recompile before releasing:

msgfmt -o src/freeletter/locale/es/LC_MESSAGES/django.mo \
    src/freeletter/locale/es/LC_MESSAGES/django.po

New strings are extracted with django-admin makemessages -l es run from src/freeletter/. A project can override any single string in its own LOCALE_PATHS without copying the whole catalogue. Emails are rendered in the active language (the project's LANGUAGE_CODE when sent from the send_freeletter_issues command); there is no per-subscriber language yet.

Development

uv sync
uv run pytest

Metadata

Release files for django-freeletter 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-freeletter 0.2.0
File Size Uploaded
django_freeletter-0.2.0.tar.gz 63.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-freeletter 0.2.0
File Interpreter ABI Platform
django_freeletter-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 85.6 kB

Release files / django_freeletter-0.2.0.tar.gz

Download URL django_freeletter-0.2.0.tar.gz
Size 63.6 kB
Tags Source
SHA-256 checksum
How to use checksums
ef9118dfd3d235ba53af487fab73d814cbcdf1a09559c43af62f867d6c610fa7
BLAKE2b-256 checksum
How to use checksums
b687c222b7f91eb4af9993829515c461a038aa138c5572d7c1ed7786bddcb8d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / django_freeletter-0.2.0-py3-none-any.whl

Download URL django_freeletter-0.2.0-py3-none-any.whl
Size 22.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bc192330ef435d1cef2a458096c9b7a2629f7b20918f5ae962bcf963aece0546
BLAKE2b-256 checksum
How to use checksums
434aabfc3db9dc53660e47ee0a0292ea03827efaf75f974259f2673e32aa835f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

0.2.1

2 release files

This release

0.2.0 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page