DEV Community

meet dhanani
meet dhanani

Posted on

How I Built a Free Stock Fair Value Calculator With Astro, React Islands, and Tailwind CSS v4

Homepage

I recently shipped MarketFairValue.com, a free stock analysis platform that calculates fair value for 140+ US stocks using four valuation models. No signup, no paywall, no ads.

In this post I want to share the architecture decisions, the tech stack, and the lessons learned from building a data-heavy financial tool as a solo developer.

What the Site Does
Before diving into the tech, here is what the product actually does:

  • Runs 4 valuation models (DCF, Graham Number, PEG, Dividend Discount) on every stock and blends them into a composite fair value
  • Labels each stock as Undervalued, Fair Value, or Overvalued
  • Lets users adjust assumptions with interactive sliders and see fair value change in real time
  • Includes 4 additional tools: an "If You Bought" calculator, a dividend income planner, an earnings countdown, and a stock comparison tool
  • Covers 140+ stocks across all major sectors, refreshed daily
  • Everything is free at marketfairvalue.com.

Now let's talk about how it is built.

Why Astro
I needed a framework that could deliver fast, SEO-friendly static pages for 140+ stocks while still supporting interactive components where needed (sliders, charts, dropdowns).

Astro was the obvious choice for three reasons:

1. Static-first with zero JS by default

Every stock page is pre-rendered at build time. The HTML ships with all the critical content already in the markup. No JavaScript needs to load before Google can crawl the fair value data, the model breakdowns, or the FAQ sections. This is critical for a site where SEO is the primary growth channel.

2. Island architecture

Not everything can be static. The assumption sliders on each stock page need React state. The comparison charts need client-side rendering. The dividend calculator needs form interactivity.

Astro's island architecture lets me keep 90% of each page as zero-JS static HTML and only hydrate the interactive pieces. A stock page might be 15KB of HTML with a single 8KB React island for the slider component.

---
// This runs at build time, ships as pure HTML
const quote = getLocalQuote(stock.symbol);
const valuations = calculateAllValuations(quote);
---

<article>
  <h1>{stock.name} Fair Value Analysis</h1>
  <p>Current Price: {formatCurrency(valuations.currentPrice)}</p>

  <!-- Only this component hydrates on the client -->
  <ValuationSliders client:visible initialData={valuations} />
</article>
Enter fullscreen mode Exit fullscreen mode

3. File-based routing with dynamic segments

Each stock gets a page at /stocks/[slug] (e.g. /stocks/aapl-fair-value). Astro generates all 140+ pages at build time from a single template. Same pattern for earnings pages, dividend pages, and comparison matchups.

React Islands for Interactivity
I use React only where interactivity is required. Here are the main islands:

Valuation Sliders - Users can adjust the growth rate and discount rate, and the fair value recalculates instantly. This is pure client-side math, no API calls needed. The initial values come from the server-rendered props.

Compare View - The stock comparison tool lets users pick two stocks (or a stock vs a benchmark like S&P 500) and renders a side-by-side returns table plus a growth chart. This uses client:visible so it only hydrates when the user scrolls to it.

Dividend Calculator - A form-driven tool with multiple inputs (investment amount, yield, growth rate, DRIP toggle). React handles the form state and recalculation.

The key pattern: server-render the static shell, pass precomputed data as props, and let React handle only the user-driven mutations.

