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

", + "mini": "

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

", + "mini": "

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

", + "mini": "

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

", + "mini": "

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

", + "mini": "

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

", + "mini": "

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

", + "mini": "

Contacto - Serviços - Projetos - MandelBlog Studio

", + }, + "ru": { + "about": "

MandelBlog создаёт сайты для сервисных компаний, студий и небольших команд, которым нужен убедительный онлайн-образ без шаблонного шума.

", + "links_heading": "Быстрые ссылки", + "support_heading": "Назначить звонок", + "support": "

Запланировать вводный звонок
info@mandelblog.com
Посмотреть услуги

", + "mini": "

Контакт - Услуги - Проекты - 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]}

' + ), + }, + ), + 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": "

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 %}
- LANG
{% csrf_token %} @@ -46,8 +48,11 @@ - - {% include 'oxyan/headers/partials/mini_basket.html' %} + + {% agency_page 'contact' as contact_page %} + {% if contact_page %} + {% agency_primary_cta %} + {% endif %}
diff --git a/mandelstudio/templatetags/__init__.py b/mandelstudio/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mandelstudio/templatetags/agency_navigation.py b/mandelstudio/templatetags/agency_navigation.py new file mode 100644 index 0000000..7c4dd40 --- /dev/null +++ b/mandelstudio/templatetags/agency_navigation.py @@ -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"])