10 Commits

12 changed files with 602 additions and 45 deletions

View File

@@ -139,6 +139,15 @@ FOOTER_CONTENT = {
} }
def _contact_form_anchor(url: str | None) -> str | None:
if not url:
return url
if url.endswith("#contact-form"):
return url
base = url.split("#", 1)[0]
return f"{base}#contact-form"
def uid() -> str: def uid() -> str:
return str(uuid.uuid4()) return str(uuid.uuid4())
@@ -3904,6 +3913,11 @@ def _standard_body(
cta = COMMON_CTA[locale] cta = COMMON_CTA[locale]
common = STANDARD_COPY[locale] common = STANDARD_COPY[locale]
cfg = common["pages"][page_key] cfg = common["pages"][page_key]
contact_cta_url = (
_contact_form_anchor(urls["contact"])
if page_key == "contact"
else urls["contact"]
)
blocks = [ blocks = [
block( block(
"saas_hero_banner", "saas_hero_banner",
@@ -3934,7 +3948,7 @@ def _standard_body(
"headline": cfg["headline"], "headline": cfg["headline"],
"sub_headline": cfg["sub"], "sub_headline": cfg["sub"],
"primary_cta_text": cta["primary"], "primary_cta_text": cta["primary"],
"primary_cta_url": urls["contact"], "primary_cta_url": contact_cta_url,
"secondary_cta_text": "", "secondary_cta_text": "",
"secondary_cta_url": "", "secondary_cta_url": "",
"hero_image": ( "hero_image": (
@@ -4066,7 +4080,7 @@ def _standard_body(
"headline": cfg["cta"], "headline": cfg["cta"],
"subheadline": common["cta_sub"], "subheadline": common["cta_sub"],
"primary_cta_text": cta["primary"], "primary_cta_text": cta["primary"],
"primary_cta_url": urls["contact"], "primary_cta_url": contact_cta_url,
"secondary_cta_text": "", "secondary_cta_text": "",
"secondary_cta_url": "", "secondary_cta_url": "",
"background_image": AGENCY_SUPPORT_IMAGE_ID, "background_image": AGENCY_SUPPORT_IMAGE_ID,

View File

@@ -15,6 +15,7 @@ from mandelstudio.management.commands._agency_content import (
CTA_VARIANTS, CTA_VARIANTS,
FOOTER_CONTENT, FOOTER_CONTENT,
NL_REPLACEMENTS, NL_REPLACEMENTS,
_contact_form_anchor,
) )
from mandelstudio.management.commands._agency_content import ( from mandelstudio.management.commands._agency_content import (
body_for as localized_body_for, body_for as localized_body_for,
@@ -548,6 +549,11 @@ def nl_home(urls: dict[str, str]) -> list[dict[str, Any]]:
def nl_standard_page(page_key: str, urls: dict[str, str]) -> list[dict[str, Any]]: def nl_standard_page(page_key: str, urls: dict[str, str]) -> list[dict[str, Any]]:
primary = COMMON_CTA["nl"]["primary"] primary = COMMON_CTA["nl"]["primary"]
secondary = COMMON_CTA["nl"]["secondary"] secondary = COMMON_CTA["nl"]["secondary"]
contact_cta_url = (
_contact_form_anchor(urls["contact"])
if page_key == "contact"
else urls["contact"]
)
page_data = { page_data = {
"about": { "about": {
"headline": "Wie MandelBlog is en hoe we werken", "headline": "Wie MandelBlog is en hoe we werken",
@@ -809,7 +815,7 @@ def nl_standard_page(page_key: str, urls: dict[str, str]) -> list[dict[str, Any]
"headline": cfg["headline"], "headline": cfg["headline"],
"sub_headline": cfg["sub"], "sub_headline": cfg["sub"],
"primary_cta_text": primary, "primary_cta_text": primary,
"primary_cta_url": urls["contact"], "primary_cta_url": contact_cta_url,
"secondary_cta_text": secondary, "secondary_cta_text": secondary,
"secondary_cta_url": urls["services"], "secondary_cta_url": urls["services"],
"hero_image": 1 if page_key != "process" else 24, "hero_image": 1 if page_key != "process" else 24,
@@ -836,8 +842,10 @@ def nl_standard_page(page_key: str, urls: dict[str, str]) -> list[dict[str, Any]
"link_text": primary "link_text": primary
if page_key in {"contact", "about"} if page_key in {"contact", "about"}
else secondary, else secondary,
"link_url": urls["contact"] "link_url": contact_cta_url
if page_key in {"contact", "about"} if page_key == "contact"
else urls["contact"]
if page_key == "about"
else urls["services"], else urls["services"],
"highlight": "none", "highlight": "none",
} }
@@ -884,7 +892,7 @@ def nl_standard_page(page_key: str, urls: dict[str, str]) -> list[dict[str, Any]
], ],
"show_contact_cta": "card", "show_contact_cta": "card",
"contact_cta_text": primary, "contact_cta_text": primary,
"contact_cta_url": urls["contact"], "contact_cta_url": contact_cta_url,
}, },
) )
) )
@@ -898,7 +906,7 @@ def nl_standard_page(page_key: str, urls: dict[str, str]) -> list[dict[str, Any]
"headline": cfg["cta"], "headline": cfg["cta"],
"subheadline": "<p>Plan een kennismakingsgesprek en we laten zien welke route logisch is voor uw bedrijf.</p>", "subheadline": "<p>Plan een kennismakingsgesprek en we laten zien welke route logisch is voor uw bedrijf.</p>",
"primary_cta_text": primary, "primary_cta_text": primary,
"primary_cta_url": urls["contact"], "primary_cta_url": contact_cta_url,
"secondary_cta_text": secondary, "secondary_cta_text": secondary,
"secondary_cta_url": urls["services"], "secondary_cta_url": urls["services"],
"background_image": 1, "background_image": 1,

View File

@@ -0,0 +1,139 @@
from __future__ import annotations
from typing import Any
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from wagtail.blocks import StreamValue
from wagtail.models import Locale, Page
CONTACT_SLUGS = {
"nl": "contact",
"en": "contact",
"de": "kontakt",
"fr": "contact",
"es": "contacto",
"it": "contatto",
"pt": "contato",
"ru": "контакт",
}
CONTACT_ANCHOR = "#contact-form"
CTA_BLOCK_TYPES = {"saas_hero_banner", "saas_cta_footer"}
def _with_contact_anchor(url: str | None) -> str | None:
if not url:
return url
if url.endswith(CONTACT_ANCHOR):
return url
return f"{url.split('#', 1)[0]}{CONTACT_ANCHOR}"
def _localized_page_url(source: Page, locale: Locale) -> str | None:
translated = (
Page.objects.filter(translation_key=source.translation_key, locale=locale)
.specific()
.first()
)
chosen = translated or source
return getattr(chosen, "url", None)
class Command(BaseCommand):
help = "Anchor contact page CTA buttons to the contact form across locales"
def add_arguments(self, parser):
parser.add_argument(
"--page-id",
type=int,
help="Optional Wagtail page id to fix.",
)
parser.add_argument(
"--apply",
action="store_true",
help="Persist and publish changes (default is dry-run).",
)
def handle(self, *args, **options):
apply_changes = options["apply"]
page_id = options.get("page_id")
with transaction.atomic():
if page_id:
page = Page.objects.filter(id=page_id).specific().first()
if page is None:
raise CommandError(f"Page id={page_id} not found")
self._fix_page(page, apply_changes=apply_changes)
else:
source = self._find_contact_source_page()
if source is None:
raise CommandError("Could not find a source contact page")
for locale in Locale.objects.all().order_by("language_code"):
page = (
Page.objects.filter(
translation_key=source.translation_key,
locale=locale,
)
.specific()
.first()
)
if page is None:
self.stdout.write(
f"SKIP {locale.language_code}: no contact translation"
)
continue
self._fix_page(page, apply_changes=apply_changes)
if not apply_changes:
raise CommandError(
"Dry-run complete. Re-run with --apply to persist changes."
)
def _find_contact_source_page(self) -> Page | None:
for code, slug in CONTACT_SLUGS.items():
locale = Locale.objects.filter(language_code=code).first()
if locale is None:
continue
page = Page.objects.filter(locale=locale, slug=slug).specific().first()
if page is not None:
return page
return None
def _fix_page(self, page: Page, *, apply_changes: bool) -> None:
specific = page.specific
code = page.locale.language_code
if not hasattr(specific, "body"):
self.stdout.write(f"SKIP {code}: no body streamfield")
return
localized_contact_url = _with_contact_anchor(
_localized_page_url(page, page.locale)
)
body = specific.body
raw_data = list(body.raw_data)
changed = False
for block in raw_data:
if block.get("type") not in CTA_BLOCK_TYPES:
continue
value = block.get("value")
if not isinstance(value, dict):
continue
if value.get("primary_cta_url") != localized_contact_url:
value["primary_cta_url"] = localized_contact_url
block["value"] = value
changed = True
if not changed:
self.stdout.write(f"OK {code}: contact CTA already anchored")
return
self.stdout.write(f"CHG {code}: anchored contact CTA buttons")
specific.body = StreamValue(body.stream_block, raw_data, is_lazy=True)
if apply_changes:
rev = specific.save_revision()
rev.publish()

View File

@@ -0,0 +1,56 @@
from django.db import migrations
LANGUAGES = ("en", "de", "fr", "es", "it", "pt", "ru")
def _add_column_if_missing(schema_editor, table_name, column_name, column_sql):
connection = schema_editor.connection
with connection.cursor() as cursor:
existing = {
row.name
for row in connection.introspection.get_table_description(cursor, table_name)
}
if column_name in existing:
return
quoted_table = schema_editor.quote_name(table_name)
quoted_column = schema_editor.quote_name(column_name)
cursor.execute(
f"ALTER TABLE {quoted_table} ADD COLUMN {quoted_column} {column_sql}"
)
def add_checkout_i18n_columns(apps, schema_editor):
for lang in LANGUAGES:
_add_column_if_missing(
schema_editor,
"checkout_fixedsurcharge",
f"name_{lang}",
"varchar(32) NULL",
)
_add_column_if_missing(
schema_editor,
"checkout_percentagesurcharge",
f"name_{lang}",
"varchar(32) NULL",
)
_add_column_if_missing(
schema_editor,
"checkout_paymentmethod",
f"label_{lang}",
"varchar(32) NULL",
)
class Migration(migrations.Migration):
dependencies = [
("mandelstudio", "0004_contact_messages"),
("checkout", "0017_remove_unused_price_fields"),
]
operations = [
migrations.RunPython(
add_checkout_i18n_columns,
migrations.RunPython.noop,
)
]

View File

@@ -0,0 +1,43 @@
from django.db import migrations
LANGUAGES = ("en", "de", "fr", "es", "it", "pt", "ru")
def _add_column_if_missing(schema_editor, table_name, column_name, column_sql):
connection = schema_editor.connection
with connection.cursor() as cursor:
existing = {
row.name
for row in connection.introspection.get_table_description(cursor, table_name)
}
if column_name in existing:
return
quoted_table = schema_editor.quote_name(table_name)
quoted_column = schema_editor.quote_name(column_name)
cursor.execute(
f"ALTER TABLE {quoted_table} ADD COLUMN {quoted_column} {column_sql}"
)
def add_address_i18n_columns(apps, schema_editor):
for lang in LANGUAGES:
_add_column_if_missing(
schema_editor,
"address_country",
f"printable_name_{lang}",
"varchar(128) NULL",
)
class Migration(migrations.Migration):
dependencies = [
("mandelstudio", "0005_checkout_i18n_columns"),
]
operations = [
migrations.RunPython(
add_address_i18n_columns,
migrations.RunPython.noop,
)
]

View File

@@ -18,6 +18,7 @@ ALLOWED_HOSTS = [
"www.mandelblog.com", "www.mandelblog.com",
"mandelblog.com", "mandelblog.com",
] ]
CANONICAL_BASE_URL = "https://www.mandelblog.com"
if "salt_target" in globals(): if "salt_target" in globals():
ALLOWED_HOSTS.append("mandelstudio.%s" % salt_target) # pylint: disable=E0602 ALLOWED_HOSTS.append("mandelstudio.%s" % salt_target) # pylint: disable=E0602
# pylint: disable=E0602 # pylint: disable=E0602

View File

@@ -1,5 +1,6 @@
from django.contrib.sitemaps.views import index as sitemap_index_view from django.contrib.sitemaps.views import index as sitemap_index_view
from django.contrib.sitemaps.views import sitemap as sitemap_section_view from django.contrib.sitemaps.views import sitemap as sitemap_section_view
from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from wagtail.models import Locale, Page from wagtail.models import Locale, Page
@@ -15,6 +16,13 @@ from ocyan.plugin.wagtail_oscar_integration.sitemap import (
) )
def _absolute_sitemap_url(request, path: str) -> str:
canonical_base = getattr(settings, "CANONICAL_BASE_URL", "").rstrip("/")
if canonical_base:
return f"{canonical_base}{path}"
return request.build_absolute_uri(path)
class WagtailSitemap(BaseWagtailSitemap): class WagtailSitemap(BaseWagtailSitemap):
def items(self): def items(self):
page_ids = [] page_ids = []
@@ -74,7 +82,7 @@ def sitemap_section(request, section=None):
def robots_txt(request): def robots_txt(request):
sitemap_url = request.build_absolute_uri("/sitemap.xml") sitemap_url = _absolute_sitemap_url(request, "/sitemap.xml")
content = "\n".join( content = "\n".join(
[ [
"User-agent: *", "User-agent: *",

File diff suppressed because one or more lines are too long

View File

@@ -227,49 +227,114 @@ body.cookie-consent-open {
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
header .header-inner > .container { header .header-inner > .container {
display: flex; display: flex !important;
flex-wrap: nowrap; flex-wrap: nowrap !important;
align-items: center; align-items: center !important;
column-gap: 0.375rem; justify-content: space-between !important;
width: 100%;
max-width: 100%;
gap: 0.5rem;
padding-top: 0.625rem; padding-top: 0.625rem;
padding-bottom: 0.625rem; padding-bottom: 0.625rem;
padding-left: max(10px, env(safe-area-inset-left, 0px));
padding-right: max(10px, env(safe-area-inset-right, 0px));
}
header .header-inner > .container > .search-wrapper,
header .header-inner > .container > .alert-messages-header {
display: none !important;
} }
header .header-inner > .container > .navbar-brand { header .header-inner > .container > .navbar-brand {
order: 1; order: 1 !important;
margin: 0; display: flex !important;
flex: 0 1 auto; align-items: center !important;
justify-content: flex-start !important;
flex: 1 1 auto !important;
min-width: 0;
margin: 0 !important;
padding: 0 !important;
}
header .header-inner > .container > .navbar-brand .logo.big_brand {
display: block;
margin: 0 !important;
max-width: none;
}
header .header-inner > .container > .navbar-brand picture {
display: block;
} }
header .header-inner > .container > .navbar-brand img { header .header-inner > .container > .navbar-brand img {
width: auto; display: block;
height: auto; width: auto !important;
max-height: 74px; height: auto !important;
max-width: 190px; max-width: min(45vw, 170px) !important;
max-height: 96px !important;
margin: 0 !important;
transform: none !important;
} }
header .header-inner > .container > .header-right { header .header-inner > .container > .header-right {
order: 2 !important;
margin: 0 0 0 auto !important;
padding: 0 !important;
display: flex !important;
flex-wrap: nowrap !important;
align-items: center !important;
justify-content: flex-end !important;
gap: 0.35rem;
flex: 0 0 auto !important;
}
header .header-right > .language-dropdown {
order: 4;
}
header .header-right > a[data-bs-target="#siteSearchModal"],
header .header-right > a.search-toggler {
order: 1;
}
header .header-right > a.user-button.menu-circle:not(.search-toggler):not([data-bs-target="#siteSearchModal"]) {
order: 2; order: 2;
margin-left: auto; }
flex: 0 0 auto;
gap: 0.375rem; header .header-right > .basket-dropdown {
order: 3;
}
header .header-right .user-button,
header .header-right .dropdown-toggle {
margin: 0 !important;
} }
header .header-right .language-dropdown, header .header-right .language-dropdown,
header .header-right .basket-dropdown, header .header-right .basket-dropdown,
header .header-right > a.user-button.menu-circle, header .header-right > a.user-button.menu-circle,
header .header-right .menu-circle { header .header-right .menu-circle {
width: 36px; width: 34px;
height: 36px; height: 34px;
min-width: 36px; min-width: 34px;
min-height: 36px; min-height: 34px;
flex-basis: 36px; flex: 0 0 34px;
}
header .header-right,
header .header-right .basket-dropdown,
header .header-right .menu-circle {
position: relative;
z-index: 100;
} }
header .header-inner > .container > .navbar-toggler { header .header-inner > .container > .navbar-toggler {
order: 3; order: 3;
margin-left: 0.125rem;
flex: 0 0 auto; flex: 0 0 auto;
margin: 0 !important;
position: fixed;
right: max(12px, env(safe-area-inset-right, 0px));
bottom: calc(12px + env(safe-area-inset-bottom, 0px));
} }
header .header-inner > .container > .navbar-collapse { header .header-inner > .container > .navbar-collapse {
@@ -336,16 +401,6 @@ body.cookie-consent-open {
border-bottom: 1px solid rgba(15, 23, 42, 0.08); border-bottom: 1px solid rgba(15, 23, 42, 0.08);
} }
header .header-right,
header .header-right .basket-dropdown,
header .header-right .menu-circle {
position: relative;
z-index: 100;
}
}
@media (max-width: 991.98px) {
body.mobile-menu-open { body.mobile-menu-open {
header .header-inner > .container > .navbar-brand { header .header-inner > .container > .navbar-brand {
opacity: 0; opacity: 0;
@@ -364,7 +419,30 @@ body.cookie-consent-open {
opacity: 0 !important; opacity: 0 !important;
pointer-events: none !important; pointer-events: none !important;
} }
}
}
@media (max-width: 374.98px) {
header .header-inner > .container {
gap: 0.25rem;
padding-left: max(8px, env(safe-area-inset-left, 0px));
padding-right: max(8px, env(safe-area-inset-right, 0px));
}
header .header-inner > .container > .navbar-brand img {
max-width: 140px !important;
max-height: 80px !important;
}
header .header-right .language-dropdown,
header .header-right .basket-dropdown,
header .header-right > a.user-button.menu-circle,
header .header-right .menu-circle {
width: 32px;
height: 32px;
min-width: 32px;
min-height: 32px;
flex: 0 0 32px;
} }
} }
@@ -412,3 +490,27 @@ body.cookie-consent-open {
} }
} }
} }
@media (max-width: 767.98px) {
.te-modern-saas .te-block--saas-hero-banner .saas-hero.saas-hero--split {
.saas-hero__stats {
width: 100% !important;
max-width: 100% !important;
gap: 0.625rem;
grid-template-columns: minmax(0, 1fr) !important;
}
.saas-hero__stat {
width: 100% !important;
min-width: 0 !important;
padding: 0.75rem 0.875rem;
}
.saas-hero__stat-value,
.saas-hero__stat-label {
white-space: normal !important;
overflow-wrap: break-word !important;
word-break: normal !important;
}
}
}

View File

@@ -106,7 +106,7 @@
</div> </div>
<div class="saas-demo__form-wrapper"> <div class="saas-demo__form-wrapper">
<form class="saas-demo__form" action="{% url "contact_form:contact-form-handler" %}" method="post"> <form id="contact-form" class="saas-demo__form" action="{% url "contact_form:contact-form-handler" %}" method="post">
{% csrf_token %} {% csrf_token %}
<div class="saas-demo__fields"> <div class="saas-demo__fields">
{% for field in self.form_fields %} {% for field in self.form_fields %}

View File

@@ -2,12 +2,41 @@ from __future__ import annotations
from django import template from django import template
from django.conf import settings from django.conf import settings
from urllib.parse import urlsplit, urlunsplit
from wagtail.models import Page from wagtail.models import Page
register = template.Library() register = template.Library()
def _canonical_base_url() -> str:
return getattr(settings, "CANONICAL_BASE_URL", "").rstrip("/")
def _with_canonical_host(url_or_path: str | None) -> str:
value = url_or_path or ""
base = _canonical_base_url()
if not base:
return value
if not value:
return f"{base}/"
if value.startswith("/"):
return f"{base}{value}"
parsed = urlsplit(value)
if parsed.scheme and parsed.netloc:
canonical = urlsplit(base)
return urlunsplit(
(
canonical.scheme,
canonical.netloc,
parsed.path,
parsed.query,
parsed.fragment,
)
)
return f"{base}/{value.lstrip('/')}"
def _normalize_language_code(language_code: str | None) -> str: def _normalize_language_code(language_code: str | None) -> str:
return (language_code or settings.LANGUAGE_CODE).split("-")[0] return (language_code or settings.LANGUAGE_CODE).split("-")[0]
@@ -41,10 +70,10 @@ def _translated_pages(page):
def _build_absolute_url(request, path: str | None, page=None) -> str: def _build_absolute_url(request, path: str | None, page=None) -> str:
if path and request is not None: if path and request is not None:
return request.build_absolute_uri(path) return _with_canonical_host(request.build_absolute_uri(path))
if page is not None: if page is not None:
return getattr(page, "full_url", "") or path or "" return _with_canonical_host(getattr(page, "full_url", "") or path or "")
return path or "" return _with_canonical_host(path or "")
@register.simple_tag @register.simple_tag
@@ -89,7 +118,7 @@ def page_canonical_url(context):
if page is not None and getattr(page, "url", None): if page is not None and getattr(page, "url", None):
return _build_absolute_url(request, page.url, page) return _build_absolute_url(request, page.url, page)
if request is not None: if request is not None:
return request.build_absolute_uri() return _with_canonical_host(request.build_absolute_uri())
return "" return ""

View File

@@ -1,5 +1,5 @@
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.urls import path from django.urls import include, path
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from ocyan.core.fender import config from ocyan.core.fender import config
@@ -7,6 +7,9 @@ from ocyan.main.urls import urlpatterns as ocyan_urlpatterns
from ocyan.plugin.contact_form.entrypoint import SHOP_BASE_URL from ocyan.plugin.contact_form.entrypoint import SHOP_BASE_URL
from ocyan.plugin.wagtail_oscar_integration.constants import CACHE_DURATION from ocyan.plugin.wagtail_oscar_integration.constants import CACHE_DURATION
from ocyan.plugin.wordspinner.views.ai_search import ai_search_view from ocyan.plugin.wordspinner.views.ai_search import ai_search_view
from ocyan.plugin.wordspinner.views.bulk import BulkAIOperationsView
from ocyan.plugin.wordspinner.views.bulk import ProcessNextBulkAIJobView
from ocyan.plugin.wordspinner.views.results import GeneratedResultsListView
from ocyan.plugin.contact_form.views import post_contact_form from ocyan.plugin.contact_form.views import post_contact_form
@@ -36,6 +39,13 @@ contact_form_urlpatterns = [
), ),
] ]
wordspinner_urlpatterns = [
path(
"wordspinner/",
include(("ocyan.plugin.wordspinner.urls", "wordspinner"), namespace="wordspinner"),
),
]
# Ensure public AI search routes are resolved before Wagtail catch-all URLs. # Ensure public AI search routes are resolved before Wagtail catch-all URLs.
ai_search_urlpatterns = [ ai_search_urlpatterns = [
path("ai-search/", ai_search_view, name="wordspinner_ai_search_public"), path("ai-search/", ai_search_view, name="wordspinner_ai_search_public"),
@@ -56,13 +66,38 @@ ai_search_urlpatterns = [
), ),
] ]
wordspinner_i18n_aliases = [
path(
"<str:lang_code>/wordspinner/ai/bulk/",
BulkAIOperationsView.as_view(),
name="wordspinner_bulk_ai_operations_i18n",
),
path(
"<str:lang_code>/wordspinner/ai/bulk/process-next/",
ProcessNextBulkAIJobView.as_view(),
name="wordspinner_bulk_ai_process_next_i18n",
),
path(
"<str:lang_code>/wordspinner/results/",
GeneratedResultsListView.as_view(),
name="wordspinner_generated_results_i18n",
),
]
if config.i18n_enabled: if config.i18n_enabled:
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
*wordspinner_urlpatterns,
*contact_form_urlpatterns, *contact_form_urlpatterns,
*ai_search_urlpatterns, *ai_search_urlpatterns,
*wordspinner_i18n_aliases,
prefix_default_language=False, prefix_default_language=False,
) )
else: else:
urlpatterns += contact_form_urlpatterns + ai_search_urlpatterns urlpatterns += (
wordspinner_urlpatterns
+ contact_form_urlpatterns
+ ai_search_urlpatterns
+ wordspinner_i18n_aliases
)
urlpatterns += ocyan_urlpatterns urlpatterns += ocyan_urlpatterns