GMI · TECHNOLOGY OBSERVATORY // ALL SYSTEMS NOMINAL
ENGINEERED BY LEOPARD DATA

SEO & Public Ticker Pages

How an invisible Blazor WASM platform became crawlable: static SSR prerendering, and a background engine that publishes free stock-grade pages

TICKER PAGE PIPELINE
rendering diagram…
flowchart TB
    A([Admin - Public Ticker Pages screen]):::app
    B[POST public-tickers/refresh<br/>Azure Functions API]:::svc
    C[(publictickerrefreshjobs<br/>Status - Pending)]:::data
    D[SeoPageBuilder<br/>systemd service on report VM]:::svc
    E[FMP - 12 datasets per ticker<br/>constituents + fundamentals]:::ext
    F[DashboardSummaryBuilder<br/>same grading engine as reports]:::svc
    G{Grade changed?}:::svc
    H[Claude writes 2-4 sentence<br/>summary - Haiku tier]:::ext
    I[Reuse cached summary]:::svc
    J[Render static HTML<br/>pages + index + sitemap]:::svc
    K[(seo-pages blob container)]:::data
    L[Deploy pipeline<br/>merges blob into client-web zip]:::svc
    M([app.grademyinvestments.com/grade/TICKER<br/>plain HTML - crawler ready]):::app
    N([Google indexes grade pages<br/>visitors funnel to signup]):::sec
    A --> B
    B --> C
    C -->|poll every 60s<br/>+ monthly self-check| D
    D --> E
    E --> F
    F --> G
    G -->|yes| H
    G -->|no| I
    H --> J
    I --> J
    J --> K
    K --> L
    D -->|queue via DevOps REST| L
    L --> M
    M --> N
classDef app fill:#1f2630,stroke:#C0894E,color:#F3EFE8;
classDef svc fill:#241c14,stroke:#E2B583,color:#F3EFE8;
classDef data fill:#22201a,stroke:#C0894E,color:#EBD3AE;
classDef ext fill:#1b1f27,stroke:#5A616B,color:#C4C0B8;
classDef sec fill:#1d2733,stroke:#5cc8e0,color:#eafaff;
From an admin button press to a crawlable static page: queue, grade, summarize, render, upload, deploy.

The Problem: WASM Is Invisible to Search Engines

Every GMI web property began as standalone Blazor WebAssembly. A crawler fetching any page received an empty HTML shell and a loading spinner — the real content only materialized after the .NET runtime downloaded and booted in the browser. Nearly everything else sat behind Google sign-in. For search engines, the platform effectively did not exist.

The fix came in two waves: first, converting the public-facing pages to prerendered static SSR so every marketing and documentation page ships as plain HTML; second, adding a set of free public ticker grade pages (/grade/NVDA and friends) that target high-intent searches like "NVDA stock grade" and funnel visitors into signup — the grade and headline stats are free, the full analysis lives behind the app.

Wave One: Static SSR Prerendering

Two Blazor static-SSR projects (Gmi.Technology.Ssr, Gmi.ClientWeb.Ssr) render the same Razor pages the WASM apps use — but server-side, with zero interactivity. At build time the pipeline boots each SSR app, crawls every @page route, and writes the responses to plain index.html files plus a generated sitemap.xml.

  • This technology site is fully static — every page you're reading is prerendered HTML; the only JavaScript is Mermaid rendering and nav toggles.
  • The client site is a hybrid: the WASM host page was renamed to app.html, prerendered public pages overlay the deployment, and an IIS rewrite serves static HTML when it exists — falling back to app.html so login and the private app boot WASM exactly as before.
  • Nothing moved servers: the same Azure App Services serve the same URLs; crawlers just get real content now.

Wave Two: Free Public Ticker Grade Pages

Admins curate which tickers get public pages — individually, by bulk paste, or by syncing whole index groups: S&P 500, Nasdaq, and Dow Jones membership comes straight from FMP's constituent endpoints, while Russell 2000 membership derives from IWM ETF holdings. Every enabled ticker gets a self-contained static page at /grade/{TICKER}: the letter grade (large and dated), six headline fundamentals, a short plain-English summary written by Claude, internal links to related tickers, and one signup CTA carrying UTM attribution. A searchable index at /grade/ lists every graded stock by index.

Same grading engine

Pages reuse DashboardSummaryBuilder — the exact pure-function scoring model the report engine uses. The generator never reimplements grading.

Cost-aware AI

Claude rewrites a ticker's summary only when its grade actually changes; unchanged grades reuse the cached paragraph. A deterministic fallback keeps pages shipping even if the API is down.

Failure isolation

One ticker failing FMP never blocks the run — it keeps serving its previous snapshot and surfaces the failure in the admin screen. The sitemap only ever lists pages that exist.

The Background Process: Gmi.SeoPageBuilder.Console

Page generation runs where the data lives. The App Services deploy from read-only packages (WEBSITE_RUN_FROM_PACKAGE=1) and the MySQL databases have no public network access, so the builder runs as a systemd service on the report VM — the same pattern as the report, billing, and Claude-analysis engines. The flow:

  1. An admin presses Refresh & Republish (or the built-in monthly check fires) → a job row lands in publictickerrefreshjobs.
  2. The builder polls the queue, pulls ~12 FMP datasets per enabled ticker (throttled), and computes each grade.
  3. Grade changed? Claude writes a fresh summary. Otherwise the cached one is reused.
  4. Every page, the /grade/ index, a search manifest, and sitemap-grades.xml are rendered deterministically and uploaded to the seo-pages blob container; stale pages for disabled tickers are deleted.
  5. The builder queues the deploy pipeline via the Azure DevOps REST API — the pipeline merges the blob contents into the client-web package, and the pages go live.

Refresh cadence is deliberately monthly plus on-demand — grades move on fundamentals, not on daily price noise, and a monthly cycle keeps FMP and Claude costs proportionate to an acquisition funnel rather than a product feature.

SEO Plumbing Details

  • Crawler-proof by construction — every public page renders fully with JavaScript disabled; ticker pages are under 10 KB of self-contained HTML.
  • Canonicals & sitemaps — every page declares its production canonical URL; robots.txt references both the prerendered-site sitemap and the generated grades sitemap, and dev deployments ship a Disallow: / robots so only production gets indexed.
  • Structured data — ticker pages emit JSON-LD (WebPage + BreadcrumbList) and OpenGraph/Twitter cards; deliberately no schemas that imply personalized financial advice.
  • Freshness signals — the visible "Grade updated" date and each sitemap <lastmod> come from the actual grade date, so a page never looks stale after an earnings surprise without also being refreshable on demand.
  • Attribution — the signup CTA carries utm_source=seo&utm_medium=ticker-page&utm_content={TICKER}, and the pages run the same GA4 property as the app, so conversions attribute back to the specific ticker that earned them.
  • Compliance — every page carries a not-investment-advice footer and FMP data attribution.