Fix Django Links Using example.com (Sites Framework)

Fix password reset emails, sitemaps, and allauth links that point to example.com: update the Site record, set SITE_ID, handle multiple environments, and get https in generated links.

Problem statement

Emails from your production site contain links like http://example.com/reset/MQ/abc123/. The sitemap lists http://example.com/about/. django-allauth confirmation emails say "Hello from example.com". Nothing in your code mentions example.com.

The text comes from the Sites framework (django.contrib.sites). When its migrations run on a fresh database, Django creates a default Site record with the domain and name example.com. Code that needs your domain without a request, or that asks for the "current site", reads that record. Until you change it, every such link uses example.com. This guide fixes the record, keeps it correct across environments, and makes generated links use https.

Where example.com comes frommigratecreates Site id=1Site recorddomain example.comget_current_sitereads SITE_IDEmails, sitemapswrong linksGenerated links use the Site record's domain, so it must be set per environment.
  1. Check the record: python manage.py shell -c "from django.contrib.sites.models import Site; print(Site.objects.values())".
  2. Update the domain and name of the SITE_ID record to your production host (for example example.org → www.yourdomain.com), without a scheme or trailing slash.
  3. Do it with a data migration or a settings-driven command so every environment gets the right value automatically.
  4. Make generated links use HTTPS: set SECURE_PROXY_SSL_HEADER, and for sitemaps set protocol = "https" where needed.
  5. If you do not need the Sites framework, remove it; get_current_site() then falls back to the request's host.

Step-by-step solution

1. Inspect the current Site

python manage.py shell -c "
from django.conf import settings
from django.contrib.sites.models import Site
print('SITE_ID =', getattr(settings, 'SITE_ID', None))
print(list(Site.objects.values('id', 'domain', 'name')))
"
where example.com leaks outPassword reset emailslinks built from the Site domainSitemapsevery URL on example.comdjango-allauth and syndicationget_current_site()Admin "view on site"absolute URLsAll of them read the same Site row with SITE_IDdjango-deployment.com
Fixing the row fixes all of them at once.

If you see {'id': 1, 'domain': 'example.com', 'name': 'example.com'}, that is the default record.

2. Update it now

