Improve public SEO and align chrome with EXIT Realty teal.
Add meta/canonical/OG, robots/sitemap, and contact-form admin email; restyle consent, icons, and scroll-top away from theme blue.
This commit is contained in:
@@ -8,11 +8,15 @@ def _phone_tel(phone: str) -> str:
|
||||
return digits
|
||||
|
||||
|
||||
def site_branding(_request):
|
||||
def site_branding(request):
|
||||
phone = settings.CONTACT_PHONE or ""
|
||||
public_site_url = (getattr(settings, "PUBLIC_SITE_URL", None) or "").rstrip("/")
|
||||
if not public_site_url and getattr(request, "build_absolute_uri", None):
|
||||
public_site_url = request.build_absolute_uri("/").rstrip("/")
|
||||
return {
|
||||
"SITE_NAME": settings.SITE_NAME,
|
||||
"SITE_TAGLINE": settings.SITE_TAGLINE,
|
||||
"PUBLIC_SITE_URL": public_site_url,
|
||||
"CONTACT_PHONE": phone,
|
||||
"CONTACT_PHONE_TEL": _phone_tel(phone),
|
||||
"CONTACT_EMAIL": settings.CONTACT_EMAIL or "",
|
||||
|
||||
@@ -21,6 +21,8 @@ class UnderConstructionMiddleware:
|
||||
"/portal/messaging/webhooks/",
|
||||
"/unsubscribe/",
|
||||
"/api/address-suggest",
|
||||
"/robots.txt",
|
||||
"/sitemap.xml",
|
||||
)
|
||||
|
||||
def __init__(self, get_response):
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Outbound notifications for public-site events."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMessage
|
||||
from django.urls import reverse
|
||||
|
||||
from leads.models import Lead
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def notify_admins_of_contact_form(lead: Lead) -> bool:
|
||||
"""
|
||||
Email CONTACT_EMAIL when someone submits the public contact form.
|
||||
|
||||
Returns True if a message was sent. Failures are logged; callers should
|
||||
not block the visitor success path on delivery errors.
|
||||
"""
|
||||
to_email = (settings.CONTACT_EMAIL or "").strip()
|
||||
if not to_email:
|
||||
logger.warning("CONTACT_EMAIL unset; skipping contact-form notification")
|
||||
return False
|
||||
|
||||
contact = lead.contact
|
||||
name = contact.full_name or "(no name)"
|
||||
phone = contact.phone or "(none)"
|
||||
email = contact.email or "(none)"
|
||||
message = (lead.message or "").strip() or "(no message)"
|
||||
|
||||
site = (settings.PUBLIC_SITE_URL or "").rstrip("/")
|
||||
portal_path = reverse("leads:detail", kwargs={"pk": lead.pk})
|
||||
portal_url = f"{site}{portal_path}" if site else portal_path
|
||||
|
||||
postal = contact.postal_address or {}
|
||||
address_bits = [
|
||||
postal.get("line1") or "",
|
||||
postal.get("line2") or "",
|
||||
", ".join(
|
||||
part
|
||||
for part in [
|
||||
postal.get("city") or "",
|
||||
postal.get("state") or "",
|
||||
postal.get("zip") or "",
|
||||
]
|
||||
if part
|
||||
),
|
||||
]
|
||||
address = "\n".join(bit for bit in address_bits if bit) or "(none)"
|
||||
|
||||
body = (
|
||||
f"New contact form inquiry from {name}.\n\n"
|
||||
f"Name: {name}\n"
|
||||
f"Email: {email}\n"
|
||||
f"Phone: {phone}\n"
|
||||
f"Address:\n{address}\n\n"
|
||||
f"Message:\n{message}\n\n"
|
||||
f"View in portal: {portal_url}\n"
|
||||
)
|
||||
|
||||
mail = EmailMessage(
|
||||
subject=f"New contact form inquiry from {name}",
|
||||
body=body,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[to_email],
|
||||
reply_to=[contact.email] if contact.email else None,
|
||||
)
|
||||
try:
|
||||
mail.send(fail_silently=False)
|
||||
except Exception:
|
||||
logger.exception("Failed to send contact-form notification to %s", to_email)
|
||||
return False
|
||||
return True
|
||||
@@ -1,13 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}About · {{ SITE_NAME }}{% endblock %}
|
||||
{% block title %}About {{ SITE_NAME }} · EXIT Realty Realtor{% endblock %}
|
||||
{% block og_title %}About {{ SITE_NAME }} · EXIT Realty Realtor{% endblock %}
|
||||
{% block twitter_title %}About {{ SITE_NAME }} · EXIT Realty Realtor{% endblock %}
|
||||
{% block meta_description %}Meet {{ SITE_NAME }}, a local EXIT Realty realtor helping families buy and sell homes with clear communication and marketing that reaches people. {{ CONTACT_SERVICE_AREA }}.{% endblock %}
|
||||
{% block og_description %}Meet {{ SITE_NAME }}, a local EXIT Realty realtor helping families buy and sell homes with clear communication and marketing that reaches people. {{ CONTACT_SERVICE_AREA }}.{% endblock %}
|
||||
{% block twitter_description %}Meet {{ SITE_NAME }}, a local EXIT Realty realtor helping families buy and sell homes with clear communication and marketing that reaches people. {{ CONTACT_SERVICE_AREA }}.{% endblock %}
|
||||
{% block og_image %}
|
||||
<meta property="og:image" content="{{ PUBLIC_SITE_URL }}{% static 'images/careers-1-570x388.jpg' %}">
|
||||
<meta property="og:image:alt" content="{{ SITE_NAME }}">
|
||||
{% endblock %}
|
||||
{% block structured_data %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "AboutPage",
|
||||
"name": "About {{ SITE_NAME|escapejs }}",
|
||||
"url": "{{ PUBLIC_SITE_URL }}{% url 'public:about' %}",
|
||||
"description": "Meet {{ SITE_NAME|escapejs }}, a local EXIT Realty realtor helping families buy and sell homes with clear communication and marketing that reaches people.",
|
||||
"mainEntity": {
|
||||
"@type": "RealEstateAgent",
|
||||
"name": "{{ SITE_NAME|escapejs }}",
|
||||
"url": "{{ PUBLIC_SITE_URL }}/",
|
||||
"telephone": "{{ CONTACT_PHONE|escapejs }}",
|
||||
"email": "{{ CONTACT_EMAIL|escapejs }}",
|
||||
"areaServed": "{{ CONTACT_SERVICE_AREA|escapejs }}",
|
||||
"worksFor": {
|
||||
"@type": "Organization",
|
||||
"name": "EXIT Realty Redefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<section class="breadcrumbs-custom bg-image context-dark" style="background-image: url({% static 'images/breadcrumbs-image-1.jpg' %});">
|
||||
<div class="breadcrumbs-custom-inner">
|
||||
<div class="container breadcrumbs-custom-container">
|
||||
<div class="breadcrumbs-custom-main">
|
||||
<h6 class="breadcrumbs-custom-subtitle title-decorated">About</h6>
|
||||
<h2 class="text-uppercase breadcrumbs-custom-title">Meet Monica</h2>
|
||||
<p class="breadcrumbs-custom-subtitle title-decorated">About</p>
|
||||
<h1 class="text-uppercase breadcrumbs-custom-title">Meet Monica</h1>
|
||||
</div>
|
||||
<ul class="breadcrumbs-custom-path">
|
||||
<li><a href="{% url 'public:home' %}">Home</a></li>
|
||||
@@ -20,13 +52,13 @@
|
||||
<div class="container">
|
||||
<div class="row row-50 justify-content-center justify-content-lg-between flex-lg-row-reverse">
|
||||
<div class="col-md-10 col-lg-6 col-xl-5">
|
||||
<h3 class="text-uppercase">Local realtor,<br>personal service</h3>
|
||||
<h2 class="text-uppercase">Local realtor,<br>personal service</h2>
|
||||
<p class="about-subtitle">I help families buy and sell homes with clear communication and marketing that actually reaches people — not just another postcard blast.</p>
|
||||
<p>This site replaces a costly third-party campaign platform. Visitors reach me through a protected contact form; every inquiry lands in my portal with source tracking so I know which campaigns work.</p>
|
||||
<a class="button button-lg button-primary button-winona" href="{% url 'public:contact' %}" data-tianji-event="about_cta" data-tianji-event-destination="contact">Work with me</a>
|
||||
</div>
|
||||
<div class="col-md-10 col-lg-6 col-xl-6">
|
||||
<img class="img-responsive" src="{% static 'images/careers-1-570x388.jpg' %}" alt="{{ SITE_NAME }}" width="570" height="388"/>
|
||||
<img class="img-responsive" src="{% static 'images/careers-1-570x388.jpg' %}" alt="{{ SITE_NAME }}, EXIT Realty realtor" width="570" height="388"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Contact · {{ SITE_NAME }}{% endblock %}
|
||||
{% block title %}Contact {{ SITE_NAME }} · Get in Touch{% endblock %}
|
||||
{% block og_title %}Contact {{ SITE_NAME }} · Get in Touch{% endblock %}
|
||||
{% block twitter_title %}Contact {{ SITE_NAME }} · Get in Touch{% endblock %}
|
||||
{% block meta_description %}Contact {{ SITE_NAME }} at EXIT Realty about buying or selling a home. {{ CONTACT_SERVICE_AREA }}. Call{% if CONTACT_PHONE %} {{ CONTACT_PHONE }}{% endif %}{% if CONTACT_EMAIL %} or email {{ CONTACT_EMAIL }}{% endif %}.{% endblock %}
|
||||
{% block og_description %}Contact {{ SITE_NAME }} at EXIT Realty about buying or selling a home. {{ CONTACT_SERVICE_AREA }}. Call{% if CONTACT_PHONE %} {{ CONTACT_PHONE }}{% endif %}{% if CONTACT_EMAIL %} or email {{ CONTACT_EMAIL }}{% endif %}.{% endblock %}
|
||||
{% block twitter_description %}Contact {{ SITE_NAME }} at EXIT Realty about buying or selling a home. {{ CONTACT_SERVICE_AREA }}. Call{% if CONTACT_PHONE %} {{ CONTACT_PHONE }}{% endif %}{% if CONTACT_EMAIL %} or email {{ CONTACT_EMAIL }}{% endif %}.{% endblock %}
|
||||
{% block structured_data %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ContactPage",
|
||||
"name": "Contact {{ SITE_NAME|escapejs }}",
|
||||
"url": "{{ PUBLIC_SITE_URL }}{% url 'public:contact' %}",
|
||||
"description": "Contact {{ SITE_NAME|escapejs }} at EXIT Realty about buying or selling a home. {{ CONTACT_SERVICE_AREA|escapejs }}.",
|
||||
"mainEntity": {
|
||||
"@type": "RealEstateAgent",
|
||||
"name": "{{ SITE_NAME|escapejs }}",
|
||||
"url": "{{ PUBLIC_SITE_URL }}/",
|
||||
"telephone": "{{ CONTACT_PHONE|escapejs }}",
|
||||
"email": "{{ CONTACT_EMAIL|escapejs }}",
|
||||
"areaServed": "{{ CONTACT_SERVICE_AREA|escapejs }}",
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"contactType": "customer service",
|
||||
"telephone": "{{ CONTACT_PHONE|escapejs }}",
|
||||
"email": "{{ CONTACT_EMAIL|escapejs }}",
|
||||
"areaServed": "{{ CONTACT_SERVICE_AREA|escapejs }}",
|
||||
"availableLanguage": "English"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<section class="breadcrumbs-custom bg-image context-dark" style="background-image: url({% static 'images/breadcrumbs-image-1.jpg' %});">
|
||||
<div class="breadcrumbs-custom-inner">
|
||||
<div class="container breadcrumbs-custom-container">
|
||||
<div class="breadcrumbs-custom-main">
|
||||
<h6 class="breadcrumbs-custom-subtitle title-decorated">Contact</h6>
|
||||
<h2 class="text-uppercase breadcrumbs-custom-title">Get in touch</h2>
|
||||
<p class="breadcrumbs-custom-subtitle title-decorated">Contact</p>
|
||||
<h1 class="text-uppercase breadcrumbs-custom-title">Get in touch</h1>
|
||||
</div>
|
||||
<ul class="breadcrumbs-custom-path">
|
||||
<li><a href="{% url 'public:home' %}">Home</a></li>
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Home · {{ SITE_NAME }}{% endblock %}
|
||||
{% block title %}{{ SITE_NAME }} · EXIT Realty | {{ CONTACT_SERVICE_AREA }}{% endblock %}
|
||||
{% block og_title %}{{ SITE_NAME }} · EXIT Realty | {{ CONTACT_SERVICE_AREA }}{% endblock %}
|
||||
{% block twitter_title %}{{ SITE_NAME }} · EXIT Realty | {{ CONTACT_SERVICE_AREA }}{% endblock %}
|
||||
{% block meta_description %}{{ SITE_NAME }}, EXIT Realty — buying and selling homes with clear advice and multi-channel marketing. {{ CONTACT_SERVICE_AREA }}.{% endblock %}
|
||||
{% block og_description %}{{ SITE_NAME }}, EXIT Realty — buying and selling homes with clear advice and multi-channel marketing. {{ CONTACT_SERVICE_AREA }}.{% endblock %}
|
||||
{% block twitter_description %}{{ SITE_NAME }}, EXIT Realty — buying and selling homes with clear advice and multi-channel marketing. {{ CONTACT_SERVICE_AREA }}.{% endblock %}
|
||||
{% block structured_data %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "RealEstateAgent",
|
||||
"name": "{{ SITE_NAME|escapejs }}",
|
||||
"url": "{{ PUBLIC_SITE_URL }}/",
|
||||
"image": "{{ PUBLIC_SITE_URL }}{% static 'images/slider-minimal-slide-1-1920x968.jpg' %}",
|
||||
"telephone": "{{ CONTACT_PHONE|escapejs }}",
|
||||
"email": "{{ CONTACT_EMAIL|escapejs }}",
|
||||
"description": "{{ SITE_NAME|escapejs }}, EXIT Realty — buying and selling homes with clear advice and multi-channel marketing. {{ CONTACT_SERVICE_AREA|escapejs }}.",
|
||||
"areaServed": "{{ CONTACT_SERVICE_AREA|escapejs }}",
|
||||
"worksFor": {
|
||||
"@type": "Organization",
|
||||
"name": "EXIT Realty Redefined"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="swiper-container swiper-slider swiper-slider-minimal" data-loop="true" data-slide-effect="fade" data-autoplay="false" data-simulate-touch="true">
|
||||
<div class="swiper-wrapper">
|
||||
@@ -24,7 +48,7 @@
|
||||
<div class="wow-outer">
|
||||
<div class="title-docor-text font-weight-bold title-decorated text-uppercase wow slideInLeft text-white">Local Market Insight</div>
|
||||
</div>
|
||||
<h1 class="text-uppercase text-white font-weight-bold wow-outer"><span class="wow slideInDown" data-wow-delay=".2s">Sell With<br>Confidence</span></h1>
|
||||
<h2 class="text-uppercase text-white font-weight-bold wow-outer"><span class="wow slideInDown" data-wow-delay=".2s">Sell With<br>Confidence</span></h2>
|
||||
<p class="text-white wow-outer"><span class="wow slideInDown" data-wow-delay=".35s">Pricing strategy, staging guidance, and multi-channel outreach so the right buyers see your listing.</span></p>
|
||||
<div class="wow-outer button-outer">
|
||||
<a class="button button-md button-primary button-winona wow slideInDown" href="{% url 'public:about' %}" data-wow-delay=".4s" data-tianji-event="hero_about">Meet Monica</a>
|
||||
@@ -39,13 +63,13 @@
|
||||
</div>
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<h3 class="text-uppercase text-center wow-outer"><span class="wow slideInDown">How I can help</span></h3>
|
||||
<h2 class="text-uppercase text-center wow-outer"><span class="wow slideInDown">How I can help</span></h2>
|
||||
<div class="row row-50">
|
||||
<div class="col-md-4 wow-outer">
|
||||
<article class="box-chloe wow slideInUp">
|
||||
<div class="box-chloe__icon linearicons-home-icon3"></div>
|
||||
<div class="box-chloe__main">
|
||||
<h4 class="box-chloe__title">Buyers</h4>
|
||||
<h3 class="box-chloe__title">Buyers</h3>
|
||||
<p>Neighborhood tours, offer strategy, and negotiation that protects your timeline and budget.</p>
|
||||
</div>
|
||||
</article>
|
||||
@@ -54,7 +78,7 @@
|
||||
<article class="box-chloe wow slideInUp" data-wow-delay=".05s">
|
||||
<div class="box-chloe__icon linearicons-apartment"></div>
|
||||
<div class="box-chloe__main">
|
||||
<h4 class="box-chloe__title">Sellers</h4>
|
||||
<h3 class="box-chloe__title">Sellers</h3>
|
||||
<p>Listing prep, professional marketing, and outreach across email, SMS, postcard, and social.</p>
|
||||
</div>
|
||||
</article>
|
||||
@@ -63,7 +87,7 @@
|
||||
<article class="box-chloe wow slideInUp" data-wow-delay=".1s">
|
||||
<div class="box-chloe__icon linearicons-chart-growth"></div>
|
||||
<div class="box-chloe__main">
|
||||
<h4 class="box-chloe__title">Market guidance</h4>
|
||||
<h3 class="box-chloe__title">Market guidance</h3>
|
||||
<p>Honest comps and timing advice — whether you move this season or plan ahead.</p>
|
||||
</div>
|
||||
</article>
|
||||
@@ -73,7 +97,7 @@
|
||||
</section>
|
||||
<section class="section section-lg bg-gray-100 text-center">
|
||||
<div class="container">
|
||||
<h3 class="text-uppercase wow-outer"><span class="wow slideInUp">Ready when you are</span></h3>
|
||||
<h2 class="text-uppercase wow-outer"><span class="wow slideInUp">Ready when you are</span></h2>
|
||||
<p class="wow-outer"><span class="text-width-1 wow slideInDown">Tell me what you are looking for. I will follow up personally — your message becomes a lead in my private portal.</span></p>
|
||||
<a class="button button-lg button-primary button-winona" href="{% url 'public:contact' %}" data-tianji-event="home_cta_contact">Contact Monica</a>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,8 @@ urlpatterns = [
|
||||
path("", views.home, name="home"),
|
||||
path("about/", views.about, name="about"),
|
||||
path("contact/", views.contact, name="contact"),
|
||||
path("robots.txt", views.robots_txt, name="robots_txt"),
|
||||
path("sitemap.xml", views.sitemap_xml, name="sitemap_xml"),
|
||||
path("under-construction/", views.under_construction, name="under_construction"),
|
||||
path("unsubscribe/<str:token>/", views.unsubscribe, name="unsubscribe"),
|
||||
path(
|
||||
|
||||
+57
-1
@@ -1,8 +1,10 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.http import require_GET, require_http_methods
|
||||
|
||||
from analytics.services import attribute_lead_from_request
|
||||
from contacts.models import Channel, ConsentRecord, Contact
|
||||
@@ -15,6 +17,14 @@ from messaging.services import (
|
||||
unsubscribe_all,
|
||||
)
|
||||
from public.forms import ContactForm, NotifyForm
|
||||
from public.notifications import notify_admins_of_contact_form
|
||||
|
||||
|
||||
def _public_site_base(request) -> str:
|
||||
base = (settings.PUBLIC_SITE_URL or "").rstrip("/")
|
||||
if base:
|
||||
return base
|
||||
return request.build_absolute_uri("/").rstrip("/")
|
||||
|
||||
|
||||
def home(request):
|
||||
@@ -25,6 +35,51 @@ def about(request):
|
||||
return render(request, "public/about.html")
|
||||
|
||||
|
||||
@require_GET
|
||||
def robots_txt(request):
|
||||
site = _public_site_base(request)
|
||||
body = "\n".join(
|
||||
[
|
||||
"User-agent: *",
|
||||
"Allow: /",
|
||||
"Disallow: /portal/",
|
||||
"Disallow: /accounts/",
|
||||
"Disallow: /admin/",
|
||||
"Disallow: /api/",
|
||||
f"Sitemap: {site}/sitemap.xml",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return HttpResponse(body, content_type="text/plain; charset=utf-8")
|
||||
|
||||
|
||||
@require_GET
|
||||
def sitemap_xml(request):
|
||||
site = _public_site_base(request)
|
||||
paths = [
|
||||
("public:home", "1.0", "weekly"),
|
||||
("public:about", "0.8", "monthly"),
|
||||
("public:contact", "0.9", "monthly"),
|
||||
]
|
||||
urls = []
|
||||
for name, priority, changefreq in paths:
|
||||
path = reverse(name)
|
||||
urls.append(
|
||||
" <url>\n"
|
||||
f" <loc>{site}{path}</loc>\n"
|
||||
f" <changefreq>{changefreq}</changefreq>\n"
|
||||
f" <priority>{priority}</priority>\n"
|
||||
" </url>"
|
||||
)
|
||||
body = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
||||
+ "\n".join(urls)
|
||||
+ "\n</urlset>\n"
|
||||
)
|
||||
return HttpResponse(body, content_type="application/xml; charset=utf-8")
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def contact(request):
|
||||
if request.method == "POST":
|
||||
@@ -72,6 +127,7 @@ def contact(request):
|
||||
status=Lead.Status.NEW,
|
||||
)
|
||||
attribute_lead_from_request(request, lead)
|
||||
notify_admins_of_contact_form(lead)
|
||||
messages.success(request, "Thanks — Monica will be in touch soon.")
|
||||
return redirect(f"{reverse('public:contact')}?sent=1")
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user