From 3ea2cfde0efc00a03c56a9ae0de61ef7c0653aa6 Mon Sep 17 00:00:00 2001 From: Ryan Westfall Date: Tue, 14 Jul 2026 07:17:31 -0500 Subject: [PATCH] Improve SEO and agentic browsing for public pages (#18). Add per-page titles/meta/Open Graph, semantic landmarks, robots/sitemap/llms.txt, and accessibility fixes so humans and agents can discover and navigate the site reliably. --- schasite/static/css/style2.css | 21 +++++- schasite/templates/schasite/about_us2.html | 33 ++++++--- schasite/templates/schasite/base2.html | 32 +++++++-- schasite/templates/schasite/calendar2.html | 20 ++++-- schasite/templates/schasite/dues2.html | 18 +++-- .../templates/schasite/dues_cancelled.html | 22 +++--- schasite/templates/schasite/dues_success.html | 22 +++--- schasite/templates/schasite/index2.html | 10 ++- schasite/templates/schasite/llms.txt | 32 +++++++++ .../templates/schasite/membership_form2.html | 70 +++++++++++-------- schasite/templates/schasite/newsletters2.html | 18 ++++- schasite/templates/schasite/robots.txt | 18 +++++ schasite/templates/schasite/scha_board2.html | 16 +++-- schasite/templates/schasite/sitemap.xml | 10 +++ .../templates/schasite/useful_links2.html | 20 ++++-- schasite/tests.py | 43 ++++++++++++ schasite/urls.py | 3 + schasite/views.py | 50 +++++++++++++ 18 files changed, 367 insertions(+), 91 deletions(-) create mode 100644 schasite/templates/schasite/llms.txt create mode 100644 schasite/templates/schasite/robots.txt create mode 100644 schasite/templates/schasite/sitemap.xml diff --git a/schasite/static/css/style2.css b/schasite/static/css/style2.css index dc99fdf..7bd6a71 100644 --- a/schasite/static/css/style2.css +++ b/schasite/static/css/style2.css @@ -10,9 +10,28 @@ section { scroll-margin-top: 70px; } +/* Skip link visible when focused */ +.visually-hidden-focusable:focus { + z-index: 1080; +} + +/* Reserve image space to reduce CLS */ +.card-img-top { + aspect-ratio: 16 / 9; + object-fit: cover; + width: 100%; + height: auto; +} + +img.img-fluid { + max-width: 100%; + height: auto; +} + /* Hero Section */ #home { background-color: rgba(var(--bs-success-rgb), 0.1); + min-height: 280px; } /* News and Contact Sections */ @@ -43,4 +62,4 @@ section { .was-validated .form-control:valid, .form-control.is-valid { border-color: #198754; -} \ No newline at end of file +} diff --git a/schasite/templates/schasite/about_us2.html b/schasite/templates/schasite/about_us2.html index 1cbaa92..9260165 100644 --- a/schasite/templates/schasite/about_us2.html +++ b/schasite/templates/schasite/about_us2.html @@ -2,16 +2,31 @@ {% load static %} {% block pagetitle %} -Stonehedge Community Homeowners Association +About Us | Stonehedge Community Homeowners Association {% endblock %} +{% block meta_tags %} + +{% endblock %} +{% block og_title %}About Us | Stonehedge Community Homeowners Association{% endblock %} +{% block og_description %}Learn about Stonehedge in Wheaton, IL — local schools, community history, shopping and dining nearby, and parks that serve residents.{% endblock %} +{% block twitter_title %}About Us | Stonehedge Community Homeowners Association{% endblock %} +{% block twitter_description %}Learn about Stonehedge in Wheaton, IL — local schools, community history, shopping and dining nearby, and parks that serve residents.{% endblock %} + {% block content %} +
+
+

About Stonehedge

+

Schools, history, nearby amenities, and parks serving our Wheaton community.

+
+
+
- Local Schools + Community Unit School District 200 logo

Excellent Schools Serving Our Community

@@ -127,7 +142,7 @@
- Community History + Historic view of the Stonehedge community

Our Rich History

@@ -161,7 +176,7 @@
- Greenwood Mall + Danada Square shopping area
Danada

1.0 miles from community

@@ -174,7 +189,7 @@
- Farmers Market + Downtown Wheaton streetscape
Downtown Wheaton

2.7 miles from community

@@ -188,7 +203,7 @@
- Farmers Market + Wheaton Farmers Market stalls
Wheaton Farmersmarket

2.8 miles from community

@@ -201,7 +216,7 @@
- Dining District + Downtown Naperville dining district
Downtown Naperville

6.2 miles from community

@@ -227,7 +242,7 @@
- Community Park + Brighton Park playground and green space
@@ -249,7 +264,7 @@
- Nature Preserve + Seven Gables Park nature preserve
diff --git a/schasite/templates/schasite/base2.html b/schasite/templates/schasite/base2.html index 9f68ab3..bd3d21b 100644 --- a/schasite/templates/schasite/base2.html +++ b/schasite/templates/schasite/base2.html @@ -5,7 +5,22 @@ {% block pagetitle %} + Stonehedge Community Homeowners Association | Wheaton, IL {% endblock %} + {% block meta_tags %} + + {% endblock %} + + + + + + + + + + + @@ -16,12 +31,13 @@ - + Skip to main content + -
{% endblock %} diff --git a/schasite/tests.py b/schasite/tests.py index a8b0bed..70fc9ff 100644 --- a/schasite/tests.py +++ b/schasite/tests.py @@ -89,6 +89,49 @@ class PublicPageTests(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, "City of Wheaton") + def test_public_pages_include_seo_metadata(self): + pages = [ + ("index2", "Home | Stonehedge"), + ("about_us2", "About Us | Stonehedge"), + ("calendar2", "Calendar | Stonehedge"), + ("dues2", "Pay Dues | Stonehedge"), + ("membership_form2", "Join Today | Stonehedge"), + ("scha_board2", "SCHA Board | Stonehedge"), + ("useful_links2", "Useful Links | Stonehedge"), + ] + + for name, title_fragment in pages: + with self.subTest(page=name): + response = self.client.get(reverse(name)) + self.assertEqual(response.status_code, 200) + self.assertContains(response, f"{title_fragment}") + self.assertContains(response, 'name="description"') + self.assertContains(response, 'property="og:title"') + self.assertContains(response, 'rel="canonical"') + self.assertContains(response, 'id="main-content"') + self.assertContains(response, "Skip to main content") + + def test_robots_txt(self): + response = self.client.get(reverse("robots_txt")) + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"].split(";")[0], "text/plain") + self.assertContains(response, "Sitemap:") + self.assertContains(response, "Disallow: /admin/") + + def test_sitemap_xml(self): + response = self.client.get(reverse("sitemap_xml")) + self.assertEqual(response.status_code, 200) + self.assertIn("xml", response["Content-Type"]) + self.assertContains(response, reverse("index2")) + self.assertContains(response, reverse("dues2")) + + def test_llms_txt(self): + response = self.client.get(reverse("llms_txt")) + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"].split(";")[0], "text/plain") + self.assertContains(response, "Stonehedge Community Homeowners Association") + self.assertContains(response, reverse("membership_form2").lstrip("/")) + @override_settings( RECAPTCHA_PUBLIC_KEY="test-public-key", diff --git a/schasite/urls.py b/schasite/urls.py index 5fcec05..3cd779e 100644 --- a/schasite/urls.py +++ b/schasite/urls.py @@ -38,6 +38,9 @@ urlpatterns = authenticated_views + [ path("membership_form", views.membership_form2, name="membership_form2"), path("scha_board", views.scha_board2, name="scha_board2"), path("useful_links", views.useful_links2, name="useful_links2"), + path("robots.txt", views.robots_txt, name="robots_txt"), + path("sitemap.xml", views.sitemap_xml, name="sitemap_xml"), + path("llms.txt", views.llms_txt, name="llms_txt"), # stripe specific urls below path("config/", views.stripe_config), path("create-checkout-session/", views.create_checkout_session), diff --git a/schasite/views.py b/schasite/views.py index eb59481..69b8076 100644 --- a/schasite/views.py +++ b/schasite/views.py @@ -18,6 +18,7 @@ from django.http.response import JsonResponse, HttpResponse from django.views.decorators.csrf import csrf_exempt # new from django.contrib.auth import logout, authenticate, login from django.contrib.auth.decorators import login_required +from django.urls import reverse import stripe import logging @@ -122,6 +123,55 @@ def useful_links(request): return render(request, "schasite/useful_links.html", {"links": useful_links}) +PUBLIC_SITEMAP_ROUTES = ( + ("index2", "weekly", "1.0"), + ("about_us2", "monthly", "0.8"), + ("calendar2", "weekly", "0.8"), + ("dues2", "monthly", "0.9"), + ("membership_form2", "monthly", "0.9"), + ("scha_board2", "monthly", "0.7"), + ("useful_links2", "monthly", "0.6"), + ("newsletters2", "monthly", "0.5"), +) + + +def robots_txt(request): + sitemap_url = request.build_absolute_uri(reverse("sitemap_xml")) + return render( + request, + "schasite/robots.txt", + {"sitemap_url": sitemap_url}, + content_type="text/plain", + ) + + +def sitemap_xml(request): + urls = [ + { + "loc": request.build_absolute_uri(reverse(name)), + "changefreq": changefreq, + "priority": priority, + } + for name, changefreq, priority in PUBLIC_SITEMAP_ROUTES + ] + return render( + request, + "schasite/sitemap.xml", + {"urls": urls}, + content_type="application/xml", + ) + + +def llms_txt(request): + site_root = request.build_absolute_uri("/") + return render( + request, + "schasite/llms.txt", + {"site_root": site_root}, + content_type="text/plain; charset=utf-8", + ) + + def index2(request): return render(request, "schasite/index2.html", {}) -- 2.54.0