Clean agency navigation and refresh core site content
This commit is contained in:
669
mandelstudio/management/commands/apply_agency_website_refresh.py
Normal file
669
mandelstudio/management/commands/apply_agency_website_refresh.py
Normal 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 d’entreprise",
|
||||||
|
"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, studio’s 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 l’effetto 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 pagina’s, 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": "Kernpagina’s 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, studio’s 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 pagina’s, 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 pagina’s 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, studio’s 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 pagina’s 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 pagina’s 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>Kernpagina’s, 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 kernpagina’s 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.'))
|
||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
0
mandelstudio/templatetags/__init__.py
Normal file
0
mandelstudio/templatetags/__init__.py
Normal file
83
mandelstudio/templatetags/agency_navigation.py
Normal file
83
mandelstudio/templatetags/agency_navigation.py
Normal 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"])
|
||||||
Reference in New Issue
Block a user