0
# Template System
1
2
Template tags for page URLs, content rendering, and template utilities for building Wagtail-powered front-ends. Wagtail provides comprehensive template integration with Django's template system.
3
4
## Capabilities
5
6
### Core Template Tags
7
8
Essential template tags for page navigation, URL generation, and content rendering.
9
10
```python { .api }
11
def pageurl(page, request=None):
12
"""
13
Get the URL for a page, with optional request context for relative URLs.
14
15
Usage in templates: {% pageurl page %}
16
17
Parameters:
18
page (Page): Page object to get URL for
19
request (HttpRequest): Optional request for relative URL generation
20
21
Returns:
22
str: URL path for the page (relative by default)
23
"""
24
25
def fullpageurl(page, request=None):
26
"""
27
Get the absolute URL for a page including domain.
28
29
Usage in templates: {% fullpageurl page %}
30
31
Parameters:
32
page (Page): Page object to get URL for
33
request (HttpRequest): Optional request for domain detection
34
35
Returns:
36
str: Absolute URL including protocol and domain
37
"""
38
39
def slugurl(slug, request=None):
40
"""
41
Get page URL by slug lookup within the current site.
42
43
Usage in templates: {% slugurl 'my-page-slug' %}
44
45
Parameters:
46
slug (str): Page slug to find and link to
47
request (HttpRequest): Optional request for site context
48
49
Returns:
50
str: URL for the page with matching slug, or empty string if not found
51
"""
52
53
def wagtail_site(request=None):
54
"""
55
Get the current Site object for use in templates.
56
57
Usage in templates: {% wagtail_site as current_site %}
58
59
Parameters:
60
request (HttpRequest): Current request for site detection
61
62
Returns:
63
Site: Current Site object with hostname, root_page, etc.
64
"""
65
66
def include_block(block_value, context=None):
67
"""
68
Render a StreamField block with its template.
69
70
Usage in templates: {% include_block page.body %}
71
72
Parameters:
73
block_value (StreamValue): StreamField content to render
74
context (dict): Additional template context
75
76
Returns:
77
str: Rendered HTML for all blocks in the StreamField
78
"""
79
80
def wagtail_version():
81
"""
82
Get the current Wagtail version string.
83
84
Usage in templates: {% wagtail_version %}
85
86
Returns:
87
str: Wagtail version (e.g., "4.1.0")
88
"""
89
```
90
91
### Rich Text Filters
92
93
Template filters for processing and rendering rich text content safely.
94
95
```python { .api }
96
def richtext(value):
97
"""
98
Process rich text content for safe HTML rendering.
99
100
Usage in templates: {{ page.body|richtext }}
101
102
Processes rich text to:
103
- Convert internal page links to proper URLs
104
- Process document download links
105
- Handle embedded content
106
- Apply security filtering
107
108
Parameters:
109
value (str): Raw rich text content from RichTextField
110
111
Returns:
112
SafeString: Processed HTML safe for rendering
113
"""
114
115
def linebreaks_richtext(value):
116
"""
117
Convert line breaks in rich text to proper HTML paragraphs.
118
119
Usage in templates: {{ content|linebreaks_richtext }}
120
121
Parameters:
122
value (str): Rich text content with line breaks
123
124
Returns:
125
SafeString: Content with proper paragraph tags
126
"""
127
```
128
129
### Image Template Tags
130
131
Template tags for rendering images with automatic rendition generation.
132
133
```python { .api }
134
def image(image, filter_spec, attrs=None):
135
"""
136
Generate an image rendition and return HTML img tag or rendition object.
137
138
Usage in templates:
139
{% image page.photo fill-400x300 %}
140
{% image page.photo width-800 as hero %}
141
142
Parameters:
143
image (Image): Image object to render
144
filter_spec (str): Image operations to apply (e.g., 'fill-400x300')
145
attrs (dict): Additional HTML attributes for img tag
146
147
Returns:
148
str or Rendition: HTML img tag or rendition object (if using 'as' syntax)
149
"""
150
151
def responsive_image(image, sizes=None, attrs=None):
152
"""
153
Generate responsive image with multiple renditions and srcset.
154
155
Usage in templates: {% responsive_image page.hero_image %}
156
157
Parameters:
158
image (Image): Image object to make responsive
159
sizes (list): List of size specifications for different breakpoints
160
attrs (dict): Additional HTML attributes
161
162
Returns:
163
str: HTML picture element with responsive images
164
"""
165
```
166
167
### Page Navigation Tags
168
169
Template tags for building navigation menus and page hierarchies.
170
171
```python { .api }
172
def get_site_root(request=None):
173
"""
174
Get the root page for the current site.
175
176
Usage in templates: {% get_site_root as site_root %}
177
178
Parameters:
179
request (HttpRequest): Current request for site detection
180
181
Returns:
182
Page: Root page of the current site
183
"""
184
185
def top_menu(parent, calling_page=None):
186
"""
187
Get top-level menu items from a parent page.
188
189
Usage in templates: {% top_menu parent calling_page %}
190
191
Parameters:
192
parent (Page): Parent page to get children from
193
calling_page (Page): Current page for active state detection
194
195
Returns:
196
QuerySet: Child pages suitable for top-level navigation
197
"""
198
199
def breadcrumbs(calling_page):
200
"""
201
Generate breadcrumb navigation for a page.
202
203
Usage in templates: {% breadcrumbs self %}
204
205
Parameters:
206
calling_page (Page): Current page to generate breadcrumbs for
207
208
Returns:
209
list: List of ancestor pages from root to current page
210
"""
211
```
212
213
### Search Template Tags
214
215
Template tags for search functionality and query handling.
216
217
```python { .api }
218
def search(query_string, model_or_queryset=None, fields=None):
219
"""
220
Perform search from templates.
221
222
Usage in templates: {% search "query" as search_results %}
223
224
Parameters:
225
query_string (str): Search query
226
model_or_queryset: Model or QuerySet to search
227
fields (list): Fields to search in
228
229
Returns:
230
SearchResults: Search results with pagination support
231
"""
232
233
def paginate(page_list, per_page, page_number):
234
"""
235
Paginate a list of items for template display.
236
237
Usage in templates: {% paginate items 10 request.GET.page %}
238
239
Parameters:
240
page_list (list): Items to paginate
241
per_page (int): Number of items per page
242
page_number (int): Current page number
243
244
Returns:
245
Page: Paginated page object with items and navigation
246
"""
247
```
248
249
### Utility Template Tags
250
251
General utility template tags for common Wagtail operations.
252
253
```python { .api }
254
def get_page_by_slug(slug, parent=None):
255
"""
256
Get a page by its slug within an optional parent.
257
258
Usage in templates: {% get_page_by_slug 'about' as about_page %}
259
260
Parameters:
261
slug (str): Page slug to find
262
parent (Page): Optional parent page to search within
263
264
Returns:
265
Page: Found page or None if not found
266
"""
267
268
def get_pages_by_type(page_type, parent=None):
269
"""
270
Get pages of a specific type.
271
272
Usage in templates: {% get_pages_by_type 'blog.BlogPage' as blog_pages %}
273
274
Parameters:
275
page_type (str): Page type in 'app.ModelName' format
276
parent (Page): Optional parent to search within
277
278
Returns:
279
QuerySet: Pages of the specified type
280
"""
281
282
def get_site_setting(setting_name, site=None):
283
"""
284
Get a site-specific setting value.
285
286
Usage in templates: {% get_site_setting 'contact_email' as contact %}
287
288
Parameters:
289
setting_name (str): Name of the setting to retrieve
290
site (Site): Optional site (defaults to current site)
291
292
Returns:
293
Any: Setting value or None if not found
294
"""
295
```
296
297
## Usage Examples
298
299
### Basic Page Templates
300
301
```html
302
<!-- Base template: base.html -->
303
<!DOCTYPE html>
304
<html lang="en">
305
<head>
306
<meta charset="UTF-8">
307
<meta name="viewport" content="width=device-width, initial-scale=1.0">
308
<title>{% block title %}{{ page.seo_title|default:page.title }} - {{ site.site_name }}{% endblock %}</title>
309
<meta name="description" content="{{ page.search_description }}">
310
{% load wagtailcore_tags %}
311
</head>
312
<body class="{% block body_class %}{% endblock %}">
313
<header>
314
{% get_site_root as site_root %}
315
<nav>
316
<a href="{% pageurl site_root %}">{{ site.site_name }}</a>
317
{% top_menu site_root self as menu_items %}
318
<ul>
319
{% for item in menu_items %}
320
<li><a href="{% pageurl item %}" {% if item == self %}class="active"{% endif %}>{{ item.title }}</a></li>
321
{% endfor %}
322
</ul>
323
</nav>
324
</header>
325
326
<main>
327
{% block breadcrumbs %}
328
{% breadcrumbs self as breadcrumb_items %}
329
<ol class="breadcrumbs">
330
{% for item in breadcrumb_items %}
331
<li><a href="{% pageurl item %}">{{ item.title }}</a></li>
332
{% endfor %}
333
</ol>
334
{% endblock %}
335
336
{% block content %}{% endblock %}
337
</main>
338
339
<footer>
340
<p>© 2023 {{ site.site_name }}. Powered by Wagtail {% wagtail_version %}.</p>
341
</footer>
342
</body>
343
</html>
344
345
<!-- Page template: blog/blog_page.html -->
346
{% extends "base.html" %}
347
{% load wagtailcore_tags wagtailimages_tags %}
348
349
{% block title %}{{ page.title }} - Blog{% endblock %}
350
351
{% block content %}
352
<article>
353
<header>
354
<h1>{{ page.title }}</h1>
355
<p class="meta">
356
Published {{ page.date|date:"F j, Y" }}
357
{% if page.author %}by {{ page.author.name }}{% endif %}
358
</p>
359
{% if page.featured_image %}
360
{% image page.featured_image fill-800x400 as hero %}
361
<img src="{{ hero.url }}" alt="{{ hero.alt }}" class="hero-image">
362
{% endif %}
363
</header>
364
365
<div class="content">
366
<p class="intro">{{ page.intro }}</p>
367
{{ page.body|richtext }}
368
</div>
369
370
<footer>
371
<p>
372
<a href="{% slugurl 'blog' %}">← Back to Blog</a>
373
</p>
374
</footer>
375
</article>
376
{% endblock %}
377
```
378
379
### StreamField Templates
380
381
```html
382
<!-- StreamField block templates -->
383
384
<!-- blocks/heading_block.html -->
385
<h{{ self.level|default:2 }} class="heading-block">{{ self.text }}</h{{ self.level|default:2 }}>
386
387
<!-- blocks/paragraph_block.html -->
388
<div class="paragraph-block">
389
{{ self|richtext }}
390
</div>
391
392
<!-- blocks/image_block.html -->
393
{% load wagtailimages_tags %}
394
<figure class="image-block">
395
{% image self.image fill-800x600 as img %}
396
<img src="{{ img.url }}" alt="{{ img.alt }}" width="{{ img.width }}" height="{{ img.height }}">
397
{% if self.caption %}
398
<figcaption>{{ self.caption|richtext }}</figcaption>
399
{% endif %}
400
</figure>
401
402
<!-- blocks/quote_block.html -->
403
<blockquote class="quote-block">
404
{{ self.quote|richtext }}
405
{% if self.attribution %}
406
<cite>{{ self.attribution }}</cite>
407
{% endif %}
408
</blockquote>
409
410
<!-- Main page template using StreamField -->
411
{% extends "base.html" %}
412
{% load wagtailcore_tags %}
413
414
{% block content %}
415
<div class="page-content">
416
<h1>{{ page.title }}</h1>
417
{% if page.intro %}
418
<div class="intro">{{ page.intro|richtext }}</div>
419
{% endif %}
420
421
<div class="stream-content">
422
{% include_block page.body %}
423
</div>
424
</div>
425
{% endblock %}
426
```
427
428
### Navigation and Menus
429
430
```html
431
<!-- Custom navigation template -->
432
{% load wagtailcore_tags %}
433
434
<!-- Main navigation -->
435
{% get_site_root as site_root %}
436
<nav class="main-nav">
437
<ul class="nav-list">
438
{% top_menu site_root self as main_items %}
439
{% for item in main_items %}
440
<li class="nav-item {% if item == self or item in self.get_ancestors %}active{% endif %}">
441
<a href="{% pageurl item %}" class="nav-link">{{ item.title }}</a>
442
443
<!-- Sub-navigation -->
444
{% if item.get_children.live %}
445
<ul class="sub-nav">
446
{% for child in item.get_children.live %}
447
<li class="sub-nav-item {% if child == self %}active{% endif %}">
448
<a href="{% pageurl child %}" class="sub-nav-link">{{ child.title }}</a>
449
</li>
450
{% endfor %}
451
</ul>
452
{% endif %}
453
</li>
454
{% endfor %}
455
</ul>
456
</nav>
457
458
<!-- Sidebar navigation -->
459
<aside class="sidebar">
460
<h3>In This Section</h3>
461
{% get_pages_by_type 'blog.BlogPage' as blog_pages %}
462
<ul class="sidebar-nav">
463
{% for page in blog_pages.live|slice:":5" %}
464
<li><a href="{% pageurl page %}">{{ page.title }}</a></li>
465
{% endfor %}
466
</ul>
467
</aside>
468
469
<!-- Footer navigation -->
470
<footer class="site-footer">
471
<div class="footer-nav">
472
{% get_page_by_slug 'contact' as contact_page %}
473
{% get_page_by_slug 'privacy' as privacy_page %}
474
{% get_page_by_slug 'terms' as terms_page %}
475
476
<ul class="footer-links">
477
{% if contact_page %}<li><a href="{% pageurl contact_page %}">{{ contact_page.title }}</a></li>{% endif %}
478
{% if privacy_page %}<li><a href="{% pageurl privacy_page %}">{{ privacy_page.title }}</a></li>{% endif %}
479
{% if terms_page %}<li><a href="{% pageurl terms_page %}">{{ terms_page.title }}</a></li>{% endif %}
480
</ul>
481
</div>
482
</footer>
483
```
484
485
### Image Handling
486
487
```html
488
{% load wagtailimages_tags %}
489
490
<!-- Basic image rendering -->
491
{% if page.hero_image %}
492
{% image page.hero_image fill-1200x600 as hero %}
493
<div class="hero" style="background-image: url({{ hero.url }});">
494
<h1>{{ page.title }}</h1>
495
</div>
496
{% endif %}
497
498
<!-- Responsive images -->
499
{% if page.featured_image %}
500
{% image page.featured_image fill-400x300 as mobile %}
501
{% image page.featured_image fill-800x600 as tablet %}
502
{% image page.featured_image fill-1200x800 as desktop %}
503
504
<picture class="responsive-image">
505
<source media="(min-width: 1024px)" srcset="{{ desktop.url }}">
506
<source media="(min-width: 768px)" srcset="{{ tablet.url }}">
507
<img src="{{ mobile.url }}" alt="{{ page.featured_image.title }}"
508
width="{{ mobile.width }}" height="{{ mobile.height }}">
509
</picture>
510
{% endif %}
511
512
<!-- Image gallery -->
513
<div class="image-gallery">
514
{% for gallery_image in page.gallery_images.all %}
515
{% image gallery_image.image fill-300x200 as thumb %}
516
{% image gallery_image.image width-1000 as full %}
517
518
<div class="gallery-item">
519
<a href="{{ full.url }}" data-lightbox="gallery">
520
<img src="{{ thumb.url }}" alt="{{ gallery_image.image.title }}">
521
</a>
522
{% if gallery_image.caption %}
523
<p class="caption">{{ gallery_image.caption }}</p>
524
{% endif %}
525
</div>
526
{% endfor %}
527
</div>
528
529
<!-- Optimized images with WebP -->
530
{% image page.hero_image fill-1200x600|format-webp as webp_hero %}
531
{% image page.hero_image fill-1200x600|format-jpeg as jpeg_hero %}
532
533
<picture>
534
<source srcset="{{ webp_hero.url }}" type="image/webp">
535
<img src="{{ jpeg_hero.url }}" alt="{{ page.hero_image.title }}" class="hero-image">
536
</picture>
537
```
538
539
### Search Templates
540
541
```html
542
<!-- Search form -->
543
<form action="{% url 'search' %}" method="get" class="search-form">
544
<input type="text" name="q" value="{{ query_string }}" placeholder="Search..." required>
545
<button type="submit">Search</button>
546
</form>
547
548
<!-- Search results template -->
549
{% load wagtailcore_tags %}
550
551
{% block content %}
552
<div class="search-results">
553
<h1>Search Results</h1>
554
555
{% if query_string %}
556
<p>You searched for: <strong>{{ query_string }}</strong></p>
557
558
{% if search_results %}
559
<p>{{ search_results.paginator.count }} result{{ search_results.paginator.count|pluralize }} found</p>
560
561
<div class="results-list">
562
{% for result in search_results %}
563
<article class="search-result">
564
<h3><a href="{% pageurl result %}">{{ result.title }}</a></h3>
565
<p class="meta">{{ result.content_type.model_class.get_verbose_name }}</p>
566
{% if result.search_description %}
567
<p>{{ result.search_description }}</p>
568
{% endif %}
569
</article>
570
{% endfor %}
571
</div>
572
573
<!-- Pagination -->
574
{% if search_results.has_other_pages %}
575
<nav class="pagination">
576
{% if search_results.has_previous %}
577
<a href="?q={{ query_string }}&page={{ search_results.previous_page_number }}">← Previous</a>
578
{% endif %}
579
580
<span class="current">
581
Page {{ search_results.number }} of {{ search_results.paginator.num_pages }}
582
</span>
583
584
{% if search_results.has_next %}
585
<a href="?q={{ query_string }}&page={{ search_results.next_page_number }}">Next →</a>
586
{% endif %}
587
</nav>
588
{% endif %}
589
590
{% else %}
591
<p>No results found.</p>
592
{% endif %}
593
594
{% endif %}
595
</div>
596
{% endblock %}
597
```
598
599
### Custom Template Tags
600
601
```python
602
# templatetags/blog_tags.py
603
from django import template
604
from django.utils import timezone
605
from blog.models import BlogPage
606
607
register = template.Library()
608
609
@register.inclusion_tag('blog/recent_posts.html')
610
def recent_blog_posts(count=5):
611
"""Get recent blog posts for sidebar."""
612
posts = BlogPage.objects.live().order_by('-date')[:count]
613
return {'posts': posts}
614
615
@register.simple_tag
616
def get_popular_posts(count=5):
617
"""Get most popular blog posts."""
618
return BlogPage.objects.live().order_by('-page_views')[:count]
619
620
@register.filter
621
def reading_time(content):
622
"""Calculate reading time for content."""
623
word_count = len(str(content).split())
624
minutes = max(1, word_count // 200) # 200 words per minute
625
return f"{minutes} min read"
626
627
@register.inclusion_tag('blog/tag_cloud.html')
628
def tag_cloud():
629
"""Generate tag cloud for blog."""
630
from django.db.models import Count
631
tags = BlogTag.objects.annotate(
632
post_count=Count('blogpage')
633
).filter(post_count__gt=0).order_by('-post_count')[:20]
634
return {'tags': tags}
635
636
# Usage in templates
637
{% load blog_tags %}
638
639
<!-- Recent posts sidebar -->
640
{% recent_blog_posts 3 %}
641
642
<!-- Popular posts -->
643
{% get_popular_posts 5 as popular %}
644
<ul>
645
{% for post in popular %}
646
<li><a href="{% pageurl post %}">{{ post.title }}</a></li>
647
{% endfor %}
648
</ul>
649
650
<!-- Reading time -->
651
<p class="meta">{{ page.body|reading_time }}</p>
652
653
<!-- Tag cloud -->
654
{% tag_cloud %}
655
```
656
657
### Form Templates
658
659
```html
660
<!-- Contact form template -->
661
{% extends "base.html" %}
662
{% load wagtailcore_tags %}
663
664
{% block content %}
665
<div class="contact-page">
666
<h1>{{ page.title }}</h1>
667
668
{% if page.intro %}
669
<div class="intro">
670
{{ page.intro|richtext }}
671
</div>
672
{% endif %}
673
674
<form action="{% pageurl page %}" method="post" class="contact-form">
675
{% csrf_token %}
676
677
{% for field in form %}
678
<div class="form-field {% if field.errors %}error{% endif %}">
679
{{ field.label_tag }}
680
{{ field }}
681
{% if field.help_text %}
682
<p class="help-text">{{ field.help_text }}</p>
683
{% endif %}
684
{% for error in field.errors %}
685
<p class="error-message">{{ error }}</p>
686
{% endfor %}
687
</div>
688
{% endfor %}
689
690
<button type="submit" class="submit-button">Send Message</button>
691
</form>
692
693
{% if page.thank_you_text %}
694
<div class="thank-you" style="display: none;">
695
{{ page.thank_you_text|richtext }}
696
</div>
697
{% endif %}
698
</div>
699
{% endblock %}
700
```