<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: meet dhanani</title>
    <description>The latest articles on DEV Community by meet dhanani (@meet_dhanani).</description>
    <link>https://dev.to/meet_dhanani</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3995536%2Fe3e7cc61-ef7e-4997-b0f4-f476adfcf530.png</url>
      <title>DEV Community: meet dhanani</title>
      <link>https://dev.to/meet_dhanani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/meet_dhanani"/>
    <language>en</language>
    <item>
      <title>I Built a Free Ingredient Substitution Finder with Astro and Zero JavaScript</title>
      <dc:creator>meet dhanani</dc:creator>
      <pubDate>Sun, 28 Jun 2026 18:19:34 +0000</pubDate>
      <link>https://dev.to/meet_dhanani/i-built-a-free-ingredient-substitution-finder-with-astro-and-zero-javascript-303j</link>
      <guid>https://dev.to/meet_dhanani/i-built-a-free-ingredient-substitution-finder-with-astro-and-zero-javascript-303j</guid>
      <description>&lt;p&gt;Last month I shipped &lt;a href="https://ingredientreplace.com/" rel="noopener noreferrer"&gt;IngredientReplace&lt;/a&gt; - a free tool that helps home cooks find ingredient substitutes with exact ratios. It covers 100+ ingredients, 500+ tested alternatives, and loads in under 1 second on mobile.&lt;/p&gt;

&lt;p&gt;The entire site is statically generated with Astro. Zero client-side JavaScript for the core experience. Here's how I built it and what I learned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;br&gt;
Every time you Google "substitute for buttermilk," you get a 2,000-word blog post about someone's childhood before the actual answer. I wanted a tool that gives you the substitute, the ratio, and the dietary info instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Astro&lt;/strong&gt;&lt;br&gt;
I chose Astro for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Static output = fast. Every page is pre-rendered HTML. No hydration, no framework runtime, no loading spinners. Google rewards fast sites with better rankings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Component-based DX. I still get reusable .astro components with scoped styles and props. It feels like React but ships zero JS to the browser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Built-in SEO features. Sitemap generation, RSS, canonical URLs, and easy structured data injection are all straightforward.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── data/
│   ├── ingredients.json      # 100+ ingredients with substitutes
│   ├── categories.json       # Groupings (dairy, eggs, flour, etc.)
│   └── context-pages.json    # Recipe-specific substitution pages
├── pages/
│   ├── index.astro
│   ├── substitute/[...slug].astro   # Dynamic routes
│   ├── sitemap.xml.ts
│   └── robots.txt.ts
├── components/
│   ├── SearchBar.astro
│   ├── SubstituteList.astro
│   └── SEOHead.astro
└── layouts/
    └── BaseLayout.astro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data lives in JSON files. Each ingredient has this shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buttermilk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Buttermilk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"substitutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Milk + Lemon Juice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ratio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1 tbsp lemon juice + milk to fill 1 cup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"vegan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"glutenFree"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Let sit 5 minutes before using"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SEO Strategy That Actually Worked&lt;/strong&gt;&lt;br&gt;
The site went from 0 to indexed on 200+ pages within 3 weeks. Here's what mattered:&lt;/p&gt;

&lt;p&gt;Every substitute page includes FAQPage and BreadcrumbList JSON-LD schemas. Google uses these for rich results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;faqJsonLd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FAQPage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mainEntity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;faq&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Question&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;acceptedAnswer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Answer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Search Feature Without a Backend&lt;/strong&gt;&lt;br&gt;
The search bar works without any API calls. At build time, I generate a lightweight search index as a static JSON file. The client-side search loads this index on first interaction (not on page load) and filters locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Only loads when user focuses the search input&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/search-index.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the initial page load at zero JS while still providing instant search results after the first keystroke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment&lt;/strong&gt;&lt;br&gt;
The site runs on Cloudflare Pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free tier handles the traffic easily&lt;/li&gt;
&lt;li&gt;Global CDN means sub-100ms TTFB worldwide&lt;/li&gt;
&lt;li&gt;Automatic deployments on git push&lt;/li&gt;
&lt;li&gt;Custom headers for caching and security&lt;/li&gt;
&lt;li&gt;Build time for 200+ pages is under 10 seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Results After 4 Weeks&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;200+ pages indexed by Google&lt;/li&gt;
&lt;li&gt;Multiple pages appearing in "People Also Ask" featured snippets&lt;/li&gt;
&lt;li&gt;Sub-1-second load times on mobile (3G)&lt;/li&gt;
&lt;li&gt;Zero hosting cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;br&gt;
Static sites still win for content-heavy tools. If your data doesn't change every minute, pre-render it.&lt;/p&gt;

