diff --git a/mandelstudio/management/commands/apply_agency_website_refresh.py b/mandelstudio/management/commands/apply_agency_website_refresh.py new file mode 100644 index 0000000..60bceeb --- /dev/null +++ b/mandelstudio/management/commands/apply_agency_website_refresh.py @@ -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": "
MandelBlog bouwt websites voor dienstverleners, studio’s en kleine teams die professioneel online willen staan zonder template-ruis.
", + "links_heading": "Snelle links", + "support_heading": "Plan een gesprek", + "support": "Plan een kennismakingsgesprek
info@mandelblog.com
Bekijk onze diensten
Contact - Diensten - Projecten - MandelBlog Studio
", + }, + "en": { + "about": "MandelBlog builds websites for service businesses, studios and small teams that need a credible online presence without template clutter.
", + "links_heading": "Quick links", + "support_heading": "Book a call", + "support": "Book an introductory call
info@mandelblog.com
View our services
Contact - Services - Projects - MandelBlog Studio
", + }, + "de": { + "about": "MandelBlog entwickelt Websites für Dienstleister, Studios und kleine Teams, die professionell auftreten möchten, ohne Template-Ballast.
", + "links_heading": "Schnellzugriff", + "support_heading": "Gespräch planen", + "support": "Erstgespräch planen
info@mandelblog.com
Leistungen ansehen
Kontakt - Dienstleistungen - Projekte - MandelBlog Studio
", + }, + "fr": { + "about": "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.
", + "links_heading": "Accès rapides", + "support_heading": "Planifier un échange", + "support": "Planifier un échange initial
info@mandelblog.com
Voir nos services
Contact - Services - Projets - MandelBlog Studio
", + }, + "es": { + "about": "MandelBlog crea sitios web para empresas de servicios, estudios y pequeños equipos que quieren una presencia creíble sin aspecto de plantilla.
", + "links_heading": "Accesos rápidos", + "support_heading": "Planificar una reunión", + "support": "Planificar una reunión inicial
info@mandelblog.com
Ver nuestros servicios
Contacto - Servicios - Proyectos - MandelBlog Studio
", + }, + "it": { + "about": "MandelBlog realizza siti per aziende di servizi, studi e piccoli team che vogliono una presenza credibile senza l’effetto template.
", + "links_heading": "Link rapidi", + "support_heading": "Prenota un colloquio", + "support": "Prenota un colloquio conoscitivo
info@mandelblog.com
Scopri i nostri servizi
Contatto - Servizi - Progetti - MandelBlog Studio
", + }, + "pt": { + "about": "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.
", + "links_heading": "Acessos rápidos", + "support_heading": "Agendar reunião", + "support": "Agendar reunião introdutória
info@mandelblog.com
Ver os nossos serviços
Contacto - Serviços - Projetos - MandelBlog Studio
", + }, + "ru": { + "about": "MandelBlog создаёт сайты для сервисных компаний, студий и небольших команд, которым нужен убедительный онлайн-образ без шаблонного шума.
", + "links_heading": "Быстрые ссылки", + "support_heading": "Назначить звонок", + "support": "Запланировать вводный звонок
info@mandelblog.com
Посмотреть услуги
Контакт - Услуги - Проекты - MandelBlog Studio
", + }, +} + + +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'{PAGE_TITLE_MAP["about"][locale]}
'
+ f'{PAGE_TITLE_MAP["services"][locale]}
'
+ f'{PAGE_TITLE_MAP["projects"][locale]}
'
+ f'{PAGE_TITLE_MAP["contact"][locale]}
MandelBlog ontwikkelt websites die vertrouwen opbouwen, duidelijk sturen op contact en eenvoudig te beheren zijn voor uw team.
", + "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": "Geen webshopdemo, maar een zakelijke website die klaar is voor aanvragen, vertrouwen en doorontwikkeling.
", + "features": [ + item({"icon": "diagram-3", "icon_image": None, "title": "Duidelijke structuur", "description": "Bezoekers vinden snel de juiste dienst, case of contactroute.
", "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": "Teksten, beelden en secties beheert u zelf in overzichtelijke blokken.
", "link_text": secondary, "link_url": urls["services"], "highlight": "none"}), + item({"icon": "shield-check", "icon_image": None, "title": "Stabiele technische basis", "description": "Een schaalbare opzet zonder overbodige complexiteit of template-ruis.
", "link_text": "Bekijk werkwijze", "link_url": urls["process"], "highlight": "none"}), + item({"icon": "graph-up-arrow", "icon_image": None, "title": "Klaar voor doorontwikkeling", "description": "Later uitbreiden met extra pagina’s, koppelingen of commerce blijft mogelijk.
", "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": "Elk pakket heeft een duidelijke scope. De exacte invulling stemmen we af in het kennismakingsgesprek.
", + "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": "We adviseren welk pakket past bij uw fase, team en doelstelling.
", + }, + ), + block( + "saas_testimonials", + { + "layout_width": "container", + "background_style": "light", + "layout": "cards", + "section_title": "Wat opdrachtgevers waarderen", + "section_subtitle": "Kleine teams kiezen voor MandelBlog omdat het traject overzichtelijk blijft en de site daarna echt bruikbaar is.
", + "testimonials": [ + item({"quote": "We kregen in korte tijd een website die eindelijk past bij onze dienstverlening en die we zelf kunnen onderhouden.
", "author_name": "Sanne de Vries", "author_title": "Studio Nova - eigenaar", "author_photo": None, "company_logo": None, "rating": 0}), + item({"quote": "Het traject was helder, de teksten kregen structuur en onze contactaanvragen lopen nu via één duidelijke route.
", "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": "We zijn duidelijk over planning, samenwerking en beheer.
", + "faqs": [ + item({"question": "Voor welke bedrijven is MandelBlog geschikt?", "answer": "Voor dienstverleners, studio’s en kleine teams die een professionele site nodig hebben zonder zwaar traject.
", "category": "Algemeen"}), + item({"question": "Kunnen we later uitbreiden?", "answer": "Ja. We bouwen een structuur waarmee extra pagina’s, talen of koppelingen later logisch aansluiten.
", "category": "Uitbreiding"}), + item({"question": "Beheren we de content zelf?", "answer": "Ja. De opzet is juist bedoeld zodat uw team pagina’s en blokken zelfstandig kan aanpassen.
", "category": "Beheer"}), + item({"question": "Wat gebeurt er na livegang?", "answer": "Dan kunt u kiezen voor onderhoud en gerichte doorontwikkeling als dat nodig is.
", "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": "Plan een kennismakingsgesprek en we laten zien welke opzet past bij uw bedrijf en team.
", + "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": "MandelBlog helpt kleine bedrijven en dienstverleners aan een website die professioneel oogt, logisch converteert en beheerbaar blijft voor het eigen team.
", + "features_title": "Waar we op letten", + "features_sub": "We werken het liefst voor organisaties die behoefte hebben aan duidelijkheid, snelheid en inhoudelijke regie.
", + "features": [ + ("people", "Voor wie we werken", "Dienstverleners, studio’s en kleine teams met een duidelijke propositie en een praktische planning.
"), + ("diagram-3", "Onze werkwijze", "We starten met scherpte in doel en inhoud, bouwen met vaste blokken en leveren beheersbaar op.
"), + ("shield-check", "Waarom het anders werkt", "Geen los template of black box, maar een duidelijke structuur waarmee u zelf verder kunt.
"), + ("person-badge", "Klein team, direct contact", "U schakelt direct met de mensen die het werk uitvoeren en keuzes vertalen naar de site.
"), + ], + "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": "Elke dienst is ingericht rondom duidelijke keuzes, bruikbare content en een technische basis die door kan groeien.
", + "features_title": "Wat we leveren", + "features_sub": "Geen losse modules, maar een traject dat aansluit op uw fase, team en doelen.
", + "features": [ + ("window", PAGE_TITLE_MAP["starter"]["nl"], "Voor ondernemers die snel professioneel online willen staan met een heldere basis.
"), + ("briefcase", PAGE_TITLE_MAP["business"]["nl"], "Voor organisaties met meerdere diensten, cases of een complexere aanbodstructuur.
"), + ("cart-check", PAGE_TITLE_MAP["webshop"]["nl"], "Voor teams die online verkoop willen toevoegen zonder de grip op techniek te verliezen.
"), + ("wrench-adjustable", PAGE_TITLE_MAP["support"]["nl"], "Voor organisaties die onderhoud, stabiliteit en doorlopende verbetering nodig hebben.
"), + ], + "extra_block": None, + "cta": "Twijfelt u welk pakket past bij uw fase?", + }, + "projects": { + "headline": "Projecten waarin structuur, inhoud en techniek samenkomen", + "sub": "Onze projecten zijn ontworpen om professioneel over te komen, vertrouwen op te bouwen en beheerbaar te blijven na livegang.
", + "features_title": "Wat u in onze projecten terugziet", + "features_sub": "We sturen niet op oppervlakkige effecten, maar op duidelijkheid en bruikbaarheid.
", + "features": [ + ("diagram-3", "Heldere pagina-opbouw", "Bezoekers begrijpen snel waar ze moeten zijn en welke stap logisch volgt.
"), + ("pencil-square", "Eenvoudig beheer", "Teams kunnen teksten, visuals en pagina’s zelf aanpassen zonder omweg.
"), + ("graph-up-arrow", "Gericht op aanvragen", "Contact en conversie zijn zichtbaar verwerkt in de structuur en inhoud.
"), + ], + "extra_block": None, + "cta": "Wilt u uw volgende project professioneel neerzetten?", + }, + "contact": { + "headline": "Laten we uw vraag concreet maken", + "sub": "Vertel kort wat u nodig heeft. U krijgt een praktische terugkoppeling met haalbare vervolgstappen.
", + "features_title": "Waarvoor u contact kunt opnemen", + "features_sub": "Kies de route die past bij uw vraag of traject.
", + "features": [ + ("rocket", "Nieuw traject", "Voor een nieuwe website, herpositionering of complete herbouw.
"), + ("briefcase", "Pakketkeuze", "Voor advies over welk pakket of welke structuur het beste past.
"), + ("tools", "Onderhoud of uitbreiding", "Voor technische ondersteuning, uitbreidingen of een vervolgfase na livegang.
"), + ], + "extra_block": block("saas_demo_request", {"layout_width": "container", "background_style": "light", "layout": "split", "section_title": "Vertel kort wat u nodig heeft", "section_subtitle": "We reageren inhoudelijk en zonder verkooppraat op uw vraag.
", "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": "We gebruiken uw gegevens alleen voor contact over deze aanvraag.
"}), + "cta": "Klaar om een eerste stap te zetten?", + }, + "process": { + "headline": "Werkwijze met duidelijke stappen en vaste keuzes", + "sub": "We houden het traject overzichtelijk: u weet wanneer iets gebeurt, wat u moet aanleveren en waar we naartoe werken.
", + "features_title": "Zo werken we samen", + "features_sub": "Kort, duidelijk en zonder onnodige ruis.
", + "features": [ + ("chat-square-text", "1. Kennismaking", "We bespreken doel, doelgroep, inhoud en wat u intern wilt kunnen beheren.
"), + ("layout-text-window", "2. Uitwerking", "We zetten structuur, inhoud en ontwerp om in een duidelijke pagina-opbouw.
"), + ("rocket", "3. Oplevering", "Na review gaat de site live en zorgen we voor een beheerbare overdracht.
"), + ("graph-up-arrow", "4. Doorontwikkeling", "Wanneer nodig bouwen we verder op basis van gedrag, vragen en nieuwe plannen.
"), + ], + "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": "We houden het traject helder en praktisch.
", "faqs": [item({"question": "Werken jullie met vaste templates?", "answer": "Nee. We gebruiken herbruikbare blokken, maar stemmen inhoud en structuur af op uw bedrijf.
", "category": "Werkwijze"}), item({"question": "Kunnen we later uitbreiden?", "answer": "Ja. De opzet is bedoeld om later door te groeien zonder opnieuw te beginnen.
", "category": "Uitbreiding"}), item({"question": "Beheren we de inhoud zelf?", "answer": "Ja. Dat is juist een belangrijk uitgangspunt van het platform.
", "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": "Plan een kennismakingsgesprek en we laten zien welke route logisch is voor uw bedrijf.
", "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?", "Voor bedrijven met een helder aanbod die snel een professionele eerste indruk willen neerzetten.
"), + ("window", "Wat krijgt u?", "Kernpagina’s, een logische navigatie en een editor waarmee uw team zelf content kan beheren.
"), + ("graph-up-arrow", "Wat levert het op?", "Een professionele basis waarmee bezoekers sneller begrijpen wat u doet en hoe ze contact opnemen.
"), + ], + "outcomes": [ + ("shield-check", "Heldere online basis", "Geen overbodige onderdelen, wel een site die vertrouwen geeft.
"), + ("people", "Eenvoudig beheer", "Uw team kan updates zelf doen zonder afhankelijkheid.
"), + ("rocket", "Snelle livegang", "Geschikt als eerste professionele stap of als vervanging van een verouderde site.
"), + ], + "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?", "Voor organisaties die meer structuur, inhoudelijke diepgang en een sterkere aanvraagroute nodig hebben.
"), + ("layout-text-window", "Wat krijgt u?", "Meer pagina-opbouw, ruimte voor cases en een SEO-vriendelijke basis die logisch meegroeit.
"), + ("graph-up-arrow", "Wat levert het op?", "Een site die uw aanbod beter uitlegt en bezoekers gerichter naar contact of aanvraag leidt.
"), + ], + "outcomes": [ + ("diagram-3", "Meer overzicht", "Diensten, cases en expertise krijgen elk hun eigen plek.
"), + ("search", "Betere vindbaarheid", "De opbouw is ingericht voor sterke inhoud en een SEO-vriendelijke basis.
"), + ("people", "Sterkere aanvragen", "Bezoekers zien sneller welke route en welk aanbod bij hen past.
"), + ], + "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?", "Voor bedrijven die hun aanbod online willen verkopen met grip op inhoud, checkout en beheer.
"), + ("credit-card", "Wat krijgt u?", "Een webshopstructuur met productoverzicht, checkout en een schaalbare basis voor orderverwerking.
"), + ("graph-up-arrow", "Wat levert het op?", "Een verkoopomgeving die past bij uw merk en niet voelt als een los demo-sjabloon.
"), + ], + "outcomes": [ + ("window", "Betere presentatie", "Producten en categorieën krijgen een zakelijke, duidelijke opbouw.
"), + ("shield-check", "Stabiele techniek", "Betalingen en orderverwerking sluiten aan op een beheerbare stack.
"), + ("rocket", "Klaar voor groei", "De commerce-opzet kan meegroeien met assortiment en processen.
"), + ], + "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?", "Voor organisaties die niet zelf alle techniek willen monitoren, oplossen en plannen.
"), + ("shield-check", "Wat krijgt u?", "Onderhoud, updates, monitoring en technische oplossingen binnen een vast werkritme.
"), + ("graph-up-arrow", "Wat levert het op?", "Meer rust, minder technische verrassingen en ruimte om gericht te verbeteren.
"), + ], + "outcomes": [ + ("activity", "Minder verstoringen", "Technische issues worden sneller gesignaleerd en opgelost.
"), + ("clipboard-data", "Doorlopende verbetering", "We werken stap voor stap aan performance, inhoud en conversie.
"), + ("people", "Vast ritme", "U weet wanneer onderhoud gebeurt en waar prioriteit ligt.
"), + ], + "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"{config['audience']}
", "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": "Elk pakket is opgebouwd rond duidelijke keuzes, beheerbaarheid en inhoud die past bij uw bedrijf.
", "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": "De waarde zit niet in losse effecten, maar in duidelijkere communicatie en een beter werkende site.
", "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": "We adviseren dit pakket wanneer onderstaande punten aansluiten op uw situatie.
", "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": "We gebruiken uw gegevens alleen voor een reactie op uw aanvraag.
"}), + 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": "Plan een kennismakingsgesprek en we adviseren eerlijk welk pakket logisch is.
", "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.')) diff --git a/mandelstudio/templates/carbasa/headers/mega.html b/mandelstudio/templates/carbasa/headers/mega.html index 1e9ad5a..20bfe80 100644 --- a/mandelstudio/templates/carbasa/headers/mega.html +++ b/mandelstudio/templates/carbasa/headers/mega.html @@ -1,40 +1,18 @@ {% extends "carbasa/headers/header.html" %} -{% load i18n oxyan category_tags ocyan_main ocyanjson wagtailsettings_tags %} +{% load agency_navigation %} {% block nav %} -{% ocyanjson "theme" "menu_depth" 1 as menu_depth %} {% endblock %} diff --git a/mandelstudio/templates/oxyan/headers/partials/carbasa-user-bar.html b/mandelstudio/templates/oxyan/headers/partials/carbasa-user-bar.html index c352038..03ac792 100644 --- a/mandelstudio/templates/oxyan/headers/partials/carbasa-user-bar.html +++ b/mandelstudio/templates/oxyan/headers/partials/carbasa-user-bar.html @@ -1,4 +1,4 @@ -{% load i18n i18n_helpers %} +{% load i18n i18n_helpers agency_navigation %}