I build websites for small local businesses in northern Germany. Most of them are a handful of pages: services, prices, a contact form, a few location pages. And almost every site I take over from someone else has the same bug: it contradicts itself.
The price on the services page says 500 €, the FAQ still says 450 €, the meta description from two years ago says "from 399 €". The opening hours on the contact page differ from the footer. Nobody notices until a customer quotes the cheapest number back on the phone.
Here is the setup I use now. Nothing clever, but it has caught real mistakes.
1. Every fact lives in one Python module
Prices, the company address, the phone number and every promise about time ("reply within 24 hours on workdays", "online in 2 to 4 weeks") live in plain modules under apps/core/data/:
# apps/core/data/pricing.py (names simplified)
WEBSITE_WITH_CARE = 500 # one-off, EUR
WEBSITE_HANDOVER = 1200 # one-off, EUR
CARE_PER_MONTH = 250
CARE_MIN_MONTHS = 6
Templates never contain a number. They get it from the context:
<p>Website with ongoing care: {{ pricing.WEBSITE_WITH_CARE }} €</p>
The same goes for JSON-LD. The structured data is built in Python from the same module, so the schema can't say something different from the visible page.
A new page is an entry in a data list, not hand-work on URLs, sitemap, navigation and schema.
2. A command that renders every page and checks the numbers
The module alone doesn't help if someone (usually me, in a hurry) types "450 €" into a template anyway. So there is a management command that renders every public page with Django's test client and checks the output:
# simplified
EURO = re.compile(r"(\d[\d.]*)\s?€")
def check_page(url, html):
allowed = set(all_prices()) # read from pricing.py
for match in EURO.finditer(visible_text(html)):
value = int(match.group(1).replace(".", ""))
if value not in allowed:
yield f"{url}: {value} € is not in pricing.py"
It looks at titles and meta descriptions too, because that's where old numbers survive longest. The same idea works for time promises: every "within X hours" on the page has to exist in the module that holds them.
It runs before every deploy, together with the normal test suite.
3. What this does not catch
This is the part I find most useful to write down. A green check means "every euro amount on the page exists in the price module". It does not mean:
- the text around the number is correct ("from 500 €" vs "500 €"),
- the number is in the right place (the monthly price shown as a one-off),
- a price in an image or PDF is current.
So I note next to every check what it cannot find. A check that has never failed on a real mistake is unproven; I test new ones by putting a wrong number in on purpose.
Why bother on a five-page site?
Because for a small business the website is often the only written price list. A contradiction there costs trust on the first call, and it is boring to find by hand. One module plus one command takes an afternoon and removes the whole class of bug.
If you run a lot of small client sites: what do you use to keep facts consistent across pages? I'm curious whether people solve this in the CMS or in code.
I'm Bastian, I build websites for small businesses in Lübeck, Germany: webagentur-scherzinger.com
Top comments (0)