&lt;p&gt;Astro's zero-JS default is a superpower for SEO. Less JavaScript means faster pages means better rankings.&lt;/p&gt;

&lt;p&gt;Structured data gets you rich results fast. FAQ schema and BreadcrumbList schema are low-effort, high-reward.&lt;/p&gt;

&lt;p&gt;JSON data files are underrated. You don't need a CMS or database for a tool with a few hundred entries. A JSON file in your repo is simpler, faster, and version-controlled.&lt;/p&gt;

&lt;p&gt;Build the thing people are Googling. "Substitute for X" gets searched millions of times per month. Building a tool that answers that query directly is more effective than writing blog posts about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try It&lt;/strong&gt;&lt;br&gt;
Check out &lt;a href="https://ingredientreplace.com/" rel="noopener noreferrer"&gt;IngredientReplace&lt;/a&gt; if you want to see the final result. It's free, no login, and works on any device.&lt;/p&gt;

&lt;p&gt;If you're building a similar content-heavy static site with Astro, happy to answer questions in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>seo</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I Built a 2,000-Page Static Site for Cooking Times with Astro and Cloudflare Pages</title>
      <dc:creator>meet dhanani</dc:creator>
      <pubDate>Sun, 28 Jun 2026 10:19:30 +0000</pubDate>
      <link>https://dev.to/meet_dhanani/how-i-built-a-2000-page-static-site-for-cooking-times-with-astro-and-cloudflare-pages-3l61</link>
      <guid>https://dev.to/meet_dhanani/how-i-built-a-2000-page-static-site-for-cooking-times-with-astro-and-cloudflare-pages-3l61</guid>
      <description>&lt;p&gt;Every night, millions of people Google the same thing: "how long to cook chicken breast."&lt;/p&gt;

&lt;p&gt;They land on a recipe blog. They scroll past a life story. They scroll past 14 ads. They finally find the answer buried in paragraph 19.&lt;/p&gt;

&lt;p&gt;I hated this experience so much that I built &lt;a href="https://cooktimeguide.com/" rel="noopener noreferrer"&gt;CookTimeGuide.com&lt;/a&gt;, a free cooking time reference with 301 foods, 11 cooking methods, and zero fluff.&lt;/p&gt;

&lt;p&gt;The site now has over 2,000 pages, loads in under 1 second, and the entire thing is statically generated from a single JSON file. Here's how I built it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Data Layer&lt;/strong&gt;&lt;br&gt;
Everything starts with one file: foods.json. Each food entry looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Chicken Breast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chicken-breast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Poultry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"safe_temp_f"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;165&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"methods"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bake"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"temps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"350f"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"time_min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"time_max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"preheat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"400f"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"time_min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"time_max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"preheat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"425f"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"time_min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"time_max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"preheat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"air-fry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"grill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"primary_keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"how long to cook chicken breast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"related_keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"chicken breast oven time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chicken breast internal temp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;301 foods. Each with multiple methods. Each method with multiple temperatures. This single JSON file generates the entire site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page Generation with Astro&lt;/strong&gt;&lt;br&gt;
I chose Astro because it's purpose-built for content-heavy static sites. The key feature: getStaticPaths() lets you generate thousands of pages from data at build time.&lt;/p&gt;

&lt;p&gt;The site has three levels of dynamic cooking pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/cook/[food]/                    → 301 pages (food hub)
/cook/[food]/[method]/           → 615 pages (method-specific)
/cook/[food]/[method]/[temp]/    → 851 pages (temperature-specific)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus prep guide pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/reheat/[food]/                  → 25 pages
/brine/[food]/                   → 11 pages
/soak/[food]/                    → 25 pages
/marinate/[food]/                → 16 pages
/thaw/[food]/                    → 15 pages
/rest/[food]/                    → 9 pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's roughly 2,000 pages generated from data, plus hub pages, method indexes, and utility pages.&lt;/p&gt;

&lt;p&gt;Each dynamic route reads from the JSON, validates that paths exist, and generates fully static HTML with zero client-side JavaScript for the core content.&lt;/p&gt;

&lt;p&gt;SEO at Scale&lt;br&gt;
When you have 2,000+ pages, SEO becomes an engineering problem, not a content problem. Here's what I had to solve:&lt;/p&gt;

&lt;p&gt;Unique titles and descriptions for every page. I built a helper that generates them from templates:&lt;/p&gt;

