Introduction
wagtail-seo comes with various SEO settings for your site, including fields for ‘Hours of operation’. I needed to take this data and output it on a template in a nice format. This approach is minimal and effective, so if you are also looking to build a global opening hours widget, then you’re in the right place.
Versions
For this example, I’m running:
- Wagtail:
6.4.1
- wagtail-seo:
3.0.0
- Django:
5.2
This will likely work with neighbouring versions, but please bear this in mind if you’re far ahead or behind compared to my current environment versions.
Creating Template Tags for wagtail-seo
Create the Template Tag File
Within your project, create a new Python file within a templatetags
directory within the app of your choice. For me, I already have an seo
app which is adding some extra functionality beyond wagtail-seo
, so it makes sense for me to put this there:
project_root
├ seo
│ ├── templatetags
│ │ ├── __init__.py
│ │ └── seo_tags.py
Within the new seo_tags.py
file, I can now declare tags and have them be available within my templates.
Add the Required Template Tags
from datetime import date
from django import template
from wagtailseo.models import SeoSettings
register = template.Library()
def default_day(day: str, today: str) -> dict[str, str]:
"""Helper function to create a default dictionary for each day of the week."""
return {
"start_time": None,
"end_time": None,
"is_today": day == today,
"open": False,
}
@register.simple_tag()
def opening_times() -> dict[str, dict[str, str]]:
"""Returns a dictionary of the opening times for each day of the week.
Returns:
dict[str, dict[str, str]]: A dictionary containing the opening times for each day of the week.
Each key is the name of the day, and the value is a dictionary with the following keys:
- start_time: The opening time for that day.
- end_time: The closing time for that day.
- is_today: A boolean indicating if the day is today.
- open: A boolean indicating if the business is open on that day.
"""
# Create initial dictionary with default values
today = date.today().strftime("%A")
times = {
"Monday": default_day("Monday", today),
"Tuesday": default_day("Tuesday", today),
"Wednesday": default_day("Wednesday", today),
"Thursday": default_day("Thursday", today),
"Friday": default_day("Friday", today),
"Saturday": default_day("Saturday", today),
"Sunday": default_day("Sunday", today),
}
# Get the opening times from the SeoSettings model
seo = SeoSettings.objects.first()
if not seo:
return {}
# Loop through the opening times and update the dictionary
for i in seo.struct_org_hours:
days = i.value.get("days")
start_time = i.value.get("start_time")
end_time = i.value.get("end_time")
if not days or not start_time or not end_time:
continue
for day in days:
times[day]["start_time"] = start_time
times[day]["end_time"] = end_time
times[day]["open"] = True
return times
@register.simple_tag()
def opening_times_today() -> dict[str, str]:
"""Returns the opening times for today. Calls the opening_times tag function to get the data.
Returns:
dict[str, str]: A dictionary containing the opening times for today.
"""
times = opening_times()
today = date.today().strftime("%A")
if today in times:
return times[today]
else:
return default_day(today, today)
There’s quite a lot going on here, but to break it down simply, this adds two new tags:
opening_times
opening_times_today
They both facilitate slightly different use cases. opening_times
returns a dictionary containing every day of the week, where the values are the start_time
, end_time
, is_today
, and open
. To understand these values a little better, please refer to the docstring starting on line 22. opening_times_today
utilises the first method to only return the opening times of the current day. This is useful if you’re just wanting to output a single line of today’s opening times, as opposed to a full table of results.
Using the Template Tags to Build an Opening Times Table
Now that my seo_tags.py
file exists and has the tags declared, I’m able to bring these into my templates to utilise.
Today’s Opening Time
{% load wagtailcore_tags seo_tags %}
{% opening_times_today as today %}
<p>
{% if today.open %}
<strong>Open today</strong> {{ today.start_time|date:"P" }} - {{ today.end_time|date:"P" }}
{% else %}
<strong>Closed today</strong>
{% endif %}
</p>
In the above example, you can see I’m loading the seo_tags
file. If you called yours something different, be sure to adjust accordingly.
I’m then assigning the results of the opening_times_today
tag as a variable called today
. Using this new today
variable, I can now access the start_time
, end_time
, and open
values from the dictionary.
The above code would output the following (I’ve added a styled border to the image so it’s clear):

The Full Week’s Opening Times
{% load wagtailcore_tags seo_tags %}
{% opening_times as days_of_the_week %}
<div>
<table>
<tr>
<th>Day</th>
<th>Open</th>
<th>Close</th>
</tr>
{% for day, times in days_of_the_week.items %}
<tr {% if times.is_today %}class="today"{% endif %}>
<td>{{ day }}</td>
<td>{% if times.open %}{{ times.start_time|date:"P" }}{% else %}Closed{% endif %}</td>
<td>{% if times.open %}{{ times.end_time|date:"P" }}{% else %}Closed{% endif %}</td>
</tr>
{% endfor %}
</table>
</div>
In the above example, the opening_times
tag is being assigned to a variable called days_of_the_week
which is then used to generate a table.
The above code outputs the following (with the same styled border for the same reasons as earlier):

Entering Opening Times
Just so this is covered, you can enter opening times by logging into the Wagtail admin, going to ‘Settings’ > ‘SEO’, and then scrolling down to the ‘Hours of operation’ section. You can see an example of how you might set some opening times by expanding to view this screenshot:
Screenshot of the ‘Hours of operation’ settings from wagtail-seo.

Limitations
The style in which the opening times are declared allows for opening times to be set more than once for a day of the week. This approach I’ve shared for displaying the opening times assumes this isn’t the case. If you’ve declared different opening times for a day more than once, it’ll use whichever is the last declaration for that day.
There’s minimal logic in place here, but if you’re looking to add this to a page more than once, such as in the header and footer, you may wish to implement some caching if the tags will be used multiple times. These tags aren’t heavy to run, but it’s worth optimising where possible.
Leave a Reply