ElitePick AI
Live agency site where a Vite SPA ships crawler-ready HTML and self-submits to Google.
- Google Indexing API
- Node.js build tooling
- TypeScript
- React 18
- Status
- Active. Initial commit 2026-01-21, most recent push 2026-02-28, 27 commits on main. Not archived. Live at elitepickai.com.
By the numbers · 6
26
Routes pre-rendered to static HTML on every build
30
URLs auto-submitted to the Google Indexing API per deploy
0
External dependencies in the indexing client
7
Security headers applied at the edge
8
Search and AI crawler user-agents explicitly admitted
~143 KB
Long-form editorial authored as typed React content modules
Summary
ElitePick AI is the live marketing and lead-capture site for an independent data and AI practice, running as a Vite single-page application on Vercel at elitepickai.com. The architecture is the interesting part, because a client-rendered SPA is normally invisible to crawlers: a post-build script rewrites the compiled bundle to inject route-specific semantic HTML for 26 routes, and a second, dependency-free script signs a service-account JWT and batch-submits every sitemap URL to the Google Indexing API on each deploy. Routes, navigation, sitemap, and pre-rendered markup all derive from a single typed content layer in the repository rather than from a CMS.
The problem
An independent data and AI practice that relied on freelance marketplaces for work had no destination of its own. Prospective clients searching for a specific skill found platform listings rather than the practitioner, and every lead arrived through an intermediary. The site needed to rank for narrow, high-intent searches, be readable by the AI assistants people increasingly ask for recommendations, and get newly published pages discovered in days rather than weeks. All of it had to run on static hosting, with no server, no editor, and no content team behind it.
Approach
Content is modelled as typed TypeScript modules under src/data (projects, services, tools, blog), so page routes, navigation, the sitemap, and the pre-rendered HTML all derive from one in-repo source instead of a CMS.
scripts/prerender.mjs runs after vite build and rewrites dist/<route>/index.html for each route, replacing the empty root div with hand-authored semantic HTML so crawlers receive real content from a client-rendered SPA; 26 pages are generated per build.
scripts/google-indexing.mjs implements a Google Indexing API client from scratch: base64url JWT assembly, RS256 signing through node:crypto createSign, an OAuth2 jwt-bearer token exchange, and multipart/mixed batch POSTs with a hand-written batch response parser.
Indexing is wired as a best-effort build step: batches of 40 URLs with a one-second delay to respect the 200-request daily quota, per-URL fallback when a batch fails, and a top-level catch that deliberately never returns a non-zero exit code so a failed submission cannot break a deploy.
A single SEOHelmet component centralises per-page title, description, canonical URL, robots and googlebot directives, Open Graph, Twitter card, and JSON-LD injection; individual page components supply their own schema.org graphs.
vercel.json handles the www-to-apex permanent redirect, cleanUrls, an SPA rewrite that deliberately excludes asset and SEO paths, year-long immutable caching for hashed assets and .webp images, and seven security headers including a scoped Content-Security-Policy.
robots.txt explicitly admits eight AI and search crawler user-agents, and public/llms.txt plus llms-full.txt publish machine-readable summaries of the site for LLM consumers alongside the conventional sitemap.
A written SEO audit checked into .lovable/plan.md diagnoses every page's tags before changes are made, and drives a repositioning pass across eleven files that also corrects wrong publication years and broken Open Graph image paths.
Architecture
Typed content layer (src/data/*.ts)React Router SPA (13 page components, shadcn/Radix UI)vite buildscripts/prerender.mjs injects per-route semantic HTML into dist for 26 routesVercel static edge (CSP/HSTS headers, cleanUrls, apex redirect, SPA rewrites)visitor; in parallel, scripts/google-indexing.mjs parses public/sitemap.xml, signs a service-account RS256 JWT, exchanges it for an OAuth2 token, and batch-submits 30 URLs to the Google Indexing API. Lead capture posts from the contact and order forms to the Web3Forms API.
| Component | Role |
|---|---|
| src/data/*.ts content layer | Typed definitions for 9 projects, 8 services, 2 tool landing pages, and 3 blog posts; the single source that routes, sitemap, and pre-rendered HTML are derived from. |
| src/routes.ts | Builds the complete route manifest by mapping over the content collections, so static and dynamic routes stay in sync with the data. |
| React Router SPA | 13 route-level page components over 49 shadcn/Radix UI primitives, with theme provider, guided product tour, and toast layers mounted at the App root. |
| SEOHelmet component | Centralised per-page head management: title, description, canonical, robots/googlebot/bingbot directives, Open Graph, Twitter card, and JSON-LD script injection. |
| scripts/prerender.mjs | Post-build step that generates semantic HTML per route and writes dist/<route>/index.html, replacing the empty root div so crawlers see content instead of an empty shell. |
| scripts/google-indexing.mjs | Dependency-free Google Indexing API client: JWT assembly and RS256 signing, OAuth2 token exchange, sitemap parsing, multipart/mixed batch submission, batch response parsing, and individual-request fallback. |
| vercel.json | Edge configuration: apex redirect, cleanUrls, SPA rewrites that exclude asset and SEO paths, immutable caching for hashed assets and .webp, and seven security headers including a scoped CSP. |
| public/robots.txt, llms.txt, llms-full.txt, sitemap.xml | Machine-readable surface for search and LLM crawlers: 30 sitemap URLs, eight explicitly permitted crawler user-agents, and a structured plain-text site description for AI assistants. |
| Web3Forms integration | Serverless lead capture from the contact and direct-order forms, formatting structured order details (service, budget band, timeline, scope flags) into a delivered email. |
Trade-offs
Chose
Post-build static HTML injection into the compiled Vite bundle
Over
Server-side rendering or migrating to a framework such as Next.js
The site ships as static files with no server runtime. Rewriting dist/<route>/index.html after the build gives crawlers real per-route content while keeping deployment to plain static hosting, at the cost of maintaining a second, hand-authored copy of each page's markup inside prerender.mjs.
Chose
A hand-written RS256 JWT and OAuth2 client using only node:crypto and fetch
Over
The official googleapis SDK
The script's own header states the constraint plainly: zero external dependencies, Node.js built-in crypto and https only. It keeps the build step free of a heavy transitive dependency tree, at the cost of owning the token-exchange and multipart batch code.
Chose
Best-effort indexing that always exits zero
Over
Failing the build when URL submission fails
Indexing is an optimisation, not a correctness requirement. The main function is wrapped in a catch that logs and returns, with an explicit comment that indexing failure should not break the build, trading submission guarantees for deploy reliability.
Chose
Multipart batch submission of 40 URLs per request with per-URL fallback
Over
One HTTP request per URL
The Google Indexing API allows 200 requests per day and a maximum of 40 items per batch. Batching keeps a full sitemap resubmission inside quota, and the fallback path exists because the batch response has to be parsed out of a multipart body that can come back in an unexpected shape.
Chose
A typed content layer committed alongside the code
Over
A headless CMS or database
Content is versioned, type-checked, and diffable with the rest of the application, and one definition feeds the router, the sitemap, and the pre-renderer. The cost is that every copy change requires a rebuild and redeploy.
At scale
196 files at HEAD across the repository (git tree, recursive)
13 route-level page components in src/pages plus 49 shadcn/Radix UI primitives in src/components/ui
577,701 bytes of TypeScript and 30,743 bytes of JavaScript per GitHub language statistics
Typed content layer covering 9 portfolio projects, 8 services, 2 tool landing pages, and 3 long-form blog posts
27 commits between 2026-01-21 and 2026-02-28
Test coverage is a single file, src/test/example.test.ts, containing one trivial expect(true).toBe(true) assertion; playwright.config.ts is present but no e2e specs are committed
17 blog images in src/assets/blog range from 1.7 MB to 2.9 MB each, unoptimised for a page-speed-sensitive marketing site
My role
Sole author and operator. The repository is a personal account repo for the author's own brand at his own domain, with his contact address embedded in the contact page content, and all 27 commits on the default branch.
