Building a Campaign Landing Page in Django, Step by Step

What makes a campaign landing page different

A campaign landing page has a narrower purpose than a conventional website page. It is usually designed to support one action, such as registering for an event, requesting information, downloading a resource, or joining a mailing list. The page must therefore connect three parts of the same system: a clear message for visitors, a form or action that Django can process, and reliable data handling on the server.

Before writing code, define the conversion goal and the information required to complete it. A form asking for a name, email address, and company will require different validation and privacy considerations from a page that only redirects visitors to another resource. Keeping the scope focused also makes it easier to test whether the page behaves correctly.

A useful campaign page normally includes:

Plan the Django structure before coding

Django separates responsibilities between models, views, templates, forms, and URL configurations. A campaign landing page can use a dedicated application, which keeps campaign code separate from unrelated features in a larger project.

For example, create an application named campaigns from the project directory:

python manage.py startapp campaigns

Register the application in the project settings under INSTALLED_APPS. The exact settings module depends on the project structure, but the principle is consistent: Django must know that the application exists before it can discover its models, templates, and migrations.

A practical file layout could look like this:

This separation prevents the view from becoming a large block of HTML and business logic. It also allows another campaign to reuse the same patterns without copying an entire feature into an unrelated module.

Create a model for campaign submissions

If the campaign collects information, define what must be stored and why. A model makes the data structure explicit and gives Django a controlled path to the database. The following example stores a name, an email address, the time of submission, and a consent flag:

from django.db import models

