nominate.ai Website Review¶
Review Date: December 20, 2025 Reviewer: Claude (AI Assistant) Audience: Campaign Brain Dev Team (FastHTML + NGINX)
Executive Summary¶
The Campaign Brain marketing site at nominate.ai is well-structured with consistent templating across pages. However, there are several SEO gaps, some style guide inconsistencies, and external CDN dependencies that should be localized for production reliability.
Priority Issues:
- 🔴 Missing critical SEO files (robots.txt, sitemap.xml, favicon)
- 🟡 External CDN dependencies need localization
- 🟡 Meta description is identical across all pages
- 🟢 Style guide discrepancy between HTML and DOCX versions
1. SEO & Indexing Issues¶
1.1 Missing Files (Critical)¶
| File | Status | Impact |
|---|---|---|
/robots.txt |
❌ 404 | Search engines can’t find crawl directives |
/sitemap.xml |
❌ 404 | Search engines can’t discover all pages |
/favicon.ico |
❌ 404 | Missing browser tab icon |
Fix: Create these files in your static directory
<!-- sitemap.xml content -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://nominate.ai/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://nominate.ai/features</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://nominate.ai/pricing</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://nominate.ai/demo</loc>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://nominate.ai/about</loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://nominate.ai/start</loc>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
</urlset>
1.2 Missing Meta Tags¶
Current state: All pages use identical meta description:
<meta name="description" content="The CRM that grass-roots campaigns actually use. Voter outreach, volunteer coordination, and AI-powered follow-ups.">
Missing tags across all pages:
<!-- Add these to your head template -->
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/apple-touch-icon.png">
<!-- Open Graph (Facebook/LinkedIn) -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://nominate.ai/">
<meta property="og:title" content="Campaign Brain - Win Your Campaign">
<meta property="og:description" content="The CRM that grass-roots campaigns actually use.">
<meta property="og:image" content="https://nominate.ai/static/images/og-image.png">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Campaign Brain - Win Your Campaign">
<meta name="twitter:description" content="The CRM that grass-roots campaigns actually use.">
<meta name="twitter:image" content="https://nominate.ai/static/images/og-image.png">
<!-- Canonical URL (important for SEO) -->
<link rel="canonical" href="https://nominate.ai/">
<!-- Language -->
<html lang="en"> <!-- Already present, good! -->
1.3 Page-Specific Meta Descriptions Needed¶
| Page | Suggested Meta Description |
|---|---|
/ |
The CRM that grass-roots campaigns actually use. Voter outreach, volunteer coordination, and AI-powered follow-ups—all in one place. |
/features |
Complete campaign toolkit: voter database, multi-channel outreach, AI follow-ups, and volunteer management. Everything you need to win. |
/pricing |
Simple, transparent pricing for Campaign Brain. Free tier available. No contracts, cancel anytime. |
/demo |
See Campaign Brain in action. Watch our 2-minute demo showing voter search, outreach workflows, and AI-powered follow-ups. |
/about |
Meet the team behind Campaign Brain. Campaign veterans and engineers building tools for grass-roots organizers. |
/start |
Start your free trial of Campaign Brain. Get your campaign organized in minutes. |
2. External CDN Dependencies¶
2.1 Current External Resources¶
| Resource | CDN URL | Current Version |
|---|---|---|
| Google Fonts | fonts.googleapis.com |
Libre Baskerville, Source Sans 3 |
| Tailwind CSS | cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/ |
2.2.19 |
| Lucide Icons | unpkg.com/lucide@latest |
latest (unstable!) |
| HTMX | unpkg.com/htmx.org@1.9.10 |
1.9.10 |
2.2 Localizing External Resources¶
A. Download and Host Locally¶
Directory structure:
/static/
├── css/
│ ├── style.css # Your custom CSS
│ ├── tailwind.min.css # Downloaded Tailwind
│ └── fonts/ # Self-hosted fonts
│ ├── libre-baskerville-regular.woff2
│ ├── libre-baskerville-bold.woff2
│ ├── source-sans-3-*.woff2
├── js/
│ ├── lucide.min.js # Pinned version
│ └── htmx.min.js # Pinned version
└── images/
├── logo.png
├── favicon.ico
├── apple-touch-icon.png
└── og-image.png
B. Download Commands¶
# Create directories
mkdir -p static/css/fonts static/js
# Download Tailwind (pin to specific version)
curl -o static/css/tailwind.min.css \
"https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"
# Download HTMX (pin to specific version)
curl -o static/js/htmx.min.js \
"https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js"
# Download Lucide (PIN TO SPECIFIC VERSION - critical!)
curl -o static/js/lucide.min.js \
"https://unpkg.com/lucide@0.263.1/dist/umd/lucide.min.js"
C. Self-Hosting Google Fonts¶
Option 1: Use google-webfonts-helper (Recommended)
- Go to: https://gwfh.mranftl.com/fonts
- Search for “Libre Baskerville” and “Source Sans 3”
- Select weights: 400, 700 for Baskerville; 400, 500, 600, 700 for Source Sans 3
- Download the zip and extract to
static/css/fonts/
Option 2: Manual @font-face
Add to static/css/fonts.css:
/* Libre Baskerville */
@font-face {
font-family: 'Libre Baskerville';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/static/css/fonts/libre-baskerville-v14-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Libre Baskerville';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/static/css/fonts/libre-baskerville-v14-latin-700.woff2') format('woff2');
}
/* Source Sans 3 */
@font-face {
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/static/css/fonts/source-sans-3-v15-latin-regular.woff2') format('woff2');
}
/* ... repeat for 500, 600, 700 weights */
D. Updated FastHTML Headers¶
from fasthtml.common import *
app = FastHTML(
hdrs=[
# Favicon
Link(rel="icon", type="image/x-icon", href="/static/images/favicon.ico"),
# Local fonts (no preconnect needed)
Link(rel="stylesheet", href="/static/css/fonts.css"),
# Local Tailwind
Link(rel="stylesheet", href="/static/css/tailwind.min.css"),
# Your custom styles (load last to override)
Link(rel="stylesheet", href="/static/css/style.css"),
# Local JavaScript (defer for performance)
Script(src="/static/js/lucide.min.js", defer=True),
Script(src="/static/js/htmx.min.js", defer=True),
]
)
E. NGINX Configuration for Static Assets¶
# Add to your nginx.conf or site config
# Cache static assets aggressively
location /static/ {
alias /path/to/your/app/static/;
expires 1y;
add_header Cache-Control "public, immutable";
# Enable gzip for text files
gzip on;
gzip_types text/css application/javascript application/json;
}
# Ensure proper MIME types
location ~* \.woff2$ {
add_header Content-Type font/woff2;
expires 1y;
add_header Cache-Control "public, immutable";
}
3. Style Guide Consistency Analysis¶
3.1 Style Guide Version Conflict¶
⚠️ You have TWO different style guides with conflicting specifications:
| Aspect | HTML Style Guide | DOCX Style Guide |
|---|---|---|
| Display Font | Barlow Condensed | Libre Baskerville |
| Heading Font | Barlow | Libre Baskerville |
| Body Font | Source Sans 3 | Source Sans 3 |
| Navy Primary | #243b5c (–cb-navy-700) |
#1E3A5F |
| Crimson/Red | #8b2332 (–cb-crimson-600) |
#8B2332 |
Current Website Implementation:
- ✅ Uses DOCX colors (correct)
- ✅ Uses Libre Baskerville + Source Sans 3 (matches DOCX)
- ❓ Doesn’t use Barlow at all (HTML guide specifies Barlow)
Recommendation: Consolidate to ONE authoritative style guide. The DOCX version appears more intentional and polished. Archive the HTML version or update it to match.
3.2 CSS Variables Alignment¶
Website CSS vs DOCX Style Guide:
| Variable | Website | DOCX Guide | Match? |
|---|---|---|---|
--color-navy-primary |
#1E3A5F |
#1E3A5F |
✅ |
--color-navy-dark |
#152942 |
#152942 |
✅ |
--color-red-primary |
#8B2332 |
#8B2332 |
✅ |
--color-bg-page |
#F8FAFC |
#F8FAFC |
✅ |
--color-text-primary |
#334155 |
#334155 |
✅ |
--color-success |
#059669 |
#059669 |
✅ |
Verdict: Core color implementation matches the DOCX guide perfectly. ✅
3.3 Typography Implementation¶
Website Implementation:
--font-display: 'Libre Baskerville', Georgia, serif;
--font-body: 'Source Sans 3', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
DOCX Specification: ✅ Matches
HTML Style Guide (different!):
--font-display: 'Barlow Condensed', 'Impact', sans-serif;
--font-heading: 'Barlow', 'Arial Narrow', sans-serif;
4. HTML/Markup Consistency¶
4.1 Consistent Elements Across Pages ✅¶
- Header/navigation structure: Identical across all pages
- Footer structure: Identical across all pages
- Dark mode toggle: Consistent implementation
- Mobile menu: Consistent implementation
- Script loading: Consistent pattern
4.2 Issues Found¶
A. <html> tag missing lang attribute in some places:
B. Button with invalid disabled attribute:
<!-- On /demo page -->
<a href="#" disabled class="btn btn-primary btn-lg">Watch Demo</a>
<!-- Fix: disabled doesn't work on <a> tags. Use: -->
<span class="btn btn-primary btn-lg btn-disabled" aria-disabled="true">Watch Demo</span>
C. Form action consistency:
<!-- Current pattern (OK but verbose) -->
<form enctype="multipart/form-data" action="/district" method="get">
<!-- Consider simplifying when not uploading files -->
<form action="/district" method="get">
4.3 Active Navigation State Missing¶
The current nav doesn’t highlight which page you’re on. Add active state:
# In your FastHTML template
def NavLink(text, href, current_path):
is_active = current_path == href
cls = "nav-link active" if is_active else "nav-link"
return A(text, href=href, cls=cls)
/* Add to style.css */
.nav-link.active {
color: white;
font-weight: 600;
}
.nav-link.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: var(--color-red-primary);
}
5. Performance Recommendations¶
5.1 Critical Rendering Path¶
Current load order:
- Google Fonts preconnect
- Google Fonts CSS
- Tailwind from CDN
- Local style.css
- Theme detection script (inline, blocking)
- Lucide from CDN
- HTMX from CDN
Optimized order (with local assets):
<head>
<!-- Critical: Theme detection must run first -->
<script>
(function() {
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);
})();
</script>
<!-- Critical CSS: Inline above-the-fold styles for fastest paint -->
<style>/* Critical styles here */</style>
<!-- Preload fonts for faster text rendering -->
<link rel="preload" href="/static/css/fonts/libre-baskerville-v14-latin-700.woff2" as="font" type="font/woff2" crossorigin>
<!-- Main stylesheets -->
<link rel="stylesheet" href="/static/css/fonts.css">
<link rel="stylesheet" href="/static/css/tailwind.min.css">
<link rel="stylesheet" href="/static/css/style.css">
<!-- Defer non-critical JS -->
<script src="/static/js/htmx.min.js" defer></script>
<script src="/static/js/lucide.min.js" defer></script>
</head>
5.2 Lucide Icons: Pin the Version!¶
🔴 Critical Issue:
<!-- DANGEROUS: "latest" means your icons can break without warning -->
<script src="https://unpkg.com/lucide@latest"></script>
Fix:
<!-- Pin to specific version -->
<script src="/static/js/lucide.min.js"></script>
<!-- Downloaded from https://unpkg.com/lucide@0.263.1/dist/umd/lucide.min.js -->
6. Accessibility Quick Wins¶
6.1 Already Good ✅¶
aria-labelon toggle buttonsalttext on logo images- Semantic HTML structure (header, main, footer, nav, section)
- Focus-visible styles defined
6.2 Improvements Needed¶
A. Skip link for keyboard navigation:
<!-- Add as first element in body -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content" class="main-content">
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--color-navy-primary);
color: white;
padding: 8px;
z-index: 1000;
}
.skip-link:focus {
top: 0;
}
B. Form labels need for attribute:
<!-- Current (OK) -->
<label for="campaign_name">Campaign Name</label>
<input id="campaign_name" ...>
<!-- Ensure all labels have matching for/id pairs -->
7. Action Items Summary¶
Immediate (This Week)¶
| Priority | Task | Effort |
|---|---|---|
| 🔴 High | Create robots.txt | 5 min |
| 🔴 High | Create sitemap.xml | 15 min |
| 🔴 High | Add favicon files | 30 min |
| 🔴 High | Pin Lucide version (or localize) | 10 min |
| 🟡 Medium | Add page-specific meta descriptions | 30 min |
| 🟡 Medium | Add Open Graph / Twitter meta tags | 30 min |
Short Term (This Sprint)¶
| Priority | Task | Effort |
|---|---|---|
| 🟡 Medium | Localize all CDN resources | 2 hours |
| 🟡 Medium | Self-host Google Fonts | 1 hour |
| 🟡 Medium | Configure NGINX caching | 30 min |
| 🟡 Medium | Add canonical URLs | 30 min |
| 🟢 Low | Add active nav state | 30 min |
| 🟢 Low | Consolidate style guides | 1 hour |
Long Term¶
| Priority | Task | Effort |
|---|---|---|
| 🟢 Low | Add skip-link for accessibility | 15 min |
| 🟢 Low | Optimize critical CSS | 2 hours |
| 🟢 Low | Add structured data (JSON-LD) | 1 hour |
Appendix A: Complete robots.txt¶
# Campaign Brain - nominate.ai
User-agent: *
Allow: /
# Block admin/internal paths (adjust as needed)
Disallow: /admin/
Disallow: /api/
# Sitemap location
Sitemap: https://nominate.ai/sitemap.xml
Appendix B: NGINX Static Asset Config¶
server {
listen 443 ssl http2;
server_name nominate.ai;
# ... SSL config ...
# Static assets with aggressive caching
location /static/ {
alias /var/www/campaign-brain/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
# Gzip text-based assets
gzip on;
gzip_vary on;
gzip_types text/css application/javascript application/json image/svg+xml;
}
# Fonts need CORS headers if serving from different domain
location ~* \.(woff2?|ttf|eot|otf)$ {
add_header Access-Control-Allow-Origin "*";
add_header Cache-Control "public, immutable";
expires 1y;
}
# robots.txt and sitemap.xml
location = /robots.txt {
alias /var/www/campaign-brain/static/robots.txt;
access_log off;
}
location = /sitemap.xml {
alias /var/www/campaign-brain/static/sitemap.xml;
add_header Content-Type "application/xml";
access_log off;
}
# Favicon
location = /favicon.ico {
alias /var/www/campaign-brain/static/images/favicon.ico;
access_log off;
}
# ... proxy to FastHTML app ...
}
Generated by Claude | Campaign Brain Website Review v1.0