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:
@@ -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
|
||||
@@ -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")
|
||||
@@ -23,6 +23,7 @@ body {
|
||||
font-family: var(--font-main);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -81,6 +82,7 @@ nav {
|
||||
.hero-section {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
min-height: 520px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -88,6 +90,11 @@ nav {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-section--compact {
|
||||
height: 40vh;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
#hero-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -95,6 +102,7 @@ nav {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
@@ -102,11 +110,14 @@ nav {
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
min-height: 12rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 4rem;
|
||||
font-size: clamp(2.5rem, 8vw, 4rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
min-height: 1.1em;
|
||||
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
@@ -282,6 +293,41 @@ nav {
|
||||
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 {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
@@ -376,7 +422,29 @@ nav {
|
||||
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: ' ▼';
|
||||
font-size: 0.7em;
|
||||
margin-left: 5px;
|
||||
@@ -387,12 +455,72 @@ nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.profile-icon-link::after {
|
||||
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 {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
@@ -599,6 +727,21 @@ input:focus, select:focus, textarea:focus {
|
||||
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 {
|
||||
color: var(--text-muted);
|
||||
margin: 0 0.5rem;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const canvas = document.getElementById('hero-canvas');
|
||||
if (!canvas) {
|
||||
// Hero animation only runs on the homepage.
|
||||
} else {
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let width, height;
|
||||
@@ -109,3 +112,4 @@ function animate() {
|
||||
}
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -26,9 +26,14 @@ class ContactViewTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Send Us a Message")
|
||||
self.assertContains(response, 'name="name"')
|
||||
self.assertContains(response, 'name="email"')
|
||||
self.assertContains(response, 'name="subject"')
|
||||
self.assertContains(response, 'id="contact-name"')
|
||||
self.assertContains(response, 'for="contact-name"')
|
||||
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")
|
||||
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.assertNotContains(response, "cookie-consent-banner")
|
||||
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"')
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from . import seo, views
|
||||
|
||||
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("chat", views.chat, name="chat"),
|
||||
path("ai_education", views.ai_education, name="ai_education"),
|
||||
|
||||
Reference in New Issue
Block a user