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.1

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.1
File Size Uploaded
django_freeletter-0.2.1.tar.gz 65.4 kB Details

Built distribution (wheel)

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

Total release size: 87.9 kB

Release files / django_freeletter-0.2.1.tar.gz

Download URL django_freeletter-0.2.1.tar.gz
Size 65.4 kB
Tags Source
SHA-256 checksum
How to use checksums
ed2ea7f892d9dc33c35ae8ab169d38bd3622ab7a248b21a3a2299c8da143e2e2
BLAKE2b-256 checksum
How to use checksums
1064eaa03b7a5e249eb899e31622b05a8da764b42bb3e879bfff2c648afe7124
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.1-py3-none-any.whl

Download URL django_freeletter-0.2.1-py3-none-any.whl
Size 22.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
87060588d73b30d7a277d05295ad29b04933d5132afdff253e7f398d533bce00
BLAKE2b-256 checksum
How to use checksums
e5146b37f91d76d9cc1dfd81592d54d8f677617089dc0eee45b5eada58dd88a2
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

This release

0.2.1 This release

2 release files

0.2.0

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