<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: WebFreesia</title>
    <description>The latest articles on DEV Community by WebFreesia (@webfreesia).</description>
    <link>https://dev.to/webfreesia</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4245037%2F4d80adba-289a-4609-8758-87d0b013ff24.png</url>
      <title>DEV Community: WebFreesia</title>
      <link>https://dev.to/webfreesia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/webfreesia"/>
    <language>en</language>
    <item>
      <title>Shipping a four-language website in 1–5 days: Readdy AI + Supabase + Resend</title>
      <dc:creator>WebFreesia</dc:creator>
      <pubDate>Sat, 14 Nov 2026 20:19:00 +0000</pubDate>
      <link>https://dev.to/webfreesia/shipping-a-four-language-website-in-1-5-days-readdy-ai-supabase-resend-hde</link>
      <guid>https://dev.to/webfreesia/shipping-a-four-language-website-in-1-5-days-readdy-ai-supabase-resend-hde</guid>
      <description>&lt;p&gt;Most small businesses do not need a bigger website. They need the same small website in four languages, online this week, for a price they can say out loud.&lt;/p&gt;

&lt;p&gt;That is the whole problem statement behind &lt;a href="https://webfreesia.com/en" rel="noopener noreferrer"&gt;WebFreesia&lt;/a&gt;, a one-person web studio I run in Seoul. Clients come with a Korean brochure site and a deadline, and they need Korean, English, Japanese and Chinese versions that a buyer in Los Angeles or Osaka can actually use — not a Google Translate widget bolted onto the header.&lt;/p&gt;

&lt;p&gt;This post is the stack and the process we use to deliver that in 1–5 days, including the parts that are boring on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Three tools do most of the work. None of them is exotic, and that is the point: a one-person studio cannot afford a stack that needs a team to babysit it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Readdy AI&lt;/strong&gt; for the first layout pass. It turns a written brief and a page list into a responsive structure — hero, sections, product grid, contact block — so the first design review happens on real pages instead of a blank canvas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; for content, products and translations. Postgres plus a tiny REST layer means the language switcher is a query parameter, not a separate build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend&lt;/strong&gt; for the things that must arrive: enquiry notifications, quote confirmations, order receipts in the customer's language and timezone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else is plain Next.js and CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content model: one page, four languages, no duplicated builds
&lt;/h2&gt;

&lt;p&gt;The mistake that eats the schedule is building four sites. Instead, there is one page record and one translation row per locale.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;hero_image&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;published&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;page_translations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;page_id&lt;/span&gt; &lt;span class="kt"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="nf"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;cascade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ko'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'en'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'ja'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'zh'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;meta_description&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A page is only published when every locale it claims to support has a row. That single rule kills the classic failure of a site that is English-complete and half-translated in Japanese, which looks worse to an overseas buyer than not having Japanese at all.&lt;/p&gt;

&lt;p&gt;Fetching is one query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pages'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'slug, hero_image, page_translations!inner(title, body, meta_description)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'page_translations.locale'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Locale-aware routing and SEO
&lt;/h2&gt;

&lt;p&gt;A language switcher that keeps the visitor on the same page is table stakes; the part people forget is telling search engines that the four URLs are the same page.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routes are &lt;code&gt;/{locale}/{slug}&lt;/code&gt; (&lt;code&gt;/en/pricing&lt;/code&gt;, &lt;code&gt;/ja/pricing&lt;/code&gt;), so a shared link keeps its language.&lt;/li&gt;
&lt;li&gt;Every page emits &lt;code&gt;hreflang&lt;/code&gt; alternates for all four locales plus &lt;code&gt;x-default&lt;/code&gt; pointing at the English version.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;html lang&amp;gt;&lt;/code&gt; and &lt;code&gt;og:locale&lt;/code&gt; follow the active locale; the default is the market you are selling into, not the language of the office.&lt;/li&gt;
&lt;li&gt;Titles and meta descriptions come from &lt;code&gt;page_translations&lt;/code&gt;, not from a template string with the brand name glued on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff is small but real: when a Japanese buyer searches the product category in Japanese, they land on the Japanese URL instead of being bounced from the English one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Email that arrives in the right language
&lt;/h2&gt;

&lt;p&gt;Enquiries are the only conversion most of these sites have, so the notification path gets the same care as the page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'WebFreesia &amp;lt;hello@webfreesia.com&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;STUDIO_INBOX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reply_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'['&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'] New enquiry from '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;renderEnquiryEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter more than the template: &lt;code&gt;reply_to&lt;/code&gt; is the visitor's address (so answering is one click, not a copy-paste), and the locale travels with the email, so the studio answers in the language the buyer wrote in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 1–5 day schedule
&lt;/h2&gt;

&lt;p&gt;Fixed-scope work is what makes the deadline believable. The clock starts after the brief, and each stage ends with an approval before the next begins.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brief &amp;amp; quote&lt;/td&gt;
&lt;td&gt;Page list, languages, deadline, then a written fixed quote&lt;/td&gt;
&lt;td&gt;Same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structure &amp;amp; design&lt;/td&gt;
&lt;td&gt;Sitemap, two homepage directions, one approved&lt;/td&gt;
&lt;td&gt;1–2 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build &amp;amp; review&lt;/td&gt;
&lt;td&gt;Responsive build, real-device testing, two revision rounds&lt;/td&gt;
&lt;td&gt;2–3 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Launch &amp;amp; handover&lt;/td&gt;
&lt;td&gt;Domain, SSL, search registration, recorded admin walkthrough&lt;/td&gt;
&lt;td&gt;Same day&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Landing pages finish in 1–2 days, corporate sites in about 3, and a store with a cart and checkout around 5. The number is a scope decision, not a promise that survives a moving brief.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, stated plainly
&lt;/h2&gt;

&lt;p&gt;Fixed pricing, quoted in USD, paid through PayPal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Launch, $300&lt;/strong&gt;: brand introduction site, 5 pages, two homepage directions, domain and SSL, basic admin, 3 months of aftercare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growth, $700&lt;/strong&gt;: 12 pages, three design directions, membership and enquiry features, live chat, 3 months of aftercare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store, $1,150&lt;/strong&gt;: online store with product variants, cart and checkout, order admin, multilingual rollout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Extra pages are $100 each, extra languages $350 each, and domain and hosting are billed at cost. Ongoing care after the included period starts at $100/month. No hourly billing, and the scope is in writing before work starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things worth knowing before you copy this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI layout tools are scaffolding, not design.&lt;/strong&gt; Readdy gets a first pass in front of the client fast; the typography, spacing and mobile behaviour still get hand-fixed. Shipping the generated output as-is is how you get a site that looks like every other generated site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four languages is a content problem, not a code problem.&lt;/strong&gt; The code took a day. Collecting and reviewing the Japanese and Chinese copy took the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the i18n model boring.&lt;/strong&gt; One table, one rule, one query. Every clever abstraction I tried first made the handover harder for the client's own admin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are building something similar, or you need the four-language version of your site online this week, the studio site is &lt;a href="https://webfreesia.com/en" rel="noopener noreferrer"&gt;webfreesia.com/en&lt;/a&gt;, and the fastest way to reach me is WhatsApp at &lt;a href="https://wa.me/821079044659" rel="noopener noreferrer"&gt;+82 10-7904-4659&lt;/a&gt; or email at &lt;a href="mailto:webfreesian@gmail.com"&gt;webfreesian@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>supabase</category>
      <category>seo</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
