<?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: Ricardo Lau Cooper</title>
    <description>The latest articles on DEV Community by Ricardo Lau Cooper (@ricardo_laucooper).</description>
    <link>https://dev.to/ricardo_laucooper</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%2F4008839%2Fc8287ec5-2565-45f5-b347-3956cd2252e0.jpg</url>
      <title>DEV Community: Ricardo Lau Cooper</title>
      <link>https://dev.to/ricardo_laucooper</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ricardo_laucooper"/>
    <language>en</language>
    <item>
      <title>I Generated 7,681 Static Pages From 78 Units of Data — Here's the Architecture</title>
      <dc:creator>Ricardo Lau Cooper</dc:creator>
      <pubDate>Tue, 30 Jun 2026 00:55:46 +0000</pubDate>
      <link>https://dev.to/ricardo_laucooper/i-generated-7681-static-pages-from-78-units-of-data-heres-the-architecture-gm5</link>
      <guid>https://dev.to/ricardo_laucooper/i-generated-7681-static-pages-from-78-units-of-data-heres-the-architecture-gm5</guid>
      <description>&lt;p&gt;Most unit converter websites are React SPAs with a single page. Google indexes one URL and calls it a day. I wanted to see what happens when you treat every possible conversion as its own page — with its own title tag, its own FAQ schema, and its own educational content. So I built &lt;a href="https://metriqcon.co" rel="noopener noreferrer"&gt;metriqcon.co&lt;/a&gt; and generated 7,681 static HTML pages from just 78 units of source data.&lt;/p&gt;

&lt;p&gt;Here's how the architecture works and what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math: How 78 Units Become 7,681 Pages
&lt;/h2&gt;

&lt;p&gt;The unit data covers 8 categories: length, weight, temperature, area, volume, speed, time, and data storage. Each category has 4–12 units. The page generation logic creates three types of URLs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Category hub pages&lt;/strong&gt; — one per category (8 total)&lt;br&gt;
&lt;code&gt;/convert/length&lt;/code&gt;, &lt;code&gt;/convert/weight&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unit pair pages&lt;/strong&gt; — every permutation of from→to within a category&lt;br&gt;
&lt;code&gt;/convert/length/meters-to-feet&lt;/code&gt;&lt;br&gt;
That's &lt;code&gt;n × (n-1)&lt;/code&gt; pages per category. With 12 length units, that's 132 pair pages for length alone. Across all categories: 548 pair pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Value pages&lt;/strong&gt; — 13 popular values (1, 2, 5, 10, 15, 20, 25, 30, 50, 100, 200, 500, 1000) for each pair&lt;br&gt;
&lt;code&gt;/convert/length/15-meters-to-feet&lt;/code&gt;&lt;br&gt;
That's 548 × 13 = 7,124 value pages.&lt;/p&gt;

&lt;p&gt;Total: 1 homepage + 8 hubs + 548 pairs + 7,124 values = &lt;strong&gt;7,681 pages&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The SEO Trick: Answer in the Title Tag
&lt;/h2&gt;

&lt;p&gt;The most important decision was what goes in the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; tag of value pages. Compare these two approaches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ "Meters to Feet Converter | Metriqcon"
✅ "15 Meters to Feet = 49.2126 ft | Metriqcon"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version puts the exact answer in the title. When Google renders this in search results, users see the conversion result without clicking. Counterintuitively, this &lt;em&gt;increases&lt;/em&gt; CTR — people click to verify or explore further because the snippet proved the site has the exact answer they need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack: Astro + React Islands
&lt;/h2&gt;

&lt;p&gt;The site uses Astro for static site generation with React islands for interactivity.&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/
│   ├── units.js          # Single source of truth: 78 units with conversion functions
│   └── content.js        # Educational content engine: real-world examples, facts, FAQs
├── layouts/
│   └── Base.astro        # Shared HTML shell with SEO head
├── pages/
│   ├── index.astro       # Homepage (React island: client:load)
│   └── convert/
│       ├── [category].astro        # Hub pages (pure static HTML)
│       └── [category]/[slug].astro # Pair + value pages (static HTML + React island)
└── react/
    ├── Metriqcon.jsx       # Full interactive converter (homepage)
    └── ConverterIsland.jsx # Lightweight converter (conversion pages)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key architectural decision: &lt;strong&gt;the conversion pages are 90% static HTML, 10% React&lt;/strong&gt;. The answer, the formula, the FAQ, the conversion table, the real-world examples — all rendered at build time as plain HTML. The only React island is the interactive converter input, which hydrates after the page loads.&lt;/p&gt;

