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.
- Check the record:
python manage.py shell -c "from django.contrib.sites.models import Site; print(Site.objects.values())". - Update the domain and name of the
SITE_IDrecord to your production host (for exampleexample.org→www.yourdomain.com), without a scheme or trailing slash. - Do it with a data migration or a settings-driven command so every environment gets the right value automatically.
- Make generated links use HTTPS: set
SECURE_PROXY_SSL_HEADER, and for sitemaps setprotocol = "https"where needed. - 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')))
"
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, nohttps://, 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)]
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()
4. Get https in generated links
The domain is only half of the URL.
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
- If reset emails do not arrive at all, see Fix Django Emails Not Sending in Production.
- Keep environments separate with Set Up a Staging Environment for Django.
- Fix other
http://links with Fix Mixed Content Warnings on a Django HTTPS Site. - Wagtail has its own Sites model with the same pitfall; see Deploy Wagtail CMS to Production.
FAQ
Why do Django password reset emails link to example.com?
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.