&lt;p&gt;Food hub: "How Long to Cook {Food}: Time &amp;amp; Temp for Every Method"&lt;br&gt;
Method page: "How Long to Cook {Food} in {Method}: Time, Temp &amp;amp; Tips"&lt;br&gt;
Temp page: "How Long to Cook {Food} in {Method} at {Temp}"&lt;br&gt;
Every single page gets a unique title and meta description. No duplicates.&lt;/p&gt;

&lt;p&gt;Structured data everywhere. Each page outputs JSON-LD for Recipe, HowTo, FAQPage, and BreadcrumbList schemas. This is what gets you rich snippets in Google results. I generate these programmatically from the same JSON data.&lt;/p&gt;

&lt;p&gt;Canonical URLs to prevent duplicate content. The temperature-specific pages (like &lt;code&gt;/cook/chicken-breast/bake/350f/&lt;/code&gt;) set their canonical URL to the method page (&lt;code&gt;/cook/chicken-breast/bake/&lt;/code&gt;). This tells Google "index the method page, treat the temp pages as supporting content."&lt;/p&gt;

&lt;p&gt;Internal linking. Every food page links to its methods. Every method page links to its temperature variants and back to the food hub. Every food hub cross-links to related prep guides (brine, marinate, thaw, rest). This creates a tight internal link graph that search engines love.&lt;/p&gt;

&lt;p&gt;Sitemap generation. Astro's sitemap integration handles this automatically, but with 2,000+ URLs you need a sitemap index that splits into multiple sitemap files.&lt;/p&gt;

&lt;p&gt;Deployment on Cloudflare Pages&lt;br&gt;
The entire site deploys to Cloudflare Pages. For a fully static site like this, it's hard to beat:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global CDN with edge caching&lt;/li&gt;
&lt;li&gt;Free for this scale&lt;/li&gt;
&lt;li&gt;Automatic deployments from git&lt;/li&gt;
&lt;li&gt;Built-in analytics without extra JavaScript&lt;/li&gt;
&lt;li&gt;Build time for 2,000+ pages is around 30-45 seconds. Astro's build is fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also set up trailingSlash: 'always' in the Astro config to match Cloudflare Pages' default behavior. This is one of those small things that prevents redirect chains and duplicate URL issues that quietly tank your SEO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features That Made a Difference&lt;/strong&gt;&lt;br&gt;
Built-in kitchen timer. This was the feature that got the most positive feedback. It uses the Screen Wake Lock API so your phone screen stays on while cooking. No more tapping your phone with greasy hands to check the timer.&lt;/p&gt;

&lt;p&gt;Weight calculator. A 4 oz chicken breast and an 8 oz chicken breast don't take the same time. The calculator scales cook time by weight. Simple math, but surprisingly few cooking sites do this.&lt;/p&gt;

&lt;p&gt;Day/night theme. Kitchens are bright during the day, dark at night. The theme switch isn't just aesthetic, it's practical.&lt;/p&gt;

&lt;p&gt;No client-side rendering for content. All cooking data is in the static HTML. JavaScript handles the timer, theme toggle, and weight calculator. If JS fails to load, you still get every cook time and temperature. This matters for Core Web Vitals and for the many users who cook with spotty kitchen WiFi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I Learned&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Long-tail SEO is underrated. "How long to cook chicken breast" is competitive. "How long to air fry chicken breast at 400 degrees" is not. With 851 temperature-specific pages, the site captures thousands of long-tail queries that bigger sites don't bother targeting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data-driven sites scale better than content-driven sites. Adding a new food means adding one JSON entry. The build generates every page, every meta tag, every schema, every internal link automatically. I can add 10 foods in 20 minutes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Astro is excellent for this use case. The island architecture means interactive components (timer, calculator) work without shipping a full framework to the browser. Most pages ship under 50KB total.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Speed is a feature users notice. Multiple users have mentioned that the site "just loads instantly." On Cloudflare's edge network with static HTML, Time to First Byte is typically under 50ms. Compare that to recipe blogs loading 15 ad scripts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Static doesn't mean limited. 2,000+ pages, structured data, dynamic internal linking, interactive components, all generated from one JSON file and deployed as static HTML. No database, no server, no ongoing infrastructure cost.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check It Out&lt;br&gt;
The site is live at cooktimeguide.com. A few pages worth looking at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cooktimeguide.com/foods/" rel="noopener noreferrer"&gt;Browse all 301 foods&lt;/a&gt;&lt;br&gt;
&lt;a href="https://cooktimeguide.com/methods/air-fry/" rel="noopener noreferrer"&gt;Air fryer cooking times&lt;/a&gt;&lt;br&gt;
&lt;a href="https://cooktimeguide.com/temperature-chart/" rel="noopener noreferrer"&gt;USDA temperature chart&lt;/a&gt;&lt;br&gt;
&lt;a href="https://cooktimeguide.com/timer/" rel="noopener noreferrer"&gt;Kitchen timer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've built a data-driven static site with Astro or a similar framework, I'd love to hear about your experience. What worked? What didn't?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>astro</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How I Built a Free Stock Fair Value Calculator With Astro, React Islands, and Tailwind CSS v4</title>
      <dc:creator>meet dhanani</dc:creator>
      <pubDate>Sun, 21 Jun 2026 16:48:01 +0000</pubDate>
      <link>https://dev.to/meet_dhanani/how-i-built-a-free-stock-fair-value-calculator-with-astro-react-islands-and-tailwind-css-v4-5542</link>
      <guid>https://dev.to/meet_dhanani/how-i-built-a-free-stock-fair-value-calculator-with-astro-react-islands-and-tailwind-css-v4-5542</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9qoytevszosr0635veen.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9qoytevszosr0635veen.png" alt="Homepage" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I recently shipped &lt;a href="https://dev.tourl"&gt;MarketFairValue.com&lt;/a&gt;, a free stock analysis platform that calculates fair value for 140+ US stocks using four valuation models. No signup, no paywall, no ads.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the Site Does&lt;/strong&gt;&lt;br&gt;
