Skip to content

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:

  1. 🔴 Missing critical SEO files (robots.txt, sitemap.xml, favicon)
  2. 🟡 External CDN dependencies need localization
  3. 🟡 Meta description is identical across all pages
  4. 🟢 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

# robots.txt content
User-agent: *
Allow: /

Sitemap: https://nominate.ai/sitemap.xml
<!-- 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)

  1. Go to: https://gwfh.mranftl.com/fonts
  2. Search for “Libre Baskerville” and “Source Sans 3”
  3. Select weights: 400, 700 for Baskerville; 400, 500, 600, 700 for Source Sans 3
  4. 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:

<!-- Current -->
<html>

<!-- Should be -->
<html lang="en">

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:

  1. Google Fonts preconnect
  2. Google Fonts CSS
  3. Tailwind from CDN
  4. Local style.css
  5. Theme detection script (inline, blocking)
  6. Lucide from CDN
  7. 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-label on toggle buttons
  • alt text 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