Note
This project is in pre-alpha development. We may introduce breaking changes without warning. Please review PR details regularly for summaries of changes when pulling new versions of this repo.
Wagtail build with common organizing features, web best practices, and modern dev tools out of the box.
Built by With the Ranks, used as a starting point for campaign, nonprofit, and organizer websites, and updated with new features periodically.
Fork this repo and customize via Tailwind theme to build your own site, and merge upstream changes to get new features as they're released. See #Customization for more information.
Supports theming via semantic Tailwind design tokens, supports multiple languages, contains a StreamField library with common organizing blocks like Signup and Donate, has integrations with common organizing platforms like Action Network and Actblue, features custom page types ready to extend and more.
The wtrx/ app (repo root, sibling to wagtail_wtr/) contains core shared features and is designed for eventual extraction as a standalone pip package
(wagtail-wtrx), inspired by CodeRed Extensions, so that sites built on top of wagtail-wtr can utilize future platform updates.
- Python 3.13+
- Node 20+ (see
.nvmrc) - PostgreSQL (production) or SQLite (dev)
git clone https://github.com/withtheranks/wagtail-wtr.git mysite
cd mysitemake venv
source .venv/bin/activate
npm installmake buildmake migrate
make setup # interactive: site name, donation platform, signup platformmake createsuperuser
make devVisit http://localhost:8000/ for the site and http://localhost:8000/admin/ for the Wagtail admin.
Use the Fork button on GitHub, or with the GitHub CLI:
gh repo fork With-the-Ranks/wagtail-wtr --clone --remote
cd wagtail-wtr # use your fork's name here if you renamed it on GitHubIf you fork manually, clone your fork and add the upstream remote:
git clone git@github.com:yourorg/your-site.git
cd your-site
git remote add upstream git@github.com:With-the-Ranks/wagtail-wtr.gitFollow the Quickstart steps above. The short version:
make venv
source .venv/bin/activate
npm install
make build
make migrate
make setup # sets site name, donation platform, signup platform
make createsuperuser
make dev- Public site name: set during
make setup— stored in the WagtailSitemodel (also editable later in Wagtail admin → Settings → Sites). - Admin interface label: update
WAGTAIL_SITE_NAMEinwagtail_wtr/settings/base.py. - Brand colors and fonts: edit
static_src/css/theme.css(see Customizing the theme).
Everything else is covered in What to customize below.
When new blocks, bug fixes, or features land in this repo, pull them into your fork:
git fetch upstream
git merge upstream/mainMost upstream changes land in wtrx/, templates/, and
static_src/css/main.css — none of which you should be editing on your fork.
The only file likely to produce a conflict is static_src/css/theme.css if you
have customised your brand colors. Resolve by keeping your @theme {} block and
accepting upstream additions (e.g. new [data-theme] presets) from the merge.
After merging, run make build to regenerate static_compiled/ from your local theme.css.
If you build something reusable — a new block, a base model improvement, a
bug fix — you can contribute it back to wagtail-wtr via a pull request.
Rule of thumb: only code in wagtail_wtr/wtrx/, templates/, and shared
static_src/ belongs upstream. Brand colors, site-specific page models, and
fork-specific configuration stay on your fork.
-
Identify the commit(s) to contribute. Your fork's
mainmay contain site-specific commits mixed in with reusable work. Usegit log --onelineto find the relevant commit SHA(s). -
Create a feature branch off upstream
main:git fetch upstream git checkout -b feature/my-feature upstream/main
-
Cherry-pick the reusable commit(s) onto the branch:
git cherry-pick <sha>
If the commit bundles site-specific changes with reusable ones, split it using
git cherry-pick --no-commit <sha>(applies the diff without committing), thengit reset HEADto unstage everything, thengit add -pto selectively stage only the reusable hunks before committing. Alternatively, reconstruct the diff manually on the new branch. -
Push the branch to your fork (origin) and open a PR targeting upstream:
git push origin feature/my-feature
Then open a PR on GitHub:
- Base repository:
With-the-Ranks/wagtail-wtr, base branch:main - Head repository: your fork, head branch:
feature/my-feature
GitHub shows a "compare across forks" link on the upstream repo's Pull Requests page. The direct URL pattern is:
https://github.com/With-the-Ranks/wagtail-wtr/compare/main...<yourorg>:feature/my-feature - Base repository:
-
After the PR is merged, rebase your fork's
mainonto upstream so the duplicate commit is cleanly dropped:git fetch upstream git checkout main git rebase upstream/main git push origin main --force-with-lease
If the upstream maintainer merged the PR without changes, Git detects the equivalent patch and skips it automatically. If the commit was amended or squash-merged upstream, you may see a small conflict — resolve it with
git rebase --skipto drop the duplicate. After the rebase your fork'smainshould be ahead by only your site-specific commits and 0 commits behind upstream.
| File / directory | What to change |
|---|---|
static_src/css/theme.css |
Brand colors (--color-primary-*, etc.), fonts, theme presets |
wagtail_wtr/settings/base.py |
WAGTAIL_SITE_NAME, WTRX_DONATION_PLATFORM, WTRX_SIGNUP_PLATFORM, LANGUAGES |
templates/ |
Override or extend any template (shadow templates/wtrx/<path>) |
static_src/javascript/ |
Add site-specific JS components |
| Wagtail admin | Settings > Branding, Navigation, Footer, Social, Integrations |
| File / directory | Why |
|---|---|
wtrx/ |
Core reusable app — blocks, page models, settings models, views, hooks. Upstream changes land here. |
static_src/css/main.css |
Tailwind infrastructure — imports, plugins, base layer. Leaving it unedited ensures conflict-free upstream merges. |
- Page models: all built-in page types (
HomePage,ContentPage,IndexPage,FormPage) live inwtrx/— use them as-is. To add site-specific page types, create a new app and subclassBasePageandHeroMixinfromwtrx/. - StreamField blocks: use
BodyStreamBlockas-is, or subclass it to add site-specific blocks (see Customizing blocks) - Settings models: create new
BaseSiteSettingsubclasses (fromwagtail.contrib.settings) in your own app if you need additional settings panels beyond whatwtrx/already provides.
If your fork needs a modified version of an existing block (e.g. adding a field to
CardBlock), subclass it at the site level rather than editing wtrx/ directly.
Wagtail's DeclarativeSubBlocksMetaclass merges parent and child block definitions
via the MRO, so a subclass only needs to redeclare the blocks it wants to change.
Example: adding a subtitle to CardBlock
Create a site-level blocks module (e.g. wagtail_wtr/mysite/blocks.py):
from django.utils.translation import gettext_lazy as _
from wagtail.blocks import CharBlock, ListBlock
from wtrx.blocks import (
BodyStreamBlock,
CardBlock,
CardGridBlock,
SectionBlock,
SectionContentBlock,
)
class SiteCardBlock(CardBlock):
"""CardBlock with an additional subtitle field."""
subtitle = CharBlock(required=False, label=_("Subtitle"))
class SiteCardGridBlock(CardGridBlock):
"""CardGridBlock that uses SiteCardBlock."""
cards = ListBlock(SiteCardBlock(), min_num=2, max_num=12, label=_("Cards"))
class SiteSectionContentBlock(SectionContentBlock):
"""Override card inside sections."""
card = SiteCardBlock()
card_grid = SiteCardGridBlock()
class SiteSectionBlock(SectionBlock):
content = SiteSectionContentBlock()
class SiteBodyStreamBlock(BodyStreamBlock):
"""Site-level override that swaps in custom blocks."""
card = SiteCardBlock()
card_grid = SiteCardGridBlock()
section = SiteSectionBlock()Then update page models to use SiteBodyStreamBlock (in a new site-specific app):
# mysite/models.py
from mysite.blocks import SiteBodyStreamBlock
class HomePage(BasePage, HeroMixin):
body = StreamField(SiteBodyStreamBlock(), ...)Key points:
wtrx/stays untouched — upstream merges are clean.SiteCardBlockinherits all upstream fields, validation, and template fromCardBlock. If upstream adds a field, your subclass gets it automatically.SectionContentBlockexists specifically to support this pattern — it's a namedStreamBlocksubclass so you can override individual child blocks without duplicating the full 17-entry block list.- The only merge friction is the import-line changes in
mysite/models.py— trivial one-line conflicts. - Template overrides: block templates live in
templates/components/streamfield/blocks/. You can modify them directly on your fork. Whenwtrxis extracted to a pip package, Django's template resolution will prefer your project-level templates over the package defaults.
| Page type | Description |
|---|---|
HomePage |
Site root with hero + StreamField body |
ContentPage |
General-purpose content page with hero + body |
IndexPage |
Auto-lists child pages in a card grid; optional intro + body |
FormPage |
Wagtail form builder with AJAX submission + email notification |
| Category | Blocks |
|---|---|
| Content | Text, Image, Video, Button, Quote, Raw HTML, Table |
| Layout | Section (with background/padding), Card Grid, Accordion |
| Composite | Callout (image + text side-by-side), Hero (mid-page) |
| Cards | Card, Person Card |
| Actions | Donate, Signup (wagtail_forms / Action Network / link variants) |
- Branding & SEO — logo, favicon, default meta image, site description
- Navigation — primary nav links, CTA button
- Footer — footer nav sections, copyright text
- Social — social platform links
- Integrations — donation platform (ActBlue), signup platform (wagtail_forms / Action Network)
- Semantic Tailwind design tokens (
bg-primary-600,font-heading, etc.) — customize by editingstatic_src/css/theme.css - Multi-lingual from day one via wagtail-localize
- AJAX form submission (FormPage + SignupBlock)
- Custom image model with focal point CSS
- Production-ready: WhiteNoise, gunicorn, django-storages (S3), dj-database-url
make venv Create .venv and install all dependencies
make dev Run development server (localhost:8000)
make build Build CSS + JS — development (Tailwind CLI + JS + fonts + images copy)
make build-prod Build CSS + JS — production (minified CSS + JS + fonts + images copy)
make build-js Copy JS source to static_compiled/js/
make build-fonts Copy font files to static_compiled/fonts/
make build-images Copy static images to static_compiled/images/
make watch Watch and rebuild CSS on file changes
make migrate Run database migrations
make createsuperuser Create admin user
make setup Interactive initial site setup
make test Run test suite
make load-data Migrate + load demo fixtures + collectstatic
make provision Provision AWS S3 bucket + IAM user (see make help)
wagtail-wtr/
├── wtrx/ # Core reusable app (don't edit on client sites)
│ ├── blocks/ # StreamField blocks (content, layout, composite, cards, actions)
│ ├── models.py # BasePage, HeroMixin, HomePage, ContentPage, IndexPage, FormField, FormPage
│ ├── views.py # search() view
│ ├── site_settings.py
│ ├── images.py # CustomImage
│ ├── templatetags/
│ └── wagtail_hooks.py
├── wagtail_wtr/ # Django project package (settings, urls, wsgi only)
│ └── settings/
│ ├── base.py
│ ├── dev.py
│ └── production.py
├── templates/
├── static_src/ # Tailwind CSS source + vanilla JS + font files
├── static_compiled/ # Tailwind CLI output (gitignored; run make build)
├── Makefile
├── pyproject.toml
└── package.json
wtrx/ is the stable core. All page models and the search view live there. To add
site-specific page types, create a new app and subclass BasePage; don't edit wtrx/ directly.
Edit the @theme {} block in static_src/css/theme.css to change the semantic design tokens:
@theme {
/* Replace these color scales with your brand palette */
--color-primary-50: #f0f9ff;
--color-primary-500: #0ea5e9;
--color-primary-600: #0284c7;
/* ... full scale 50–950 ... */
/* Font stacks */
--font-heading: 'Your Heading Font', system-ui, sans-serif;
--font-body: 'Your Body Font', system-ui, sans-serif;
}All templates use only semantic tokens (bg-primary-600, font-heading, etc.), so
changing the @theme {} values immediately re-themes the entire site. Rebuild after changes:
make build-prodtheme.css also ships named theme presets ([data-theme="grassroots"], etc.) as
CSS overrides — no rebuild needed when switching between presets at runtime. Client
forks that don't use these can delete those blocks.
In wagtail_wtr/settings/base.py, uncomment or add languages:
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
("en", "English"),
("es", "Spanish"),
]Translations are managed via the Wagtail admin using wagtail-localize.
Set defaults in settings/base.py:
WTRX_DONATION_PLATFORM = "none" # none | actblue
WTRX_SIGNUP_PLATFORM = "wagtail_forms" # wagtail_forms | action_network | noneOverride per-site in the Wagtail admin under Settings > Integrations.
# Watch mode: rebuilds CSS on file changes
make watch
# Run tests
python manage.py test wtrx wagtail_wtr
# Run a specific test module
python manage.py test wtrx.tests.test_images
# Generate migrations after model changes
python manage.py makemigrations
python manage.py migrateCreate wagtail_wtr/settings/local.py for personal overrides (gitignored):
from .dev import *
DATABASES = { ... } # override as needed
DEBUG = TrueA render.yaml Blueprint is included. To deploy:
- Push your fork to GitHub.
- In the Render dashboard, click New → Blueprint and connect your repo.
- Render auto-provisions a PostgreSQL database and generates a
SECRET_KEY. - Set the required env vars in the Render dashboard (or in
render.yamlbefore importing):ALLOWED_HOSTS— your Render hostname, e.g.mysite.onrender.comWAGTAILADMIN_BASE_URL— full public URL, e.g.https://mysite.onrender.com
- Deploy. The Docker build compiles CSS/JS and installs Python deps.
Render's
preDeployCommandrunscollectstatic(with S3 credentials available). On startup,bin/start.shrunsmigratethen starts gunicorn — the health check responds immediately because collectstatic has already completed.
Copy .env.example to .env (gitignored) for local production-settings overrides.
Ships with:
- Two-stage
Dockerfile: Node 20 (Tailwind CLI build) → Python 3.13-slim (app) preDeployCommand(render.yaml): intended to runcollectstatic --noinputwith S3 credentials before the container starts — see note belowbin/start.shentrypoint: runsmigrate --noinputthen starts gunicorn immediately/_health/endpoint for zero-downtime deploy health checksrender.yamlpreDeployCommandrunscollectstaticbefore the container takes trafficwhitenoisefor static file serving when S3 is not configuredgunicornas WSGI server with$PORTbinding for Render compatibilitydj-database-urlforDATABASE_URLenv vardjango-storages[s3]+wagtail-storagesfor S3 media and static (optional — see below)
Known issue — manual collectstatic after CSS-changing deploys (S3 path)
When
AWS_STORAGE_BUCKET_NAMEis set,bin/start.shskipscollectstaticbecauserender.yaml'spreDeployCommandis supposed to run it before the container starts. However,preDeployCommandhas not been confirmed to run reliably — its output does not appear in Render's runtime deploy logs.Until this is investigated and fixed, after any deploy that changes CSS, JS, or other static assets you must manually run collectstatic via the Render dashboard:
- In the Render dashboard, open your service.
- Click the Shell tab.
- Run:
python manage.py collectstatic --noinputThis is tracked in PLAN.md Phase 17.
SECRET_KEY=... # auto-generated on Render
DATABASE_URL=postgres://... # auto-wired on Render
ALLOWED_HOSTS=mysite.com,www.mysite.com
WAGTAILADMIN_BASE_URL=https://mysite.com
DJANGO_SETTINGS_MODULE=wagtail_wtr.settings.production
When AWS_STORAGE_BUCKET_NAME is set, both user-uploaded media (images, documents)
and collected static files (CSS, JS, fonts) are stored in S3 under separate prefixes:
{bucket}/media/— user uploads{bucket}/static/— compiled static assets
Omit the variable to use WhiteNoise for static files and local filesystem for media.
Run make provision to create the S3 bucket and a scoped IAM user in one step.
The script uses your local AWS CLI profile — no credentials are typed into the script.
1. Configure an AWS CLI profile (if you haven't already):
aws configure --profile wagtail-wtr-provisioner
# Prompts for: Access Key ID, Secret Access Key, region, output format2. Run the provisioning script:
make provision SITE=mysite ENV=production PROFILE=wagtail-wtr-provisioner
make provision SITE=mysite ENV=staging PROFILE=wagtail-wtr-provisionerOmit PROFILE to use the default AWS CLI profile. ENV defaults to production.
The script will display the AWS account ID and authenticated identity before making any changes, so you can confirm you're targeting the right account.
This creates:
- S3 bucket:
mysite-wagtail-wtr-production(or-staging) - IAM user:
mysite-wagtail-wtr-productionwith an inline policy scoped to that bucket only
It then prints the four env vars to paste into the Render dashboard.
Required IAM permissions for the profile you use:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3Provisioning",
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:HeadBucket",
"s3:PutBucketPolicy",
"s3:PutBucketCORS",
"s3:PutPublicAccessBlock"
],
"Resource": "arn:aws:s3:::*"
},
{
"Sid": "IAMProvisioning",
"Effect": "Allow",
"Action": [
"iam:CreateUser",
"iam:GetUser",
"iam:PutUserPolicy",
"iam:CreateAccessKey"
],
"Resource": "arn:aws:iam::*:user/*"
},
{
"Sid": "STSGetCallerIdentity",
"Effect": "Allow",
"Action": "sts:GetCallerIdentity",
"Resource": "*"
}
]
}Create this as a policy named wagtail-wtr-provisioner in IAM → Policies → Create policy
and attach it to your admin user. If your admin user already has AdministratorAccess,
no extra policy is needed.
If you prefer to create resources manually, set these env vars in the Render dashboard:
AWS_STORAGE_BUCKET_NAME=mysite-wagtail-wtr-production
AWS_S3_REGION_NAME=us-east-1
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_S3_CUSTOM_DOMAIN=assets.mysite.com # optional: CloudFront or custom domain
To provision a new S3 bucket and scoped IAM user automatically:
make provision SITE=mysite ENV=production
# optional: make provision SITE=mysite ENV=staging PROFILE=my-aws-profileThis creates the bucket, sets the public-read policy, and outputs the AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY for a dedicated IAM user scoped to that bucket.
When EMAIL_HOST is set, Django sends mail via SMTP (compatible with Mailgun,
AWS SES, Postmark, or any standard SMTP provider). Without it, emails are printed
to container logs (console backend).
EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587
EMAIL_HOST_USER=postmaster@mg.mysite.com
EMAIL_HOST_PASSWORD=...
DEFAULT_FROM_EMAIL=hello@mysite.com
When Cloudflare is in front of your site, set these two environment variables and the platform will automatically purge cached pages on publish and flush the entire cache whenever site settings (branding, navigation, footer, social, integrations) are saved.
CLOUDFLARE_BEARER_TOKEN=your-api-token
CLOUDFLARE_ZONE_ID=your-zone-id
To get these values:
-
Zone ID — in the Cloudflare dashboard, select your domain. The Zone ID appears in the right-hand panel under API on the Overview page.
-
Bearer token — go to My Profile → API Tokens → Create Token. Use the Edit zone resources template (or create a custom token) with these permissions:
- Zone → Cache Purge → Purge
- Zone Resources → Include → your specific zone (or all zones)
MIT. See LICENSE.