Before diving into the tech, here is what the product actually does:&lt;/p&gt;

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

&lt;p&gt;Now let's talk about how it is built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Astro&lt;/strong&gt;&lt;br&gt;
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).&lt;/p&gt;

&lt;p&gt;Astro was the obvious choice for three reasons:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;1. Static-first with zero JS by default&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;2. Island architecture&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;// This runs at build time, ships as pure HTML&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLocalQuote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valuations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateAllValuations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;---&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;Fair&lt;/span&gt; &lt;span class="nx"&gt;Value&lt;/span&gt; &lt;span class="nx"&gt;Analysis&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Current&lt;/span&gt; &lt;span class="nx"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valuations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="nx"&gt;Only&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="nx"&gt;hydrates&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ValuationSliders&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="nx"&gt;initialData&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;valuations&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/article&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;3. File-based routing with dynamic segments&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each stock gets a page at &lt;code&gt;/stocks/[slug]&lt;/code&gt; (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.&lt;/p&gt;

&lt;p&gt;React Islands for Interactivity&lt;br&gt;
I use React only where interactivity is required. Here are the main islands:&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Compare View - The stock comparison tool lets users pick two stocks (or a stock vs a benchmark like S&amp;amp;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.&lt;/p&gt;

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

&lt;p&gt;The key pattern: server-render the static shell, pass precomputed data as props, and let React handle only the user-driven mutations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// React island receives server-computed data as props&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ValuationResult&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;defaultGrowthRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;defaultDiscountRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ValuationSliders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;growthRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setGrowthRate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultGrowthRate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;discountRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDiscountRate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultDiscountRate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Recalculate on slider change, no API call needed&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fairValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;computeDCF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;growthRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;discountRate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;growthRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;discountRate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"range"&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;growthRate&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Fair Value: $&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fairValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Tailwind CSS v4&lt;/em&gt;&lt;br&gt;
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.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fcd535&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary-hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f0b90b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-ink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#eaecef&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#b7bdc6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-mute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#848e9c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e2329&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#181a20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Pipeline&lt;/strong&gt;&lt;br&gt;
Financial data refreshes daily through an automated pipeline:&lt;/p&gt;

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

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Data Provider -&amp;gt; JSON files -&amp;gt; Build-time calculations -&amp;gt; Static HTML -&amp;gt; CDN&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO as a First-Class Concern&lt;/strong&gt;&lt;br&gt;
For a free tool with no marketing budget, organic search is everything. Some things I prioritized from day one:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Structured data on every page&lt;/em&gt;. 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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One H1 per page with the primary keyword&lt;/em&gt;. Every stock page has an H1 like "AAPL (Apple) Fair Value Analysis". Simple but important.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Keyword-rich URLs&lt;/em&gt;. Stock pages use /stocks/aapl-fair-value instead of /stocks/aapl. The slug includes the target keyword.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Internal linking&lt;/em&gt;. 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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pre-rendered content&lt;/em&gt;. 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lessons Learned&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What's Next&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Try It&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The whole thing is live at &lt;a href="https://dev.tourl"&gt;marketfairvalue.com&lt;/a&gt;. Pick any stock and you will see all four valuation models, the composite estimate, and interactive sliders to test your own assumptions.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
