[{"data":1,"prerenderedAt":224},["ShallowReactive",2],{"blog:page:\u002Fen\u002Fblog\u002Furl-model":3,"blog:articles:en":166,"blog:translations:\u002Fen\u002Fblog\u002Furl-model":221},{"id":4,"title":5,"author":6,"body":7,"cover":153,"coverAlt":154,"date":155,"description":156,"extension":157,"featured":158,"meta":159,"navigation":158,"path":121,"seo":160,"stem":161,"tags":162,"__hash__":165},"blog\u002Fen\u002Fblog\u002F1.url-model\u002Findex.md","One URL model for every public page","John Martin",{"type":8,"value":9,"toc":147},"minimark",[10,14,23,33,36,43,48,96,100,103,109,112,116,127,133,136],[11,12,5],"h1",{"id":13},"one-url-model-for-every-public-page",[15,16,17,18,22],"p",{},"Every page on ",[19,20,21],"code",{},"ecoma.io"," lives under one shape:",[24,25,31],"pre",{"className":26,"code":28,"language":29,"meta":30},[27],"language-text","\u002F\u003Clocale>\u002F\u003Cmount>\u002F\u003Cpath>\n","text","",[19,32,28],{"__ignoreMap":30},[15,34,35],{},"That is not a convention we hope teams follow. It is parsed, rejected and built\nby two small libraries, so a page cannot silently drift into a shape the rest\nof the platform does not understand.",[15,37,38],{},[39,40],"img",{"alt":41,"src":42},"Diagram of a public website as three stacked layers, from pages down to static delivery","\u002Fassets\u002Fpublic-web-layers.svg",[44,45,47],"h2",{"id":46},"the-three-segments","The three segments",[49,50,51,67,90],"ul",{},[52,53,54,58,59,62,63,66],"li",{},[55,56,57],"strong",{},"locale"," comes first, always: ",[19,60,61],{},"\u002Fen\u002F..."," or ",[19,64,65],{},"\u002Fvi\u002F...",". There is no\nunprefixed variant, so a URL never needs to guess its own language.",[52,68,69,72,73,76,77,80,81,84,85,89],{},[55,70,71],{},"mount"," names the surface: ",[19,74,75],{},"blog",", ",[19,78,79],{},"docs",". It matches on a full segment\nboundary - ",[19,82,83],{},"\u002Fen\u002Fblogging"," is ",[86,87,88],"em",{},"not"," the blog.",[52,91,92,95],{},[55,93,94],{},"path"," is everything after the mount, and it belongs to the app that owns\nthe mount.",[44,97,99],{"id":98},"strictness-is-a-feature","Strictness is a feature",[15,101,102],{},"The parser does not repair input. A trailing slash, an empty segment, or an\nunknown locale is rejected with a typed reason:",[24,104,107],{"className":105,"code":106,"language":29,"meta":30},[27],"\u002Fen        -> ok\n\u002Fen\u002Fdocs\u002F  -> rejected: trailing slash\n\u002FEN\u002Fdocs   -> rejected: unknown locale\n\u002Fen\u002F\u002Fdocs  -> rejected: empty segment\n",[19,108,106],{"__ignoreMap":30},[15,110,111],{},"Repairs feel helpful and are quietly harmful: a URL that \"works\" in a\nnon-canonical form gets indexed, cached and shared in exactly that form.\nRejection keeps one canonical address per page.",[44,113,115],{"id":114},"slugs-survive-translation","Slugs survive translation",[15,117,118,119,122,123,126],{},"An article keeps the same slug in both languages. ",[19,120,121],{},"\u002Fen\u002Fblog\u002Furl-model"," and\n",[19,124,125],{},"\u002Fvi\u002Fblog\u002Furl-model"," are the same resource wearing two languages, so the locale\nswitcher can move a reader between them without a lookup table.",[15,128,129],{},[39,130],{"alt":131,"src":132},"One article slug branching into an English and a Vietnamese path","\u002Fassets\u002Ftwo-locales-one-page.svg",[134,135],"hr",{},[15,137,138],{},[86,139,140,141,146],{},"Related: the docs surface documents the same model in more depth - see\n",[142,143,145],"a",{"href":144},"\u002Fen\u002Fblog\u002Fbuild-time-content","Public web layers"," for how content reaches the\npage.",{"title":30,"searchDepth":148,"depth":148,"links":149},2,[150,151,152],{"id":46,"depth":148,"text":47},{"id":98,"depth":148,"text":99},{"id":114,"depth":148,"text":115},"\u002Fassets\u002Fcover-url-model.svg","Cover illustration: a single address bar splitting into locale, mount and path segments","2026-09-02","How \u002Flocale\u002Fmount\u002Fpath keeps every public page predictable - and why we enforce it instead of hoping for it.","md",true,{},{"title":5,"description":156},"en\u002Fblog\u002F1.url-model\u002Findex",[163,164],"architecture","urls","a2zBpBvT7MXRQSz8LieMDkdfYoOVnGMyHabvRei3MAQ",[167,176,188,200,211,218],{"path":168,"stem":169,"title":170,"description":171,"date":172,"author":6,"tags":173,"featured":175},"\u002Fen\u002Fblog\u002Fdemo-content-note","en\u002Fblog\u002F6.demo-content-note\u002Findex","What this blog is (and is not)","This is a demo vertical slice - real layout, real pipelines, deliberately editorial content.","2026-10-07",[174],"meta",false,{"path":177,"stem":178,"title":179,"description":180,"date":181,"author":6,"tags":182,"featured":175,"cover":185},"\u002Fen\u002Fblog\u002Freading-first-design","en\u002Fblog\u002F5.reading-first-design\u002Findex","A reading page is not a documentation page","Why the blog article layout deliberately differs from the docs layout - hierarchy, width, and restraint.","2026-09-30",[183,184],"design","typography",{"src":186,"alt":187},"\u002Fassets\u002Fcover-reading-first.svg","Cover illustration: a narrow single-column article layout chosen over a cluttered multi-column layout",{"path":189,"stem":190,"title":191,"description":192,"date":193,"author":6,"tags":194,"featured":175,"cover":197},"\u002Fen\u002Fblog\u002Fstatic-deployment","en\u002Fblog\u002F4.static-deployment\u002Findex","Deploying a blog as static assets","No Worker script, no database binding - the whole deployment is a directory of files and a 404 page.","2026-09-23",[195,196],"deployment","cloudflare",{"src":198,"alt":199},"\u002Fassets\u002Fcover-static-deployment.svg","Cover illustration: a folder of HTML and SVG files flowing onto a globe of CDN edge nodes",{"path":201,"stem":202,"title":203,"description":204,"date":205,"author":6,"tags":206,"featured":175,"cover":209},"\u002Fen\u002Fblog\u002Flocale-switching","en\u002Fblog\u002F3.locale-switching\u002Findex","Locale switching that never lies","A language switcher should only offer what exists. Here is how the link list is derived from real content.","2026-09-16",[207,208],"i18n","ux",{"src":132,"alt":210},"Cover illustration: two locale paths joining into one shared page layout",{"path":144,"stem":212,"title":213,"description":214,"date":215,"author":6,"tags":216,"featured":175},"en\u002Fblog\u002F2.build-time-content\u002Findex","Content is resolved at build time","Why this blog ships as prerendered files instead of a content API - and what that buys.","2026-09-09",[163,217],"static-sites",{"path":121,"stem":161,"title":5,"description":156,"date":155,"author":6,"tags":219,"featured":158,"cover":220},[163,164],{"src":153,"alt":154},[222,223],"en","vi",1791809694139]