Clean agency navigation and refresh core site content

This commit is contained in:
2026-03-30 18:27:51 +02:00
parent ebde2806c1
commit 0baae1dbe6
5 changed files with 772 additions and 37 deletions

View File

@@ -0,0 +1,669 @@
from __future__ import annotations
import copy
import uuid
from typing import Any
from django.core.management.base import BaseCommand
from django.db import transaction
from django.utils.text import slugify
from wagtail.blocks import StreamValue
from wagtail.models import Locale, Page
from mandelstudio.models import LocalizedFooterContent
SOURCE_PAGE_IDS = {
"home": 127,
"about": 128,
"services": 129,
"projects": 130,
"contact": 131,
"process": 192,
"starter": 200,
"business": 201,
"webshop": 202,
"support": 203,
"ai_search": 199,
}
PAGE_TITLE_MAP = {
"about": {
"nl": "Over ons",
"en": "About us",
"de": "Über uns",
"fr": "À propos",
"es": "Sobre nosotros",
"it": "Chi siamo",
"pt": "Sobre nós",
"ru": "О нас",
},
"services": {
"nl": "Diensten",
"en": "Services",
"de": "Dienstleistungen",
"fr": "Services",
"es": "Servicios",
"it": "Servizi",
"pt": "Serviços",
"ru": "Услуги",
},
"projects": {
"nl": "Projecten",
"en": "Projects",
"de": "Projekte",
"fr": "Projets",
"es": "Proyectos",
"it": "Progetti",
"pt": "Projetos",
"ru": "Проекты",
},
"contact": {
"nl": "Contact",
"en": "Contact",
"de": "Kontakt",
"fr": "Contact",
"es": "Contacto",
"it": "Contatto",
"pt": "Contacto",
"ru": "Контакт",
},
"process": {
"nl": "Werkwijze",
"en": "How we work",
"de": "Vorgehensweise",
"fr": "Méthode de travail",
"es": "Método de trabajo",
"it": "Metodo di lavoro",
"pt": "Método de trabalho",
"ru": "Как мы работаем",
},
"starter": {
"nl": "Starter-website",
"en": "Starter website",
"de": "Starter-Website",
"fr": "Site de démarrage",
"es": "Sitio web inicial",
"it": "Sito starter",
"pt": "Website inicial",
"ru": "Стартовый сайт",
},
"business": {
"nl": "Zakelijke website",
"en": "Business website",
"de": "Geschäftswebsite",
"fr": "Site dentreprise",
"es": "Sitio web empresarial",
"it": "Sito business",
"pt": "Site empresarial",
"ru": "Бизнес-сайт",
},
"webshop": {
"nl": "Webshop-implementatie",
"en": "Webshop implementation",
"de": "Webshop-Implementierung",
"fr": "Implémentation e-commerce",
"es": "Implementación webshop",
"it": "Implementazione webshop",
"pt": "Implementação de webshop",
"ru": "Внедрение вебшопа",
},
"support": {
"nl": "Onderhoud & groei",
"en": "Maintenance & growth",
"de": "Wartung & Wachstum",
"fr": "Maintenance & croissance",
"es": "Mantenimiento y crecimiento",
"it": "Manutenzione e crescita",
"pt": "Manutenção & crescimento",
"ru": "Поддержка и рост",
},
"ai_search": {
"nl": "AI-zoekfunctie",
"en": "AI search",
"de": "KI-Suche",
"fr": "Recherche IA",
"es": "Búsqueda con IA",
"it": "Ricerca IA",
"pt": "Pesquisa com IA",
"ru": "Поиск с ИИ",
},
}
COMMON_CTA = {
"nl": {"primary": "Plan een kennismakingsgesprek", "secondary": "Bekijk onze diensten"},
"en": {"primary": "Book an introductory call", "secondary": "View our services"},
"de": {"primary": "Erstgespräch planen", "secondary": "Unsere Leistungen ansehen"},
"fr": {"primary": "Planifier un échange initial", "secondary": "Voir nos services"},
"es": {"primary": "Planificar una reunión inicial", "secondary": "Ver nuestros servicios"},
"it": {"primary": "Prenota un colloquio conoscitivo", "secondary": "Scopri i nostri servizi"},
"pt": {"primary": "Agendar reunião introdutória", "secondary": "Ver os nossos serviços"},
"ru": {"primary": "Запланировать вводный звонок", "secondary": "Посмотреть услуги"},
}
CTA_VARIANTS = {
"nl": [
"Plan gratis gesprek",
"Plan intake",
"Plan dienstengesprek",
"Contact Support",
"Start jouw project",
"Vraag intake aan",
"Plan kennismaking",
"Bekijk diensten",
"Bekijk alle diensten",
"Vraag startergesprek aan",
"Plan startergesprek",
"Plan zakelijk gesprek",
"Start webshop traject",
"Vraag supportplan aan",
"Plan gratis kennismaking",
"Bekijk projectresultaten",
],
}
NL_REPLACEMENTS = {
"New": "Nieuw",
"Popular": "Populair",
"AI Search": "AI-zoekfunctie",
"custom blokken": "maatwerkblokken",
"monitoring-ready basis": "stabiele technische basis",
"Monitoring + fixes": "Monitoring en technische oplossingen",
"SEO-ready basis": "SEO-vriendelijke basis",
"Starter Website": "Starter-website",
"Business Website": "Zakelijke website",
"Support & Groei": "Onderhoud & groei",
"24u": "binnen 24 uur",
"24u Reactietijd": "Reactie binnen 24 uur",
"15m Intake call": "Intakegesprek van 15 minuten",
"100% Vrijblijvend": "Volledig vrijblijvend",
"Webshop Implementatie": "Webshop-implementatie",
"Doorlopend Verbetering": "Doorlopende verbetering",
"Monitoring-ready stack": "Stabiele technische basis",
}
FOOTER_CONTENT = {
"nl": {
"about": "<p>MandelBlog bouwt websites voor dienstverleners, studios en kleine teams die professioneel online willen staan zonder template-ruis.</p>",
"links_heading": "Snelle links",
"support_heading": "Plan een gesprek",
"support": "<p><a href=\"/contact/\">Plan een kennismakingsgesprek</a><br/><a href=\"mailto:info@mandelblog.com\">info@mandelblog.com</a><br/><a href=\"/diensten/\">Bekijk onze diensten</a></p>",
"mini": "<p><a href=\"/contact/\">Contact</a> - <a href=\"/diensten/\">Diensten</a> - <a href=\"/projecten/\">Projecten</a> - MandelBlog Studio</p>",
},
"en": {
"about": "<p>MandelBlog builds websites for service businesses, studios and small teams that need a credible online presence without template clutter.</p>",
"links_heading": "Quick links",
"support_heading": "Book a call",
"support": "<p><a href=\"/en/contact/\">Book an introductory call</a><br/><a href=\"mailto:info@mandelblog.com\">info@mandelblog.com</a><br/><a href=\"/en/services/\">View our services</a></p>",
"mini": "<p><a href=\"/en/contact/\">Contact</a> - <a href=\"/en/services/\">Services</a> - <a href=\"/en/projects/\">Projects</a> - MandelBlog Studio</p>",
},
"de": {
"about": "<p>MandelBlog entwickelt Websites für Dienstleister, Studios und kleine Teams, die professionell auftreten möchten, ohne Template-Ballast.</p>",
"links_heading": "Schnellzugriff",
"support_heading": "Gespräch planen",
"support": "<p><a href=\"/de/kontakt/\">Erstgespräch planen</a><br/><a href=\"mailto:info@mandelblog.com\">info@mandelblog.com</a><br/><a href=\"/de/dienstleistungen/\">Leistungen ansehen</a></p>",
"mini": "<p><a href=\"/de/kontakt/\">Kontakt</a> - <a href=\"/de/dienstleistungen/\">Dienstleistungen</a> - <a href=\"/de/projekte/\">Projekte</a> - MandelBlog Studio</p>",
},
"fr": {
"about": "<p>MandelBlog conçoit des sites pour les sociétés de services, les studios et les petites équipes qui veulent une présence crédible, sans surcharge de template.</p>",
"links_heading": "Accès rapides",
"support_heading": "Planifier un échange",
"support": "<p><a href=\"/fr/contact/\">Planifier un échange initial</a><br/><a href=\"mailto:info@mandelblog.com\">info@mandelblog.com</a><br/><a href=\"/fr/services/\">Voir nos services</a></p>",
"mini": "<p><a href=\"/fr/contact/\">Contact</a> - <a href=\"/fr/services/\">Services</a> - <a href=\"/fr/projets/\">Projets</a> - MandelBlog Studio</p>",
},
"es": {
"about": "<p>MandelBlog crea sitios web para empresas de servicios, estudios y pequeños equipos que quieren una presencia creíble sin aspecto de plantilla.</p>",
"links_heading": "Accesos rápidos",
"support_heading": "Planificar una reunión",
"support": "<p><a href=\"/es/contacto/\">Planificar una reunión inicial</a><br/><a href=\"mailto:info@mandelblog.com\">info@mandelblog.com</a><br/><a href=\"/es/servicios/\">Ver nuestros servicios</a></p>",
"mini": "<p><a href=\"/es/contacto/\">Contacto</a> - <a href=\"/es/servicios/\">Servicios</a> - <a href=\"/es/proyectos/\">Proyectos</a> - MandelBlog Studio</p>",
},
"it": {
"about": "<p>MandelBlog realizza siti per aziende di servizi, studi e piccoli team che vogliono una presenza credibile senza leffetto template.</p>",
"links_heading": "Link rapidi",
"support_heading": "Prenota un colloquio",
"support": "<p><a href=\"/it/contatto/\">Prenota un colloquio conoscitivo</a><br/><a href=\"mailto:info@mandelblog.com\">info@mandelblog.com</a><br/><a href=\"/it/servizi/\">Scopri i nostri servizi</a></p>",
"mini": "<p><a href=\"/it/contatto/\">Contatto</a> - <a href=\"/it/servizi/\">Servizi</a> - <a href=\"/it/progetti/\">Progetti</a> - MandelBlog Studio</p>",
},
"pt": {
"about": "<p>A MandelBlog cria sites para empresas de serviços, estúdios e pequenas equipas que precisam de uma presença credível sem aparência de template.</p>",
"links_heading": "Acessos rápidos",
"support_heading": "Agendar reunião",
"support": "<p><a href=\"/pt/contato/\">Agendar reunião introdutória</a><br/><a href=\"mailto:info@mandelblog.com\">info@mandelblog.com</a><br/><a href=\"/pt/servicos/\">Ver os nossos serviços</a></p>",
"mini": "<p><a href=\"/pt/contato/\">Contacto</a> - <a href=\"/pt/servicos/\">Serviços</a> - <a href=\"/pt/projetos/\">Projetos</a> - MandelBlog Studio</p>",
},
"ru": {
"about": "<p>MandelBlog создаёт сайты для сервисных компаний, студий и небольших команд, которым нужен убедительный онлайн-образ без шаблонного шума.</p>",
"links_heading": "Быстрые ссылки",
"support_heading": "Назначить звонок",
"support": "<p><a href=\"/ru/kontakt/\">Запланировать вводный звонок</a><br/><a href=\"mailto:info@mandelblog.com\">info@mandelblog.com</a><br/><a href=\"/ru/uslugi/\">Посмотреть услуги</a></p>",
"mini": "<p><a href=\"/ru/kontakt/\">Контакт</a> - <a href=\"/ru/uslugi/\">Услуги</a> - <a href=\"/ru/proekty/\">Проекты</a> - MandelBlog Studio</p>",
},
}
def uid() -> str:
return str(uuid.uuid4())
def block(block_type: str, value: dict[str, Any]) -> dict[str, Any]:
return {"type": block_type, "value": value, "id": uid()}
def item(value: dict[str, Any] | str) -> dict[str, Any]:
return {"type": "item", "value": value, "id": uid()}
def replace_nested(node: Any, replacements: dict[str, str]) -> Any:
if isinstance(node, dict):
return {key: replace_nested(value, replacements) for key, value in node.items()}
if isinstance(node, list):
return [replace_nested(value, replacements) for value in node]
if isinstance(node, str):
for source, target in replacements.items():
node = node.replace(source, target)
return node
return node
def clone(blocks: list[dict[str, Any]]) -> list[dict[str, Any]]:
return copy.deepcopy(blocks)
def footer_stream_data(locale: str, links: dict[str, str]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
cfg = FOOTER_CONTENT[locale]
footer = [
block("about_us", {"heading": "MandelBlog Studio", "content": cfg["about"]}),
block(
"text",
{
"heading": cfg["links_heading"],
"content": (
f'<p><a href="{links["about"]}">{PAGE_TITLE_MAP["about"][locale]}</a><br/>'
f'<a href="{links["services"]}">{PAGE_TITLE_MAP["services"][locale]}</a><br/>'
f'<a href="{links["projects"]}">{PAGE_TITLE_MAP["projects"][locale]}</a><br/>'
f'<a href="{links["contact"]}">{PAGE_TITLE_MAP["contact"][locale]}</a></p>'
),
},
),
block("text", {"heading": cfg["support_heading"], "content": cfg["support"]}),
]
mini = [block("text", cfg["mini"])]
return footer, mini
def nl_home(urls: dict[str, str]) -> list[dict[str, Any]]:
primary = COMMON_CTA["nl"]["primary"]
secondary = COMMON_CTA["nl"]["secondary"]
return [
block(
"saas_hero_banner",
{
"layout_width": "container",
"background_style": "light",
"layout": "split",
"badge_text": "MANDELBLOG STUDIO",
"badge_url": urls["home"],
"headline": "Websites voor bedrijven die professioneel willen groeien",
"sub_headline": "<p>MandelBlog ontwikkelt websites die vertrouwen opbouwen, duidelijk sturen op contact en eenvoudig te beheren zijn voor uw team.</p>",
"primary_cta_text": primary,
"primary_cta_url": urls["contact"],
"secondary_cta_text": secondary,
"secondary_cta_url": urls["services"],
"hero_image": 1,
"video_url": "",
"stats": [
item({"value": "3", "label": "Heldere stappen"}),
item({"value": "1", "label": "Vast aanspreekpunt"}),
item({"value": "8", "label": "Beschikbare talen"}),
],
"customer_logos_title": "Gebouwd met Wagtail, Django en beproefde componenten",
},
),
block(
"saas_features",
{
"layout_width": "container",
"background_style": "light",
"layout": "grid",
"section_title": "Waar MandelBlog op stuurt",
"section_subtitle": "<p>Geen webshopdemo, maar een zakelijke website die klaar is voor aanvragen, vertrouwen en doorontwikkeling.</p>",
"features": [
item({"icon": "diagram-3", "icon_image": None, "title": "Duidelijke structuur", "description": "<p>Bezoekers vinden snel de juiste dienst, case of contactroute.</p>", "link_text": PAGE_TITLE_MAP["process"]["nl"], "link_url": urls["process"], "highlight": "featured"}),
item({"icon": "pencil-square", "icon_image": None, "title": "Zelf te beheren", "description": "<p>Teksten, beelden en secties beheert u zelf in overzichtelijke blokken.</p>", "link_text": secondary, "link_url": urls["services"], "highlight": "none"}),
item({"icon": "shield-check", "icon_image": None, "title": "Stabiele technische basis", "description": "<p>Een schaalbare opzet zonder overbodige complexiteit of template-ruis.</p>", "link_text": "Bekijk werkwijze", "link_url": urls["process"], "highlight": "none"}),
item({"icon": "graph-up-arrow", "icon_image": None, "title": "Klaar voor doorontwikkeling", "description": "<p>Later uitbreiden met extra paginas, koppelingen of commerce blijft mogelijk.</p>", "link_text": "Bekijk projecten", "link_url": urls["projects"], "highlight": "none"}),
],
"columns": "2",
},
),
block(
"saas_pricing",
{
"layout_width": "container",
"background_style": "light",
"layout": "cards",
"section_title": "Onze pakketten",
"section_subtitle": "<p>Elk pakket heeft een duidelijke scope. De exacte invulling stemmen we af in het kennismakingsgesprek.</p>",
"show_annual_toggle": False,
"annual_discount_text": "",
"tiers": [
item({"name": PAGE_TITLE_MAP["starter"]["nl"], "description": "Voor ondernemers die professioneel online willen starten", "price_monthly": None, "price_annual": None, "price_suffix": "", "custom_price_text": "Op offertebasis", "features": [item({"text": "Kernpaginas en duidelijke navigatie", "included": True, "tooltip": ""}), item({"text": "Editor voor eigen contentbeheer", "included": True, "tooltip": ""}), item({"text": "Mobiel sterke presentatie", "included": True, "tooltip": ""})], "cta_text": primary, "cta_url": urls["contact"], "cta_style": "secondary", "is_featured": False, "featured_label": ""}),
item({"name": PAGE_TITLE_MAP["business"]["nl"], "description": "Voor dienstverleners met meerdere proposities of groeiplannen", "price_monthly": None, "price_annual": None, "price_suffix": "", "custom_price_text": "Op offertebasis", "features": [item({"text": "Meer ruimte voor diensten en cases", "included": True, "tooltip": ""}), item({"text": "Conversiegerichte opbouw", "included": True, "tooltip": ""}), item({"text": "SEO-vriendelijke basis", "included": True, "tooltip": ""})], "cta_text": primary, "cta_url": urls["contact"], "cta_style": "primary", "is_featured": True, "featured_label": "Aanbevolen"}),
item({"name": PAGE_TITLE_MAP["webshop"]["nl"], "description": "Voor organisaties die een zakelijke site willen uitbreiden met online verkoop", "price_monthly": None, "price_annual": None, "price_suffix": "", "custom_price_text": "Op offertebasis", "features": [item({"text": "Productstructuur en checkout", "included": True, "tooltip": ""}), item({"text": "Betalingen en orderverwerking", "included": True, "tooltip": ""}), item({"text": "Schaalbare commerce-opzet", "included": True, "tooltip": ""})], "cta_text": primary, "cta_url": urls["contact"], "cta_style": "secondary", "is_featured": False, "featured_label": ""}),
item({"name": PAGE_TITLE_MAP["support"]["nl"], "description": "Voor teams die onderhoud, technische rust en doorlopende optimalisatie nodig hebben", "price_monthly": None, "price_annual": None, "price_suffix": "", "custom_price_text": "Maandelijks traject", "features": [item({"text": "Updates en onderhoud", "included": True, "tooltip": ""}), item({"text": "Monitoring en technische oplossingen", "included": True, "tooltip": ""}), item({"text": "Doorlopende verbetering", "included": True, "tooltip": ""})], "cta_text": primary, "cta_url": urls["contact"], "cta_style": "secondary", "is_featured": False, "featured_label": ""}),
],
"footer_text": "<p>We adviseren welk pakket past bij uw fase, team en doelstelling.</p>",
},
),
block(
"saas_testimonials",
{
"layout_width": "container",
"background_style": "light",
"layout": "cards",
"section_title": "Wat opdrachtgevers waarderen",
"section_subtitle": "<p>Kleine teams kiezen voor MandelBlog omdat het traject overzichtelijk blijft en de site daarna echt bruikbaar is.</p>",
"testimonials": [
item({"quote": "<p>We kregen in korte tijd een website die eindelijk past bij onze dienstverlening en die we zelf kunnen onderhouden.</p>", "author_name": "Sanne de Vries", "author_title": "Studio Nova - eigenaar", "author_photo": None, "company_logo": None, "rating": 0}),
item({"quote": "<p>Het traject was helder, de teksten kregen structuur en onze contactaanvragen lopen nu via één duidelijke route.</p>", "author_name": "Mark Jansen", "author_title": "Jansen Interieur - medeoprichter", "author_photo": None, "company_logo": None, "rating": 0}),
],
"customer_logos": [],
"aggregate_rating": "",
"aggregate_source": "",
},
),
block(
"saas_faq",
{
"layout_width": "container",
"background_style": "light",
"layout": "accordion",
"section_title": "Veelgestelde vragen",
"section_subtitle": "<p>We zijn duidelijk over planning, samenwerking en beheer.</p>",
"faqs": [
item({"question": "Voor welke bedrijven is MandelBlog geschikt?", "answer": "<p>Voor dienstverleners, studios en kleine teams die een professionele site nodig hebben zonder zwaar traject.</p>", "category": "Algemeen"}),
item({"question": "Kunnen we later uitbreiden?", "answer": "<p>Ja. We bouwen een structuur waarmee extra paginas, talen of koppelingen later logisch aansluiten.</p>", "category": "Uitbreiding"}),
item({"question": "Beheren we de content zelf?", "answer": "<p>Ja. De opzet is juist bedoeld zodat uw team paginas en blokken zelfstandig kan aanpassen.</p>", "category": "Beheer"}),
item({"question": "Wat gebeurt er na livegang?", "answer": "<p>Dan kunt u kiezen voor onderhoud en gerichte doorontwikkeling als dat nodig is.</p>", "category": "Support"}),
],
"show_contact_cta": "card",
"contact_cta_text": primary,
"contact_cta_url": urls["contact"],
},
),
block(
"saas_cta_footer",
{
"layout_width": "container",
"background_style": "light",
"layout": "banner",
"headline": "Wilt u een website die vertrouwen geeft en werk uit handen neemt?",
"subheadline": "<p>Plan een kennismakingsgesprek en we laten zien welke opzet past bij uw bedrijf en team.</p>",
"primary_cta_text": primary,
"primary_cta_url": urls["contact"],
"secondary_cta_text": secondary,
"secondary_cta_url": urls["services"],
"background_image": 1,
"side_image": 1,
"show_no_credit_card": "with-icon",
"no_credit_card_text": "Volledig vrijblijvend",
},
),
]
def nl_standard_page(page_key: str, urls: dict[str, str]) -> list[dict[str, Any]]:
primary = COMMON_CTA["nl"]["primary"]
secondary = COMMON_CTA["nl"]["secondary"]
page_data = {
"about": {
"headline": "Wie MandelBlog is en hoe we werken",
"sub": "<p>MandelBlog helpt kleine bedrijven en dienstverleners aan een website die professioneel oogt, logisch converteert en beheerbaar blijft voor het eigen team.</p>",
"features_title": "Waar we op letten",
"features_sub": "<p>We werken het liefst voor organisaties die behoefte hebben aan duidelijkheid, snelheid en inhoudelijke regie.</p>",
"features": [
("people", "Voor wie we werken", "<p>Dienstverleners, studios en kleine teams met een duidelijke propositie en een praktische planning.</p>"),
("diagram-3", "Onze werkwijze", "<p>We starten met scherpte in doel en inhoud, bouwen met vaste blokken en leveren beheersbaar op.</p>"),
("shield-check", "Waarom het anders werkt", "<p>Geen los template of black box, maar een duidelijke structuur waarmee u zelf verder kunt.</p>"),
("person-badge", "Klein team, direct contact", "<p>U schakelt direct met de mensen die het werk uitvoeren en keuzes vertalen naar de site.</p>"),
],
"extra_block": block("saas_animated_stats", {"layout_width": "container", "background_style": "light", "layout": "cards-grid", "badge": "Werkwijze", "heading": "Onze aanpak in 3 stappen", "subheading": "Kort traject, duidelijke keuzes en daarna een site die voor uw team werkt.", "stats": [item({"value": "1", "prefix": None, "suffix": "", "label": "Kennismaking", "description": "We bepalen doel, inhoud en prioriteiten.", "icon": "chat-square-text", "highlight": False}), item({"value": "2", "prefix": None, "suffix": "", "label": "Uitwerking", "description": "We bouwen de paginas en stemmen de inhoud af.", "icon": "layout-text-window", "highlight": False}), item({"value": "3", "prefix": None, "suffix": "", "label": "Oplevering", "description": "U krijgt uitleg, beheer en een duidelijke vervolgstap.", "icon": "rocket", "highlight": False})], "animation_duration": 1800, "animation_easing": "ease-out", "start_on_scroll": True, "show_logos": False, "logos_heading": "", "company_logos": []}),
"cta": "Wilt u weten of onze aanpak past bij uw bedrijf?",
},
"services": {
"headline": "Diensten voor bedrijven die overzicht en kwaliteit willen",
"sub": "<p>Elke dienst is ingericht rondom duidelijke keuzes, bruikbare content en een technische basis die door kan groeien.</p>",
"features_title": "Wat we leveren",
"features_sub": "<p>Geen losse modules, maar een traject dat aansluit op uw fase, team en doelen.</p>",
"features": [
("window", PAGE_TITLE_MAP["starter"]["nl"], "<p>Voor ondernemers die snel professioneel online willen staan met een heldere basis.</p>"),
("briefcase", PAGE_TITLE_MAP["business"]["nl"], "<p>Voor organisaties met meerdere diensten, cases of een complexere aanbodstructuur.</p>"),
("cart-check", PAGE_TITLE_MAP["webshop"]["nl"], "<p>Voor teams die online verkoop willen toevoegen zonder de grip op techniek te verliezen.</p>"),
("wrench-adjustable", PAGE_TITLE_MAP["support"]["nl"], "<p>Voor organisaties die onderhoud, stabiliteit en doorlopende verbetering nodig hebben.</p>"),
],
"extra_block": None,
"cta": "Twijfelt u welk pakket past bij uw fase?",
},
"projects": {
"headline": "Projecten waarin structuur, inhoud en techniek samenkomen",
"sub": "<p>Onze projecten zijn ontworpen om professioneel over te komen, vertrouwen op te bouwen en beheerbaar te blijven na livegang.</p>",
"features_title": "Wat u in onze projecten terugziet",
"features_sub": "<p>We sturen niet op oppervlakkige effecten, maar op duidelijkheid en bruikbaarheid.</p>",
"features": [
("diagram-3", "Heldere pagina-opbouw", "<p>Bezoekers begrijpen snel waar ze moeten zijn en welke stap logisch volgt.</p>"),
("pencil-square", "Eenvoudig beheer", "<p>Teams kunnen teksten, visuals en paginas zelf aanpassen zonder omweg.</p>"),
("graph-up-arrow", "Gericht op aanvragen", "<p>Contact en conversie zijn zichtbaar verwerkt in de structuur en inhoud.</p>"),
],
"extra_block": None,
"cta": "Wilt u uw volgende project professioneel neerzetten?",
},
"contact": {
"headline": "Laten we uw vraag concreet maken",
"sub": "<p>Vertel kort wat u nodig heeft. U krijgt een praktische terugkoppeling met haalbare vervolgstappen.</p>",
"features_title": "Waarvoor u contact kunt opnemen",
"features_sub": "<p>Kies de route die past bij uw vraag of traject.</p>",
"features": [
("rocket", "Nieuw traject", "<p>Voor een nieuwe website, herpositionering of complete herbouw.</p>"),
("briefcase", "Pakketkeuze", "<p>Voor advies over welk pakket of welke structuur het beste past.</p>"),
("tools", "Onderhoud of uitbreiding", "<p>Voor technische ondersteuning, uitbreidingen of een vervolgfase na livegang.</p>"),
],
"extra_block": block("saas_demo_request", {"layout_width": "container", "background_style": "light", "layout": "split", "section_title": "Vertel kort wat u nodig heeft", "section_subtitle": "<p>We reageren inhoudelijk en zonder verkooppraat op uw vraag.</p>", "form_fields": [item({"field_type": "text", "label": "Naam", "placeholder": "Uw naam", "required": True}), item({"field_type": "email", "label": "E-mail", "placeholder": "naam@bedrijf.nl", "required": True}), item({"field_type": "company", "label": "Bedrijf", "placeholder": "Bedrijfsnaam", "required": True}), item({"field_type": "message", "label": "Vraag of project", "placeholder": "Waar zoekt u hulp bij?", "required": False})], "submit_button_text": primary, "form_action_url": urls["contact"], "benefits_title": "Wat u kunt verwachten", "benefits": [item("Reactie binnen 24 uur"), item("Intakegesprek van 15 minuten"), item("Volledig vrijblijvend")], "side_image": 1, "privacy_text": "<p>We gebruiken uw gegevens alleen voor contact over deze aanvraag.</p>"}),
"cta": "Klaar om een eerste stap te zetten?",
},
"process": {
"headline": "Werkwijze met duidelijke stappen en vaste keuzes",
"sub": "<p>We houden het traject overzichtelijk: u weet wanneer iets gebeurt, wat u moet aanleveren en waar we naartoe werken.</p>",
"features_title": "Zo werken we samen",
"features_sub": "<p>Kort, duidelijk en zonder onnodige ruis.</p>",
"features": [
("chat-square-text", "1. Kennismaking", "<p>We bespreken doel, doelgroep, inhoud en wat u intern wilt kunnen beheren.</p>"),
("layout-text-window", "2. Uitwerking", "<p>We zetten structuur, inhoud en ontwerp om in een duidelijke pagina-opbouw.</p>"),
("rocket", "3. Oplevering", "<p>Na review gaat de site live en zorgen we voor een beheerbare overdracht.</p>"),
("graph-up-arrow", "4. Doorontwikkeling", "<p>Wanneer nodig bouwen we verder op basis van gedrag, vragen en nieuwe plannen.</p>"),
],
"extra_block": None,
"cta": "Wilt u dit traject ook voor uw website?",
},
}
cfg = page_data[page_key]
blocks = [
block("saas_hero_banner", {"layout_width": "container", "background_style": "light", "layout": "split", "badge_text": "MANDELBLOG STUDIO" if page_key != "services" else "DIENSTEN", "badge_url": urls[page_key], "headline": cfg["headline"], "sub_headline": cfg["sub"], "primary_cta_text": primary, "primary_cta_url": urls["contact"], "secondary_cta_text": secondary, "secondary_cta_url": urls["services"], "hero_image": 1 if page_key != "process" else 24, "video_url": "", "stats": [], "customer_logos_title": ""}),
block("saas_features", {"layout_width": "container", "background_style": "light", "layout": "grid", "section_title": cfg["features_title"], "section_subtitle": cfg["features_sub"], "features": [item({"icon": icon, "icon_image": None, "title": title, "description": desc, "link_text": primary if page_key in {"contact", "about"} else secondary, "link_url": urls["contact"] if page_key in {"contact", "about"} else urls["services"], "highlight": "none"}) for icon, title, desc in cfg["features"]], "columns": "2" if len(cfg["features"]) <= 4 else "3"}),
]
if cfg["extra_block"] is not None:
blocks.append(cfg["extra_block"])
blocks.append(block("saas_faq", {"layout_width": "container", "background_style": "light", "layout": "accordion", "section_title": "Veelgestelde vragen", "section_subtitle": "<p>We houden het traject helder en praktisch.</p>", "faqs": [item({"question": "Werken jullie met vaste templates?", "answer": "<p>Nee. We gebruiken herbruikbare blokken, maar stemmen inhoud en structuur af op uw bedrijf.</p>", "category": "Werkwijze"}), item({"question": "Kunnen we later uitbreiden?", "answer": "<p>Ja. De opzet is bedoeld om later door te groeien zonder opnieuw te beginnen.</p>", "category": "Uitbreiding"}), item({"question": "Beheren we de inhoud zelf?", "answer": "<p>Ja. Dat is juist een belangrijk uitgangspunt van het platform.</p>", "category": "Beheer"})], "show_contact_cta": "card", "contact_cta_text": primary, "contact_cta_url": urls["contact"]}))
blocks.append(block("saas_cta_footer", {"layout_width": "container", "background_style": "light", "layout": "banner", "headline": cfg["cta"], "subheadline": "<p>Plan een kennismakingsgesprek en we laten zien welke route logisch is voor uw bedrijf.</p>", "primary_cta_text": primary, "primary_cta_url": urls["contact"], "secondary_cta_text": secondary, "secondary_cta_url": urls["services"], "background_image": 1, "side_image": 1, "show_no_credit_card": "with-icon", "no_credit_card_text": "Volledig vrijblijvend"}))
return blocks
def nl_service_page(kind: str, urls: dict[str, str]) -> list[dict[str, Any]]:
primary = COMMON_CTA["nl"]["primary"]
secondary = COMMON_CTA["nl"]["secondary"]
config = {
"starter": {
"title": "Starter-website",
"audience": "Voor ondernemers of kleine teams die snel professioneel online willen staan met een duidelijke basis.",
"what": [
("layout-text-window", "Voor wie is dit?", "<p>Voor bedrijven met een helder aanbod die snel een professionele eerste indruk willen neerzetten.</p>"),
("window", "Wat krijgt u?", "<p>Kernpaginas, een logische navigatie en een editor waarmee uw team zelf content kan beheren.</p>"),
("graph-up-arrow", "Wat levert het op?", "<p>Een professionele basis waarmee bezoekers sneller begrijpen wat u doet en hoe ze contact opnemen.</p>"),
],
"outcomes": [
("shield-check", "Heldere online basis", "<p>Geen overbodige onderdelen, wel een site die vertrouwen geeft.</p>"),
("people", "Eenvoudig beheer", "<p>Uw team kan updates zelf doen zonder afhankelijkheid.</p>"),
("rocket", "Snelle livegang", "<p>Geschikt als eerste professionele stap of als vervanging van een verouderde site.</p>"),
],
"choose": ["U wilt snel professioneel online staan.", "U heeft vooral kernpaginas en duidelijke navigatie nodig.", "U wilt zelf teksten en beelden kunnen aanpassen."],
"duration": "Gemiddelde oplevering: 2 tot 4 weken",
},
"business": {
"title": "Zakelijke website",
"audience": "Voor dienstverleners en teams die meerdere proposities, cases of funnelstappen helder willen presenteren.",
"what": [
("briefcase", "Voor wie is dit?", "<p>Voor organisaties die meer structuur, inhoudelijke diepgang en een sterkere aanvraagroute nodig hebben.</p>"),
("layout-text-window", "Wat krijgt u?", "<p>Meer pagina-opbouw, ruimte voor cases en een SEO-vriendelijke basis die logisch meegroeit.</p>"),
("graph-up-arrow", "Wat levert het op?", "<p>Een site die uw aanbod beter uitlegt en bezoekers gerichter naar contact of aanvraag leidt.</p>"),
],
"outcomes": [
("diagram-3", "Meer overzicht", "<p>Diensten, cases en expertise krijgen elk hun eigen plek.</p>"),
("search", "Betere vindbaarheid", "<p>De opbouw is ingericht voor sterke inhoud en een SEO-vriendelijke basis.</p>"),
("people", "Sterkere aanvragen", "<p>Bezoekers zien sneller welke route en welk aanbod bij hen past.</p>"),
],
"choose": ["U heeft meerdere diensten of doelgroepen.", "U wilt cases, expertise en bewijs beter laten zien.", "U zoekt meer structuur dan een startsite biedt."],
"duration": "Gemiddelde oplevering: 2 tot 4 weken",
},
"webshop": {
"title": "Webshop-implementatie",
"audience": "Voor organisaties die online verkoop willen toevoegen zonder in een standaardshop te belanden.",
"what": [
("cart-check", "Voor wie is dit?", "<p>Voor bedrijven die hun aanbod online willen verkopen met grip op inhoud, checkout en beheer.</p>"),
("credit-card", "Wat krijgt u?", "<p>Een webshopstructuur met productoverzicht, checkout en een schaalbare basis voor orderverwerking.</p>"),
("graph-up-arrow", "Wat levert het op?", "<p>Een verkoopomgeving die past bij uw merk en niet voelt als een los demo-sjabloon.</p>"),
],
"outcomes": [
("window", "Betere presentatie", "<p>Producten en categorieën krijgen een zakelijke, duidelijke opbouw.</p>"),
("shield-check", "Stabiele techniek", "<p>Betalingen en orderverwerking sluiten aan op een beheerbare stack.</p>"),
("rocket", "Klaar voor groei", "<p>De commerce-opzet kan meegroeien met assortiment en processen.</p>"),
],
"choose": ["U wilt online verkoop combineren met een zakelijke website.", "U heeft behoefte aan grip op structuur en techniek.", "U zoekt geen standaard thema, maar een doordachte implementatie."],
"duration": "Gemiddelde oplevering: 3 tot 6 weken",
},
"support": {
"title": "Onderhoud & groei",
"audience": "Voor teams die hun website of webshop stabiel willen houden en gericht willen doorontwikkelen.",
"what": [
("tools", "Voor wie is dit?", "<p>Voor organisaties die niet zelf alle techniek willen monitoren, oplossen en plannen.</p>"),
("shield-check", "Wat krijgt u?", "<p>Onderhoud, updates, monitoring en technische oplossingen binnen een vast werkritme.</p>"),
("graph-up-arrow", "Wat levert het op?", "<p>Meer rust, minder technische verrassingen en ruimte om gericht te verbeteren.</p>"),
],
"outcomes": [
("activity", "Minder verstoringen", "<p>Technische issues worden sneller gesignaleerd en opgelost.</p>"),
("clipboard-data", "Doorlopende verbetering", "<p>We werken stap voor stap aan performance, inhoud en conversie.</p>"),
("people", "Vast ritme", "<p>U weet wanneer onderhoud gebeurt en waar prioriteit ligt.</p>"),
],
"choose": ["U wilt een vaste partner voor technisch onderhoud.", "Uw site vraagt om kleine verbeteringen in plaats van een volledige herbouw.", "U wilt sneller kunnen schakelen bij issues of uitbreidingen."],
"duration": "Reactie binnen 24 uur",
},
}[kind]
return [
block("saas_hero_banner", {"layout_width": "container", "background_style": "light", "layout": "split", "badge_text": "PAKKET", "badge_url": urls[kind], "headline": config["title"], "sub_headline": f"<p>{config['audience']}</p>", "primary_cta_text": primary, "primary_cta_url": urls['contact'], "secondary_cta_text": secondary, "secondary_cta_url": urls['services'], "hero_image": 23, "video_url": "", "stats": [item({"value": config['duration'], "label": "Doorlooptijd"}), item({"value": "Reactie binnen 24 uur", "label": "Communicatie"}), item({"value": "Volledig vrijblijvend", "label": "Kennismaking"})], "customer_logos_title": ""}),
block("saas_features", {"layout_width": "container", "background_style": "light", "layout": "grid", "section_title": "Wat krijgt u?", "section_subtitle": "<p>Elk pakket is opgebouwd rond duidelijke keuzes, beheerbaarheid en inhoud die past bij uw bedrijf.</p>", "features": [item({"icon": icon, "icon_image": None, "title": title, "description": desc, "link_text": secondary, "link_url": urls['services'], "highlight": "none"}) for icon, title, desc in config['what']], "columns": "3"}),
block("saas_features", {"layout_width": "container", "background_style": "light", "layout": "grid", "section_title": "Wat levert het op?", "section_subtitle": "<p>De waarde zit niet in losse effecten, maar in duidelijkere communicatie en een beter werkende site.</p>", "features": [item({"icon": icon, "icon_image": None, "title": title, "description": desc, "link_text": primary, "link_url": urls['contact'], "highlight": "none"}) for icon, title, desc in config['outcomes']], "columns": "3"}),
block("saas_demo_request", {"layout_width": "container", "background_style": "light", "layout": "split", "section_title": "Wanneer kiest u dit pakket?", "section_subtitle": "<p>We adviseren dit pakket wanneer onderstaande punten aansluiten op uw situatie.</p>", "form_fields": [item({"field_type": "text", "label": "Naam", "placeholder": "Uw naam", "required": True}), item({"field_type": "email", "label": "E-mail", "placeholder": "naam@bedrijf.nl", "required": True}), item({"field_type": "company", "label": "Bedrijf", "placeholder": "Bedrijfsnaam", "required": True}), item({"field_type": "message", "label": "Vraag of context", "placeholder": "Vertel kort waar u nu staat", "required": False})], "submit_button_text": primary, "form_action_url": urls['contact'], "benefits_title": "Dit pakket past wanneer", "benefits": [item(text) for text in config['choose']], "side_image": 23, "privacy_text": "<p>We gebruiken uw gegevens alleen voor een reactie op uw aanvraag.</p>"}),
block("saas_cta_footer", {"layout_width": "container", "background_style": "light", "layout": "banner", "headline": f"Wilt u weten of {config['title'].lower()} past bij uw situatie?", "subheadline": "<p>Plan een kennismakingsgesprek en we adviseren eerlijk welk pakket logisch is.</p>", "primary_cta_text": primary, "primary_cta_url": urls['contact'], "secondary_cta_text": secondary, "secondary_cta_url": urls['services'], "background_image": 1, "side_image": 1, "show_no_credit_card": "with-icon", "no_credit_card_text": "Volledig vrijblijvend"}),
]
def nl_body_for(page_key: str, urls: dict[str, str]) -> list[dict[str, Any]]:
if page_key == 'home':
return nl_home(urls)
if page_key in {'about', 'services', 'projects', 'contact', 'process'}:
return nl_standard_page(page_key, urls)
if page_key in {'starter', 'business', 'webshop', 'support'}:
return nl_service_page(page_key, urls)
raise KeyError(page_key)
class Command(BaseCommand):
help = "Apply MandelBlog agency cleanup content to the main site tree"
def add_arguments(self, parser):
parser.add_argument('--apply', action='store_true', help='Persist and publish changes')
def handle(self, *args, **options):
apply_changes = options['apply']
with transaction.atomic():
for locale in Locale.objects.all().order_by('language_code'):
language_code = locale.language_code
translated_pages = {}
for key, source_id in SOURCE_PAGE_IDS.items():
source = Page.objects.get(id=source_id)
translated = Page.objects.filter(translation_key=source.translation_key, locale=locale).specific().first()
if translated:
translated_pages[key] = translated
urls = {key: page.url for key, page in translated_pages.items() if getattr(page, 'url', None)}
if 'home' not in urls:
continue
for key, page in translated_pages.items():
changed = False
title_map = PAGE_TITLE_MAP.get(key)
if title_map and title_map.get(language_code) and page.title != title_map[language_code]:
page.title = title_map[language_code]
changed = True
if key == 'ai_search':
if changed and apply_changes:
rev = page.save_revision()
rev.publish()
continue
if hasattr(page, 'body'):
raw_data = list(page.body.raw_data)
if language_code == 'nl' and key in {'home', 'about', 'services', 'projects', 'contact', 'process', 'starter', 'business', 'webshop', 'support'}:
page.body = StreamValue(page.body.stream_block, nl_body_for(key, urls), is_lazy=True)
changed = True
else:
replacements = {}
replacements.update(NL_REPLACEMENTS if language_code == 'nl' else {})
if language_code in COMMON_CTA:
primary = COMMON_CTA[language_code]['primary']
secondary = COMMON_CTA[language_code]['secondary']
for variant in CTA_VARIANTS.get('nl', []):
replacements[variant] = primary
replacements['Bekijk diensten'] = secondary
replacements['Bekijk alle diensten'] = secondary
replacements['Plan kennismaking'] = primary
updated_raw_data = replace_nested(clone(raw_data), replacements)
if updated_raw_data != raw_data:
page.body = StreamValue(page.body.stream_block, updated_raw_data, is_lazy=True)
changed = True
if changed:
self.stdout.write(f"{language_code}: update {key} -> {page.title}")
if apply_changes:
rev = page.save_revision()
rev.publish()
footer_cfg = FOOTER_CONTENT.get(language_code)
footer_obj = LocalizedFooterContent.objects.filter(locale=locale).first()
if footer_obj and footer_cfg:
link_urls = {
'about': urls.get('about', '/'),
'services': urls.get('services', '/'),
'projects': urls.get('projects', '/'),
'contact': urls.get('contact', '/'),
}
footer_data, mini_data = footer_stream_data(language_code, link_urls)
footer_obj.footer = StreamValue(footer_obj.footer.stream_block, footer_data, is_lazy=True)
footer_obj.mini_footer = StreamValue(footer_obj.mini_footer.stream_block, mini_data, is_lazy=True)
self.stdout.write(f"{language_code}: update footer")
if apply_changes:
footer_obj.save()
if not apply_changes:
transaction.set_rollback(True)
self.stdout.write(self.style.WARNING('Dry run complete; no changes saved.'))
else:
self.stdout.write(self.style.SUCCESS('Agency website refresh applied.'))

View File

@@ -1,40 +1,18 @@
{% extends "carbasa/headers/header.html" %} {% extends "carbasa/headers/header.html" %}
{% load i18n oxyan category_tags ocyan_main ocyanjson wagtailsettings_tags %} {% load agency_navigation %}
{% block nav %} {% block nav %}
{% ocyanjson "theme" "menu_depth" 1 as menu_depth %}
<div class="collapse navbar-collapse menu-bar page-menu-bar" id="navbarSupportedContent"> <div class="collapse navbar-collapse menu-bar page-menu-bar" id="navbarSupportedContent">
<div class="brand-wrapper"> <div class="brand-wrapper">
{% include 'partials/brand.html' with big=True %} {% include 'partials/brand.html' with big=True %}
</div> </div>
{% agency_nav_pages as nav_pages %}
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if request.LANGUAGE_CODE == 'nl' %} {% for nav_page in nav_pages %}
<li class="megamenu nav-item"> <li class="nav-item child">
<span class="overlay"></span> <a class="nav-link" href="{{ nav_page.url }}">{{ nav_page.title }}</a>
<a class="toggler nav-link" tabindex="0" aria-label="{% trans 'Open Megamenu' %}">
{% trans "Our Collection" %} <i class="fa fa-chevron-down small ms-1"></i>
</a>
<div class="outer">
<nav id="header_breadcrumb" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a data-path="root" tabindex="-1">{% trans "Our collection" %}</a></li>
</ol>
</nav>
<ul class="inner">
<li class="category-main">
<a class="nav-link main-assortment" data-name="{{ menu_item.name|safe }}" href="{% url 'catalogue:index' %}" tabindex="-1">
{% trans "View" %} <b class="ms-1">{% trans "Our Collections" %}</b>
</a>
</li> </li>
{% category_tree depth=menu_depth as category_tree_items %} {% endfor %}
{% include "webshop/mega_dropdown.html" with menu_items=category_tree_items %}
</ul>
</div>
</li>
{% endif %}
{% rootpage_as_category as page_tree_root %}
{% category_tree 2 page_tree_root as page_tree_items %}
{% include "partials/dropdown.html" with menu_items=page_tree_items limit=2 %}
</ul> </ul>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,4 +1,4 @@
{% load i18n i18n_helpers %} {% load i18n i18n_helpers agency_navigation %}
<style> <style>
.ms-lang-switcher { display: inline-flex; align-items: center; } .ms-lang-switcher { display: inline-flex; align-items: center; }
.ms-lang-switcher .form-select { .ms-lang-switcher .form-select {
@@ -16,16 +16,18 @@
border-color: #3b82f6; border-color: #3b82f6;
box-shadow: 0 0 0 0.18rem rgba(59, 130, 246, 0.18); box-shadow: 0 0 0 0.18rem rgba(59, 130, 246, 0.18);
} }
.ms-lang-switcher-link { .ms-header-cta {
font-size: 0.74rem; display: inline-flex;
color: #64748b; align-items: center;
border-radius: 999px;
padding: 0.55rem 1rem;
font-size: 0.84rem;
font-weight: 700;
text-decoration: none; text-decoration: none;
margin-right: 0.5rem; margin-left: 0.5rem;
} }
.ms-lang-switcher-link:hover { color: #1d4ed8; }
</style> </style>
<div class="header-right"> <div class="header-right">
<a class="ms-lang-switcher-link" href="/language-test/" title="Language test">LANG</a>
<form action="{% url 'set_language' %}" method="post" class="ms-lang-switcher me-2" aria-label="Language switcher"> <form action="{% url 'set_language' %}" method="post" class="ms-lang-switcher me-2" aria-label="Language switcher">
{% csrf_token %} {% csrf_token %}
<input name="next" type="hidden" value="{{ request.path|untranslated_url }}"> <input name="next" type="hidden" value="{{ request.path|untranslated_url }}">
@@ -46,8 +48,11 @@
<a tabindex="0" aria-label="Open Search" role="search" class="search-toggler user-button menu-circle"> <a tabindex="0" aria-label="Open Search" role="search" class="search-toggler user-button menu-circle">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</a> </a>
<a href="{% url 'customer:summary' %}" aria-label="{% trans 'Customer summary' %}" class="user-button menu-circle"><i class="fa fa-user-solid"></i></a>
{% include 'oxyan/headers/partials/mini_basket.html' %} {% agency_page 'contact' as contact_page %}
{% if contact_page %}
<a href="{{ contact_page.url }}" class="btn btn-primary ms-header-cta">{% agency_primary_cta %}</a>
{% endif %}
</div> </div>
<div class="alert-messages-header" aria-live="polite"> <div class="alert-messages-header" aria-live="polite">

View File

View File

@@ -0,0 +1,83 @@
from __future__ import annotations
from django import template
from wagtail.models import Locale, Page
register = template.Library()
SOURCE_PAGE_IDS = {
"about": 128,
"services": 129,
"projects": 130,
"contact": 131,
"process": 192,
}
NAV_ORDER = ["services", "projects", "process", "about", "contact"]
def _resolve_locale(language_code: str | None) -> Locale | None:
if not language_code:
return None
try:
return Locale.objects.get(language_code=language_code)
except Locale.DoesNotExist:
return None
def _translated_page(source_id: int, language_code: str | None) -> Page | None:
locale = _resolve_locale(language_code)
try:
source = Page.objects.get(id=source_id)
except Page.DoesNotExist:
return None
if locale is None:
return source.specific
translated = (
Page.objects.filter(translation_key=source.translation_key, locale=locale)
.live()
.public()
.specific()
.first()
)
return translated or source.specific
@register.simple_tag(takes_context=True)
def agency_nav_pages(context):
request = context.get("request")
language_code = getattr(request, "LANGUAGE_CODE", None)
pages = []
for key in NAV_ORDER:
page = _translated_page(SOURCE_PAGE_IDS[key], language_code)
if page is not None:
pages.append(page)
return pages
@register.simple_tag(takes_context=True)
def agency_page(context, key: str):
request = context.get("request")
language_code = getattr(request, "LANGUAGE_CODE", None)
source_id = SOURCE_PAGE_IDS.get(key)
if source_id is None:
return None
return _translated_page(source_id, language_code)
CTA_LABELS = {
"nl": "Plan een kennismakingsgesprek",
"en": "Book an introductory call",
"de": "Erstgespräch planen",
"fr": "Planifier un échange initial",
"es": "Planificar una reunión inicial",
"it": "Prenota un colloquio conoscitivo",
"pt": "Agendar reunião introdutória",
"ru": "Запланировать вводный звонок",
}
@register.simple_tag(takes_context=True)
def agency_primary_cta(context):
request = context.get("request")
language_code = getattr(request, "LANGUAGE_CODE", None) or "nl"
return CTA_LABELS.get(language_code, CTA_LABELS["nl"])