class CampaignSubmission(models.Model:

name = models.CharField(max_length=150)
email = models.EmailField()
consent = models.BooleanField(default=False)
submitted_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-submitted_at']

The closing parenthesis in the class declaration must be included in real code: the complete declaration is CampaignSubmission(models.Model). It is shown separately here to keep the example readable in a restricted HTML format.

Use a field length that reflects the application requirement rather than accepting unlimited text. If an email address must be unique for a specific campaign, that rule can be implemented with a database constraint, but the decision depends on the expected user journey. A visitor may legitimately register more than once if the campaign supports several sessions or if a correction is needed.

After creating or changing the model, generate and apply migrations:

python manage.py makemigrations campaigns
python manage.py migrate

Do not treat the database as a temporary form inbox. Decide how long submissions should be retained, who can access them, and how deletion requests or corrections will be handled. Those decisions belong in the application design, not as an afterthought during deployment.

Build a ModelForm with deliberate validation

A ModelForm can connect the HTML form to the model while providing a central place for field validation. In campaigns/forms.py, define only the fields that visitors are allowed to submit:

from django import forms
from .models import CampaignSubmission

class CampaignSubmissionForm(forms.ModelForm):

class Meta:
model = CampaignSubmission
fields = ['name', 'email', 'consent']
widgets = {
'name': forms.TextInput(attrs={'autocomplete': 'name'}),
'email': forms.EmailInput(attrs={'autocomplete': 'email'}),
}

Django validates required fields and email syntax at the form layer. Add custom rules when the campaign needs them. For example, a clean_email method can normalize whitespace and reject an address that is not appropriate for the campaign. Avoid treating client-side validation as a security control because a browser can bypass JavaScript and submit a crafted request directly to the server.

Consent requires particular care. A checkbox should not be preselected when the application expects an affirmative action, and the server should reject a submission when the required consent value is false. The form must explain what the visitor is agreeing to. Do not collect unrelated information merely because the database can store it.

Connect the form to a safe view

The view needs two distinct paths: a GET request that displays an empty form and a POST request that validates and saves submitted data. A function-based view is suitable for a compact campaign flow:

from django.shortcuts import render, redirect
from .forms import CampaignSubmissionForm

def landing_page(request):

if request.method == 'POST':
form = CampaignSubmissionForm(request.POST)
if form.is_valid():
form.save()
return redirect('campaign_success')
else:
form = CampaignSubmissionForm()
return render(request, 'campaigns/landing_page.html', {'form': form})

This follows the POST, Redirect, GET pattern. After a successful submission, the browser is redirected to a success URL rather than rendering the result directly from the POST request. Refreshing the confirmation page therefore does not normally repeat the original form submission.

For campaigns where duplicate submissions are costly or undesirable, add an explicit strategy. Possible approaches include a unique constraint for an appropriate combination of fields, a short-lived submission token, or a business rule that identifies repeated requests. Do not reject duplicates based only on an email address unless that behavior is clear to visitors and valid for the campaign.

Use Django's message framework if the page needs a temporary status message, and log unexpected failures without recording sensitive form contents in logs. In production, errors should not expose tracebacks or database details to visitors.

Define campaign URLs that remain readable

In campaigns/urls.py, add named routes:

from django.urls import path
from . import views

urlpatterns = [

path('campaign/', views.landing_page, name='campaign_landing'),
path('campaign/thanks/', views.success_page, name='campaign_success'),
]

Include these routes from the main project URL configuration. Named URLs are preferable to hard-coded paths in templates and redirects. If the campaign URL later changes, the view and template can continue referring to the route name.

When several campaigns share the same application, use a stable identifier such as a slug. A campaign model can hold the headline, description, active status, and destination settings, while a generic view renders the selected campaign. That approach is useful when non-developers need to manage copy, but it introduces additional administration and validation requirements. For one short-lived page, a dedicated route and template may be clearer and safer.

Design the landing page template around the action

The template should present the offer before asking for information. Extend the project base template so that shared metadata, navigation, and static file handling remain consistent:

{% extends 'base.html' %}
{% load static %}
{% block content %}
<main>
<section>
<h1>A specific campaign promise</h1>
<p>Explain what the visitor receives and what happens after submission.</p>
</section>
<form method="post">
{% csrf_token %}


<button type="submit">Request the information</button>
</form>
</main>
{% endblock %}

The CSRF token is essential for a session-authenticated Django form. It helps protect against cross-site request forgery and must be present inside the POST form. If custom markup is needed, render each field separately so labels, help text, errors, and autocomplete attributes remain associated with the correct control.

Use a button label that describes the result of the action. Avoid vague labels when the visitor may be unsure what will happen. Keep the main call to action visually distinct, but do not hide legal information or error messages through color alone.

The page title and meta description should reflect the actual campaign offer. Add a clear heading hierarchy, a visible focus state, sufficient contrast, and labels that remain understandable when the layout is enlarged. Images should have meaningful alternative text when they convey information. Decorative images can use an empty alternative attribute.

Add styling without making the page fragile

Campaign pages often benefit from a focused stylesheet rather than a large collection of visual effects. Define a readable content width, consistent spacing, responsive form controls, and a prominent but accessible action button. Keep the form usable on narrow screens by allowing inputs to occupy the available width and by avoiding fixed horizontal dimensions.

Load a static stylesheet through Django's static files system and keep source assets separate from collected production assets. In development, Django can serve static files according to the project configuration. In production, the deployment process should collect and serve them through an appropriately configured web server or storage layer.

Do not rely on JavaScript to make the primary submission work. A visitor with scripts disabled, a browser extension that blocks a library, or a network interruption should still receive a useful server-rendered page. JavaScript can improve field feedback or prevent an accidental second click, but the server remains responsible for validation and persistence.

Protect the submission flow

A public campaign form is an exposed input surface. Before deployment, review the following controls:

Do not send personal data to analytics systems without understanding what is collected and whether the processing is appropriate. If tracking parameters are used, define which parameters are accepted and avoid placing sensitive information in URLs. A campaign identifier can be useful, while an email address in a query string creates unnecessary exposure through browser history, server logs, and shared links.

Add attribution and measurement without contaminating the form

Campaign performance often requires identifying the source of a visit. Common query parameters can be captured from the request and stored with the submission, provided the purpose is documented and the values are validated. For example, a source field can record a short campaign label, while arbitrary query strings should not be copied into a database without limits.

Keep attribution separate from visitor-entered content when possible. A hidden field can be modified by the browser, so it must be treated as untrusted input. If the application needs trustworthy campaign metadata, resolve a known campaign identifier on the server and validate that the campaign is active.

Measurement should also cover technical events: page rendering errors, rejected submissions, successful saves, and email delivery failures if confirmation messages are sent. Logs should contain enough context to diagnose problems without exposing names, email addresses, or raw request bodies. Establish a retention period for operational logs and submissions rather than keeping all data indefinitely.

Test the complete visitor journey

Testing a landing page means more than checking that the first screen looks correct. Test the complete path from page request to database record and confirmation response.

  1. Open the landing page with a GET request and confirm that the form renders.
  2. Submit an empty form and verify that each required field produces an understandable error.
  3. Submit malformed email values and confirm that the server rejects them.
  4. Submit valid data and check that one record is saved with the expected values.
  5. Refresh the success page and verify that the original POST is not repeated.
  6. Attempt a POST without a valid CSRF token and confirm that the request is rejected according to the security configuration.
  7. Test keyboard navigation, visible focus, labels, error placement, and mobile widths.
  8. Review the rendered page with scripts disabled to confirm that the core action still works.
  9. Check that unauthorized users cannot access administrative submission data.

Django's test client can exercise views without opening a real browser. A test suite should cover the form's validation rules, the view's redirect behavior, and any deduplication or attribution rules. Browser-based testing remains useful for layout, focus management, and interactions that depend on CSS or JavaScript.

Prepare deployment and ongoing maintenance

Before publishing the page, run Django's deployment checks and review settings for the target environment. Confirm that DEBUG is disabled in production, static files are available, the database migration has been applied, and the host configuration accepts the intended domain. Configure error reporting that alerts maintainers without revealing sensitive details to visitors.

Use a backup and recovery plan for the database before accepting submissions. A backup is useful only when restoration has been tested in an appropriate environment. If the campaign is temporary, define what happens when it ends: close the form, redirect the URL, archive the records according to the retention policy, or display a clear end-of-campaign message.

Monitor the page after release. A sudden increase in validation errors may indicate confusing copy, a broken integration, or automated abuse. A high number of page requests with few successful submissions can also reveal performance, accessibility, or trust problems. Technical monitoring should complement, not replace, direct review of the user journey.

A reusable implementation checklist

Use this checklist when reviewing a Django campaign landing page:

A campaign landing page is a small feature with several responsibilities. Django provides the structure for handling them, but quality depends on the decisions around data collection, validation, accessibility, security, and maintenance. Building each layer deliberately produces a page that is more dependable than a template focused only on appearance.

Frequently asked questions

Should a campaign page use a separate Django application?

A separate application is useful when the campaign has its own models, forms, views, and tests or when several campaigns will share the same infrastructure. A single short-lived page can remain within an existing application if that keeps the project clearer.

Can client-side JavaScript replace Django form validation?

No. Browser validation and JavaScript feedback improve usability, but they can be bypassed or fail to load. Django must validate the submitted data on the server before saving it or triggering another action.

Why use a success page instead of rendering a message after POST?

The redirect after a successful POST follows the POST, Redirect, GET pattern. It reduces accidental resubmission when the visitor refreshes the confirmation page and gives the success state its own URL.

How should duplicate submissions be handled?

The answer depends on the campaign's business rule. A database constraint, a submission token, or a review rule may be appropriate. Avoid assuming that every repeated email address is an error, because visitors may need to correct or repeat a registration.

Is collecting an email address enough for a campaign?

Not necessarily. The form should collect the minimum information required for the stated purpose, explain how it will be used, and apply an appropriate retention and access policy. The technical model should reflect those decisions.

Learn more about this: