arukutomaru: a cross-border shop whose main market is now Korea
A small shop selling around 30 products worldwide. After we rebuilt it around five languages and multilingual SEO, it went from 80% Japanese buyers to Korea leading on both visits and revenue.
- Period
- In development and production use
- Stack
- Astro (static output) React + Tailwind CSS Hono / Cloudflare Workers Cloudflare D1 + R2 d1zzle Better Auth Stripe Storyblok (headless CMS)
- Serves Japanese, English, Korean, Chinese and Thai
- Each language has its own URLs, paired for search engines through hreflang and the sitemap
- The storefront is generated statically with Astro, so product pages ship as finished HTML
- No fixed costs, only Cloudflare usage and Stripe's payment fees
The problem it solves
The problem this project set out to solve was not cost. It was which country’s customers find the shop. arukutomaru used to run on STORES, and back then 80% of its buyers were Japanese. The products themselves sell perfectly well abroad, so we took the view that they simply were not being found, and made multilingual support and multilingual SEO the centre of the rebuild.
Customers from Korea grew from there, and today Korea leads on both visits and revenue. People who never saw the shop while it was Japanese-only now arrive on it straight from search.
Cost was the second goal. For a shop with a few dozen products, the monthly fee of a hosted commerce platform is heavy relative to what it sells, so the new stack costs nothing beyond Cloudflare usage and Stripe’s payment fees.
Multilingual support and multilingual SEO
The shop serves Japanese, English, Korean, Chinese and Thai. Three decisions mattered.
Give each language its own URLs. Japanese sits at the root (/products/2) and every other
language under a code prefix (/en/products/2). If one URL merely switches what it displays,
a search engine has no separate page to index for that language. hreflang only means something
once the URLs are split.
Do not ship a language before its copy exists. The moment a language joins the active list,
any page without a translation goes out with English content instead. That fills the index with
pages nearly identical to /en/, which Google treats as duplicates and declines to index. A
language joins the list only once its copy is ready.
Serve product pages as static HTML. Astro generates them ahead of time, so a crawler reads the text in each language without waiting for JavaScript. They come from a CDN edge, which keeps them fast from abroad, and for a cross-border shop that difference turns directly into orders.
How it is put together
It is a pnpm workspaces and Turborepo monorepo, split into three applications.
| App | What it is |
|---|---|
apps/web | The public storefront. Astro (static) with React and Tailwind |
apps/admin | The product-management SPA. Vite and React, protected by Cloudflare Access |
apps/api | A Hono REST API on Cloudflare Workers, talking to D1 and R2 |
The API is REST, and the front end calls it through Hono’s typed RPC client
(hc<AppType>). As long as route handlers are written as a method chain, the API’s types
are the front end’s types, and a changed endpoint fails the build.
Product images live in R2. Only editorial content such as blog posts goes through Storyblok, a headless CMS; the product data itself deliberately stays out of the CMS.