Improve Lighthouse Agentic Browsing readiness (#8)
## 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:
@@ -49,18 +49,18 @@
|
||||
<body>
|
||||
|
||||
<!-- Navigation Bar -->
|
||||
<nav>
|
||||
<nav aria-label="Main navigation">
|
||||
<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">
|
||||
</a>
|
||||
<button class="mobile-menu-btn" aria-label="Menu">☰</button>
|
||||
<ul class="nav-links">
|
||||
<button type="button" class="mobile-menu-btn" aria-label="Open menu" aria-expanded="false" aria-controls="main-nav-links">☰</button>
|
||||
<ul class="nav-links" id="main-nav-links">
|
||||
<li><a href="{% url 'public_index' %}"
|
||||
class="{% if request.resolver_match.url_name == 'public_index' %}active{% endif %}">Home</a></li>
|
||||
<li class="dropdown">
|
||||
<a href="#"
|
||||
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>
|
||||
<ul class="dropdown-content">
|
||||
<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 %}"
|
||||
id="services-menu-button" aria-expanded="false" aria-haspopup="true" aria-controls="services-menu">Services</button>
|
||||
<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 'bot' %}">AI Agents</a></li>
|
||||
<li><a href="{% url 'ml_model' %}">ML Models</a></li>
|
||||
@@ -83,14 +83,15 @@
|
||||
class="{% if 'financial' in request.path %}active{% endif %}"
|
||||
data-tianji-event="nav_financials">Financials</a></li>
|
||||
<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"
|
||||
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" />
|
||||
<path d="M4 21v-1a6 6 0 0 1 12 0v1" />
|
||||
</svg>
|
||||
</a>
|
||||
<ul class="dropdown-content profile-dropdown-content">
|
||||
</button>
|
||||
<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><a href="{% url 'change_password' %}">Change Password</a></li>
|
||||
<li>
|
||||
@@ -123,7 +124,7 @@
|
||||
<a href="{% url 'terms_of_service' %}" data-tianji-event="footer_terms">Terms & Privacy</a>
|
||||
{% if tianji_enabled %}
|
||||
<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 %}
|
||||
</p>
|
||||
<p class="footer-text footer-text--muted">© 2023 -
|
||||
@@ -133,10 +134,11 @@
|
||||
</footer>
|
||||
|
||||
{% if tianji_enabled %}
|
||||
<div id="cookie-consent-banner" class="cookie-consent-banner" hidden role="dialog" aria-live="polite"
|
||||
aria-label="Cookie consent">
|
||||
<div id="cookie-consent-banner" class="cookie-consent-banner" hidden role="dialog" aria-modal="true"
|
||||
aria-labelledby="cookie-consent-title" aria-describedby="cookie-consent-description">
|
||||
<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.
|
||||
See our <a href="{% url 'terms_of_service' %}">Terms of Service & 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>
|
||||
@@ -157,9 +159,41 @@
|
||||
|
||||
if (mobileBtn && navLinks) {
|
||||
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>
|
||||
{% block tracking_events %}{% endblock %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
{% block content %}
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-section" style="height: 40vh; min-height: 300px;">
|
||||
<div class="hero-section hero-section--compact">
|
||||
<div class="hero-content">
|
||||
<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>
|
||||
@@ -32,24 +32,28 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<h4 class="card-title" style="margin-bottom: 2rem;">Send Us a Message</h4>
|
||||
<form action="{% url 'contact' %}" method="POST">
|
||||
<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" aria-labelledby="contact-form-heading">
|
||||
{% csrf_token %}
|
||||
<div class="form-row">
|
||||
<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 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 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 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 class="form-group">
|
||||
@@ -69,15 +73,15 @@
|
||||
<!-- Info Column -->
|
||||
<div>
|
||||
<div class="card">
|
||||
<h5 class="card-title">Contact Information</h5>
|
||||
<ul style="list-style: none;">
|
||||
<li style="margin-bottom: 1rem; display: flex; align-items: center; gap: 1rem;">
|
||||
<span style="color: var(--primary-color);">📞</span>
|
||||
<p style="color: var(--text-muted);">+1 (330) 402-2675</p>
|
||||
<h3 class="card-title">Contact Information</h3>
|
||||
<ul class="contact-info-list">
|
||||
<li class="contact-info-item">
|
||||
<span class="contact-info-icon" aria-hidden="true">📞</span>
|
||||
<a href="tel:+13304022675">+1 (330) 402-2675</a>
|
||||
</li>
|
||||
<li style="margin-bottom: 1rem; display: flex; align-items: center; gap: 1rem;">
|
||||
<span style="color: var(--primary-color);">✉️</span>
|
||||
<p style="color: var(--text-muted);">ryan@aimloperations.com</p>
|
||||
<li class="contact-info-item">
|
||||
<span class="contact-info-icon" aria-hidden="true">✉️</span>
|
||||
<a href="mailto:ryan@aimloperations.com">ryan@aimloperations.com</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -96,4 +100,4 @@
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -106,13 +106,16 @@
|
||||
-->
|
||||
|
||||
<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 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 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 class="form-group col-12">
|
||||
{% 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 %}
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-section">
|
||||
<canvas id="hero-canvas"></canvas>
|
||||
<canvas id="hero-canvas" aria-hidden="true"></canvas>
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">AI ML Operations</h1>
|
||||
<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">
|
||||
<span class="pricing-toggle-label active" id="monthlyLabel">Monthly</span>
|
||||
<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>
|
||||
</label>
|
||||
<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;">✓ Backend Admin Access</li>
|
||||
</ul>
|
||||
<a href="#" class="btn">Get Started</a>
|
||||
<a href="{% url 'contact' %}?subject=Web%20Hosting%20Standard%20Plan" class="btn">Get Started</a>
|
||||
</div>
|
||||
|
||||
<!-- 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;">✓ Priority Support</li>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user