mise
Reservation-ready website template for small businesses in Japan.
3 languages. 1 file to brand. Deploy to Cloudflare in minutes.
mise (French: mise en place -- everything in its place) is a production-ready Astro 6 template that gives restaurants, salons, studios, and creators a professional multilingual website with a complete reservation system. No SaaS fees. You own the code, the data, and the domain.
Built for foreign entrepreneurs running businesses in Japan -- manage your site in English or French, serve your customers in Japanese. Also built for agencies and developers who need to ship client sites fast.
- You are opening a shop in Japan and need a site that speaks to local customers in Japanese while you manage it in English or French
- You want reservations without paying monthly for a booking platform
- You want to deploy once and forget about it -- Cloudflare free tier handles the rest
- You need Japanese legal compliance out of the box (tokushoho, privacy policy, terms)
git clone https://github.com/mo3moha/mise.git my-shop
cd my-shop
npm install
npm run dev
# Open http://localhost:4321The demo renders a French bistro in Daikanyama with full reservation flow. Replace the demo data with your content and deploy.
+-----------------------+
| Cloudflare CDN |
| (static pages) |
+-----------+-----------+
|
+----------------+----------------+
| |
+---------+----------+ +----------+---------+
| Static Pages | | API Workers |
| (prerendered) | | (on-demand) |
+--------------------+ +----------+---------+
| / (ja) | | POST /api/reserve |
| /en/ (en) | | GET /api/reserve/ |
| /fr/ (fr) | | [id]/confirm |
| /privacy | | GET /api/reserve/ |
| /terms | | [id]/decline |
| /tokushoho | +----------+---------+
| /sitemap.xml | |
| /robots.txt | v
+--------------------+ +----------+---------+
| Cloudflare D1 |
| (SQLite) |
+----------+---------+
|
v
+----------+---------+
| Resend |
| (transactional |
| email) |
+--------------------+
Static + SSR hybrid: Pages are prerendered at build time for near-zero latency. Only the reservation API runs as Cloudflare Workers with scale-to-zero billing. Zero JS shipped until the reservation form scrolls into view (React 19 island with client:visible).
- Multi-step form with date scroll picker, time slot grid, party size selector
- React 19 island -- hydrates only when the form enters the viewport
- Honeypot spam protection + Zod validation + duplicate detection
- Owner receives email with one-click Confirm / Decline buttons
- Guest receives instant receipt, then confirmation or decline notification
- All emails rendered in the recipient's language (ja/en/fr)
- Cryptographic confirm tokens via Web Crypto API with 7-day expiry
- IP hashing with daily-rotating salt (privacy-first, no raw IPs stored)
- UTM tracking + referrer capture for reservation analytics
- Customer deduplication by email with
ON CONFLICTupserts - Atomic D1 batch writes (reservation + event log in one transaction)
- Japanese (default, no URL prefix),
/en/,/fr/ - 80+ translated keys: UI strings, form labels, email templates, section headings
- Section labels adapt to shop type ("Menu" for restaurants, "Services" for salons, "Carte" in French)
hreflangtags +x-default+ locale-aware Open Graph- Owner emails respect
SHOP_LANGenv var; guest emails auto-detect from TLD
- JSON-LD structured data with
ReserveActionschema (tells Google this business accepts reservations) - Correct
@typeper shop type:Restaurant,BeautySalon,LocalBusiness OpeningHoursSpecification, geo coordinates, cuisine type, price range- Dynamic XML sitemap with
xhtml:linkalternates per locale robots.txtwelcoming AI crawlers: GPTBot, ClaudeBot, PerplexityBot, Google-Extended
- Dark mode with system preference detection + manual toggle + localStorage persistence
- CSS custom properties -- one file (
brand.css) re-skins everything - Scroll-reveal animations that respect
prefers-reduced-motion - Skip-to-content link +
:focus-visibleoutlines (WCAG 2.4.1) - Responsive from 320px to ultrawide, mobile-first
- Zod schema validation on all API inputs with strict regex patterns
- Parameterized SQL everywhere -- zero string interpolation in queries
escapeHtml()on all dynamic output in emails and HTML responsesX-Content-Type-Options: nosniffon all API responsesX-Frame-Options: DENY+Content-Security-Policyon confirm/decline pagesReferrer-Policy: no-referreron admin-facing pages- IDOR protection: token + reservation ID validated together (no existence probing)
- Confirm tokens nullified after use (single-use links)
- IP hashing via SHA-256 with daily-rotating salt
- Referrer URLs stripped of query params before storage
- Tokushoho (特定商取引法に基づく表記) -- required for Japanese commercial sites
- Privacy Policy
- Terms of Service
mise adapts labels and structured data based on your business type:
| Type | Section Label (ja/en/fr) | JSON-LD @type |
|---|---|---|
restaurant |
メニュー / Menu / Carte | Restaurant |
salon |
メニュー / Services / Prestations | BeautySalon |
studio |
料金・コース / Plans & Courses / Tarifs & Cours | LocalBusiness |
creator |
サービス / Services / Services | LocalBusiness |
general |
サービス・料金 / Services & Pricing / Services & Tarifs | LocalBusiness |
- A Cloudflare account (free tier works)
- Wrangler CLI
- A Resend account for transactional email
# 1. Create D1 database
wrangler d1 create mise
# 2. Copy the database_id from the output into wrangler.toml
# 3. Apply the schema
wrangler d1 execute mise --file=db/0001_initial.sql
# 4. Set secrets (never committed to source)
wrangler secret put RESEND_API_KEY
wrangler secret put NOTIFICATION_EMAIL # where you receive reservation alerts
wrangler secret put FROM_EMAIL # sender address (verify in Resend first)
wrangler secret put SITE_URL # https://yourshop.com
# 5. Update site URL in astro.config.mjs
# site: "https://yourshop.com"
# 6. Build and deploy
npm run build
wrangler deploy| Variable | Required | Description |
|---|---|---|
RESEND_API_KEY |
Yes | Resend API key for transactional email |
NOTIFICATION_EMAIL |
Yes | Email address that receives reservation alerts |
FROM_EMAIL |
Yes | Sender email (must be verified domain in Resend) |
SITE_URL |
Yes | Full public URL for generating email links |
SHOP_LANG |
No | Owner's preferred language for admin emails (ja/en/fr, default: ja) |
Guest submits reservation form
|
v
POST /api/reserve
|-- Zod validation
|-- Honeypot check
|-- Duplicate detection (same email + date + time)
|-- Upsert customer record
|-- Insert reservation + event (atomic D1 batch)
|
+---> [waitUntil: fire-and-forget]
|
+---> Receipt email --> GUEST
| "We received your request. Please wait for confirmation."
| (Language: auto-detected from guest email TLD)
|
+---> Notification email --> OWNER
"New reservation from Tanaka-san, 2026-04-12 19:00, 4 guests"
[ Confirm ] [ Decline ]
(Language: SHOP_LANG env var, default: ja)
|
+---------+---------+
| |
Owner clicks Owner clicks
[Confirm] [Decline]
| |
v v
GET .../confirm GET .../decline
| |
Shows HTML form Shows HTML form
with POST button with POST button
| |
v v
POST (submit) POST (submit)
| |
- status='confirmed' - status='declined'
- Token nullified - Token nullified
- Event logged - Event logged
| |
v v
Confirmation Decline email
email --> GUEST --> GUEST
All visual theming lives in one file. Create or edit public/brand.css:
/* public/brand.css */
:root {
/* Colors */
--color-brand-primary: #1a1a1a;
--color-brand-accent: #c8a96e; /* Change this to your brand color */
--color-brand-surface: #fafaf8;
--color-brand-surface-2: #f0ede8;
--color-brand-text: #2d2d2d;
--color-brand-text-muted:#6b6b6b;
--color-brand-border: #e2ddd8;
/* Typography */
--font-display: "Cormorant Garamond", "Noto Serif JP", Georgia, serif;
--font-body: "DM Sans", system-ui, sans-serif;
/* Hero background (optional) */
--hero-image: url("/images/hero.jpg");
}
/* Dark mode overrides */
[data-theme="dark"] {
--color-brand-primary: #f5f0eb;
--color-brand-accent: #d4a96a;
--color-brand-surface: #141210;
--color-brand-surface-2: #1e1b18;
--color-brand-text: #e8e2da;
--color-brand-text-muted:#9a9086;
--color-brand-border: #2e2a26;
}Change the accent color, swap fonts, add a hero image -- the entire site updates. Both light and dark modes.
All translations live in src/i18n/translations.ts. To add a new language (example: Chinese):
1. Add the locale to the languages map:
export const languages = {
ja: "日本語",
en: "English",
fr: "Français",
zh: "中文", // new
} as const;2. Add a zh block to the ui object with all 80+ translation keys (copy en as a starting point).
3. Add a zh block to emailStrings in src/lib/services/email.ts for email translations.
4. Create src/pages/zh/index.astro:
---
import Layout from "../../layouts/Base.astro";
import SeoHead from "../../components/SeoHead.astro";
import ShopPage from "../../components/ShopPage.astro";
import { getDemoShop } from "../../lib/demo-data";
const lang = "zh" as const;
const shop = getDemoShop(lang);
---
<Layout title={shop.name} description={shop.concept} lang={lang}>
<SeoHead slot="head" lang={lang} shopName={shop.name} description={shop.concept}
shopType={shop.shopType} cuisineType={shop.cuisineType}
address={shop.address} phone={shop.phone} businessHours={shop.businessHours} />
<ShopPage lang={lang} shop={shop} />
</Layout>5. Add "zh" to the locales array in astro.config.mjs.
6. Add the new locale to the sitemap in src/pages/sitemap.xml.ts.
Three tables in Cloudflare D1 (SQLite), defined in db/0001_initial.sql:
customers -- Deduplicated by email via ON CONFLICT. Tracks first/last visit timestamps.
reservations -- Full lifecycle with a status machine:
pending --> confirmed --> completed
| |
+--> declined +--> no_show
|
+--> cancelled
Includes analytics columns: lead_days, day_of_week, hour_bucket, UTM params, and hashed IP. Reservation analytics without third-party tools.
reservation_events -- Immutable audit log of every status transition with actor and timestamp.
mise/
astro.config.mjs # Astro 6 config (static output, i18n, CF adapter)
wrangler.toml # Cloudflare Workers + D1 config
db/
0001_initial.sql # D1 schema (customers, reservations, events)
public/
brand.css # Create this -- your brand overrides
src/
i18n/
translations.ts # 80+ keys x 3 languages
layouts/
Base.astro # HTML shell, dark mode, hreflang, skip link
components/
SeoHead.astro # JSON-LD, OGP, canonical, hreflang
ShopPage.astro # 6-section page (hero, menu, info, access, reserve)
LanguageSwitcher.astro # Language dropdown
reservation/
ReservationForm.tsx # React 19 island (client:visible)
DateScrollPicker.tsx # Horizontal scrolling date picker
TimeSlotPicker.tsx # Time slot grid
PartySizePicker.tsx # +/- party size control
useReservationForm.ts # Form state management hook
lib/
demo-data.ts # Sample bistro data (replace with your content)
token.ts # Web Crypto token generation + IP hashing
schemas/
config.ts # ShopType, BusinessHour, JSON-LD type mapping
reservation.ts # Zod validation schema for reservation input
services/
email.ts # 3-language email templates via Resend
enrichment.ts # Lead days, day of week, hour bucket calculations
utils/
escape-html.ts # XSS prevention
normalize-email.ts # Email normalization
pages/
index.astro # / (Japanese, default locale)
en/index.astro # /en/ (English)
fr/index.astro # /fr/ (French)
404.astro
privacy.astro
terms.astro
tokushoho.astro # Japanese commerce law (特商法)
sitemap.xml.ts # Dynamic XML sitemap with hreflang alternates
robots.txt.ts # AI-crawler-friendly robots.txt
api/
reserve.ts # POST: create reservation
reserve/[id]/
confirm.ts # GET/POST: owner confirms reservation
decline.ts # GET/POST: owner declines reservation
styles/
global.css # CSS custom properties, dark mode, animations
| Layer | Technology | Role |
|---|---|---|
| Framework | Astro 6 | Static pages + SSR API routes in one project |
| Islands | React 19 | Interactive reservation form, hydrated on scroll |
| Styling | Tailwind CSS 4 | Utility-first, tree-shaken, zero unused CSS |
| Database | Cloudflare D1 (SQLite) | Edge database, generous free tier |
| Runtime | Cloudflare Workers | Scale-to-zero, globally distributed |
| Resend | Developer-friendly transactional email API | |
| Validation | Zod 4 | Runtime input validation with strict schemas |
| Crypto | Web Crypto API | Token generation + IP hashing, zero dependencies |
mise is designed as a template for EmDash CMS. The demo uses demo-data.ts as a content source -- replace getDemoShop() with your CMS data fetch when connecting to EmDash.
Contributions welcome. Please open an issue first to discuss what you would like to change.
npm run dev # Dev server on :4321
npm run build # Production build
npm run preview # Preview build locallyMIT -- use it for client projects, SaaS, personal sites, whatever you want. Attribution appreciated but not required.
Built by moha -- hospitality professional turned developer.
Halekulani. Marriott. Code.