anders leeHow 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.
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 } }));
}
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.
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
}
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.
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);
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.
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();
}
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.
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.
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.