// React island receives server-computed data as props
interface Props {
  currentPrice: number;
  models: ValuationResult[];
  defaultGrowthRate: number;
  defaultDiscountRate: number;
}
export default function ValuationSliders(props: Props) {
  const [growthRate, setGrowthRate] = useState(props.defaultGrowthRate);
  const [discountRate, setDiscountRate] = useState(props.defaultDiscountRate);

  // Recalculate on slider change, no API call needed
  const fairValue = useMemo(
    () => computeDCF(props.currentPrice, growthRate, discountRate),
    [growthRate, discountRate]
  );

  return (
    <div>
      <input type="range" value={growthRate} onChange={...} />
      <p>Fair Value: ${fairValue.toFixed(2)}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tailwind CSS v4
The site uses Tailwind v4 with a design token system defined in global.css. Dark mode is the default (the target audience for financial tools prefers it), with light mode available via a .light class toggle.

All colors are referenced through semantic tokens like text-ink, bg-surface, text-primary, bg-canvas rather than raw hex values. This makes theming consistent and makes it easy to adjust the palette without touching component code.

@theme {
  --color-primary: #fcd535;
  --color-primary-hover: #f0b90b;
  --color-ink: #eaecef;
  --color-body: #b7bdc6;
  --color-mute: #848e9c;
  --color-surface: #1e2329;
  --color-canvas: #181a20;
}
Enter fullscreen mode Exit fullscreen mode

One thing I learned: always define both your dark and light mode tokens upfront. I started with dark-only and retrofitting light mode later meant auditing every component. If I were starting over, I would define both palettes on day one.

Data Pipeline
Financial data refreshes daily through an automated pipeline:

  1. A scheduled job fetches updated financials (price, EPS, free cash flow, dividends, book value, growth estimates) for all 140+ stocks
  2. The raw data is normalized and written to local JSON files
  3. At build time, Astro reads these JSON files and runs all four valuation models for every stock
  4. The site rebuilds and deploys automatically via GitHub Actions

There is no runtime API. Every number you see on the site was computed at build time and baked into the HTML. This means the site is fast (no loading spinners), cheap to host (static files), and resilient (no server to go down).

Data Provider -> JSON files -> Build-time calculations -> Static HTML -> CDN

SEO as a First-Class Concern
For a free tool with no marketing budget, organic search is everything. Some things I prioritized from day one:

Structured data on every page. Each stock page includes JSON-LD markup for the financial data. The homepage has FAQ schema. Breadcrumbs are marked up. This helps Google understand what each page is about and can surface rich results.

One H1 per page with the primary keyword. Every stock page has an H1 like "AAPL (Apple) Fair Value Analysis". Simple but important.

Keyword-rich URLs. Stock pages use /stocks/aapl-fair-value instead of /stocks/aapl. The slug includes the target keyword.

Internal linking. Every stock page links to that stock's earnings page, dividend page, comparison page, and "If You Bought" page. This creates a tight internal link graph that helps Google discover and value every page.

Pre-rendered content. Because Astro generates everything at build time, Google sees the full content on first crawl. No hydration delay, no "please wait while we load the data" spinners.

Lessons Learned

  1. Do the math at build time, not runtime. Financial calculations are deterministic given the same inputs. There is no reason to run DCF models in the browser when you can precompute them during the build. This eliminated an entire class of loading-state and error-handling complexity.

  2. Islands are underrated. Before Astro, I would have built this as a full React SPA. That would have meant shipping hundreds of KB of JavaScript for pages that are 90% static text and numbers. The island model gave me the best of both worlds.

  3. Design tokens save you from yourself. I changed the background color palette three times during development. Because everything used tokens like bg-surface instead of bg-[#1e2329], each change was a single-line edit in the CSS file.

  4. SEO is a feature, not an afterthought. I built the URL structure, heading hierarchy, and structured data into the architecture from the start. Trying to bolt on SEO later is painful and usually results in compromises.

  5. Ship fewer features, but complete ones. I initially planned 10 tools. I shipped 5. Each one is polished and connected to every stock page. Five complete tools beat ten half-finished ones.

What's Next

  • Expanding coverage toward the full S&P 500
  • Adding historical fair value charts (how has the composite estimate changed over time?)
  • More benchmark comparisons (sector ETFs, international indices)
  • Possibly a "watchlist" feature using local storage (still no signup required)

Try It

The whole thing is live at marketfairvalue.com. Pick any stock and you will see all four valuation models, the composite estimate, and interactive sliders to test your own assumptions.

If you have questions about the architecture or want to discuss the Astro islands pattern, drop a comment. Happy to go deeper on any of this.

Top comments (0)