Skip to content

mo3moha/mise

Repository files navigation

mise
Reservation-ready website template for small businesses in Japan.
3 languages. 1 file to brand. Deploy to Cloudflare in minutes.

Astro 6 React 19 Cloudflare Workers Tailwind 4 i18n ja/en/fr Security audited License MIT


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.

Why mise

  • 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)

Quick Start

git clone https://github.com/mo3moha/mise.git my-shop
cd my-shop
npm install
npm run dev
# Open http://localhost:4321

The demo renders a French bistro in Daikanyama with full reservation flow. Replace the demo data with your content and deploy.

Architecture

                         +-----------------------+
                         |   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).

Features

Reservation System

  • 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 CONFLICT upserts
  • Atomic D1 batch writes (reservation + event log in one transaction)

3-Language i18n

  • 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)
  • hreflang tags + x-default + locale-aware Open Graph
  • Owner emails respect SHOP_LANG env var; guest emails auto-detect from TLD

SEO

  • JSON-LD structured data with ReserveAction schema (tells Google this business accepts reservations)
  • Correct @type per shop type: Restaurant, BeautySalon, LocalBusiness
  • OpeningHoursSpecification, geo coordinates, cuisine type, price range
  • Dynamic XML sitemap with xhtml:link alternates per locale
  • robots.txt welcoming AI crawlers: GPTBot, ClaudeBot, PerplexityBot, Google-Extended

Design

  • 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-visible outlines (WCAG 2.4.1)
  • Responsive from 320px to ultrawide, mobile-first

Security (3-pass audit, 0 remaining issues)

  • 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 responses
  • X-Content-Type-Options: nosniff on all API responses
  • X-Frame-Options: DENY + Content-Security-Policy on confirm/decline pages
  • Referrer-Policy: no-referrer on 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

Legal Pages (Japan-ready)

  • Tokushoho (特定商取引法に基づく表記) -- required for Japanese commercial sites
  • Privacy Policy
  • Terms of Service

5 Shop Types

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

Deploy to Cloudflare

Prerequisites

Steps

# 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

Environment Variables

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)

Email Flow

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

Customize Your Brand

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.

Add a Language

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.

Database Schema

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.

Project Structure

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

Tech Stack

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
Email 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

EmDash CMS

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.

Contributing

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 locally

License

MIT -- 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.

About

First reservation template for EmDash CMS. Astro 6 + Cloudflare Workers + D1. 3-language i18n, full email booking flow, security audited. MIT.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors