Improve Lighthouse Agentic Browsing readiness (#8)
Deploy Company Site / test (push) Successful in 10s
Deploy Company Site / deploy (push) Successful in 5s

## Summary

- Add `robots.txt`, `sitemap.xml`, and `llms.txt` endpoints so crawlers and AI agents can discover public pages
- Fix accessibility tree issues: contact form labels, semantic nav/profile dropdown buttons, cookie consent dialog ARIA, and dead `href="#"` links
- Reduce layout shift on homepage/contact via hero min-heights, trusted-by marquee sizing, and fixed cookie banner
- Add regression tests and `docs/agentic-browsing.md` for Lighthouse agentic-browsing audits

Closes #5. Also addresses overlap with #3 (sitemap + robots.txt).

## Test plan

- [x] `python manage.py test public.tests` (14 tests pass)
- [ ] Verify `GET /robots.txt`, `/sitemap.xml`, `/llms.txt` return 200 in staging/prod
- [ ] Confirm contact form labels visible and submittable on `/contact`
- [ ] Run Lighthouse `--only-categories=agentic-browsing` on homepage and contact page
- [ ] Smoke test mobile nav dropdown and cookie consent banner

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-07-02 11:36:54 +00:00
parent c8574d76d4
commit 846f1b8a37
14 changed files with 436 additions and 44 deletions
+45
View File
@@ -0,0 +1,45 @@
# Agentic Browsing Readiness
This site targets Google's experimental **Agentic Browsing** Lighthouse category (v13.3+), which checks whether AI agents can read, navigate, and act on public pages.
PageSpeed Insights does not yet expose this category. Run audits locally:
```bash
npx lighthouse@latest https://aimloperations.com \
--only-categories=agentic-browsing \
--chrome-flags="--enable-experimental-web-platform-features" \
--output=html --output-path=agentic-browsing-report.html
```
Test at minimum:
- `/` (homepage)
- `/contact` (primary conversion page)
## What we ship
| Check | Implementation |
|-------|----------------|
| **llms.txt** | `GET /llms.txt` — machine-readable site summary |
| **robots.txt** | `GET /robots.txt` — crawl rules + sitemap reference |
| **sitemap.xml** | `GET /sitemap.xml` — public marketing URLs |
| **Accessibility tree** | Form labels, semantic nav controls, ARIA on dialogs |
| **Layout stability** | Fixed cookie banner, reserved hero/marquee space, font fallbacks |
WebMCP is intentionally deferred until browser and agent support stabilizes.
## Regression checklist
Before merging public-facing template or CSS changes:
1. Contact form fields have associated `<label>` elements (not placeholder-only).
2. Nav dropdowns use `<button>` triggers with `aria-expanded` / `aria-haspopup`.
3. Cookie consent banner stays `position: fixed` (no document flow shift).
4. `GET /robots.txt`, `/sitemap.xml`, `/llms.txt` return 200.
5. Re-run Lighthouse agentic-browsing on homepage and contact page.
## References
- [Lighthouse Agentic Browsing audit overview](https://locomotive.agency/blog/lighthouse-agentic-browsing-audit/)
- [llms.txt proposal](https://llmstxt.org/)
- Gitea issue #5
+66
View File
@@ -0,0 +1,66 @@
"""Machine-readable site discovery endpoints for crawlers and AI agents."""
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.urls import reverse
# Public marketing pages included in sitemap and llms.txt.
PUBLIC_PAGE_ENTRIES = (
("public_index", "Home", "weekly", "1.0"),
("forward_deployed", "Forward-Deployed AI", "monthly", "0.9"),
("bot", "AI Agents", "monthly", "0.9"),
("ml_model", "ML Models", "monthly", "0.8"),
("chat", "Secure AI Chat", "monthly", "0.8"),
("ai_sensor", "AI Sensor Algorithms", "monthly", "0.7"),
("ai_education", "AI Education", "monthly", "0.7"),
("computers", "Computer Builds", "monthly", "0.7"),
("file_hosting", "File Hosting", "monthly", "0.7"),
("web_design", "Web Design and Hosting", "monthly", "0.8"),
("contact", "Contact", "monthly", "0.9"),
("terms_of_service", "Terms of Service and Privacy", "yearly", "0.3"),
)
def _absolute_url(request, url_name):
return request.build_absolute_uri(reverse(url_name))
def robots_txt(request):
sitemap_url = _absolute_url(request, "sitemap_xml")
content = render_to_string(
"public/robots.txt",
{"sitemap_url": sitemap_url},
)
return HttpResponse(content, content_type="text/plain; charset=utf-8")
def sitemap_xml(request):
pages = [
{
"loc": _absolute_url(request, url_name),
"changefreq": changefreq,
"priority": priority,
}
for url_name, _title, changefreq, priority in PUBLIC_PAGE_ENTRIES
]
content = render_to_string("public/sitemap.xml", {"pages": pages})
return HttpResponse(content, content_type="application/xml; charset=utf-8")
def llms_txt(request):
pages = [
{
"title": title,
"url": _absolute_url(request, url_name),
}
for url_name, title, _changefreq, _priority in PUBLIC_PAGE_ENTRIES
]
content = render_to_string(
"public/llms.txt",
{
"site_url": request.build_absolute_uri("/"),
"contact_url": _absolute_url(request, "contact"),
"pages": pages,
},
)
return HttpResponse(content, content_type="text/plain; charset=utf-8")
+145 -2
View File
@@ -23,6 +23,7 @@ body {
font-family: var(--font-main); font-family: var(--font-main);
line-height: 1.6; line-height: 1.6;
overflow-x: hidden; overflow-x: hidden;
text-rendering: optimizeLegibility;
} }
a { a {
@@ -81,6 +82,7 @@ nav {
.hero-section { .hero-section {
position: relative; position: relative;
height: 100vh; height: 100vh;
min-height: 520px;
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -88,6 +90,11 @@ nav {
overflow: hidden; overflow: hidden;
} }
.hero-section--compact {
height: 40vh;
min-height: 300px;
}
#hero-canvas { #hero-canvas {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -95,6 +102,7 @@ nav {
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 0; z-index: 0;
pointer-events: none;
} }
.hero-content { .hero-content {
@@ -102,11 +110,14 @@ nav {
z-index: 1; z-index: 1;
text-align: center; text-align: center;
padding: 0 1rem; padding: 0 1rem;
min-height: 12rem;
} }
.hero-title { .hero-title {
font-size: 4rem; font-size: clamp(2.5rem, 8vw, 4rem);
font-weight: 800; font-weight: 800;
line-height: 1.1;
min-height: 1.1em;
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
@@ -282,6 +293,41 @@ nav {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.form-label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-color);
font-weight: 500;
font-size: 0.95rem;
}
.contact-info-list {
list-style: none;
}
.contact-info-item {
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 1rem;
}
.contact-info-icon {
color: var(--primary-color);
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.form-control { .form-control {
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
@@ -376,7 +422,29 @@ nav {
display: block; display: block;
} }
.dropdown>a::after { .dropdown.dropdown-open .dropdown-content {
display: block;
}
.nav-dropdown-trigger {
background: none;
border: none;
cursor: pointer;
font: inherit;
color: var(--text-color);
font-weight: 500;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 0;
}
.nav-dropdown-trigger:hover,
.nav-dropdown-trigger.active {
color: var(--primary-color);
}
.dropdown .nav-dropdown-trigger::after {
content: ' ▼'; content: ' ▼';
font-size: 0.7em; font-size: 0.7em;
margin-left: 5px; margin-left: 5px;
@@ -387,12 +455,72 @@ nav {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.25rem; padding: 0.25rem;
background: none;
border: none;
cursor: pointer;
font: inherit;
} }
.profile-icon-link::after { .profile-icon-link::after {
display: none !important; display: none !important;
} }
/* Trusted-by marquee */
.trusted-by-section {
padding: 2.5rem 0;
background: var(--bg-color);
}
.trusted-by-label {
text-align: center;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.8rem;
margin-bottom: 1.25rem;
}
.trusted-by-marquee {
overflow: hidden;
min-height: 48px;
}
.trusted-by-marquee-row {
display: flex;
width: max-content;
}
.trusted-by-track {
display: flex;
align-items: center;
gap: 3rem;
padding-right: 3rem;
min-height: 48px;
}
.trusted-by-logo img {
display: block;
height: 48px;
width: auto;
max-width: 320px;
}
@media (prefers-reduced-motion: no-preference) {
.trusted-by-marquee-row {
animation: trusted-by-scroll 30s linear infinite;
}
}
@keyframes trusted-by-scroll {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.profile-icon { .profile-icon {
width: 28px; width: 28px;
height: 28px; height: 28px;
@@ -599,6 +727,21 @@ input:focus, select:focus, textarea:focus {
color: var(--primary-color); color: var(--primary-color);
} }
.footer-link-btn {
background: none;
border: none;
padding: 0;
color: var(--text-muted);
font: inherit;
font-size: 0.9rem;
cursor: pointer;
text-decoration: none;
}
.footer-link-btn:hover {
color: var(--primary-color);
}
.footer-separator { .footer-separator {
color: var(--text-muted); color: var(--text-muted);
margin: 0 0.5rem; margin: 0 0.5rem;
@@ -1,4 +1,7 @@
const canvas = document.getElementById('hero-canvas'); const canvas = document.getElementById('hero-canvas');
if (!canvas) {
// Hero animation only runs on the homepage.
} else {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
let width, height; let width, height;
@@ -109,3 +112,4 @@ function animate() {
} }
animate(); animate();
}
+49 -15
View File
@@ -49,18 +49,18 @@
<body> <body>
<!-- Navigation Bar --> <!-- Navigation Bar -->
<nav> <nav aria-label="Main navigation">
<a href="{% url 'public_index' %}" class="brand-logo"> <a href="{% url 'public_index' %}" class="brand-logo">
<img src="{% static 'public/img/logo.png' %}" alt="AI ML Operations" class="brand-logo-img" width="243" height="28"> <img src="{% static 'public/img/logo.png' %}" alt="AI ML Operations" class="brand-logo-img" width="243" height="28">
</a> </a>
<button class="mobile-menu-btn" aria-label="Menu"></button> <button type="button" class="mobile-menu-btn" aria-label="Open menu" aria-expanded="false" aria-controls="main-nav-links"></button>
<ul class="nav-links"> <ul class="nav-links" id="main-nav-links">
<li><a href="{% url 'public_index' %}" <li><a href="{% url 'public_index' %}"
class="{% if request.resolver_match.url_name == 'public_index' %}active{% endif %}">Home</a></li> class="{% if request.resolver_match.url_name == 'public_index' %}active{% endif %}">Home</a></li>
<li class="dropdown"> <li class="dropdown">
<a href="#" <button type="button" class="nav-dropdown-trigger{% if request.resolver_match.url_name in 'forward_deployed,ai_education,ai_sensor,bot,chat,computers,file_hosting,ml_model,web_design' %} active{% endif %}"
class="{% if request.resolver_match.url_name in 'forward_deployed,ai_education,ai_sensor,bot,chat,computers,file_hosting,ml_model,web_design' %}active{% endif %}">Services</a> id="services-menu-button" aria-expanded="false" aria-haspopup="true" aria-controls="services-menu">Services</button>
<ul class="dropdown-content"> <ul class="dropdown-content" id="services-menu" role="menu" aria-labelledby="services-menu-button">
<li><a href="{% url 'forward_deployed' %}">Forward-Deployed AI</a></li> <li><a href="{% url 'forward_deployed' %}">Forward-Deployed AI</a></li>
<li><a href="{% url 'bot' %}">AI Agents</a></li> <li><a href="{% url 'bot' %}">AI Agents</a></li>
<li><a href="{% url 'ml_model' %}">ML Models</a></li> <li><a href="{% url 'ml_model' %}">ML Models</a></li>
@@ -83,14 +83,15 @@
class="{% if 'financial' in request.path %}active{% endif %}" class="{% if 'financial' in request.path %}active{% endif %}"
data-tianji-event="nav_financials">Financials</a></li> data-tianji-event="nav_financials">Financials</a></li>
<li class="dropdown" id="user-profile-dropdown"> <li class="dropdown" id="user-profile-dropdown">
<a href="#" class="profile-icon-link" title="{{ user.get_full_name|default:user.username }}"> <button type="button" class="profile-icon-link" aria-label="Account menu for {{ user.get_full_name|default:user.username }}"
aria-expanded="false" aria-haspopup="true" aria-controls="profile-menu">
<svg class="profile-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" <svg class="profile-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="8" r="4" /> <circle cx="12" cy="8" r="4" />
<path d="M4 21v-1a6 6 0 0 1 12 0v1" /> <path d="M4 21v-1a6 6 0 0 1 12 0v1" />
</svg> </svg>
</a> </button>
<ul class="dropdown-content profile-dropdown-content"> <ul class="dropdown-content profile-dropdown-content" id="profile-menu" role="menu">
<li class="profile-name-item">{{ user.get_full_name|default:user.username }}</li> <li class="profile-name-item">{{ user.get_full_name|default:user.username }}</li>
<li><a href="{% url 'change_password' %}">Change Password</a></li> <li><a href="{% url 'change_password' %}">Change Password</a></li>
<li> <li>
@@ -123,7 +124,7 @@
<a href="{% url 'terms_of_service' %}" data-tianji-event="footer_terms">Terms &amp; Privacy</a> <a href="{% url 'terms_of_service' %}" data-tianji-event="footer_terms">Terms &amp; Privacy</a>
{% if tianji_enabled %} {% if tianji_enabled %}
<span class="footer-separator">|</span> <span class="footer-separator">|</span>
<a href="#" data-open-cookie-preferences data-tianji-event="footer_cookie_preferences">Cookie Preferences</a> <button type="button" class="footer-link-btn" data-open-cookie-preferences data-tianji-event="footer_cookie_preferences">Cookie Preferences</button>
{% endif %} {% endif %}
</p> </p>
<p class="footer-text footer-text--muted">&copy; 2023 - <p class="footer-text footer-text--muted">&copy; 2023 -
@@ -133,10 +134,11 @@
</footer> </footer>
{% if tianji_enabled %} {% if tianji_enabled %}
<div id="cookie-consent-banner" class="cookie-consent-banner" hidden role="dialog" aria-live="polite" <div id="cookie-consent-banner" class="cookie-consent-banner" hidden role="dialog" aria-modal="true"
aria-label="Cookie consent"> aria-labelledby="cookie-consent-title" aria-describedby="cookie-consent-description">
<div class="cookie-consent-content"> <div class="cookie-consent-content">
<p class="cookie-consent-text"> <h2 id="cookie-consent-title" class="visually-hidden">Cookie consent</h2>
<p class="cookie-consent-text" id="cookie-consent-description">
<span class="cookie-consent-text-full">We use analytics tracking to understand how visitors use our site. Tracking runs only if you accept. <span class="cookie-consent-text-full">We use analytics tracking to understand how visitors use our site. Tracking runs only if you accept.
See our <a href="{% url 'terms_of_service' %}">Terms of Service &amp; Privacy Policy</a> for details.</span> See our <a href="{% url 'terms_of_service' %}">Terms of Service &amp; Privacy Policy</a> for details.</span>
<span class="cookie-consent-text-short">We use analytics if you accept. <a href="{% url 'terms_of_service' %}">Privacy Policy</a></span> <span class="cookie-consent-text-short">We use analytics if you accept. <a href="{% url 'terms_of_service' %}">Privacy Policy</a></span>
@@ -157,9 +159,41 @@
if (mobileBtn && navLinks) { if (mobileBtn && navLinks) {
mobileBtn.addEventListener('click', function () { mobileBtn.addEventListener('click', function () {
navLinks.classList.toggle('active'); const isOpen = navLinks.classList.toggle('active');
mobileBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
mobileBtn.setAttribute('aria-label', isOpen ? 'Close menu' : 'Open menu');
}); });
} }
function closeDropdown(dropdown) {
dropdown.classList.remove('dropdown-open');
const trigger = dropdown.querySelector('.nav-dropdown-trigger, .profile-icon-link');
if (trigger) {
trigger.setAttribute('aria-expanded', 'false');
}
}
document.querySelectorAll('.dropdown').forEach(function (dropdown) {
const trigger = dropdown.querySelector('.nav-dropdown-trigger, .profile-icon-link');
if (!trigger) {
return;
}
trigger.addEventListener('click', function (event) {
if (window.matchMedia('(min-width: 769px)').matches && dropdown.querySelector('.nav-dropdown-trigger')) {
return;
}
event.preventDefault();
const isOpen = dropdown.classList.toggle('dropdown-open');
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
});
document.addEventListener('click', function (event) {
if (!event.target.closest('.dropdown')) {
document.querySelectorAll('.dropdown.dropdown-open').forEach(closeDropdown);
}
});
}); });
</script> </script>
{% block tracking_events %}{% endblock %} {% block tracking_events %}{% endblock %}
@@ -6,7 +6,7 @@
{% block content %} {% block content %}
<!-- Hero Section --> <!-- Hero Section -->
<div class="hero-section" style="height: 40vh; min-height: 300px;"> <div class="hero-section hero-section--compact">
<div class="hero-content"> <div class="hero-content">
<h1 class="hero-title">Get in Touch</h1> <h1 class="hero-title">Get in Touch</h1>
<p class="hero-subtitle hero-subtitle--wide">Describe the workflow or bottleneck you want to automate—we will help you scope a forward-deployed solution.</p> <p class="hero-subtitle hero-subtitle--wide">Describe the workflow or bottleneck you want to automate—we will help you scope a forward-deployed solution.</p>
@@ -32,24 +32,28 @@
{% endif %} {% endif %}
<div class="card"> <div class="card">
<h4 class="card-title" style="margin-bottom: 2rem;">Send Us a Message</h4> <h2 class="card-title contact-form-heading" id="contact-form-heading" style="margin-bottom: 2rem;">Send Us a Message</h2>
<form action="{% url 'contact' %}" method="POST"> <form action="{% url 'contact' %}" method="POST" aria-labelledby="contact-form-heading">
{% csrf_token %} {% csrf_token %}
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="name" placeholder="Your Name"> <label class="form-label" for="contact-name">Your Name</label>
<input type="text" class="form-control" name="name" id="contact-name" autocomplete="name" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="email" class="form-control" name="email" placeholder="Your Email"> <label class="form-label" for="contact-email">Your Email</label>
<input type="email" class="form-control" name="email" id="contact-email" autocomplete="email" required>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="subject" id="contact-subject" placeholder="Subject" value="{{ request.GET.subject|default:'' }}"> <label class="form-label" for="contact-subject">Subject</label>
<input type="text" class="form-control" name="subject" id="contact-subject" value="{{ request.GET.subject|default:'' }}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<textarea name="message" class="form-control" rows="5" placeholder="What workflow is costing you the most time? What systems does it touch?"></textarea> <label class="form-label" for="contact-message">Message</label>
<textarea name="message" class="form-control" id="contact-message" rows="5" placeholder="What workflow is costing you the most time? What systems does it touch?"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -69,15 +73,15 @@
<!-- Info Column --> <!-- Info Column -->
<div> <div>
<div class="card"> <div class="card">
<h5 class="card-title">Contact Information</h5> <h3 class="card-title">Contact Information</h3>
<ul style="list-style: none;"> <ul class="contact-info-list">
<li style="margin-bottom: 1rem; display: flex; align-items: center; gap: 1rem;"> <li class="contact-info-item">
<span style="color: var(--primary-color);">📞</span> <span class="contact-info-icon" aria-hidden="true">📞</span>
<p style="color: var(--text-muted);">+1 (330) 402-2675</p> <a href="tel:+13304022675">+1 (330) 402-2675</a>
</li> </li>
<li style="margin-bottom: 1rem; display: flex; align-items: center; gap: 1rem;"> <li class="contact-info-item">
<span style="color: var(--primary-color);">✉️</span> <span class="contact-info-icon" aria-hidden="true">✉️</span>
<p style="color: var(--text-muted);">ryan@aimloperations.com</p> <a href="mailto:ryan@aimloperations.com">ryan@aimloperations.com</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -96,4 +100,4 @@
} }
</script> </script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@@ -106,13 +106,16 @@
--> -->
<div class="form-group col-6" style="padding-left:1rem"> <div class="form-group col-6" style="padding-left:1rem">
<input type="email" class="form-control" name="email" placeholder="Your Email"> <label for="legacy-contact-email">Your Email</label>
<input type="email" class="form-control" name="email" id="legacy-contact-email" placeholder="Your Email">
</div> </div>
<div class="form-group col-6" style="padding-right:1rem"> <div class="form-group col-6" style="padding-right:1rem">
<input type="text" class="form-control" name="name" placeholder="Your Name"> <label for="legacy-contact-name">Your Name</label>
<input type="text" class="form-control" name="name" id="legacy-contact-name" placeholder="Your Name">
</div> </div>
<div class="form-group col-12" style="padding:1rem"> <div class="form-group col-12" style="padding:1rem">
<textarea name="message" type="text" class="form-control" rows="5" placeholder="Your message"></textarea> <label for="legacy-contact-message">Your Message</label>
<textarea name="message" id="legacy-contact-message" class="form-control" rows="5" placeholder="Your message"></textarea>
</div> </div>
<div class="form-group col-12"> <div class="form-group col-12">
{% if capchaForm %} {% if capchaForm %}
@@ -0,0 +1,19 @@
# AI ML Operations, LLC
> Forward-deployed AI engineering. We embed with your team to architect, build, and deploy custom agentic workflows, integrations, and production AI systems.
AI ML Operations helps organizations move from AI pilots to production systems. Core services include forward-deployed AI engineering, custom AI agents, ML model development, secure hosted chat, sensor algorithms, education, hardware builds, and web hosting.
## Key pages
{% for page in pages %}- [{{ page.title }}]({{ page.url }})
{% endfor %}
## Contact
- [Contact form]({{ contact_url }})
- Phone: +1 (330) 402-2675
- Email: ryan@aimloperations.com
## Site
{{ site_url }}
@@ -11,7 +11,7 @@
{% block content %} {% block content %}
<!-- Hero Section --> <!-- Hero Section -->
<div class="hero-section"> <div class="hero-section">
<canvas id="hero-canvas"></canvas> <canvas id="hero-canvas" aria-hidden="true"></canvas>
<div class="hero-content"> <div class="hero-content">
<h1 class="hero-title">AI ML Operations</h1> <h1 class="hero-title">AI ML Operations</h1>
<p class="hero-subtitle hero-subtitle--wide"> <p class="hero-subtitle hero-subtitle--wide">
@@ -0,0 +1,20 @@
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /accounts/
Disallow: /financial/
Disallow: /planning/
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: PerplexityBot
Allow: /
Sitemap: {{ sitemap_url }}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for page in pages %}
<url>
<loc>{{ page.loc }}</loc>
<changefreq>{{ page.changefreq }}</changefreq>
<priority>{{ page.priority }}</priority>
</url>
{% endfor %}
</urlset>
@@ -97,7 +97,7 @@ visually stunning, functional websites tailored to your business.{% endblock %}
<div class="pricing-toggle" role="group" aria-label="Billing period"> <div class="pricing-toggle" role="group" aria-label="Billing period">
<span class="pricing-toggle-label active" id="monthlyLabel">Monthly</span> <span class="pricing-toggle-label active" id="monthlyLabel">Monthly</span>
<label class="pricing-switch" for="pricingToggle"> <label class="pricing-switch" for="pricingToggle">
<input type="checkbox" id="pricingToggle" role="switch" aria-labelledby="monthlyLabel yearlyLabel"> <input type="checkbox" id="pricingToggle" aria-label="Toggle between monthly and yearly billing">
<span class="pricing-switch-slider"></span> <span class="pricing-switch-slider"></span>
</label> </label>
<span class="pricing-toggle-label" id="yearlyLabel">Yearly</span> <span class="pricing-toggle-label" id="yearlyLabel">Yearly</span>
@@ -119,7 +119,7 @@ visually stunning, functional websites tailored to your business.{% endblock %}
<li style="margin-bottom: 0.5rem;">✓ Email Notifications</li> <li style="margin-bottom: 0.5rem;">✓ Email Notifications</li>
<li style="margin-bottom: 0.5rem;">✓ Backend Admin Access</li> <li style="margin-bottom: 0.5rem;">✓ Backend Admin Access</li>
</ul> </ul>
<a href="#" class="btn">Get Started</a> <a href="{% url 'contact' %}?subject=Web%20Hosting%20Standard%20Plan" class="btn">Get Started</a>
</div> </div>
<!-- Yearly Card --> <!-- Yearly Card -->
@@ -135,7 +135,7 @@ visually stunning, functional websites tailored to your business.{% endblock %}
<li style="margin-bottom: 0.5rem;">✓ 2 Months Free (Yearly)</li> <li style="margin-bottom: 0.5rem;">✓ 2 Months Free (Yearly)</li>
<li style="margin-bottom: 0.5rem;">✓ Priority Support</li> <li style="margin-bottom: 0.5rem;">✓ Priority Support</li>
</ul> </ul>
<a href="#" class="btn" style="background: var(--secondary-color); color: white;">Save 20%</a> <a href="{% url 'contact' %}?subject=Web%20Hosting%20Premium%20Plan" class="btn" style="background: var(--secondary-color); color: white;">Save 20%</a>
</div> </div>
</div> </div>
</div> </div>
+44 -3
View File
@@ -26,9 +26,14 @@ class ContactViewTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Send Us a Message") self.assertContains(response, "Send Us a Message")
self.assertContains(response, 'name="name"') self.assertContains(response, 'id="contact-name"')
self.assertContains(response, 'name="email"') self.assertContains(response, 'for="contact-name"')
self.assertContains(response, 'name="subject"') self.assertContains(response, 'id="contact-email"')
self.assertContains(response, 'for="contact-email"')
self.assertContains(response, 'id="contact-subject"')
self.assertContains(response, 'for="contact-subject"')
self.assertContains(response, 'id="contact-message"')
self.assertContains(response, 'for="contact-message"')
@patch("public.views.send_contact_email") @patch("public.views.send_contact_email")
def test_contact_post_success_saves_contact_and_sends_email(self, mock_send_email): def test_contact_post_success_saves_contact_and_sends_email(self, mock_send_email):
@@ -125,3 +130,39 @@ class TianjiTrackingTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "cookie-consent-banner") self.assertNotContains(response, "cookie-consent-banner")
self.assertNotContains(response, "tianji-config") self.assertNotContains(response, "tianji-config")
class AgenticBrowsingSeoTests(TestCase):
def test_robots_txt_is_available(self):
response = self.client.get(reverse("robots_txt"))
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "text/plain; charset=utf-8")
self.assertContains(response, "User-agent: *")
self.assertContains(response, "Sitemap:")
def test_sitemap_xml_lists_public_pages(self):
response = self.client.get(reverse("sitemap_xml"))
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "application/xml; charset=utf-8")
self.assertContains(response, "<urlset")
self.assertContains(response, reverse("contact"))
self.assertContains(response, reverse("forward_deployed"))
def test_llms_txt_is_available(self):
response = self.client.get(reverse("llms_txt"))
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "text/plain; charset=utf-8")
self.assertContains(response, "# AI ML Operations, LLC")
self.assertContains(response, "## Key pages")
self.assertContains(response, reverse("contact"))
def test_homepage_uses_semantic_nav_controls(self):
response = self.client.get(reverse("public_index"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'id="services-menu-button"')
self.assertContains(response, 'aria-haspopup="true"')
self.assertContains(response, 'aria-label="Main navigation"')
+4 -1
View File
@@ -1,8 +1,11 @@
from django.urls import path from django.urls import path
from . import views from . import seo, views
urlpatterns = [ urlpatterns = [
path("robots.txt", seo.robots_txt, name="robots_txt"),
path("sitemap.xml", seo.sitemap_xml, name="sitemap_xml"),
path("llms.txt", seo.llms_txt, name="llms_txt"),
path("", views.index, name="public_index"), path("", views.index, name="public_index"),
path("chat", views.chat, name="chat"), path("chat", views.chat, name="chat"),
path("ai_education", views.ai_education, name="ai_education"), path("ai_education", views.ai_education, name="ai_education"),