&lt;p&gt;This means Google's crawler sees the full content immediately, without executing JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Thin Content Problem (And How I Solved It)
&lt;/h2&gt;

&lt;p&gt;Here's what I didn't anticipate. After deploying 7,681 pages, Google crawled about 200 of them and put 7,639 in "Discovered - currently not indexed." The reason: Google saw thousands of pages with identical structure and minimal text variation, and classified them as thin content.&lt;/p&gt;

&lt;p&gt;The fix was building a content engine that generates genuinely unique text for every unit pair:&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;// src/data/content.js&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REAL_WORLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;examples&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="s2"&gt;a bag of sugar (1 kg)&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="s2"&gt;an average adult man (about 80 kg)&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="s2"&gt;a grand piano (about 300 kg)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The kilogram is the base SI unit of mass...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;examples&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="s2"&gt;a loaf of bread (about 1 lb)&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="s2"&gt;a newborn baby (about 7.5 lb)&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="s2"&gt;an adult golden retriever (65–75 lb)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pounds are the primary weight unit in the US...&lt;/span&gt;&lt;span class="dl"&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;Each conversion page now renders unique paragraphs combining the &lt;code&gt;from&lt;/code&gt; unit's context, the &lt;code&gt;to&lt;/code&gt; unit's context, real-world examples for both, historical "did you know" facts, category-specific use cases, and 7 FAQ items (up from 3). The FAQ items also generate JSON-LD structured data for Google rich results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Live Math Parser
&lt;/h2&gt;

&lt;p&gt;One feature that differentiates the converter: the input field accepts math expressions, not just numbers. Type &lt;code&gt;(12 * 4) + 6.5&lt;/code&gt; and it evaluates live, then converts the result across all units simultaneously.&lt;/p&gt;

&lt;p&gt;The parser is sandboxed — no &lt;code&gt;eval()&lt;/code&gt;. It validates the input against a whitelist of safe characters, then uses a &lt;code&gt;new Function()&lt;/code&gt; in strict mode:&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;function&lt;/span&gt; &lt;span class="nf"&gt;parseExpression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[\d\s\+\-\*\/\(\)\.\,\^&lt;/span&gt;&lt;span class="sr"&gt;%&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cleaned&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="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid characters&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;safe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/,/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\^&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`"use strict"; return (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;safe&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;This catches engineers and developers who are calculating something before converting — a use case no other converter site handles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build Performance
&lt;/h2&gt;

&lt;p&gt;Astro builds all 7,681 pages in under 25 seconds on a standard machine. The output is about 180MB of static HTML, deployed to Vercel's edge network. Total hosting cost: $0 (Vercel free tier).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;✓ sitemap.xml: 7,681 URLs
✓ Built in 20.35s
✓ Deployed to Vercel edge network
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with fewer pages.&lt;/strong&gt; 7,681 pages on a brand-new domain triggered Google's thin content filters. Starting with 500–1,000 of the highest-traffic conversion pairs, then expanding after building domain authority, would have been smarter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add a blog from day one.&lt;/strong&gt; Programmatic pages alone don't build topical authority. A handful of articles like "How to convert cooking measurements" or "Understanding metric vs imperial" would have given Google a clearer signal about what the site is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The site is live at &lt;a href="https://metriqcon.co" rel="noopener noreferrer"&gt;metriqcon.co&lt;/a&gt;. The full source generates from a single &lt;code&gt;units.js&lt;/code&gt; data file — every page, the sitemap, and the converter all import from the same place.&lt;/p&gt;

&lt;p&gt;Interested in hearing what features you'd add or how you'd approach the SEO challenge differently.&lt;/p&gt;

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