In the admin, open Sites → Sites, edit the record matching SITE_ID, and set:

  • Domain name: www.yourdomain.com (host only, no https://, no path)
  • Display name: Your Site

Or from the shell:

python manage.py shell -c "
from django.contrib.sites.models import Site
Site.objects.filter(pk=1).update(domain='www.yourdomain.com', name='Your Site')
"

The Sites framework caches the current site in memory per process. Restart Gunicorn and Celery workers after the change so they do not keep serving the old value.

3. Make it correct in every environment

A manual admin edit is lost when you create a new database (staging, a restore test, a new region). Drive the value from settings instead:

# settings.py
SITE_ID = 1
SITE_DOMAIN = os.environ.get("SITE_DOMAIN", "localhost:8000")
SITE_NAME = os.environ.get("SITE_NAME", "Your Site (dev)")
# core/migrations/0005_set_site_domain.py
from django.conf import settings
from django.db import migrations


def set_site(apps, schema_editor):
    Site = apps.get_model("sites", "Site")
    Site.objects.update_or_create(
        pk=settings.SITE_ID,
        defaults={"domain": settings.SITE_DOMAIN, "name": settings.SITE_NAME},
    )


class Migration(migrations.Migration):
    dependencies = [("sites", "0002_alter_domain_unique"), ("core", "0004_previous")]
    operations = [migrations.RunPython(set_site, migrations.RunPython.noop)]
the domain comes from the environmentSITE_ID = 1SITE_DOMAIN= os.environ.get("SITE_DOMAIN", "localhost:8000")Site.objects.update_or_create(id=settings.SITE_ID,defaults={"domain": settings.SITE_DOMAIN, "name": settings.SITE_NAME})1231Different per environment, no code change.2Run in a data migration or a deploy command.3Host name only: no https://, no trailing slash.Run it on every deploy so restored dumps get corrected toodjango-deployment.com
A small management command is easy to add to the deploy script.

A migration runs once per database. To keep the value in sync on every deploy (for example when a domain changes), use a small management command called by the deploy script:

# core/management/commands/sync_site.py
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand


class Command(BaseCommand):
    def handle(self, *args, **options):
        Site.objects.update_or_create(
            pk=settings.SITE_ID,
            defaults={"domain": settings.SITE_DOMAIN, "name": settings.SITE_NAME},
        )
        Site.objects.clear_cache()

The domain is only half of the URL.

the right domain, the wrong schemeLinks start with http://TLS ends at the proxy✕request.is_secure() is False✕emails link to httpLinks start with https://✓proxy sets X-Forwarded-Proto✓SECURE_PROXY_SSL_HEADER set✓emails use the https protocolThe Site record stores only the domain, never the schemedjango-deployment.com
Password reset emails also accept an explicit protocol argument.

Password reset. PasswordResetView builds links with https when request.is_secure() is true. Behind a proxy, set SECURE_PROXY_SSL_HEADER and forward X-Forwarded-Proto, or links are generated with http.

Sitemaps. The sitemap framework uses the request's scheme when it has one, and each Sitemap class can force it:

from django.contrib.sitemaps import Sitemap


class PostSitemap(Sitemap):
    protocol = "https"

Emails sent from Celery or management commands. There is no request, so templates that use {{ protocol }}://{{ domain }} need the values passed in explicitly:

from django.contrib.sites.models import Site

context = {"protocol": "https", "domain": Site.objects.get_current().domain}

django-allauth uses the current site's name and domain in its default email templates and requires the Sites framework; the Site record fix covers it. Its ACCOUNT_DEFAULT_HTTP_PROTOCOL setting controls the scheme of links in emails.

5. Or drop the Sites framework

If no installed app requires it (allauth does; the admin and auth do not), removing django.contrib.sites from INSTALLED_APPS makes get_current_site(request) return a RequestSite built from the request's host. Links then follow whatever host the request came in on, validated by ALLOWED_HOSTS. Code that runs without a request (Celery, commands) then needs another source for the domain, such as a SITE_URL setting.

Verification check:

python manage.py shell -c "from django.contrib.sites.models import Site; print(Site.objects.get_current().domain)"
curl -s https://www.yourdomain.com/sitemap.xml | grep -o "<loc>[^<]*" | head -3

Trigger a password reset for a test account and check the link in the received email: correct domain and https://.

Explanation

The Sites framework was designed for running several sites from one project and database, each identified by a Site row and selected with SITE_ID. Many apps use it simply to know "our domain" when there is no request to ask. Because Django seeds the table with a placeholder on first migration, every project that uses it starts with example.com until someone changes the row. Treating the row as configuration, set from settings on each environment, removes the manual step that is so easy to miss.

Edge cases / notes

Staging and production share a database dump

Restoring a production dump into staging copies the production Site record, so staging emails link to production. Run the sync command after every restore.

Multiple sites

With several domains, use one Site per domain and either separate SITE_ID values per deployment or CurrentSiteMiddleware with request-based lookup (leave SITE_ID unset and Django matches the request host against Site domains).

When to automate this

Call the site sync command in every deploy and after every database restore, and add a check to your post-deploy verification that the current Site domain matches the expected host.

Next steps

FAQ

The Sites framework's default record still has the domain example.com. Update the record for your SITE_ID.

Should the Site domain include https://?

No. Enter only the host name (and port if needed). The scheme comes from the request or explicit settings.

How do I set the Site domain automatically on deploy?

Use a data migration for new databases and a management command in the deploy that updates the record from settings.

Can I remove django.contrib.sites?

Yes, if no installed app depends on it. get_current_site() then uses the request's host.

Keep going