{
    "componentChunkName": "component---src-templates-tag-page-jsx",
    "path": "/en/blog/tags/expo-router",
    "result": {"pageContext":{"pageType":"tag","title":"expo router","posts":[{"node":{"html":"<p>One of the annoying little side quests of shipping an app is that Google occasionally forces you to become a website owner too.</p>\n<p>Back when I <a href=\"/en/blog/coding/musclog-leveraging-my-reactjs-experience-to-build-a-react-native-app/\">first wrote about Musclog in 2024</a>, the website mostly existed because Google wanted it to exist. I needed the privacy policy, the public pages, the whole respectable app developer starter pack, so I quickly spun up a small Next.js site, got the app approved, and moved on with my life.</p>\n<p>That worked right until I had to touch the website again.</p>\n<p>New screenshots? Other repo. New feature copy? Other repo. Legal page update? Other repo. Translation tweak? Other repo.</p>\n<p>None of that was actually hard, which almost made it worse. It was just annoying enough to keep reminding me that I had split one product into two codebases for a reason I no longer respected.</p>\n<p>So now I don’t.</p>\n<p>Musclog’s website lives inside the app repo. Same Expo Router project. Same deploy. <a href=\"https://musclog.app/\" target=\"_blank\" rel=\"noreferrer\">musclog.app</a> is the public site, <a href=\"https://musclog.app/app\" target=\"_blank\" rel=\"noreferrer\">musclog.app/app</a> is the actual web app, and Android + iOS still come from the same codebase.</p>\n<p>Best practice? Questionable. Convenient? Extremely.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 61.71875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB0ElEQVQoz2WRTW7bMBCF1YVjcSiL4j8pkbRkxQri2G4c20mzyC5IWyDdNAVa9Bg9Q7e9StGz9EQFZcVNUeDbzOA9vuFMkjIS4YQEgwVDrECHTg8SNDMCK4E3Dl9XEy0mTqeKprxIGUlAUJAUBB3l2QnNxyxHfTmgOJQyDZKvVvrtx1fvb9C6wV6B5SBZctSdPV6fPWxn3dw4N1GibzJQDDs9qsn89uv+x+/k13d43OFKQiWfzbzAkq0/v1t8ul89PdC7dWo48H4ixcAp8Ao+vB7//DK+36XLJmstlBLUMZnTfNmQN4ti09GbBa4UiGIwVxJ7nd2d4m/7UefR3GWnJVgBkg5mJGjpvbHW1VMm46ugOdYcDMelhKCKrhbL9eiihc5ltQbD/5ohblWClVAZmFY4GOxlJMgsqCyoSaPRbX2y9RBU5iVoBuKFOWIkrh1uA24qPDNZayMzndUKnCCdy9sSLOtj/zdLBkaAldhKsBz1wAHDkKLximo4bTQjyY5AzwkjZjY9314udpuL3dX51aVqwpgX8EJ5IIkzPIM0Q7oP15w4w0LJQplXuk/msT8w6JO4dBtrMve8DcRbpNnEKeotMSrXEktG9jWZlv2P/uEPhuJAcZJWdDcAAAAASUVORK5CYII='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog public website rendered from the same Expo Router codebase as the app\"\n        title=\"Musclog public website rendered from the same Expo Router codebase as the app\"\n        src=\"/static/f71a459f3034e47544de2f3ce309dd61/42a19/musclog-shared-router-website-home-1.png\"\n        srcset=\"/static/f71a459f3034e47544de2f3ce309dd61/e3135/musclog-shared-router-website-home-1.png 256w,\n/static/f71a459f3034e47544de2f3ce309dd61/06341/musclog-shared-router-website-home-1.png 512w,\n/static/f71a459f3034e47544de2f3ce309dd61/42a19/musclog-shared-router-website-home-1.png 1024w,\n/static/f71a459f3034e47544de2f3ce309dd61/e8464/musclog-shared-router-website-home-1.png 1536w,\n/static/f71a459f3034e47544de2f3ce309dd61/2eb59/musclog-shared-router-website-home-1.png 2048w,\n/static/f71a459f3034e47544de2f3ce309dd61/d7fb2/musclog-shared-router-website-home-1.png 2184w\"\n        sizes=\"(max-width: 1024px) 100vw, 1024px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog public website rendered from the same Expo Router codebase as the app</figcaption>\n  </figure></p>\n<h2 id=\"ok-but-why\" style=\"position:relative;\"><a href=\"#ok-but-why\" aria-label=\"ok but why permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Ok but why?</h2>\n<p>Basically because Expo Router removed my last excuse.</p>\n<p>Musclog already used <a href=\"https://expo.github.io/router/docs\" target=\"_blank\" rel=\"noreferrer\">Expo Router</a>, so the app was already organized as file-based routes inside <code class=\"language-text\">app/</code>. Then I realized I could just create a route group called <code class=\"language-text\">(website)</code>, move the public pages there, keep the actual app under <code class=\"language-text\">app/app/*</code>, and stop pretending these were two different products.</p>\n<p>The separate repo had become one of those architecture decisions that sounds clean in theory and then quietly charges you maintenance tax forever:</p>\n<ul>\n<li>two PRs for one product change</li>\n<li>two deployments</li>\n<li>two places for translations</li>\n<li>two places for legal pages</li>\n<li>two places to forget something</li>\n</ul>\n<p>No thanks.</p>\n<h2 id=\"the-split\" style=\"position:relative;\"><a href=\"#the-split\" aria-label=\"the split permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The split</h2>\n<p>The funny part is that the routing itself ended up being the least cursed part of the whole thing:</p>\n<ul>\n<li><code class=\"language-text\">app/app/*</code> is the actual Musclog app. Workouts, nutrition, AI coach, all of it.</li>\n<li><code class=\"language-text\">app/(website)/*</code> is the public website. Landing page, privacy policy, terms, contact page, calculator.</li>\n</ul>\n<p>The root route just checks the platform and sends you where you belong:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token comment\">// app/index.tsx</span>\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> Redirect<span class=\"token punctuation\">,</span> useRouter <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'expo-router'</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> useEffect <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'react'</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> Platform <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'react-native'</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">default</span> <span class=\"token keyword\">function</span> <span class=\"token function\">Index</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token keyword\">const</span> router <span class=\"token operator\">=</span> <span class=\"token function\">useRouter</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n  <span class=\"token function\">useEffect</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>Platform<span class=\"token punctuation\">.</span><span class=\"token constant\">OS</span> <span class=\"token operator\">===</span> <span class=\"token string\">'web'</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n      router<span class=\"token punctuation\">.</span><span class=\"token function\">replace</span><span class=\"token punctuation\">(</span><span class=\"token string\">'/home'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span>\r\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">[</span>router<span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>Platform<span class=\"token punctuation\">.</span><span class=\"token constant\">OS</span> <span class=\"token operator\">===</span> <span class=\"token string\">'web'</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">return</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token punctuation\">}</span>\r\n\r\n  <span class=\"token keyword\">return</span> <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">Redirect</span></span> <span class=\"token attr-name\">href</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>/app<span class=\"token punctuation\">\"</span></span> <span class=\"token punctuation\">/></span></span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>That’s it. On web, <code class=\"language-text\">/</code> becomes the website. On native, <code class=\"language-text\">/</code> becomes the app.</p>\n<p>The website also gets its own lighter layout:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token comment\">// app/(website)/_layout.web.tsx</span>\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> Slot <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'expo-router'</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> WebsiteChrome <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'@/components/website/WebsiteChrome'</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> WebsiteProviders <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'@/components/website/WebsiteProviders'</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">default</span> <span class=\"token keyword\">function</span> <span class=\"token function\">WebsiteLayout</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\r\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">WebsiteProviders</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">WebsiteChrome</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n        </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">Slot</span></span> <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\r\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">WebsiteChrome</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">WebsiteProviders</span></span><span class=\"token punctuation\">></span></span>\r\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>That matters because the real app layout had already grown into an actual app layout. Database boot, migrations, React Query, safe area, gestures, modals, snackbars, camera, coach context, all the usual “small side project” lies. The website does not need any of that just to explain what the app does.</p>\n<p>And if a native user somehow lands on a website-only route, the fix is beautifully blunt:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token comment\">// app/(website)/home.tsx</span>\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> Redirect <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'expo-router'</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">default</span> <span class=\"token keyword\">function</span> <span class=\"token function\">Home</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token keyword\">return</span> <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">Redirect</span></span> <span class=\"token attr-name\">href</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>/app<span class=\"token punctuation\">\"</span></span> <span class=\"token punctuation\">/></span></span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Done. Back to your macros.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 61.71875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAABt0lEQVQoz02SWW/jMAyE/RaLdqyLkiXfpxznstOi6S6w//93LZS4aYB5IuYDhkMGO554iWRfpSBZyJNQUoIslDREClrEBkHweLbxRxkpGWuxITwJQIuniGSgOaQcjOyW03Rfp++bu6/ua2mWmShOOCWaE8Uh3ZAAUvlUlKvIyCjDqFBxY5gr+VyzU8Omcl+bqNRRoSKDkUUwGxKARbBIrMwvU309qKHSc9v9XextEktb/LvUf67q2KlD236es0MvuwIyfFIbDBaxr/RY41iLsTTXIb05eevxo09vTp97PhQ4VGqoRJu/kMBneAinWg0VqywfC7M6PHf83OivSS8DHwoxFubUyyanpXkhW2FEczM0xTyqrsSuqNdjvjicG3t15XpIp1a2RXV02dSJOnt1HBBkT4XICE9AcaJFiAwUi6wkRhAtIBVEiVBSEA/DDxIQyV6CPvcpWAKlhrnyQ0EhV9udUkmm8t3/CwNyc3GsMDu6T+pMXR2RLBQU+4pm6Y4lsUU8D0S8wz8ZfHKeEP9e3GOc+mdI/Qp+ovyQsOTd//th8NjtKWJkUlt3X6bvlTaW+G7fDD+F/QccPj1gi9mJrAAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog public website rendered from the same Expo Router codebase as the app\"\n        title=\"Musclog public website rendered from the same Expo Router codebase as the app\"\n        src=\"/static/c50b08ecc21b0e02ed7774578063a5f6/42a19/musclog-shared-router-website-home-2.png\"\n        srcset=\"/static/c50b08ecc21b0e02ed7774578063a5f6/e3135/musclog-shared-router-website-home-2.png 256w,\n/static/c50b08ecc21b0e02ed7774578063a5f6/06341/musclog-shared-router-website-home-2.png 512w,\n/static/c50b08ecc21b0e02ed7774578063a5f6/42a19/musclog-shared-router-website-home-2.png 1024w,\n/static/c50b08ecc21b0e02ed7774578063a5f6/e8464/musclog-shared-router-website-home-2.png 1536w,\n/static/c50b08ecc21b0e02ed7774578063a5f6/2eb59/musclog-shared-router-website-home-2.png 2048w,\n/static/c50b08ecc21b0e02ed7774578063a5f6/d7fb2/musclog-shared-router-website-home-2.png 2184w\"\n        sizes=\"(max-width: 1024px) 100vw, 1024px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog public website rendered from the same Expo Router codebase as the app</figcaption>\n  </figure></p>\n<h2 id=\"the-desktop-phone-nonsense\" style=\"position:relative;\"><a href=\"#the-desktop-phone-nonsense\" aria-label=\"the desktop phone nonsense permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The desktop phone nonsense</h2>\n<p>This is where I stopped behaving like a normal person.</p>\n<p>I wanted <a href=\"https://musclog.app/app/\" target=\"_blank\" rel=\"noreferrer\">musclog.app/app</a> on desktop to show the actual working app inside a phone frame. Partly because it looks nice. Partly because it doubles as a product preview. Mostly because once the idea entered my head, every less ridiculous option started feeling wrong.</p>\n<p>The annoying detail was that <code class=\"language-text\">/app</code> should look like a phone, but <code class=\"language-text\">/home</code> absolutely should not. So routing alone was not enough. I needed the raw HTML shell to know, before hydration, whether the browser should render the fake phone wrapper or not.</p>\n<p>That logic lives in <code class=\"language-text\">app/+html.tsx</code>, inside a script in the document <code class=\"language-text\">&lt;head></code>:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">function</span> <span class=\"token function\">landingPanelGate</span><span class=\"token punctuation\">(</span>base<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">function</span> <span class=\"token function\">update</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n      <span class=\"token keyword\">const</span> raw <span class=\"token operator\">=</span> window<span class=\"token punctuation\">.</span>location<span class=\"token punctuation\">.</span>pathname<span class=\"token punctuation\">;</span>\r\n      <span class=\"token keyword\">const</span> path <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>base <span class=\"token operator\">&amp;&amp;</span> raw<span class=\"token punctuation\">.</span><span class=\"token function\">startsWith</span><span class=\"token punctuation\">(</span>base<span class=\"token punctuation\">)</span> <span class=\"token operator\">?</span> raw<span class=\"token punctuation\">.</span><span class=\"token function\">slice</span><span class=\"token punctuation\">(</span>base<span class=\"token punctuation\">.</span>length<span class=\"token punctuation\">)</span> <span class=\"token operator\">:</span> raw<span class=\"token punctuation\">)</span> <span class=\"token operator\">||</span> <span class=\"token string\">'/'</span><span class=\"token punctuation\">;</span>\r\n\r\n      <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>path<span class=\"token punctuation\">.</span><span class=\"token function\">startsWith</span><span class=\"token punctuation\">(</span><span class=\"token string\">'/app'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        document<span class=\"token punctuation\">.</span>documentElement<span class=\"token punctuation\">.</span>classList<span class=\"token punctuation\">.</span><span class=\"token function\">add</span><span class=\"token punctuation\">(</span><span class=\"token string\">'hide-desktop-wrapper'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n      <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token punctuation\">{</span>\r\n        document<span class=\"token punctuation\">.</span>documentElement<span class=\"token punctuation\">.</span>classList<span class=\"token punctuation\">.</span><span class=\"token function\">remove</span><span class=\"token punctuation\">(</span><span class=\"token string\">'hide-desktop-wrapper'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n      <span class=\"token punctuation\">}</span>\r\n    <span class=\"token punctuation\">}</span>\r\n\r\n    <span class=\"token function\">update</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    window<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'popstate'</span><span class=\"token punctuation\">,</span> update<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token keyword\">const</span> origPush <span class=\"token operator\">=</span> history<span class=\"token punctuation\">.</span><span class=\"token function\">pushState</span><span class=\"token punctuation\">.</span><span class=\"token function\">bind</span><span class=\"token punctuation\">(</span>history<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    history<span class=\"token punctuation\">.</span><span class=\"token function-variable function\">pushState</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">function</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">...</span>args<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n      <span class=\"token function\">origPush</span><span class=\"token punctuation\">(</span><span class=\"token operator\">...</span>args<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n      <span class=\"token function\">update</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token keyword\">const</span> origReplace <span class=\"token operator\">=</span> history<span class=\"token punctuation\">.</span><span class=\"token function\">replaceState</span><span class=\"token punctuation\">.</span><span class=\"token function\">bind</span><span class=\"token punctuation\">(</span>history<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    history<span class=\"token punctuation\">.</span><span class=\"token function-variable function\">replaceState</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">function</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">...</span>args<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n      <span class=\"token function\">origReplace</span><span class=\"token punctuation\">(</span><span class=\"token operator\">...</span>args<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n      <span class=\"token function\">update</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token punctuation\">}</span> <span class=\"token keyword\">catch</span> <span class=\"token punctuation\">(</span>_<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span><span class=\"token punctuation\">}</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The important part is the <code class=\"language-text\">pushState</code> / <code class=\"language-text\">replaceState</code> monkey patch. First paint was easy. Client-side navigation was the annoying bit. Without this, you could leave <code class=\"language-text\">/app</code> and keep the wrong shell around like Chrome had forgotten what page it was on.</p>\n<p>The HTML shell itself is basically the landing panel, the routed app, and the phone frame:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>body</span> <span class=\"token attr-name\">className</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>expo-web-body<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n  </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span> <span class=\"token attr-name\">className</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>expo-web-landing<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">...</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n  </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>script</span> <span class=\"token attr-name\">dangerouslySetInnerHTML</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token punctuation\">{</span> __html<span class=\"token operator\">:</span> <span class=\"token constant\">LANDING_I18N_SCRIPT</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">}</span></span> <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\r\n  </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span> <span class=\"token attr-name\">className</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>expo-web-root<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span> <span class=\"token attr-name\">className</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>expo-web-app-shell<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token punctuation\">{</span>children<span class=\"token punctuation\">}</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>img</span>\r\n      <span class=\"token attr-name\">className</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>expo-web-phone-frame<span class=\"token punctuation\">\"</span></span>\r\n      <span class=\"token attr-name\">src</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token function\">withExpoBaseUrl</span><span class=\"token punctuation\">(</span><span class=\"token constant\">PHONE_FRAME_SRC</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">}</span></span>\r\n      <span class=\"token attr-name\">alt</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span><span class=\"token punctuation\">\"</span></span>\r\n      <span class=\"token attr-name\">aria-hidden</span>\r\n    <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\r\n  </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>body</span><span class=\"token punctuation\">></span></span></code></pre></div>\n<p>Then CSS commits the crime:</p>\n<div class=\"gatsby-highlight\" data-language=\"css\"><pre class=\"language-css\"><code class=\"language-css\"><span class=\"token atrule\"><span class=\"token rule\">@media</span> <span class=\"token punctuation\">(</span><span class=\"token property\">min-width</span><span class=\"token punctuation\">:</span> 1024px<span class=\"token punctuation\">)</span></span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token selector\">.expo-web-root</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token property\">--frame-h</span><span class=\"token punctuation\">:</span> <span class=\"token function\">min</span><span class=\"token punctuation\">(</span>100dvh<span class=\"token punctuation\">,</span> <span class=\"token function\">max</span><span class=\"token punctuation\">(</span><span class=\"token function\">min</span><span class=\"token punctuation\">(</span>360px<span class=\"token punctuation\">,</span> 100dvh<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> 85dvh<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token property\">aspect-ratio</span><span class=\"token punctuation\">:</span> 1438 / 2976<span class=\"token punctuation\">;</span>\r\n    <span class=\"token property\">width</span><span class=\"token punctuation\">:</span> <span class=\"token function\">min</span><span class=\"token punctuation\">(</span>100vw<span class=\"token punctuation\">,</span> <span class=\"token function\">calc</span><span class=\"token punctuation\">(</span><span class=\"token function\">var</span><span class=\"token punctuation\">(</span>--frame-h<span class=\"token punctuation\">)</span> * 1438 / 2976<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token property\">max-height</span><span class=\"token punctuation\">:</span> <span class=\"token function\">var</span><span class=\"token punctuation\">(</span>--frame-h<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token property\">overflow</span><span class=\"token punctuation\">:</span> hidden<span class=\"token punctuation\">;</span>\r\n  <span class=\"token punctuation\">}</span>\r\n\r\n  <span class=\"token selector\">.expo-web-app-shell</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token property\">position</span><span class=\"token punctuation\">:</span> absolute<span class=\"token punctuation\">;</span>\r\n    <span class=\"token property\">left</span><span class=\"token punctuation\">:</span> 7.4409%<span class=\"token punctuation\">;</span>\r\n    <span class=\"token property\">top</span><span class=\"token punctuation\">:</span> 2.9906%<span class=\"token punctuation\">;</span>\r\n    <span class=\"token property\">right</span><span class=\"token punctuation\">:</span> 6.3282%<span class=\"token punctuation\">;</span>\r\n    <span class=\"token property\">bottom</span><span class=\"token punctuation\">:</span> 3.125%<span class=\"token punctuation\">;</span>\r\n    <span class=\"token property\">zoom</span><span class=\"token punctuation\">:</span> 0.85<span class=\"token punctuation\">;</span>\r\n  <span class=\"token punctuation\">}</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Yes, I measured the transparent screen area inside the PNG until the app lined up correctly. There is no elegant geometry behind those percentages. It was just me, a phone bezel, and a lot of nudging until the app stopped looking crooked.</p>\n<p>On routes that are not <code class=\"language-text\">/app</code>, the whole performance gets disabled:</p>\n<div class=\"gatsby-highlight\" data-language=\"css\"><pre class=\"language-css\"><code class=\"language-css\"><span class=\"token selector\">.hide-desktop-wrapper .expo-web-landing,\r\n.hide-desktop-wrapper .expo-web-phone-frame</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token property\">display</span><span class=\"token punctuation\">:</span> none <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span>\r\n\r\n<span class=\"token selector\">.hide-desktop-wrapper .expo-web-app-shell</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token property\">position</span><span class=\"token punctuation\">:</span> static <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token property\">width</span><span class=\"token punctuation\">:</span> 100% <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token property\">height</span><span class=\"token punctuation\">:</span> auto <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token property\">zoom</span><span class=\"token punctuation\">:</span> 1 <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token property\">overflow</span><span class=\"token punctuation\">:</span> visible <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Yes, it uses <code class=\"language-text\">!important</code>. This is pre-hydration CSS whose entire job is keeping the lie intact. We are not doing refined architecture at this layer.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 61.71875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAABS0lEQVQoz42SyUoDQRCGgxOTrq7pfbbuZBKTmTEwWSAgKKLghtvBQy6CHs3Nk94UwZPP4Wv4DD6VqBdhION3Kor6qL+gGrQCfvNbER/bApEDFehXJxvVFhACAEDandHYTbcxG9BJHpQZZwwAVskAoIzWJvTWGvunrwfPn+uPD633F/V0IynC6s0AECZRFMWEeMX4bHR8z04O/dtzc7EjfQYUamIro6WSCDQunN0aJoU1iQptKKWsiU0plVoro6Hhda+Pyo+3dHkpho4PEsF5vYyUEiC0Tewk71/tmVnGXShcyFfLiNhsNufz+XJ5Z7TWUWDTDguUH2kZB/Vyq9Uqy3KxWGilgtT1Z6XNM512w15XiLrYiEgI8TyP+X6Q2rgY2s1cpd14oyel+MfNP/iIUWp13yW7UzPoxH3Hap/kL4wxLjgXXAjBOa++5xcHAzI9yvyvqQAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog desktop web app rendered inside a phone frame next to the landing panel\"\n        title=\"Musclog desktop web app rendered inside a phone frame next to the landing panel\"\n        src=\"/static/49361b3283ae34ef30a950b13c7f5594/42a19/musclog-desktop-phone-frame-wrapper.png\"\n        srcset=\"/static/49361b3283ae34ef30a950b13c7f5594/e3135/musclog-desktop-phone-frame-wrapper.png 256w,\n/static/49361b3283ae34ef30a950b13c7f5594/06341/musclog-desktop-phone-frame-wrapper.png 512w,\n/static/49361b3283ae34ef30a950b13c7f5594/42a19/musclog-desktop-phone-frame-wrapper.png 1024w,\n/static/49361b3283ae34ef30a950b13c7f5594/e8464/musclog-desktop-phone-frame-wrapper.png 1536w,\n/static/49361b3283ae34ef30a950b13c7f5594/2eb59/musclog-desktop-phone-frame-wrapper.png 2048w,\n/static/49361b3283ae34ef30a950b13c7f5594/d7fb2/musclog-desktop-phone-frame-wrapper.png 2184w\"\n        sizes=\"(max-width: 1024px) 100vw, 1024px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog desktop web app rendered inside a phone frame next to the landing panel</figcaption>\n  </figure></p>\n<h2 id=\"three-tiny-crimes\" style=\"position:relative;\"><a href=\"#three-tiny-crimes\" aria-label=\"three tiny crimes permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Three tiny crimes</h2>\n<p><img src=\"/377ac4354ce553fccf98c5d8d1017e50/straight-to-jail.gif\" alt=\"Straight to jail. Right away.\"></p>\n<p>Once the shell worked, the smaller annoyances started lining up like they had booked appointments.</p>\n<h3 id=\"1-the-landing-panel-needed-i18n-before-hydration\" style=\"position:relative;\"><a href=\"#1-the-landing-panel-needed-i18n-before-hydration\" aria-label=\"1 the landing panel needed i18n before hydration permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>1. The landing panel needed i18n before hydration</h3>\n<p>The text beside the phone frame lives in raw HTML, which means React and i18n are still sleeping when the page first renders. So if I wanted a Portuguese page to not flash English first, I had to patch it myself from <code class=\"language-text\">localStorage</code>:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">function</span> <span class=\"token function\">landingI18nPatcher</span><span class=\"token punctuation\">(</span>translations<span class=\"token punctuation\">,</span> storageKey<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">let</span> lang <span class=\"token operator\">=</span> localStorage<span class=\"token punctuation\">.</span><span class=\"token function\">getItem</span><span class=\"token punctuation\">(</span>storageKey<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">let</span> s <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>lang <span class=\"token operator\">&amp;&amp;</span> translations<span class=\"token punctuation\">[</span>lang<span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">||</span> translations<span class=\"token punctuation\">[</span><span class=\"token string\">'en-US'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\r\n\r\n    document<span class=\"token punctuation\">.</span><span class=\"token function\">querySelectorAll</span><span class=\"token punctuation\">(</span><span class=\"token string\">'[data-landing-i18n]'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">forEach</span><span class=\"token punctuation\">(</span><span class=\"token keyword\">function</span> <span class=\"token punctuation\">(</span>el<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n      <span class=\"token keyword\">let</span> k <span class=\"token operator\">=</span> el<span class=\"token punctuation\">.</span><span class=\"token function\">getAttribute</span><span class=\"token punctuation\">(</span><span class=\"token string\">'data-landing-i18n'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n      <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>k <span class=\"token operator\">&amp;&amp;</span> s<span class=\"token punctuation\">[</span>k<span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        el<span class=\"token punctuation\">.</span>textContent <span class=\"token operator\">=</span> s<span class=\"token punctuation\">[</span>k<span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\r\n      <span class=\"token punctuation\">}</span>\r\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token punctuation\">}</span> <span class=\"token keyword\">catch</span> <span class=\"token punctuation\">(</span>_<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span><span class=\"token punctuation\">}</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Not elegant. Also not flash-banging a Portuguese page with English text, so I’m keeping it.</p>\n<h3 id=\"2-raw-html-assets-do-not-get-expos-usual-help\" style=\"position:relative;\"><a href=\"#2-raw-html-assets-do-not-get-expos-usual-help\" aria-label=\"2 raw html assets do not get expos usual help permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>2. Raw HTML assets do not get Expo’s usual help</h3>\n<p>The phone frame PNG and QR code are referenced from raw HTML too, which means Expo’s usual asset niceness does not really help me there. So I needed a helper for base URLs:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">function</span> <span class=\"token function\">withExpoBaseUrl</span><span class=\"token punctuation\">(</span>path<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token keyword\">const</span> base <span class=\"token operator\">=</span> process<span class=\"token punctuation\">.</span>env<span class=\"token punctuation\">.</span><span class=\"token constant\">EXPO_BASE_URL</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>base <span class=\"token operator\">==</span> <span class=\"token keyword\">null</span> <span class=\"token operator\">||</span> base <span class=\"token operator\">===</span> <span class=\"token string\">''</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">return</span> path<span class=\"token punctuation\">;</span>\r\n  <span class=\"token punctuation\">}</span>\r\n\r\n  <span class=\"token keyword\">const</span> basePath <span class=\"token operator\">=</span> <span class=\"token function\">String</span><span class=\"token punctuation\">(</span>base<span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">replace</span><span class=\"token punctuation\">(</span><span class=\"token regex\"><span class=\"token regex-delimiter\">/</span><span class=\"token regex-source language-regex\">^\\/+|\\/+$</span><span class=\"token regex-delimiter\">/</span><span class=\"token regex-flags\">g</span></span><span class=\"token punctuation\">,</span> <span class=\"token string\">''</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token keyword\">const</span> normalized <span class=\"token operator\">=</span> path<span class=\"token punctuation\">.</span><span class=\"token function\">startsWith</span><span class=\"token punctuation\">(</span><span class=\"token string\">'/'</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">?</span> path <span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">/</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>path<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token keyword\">return</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">/</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>basePath<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>normalized<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>And because those files live outside the React Native asset pipeline, I copy them into <code class=\"language-text\">public/</code> before dev and export. Forget that step once and the site immediately reminds you who is in charge.</p>\n<h3 id=\"3-expo-export-occasionally-needed-emotional-support\" style=\"position:relative;\"><a href=\"#3-expo-export-occasionally-needed-emotional-support\" aria-label=\"3 expo export occasionally needed emotional support permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>3. <code class=\"language-text\">expo export</code> occasionally needed emotional support</h3>\n<p>In some environments, <code class=\"language-text\">expo export --platform web</code> would successfully finish and then just stay alive for no reason. Dist folder there. Files generated. Process spiritually complete, technically still hanging around.</p>\n<p>So now there is a wrapper script:</p>\n<div class=\"gatsby-highlight\" data-language=\"js\"><pre class=\"language-js\"><code class=\"language-js\"><span class=\"token comment\">// scripts/export-web-wrapper.js</span>\r\n<span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>output<span class=\"token punctuation\">.</span><span class=\"token function\">includes</span><span class=\"token punctuation\">(</span><span class=\"token string\">'Exported: dist'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n  console<span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span><span class=\"token string\">'[export-web-wrapper] Detected successful export. Forcing exit in 5s...'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token function\">setTimeout</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> process<span class=\"token punctuation\">.</span><span class=\"token function\">exit</span><span class=\"token punctuation\">(</span><span class=\"token number\">0</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> <span class=\"token number\">5000</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Not glamorous. Very effective.</p>\n<h2 id=\"then-modals-got-weird\" style=\"position:relative;\"><a href=\"#then-modals-got-weird\" aria-label=\"then modals got weird permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Then modals got weird</h2>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 61.71875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAABD0lEQVQoz53SS27CMBCAYUtUIp5x4ldMnAST2E3BcVA3kXqOcgbuf4eKhypoN8DoW4xG+ndDAAARAYDxAoucMnxUjiSj5wFYb4cqeN7Uom0eoXaBKK2lEBTApbRJ+01MF904ddO0HqMbx/UY2xj9lHyK/bi76D4TWdnKGAOIKjj1vhFdK/o7/N/lytVElaUQHBB5dMXQsGBv5aGWW/fnyILFfiXngUitzjEUoSl8XXh70tvLwn2tt4756k6osDNi/iCAmC2XyJj+SnlnWWPy1sBKgrmipfjdb1HJyTzPx+NRac1syWqNlXoQs5pM+/334SCVXIqcavEUkmXZ22IBCM+Wp/j6YYhUvRSfBuGF+AfXMjfjsQh0mgAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog desktop modal rendered correctly inside the phone frame\"\n        title=\"Musclog desktop modal rendered correctly inside the phone frame\"\n        src=\"/static/a2af4c3fcf36eee559fddfb48a5143d1/42a19/musclog-desktop-modal-inside-phone-frame.png\"\n        srcset=\"/static/a2af4c3fcf36eee559fddfb48a5143d1/e3135/musclog-desktop-modal-inside-phone-frame.png 256w,\n/static/a2af4c3fcf36eee559fddfb48a5143d1/06341/musclog-desktop-modal-inside-phone-frame.png 512w,\n/static/a2af4c3fcf36eee559fddfb48a5143d1/42a19/musclog-desktop-modal-inside-phone-frame.png 1024w,\n/static/a2af4c3fcf36eee559fddfb48a5143d1/e8464/musclog-desktop-modal-inside-phone-frame.png 1536w,\n/static/a2af4c3fcf36eee559fddfb48a5143d1/2eb59/musclog-desktop-modal-inside-phone-frame.png 2048w,\n/static/a2af4c3fcf36eee559fddfb48a5143d1/d7fb2/musclog-desktop-modal-inside-phone-frame.png 2184w\"\n        sizes=\"(max-width: 1024px) 100vw, 1024px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog desktop modal rendered correctly inside the phone frame</figcaption>\n  </figure></p>\n<p>Of course they did.</p>\n<p>When the web app lives inside a fake phone on desktop, React Native Web’s default <code class=\"language-text\">Modal</code> behavior starts looking ridiculous very quickly. Portaling straight to <code class=\"language-text\">document.body</code> is fine when the whole app owns the page. It is much less fine when the app is visually clipped inside a phone shell and the modal suddenly decides it belongs to the whole browser window instead.</p>\n<p>So I added a <code class=\"language-text\">WebModalShellProvider</code> high in <code class=\"language-text\">app/app/_layout.tsx</code>, with an overlay host inside the phone shell:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token comment\">// context/WebModalShellContext.web.tsx</span>\r\n<span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\r\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">WebModalShellContext.Provider</span></span> <span class=\"token attr-name\">value</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token punctuation\">{</span> hostElement <span class=\"token punctuation\">}</span><span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">View</span></span> <span class=\"token attr-name\">style</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token punctuation\">{</span> flex<span class=\"token operator\">:</span> <span class=\"token number\">1</span><span class=\"token punctuation\">,</span> minHeight<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span> height<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span> width<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span> position<span class=\"token operator\">:</span> <span class=\"token string\">'relative'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">View</span></span> <span class=\"token attr-name\">style</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token punctuation\">{</span> flex<span class=\"token operator\">:</span> <span class=\"token number\">1</span><span class=\"token punctuation\">,</span> minHeight<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span> height<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span> width<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">}</span></span> <span class=\"token attr-name\">collapsable</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token boolean\">false</span><span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n        </span><span class=\"token punctuation\">{</span>children<span class=\"token punctuation\">}</span><span class=\"token plain-text\">\r\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">View</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">View</span></span>\r\n        <span class=\"token attr-name\">id</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>expo-web-modal-shell-host<span class=\"token punctuation\">\"</span></span>\r\n        <span class=\"token attr-name\">ref</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span>setHostRef<span class=\"token punctuation\">}</span></span>\r\n        <span class=\"token attr-name\">collapsable</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token boolean\">false</span><span class=\"token punctuation\">}</span></span>\r\n        <span class=\"token attr-name\">style</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token punctuation\">{</span>\r\n          position<span class=\"token operator\">:</span> <span class=\"token string\">'absolute'</span><span class=\"token punctuation\">,</span>\r\n          left<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n          right<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n          top<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n          bottom<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n          zIndex<span class=\"token operator\">:</span> <span class=\"token number\">1_000_000</span><span class=\"token punctuation\">,</span>\r\n        <span class=\"token punctuation\">}</span><span class=\"token punctuation\">}</span></span>\r\n      <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\r\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">View</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n  </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">WebModalShellContext.Provider</span></span><span class=\"token punctuation\">></span></span>\r\n<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Then <code class=\"language-text\">Modal.web.tsx</code> does the obvious-not-obvious thing and switches between a portal inside that host and the normal RN modal:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">const</span> useShellPortal <span class=\"token operator\">=</span> Platform<span class=\"token punctuation\">.</span><span class=\"token constant\">OS</span> <span class=\"token operator\">===</span> <span class=\"token string\">'web'</span> <span class=\"token operator\">&amp;&amp;</span> isDesktopFrame <span class=\"token operator\">&amp;&amp;</span> hostElement <span class=\"token operator\">!=</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>useShellPortal<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token keyword\">return</span> <span class=\"token function\">createPortal</span><span class=\"token punctuation\">(</span>\r\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">View</span></span>\r\n      <span class=\"token attr-name\">style</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token punctuation\">{</span>\r\n        position<span class=\"token operator\">:</span> <span class=\"token string\">'absolute'</span><span class=\"token punctuation\">,</span>\r\n        left<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n        right<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n        top<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n        bottom<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n        width<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span>\r\n        height<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span>\r\n      <span class=\"token punctuation\">}</span><span class=\"token punctuation\">}</span></span>\r\n    <span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\r\n      </span><span class=\"token punctuation\">{</span>children<span class=\"token punctuation\">}</span><span class=\"token plain-text\">\r\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">View</span></span><span class=\"token punctuation\">></span></span><span class=\"token punctuation\">,</span>\r\n    hostElement\r\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span>\r\n\r\n<span class=\"token keyword\">return</span> <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">RNModal</span></span> <span class=\"token attr-name\">visible</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span>visible<span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">></span></span><span class=\"token punctuation\">{</span>children<span class=\"token punctuation\">}</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">RNModal</span></span><span class=\"token punctuation\">></span></span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>And the shared overlay hook knows whether it should behave like a full viewport modal or a fake-phone viewport modal:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">useWebModalLayerStyle</span><span class=\"token punctuation\">(</span>options <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span><span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token keyword\">const</span> isDesktopFrame <span class=\"token operator\">=</span> <span class=\"token function\">useWebDesktopPhoneFrame</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>isDesktopFrame<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span>\r\n      position<span class=\"token operator\">:</span> <span class=\"token string\">'absolute'</span><span class=\"token punctuation\">,</span>\r\n      top<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n      left<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n      right<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n      bottom<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n      width<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span>\r\n      height<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span>\r\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\r\n  <span class=\"token punctuation\">}</span>\r\n\r\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span>\r\n    position<span class=\"token operator\">:</span> <span class=\"token string\">'fixed'</span><span class=\"token punctuation\">,</span>\r\n    top<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n    left<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n    right<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n    bottom<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\r\n    width<span class=\"token operator\">:</span> <span class=\"token string\">'100vw'</span><span class=\"token punctuation\">,</span>\r\n    height<span class=\"token operator\">:</span> <span class=\"token string\">'100dvh'</span><span class=\"token punctuation\">,</span>\r\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>That fixed the visual part. Then pointer events decided they wanted attention too.</p>\n<p>On native, <code class=\"language-text\">pointerEvents=\"box-none\"</code> is normal. On HTML, that turns into <code class=\"language-text\">pointer-events: box-none</code>, which is not real CSS, which means the browser gets to improvise. So there is now a defensive rule for the overlay host:</p>\n<div class=\"gatsby-highlight\" data-language=\"css\"><pre class=\"language-css\"><code class=\"language-css\"><span class=\"token selector\">.expo-web-app-shell #expo-web-modal-shell-host</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token property\">pointer-events</span><span class=\"token punctuation\">:</span> none <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span>\r\n\r\n<span class=\"token selector\">.expo-web-app-shell #expo-web-modal-shell-host *</span> <span class=\"token punctuation\">{</span>\r\n  <span class=\"token property\">pointer-events</span><span class=\"token punctuation\">:</span> auto <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>There is also a <code class=\"language-text\">useLayoutEffect</code> forcing the same thing inline, because by that point I was no longer interested in finding out how polite the correct solution was.</p>\n<h2 id=\"look-i-have-a-type\" style=\"position:relative;\"><a href=\"#look-i-have-a-type\" aria-label=\"look i have a type permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Look, I have a type</h2>\n<p>If you’ve been reading this blog for a while, none of this should surprise you.</p>\n<p>I already <a href=\"/en/blog/coding/i-made-a-top-down-game-version-of-my-blog-with-phaser-and-react/\">turned this blog into a top-down RPG</a> because the Konami Code deserved a bigger payoff than just a background effect.</p>\n<p>Then I <a href=\"/en/blog/coding/my-blog-now-has-stories-and-im-not-sure-why/\">added Stories to the blog</a> because my internet provider’s app had Stories, and something about that annoyed me deeply enough that I made it everybody else’s problem.</p>\n<p>So yes, of course my fitness app now ships its website from the same Expo Router repo, and of course the desktop web app runs inside a giant picture of a phone. This is not a surprising escalation. This is just continuity.</p>\n<h2 id=\"why-im-keeping-it\" style=\"position:relative;\"><a href=\"#why-im-keeping-it\" aria-label=\"why im keeping it permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Why I’m keeping it</h2>\n<p>Past the comedy value, this fixed a very real maintenance tax.</p>\n<p>Translations are shared. Design tokens are shared. Components are shared. Legal pages live next to the product they are legally defending. When I change copy, screenshots, or feature positioning, I don’t need to remember which repo holds the respectable public version of the same app.</p>\n<p>Deploy also got nicer. One build. One export. One artifact. The website and the web app cannot drift apart unless I deliberately make them drift apart, which is much harder when they are literally shipped together.</p>\n<p>And when I shipped the <a href=\"/en/blog/coding/musclog-redesign-nutrition-tracking-and-why-your-fitness-app-subscription-is-a-scam/\">Musclog redesign</a>, the website basically came along for the ride. Same spacing tokens, same colors, updated screenshots, same PR. Exactly the kind of boring maintenance win that makes a slightly weird architecture decision feel smart six months later.</p>\n<h2 id=\"conclusion\" style=\"position:relative;\"><a href=\"#conclusion\" aria-label=\"conclusion permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Conclusion</h2>\n<p>Could I have kept the website in a separate Next.js repo like a more emotionally stable person? Sure.</p>\n<p>Was I ever going to keep doing that once Expo Router made this possible? No.</p>\n<p>The funny part is that the routing was the boring part. Expo Router handled that just fine. The annoying work lived in all the little browser lies around it: route gating before hydration, raw HTML i18n, asset syncing, shell-aware modals, pointer-event weirdness, and convincing desktop Chrome to temporarily believe it was a phone.</p>\n<p>Still worth it.</p>\n<p>If you want to poke around the code, <a href=\"https://github.com/blopa/musclog-app\" target=\"_blank\" rel=\"noreferrer\">Musclog is open-source on GitHub</a>. If you want to use it, it’s at <a href=\"https://musclog.app\" target=\"_blank\" rel=\"noreferrer\">musclog.app</a>. The website lives in the app repo now.</p>\n<p>Everybody lives here now.</p>","fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI2LTA0LTI1VDAwOjAwOjAwLjAwMFo=","slug":"/2026/2026-04-25-website-or-app-yes-hacky-expo-router-guide.en/","path":"/blog/coding/website-or-app-yes-hacky-expo-router-guide/","locale":"en"},"frontmatter":{"tags":["expo","expo router","react native","musclog","website","monorepo","javascript","typescript"],"categories":["coding"],"allowComments":true,"publishOnMedium":true,"cover":null,"date":"2026-04-25T00:00:00.000Z","id":null,"path":"website-or-app-yes-hacky-expo-router-guide","show":true,"title":"Website or App? Yes! The hacky guide to building sites with Expo Router","hideExcerpt":false,"subtitle":"The routing part was easy. The fake phone in the browser was not."}}}],"ogImage":"/static/f71a459f3034e47544de2f3ce309dd61/42a19/musclog-shared-router-website-home-1.png","language":"en","intl":{"language":"en","languages":["en","pt-br"],"messages":{"site_title":"pablo.gg","title":"Title","author":"@thepiratepablo","search_placeholder":"Search...","about":"About","photos":"Photos","archive":"Archive","contact":"Contact","close":"Close","contact_page":"Contact page","see_more":"See more posts","built_with":"Built with ","buy_me_a_soda":"Buy me a soda","blog":"Blog","blog_posts":"Blog posts","go_to_post":"Go to post","search":"Search","loading":"Loading...","search_results":"Search results","search_results_for":"{quantity} search results for: \"{query}\"","search_for_query":"Search for \"{query}\"","no_results":"No results","home":"Home","description":"Yet another developer personal blog","go_back":"Go back to the homepage","thats_me":"That's me!","got_it":"Got it!","check_it_out":"Check it out!","we_are":"We are","e3":"E3","away_from_next_sgf":"away from Summer Game Fest 2026","away_from_next_gamescom":"away from Gamescom 2026","sgf_countdown":"Summer Game Fest Countdown","gamescom_countdown":"Gamescom Countdown","e3_paragraph_1":"This page once featured a countdown to the next E3 event, a moment that countless gamers and industry professionals looked forward to each year. E3 was not just an event; it was a celebration of our shared passion for video games, a place where dreams were realized, and memories were made.","e3_paragraph_2":"From the electrifying announcements to the hands-on demos, E3 was the heartbeat of the gaming world. It brought together people from all corners of the globe, united by their love for games. For many, it was a chance to meet their heroes, discover new titles, and experience the thrill of the latest innovations in gaming technology.","e3_paragraph_3":"However, as the gaming landscape has evolved, so too has the way we connect and celebrate our passion. While E3 has come to an end, the spirit of excitement and community it fostered lives on. We now look forward to new ways of coming together, sharing our love for games, and creating new memories.","e3_paragraph_4":"Though the countdown is gone, the legacy of E3 will forever remain in our hearts, reminding us of the incredible journeys we've taken and the bonds we've formed along the way.","sec":"Sec","secs":"Secs","min":"Min","mins":"Mins","hour":"Hour","hours":"Hours","day":"Day","days":"Days","month":"Month","months":"Months","year":"Year","years":"Years","recent_posts":"Recent posts","email":"Email","twitter":"Twitter","name":"Name","page":"Page","fill_this_want_reply":"Fill this in if you want me to contact you back","sorry_this_post_unavailable_language":"Sorry, this post is not available in the language you picked","language":"Language","comment":"Comment","comments":"Comments","no_comments":"No comments yet.","post_comment":"Post comment","send_message":"Send message","message":"Message","post_a_comment":"Post a comment","your_comment_submitted":"Your comment has been successfully submitted.","your_message_submitted":"Your message has been successfully submitted.","on":"on","ok":"Ok","copy":"Copy","copied":"Copied","photo_num":"Photo {num}","the_matrix_has_you":"The Matrix has you...","about_paragraph_1":"A technology enthusiast from an early age, I have always been interested in computers and video games.","about_paragraph_2":"I graduated in Information Technology at Estácio de Sá University and always try to find out about new technologies and get involved in new development projects, some of which the code can be found on GitHub.","cookie_banner_consent":"By using this website you agree to our use of cookies to deliver a better experience.","written_in":"Written in ","no_post_this_tag":"No post in English include this tag.","tags":"Tags","tag_colon":"Tag: ","tags_colon":"Tags: ","posts_tagged":"Posts tagged with ","categories":"Categories","category":"Category","category_colon":"Category: ","posts_on_category":"Posts on category ","related_posts":"Related posts","read_time":"🕒 {time} min. read","create_post":"Create Post","show":"Show","date":"Date","download":"Download","add_tag":"Add Tag","hide_excerpt":"Hide Excerpt","publish_on_medium":"Publish on Medium","allow_comments":"Allow Comments","subtitle":"Subtitle","you_must_be_truly_desperate":"You must be truly desperate to come to me for help","game.game_title":"pablo.gg - The Game","game.next":"Next","game.ok":"Ok","game.loading_asset_colon":"Loading asset:","game.loading":"Loading...","game.characters.npc_01":"Frost","game.characters.npc_02":"Gavin","game.characters.npc_03":"Giles","game.characters.npc_04":"Godfrey","game.characters.npc_05":"Hugh","game.characters.npc_06":"Ivar","game.characters.npc_07":"Leopold","game.characters.npc_08":"Lucian","game.characters.npc_09":"Gumercindo","game.characters.npc_10":"Mr. Flower","game.characters.npc_11":"Maxim","game.characters.npc_12":"Milo","game.characters.npc_13":"Otto","game.characters.npc_14":"Palmer","game.characters.npc_15":"Quentin","game.characters.npc_16":"Sebastian","game.characters.npc_17":"Neville","game.characters.npc_18":"Cassian","game.characters.npc_19":"Balthasar","game.characters.npc_20":"Jasper","game.characters.sign_01":"Sign","game.characters.book_01":"Book","game.characters.home_page_city_sign":"Sign","game.characters.coding_category_city_sign_01":"Sign","game.characters.coding_category_city_sign_02":"Sign","game.characters.events_category_city_sign":"Sign","game.characters.funny_category_city_sign":"Sign","game.characters.gadgets_category_city_sign_01":"Sign","game.characters.gadgets_category_city_sign_02":"Sign","game.characters.games_category_city_sign":"Sign","game.characters.general_category_city_sign":"Sign","game.characters.tips_category_city_sign":"Sign","game.characters.collectibles_category_city_sign":"Sign","game.characters.sword":"Info","game.characters.push":"Info","game.gamepad.a_button":"A Button","game.gamepad.b_button":"B Button","game.gamepad.d_pad_left":"D-Pad Left","game.gamepad.d_pad_up":"D-Pad Up","game.gamepad.d_pad_right":"D-Pad Right","game.gamepad.d_pad_down":"D-Pad Down","game.gamepad.start_button":"Start Button","game.menu.start":"Start","game.menu.exit":"Exit","game.menu.settings":"Settings","game.game_over.game_over":"Game Over","game.game_over.retry":"Retry","game.game_over.exit":"Exit","game.start_menu.save_game":"Save Game","game.start_menu.exit":"Exit","game.browse_posts.choose_a_post":"Choose a Post to read","game.dialogs.npc_01.01":"Hey, you're finally awake!","game.dialogs.npc_01.02":"What, you don't know where you are?","game.dialogs.npc_01.03":"Don't be silly, you're at the Home Page City, remember?","game.dialogs.npc_01.04":"This city was founded by Pablo Montenegro to be the start of your journey","game.dialogs.npc_01.05":"Go explore the world and find other cities where you can read the accumulated knowledge of our civilization...","game.dialogs.npc_01.06":"... some people call it \"Blog Posts\", I don't know why...","game.dialogs.npc_02.01":"Be careful with the Slimes that live in the wild.","game.dialogs.npc_02.02":"Press SPACE to use your sword","game.dialogs.npc_02.03":"What is SPACE? I have no idea.","game.dialogs.npc_03.01":"Hello, welcome to our library","game.dialogs.npc_03.02":"We only have one book, which contains all of this city category publications.","game.dialogs.npc_03.03":"Go check it out!","game.dialogs.npc_04.01":"I like snails","game.dialogs.npc_05.01":"Incomplete sentences may cause some","game.dialogs.npc_06.01":"Red is greener than purple, for sure.","game.dialogs.npc_07.01":"Having a beard is the new not having a beard","game.dialogs.npc_08.01":"Sup","game.dialogs.npc_09.01":"\" - Cooper, what are you doing?\"\n\" - Docking.\"","game.dialogs.npc_10.01":"I should buy a boat","game.dialogs.npc_11.01":"Have you heard the me neither joke? Me neither.","game.dialogs.npc_12.01":"I clean toilet and rescue princesses, good life, yes?","game.dialogs.npc_13.01":"We want the airwaves back","game.dialogs.npc_14.01":"Save the cheerleader, save the world","game.dialogs.npc_15.01":"Hello, how are you?","game.dialogs.npc_15.02":"Ok bye!","game.dialogs.npc_16.01":"A kangaroo is really just a rabbit on steroids","game.dialogs.npc_17.01":"For the 216th time, he said he would quit drinking soda after this last Coke","game.dialogs.npc_18.01":"Nothing and everything is possimpible","game.dialogs.npc_19.01":"For a city called \"Events\", there isn't much going on...","game.dialogs.npc_20.01":"I have heard that there's a way to push some objects in this game, but I don't know how to do it.","game.dialogs.sign_01.01":"Congrats, you can read this!","game.dialogs.book_01.01":"Hey, thanks for trying out this very weird way to access my website","game.dialogs.book_01.02":"This project wouldn't be possible without the awesome open-source work of many people, like:","game.dialogs.book_01.03":"ArMM1998 - For the characters sprites and tilesets","game.dialogs.book_01.04":"PixElthen - For the slime sprites","game.dialogs.book_01.05":"pixelartm - For the pirate hat sprites","game.dialogs.book_01.06":"jkjkke - For the Game Over screen background","game.dialogs.book_01.07":"KnoblePersona - For the Main Menu screen background","game.dialogs.book_01.08":"Min - For the open book sprite","game.dialogs.book_01.09":"And of course, Richard Davey for making Phaser.io!","game.dialogs.home_page_city_sign":"City of Home Page","game.dialogs.coding_category_city_sign.01":"City of Coding Category","game.dialogs.coding_category_city_sign.02":"City of Coding Category","game.dialogs.events_category_city_sign":"City of Events Category","game.dialogs.funny_category_city_sign":"City of Funny Category","game.dialogs.gadgets_category_city_sign.01":"City of Gadgets Category","game.dialogs.gadgets_category_city_sign.02":"City of Gadgets Category","game.dialogs.games_category_city_sign":"City of Games Category","game.dialogs.general_category_city_sign":"City of General Category","game.dialogs.tips_category_city_sign":"City of Tips Category","game.dialogs.collectibles_category_city_sign":"City of Collectibles Category","game.dialogs.sword_item_description":"You can now attack, press SPACE to use your sword.","game.dialogs.push_item_description":"Now you can push some objects, press SPACE in front of a object to use push it.","zelda_timeline.title":"Zelda Timeline","zelda_timeline.timeline_split":"Timeline Split","zelda_timeline.timeline_unification":"Timeline Unification","zelda_timeline.icons_from":"The icons used on this page are from zeldauniverse.net and game-icons.net","zelda_timeline.creation":"Creation","zelda_timeline.creation_of_land_sky":"The Creation of the Land and Sky","zelda_timeline.goddess_hylia_and_sky_era":"Goddess Hylia and Sky Era","zelda_timeline.skyward_sword":"Skyward Sword","zelda_timeline.the_ancient_battle":"The Ancient Battle and the reincarnation of the Goddess Hylia","zelda_timeline.return_to_surface":"Return to the surface","zelda_timeline.era_of_chaos":"Era of Chaos","zelda_timeline.sacred_realm_sealed":"The Sacred Realm is Sealed","zelda_timeline.era_of_prosperity":"Era of Prosperity","zelda_timeline.establishment_of_hyrule":"The Hyrule Kingdom is established","zelda_timeline.force_era":"Force Era","zelda_timeline.the_minish_cap":"The Minish Cap","zelda_timeline.rise_of_evil_vaati":"The Rise of the Evil Vaati","zelda_timeline.four_swords":"Four Swords","zelda_timeline.resurrection_of_vaati":"The Resurrection of Vaati","zelda_timeline.era_of_the_hero_of_time":"Era of the Hero of Time","zelda_timeline.hyrulean_civil_war":"Hyrulean Civil War","zelda_timeline.ocarina_of_time":"Ocarina of Time","zelda_timeline.sacred_realm_becomes_dark_world":"The Sacred Realm Becomes the Dark World","zelda_timeline.ganondorf_becomes_ganon":"Ganondorf becomes Ganon","zelda_timeline.hero_is_defeated":"Hero is defeated","zelda_timeline.decline_of_last_hero":"The Decline of Hyrule and the Last Hero","zelda_timeline.the_imprisoning_war":"The Imprisoning War","zelda_timeline.era_of_dark_and_light":"Era of Light and Dark","zelda_timeline.a_link_to_the_past":"A Link to the Past","zelda_timeline.resurrection_of_ganon":"The Resurrection of Ganon","zelda_timeline.resurrection_of_ganon_is_prevented":"The Resurrection of Ganon is prevented","zelda_timeline.links_awakening":"Link's Awakening","zelda_timeline.oracle_of_ages_and_seasons":"Oracle of Ages and Oracle of Seasons","zelda_timeline.a_link_between_worlds":"A Link Between Worlds","zelda_timeline.tri_force_heroes":"Tri Force Heroes","zelda_timeline.the_gold_era":"The Gold Era","zelda_timeline.monarchs_of_hyrule_use_triforce":"The Monarchs of Hyrule Use the Triforce","zelda_timeline.era_of_decline":"The Era of Decline","zelda_timeline.tragedy_of_princess_zelda_1":"The Tragedy of Princess Zelda I","zelda_timeline.the_legend_of_zelda":"The Legend of Zelda","zelda_timeline.adventure_of_link":"Adventure of Link","zelda_timeline.hero_defeated":"Hero Defeated","zelda_timeline.child_era":"Child Era","zelda_timeline.adult_era":"Adult Era","zelda_timeline.sacred_realm_protected":"The Sacred Realm Remains Protected","zelda_timeline.twilight_realm_and_legacy_of_hero":"The Twilight Realm and the Legacy of the Hero","zelda_timeline.majoras_mask":"Majora's Mask","zelda_timeline.prince_of_thieves_is_executed":"The Demon Thief, Ganondorf, Is Executed","zelda_timeline.twilight_era":"The Twilight Era","zelda_timeline.twilight_princess":"Twilight Princess","zelda_timeline.shadow_invasion":"The Shadow Invasion","zelda_timeline.shadow_era":"The Shadow Era","zelda_timeline.four_swords_adventures":"Four Swords Adventures","zelda_timeline.reincarnation_of_ganondorf":"The Reincarnation of Ganondorf","zelda_timeline.ganondorf_is_sealed":"Ganondorf is sealed","zelda_timeline.hero_of_wind_and_new_world":"The Hero of Wind and a New World","zelda_timeline.era_without_a_hero":"The Era without a Hero","zelda_timeline.ganondorf_is_resurrected":"Ganondorf is Resurrected","zelda_timeline.hyrule_is_sealed_and_flooded":"Hyrule is sealed and then flooded","zelda_timeline.era_of_the_great_sea":"The Era of the Great Sea","zelda_timeline.the_wind_waker":"The Wind Waker","zelda_timeline.era_of_the_great_voyage":"The Era of the Great Voyage","zelda_timeline.phantom_hourglass":"Phantom Hourglass","zelda_timeline.era_of_hyrule_rebirth":"The Era of Hyrule's Rebirth","zelda_timeline.new_continent_discovered":"New Continent Discovered","zelda_timeline.new_hyrule_is_founded":"A New Hyrule Kingdom is founded","zelda_timeline.spirit_tracks":"Spirit Tracks","zelda_timeline.evil_king_malladus_is_resurrected":"The Evil King Malladus is Resurrected","zelda_timeline.age_of_calamity":"Age of Calamity","zelda_timeline.breath_of_the_wild":"Breath of the Wild","zelda_timeline.era_of_the_wilds":"Era of the Wilds","zelda_timeline.calamity_ganon_is_sealed":"Calamity Ganon is Sealed. Technology is banned, leading some Sheikah to form the Yiga Clan","zelda_timeline.divine_beasts_are_cleansed":"The Divine Beasts are cleansed and Calamity Ganon is Sealed","zelda_timeline.tears_of_the_kingdom":"Tears of the Kingdom","zelda_timeline.hyrule_kingdom_is_teared_apart":"Ganondorf is Resurrected (I think?)","blog_categories.games":"Games","blog_categories.general":"General","blog_categories.tips":"Tips","blog_categories.events":"Events","blog_categories.coding":"Coding","blog_categories.funny":"Funny","blog_categories.collectibles":"Collectibles","blog_categories.gadgets":"Gadgets","forty_two_page.title":"Forty Two","forty_two_page.description":"So long and thanks for all the fish!","projects_page.title":"Projects","projects_page.description":"Here is a list of some of my favorites personal projects.","projects_page.gatsbyMaterialUiBlogDescription":"A simple Gatsby Blog Starter with Material UI.","projects_page.contractBuilderDescription":"Contract Builder is a free open-source project that allows anyone to easily maintain and build any kind of contract (legal documents, lawsuit, rent, agreements, construction and so on) using Google Spreadsheets. This was develop as a personal project to help a friend who was struggling spending up to an hour to make a custom contract, now she is able to do it in less than 5 minutes. Hooray!","projects_page.resumeBuilderDescription":"Resume Builder is a free open-source project that allows anyone to easily maintain and build any kind of resume using Google Spreadsheets. This was develop as a personal project to help a friend who was struggling spending up to an hour to make a custom resumes.","projects_page.magentoChatbotDescription":"With this module you can fully integrate your Magento store with the most popular chat apps in the market. This means that by simply installing this module and a few clicks you can have a new way to show and sell your products to your clients. Very easy to use! Try now, it's FREE.","projects_page.jamStackSortenerDescription":"This is a basic URL shortener POC build with Gatsby.","projects_page.gotinhaDescription":"It was always my dream to make my own game, and after trying Unity a couple years ago, I decided to try it again with something I'm more familiar with: Javascript. As a frontend developer, Javascript is already the language that I write most of my code at work and also in my personal projects, and after a quick search I was able to find the amazing PhaserJS Framework for building 2D web games.","e3_2012_photos.title":"E3 2012","e3_2012_photos.description":"On June 2012 I attended E3 as a media press for a full coverage for Nintendo Blast.","e3_2013_photos.title":"E3 2013","e3_2013_photos.description":"On June 2013 I attended E3 as a media press for a full coverage for Game Blast.","e3_2014_photos.title":"E3 2014","e3_2014_photos.description":"On June 2014 I attended E3 as a media press for a full coverage for Game Blast.","e3_2015_photos.title":"E3 2015","e3_2015_photos.description":"On June 2015 I attended E3 as a media press for a full coverage for Game Blast and Game Over TV.","e3_2017_photos.title":"E3 2017","e3_2017_photos.description":"On June 2012 I attended E3 as a media press for a full coverage for PlayReplay and Game Over TV.","e3_2019_photos.title":"E3 2019","e3_2019_photos.description":"On June 2019 I attended E3 as a media press for a full coverage for PlayReplay.","gamescom_2019_photos.title":"Gamescom 2019","gamescom_2019_photos.description":"On August 2019 I attended Gamescom as a media press for a full coverage for PlayReplay.","san_francisco_2019_photos.title":"San Francisco 2019","san_francisco_2019_photos.description":"On September 2019 I've traveled to San Francisco for the Metallica S&M2 concert.","notfound.title":"404: Not found","notfound.header":"404 NOT FOUND","notfound.description":"Sorry, this page doesn't seem to exist. Perhaps the archives are incomplete?","seo_keywords.developer":"developer","seo_keywords.development":"development","seo_keywords.javascript":"javascript","seo_keywords.es6":"es6","seo_keywords.e3":"e3","seo_keywords.sgf":"sgf","seo_keywords.gamescom":"gamescom","seo_keywords.countdown":"countdown","seo_keywords.archive":"archive","seo_keywords.about_me":"about me","seo_keywords.personal_blog":"personal blog","seo_keywords.personal_projects":"personal projects","seo_keywords.travels":"travels","seo_keywords.tips":"tips","seo_keywords.lifehacks":"lifehacks","seo_keywords.reviews":"reviews","seo_keywords.games":"games","seo_keywords.timeline":"timeline","seo_keywords.photos":"photos","cookie_law.we_use_cookies":"We use cookies to ensure you get the best experience on our website. By using our website you agree to our ","cookie_law.title":"Cookie policy","cookie_law.what_are_cookies":"What are cookies?","cookie_law.what_are_cookies_text":"As is common practice with almost all professional websites this site uses cookies, which are tiny files that are downloaded to your computer, to improve your experience. This page describes what information they gather, how we use it and why we sometimes need to store these cookies. We will also share how you can prevent these cookies from being stored however this may downgrade or 'break' certain elements of the sites functionality. For more general information on cookies, please read ","cookie_law.what_are_cookies_more_info_url":"https://en.wikipedia.org/wiki/HTTP_cookie","cookie_law.how_we_use_cookies":"How we use cookies","cookie_law.how_we_use_cookies_text":"We use cookies for a variety of reasons detailed below. Unfortunately in most cases there are no industry standard options for disabling cookies without completely disabling the functionality and features they add to this site. It is recommended that you leave on all cookies if you are not sure whether you need them or not in case they are used to provide a service that you use.","cookie_law.disabling_cookies":"Disabling cookies","cookie_law.disabling_cookies_text":"You can prevent the setting of cookies by adjusting the settings on your browser (see your browser Help for how to do this). Be aware that disabling cookies will affect the functionality of this and many other websites that you visit. Disabling cookies will usually result in also disabling certain functionality and features of the this site. Therefore it is recommended that you do not disable cookies.","cookie_law.the_cookies_we_set":"The cookies we set","cookie_law.site_preferences_cookie":"Site preferences cookies","cookie_law.site_preferences_cookie_text":"In order to provide you with a great experience on this site we provide the functionality to set your preferences for how this site runs when you use it. In order to remember your preferences we need to set cookies so that this information can be called whenever you interact with a page is affected by your preferences.","cookie_law.third_party_cookies":"Third party cookies","cookie_law.third_party_cookies_text":"In some special cases we also use cookies provided by trusted third parties. The following section details which third party cookies you might encounter through this site.","cookie_law.third_party_cookies_item_1":"This site uses Google Analytics which is one of the most widespread and trusted analytics solution on the web for helping us to understand how you use the site and ways that we can improve your experience. These cookies may track things such as how long you spend on the site and the pages that you visit so we can continue to produce engaging content. For more information on Google Analytics cookies, see the official Google Analytics page.","cookie_law.third_party_cookies_item_2":"From time to time we test new features and make subtle changes to the way that the site is delivered. When we are still testing new features these cookies may be used to ensure that you receive a consistent experience whilst on the site whilst ensuring we understand which optimisations our users appreciate the most.","cookie_law.more_information":"More information","cookie_law.more_information_text":"Hopefully that has clarified things for you and as was previously mentioned if there is something that you aren't sure whether you need or not it's usually safer to leave cookies enabled in case it does interact with one of the features you use on our site. However if you are still looking for more information then you can contact us through our "},"routed":true,"originalPath":"/blog/tags/expo-router","redirect":true,"redirectDefaultLanguageToRoot":false,"defaultLanguage":"en","fallbackLanguage":"","ignoredPaths":[]},"locale":"en"}},
    "staticQueryHashes": ["1156153307","1355482417","1591365477","1628619374","2127381735","2288279559","26159077","3566410298","3649515864","3847325417","3982724423","928834867"]}