A multilingual, offline-capable, brand-customizable guest information PWA for vacation rentals — cabins, villas, apartments, anything you let guests stay in.
A free, open-source alternative to InfoSpot, TouchStay, and other "guest info" SaaS tools. Built on Astro 5 + Tailwind 4 — fast, owned by you, no monthly fee. Mobile-first, sub-200kb first load, full offline support after the first visit.
- 📱 Mobile-first — designed at 390px, scales clean to desktop
- 🌍 Multilingual — ships with EN/DE/HR/IT; add any language with one DeepL command
- 🛜 PWA + offline — service worker precaches the whole site so guests with patchy signal can still read instructions
- 🪪 Wi-Fi tap-to-join QR — guests scan with their camera, phone joins automatically (
WIFI:URI scheme) - 🗺️ Embedded interactive map for nearby places + open-in-Maps deeplink
- ☎️ Tap-to-call emergency + host contact numbers
- 🎨 Brand-customizable — single config file (
src/config/property.ts) controls all property-specific data - 🔒
noindexby default — guest info, not search-traffic content - 🆓 No SaaS fees — host it on Vercel / Netlify / Cloudflare Pages free tier
- 🖨️ Printable A4 poster —
npm run postergenerates a fridge-ready PNG with Wi-Fi QR + guide QR - 📰 Print-friendly stylesheet — guests can print any section as clean monochrome A4
- 💸 Affiliate-link helper — earn commission from Viator / GetYourGuide / Booking / Amazon links with one config field
- 📊 Privacy-friendly analytics (optional) — Plausible / Umami / GoatCounter, no cookie banner needed
- 🤖 AI bootstrapper — paste a property description (or URL) → first-draft guide in 30 seconds via Claude
- 📍 Google Places auto-import — turn a list of place IDs into rich "Places to visit" markdown with photos, ratings, opening hours
| Home Wi-Fi QR, house rules, sections grouped by category |
Section page Safety callouts, prose styled for readability |
Explore the area Embedded map + place lists |
![]() |
![]() |
![]() |
InfoSpot, TouchStay, and similar tools want £15–£60/month per property for what is, fundamentally, a static webpage with QR codes. This template gives you the same thing in 200 KB of static HTML, deployable for free, owned by you, and customizable down to the last pixel.
| InfoSpot / TouchStay | Guest Guide | |
|---|---|---|
| Monthly cost | £15–£60 / property | $0 |
| Custom domain | Paid tier | Yes |
| Offline support | Limited | Full PWA |
| Multilingual | Paid tier | Yes, 4 languages |
| Custom design | Templates only | Full HTML/CSS control |
| Data ownership | Their servers | Your repo |
git clone https://github.com/simonseifert/guest-guide.git my-guest-guide
cd my-guest-guide
npm install
npm run dev # open http://localhost:4321ANTHROPIC_API_KEY=sk-ant-... npm run bootstrapThe CLI prompts for a property description (or paste a public listing URL) and Claude writes a full first-draft guidebook — src/config/property.ts plus 8-10 section markdown files — in about 30 seconds. Costs ~$0.15 in API credits per run. See AI bootstrap below for details.
Single source of truth for everything property-specific: name, address, host names + phone numbers, Wi-Fi SSID + password, map URLs, languages.
export const PROPERTY = {
brand: { name: 'My Cabin', shortName: 'My Cabin', logoSrc: '/logo.svg', tagline: 'Guest guide' },
address: { line1: '...', city: '...', postcode: '...', country: '...' },
taxId: null, // local tax/business ID — set to null to hide
hosts: [
{ role: 'host', name: 'Your Name', phone: '+1 555 0100' },
{ role: 'helper', name: 'Local Helper', phone: '+1 555 0101' },
],
wifi: { ssid: 'MyCabinWiFi', password: 'replace-me-please', encryption: 'WPA' },
hero: { photoSrc: '/images/hero-placeholder.svg', photoAlt: 'A cabin at twilight' },
map: { embedUrl: '...', shareUrl: '...', placeLabel: 'Address · Area' },
languages: ['en', 'de', 'hr', 'it'],
defaultLanguage: 'en',
noindex: true,
siteUrl: 'https://your-property.example.com',
affiliates: { viator: '', getYourGuide: '', booking: '', amazon: '' }, // optional
analytics: null, // optional
};| File | Replace with |
|---|---|
logo.svg |
Your wordmark (white-on-transparent, ~612×120 viewBox works best for the dark hero overlay) |
images/hero-placeholder.svg |
Your hero photo — JPG/WebP at ~1920×1280 atmospheric. Update hero.photoSrc in property.ts accordingly. |
icon-192.png, icon-512.png, icon-maskable.png, apple-touch-icon.png, icon.svg |
Regenerate via node scripts/generate-icons.mjs (edit colors at the top of the script first) |
favicon.svg |
Your favicon |
Each markdown file is one section page. Frontmatter:
---
title: Section title
icon: 💦 # emoji shown on the section page header
order: 1 # sort order within its category
category: indulge # rules | indulge | tech | stay
summary: One-line description shown on the homepage card.
safetyCritical: true # adds a safety callout banner
---
Markdown body…The four categories control how sections group on the homepage:
rules— featured prominently above the categories (typically house rules)indulge— the fun stuff (jacuzzi, sauna, BBQ, TV)tech— appliances + how-to (coffee, oven, water heater, audio system)stay— practical (check-out, area, emergency contacts)
The starter content is brand-neutral placeholder text. Read it, edit it, replace it. The category labels and homepage section eyebrows live in src/i18n/ui.ts.
npm run build # → ./dist/Anywhere static-hosted will do. Recommended: Vercel — push to GitHub, click "Import" in Vercel dashboard, done. No env vars needed.
src/
config/property.ts ← edit me first
content/sections/{en,de,hr,it}/*.md
components/
Hero, WifiCard, RulesFeature, AntlerDivider,
SectionGroup, SectionRow, MapPreview, Icon
layouts/Layout.astro ← header + footer + html shell
pages/
index.astro ← / → language-detected redirect
[lang]/index.astro ← /en, /de, /hr, /it (homepages)
[lang]/[slug].astro ← per-section pages
i18n/ui.ts ← UI strings + per-language labels
lib/remark-affiliates.mjs ← rewrites affiliate links at build time
styles/global.css ← design system tokens + component CSS
styles/print.css ← print stylesheet (single column, hide chrome)
public/
logo.svg ← your wordmark
favicon.svg, icon-*.png, apple-touch-icon.png
images/hero-placeholder.svg ← swap for your hero photo
manifest.webmanifest ← (auto-generated at build)
scripts/
bootstrap.mjs ← AI guidebook bootstrapper (Anthropic)
fetch-places.mjs ← Google Places API → places.md
generate-icons.mjs ← regenerates PWA icons from a source SVG
generate-poster.mjs ← writes dist/poster.png — fridge-ready Wi-Fi + guide QR
translate.mjs ← translate EN markdown into another language
places.config.example.json ← copy to places.config.json (gitignored) for fetch-places
- Astro 5 (static export)
- Tailwind 4 via the Vite plugin
- TypeScript strict
- @vite-pwa/astro for service worker + manifest generation (Workbox under the hood)
- qrcode for build-time Wi-Fi QR generation
- rehype-external-links so external markdown links open in new tabs
The fastest way from "blank repo" to "first-draft guidebook" is to let Claude do the typing.
ANTHROPIC_API_KEY=sk-ant-... npm run bootstrapThe CLI offers three input modes:
# Paste a public listing URL (Booking, Vrbo, your own site)
npm run bootstrap -- --url=https://example.com/listings/cabin
# Pass a description directly
npm run bootstrap -- --description="3-bedroom log cabin in Aspen with hot tub, sauna, fireplace, OFYR grill"
# Or run with no args and the CLI opens an interactive prompt
npm run bootstrapIt writes:
src/config/property.ts— name, address, host placeholder, Wi-Fi placeholder, languagessrc/content/sections/<lang>/*.md— welcome, rules, checkout, emergency, plus auto-detected amenity sections (jacuzzi, sauna, grill, tv, coffee, oven, water, avr, places, explore)
After it runs, npm run dev to preview. Always do a human pass — Claude is good at structure and tone but you need to fill in real Wi-Fi credentials, real local picks, real host phone numbers. Use --dry-run to see the plan without writing files.
The AI runs at bootstrap time, not at runtime. After it finishes, the repo is just markdown — no API key, no backend, no third-party calls. The static-PWA story is preserved.
If you have a list of nearby attractions, restaurants, and shops, you can populate a rich "Places to visit" section with one command instead of writing each entry by hand.
Use Google's Place ID Finder — search for a place, copy the ID that starts with ChIJ….
cp places.config.example.json places.config.json{
"categories": [
{
"name": "Walking distance",
"icon": "🥾",
"subhead": "under 15 min",
"places": [
{ "id": "ChIJN1t_tDeuEmsRUsoyG83frY4" },
{ "id": "ChIJ5mO0wxK0t4kRA-AiFx6_kTk" }
]
},
{
"name": "Where to eat",
"icon": "🍽️",
"places": [{ "id": "ChIJ..." }]
}
]
}GOOGLE_PLACES_API_KEY=xxx npm run fetch-placesFor each place it pulls name, address, rating, review count, editorial summary, opening hours, and the first photo (saved to public/places/). It generates src/content/sections/<lang>/places.md for every language in PROPERTY.languages. Place data is fetched in each language using Google's languageCode parameter — no separate translation pass needed.
API responses are cached in .places-cache/ for 30 days, so re-runs don't re-bill. Pass --force to bust the cache. Pass --slug attractions to use a different filename. Pass --langs en,de to limit which language files are written.
Costs: ~$0.017 per place lookup, $0.007 per photo. Free tier covers most setups (a typical 30-place list costs under $1). See the Places API pricing.
If you have a Viator, GetYourGuide, Booking.com, or Amazon Associates partner account, the template can attribute every outbound link to you with a single config edit. Hosts using paid SaaS tools cite affiliate revenue as the #1 reason they upgrade tiers — here it's free.
// src/config/property.ts
affiliates: {
viator: 'YOUR_PID', // Viator partner code
getYourGuide: 'YOUR_ID', // GetYourGuide partner ID
booking: 'YOUR_AID', // Booking.com aid parameter
amazon: 'yourtag-20', // Amazon Associates tag
},Don't have a partner account? Apply: Viator · GetYourGuide · Booking · Amazon Associates.
- [Eiffel Tower skip-the-line](viator:https://viator.com/tours/Paris/d479-12345)
- [Louvre guided tour](gyg:https://getyourguide.com/paris-l16/abc-12345)
- [Hotel near you](booking:https://booking.com/hotel/foo.html)
- [Travel adapter](amazon:https://amazon.com/dp/B07XYZ)The build strips the viator: prefix, appends your partner ID as a query parameter, and adds target="_blank" rel="sponsored noopener" (Google requires the sponsored rel for paid links — done automatically).
If you leave a partner ID empty, the link still works — it just doesn't earn you a commission. Plain markdown links work as normal.
Plug in one privacy-friendly analytics provider to learn which sections guests actually open. All three options are GDPR-compliant without a cookie banner.
// src/config/property.ts — pick ONE, leave the rest as null
analytics: { plausible: { domain: 'guest-guide.example.com' } },
// analytics: { umami: { src: 'https://umami.example.com/script.js', websiteId: 'xxx' } },
// analytics: { goatcounter: { code: 'your-code' } },
// analytics: null, // disabled (zero third-party requests)Recommended providers:
- Plausible — €9/mo or self-host (lightweight 1 KB script)
- Umami — free self-host, $20/mo cloud
- GoatCounter — free for non-commercial sites
The hosted analytics dashboard tells you "Hot tub gets 80% of section views, Music system gets 5%" — now you know where to put your effort.
When a guest's phone dies (or they just prefer paper), you want them to be able to walk to the fridge and find the Wi-Fi password and your phone number.
npm run poster
# → dist/poster.png (A4 at 300 DPI, ~1.4 MB)Two QR codes — one to join the Wi-Fi automatically, one to open the digital guide — plus the property name, Wi-Fi credentials in plain text, the host phone, and the European emergency number. All pulled from src/config/property.ts. Print at A4 with no scaling.
The site itself also has a print stylesheet — guests can print any section page (rules, hot tub instructions, check-out) as clean monochrome A4. The web chrome (header, language switcher, footer, embedded maps) is hidden automatically.
Ships with EN/DE/HR/IT. Adding a new language is one command + a two-line edit.
# 1. Generate translated markdown for the target language (e.g. Spanish):
DEEPL_API_KEY=xxx npm run translate -- es
# Or pick a different provider:
OPENAI_API_KEY=xxx npm run translate -- es --provider openai
ANTHROPIC_API_KEY=xxx npm run translate -- es --provider anthropic
GOOGLE_TRANSLATE_API_KEY=xxx npm run translate -- es --provider googleDeepL's free tier gives you 500k characters/month — more than enough for the ~30 KB of guidebook text. Get your free API key, set it as an env var, run the command. Translation takes about 30 seconds for all 13 sections.
The script preserves frontmatter (only title and summary are translated), markdown structure, links, code blocks, emoji, and [bracketed placeholders]. Pass --force to overwrite existing translations.
After translating, two more edits to make the language appear in the UI:
// src/i18n/ui.ts
export const languages = {
en: 'English',
de: 'Deutsch',
hr: 'Hrvatski',
it: 'Italiano',
es: 'Español', // ← add this
} as const;// src/config/property.ts
languages: ['en', 'de', 'hr', 'it', 'es'], // ← add 'es'UI strings (nav labels, "Read more", category headings) automatically fall back to English for any language that doesn't have its own dictionary in src/i18n/ui.ts. To get fully native-language UI, copy the en block in ui and translate the values — about 30 strings.
The translate script accepts any ISO 2-letter code your provider supports. DeepL covers ~30 languages; OpenAI/Anthropic cover ~100. Common picks for vacation rentals: es (Spanish), fr (French), nl (Dutch), pl (Polish), cs (Czech), sk (Slovak), sl (Slovenian), hu (Hungarian), pt (Portuguese), ja (Japanese), zh (Chinese).
Drop it from PROPERTY.languages, delete src/content/sections/<lang>/, and remove its entry from src/i18n/ui.ts. That's it.
Always do a human pass. Machine translation gets you 90% of the way there. The other 10% — tone, idiom, local quirks like "OIB" vs "Tax ID" — still benefits from a native speaker reading through once.
MIT — do whatever you want, no warranty.



