[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site:languages:en":3,"site:settings:en":22,"site:navigation:en":43,"post:shipping-a-multilingual-site-without-duplicating-your-codebase:en":169,"post:related:shipping-a-multilingual-site-without-duplicating-your-codebase:en":248},[4,13],{"id":5,"code":6,"name":7,"nativeName":7,"direction":8,"flagEmoji":9,"isDefault":10,"isActive":10,"sortOrder":11,"createdAt":12,"updatedAt":12},"cms5x82ws0000p90jaubd6xdp","en","English","LTR","🇬🇧",true,0,"2026-07-29T10:07:30.747Z",{"id":14,"code":15,"name":16,"nativeName":17,"direction":8,"flagEmoji":18,"isDefault":19,"isActive":10,"sortOrder":20,"createdAt":21,"updatedAt":21},"cms5x82x90001p90je35iy8zx","id","Indonesian","Bahasa Indonesia","🇮🇩",false,1,"2026-07-29T10:07:30.766Z",{"site.name":23,"site.shortName":24,"site.logo_dark":25,"site.logo_light":26,"site.url":27,"site.defaultLocale":6,"contact.email":28,"contact.sales_email":29,"contact.phone":30,"contact.whatsapp":31,"contact.address":32,"contact.map_embed_url":33,"contact.office_hours":34,"company.legal_name":23,"company.founded":35,"seo.default_title":36,"seo.default_description":37,"seo.twitter_handle":38,"seo.default_og_image":39,"social.links":40,"features.newsletter_enabled":41,"features.blog_enabled":41,"features.careers_enabled":41,"integrations.google_analytics_id":42,"integrations.recaptcha_site_key":42},"PT Moodkuit Solutions Technologies","Moodkuit","\u002Fimages\u002Flogo\u002Fblack-logo.png","\u002Fimages\u002Flogo\u002Fwhite-logo.png","https:\u002F\u002Fmoodkuit.com","hello@moodkuit.com","sales@moodkuit.com","+62 21 5010 8899","+62 812 1000 8899","Menara Kuningan, 12th Floor, Jl. H. R. Rasuna Said, Jakarta Selatan 12940, Indonesia","https:\u002F\u002Fmaps.google.com\u002F?q=-6.2238,106.8317","Mon–Fri, 09:00–18:00 WIB","2018","Moodkuit — Enterprise Software House","Moodkuit designs, builds and operates enterprise software platforms — custom systems, web, mobile, cloud and data.","@moodkuit","\u002Fimages\u002Fhero\u002Fhero-slider.jpg","[{\"platform\":\"linkedin\",\"url\":\"https:\u002F\u002Flinkedin.com\u002Fcompany\u002Fmoodkuit\"},{\"platform\":\"github\",\"url\":\"https:\u002F\u002Fgithub.com\u002Fmoodkuit\"},{\"platform\":\"x\",\"url\":\"https:\u002F\u002Fx.com\u002Fmoodkuit\"},{\"platform\":\"instagram\",\"url\":\"https:\u002F\u002Finstagram.com\u002Fmoodkuit\"}]","true","",{"HEADER":44,"FOOTER_PRIMARY":93,"FOOTER_SECONDARY":106,"LEGAL":127,"SOCIAL":143},[45,52,57,63,69,75,81,87],{"id":46,"href":47,"target":48,"icon":49,"sortOrder":11,"label":50,"caption":49,"children":51},"cms5x85l800nvp90jgvd1p33h","\u002F","_self",null,"Home",[],{"id":53,"href":54,"target":48,"icon":49,"sortOrder":20,"label":55,"caption":49,"children":56},"cms5x85lq00o1p90j6pq2ixue","\u002Fabout","About",[],{"id":58,"href":59,"target":48,"icon":49,"sortOrder":60,"label":61,"caption":49,"children":62},"cms5x85m700o7p90jfrpuma9c","\u002Fservices",2,"Services",[],{"id":64,"href":65,"target":48,"icon":49,"sortOrder":66,"label":67,"caption":49,"children":68},"cms5x85ml00odp90jhbidzcte","\u002Fportfolio",3,"Portfolio",[],{"id":70,"href":71,"target":48,"icon":49,"sortOrder":72,"label":73,"caption":49,"children":74},"cms5x85mw00ojp90jm01w80sb","\u002Fcase-studies",4,"Case Studies",[],{"id":76,"href":77,"target":48,"icon":49,"sortOrder":78,"label":79,"caption":49,"children":80},"cms5x85n700opp90j8c67qr9t","\u002Fblog",5,"Blog",[],{"id":82,"href":83,"target":48,"icon":49,"sortOrder":84,"label":85,"caption":49,"children":86},"cms5x85nk00ovp90jzoeadygb","\u002Fcareers",6,"Careers",[],{"id":88,"href":89,"target":48,"icon":49,"sortOrder":90,"label":91,"caption":49,"children":92},"cms5x85nu00p1p90jb67ikv77","\u002Fcontact",7,"Contact",[],[94,97,100,103],{"id":95,"href":54,"target":48,"icon":49,"sortOrder":11,"label":55,"caption":49,"children":96},"cms5x85od00p8p90j616esl9i",[],{"id":98,"href":83,"target":48,"icon":49,"sortOrder":20,"label":85,"caption":49,"children":99},"cms5x85ot00pep90jrto5fvh7",[],{"id":101,"href":71,"target":48,"icon":49,"sortOrder":60,"label":73,"caption":49,"children":102},"cms5x85p400pkp90jd8usqva3",[],{"id":104,"href":89,"target":48,"icon":49,"sortOrder":66,"label":91,"caption":49,"children":105},"cms5x85pg00pqp90jvlmbrz8q",[],[107,112,117,122],{"id":108,"href":109,"target":48,"icon":49,"sortOrder":11,"label":110,"caption":49,"children":111},"cms5x85q100pxp90juyka7vco","\u002Fservices\u002Fcustom-software-development","Custom Software",[],{"id":113,"href":114,"target":48,"icon":49,"sortOrder":20,"label":115,"caption":49,"children":116},"cms5x85qj00q3p90jdh85gytx","\u002Fservices\u002Fweb-platform-engineering","Web Platforms",[],{"id":118,"href":119,"target":48,"icon":49,"sortOrder":60,"label":120,"caption":49,"children":121},"cms5x85r400q9p90jdhsjcffv","\u002Fservices\u002Fmobile-application-development","Mobile Apps",[],{"id":123,"href":124,"target":48,"icon":49,"sortOrder":66,"label":125,"caption":49,"children":126},"cms5x85rw00qfp90j1rs0xc9k","\u002Fservices\u002Fcloud-devops","Cloud & DevOps",[],[128,133,138],{"id":129,"href":130,"target":48,"icon":49,"sortOrder":11,"label":131,"caption":49,"children":132},"cms5x85sm00qmp90jki97thzi","\u002Fprivacy","Privacy Policy",[],{"id":134,"href":135,"target":48,"icon":49,"sortOrder":20,"label":136,"caption":49,"children":137},"cms5x85sw00qsp90j96hsi9if","\u002Fterms","Terms of Service",[],{"id":139,"href":140,"target":48,"icon":49,"sortOrder":60,"label":141,"caption":49,"children":142},"cms5x85ta00qyp90je9qxvdrq","\u002Ffaq","FAQ",[],[144,151,157,163],{"id":145,"href":146,"target":147,"icon":148,"sortOrder":11,"label":149,"caption":49,"children":150},"cms5x85to00r5p90jlpp2dj9y","https:\u002F\u002Flinkedin.com\u002Fcompany\u002Fmoodkuit","_blank","linkedin","LinkedIn",[],{"id":152,"href":153,"target":147,"icon":154,"sortOrder":20,"label":155,"caption":49,"children":156},"cms5x85u000rbp90jz220we6w","https:\u002F\u002Fgithub.com\u002Fmoodkuit","github","GitHub",[],{"id":158,"href":159,"target":147,"icon":160,"sortOrder":60,"label":161,"caption":49,"children":162},"cms5x85uc00rhp90j909vvybj","https:\u002F\u002Fx.com\u002Fmoodkuit","x","X",[],{"id":164,"href":165,"target":147,"icon":166,"sortOrder":66,"label":167,"caption":49,"children":168},"cms5x85un00rnp90j4c6x21gw","https:\u002F\u002Finstagram.com\u002Fmoodkuit","instagram","Instagram",[],{"id":170,"authorId":171,"coverImageId":172,"status":173,"publishedAt":174,"readingMinutes":78,"viewCount":175,"isFeatured":19,"allowComments":19,"createdAt":176,"updatedAt":177,"deletedAt":49,"coverImage":178,"author":192,"categories":196,"tags":212,"translation":240},"cms5x84j900bbp90jgvsq13h5","cms5x83ok0035p90j1tmyvb7d","cms5x83y9005vp90j0shkjq4a","PUBLISHED","2026-05-25T09:00:00.000Z",8,"2026-07-29T10:07:32.853Z","2026-07-29T12:16:56.902Z",{"id":172,"folderId":179,"uploadedById":180,"type":181,"filename":182,"originalName":182,"mimeType":183,"size":184,"width":185,"height":186,"storageKey":187,"url":188,"thumbnailUrl":188,"alt":189,"caption":49,"blurhash":49,"createdAt":190,"updatedAt":191,"deletedAt":49},"cms5x83pu003jp90j5lwg5qs2","cms5x83o00034p90jw5cmslod","IMAGE","post-2.jpg","image\u002Fjpeg",30668,850,402,"seed\u002Fpost\u002Fshipping-a-multilingual-site-without-duplicating-your-codebase\u002Fpost-2.jpg","https:\u002F\u002Fcdn.moodkuit.com\u002Fmoodkuit\u002Fseed\u002Fpost\u002Fshipping-a-multilingual-site-without-duplicating-your-codebase\u002Fpost-2.jpg","Locale-prefixed routes mapped out on a whiteboard","2026-07-29T10:07:32.097Z","2026-07-29T10:31:55.658Z",{"id":171,"name":193,"avatarUrl":194,"bio":195},"Rani Prameswari","\u002Fimages\u002Fnews\u002Fauthor_img2.jpg","Design systems practitioner and the byline on most of the engineering blog.",[197],{"postId":170,"categoryId":198,"category":199},"cms5x84dx00a0p90jq4hj1xcq",{"id":198,"parentId":49,"sortOrder":11,"createdAt":200,"updatedAt":200,"deletedAt":49,"translations":201},"2026-07-29T10:07:32.661Z",[202,207],{"id":203,"categoryId":198,"locale":6,"name":204,"slug":205,"description":49,"seoTitle":49,"seoDescription":49,"createdAt":206,"updatedAt":206},"cms5x84e300a2p90jrmk5oan0","Engineering","engineering","2026-07-29T10:07:32.667Z",{"id":208,"categoryId":198,"locale":15,"name":209,"slug":210,"description":49,"seoTitle":49,"seoDescription":49,"createdAt":211,"updatedAt":211},"cms5x84e900a4p90jp567w33d","Rekayasa","rekayasa","2026-07-29T10:07:32.674Z",[213,226],{"postId":170,"tagId":214,"tag":215},"cms5x84fh00afp90j1d0ctmnn",{"id":214,"createdAt":216,"updatedAt":216,"translations":217},"2026-07-29T10:07:32.717Z",[218,223],{"id":219,"tagId":214,"locale":6,"name":220,"slug":221,"description":49,"createdAt":222,"updatedAt":222},"cms5x84fm00ahp90jf5gcjs6f","Nuxt","nuxt","2026-07-29T10:07:32.723Z",{"id":224,"tagId":214,"locale":15,"name":220,"slug":221,"description":49,"createdAt":225,"updatedAt":225},"cms5x84fs00ajp90jv6hskwd8","2026-07-29T10:07:32.728Z",{"postId":170,"tagId":227,"tag":228},"cms5x84hk00azp90j1ae34zax",{"id":227,"createdAt":229,"updatedAt":229,"translations":230},"2026-07-29T10:07:32.793Z",[231,236],{"id":232,"tagId":227,"locale":6,"name":233,"slug":234,"description":49,"createdAt":235,"updatedAt":235},"cms5x84hm00b1p90jq9tne7j9","Performance","performance","2026-07-29T10:07:32.795Z",{"id":237,"tagId":227,"locale":15,"name":238,"slug":234,"description":49,"createdAt":239,"updatedAt":239},"cms5x84hq00b3p90j0j6ve1uh","Performa","2026-07-29T10:07:32.798Z",{"id":241,"postId":170,"locale":6,"title":242,"slug":243,"excerpt":244,"content":245,"seoTitle":246,"seoDescription":244,"seoKeywords":49,"canonicalUrl":49,"ogImageUrl":49,"noIndex":19,"noFollow":19,"createdAt":247,"updatedAt":247},"cms5x84jd00bdp90j79fx6ejp","Shipping a multilingual site without duplicating your codebase","shipping-a-multilingual-site-without-duplicating-your-codebase","Locale-prefixed routing, hreflang, canonical URLs and per-locale slugs — handled once, in the router.","\u003Cp>Most multilingual sites go wrong in the same place: the URL. Once slugs differ per locale, naive routing forces either duplicated pages or English URLs everywhere.\u003C\u002Fp>\u003Ch2>The routing contract\u003C\u002Fh2>\u003Cp>The default locale is served unprefixed. Every other locale gets a prefix. Slugs are resolved per locale from the database, and each page emits a complete set of \u003Ccode>hreflang\u003C\u002Fcode> alternates plus \u003Ccode>x-default\u003C\u002Fcode>.\u003C\u002Fp>","Shipping a multilingual site without duplicating your codebase | Moodkuit","2026-07-29T10:07:32.857Z",[249,272],{"id":250,"authorId":171,"coverImageId":251,"status":173,"publishedAt":252,"readingMinutes":72,"viewCount":253,"isFeatured":19,"allowComments":19,"createdAt":254,"updatedAt":255,"deletedAt":49,"coverImage":256,"translation":264},"cms5x84kj00bnp90j5qo7aas1","cms5x83yk005zp90jsek60xg4","2026-06-10T09:00:00.000Z",14,"2026-07-29T10:07:32.899Z","2026-07-29T12:36:45.498Z",{"id":251,"folderId":179,"uploadedById":180,"type":181,"filename":257,"originalName":257,"mimeType":183,"size":258,"width":185,"height":186,"storageKey":259,"url":260,"thumbnailUrl":260,"alt":261,"caption":49,"blurhash":49,"createdAt":262,"updatedAt":263,"deletedAt":49},"post-4.jpg",36028,"seed\u002Fpost\u002Fcore-web-vitals-budgets-that-survive-the-second-sprint\u002Fpost-4.jpg","https:\u002F\u002Fcdn.moodkuit.com\u002Fmoodkuit\u002Fseed\u002Fpost\u002Fcore-web-vitals-budgets-that-survive-the-second-sprint\u002Fpost-4.jpg","Performance budget failing a continuous integration run","2026-07-29T10:07:32.108Z","2026-07-29T10:31:55.623Z",{"id":265,"postId":250,"locale":6,"title":266,"slug":267,"excerpt":268,"content":269,"seoTitle":270,"seoDescription":268,"seoKeywords":49,"canonicalUrl":49,"ogImageUrl":49,"noIndex":19,"noFollow":19,"createdAt":271,"updatedAt":271},"cms5x84kn00bpp90jwdu8sgdr","Core Web Vitals budgets that survive the second sprint","core-web-vitals-budgets-that-survive-the-second-sprint","A launch-day Lighthouse score is a snapshot. A CI budget is a guarantee.","\u003Cp>Performance regressions arrive one innocuous import at a time. The fix is structural: budgets enforced in CI, with the build failing when LCP or total bundle size crosses the line.\u003C\u002Fp>","Core Web Vitals budgets that survive the second sprint | Moodkuit","2026-07-29T10:07:32.902Z",{"id":273,"authorId":171,"coverImageId":274,"status":173,"publishedAt":275,"readingMinutes":72,"viewCount":175,"isFeatured":10,"allowComments":19,"createdAt":276,"updatedAt":277,"deletedAt":49,"coverImage":278,"translation":286},"cms5x84i100b5p90j5aybdulu","cms5x83y2005tp90jglrwh85p","2026-05-17T09:00:00.000Z","2026-07-29T10:07:32.810Z","2026-07-29T12:17:05.948Z",{"id":274,"folderId":179,"uploadedById":180,"type":181,"filename":279,"originalName":279,"mimeType":183,"size":280,"width":185,"height":186,"storageKey":281,"url":282,"thumbnailUrl":282,"alt":283,"caption":49,"blurhash":49,"createdAt":284,"updatedAt":285,"deletedAt":49},"post-1.jpg",52352,"seed\u002Fpost\u002Fwhy-we-model-translations-as-rows-not-json\u002Fpost-1.jpg","https:\u002F\u002Fcdn.moodkuit.com\u002Fmoodkuit\u002Fseed\u002Fpost\u002Fwhy-we-model-translations-as-rows-not-json\u002Fpost-1.jpg","Database schema sketch comparing JSON blobs with translation tables","2026-07-29T10:07:32.090Z","2026-07-29T10:31:55.693Z",{"id":287,"postId":273,"locale":6,"title":288,"slug":289,"excerpt":290,"content":291,"seoTitle":292,"seoDescription":290,"seoKeywords":49,"canonicalUrl":49,"ogImageUrl":49,"noIndex":19,"noFollow":19,"createdAt":293,"updatedAt":293},"cms5x84i900b7p90jui0vovq3","Why we model translations as rows, not JSON","why-we-model-translations-as-rows-not-json","A JSON translation blob is convenient until the day you need to find every untranslated heading. Relational translation tables cost one join and return that answer in milliseconds.","\u003Cp>Storing translations in a JSON column looks like the pragmatic choice. One column, no joins, no migrations when a locale is added. The bill arrives later.\u003C\u002Fp>\u003Ch2>What breaks\u003C\u002Fh2>\u003Cp>Reporting on translation coverage becomes a full table scan with JSON path expressions. Uniqueness on slugs per locale cannot be enforced by the database. Partial indexes on published, localized content are unavailable. And every consumer must agree on the blob shape forever.\u003C\u002Fp>\u003Ch2>What we do instead\u003C\u002Fh2>\u003Cp>Every translatable entity has a sibling table keyed on \u003Ccode>(entity_id, locale)\u003C\u002Fcode> with a second unique constraint on \u003Ccode>(locale, slug)\u003C\u002Fcode>. Adding Japanese is an insert into \u003Ccode>languages\u003C\u002Fcode> — no migration, no deploy.\u003C\u002Fp>","Why we model translations as rows, not JSON | Moodkuit","2026-07-29T10:07:32.817Z"]