A 180-page reference site for the price of a domain (Astro + Cloudflare Workers)

# astro# cloudflare# webdev# seo
A 180-page reference site for the price of a domain (Astro + Cloudflare Workers)anders lee

How a static site with calculators, drawings, PDFs and social images is generated from data, released in batches, and hosted for free.

I built steelweight-calculator.com, a reference site for
steel section weights: one page per beam, channel and angle, weight charts, pipe schedules, rebar
and sheet gauges, plus calculators. It has about 180 pages and 570 files today and costs
nothing to run apart from the domain. Here is the setup and a few things I would do the same way again.

Everything is generated from data

The site is Astro in fully static mode. A handful of Python scripts turn the
sources (standards, spreadsheets, open-source libraries) into JSON in src/data/, and each
JSON row becomes a page through getStaticPaths():

// src/pages/beams/[slug].astro
export function getStaticPaths() {
  return publishedOfKind("beam").map((section) => ({ params: { slug: section.slug }, props: { section } }));
}
Enter fullscreen mode Exit fullscreen mode

The page then renders everything from that one row: weight in kg/m and lb/ft, a table of standard
lengths, surface area for painting, the nearest equivalents in other standards, and an SVG
cross-section drawn to scale from the dimensions. No images to maintain; change the data and every
drawing follows.

The calculators are small TypeScript modules that Astro bundles into the page, sharing the same
weight functions as the build (no UI framework on the client). Astro inlines
the 4 KB stylesheet with build.inlineStylesheets: "always", so there is no render-blocking CSS
request.

Hosting: Workers static assets, no Worker code

Cloudflare Workers can serve a directory of static files without any script:

// wrangler.jsonc
{
  "name": "steelweight-calculator",
  "compatibility_date": "2026-09-01",
  "assets": { "directory": "./dist", "not_found_handling": "404-page" },
  "workers_dev": false,
  "preview_urls": false
}
Enter fullscreen mode Exit fullscreen mode

npm run build && wrangler deploy uploads only changed files. Two settings matter for SEO:
workers_dev: false and preview_urls: false, so the site is not also served (and indexed) on a
*.workers.dev address. The custom domain and the www → apex redirect are set up once in the
dashboard.

With trailingSlash: "always" and build.format: "directory", every URL has exactly one form,
which keeps canonical URLs, the sitemap and internal links consistent.

Release pages in batches

There are 222 section pages with real search demand, but a new domain publishing hundreds of
near-identical pages on day one is asking to be ignored. So the build only renders the first N:

export const PUBLISHED = published.sections.slice(0, release.sections).map(findSection);
Enter fullscreen mode Exit fullscreen mode

published.json lists the sections by search demand (from Keyword Planner exports), interleaving
Indian, European and American sections so every batch covers all three markets. release.json
holds N; raising it by 50 a week is the whole release process. Links on other pages use an
isPublished() check so nothing points at a page that is not built yet, and a link checker over
dist/ runs before every deploy.

Images without an image editor

Each page gets a 1200×630 social preview showing its key number ("ISMB 200 · 24.17 kg/m"), and the
chart pages get a printable PDF. Both are made with Playwright after the build: a script walks
dist/, pulls the <h1> and the answer line out of each page, fills an HTML template and takes a
screenshot. The PDFs are the chart pages printed with a print stylesheet.

The same trick makes 1000×1500 Pinterest images: the page's drawing or the first rows of its table,
trimmed in the browser until they fit:

for (;;) {
  more.textContent = hidden > 0 ? `+ ${hidden} more rows on the page` : "";
  if (main.scrollHeight <= main.clientHeight || rows.length <= 3) break;
  rows.pop().remove();
}
Enter fullscreen mode Exit fullscreen mode

An RSS feed lists those images, and Pinterest turns each new feed item into a pin. Because each
image keeps the date it was first generated, only pages from the new batch show up as new.

Data you can check

The part I spent the most time on was not the site but the data: recomputing derived columns
(mass from area, radius of gyration from inertia) for every row found misprints in the standards
themselves. I wrote that up separately, and the cleaned data is on GitHub as CSV/JSON:
steel-section-weights.

Numbers so far

  • About 180 pages built, 34 MB including images and PDFs, Lighthouse accessibility 100.
  • Cost: the domain. Cloudflare's free plan covers the hosting, analytics and DNS.
  • Deploy: one command, about 15 seconds of build and a few seconds of upload.

If you are building something similar (a calculator or reference site from structured data), the
combination of a static generator, a data folder, and a scheduled release.json bump has been
very low maintenance.