"Možná nepotřebujete AWS."
Hosting price comparison calculator for the Czech market. Compare AWS, Hetzner, MasterDC, and Forpsi for typical small-business workloads — with hidden costs surfaced up-front, opinionated articles, and a weekly auto-refresh of vendor prices.
Versioning: CalVer (vYYYY.MM.DD, derived from data refresh) · Live: https://kalkulackahostingu.cz
Pick a project type (static site / WordPress shop / API+DB / high-traffic), adjust sliders for traffic / DB / storage / transfer, and the page shows the monthly cost on each provider — picking the cheapest fitting package, adding overages, and listing the gotchas you'd otherwise read about on a forum at 2 AM.
All calculation is client-side. Prices live in data/providers.json and
refresh weekly via a GitHub Action.
- Next.js 16 (App Router, static export →
out/) - TypeScript (strict)
- Tailwind CSS v4 with hand-rolled Material 3 design tokens — color roles, elevation, motion easing, full M3 type scale
- Cloudflare Pages for hosting; Cloudflare DNS + CDN
@xyflow/react— interactive codebase map on/o-mne(route-split, doesn't load on the calculator)tsx— runs the weekly price-refresh script in CI
No server runtime — every page is prerendered HTML + JS at build time.
Requires Node.js 20+.
npm install
npm run dev # http://localhost:3000
npm run build # static export → ./out
npm run lint
npx tsc --noEmit # type check| Route | What it is |
|---|---|
/ |
Hero · calculator · articles teaser · contact CTA · methodology |
/clanky |
Article index (cards grid) |
/clanky/proc-aws-neni-pro-male-firmy |
Opinion piece on AWS for small firms |
/clanky/proc-mit-data-v-cr |
Compliance / latency angle for CZ datacenter |
/clanky/pet-skrytych-nakladu-aws |
Concrete numbers on AWS hidden costs |
/o-mne |
Personal positioning lead · about Martin (DevOps / Linux / AI systems) · project version · codebase map · key code snippet |
data/
providers.json # single source of truth for all prices
src/
app/
layout.tsx # SEO meta, Inter + JetBrains Mono, theme init script
page.tsx # home — hero + calculator + articles teaser + CTA
globals.css # M3 design tokens (class-based light/dark)
icon.svg # Next App Router favicon (auto-detected)
clanky/
page.tsx # article index
<slug>/page.tsx # one file per article (×3)
o-mne/
page.tsx # about + version chip + codebase map
components/
CalculatorApp.tsx # stateful client component — owns scenario + input
ScenarioPicker.tsx # 4 preset scenarios as M3 chip-cards
InputSliders.tsx # traffic / DB / storage / transfer + toggles
ResultsTable.tsx # sorted list of providers
ProviderCard.tsx # per-provider card with breakdown + Vantage link
Recommendation.tsx # context-aware suggestion box
HiddenCosts.tsx # gotchas (NAT Gateway, snapshots, …)
ContactCTA.tsx # mailto for consultations
ArticlesTeaser.tsx # top-3 article cards on home
ArticleCard.tsx # single article card (index + teaser)
ArticleLayout.tsx # shared article shell — header, prose, footer CTA
Header.tsx # nav + logo + ThemeToggle
Footer.tsx # data freshness + version label
ThemeToggle.tsx # light/dark toggle, useSyncExternalStore
CodebaseMap.tsx # React Flow node-link graph on /o-mne
ui/ # Card, Chip, Slider, Switch (M3 primitives)
lib/
types.ts # canonical types — read this first
providers.ts # loads JSON, exports providers + scenarios
calculator.ts # pickPackage + overage math + currency conversion
articles.ts # article metadata (slug, title, date, tags)
version.ts # APP_VERSION — bump with package.json
public/
logo.svg # full wordmark version (mark + "kalkulackahostingu.cz")
logo-mark.svg # square mark for OG / social
scripts/
fetch-prices.ts # weekly price refresher (see below)
.github/
workflows/
check-prices.yml # weekly cron + PR / issue automation
Each Provider has:
- A list of packages (real SKUs the vendor publishes —
CCX13,EC2 t3.small,VPS Start) withvcpu,ramGB, included storage and transfer, a price in the vendor's native currency, and asource.url+fetchedAt. - Overage rates for storage and transfer above what the package includes.
- Optional
managedDb(currently only AWS RDS) for vendors that bill databases separately from the VPS.
calculate(input, provider, rates):
- Estimates needed
vcpu+ramGBfrommonthlyVisitors. pickPackage()returns the cheapest SKU withvcpu ≥ needed && ram ≥ needed, or marksinsufficientCapacityand falls back to the largest available.- Adds extra-storage / extra-transfer / managed-DB / CDN costs using the provider's overage rates.
- Converts everything to CZK via
exchangeRate.
The calculator never mutates providers.json — it consumes it. AWS package
cards also deep-link to <https://instances.vantage.sh/aws/ec2/> for
benchmarks, network performance, and Reserved/Spot pricing the calculator
doesn't try to reproduce.
Hosting prices change rarely (months), but they do change. Manual quarterly updates is how comparison sites quietly go stale. Instead:
scripts/fetch-prices.tspulls live prices each week:- AWS — official Bulk Pricing API
for EC2 + RDS in
eu-central-1(no auth, ~150 MB JSON). - Hetzner — hand-maintained: their Cloud API now requires a project
token for every endpoint (incl.
/v1/pricing), and the public pricing page is JS-rendered. Reviewed quarterly. - MasterDC, Forpsi — HTML scrape (anchor-tag-specific CZK price regex).
- Exchange rates — ECB daily reference rates XML.
- AWS — official Bulk Pricing API
for EC2 + RDS in
- The script validates the new JSON before writing (positive amounts, known currencies, source metadata present) and refuses to write a broken file.
.github/workflows/check-prices.ymlruns the fetcher every Monday at 06:00 UTC (and on manual dispatch). It:- Opens a PR with a price-change diff table (
Before | After | Δ%) if anything changed. - Opens an issue if any per-package fetcher errored — most likely cause is a Czech provider changing their HTML.
- Fails the workflow red if validation rejected the new file.
- Opens a PR with a price-change diff table (
Merging the PR triggers a Cloudflare Pages redeploy automatically. You never
auto-commit prices to main — a human eyeballs the diff first.
You can run the fetcher locally:
npm run fetch-pricesThree launch posts live under /clanky. Each is a plain TSX page — no MDX
dependency, no CMS. Article metadata (slug, title, excerpt, date, tags,
reading minutes) lives in src/lib/articles.ts so the index page and home
teaser stay in sync automatically.
The ArticleLayout component handles the shared header (tags + title +
date), prose container, and a footer that links back to the calculator. Each
article body is real prose, not lorem ipsum.
If you scale past ~5 posts and JSX-in-page editing starts feeling clunky,
that's the moment to add @next/mdx and migrate the bodies — until then,
plain TSX is the simpler choice.
- Logo concept — a cloud + three descending bars. Reads as Kč (the
cloud sits where the
čháček would), as a price comparison chart (descending bars = lower price), and as cloud hosting. Hard-coded colors inpublic/logo*.svg(for OG / social); the in-app Header useshsl(var(--md-primary))so it follows the active theme. - Light / dark mode — class-based (
<html class="dark">). The inlinethemeInitScriptinlayout.tsxruns before first paint to avoid FOUC: readslocalStorage, falls back to OS preference. TheThemeTogglecomponent reflects the current state viauseSyncExternalStorelistening to<html class>changes. - Design system — M3 color roles, elevation 1–5, full M3 type scale, and
motion easing tokens in
src/app/globals.css. No external UI library; every primitive is insrc/components/ui/.
This project uses calendar versioning (vYYYY.MM.DD) for the user-facing
label, not SemVer. The version is computed in src/lib/version.ts directly
from data/providers.json's lastUpdated field — every weekly price refresh
automatically bumps the version.
Why CalVer:
- The meaningful "release" of a price-comparison tool is when prices change, not when code commits. SemVer would conflate the two and bury the user-relevant signal ("are these numbers fresh?").
- Visitors read freshness directly from the version label — no separate "last updated" line needed.
- Removes the bikeshed of "is this a patch or a minor?" — the weekly cron decides automatically.
package.json keeps a SemVer-shaped version field (0.1.0) for npm /
tooling compatibility; only the user-facing label uses CalVer. They're
intentionally decoupled — bump package.json only on breaking changes you
want to signal to downstream consumers.
- Add a
Providerentry todata/providers.jsonwith at least one package (correctvcpu/ramGB/includedStorageGB/includedTransferGBand a publishedsource.url). - Add it to the
ProviderIdunion insrc/lib/types.ts. - Add a fetcher in
scripts/fetch-prices.tsand wire it into theswitchinmain(). - Add a node to
CodebaseMap.tsxif you want it on the architecture map. - Run
npm run fetch-pricesto confirm prices come through.
The UI requires no changes — it iterates over providers from the JSON.
Append a Scenario to data/providers.json with sensible defaults. The
calculator and UI pick it up automatically. Optionally list providers in
bestFor so Recommendation.tsx can prefer them when relevant.
- Append a metadata entry to
src/lib/articles.ts(top of the array — newest first). - Create
src/app/clanky/<slug>/page.tsximportingArticleLayoutand passing the meta. Write the body as plain JSX/prose. - Index page (
/clanky) and the home teaser pick it up automatically.
Cloudflare Pages → connected to this repo's main branch:
- Build command:
npm run build - Output directory:
out - Node version: 20
DNS for kalkulackahostingu.cz is on Cloudflare; SSL is auto-issued via
Let's Encrypt. Custom domain wired in the Pages project settings.
All numbers shown are orientational. They model the dominant cost lines but cannot account for every billable item (e.g. AWS region-to-region transfer, private link, optional backups). Always validate against the provider's own calculator before signing up.
MIT.
Martin Baránek · martin.baranek@outlook.com