Three months ago I started taking on digital transformation work for small Taiwanese B&Bs. Not the consulting-deck kind — the kind where the owner has been running a WhatsApp group to handle reservations, has no website, or is entirely dependent on Booking.com taking a cut of every room. By the time I wrapped up, I had built five websites covering nine guesthouses across the island.
The brief was consistent: get these owners off third-party platforms, give them something fast enough to not lose a guest on mobile, and make sure they could update their own room listings and pricing without calling me. The tech choices fell out of those constraints.
All five run on Astro 6 with Tailwind CSS v4. Going in I figured the second and third sites would be mostly copy-paste. They weren’t. Each one surfaced something the previous one hadn’t.
Why Astro for This Use Case
B&B marketing sites are mostly read traffic. Guests land, browse rooms, check prices, decide. The interactive surface is small — a booking form, maybe a date picker. Everything else is HTML that never changes between requests.
Astro’s island architecture fits this naturally: ship static HTML for the 95% of the page that’s content, hydrate only the booking widget. The output is fast by default without manual optimization. For owners who can’t afford bad first impressions on slow rural connections, that matters.
The First Real Decision: SSG or SSR?
This was the split that mattered most across the five projects.
Lightstayshengchun is a small guesthouse — clean rooms, fixed pricing, a contact number in the footer. No booking forms, no dynamic anything. Static site, deployed to Firebase Hosting. Done. Cold start doesn’t matter when there’s no server.
The others needed server-side features: contact forms that actually send email, dynamic pricing that changes by season or availability. For those I reached for @astrojs/node and deployed to Railway. Astro’s adapter story here is pretty clean — you swap the adapter and the rest of your code stays the same.
The honest gotcha: Railway’s free tier cold starts are noticeable. First request after inactivity takes a few seconds. I added Cloudflare in front of all the SSR sites to cache what I could at the edge, which helped a lot for the marketing pages even if it doesn’t help the form endpoints.
One architectural note: Cloudflare in front of Railway means guests hitting the room listing pages almost always get a cached response. The SSR cost is only paid on form submissions and the occasional cache miss. For the price point these clients are at, that tradeoff works.
Responsive Images Were More Work Than I Expected
Every client sent me a folder of high-res JPEGs. Some were 8MB each. On mobile that’s brutal — and for guesthouses trying to compete with OTA listings, a slow image load is a conversion killer.
I wrote a script convert-to-webp.mjs that takes every image in the source folder and spits out three sizes — 480, 960, and 1600px — in both WebP and AVIF:
import sharp from "sharp";
import { readdir } from "fs/promises";
const widths = [480, 960, 1600];
const formats = ["webp", "avif"];
for (const file of await readdir("./src/assets/raw")) {
for (const width of widths) {
for (const fmt of formats) {
await sharp(`./src/assets/raw/${file}`)
.resize(width)
.toFormat(fmt)
.toFile(`./public/images/${file}-${width}.${fmt}`);
}
}
}
Then in the Astro component I wire up srcset manually. It’s not glamorous but it works. For super-inn the total image weight went from around 14MB to under 1MB — roughly 92% reduction. That’s the kind of number clients actually understand.
Font Loading Was a Surprising Pain Point
Tailwind v4 changed how fonts work compared to v3, and on top of that, Google Fonts was blocking first paint on a couple of sites. The fix that actually worked was the classic media="print" swap trick:
<link
rel="preconnect"
href="https://fonts.googleapis.com"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Noto+Serif+TC&display=swap"
media="print"
onload="this.media='all'"
/>
Preconnect gets the DNS lookup happening early, and the media="print" means the browser doesn’t treat it as render-blocking. Once it loads, onload flips it to all. Nothing novel here, but I had to rediscover it each time until I just built it into my base layout template.
The Admin Panel Problem
Three of the five clients wanted to update their own promotional banners and room cards without touching code. This is the problem that most digital transformation projects for small businesses get wrong — you hand the owner a Notion export or a CMS login and they never touch it again because the mental model doesn’t match how they think about their business.
I didn’t want to stand up a CMS or a database for something this lightweight. Most of these owners would never log into a Contentful dashboard. What they needed was something that felt like editing a form, not managing content.
I ended up using the /astro-promo-admin skill — a web admin interface that lets clients edit banner copy and card content through a form, then commits the changes via Octokit directly to the deploy branch. When the branch updates, the CI/CD pipeline rebuilds and deploys. The client never sees a terminal.
The trade-off is that deploys take a minute or two instead of being instant. For B&B sites updating seasonal promotions, nobody cares. If this were a news site, it’d be a dealbreaker.
The deeper point: before this, updating room pricing meant the owner calling me, waiting a day, and paying for an hour of my time. Now they do it themselves. That shift — from “call the developer” to “edit and save” — is the actual value delivered, not the Astro build.
What Tailwind v4 Actually Changed
I was a bit nervous about v4 because I had v3 muscle memory. In practice, the CSS-first configuration — defining your design tokens in a .css file instead of tailwind.config.js — took about a day to adjust to and then felt cleaner. Especially for projects with shared color palettes across multiple pages.
One thing that bit me: some v3 utility names changed or got removed. I ran into this with ring utilities and a couple of gradient helpers. Nothing that took more than 20 minutes to fix, but worth knowing if you’re migrating.
Would I Use This Stack Again?
Yes, for this use case. Astro’s island architecture means the pages are fast by default, and Tailwind v4 keeps the styling consistent without needing a component library. The SSG/SSR split per project gives me flexibility without committing to a single deployment model.
The part I’d do differently: write the image conversion script once and put it in a shared tooling repo instead of copying it into each project. By the fifth site, I was maintaining four slightly-different versions of the same script.
The broader lesson: the technical choices here — Astro over Next.js, GitHub API over a CMS, Firebase for static and Railway for SSR — weren’t made in the abstract. They were made because these owners needed fast, cheap, maintainable sites they could actually operate. The stack follows the constraint, not the other way around.