{
    "componentChunkName": "component---src-templates-blog-post-jsx",
    "path": "/pt-br/blog/coding/site-ou-app-sim-o-guia-gambiarrístico-para-sites-feitos-em-expo-router/",
    "result": {"data":{"site":{"siteMetadata":{"siteUrl":"https://pablo.gg"}},"markdownRemark":{"id":"e4f83ec8-3a67-5b7a-8ad3-eae98b17d4f7","excerpt":"Uma das side quests mais chatas de publicar um app na Google Play Store é que o Google às vezes decide que você também virou dono de site. Lá em 2024, quando eu…","html":"<p>Uma das side quests mais chatas de publicar um app na Google Play Store é que o Google às vezes decide que você também virou dono de site.</p>\n<p>Lá em 2024, quando eu <a href=\"/pt-br/blog/coding/musclog-aproveitando-minha-experiencia-com-reactjs-para-criar-um-app-em-react-native/\">escrevi pela primeira vez sobre o Musclog</a>, o site existia basicamente porque o Google queria que ele existisse. Eu precisava da política de privacidade, das páginas públicas, do kit básico do desenvolvedor de app respeitável, então subi rapidinho um site pequeno em Next.js, consegui aprovar o app, e segui com a minha vida.</p>\n<p>Funcionou perfeitamente até o dia em que eu precisei mexer no site de novo.</p>\n<p>Screenshots novas? Outro repo. Texto de feature nova? Outro repo. Atualização de página legal? Outro repo. Ajustezinho de tradução? Outro repo.</p>\n<p>Nada disso era difícil de verdade, o que quase piorava a situação. Era só chato o suficiente pra continuar me lembrando que eu tinha dividido um produto em duas bases de código por um motivo que eu já não respeitava mais.</p>\n<p>Então agora eu não faço mais isso.</p>\n<p>O site do Musclog mora dentro do repo do app. Mesmo projeto com Expo Router. Mesmo deploy. <a href=\"https://musclog.app/\" target=\"_blank\" rel=\"noreferrer\">musclog.app</a> é o site público, <a href=\"https://musclog.app/app\" target=\"_blank\" rel=\"noreferrer\">musclog.app/app</a> é o app web de verdade, e Android + iOS continuam saindo da mesma base de código.</p>\n<p>Boa prática? Questionável. Conveniente? Demais.</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=\"Site público do Musclog renderizado pela mesma base de código com Expo Router do app\"\n        title=\"Site público do Musclog renderizado pela mesma base de código com Expo Router do 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\">Site público do Musclog renderizado pela mesma base de código com Expo Router do app</figcaption>\n  </figure></p>\n<h2 id=\"ok-mas-por-que\" style=\"position:relative;\"><a href=\"#ok-mas-por-que\" aria-label=\"ok mas por que 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 mas por quê?</h2>\n<p>Basicamente porque o Expo Router arrancou a minha última desculpa.</p>\n<p>O Musclog já usava <a href=\"https://expo.github.io/router/docs\" target=\"_blank\" rel=\"noreferrer\">Expo Router</a>, então o app já estava organizado em rotas baseadas em arquivos dentro da pasta <code class=\"language-text\">app/</code>. Aí eu percebi que podia simplesmente criar um grupo de rotas chamado <code class=\"language-text\">(website)</code>, mover as páginas públicas pra lá, deixar o app de verdade em <code class=\"language-text\">app/app/*</code>, e parar de fingir que eu estava lidando com dois produtos diferentes.</p>\n<p>O repo separado tinha virado uma daquelas decisões de arquitetura que parecem limpas na teoria e depois ficam te cobrando imposto de manutenção pra sempre:</p>\n<ul>\n<li>dois PRs pra uma mudança num produto</li>\n<li>dois deploys</li>\n<li>dois lugares pras traduções</li>\n<li>dois lugares pras páginas legais</li>\n<li>dois lugares pra esquecer alguma coisa</li>\n</ul>\n<p>Sem condições.</p>\n<h2 id=\"a-divisao\" style=\"position:relative;\"><a href=\"#a-divisao\" aria-label=\"a divisao 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>A divisão</h2>\n<p>A parte engraçada é que o roteamento em si acabou sendo a parte menos amaldiçoada dessa história:</p>\n<ul>\n<li><code class=\"language-text\">app/app/*</code> é o Musclog de verdade. Treinos, nutrição, coach com IA, tudo.</li>\n<li><code class=\"language-text\">app/(website)/*</code> é o site público. Landing page, política de privacidade, termos, contato, calculadora.</li>\n</ul>\n<p>A rota raiz só checa a plataforma e te joga pra onde você pertence:</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>É isso. No web, <code class=\"language-text\">/</code> vira o site. No native, <code class=\"language-text\">/</code> vira o app.</p>\n<p>O site também ganha o próprio layout mais leve:</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>Isso importa porque o layout do app de verdade já tinha crescido e virado um layout de app mesmo. Boot de banco, migrações, React Query, safe area, gestos, modais, snackbars, câmera, contexto do coach, todas aquelas mentiras clássicas de “é só um projetinho paralelo”. O site não precisa de nada disso só pra explicar o que o app faz.</p>\n<p>E se algum usuário nativo cair numa rota exclusiva do site, a correção é maravilhosamente direta:</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>Pronto. Volta pros 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=\"Site público do Musclog renderizado pela mesma base de código com Expo Router do app\"\n        title=\"Site público do Musclog renderizado pela mesma base de código com Expo Router do 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\">Site público do Musclog renderizado pela mesma base de código com Expo Router do app</figcaption>\n  </figure></p>\n<h2 id=\"a-palhacada-do-celular-no-desktop\" style=\"position:relative;\"><a href=\"#a-palhacada-do-celular-no-desktop\" aria-label=\"a palhacada do celular no desktop 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>A palhaçada do celular no desktop</h2>\n<p>Foi aqui que eu parei de me comportar como uma pessoa normal.</p>\n<p>Eu queria que <a href=\"https://musclog.app/app/\" target=\"_blank\" rel=\"noreferrer\">musclog.app/app</a>, no desktop, mostrasse o app funcionando dentro de uma moldura de celular. Um pouco porque fica bonito. Um pouco porque funciona como preview do produto. Mas principalmente porque, depois que a ideia entrou na minha cabeça, qualquer opção menos ridícula começou a parecer errada.</p>\n<p>O detalhe chato era que <code class=\"language-text\">/app</code> precisava parecer um celular, mas <code class=\"language-text\">/home</code> definitivamente não. Então só o roteamento não bastava. Eu precisava que o shell bruto de HTML soubesse, antes da hidratação, se o navegador deveria renderizar a gambiarra do celular fake ou não.</p>\n<p>Essa lógica mora em <code class=\"language-text\">app/+html.tsx</code>, dentro de um script no <code class=\"language-text\">&lt;head></code> do documento:</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>A parte importante é a gambiarra em <code class=\"language-text\">pushState</code> / <code class=\"language-text\">replaceState</code>. A primeira renderização foi tranquila. A navegação client-side é que era a parte chata. Sem isso, dava pra sair de <code class=\"language-text\">/app</code> e continuar com o shell errado ali, como se o Chrome tivesse esquecido em que página estava.</p>\n<p>O shell HTML em si é basicamente o painel da landing, o app roteado, e a moldura do celular:</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>Aí o CSS comete o 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>Sim, eu medi a área transparente da tela dentro do PNG até o app encaixar direito. Não tem geometria elegante nenhuma por trás dessas porcentagens. Era só eu, uma moldura de celular, e muito empurra-pra-lá até o app parar de parecer torto.</p>\n<p>Nas rotas que não são <code class=\"language-text\">/app</code>, o espetáculo inteiro é desligado:</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>Sim, usa <code class=\"language-text\">!important</code>. Isso é CSS de pré-hidratação cujo trabalho inteiro é manter a mentira de pé. Não estamos fazendo arquitetura refinada nessa camada.</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=\"App web do Musclog no desktop renderizado dentro de uma moldura de celular ao lado da landing\"\n        title=\"App web do Musclog no desktop renderizado dentro de uma moldura de celular ao lado da landing\"\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\">App web do Musclog no desktop renderizado dentro de uma moldura de celular ao lado da landing</figcaption>\n  </figure></p>\n<h2 id=\"tres-pequenos-crimes\" style=\"position:relative;\"><a href=\"#tres-pequenos-crimes\" aria-label=\"tres pequenos 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>Três pequenos crimes</h2>\n<p><img src=\"/377ac4354ce553fccf98c5d8d1017e50/straight-to-jail.gif\" alt=\"Direto pra cadeia. Na hora.\"></p>\n<p>Depois que o shell funcionou, os incômodos menores começaram a aparecer como se tivessem marcado horário.</p>\n<h3 id=\"1-o-painel-da-landing-precisava-de-i18n-antes-da-hidratacao\" style=\"position:relative;\"><a href=\"#1-o-painel-da-landing-precisava-de-i18n-antes-da-hidratacao\" aria-label=\"1 o painel da landing precisava de i18n antes da hidratacao 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. O painel da landing precisava de i18n antes da hidratação</h3>\n<p>O texto do lado do celular mora em HTML cru, o que significa que React e i18n ainda estão dormindo quando a página renderiza pela primeira vez. Então, se eu quisesse que a página em português não desse um flash de inglês antes, eu tinha que remendar isso manualmente a partir do <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>Não é elegante. Também evita jogar um flashbang de texto em inglês numa página em português, então eu vou manter.</p>\n<h3 id=\"2-assets-em-html-cru-nao-recebem-a-ajuda-de-sempre-do-expo\" style=\"position:relative;\"><a href=\"#2-assets-em-html-cru-nao-recebem-a-ajuda-de-sempre-do-expo\" aria-label=\"2 assets em html cru nao recebem a ajuda de sempre do expo 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. Assets em HTML cru não recebem a ajuda de sempre do Expo</h3>\n<p>O PNG da moldura do celular e o QR code também são referenciados a partir de HTML cru, então aquela conveniência normal de assets do Expo não me ajudava muito ali. Resultado: eu precisei de um helper pra base URL:</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>E como esses arquivos vivem fora do pipeline normal de assets do React Native, eu copio tudo pra <code class=\"language-text\">public/</code> antes de rodar dev e export. Esquece esse passo uma vez e o site imediatamente te lembra quem manda.</p>\n<h3 id=\"3-o-expo-export-as-vezes-precisava-de-apoio-emocional\" style=\"position:relative;\"><a href=\"#3-o-expo-export-as-vezes-precisava-de-apoio-emocional\" aria-label=\"3 o expo export as vezes precisava de apoio emocional 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. O <code class=\"language-text\">expo export</code> às vezes precisava de apoio emocional</h3>\n<p>Em alguns ambientes, <code class=\"language-text\">expo export --platform web</code> terminava com sucesso e depois só… continuava vivo sem motivo nenhum. Pasta <code class=\"language-text\">dist</code> lá. Arquivos gerados. Processo espiritualmente concluído, tecnicamente ainda pendurado.</p>\n<p>Então agora existe um script wrapper:</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>Não é glamouroso… mas resolve.</p>\n<h2 id=\"ai-os-modais-ficaram-esquisitos\" style=\"position:relative;\"><a href=\"#ai-os-modais-ficaram-esquisitos\" aria-label=\"ai os modais ficaram esquisitos 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>Aí os modais ficaram esquisitos</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=\"Modal do Musclog no desktop renderizado corretamente dentro da moldura do celular\"\n        title=\"Modal do Musclog no desktop renderizado corretamente dentro da moldura do celular\"\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\">Modal do Musclog no desktop renderizado corretamente dentro da moldura do celular</figcaption>\n  </figure></p>\n<p>Claro que ficaram.</p>\n<p>Quando o app web mora dentro de um celular fake no desktop, o comportamento padrão de <code class=\"language-text\">Modal</code> no React Native Web começa a ficar ridículo bem rápido. Fazer portal direto pro <code class=\"language-text\">document.body</code> funciona bem quando o app é dono da página inteira. Funciona bem menos quando o app está visualmente recortado dentro de uma carcaça de celular e o modal decide do nada que agora pertence à janela inteira do navegador.</p>\n<p>Então eu adicionei um <code class=\"language-text\">WebModalShellProvider</code> lá em cima em <code class=\"language-text\">app/app/_layout.tsx</code>, com um host de overlay dentro do shell do celular:</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>Aí o <code class=\"language-text\">Modal.web.tsx</code> faz aquela coisa óbvia que só parece óbvia depois, alternando entre um portal dentro desse host e o modal normal do RN:</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>E o hook compartilhado do overlay sabe se ele deve se comportar como um modal de viewport inteira ou como um modal de viewport de celular fake:</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>Isso resolveu a parte visual. Aí os pointer events decidiram que também queriam atenção.</p>\n<p>No native, <code class=\"language-text\">pointerEvents=\"box-none\"</code> é normal. No HTML, isso vira <code class=\"language-text\">pointer-events: box-none</code>, que não é CSS de verdade, então o navegador fica livre pra improvisar. Então agora existe uma regra defensiva pro host do overlay:</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>Também existe um <code class=\"language-text\">useLayoutEffect</code> forçando a mesma coisa inline, porque a essa altura eu já não tinha mais interesse em descobrir quão elegante seria a solução correta.</p>\n<h2 id=\"eu-claramente-tenho-um-padrao\" style=\"position:relative;\"><a href=\"#eu-claramente-tenho-um-padrao\" aria-label=\"eu claramente tenho um padrao 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>Eu claramente tenho um padrão</h2>\n<p>Se você já lê este blog há algum tempo, nada disso deveria te surpreender.</p>\n<p>Eu já <a href=\"/pt-br/blog/coding/eu-criei-um-jogo-para-acessar-o-conteudo-do-meu-blog-com-phaser-e-react/\">transformei este blog num RPG top-down</a> porque o Konami Code merecia uma recompensa maior do que só um efeitinho de fundo.</p>\n<p>Depois eu <a href=\"/pt-br/blog/coding/meu-blog-tem-stories-agora-mas-nao-me-pergunte-por-que/\">coloquei Stories no blog</a> porque o app da minha operadora tinha Stories, e isso me irritou profundamente num nível suficiente pra eu transformar o problema em problema de todo mundo.</p>\n<p>Então sim, é claro que agora o meu app de fitness serve o site a partir do mesmo repo com Expo Router, e é claro que o app web no desktop roda dentro de uma foto gigante de um celular. Isso não é uma escalada surpreendente. É só continuidade.</p>\n<h2 id=\"por-que-eu-vou-manter-isso\" style=\"position:relative;\"><a href=\"#por-que-eu-vou-manter-isso\" aria-label=\"por que eu vou manter isso 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>Por que eu vou manter isso</h2>\n<p>Passando da piada, isso resolveu um imposto de manutenção bem real.</p>\n<p>As traduções são compartilhadas. Os design tokens são compartilhados. Os componentes são compartilhados. As páginas legais ficam do lado do produto que elas estão legalmente defendendo. Quando eu mudo texto, screenshot ou posicionamento de feature, eu não preciso lembrar qual repo guarda a versão pública e comportada do mesmo app.</p>\n<p>O deploy também ficou melhor. Um build. Um export. Um artefato. O site e o app web não têm como sair de sincronia a não ser que eu faça questão de separá-los, o que fica bem mais difícil quando eles literalmente são publicados juntos.</p>\n<p>E quando eu lancei o <a href=\"/pt-br/blog/coding/musclog-redesign-acompanhamento-nutricional-e-por-que-a-assinatura-do-seu-app-de-fitness-e-uma-enganacao/\">redesign do Musclog</a>, o site basicamente veio junto no embalo. Mesmo spacing, mesmas cores, screenshots atualizadas, mesmo PR. Exatamente o tipo de vitória chata de manutenção que faz uma decisão de arquitetura meio estranha parecer inteligente seis meses depois.</p>\n<h2 id=\"conclusao\" style=\"position:relative;\"><a href=\"#conclusao\" aria-label=\"conclusao 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>Conclusão</h2>\n<p>Eu poderia ter mantido o site num repo separado em Next.js como uma pessoa emocionalmente mais estável? Poderia.</p>\n<p>Eu ia continuar fazendo isso depois que o Expo Router deixou esse caminho aberto? Não.</p>\n<p>A parte engraçada é que o roteamento foi a parte sem graça. O Expo Router resolveu isso muito bem. O trabalho irritante estava em todas as mentirinhas de navegador ao redor: bloqueio de rota antes da hidratação, i18n em HTML cru, sincronização de assets, modais que sabiam em que shell estavam, esquisitice com pointer-events, e convencer o Chrome do desktop a acreditar temporariamente que era um celular.</p>\n<p>Ainda assim, valeu muito a pena.</p>\n<p>Se quiser fuçar o código, o <a href=\"https://github.com/blopa/musclog-app\" target=\"_blank\" rel=\"noreferrer\">Musclog é open-source no GitHub</a>. Se quiser usar, tá em <a href=\"https://musclog.app\" target=\"_blank\" rel=\"noreferrer\">musclog.app</a>. O site mora dentro do repo do app agora.</p>\n<p>Agora mora todo mundo aqui.</p>","fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI2LTA0LTI1VDAwOjAwOjAwLjAwMFo=","slug":"/2026/2026-04-25-site-ou-app-sim-o-guia-gambiarrístico-para-sites-feitos-em-expo-router.pt-br/","path":"/blog/coding/site-ou-app-sim-o-guia-gambiarrístico-para-sites-feitos-em-expo-router/","locale":"pt-br"},"readingTime":{"minutes":11.475},"frontmatter":{"path":"site-ou-app-sim-o-guia-gambiarrístico-para-sites-feitos-em-expo-router","allowComments":true,"title":"Site ou App? Sim. O guia gambiarrístico para sites feitos em Expo Router","date":"2026-04-25T00:00:00.000Z","categories":["coding"],"tags":["expo","expo router","react native","musclog","website","monorepo","javascript","typescript"],"hideExcerpt":false,"subtitle":"O roteamento foi fácil. O celular fake no navegador foi a parte amaldiçoada."}},"categoryImage":{"childImageSharp":{"original":{"width":1920,"height":1080,"src":"/static/categories_coding-aa17d098a6a0d8e0a764a345865f5ffb.jpg"}}}},"pageContext":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI2LTA0LTI1VDAwOjAwOjAwLjAwMFo=","relatedPosts":[{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI2LTAzLTIyVDAwOjAwOjAwLjAwMFo=","slug":"/2026/2026-03-22-musclog-redesign-acompanhamento-nutricional-e-por-que-a-assinatura-do-seu-app-de-fitness-e-uma-enganacao.pt-br/","path":"/blog/coding/musclog-redesign-acompanhamento-nutricional-e-por-que-a-assinatura-do-seu-app-de-fitness-e-uma-enganacao/","locale":"pt-br"},"frontmatter":{"tags":["react","react native","expo","android","fitness","musclog","nutrition","usda","open food facts","stitch","design","offline","watermelondb","nativewind"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2026-03-22T00:00:00.000Z","id":null,"path":"musclog-redesign-acompanhamento-nutricional-e-por-que-a-assinatura-do-seu-app-de-fitness-e-uma-enganacao","show":true,"title":"Musclog: Redesign do acompanhamento nutricional e por que a assinatura do seu app de fitness é uma enganação","hideExcerpt":false,"subtitle":"Nova interface, bancos de dados de alimentos reais e um desabafo sobre assinaturas de apps de fitness que estou guardando há dois anos"}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI0LTA5LTIxVDAwOjAwOjAwLjAwMFo=","slug":"/2024/2024-09-21-musclog-aproveitando-minha-experiencia-com-reactjs-para-criar-um-app-em-react-native.pt-br/","path":"/blog/coding/musclog-aproveitando-minha-experiencia-com-reactjs-para-criar-um-app-em-react-native/","locale":"pt-br"},"frontmatter":{"tags":["react","react native","expo","android","workout","tracker","logger","musclog"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2024-09-21T00:00:00.000Z","id":null,"path":"musclog-aproveitando-minha-experiencia-com-reactjs-para-criar-um-app-em-react-native","show":true,"title":"Musclog: Aproveitando minha experiência com React.js para criar um App em React Native","hideExcerpt":false,"subtitle":"De Bodybuilding para Codebuilding: Criando um aplicativo de fitness completo usando React Native e Expo"}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI2LTAyLTEyVDAwOjAwOjAwLjAwMFo=","slug":"/2026/2026-02-12-por-que-a-regra-das-7700-calorias-e-falha-e-como-eu-a-corrigi-no-meu-app.pt-br/","path":"/blog/coding/por-que-a-regra-das-7700-calorias-e-falha-e-como-eu-a-corrigi-no-meu-app/","locale":"pt-br"},"frontmatter":{"tags":["typescript","musclog","algorithms","fitness","nutrition","bodybuilding"],"categories":["coding"],"allowComments":true,"publishOnMedium":true,"cover":null,"date":"2026-02-12T00:00:00.000Z","id":null,"path":"por-que-a-regra-das-7700-calorias-e-falha-e-como-eu-a-corrigi-no-meu-app","show":true,"title":"Por que a regra das 7700 calorias é falha (e como eu a corrigi no meu app)","hideExcerpt":false,"subtitle":"Spoiler: seu corpo não leu o manual"}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI0LTEwLTI1VDAwOjAwOjAwLjAwMFo=","slug":"/2024/2024-10-25-compartilhando-cookies-encriptados-do-laravel-com-next-js.pt-br/","path":"/blog/coding/compartilhando-cookies-encriptados-do-laravel-com-next-js/","locale":"pt-br"},"frontmatter":{"tags":["laravel","next.js","cookies","encriptação","sincronização","desenvolvimento web","php","javascript"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2024-10-25T00:00:00.000Z","id":null,"path":"compartilhando-cookies-encriptados-do-laravel-com-next-js","show":true,"title":"Compartilhando cookies encriptados do Laravel com Next.js","hideExcerpt":false,"subtitle":"Conectando Laravel e Next.js: Como sincronizamos cookies encriptados entre dois frameworks diferentes."}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI0LTA2LTE2VDAwOjAwOjAwLjAwMFo=","slug":"/2024/2024-06-16-calculando-minhas-horas-de-trabalho-em-projetos-pessoais-usando-nodejs.pt-br/","path":"/blog/coding/calculando-minhas-horas-de-trabalho-em-projetos-pessoais-usando-nodejs/","locale":"pt-br"},"frontmatter":{"tags":["nodejs","git","foss","javascript","simple-git","math"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2024-06-16T00:00:00.000Z","id":null,"path":"calculando-minhas-horas-de-trabalho-em-projetos-pessoais-usando-nodejs","show":true,"title":"Calculando minhas horas de trabalho em projetos pessoais usando Node.js","hideExcerpt":false,"subtitle":"Descobrindo da pior maneira que algumas informações não deveriam ser conhecidas"}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDIzLTEyLTIwVDAwOjAwOjAwLjAwMFo=","slug":"/2023/2023-12-20-como-automatizar-a-criacao-de-instagram-reels-usando-gpt-da-openai.pt-br/","path":"/blog/coding/como-automatizar-a-criacao-de-instagram-reels-usando-gpt-da-openai/","locale":"pt-br"},"frontmatter":{"tags":["openai","gpt","javascript","node","nodejs","instagram"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2023-12-20T00:00:00.000Z","id":null,"path":"como-automatizar-a-criacao-de-instagram-reels-usando-gpt-da-openai","show":true,"title":"Como automatizar a criação de Instagram Reels usando GPT da OpenAI","hideExcerpt":false,"subtitle":"Vídeos tutoriais automatizados com GPT-3.5 e Node.js."}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDIzLTEwLTI1VDAwOjAwOjAwLjAwMFo=","slug":"/2023/2023-10-25-como-criar-thumbnails-para-seus-videos-automaticamente-com-nodejs.pt-br/","path":"/blog/coding/como-criar-thumbnails-para-seus-videos-automaticamente-com-nodejs/","locale":"pt-br"},"frontmatter":{"tags":["instagram","miniatura","node","nodejs","javascript","canvas","imagem","vídeo"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2023-10-25T00:00:00.000Z","id":null,"path":"como-criar-thumbnails-para-seus-videos-automaticamente-com-nodejs","show":true,"title":"Como criar thumbnails para seus vídeos automaticamente com Node.js","hideExcerpt":false,"subtitle":"A melhor forma de criar thumbnails para seus vídeos no Instagram"}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDIzLTEwLTE0VDAwOjAwOjAwLjAwMFo=","slug":"/2023/2023-10-14-publicando-reels-no-instagram-usando-nodejs-e-a-arte-de-superar-limitacoes.pt-br/","path":"/blog/coding/publicando-reels-no-instagram-usando-nodejs-e-a-arte-de-superar-limitacoes/","locale":"pt-br"},"frontmatter":{"tags":["instagram","node","nodejs","javascript","axios","ngrok","ffmpeg","automação"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2023-10-14T00:00:00.000Z","id":null,"path":"publicando-reels-no-instagram-usando-nodejs-e-a-arte-de-superar-limitacoes","show":true,"title":"Publicando Reels no Instagram usando Node.js e a arte de superar limitações","hideExcerpt":false,"subtitle":"Afinal, por que gastar 30 segundos fazendo algo manualmente quando você pode gastar 30 horas automatizando-o?"}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDIzLTA5LTA0VDAwOjAwOjAwLjAwMFo=","slug":"/2023/2023-09-04-meu-blog-tem-stories-agora-mas-nao-me-pergunte-por-que.pt-br/","path":"/blog/coding/meu-blog-tem-stories-agora-mas-nao-me-pergunte-por-que/","locale":"pt-br"},"frontmatter":{"tags":["instagram","instagram stories","invenções desnecessárias","gatsby","react","javascript"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2023-09-04T00:00:00.000Z","id":null,"path":"meu-blog-tem-stories-agora-mas-nao-me-pergunte-por-que","show":true,"title":"Meu blog tem Stories agora, mas não me pergunte por quê","hideExcerpt":false,"subtitle":"Quem disse que tédio não é produtivo?"}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDIzLTA4LTAyVDAwOjAwOjAwLjAwMFo=","slug":"/2023/2023-08-02-usando-google-fitness-api-para-calcular-meu-tdee-e-mais.pt-br/","path":"/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/","locale":"pt-br"},"frontmatter":{"tags":["google cloud","fitness api","google fit","nodejs","node","javascript"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2023-08-02T00:00:00.000Z","id":null,"path":"usando-google-fitness-api-para-calcular-meu-tdee-e-mais","show":true,"title":"Usando Google Fitness API para calcular meu TDEE e mais","hideExcerpt":false,"subtitle":"De calorias para código: Integrando Google Fitness API para insights personalizados de saúde"}}],"alternativeHtml":"<p>Uma das side quests mais chatas de publicar um app na Google Play Store é que o Google às vezes decide que você também virou dono de site.</p>\n<p>Lá em 2024, quando eu <a href=\"/pt-br/blog/coding/musclog-aproveitando-minha-experiencia-com-reactjs-para-criar-um-app-em-react-native/\">escrevi pela primeira vez sobre o Musclog</a>, o site existia basicamente porque o Google queria que ele existisse. Eu precisava da política de privacidade, das páginas públicas, do kit básico do desenvolvedor de app respeitável, então subi rapidinho um site pequeno em Next.js, consegui aprovar o app, e segui com a minha vida.</p>\n<p>Funcionou perfeitamente até o dia em que eu precisei mexer no site de novo.</p>\n<p>Screenshots novas? Outro repo. Texto de feature nova? Outro repo. Atualização de página legal? Outro repo. Ajustezinho de tradução? Outro repo.</p>\n<p>Nada disso era difícil de verdade, o que quase piorava a situação. Era só chato o suficiente pra continuar me lembrando que eu tinha dividido um produto em duas bases de código por um motivo que eu já não respeitava mais.</p>\n<p>Então agora eu não faço mais isso.</p>\n<p>O site do Musclog mora dentro do repo do app. Mesmo projeto com Expo Router. Mesmo deploy. <a href=\"https://musclog.app/\" target=\"_blank\" rel=\"noreferrer\">musclog.app</a> é o site público, <a href=\"https://musclog.app/app\" target=\"_blank\" rel=\"noreferrer\">musclog.app/app</a> é o app web de verdade, e Android + iOS continuam saindo da mesma base de código.</p>\n<p>Boa prática? Questionável. Conveniente? Demais.</p>\n<p></p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span class=\"gatsby-resp-image-wrapper\" style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \">\n      <span class=\"gatsby-resp-image-background-image\" 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;\"></span>\n  <img class=\"gatsby-resp-image-image\" alt=\"Site público do Musclog renderizado pela mesma base de código com Expo Router do app\" title=\"Site público do Musclog renderizado pela mesma base de código com Expo Router do app\" src=\"/static/f71a459f3034e47544de2f3ce309dd61/42a19/musclog-shared-router-website-home-1.png\" 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\" sizes=\"(max-width: 1024px) 100vw, 1024px\" style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\" loading=\"lazy\" decoding=\"async\">\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Site público do Musclog renderizado pela mesma base de código com Expo Router do app</figcaption>\n  </figure><p></p>\n<h2 id=\"ok-mas-por-que\" style=\"position:relative;\"><a href=\"#ok-mas-por-que\" aria-label=\"ok mas por que 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 mas por quê?</h2>\n<p>Basicamente porque o Expo Router arrancou a minha última desculpa.</p>\n<p>O Musclog já usava <a href=\"https://expo.github.io/router/docs\" target=\"_blank\" rel=\"noreferrer\">Expo Router</a>, então o app já estava organizado em rotas baseadas em arquivos dentro da pasta <code class=\"language-text\">app/</code>. Aí eu percebi que podia simplesmente criar um grupo de rotas chamado <code class=\"language-text\">(website)</code>, mover as páginas públicas pra lá, deixar o app de verdade em <code class=\"language-text\">app/app/*</code>, e parar de fingir que eu estava lidando com dois produtos diferentes.</p>\n<p>O repo separado tinha virado uma daquelas decisões de arquitetura que parecem limpas na teoria e depois ficam te cobrando imposto de manutenção pra sempre:</p>\n<ul>\n<li>dois PRs pra uma mudança num produto</li>\n<li>dois deploys</li>\n<li>dois lugares pras traduções</li>\n<li>dois lugares pras páginas legais</li>\n<li>dois lugares pra esquecer alguma coisa</li>\n</ul>\n<p>Sem condições.</p>\n<h2 id=\"a-divisao\" style=\"position:relative;\"><a href=\"#a-divisao\" aria-label=\"a divisao 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>A divisão</h2>\n<p>A parte engraçada é que o roteamento em si acabou sendo a parte menos amaldiçoada dessa história:</p>\n<ul>\n<li><code class=\"language-text\">app/app/*</code> é o Musclog de verdade. Treinos, nutrição, coach com IA, tudo.</li>\n<li><code class=\"language-text\">app/(website)/*</code> é o site público. Landing page, política de privacidade, termos, contato, calculadora.</li>\n</ul>\n<p>A rota raiz só checa a plataforma e te joga pra onde você pertence:</p>\n<div class=\"copy-code-block\"><button tabindex=\"0\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token comment\">// app/index.tsx</span>\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>\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>\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>\n\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>\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>\n\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\">=&gt;</span> <span class=\"token punctuation\">{</span>\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>\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>\n    <span class=\"token punctuation\">}</span>\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>\n\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>\n    <span class=\"token keyword\">return</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\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\">/&gt;</span></span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>É isso. No web, <code class=\"language-text\">/</code> vira o site. No native, <code class=\"language-text\">/</code> vira o app.</p>\n<p>O site também ganha o próprio layout mais leve:</p>\n<div class=\"copy-code-block\"><button tabindex=\"1\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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>\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>\n\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>\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>\n\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>\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\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\">&gt;</span></span><span class=\"token plain-text\">\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\">&gt;</span></span><span class=\"token plain-text\">\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\">/&gt;</span></span><span class=\"token plain-text\">\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\">&gt;</span></span><span class=\"token plain-text\">\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\">&gt;</span></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>Isso importa porque o layout do app de verdade já tinha crescido e virado um layout de app mesmo. Boot de banco, migrações, React Query, safe area, gestos, modais, snackbars, câmera, contexto do coach, todas aquelas mentiras clássicas de “é só um projetinho paralelo”. O site não precisa de nada disso só pra explicar o que o app faz.</p>\n<p>E se algum usuário nativo cair numa rota exclusiva do site, a correção é maravilhosamente direta:</p>\n<div class=\"copy-code-block\"><button tabindex=\"2\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token comment\">// app/(website)/home.tsx</span>\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>\n\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>\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\">/&gt;</span></span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>Pronto. Volta pros macros.</p>\n<p></p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span class=\"gatsby-resp-image-wrapper\" style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \">\n      <span class=\"gatsby-resp-image-background-image\" 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;\"></span>\n  <img class=\"gatsby-resp-image-image\" alt=\"Site público do Musclog renderizado pela mesma base de código com Expo Router do app\" title=\"Site público do Musclog renderizado pela mesma base de código com Expo Router do app\" src=\"/static/c50b08ecc21b0e02ed7774578063a5f6/42a19/musclog-shared-router-website-home-2.png\" 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\" sizes=\"(max-width: 1024px) 100vw, 1024px\" style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\" loading=\"lazy\" decoding=\"async\">\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Site público do Musclog renderizado pela mesma base de código com Expo Router do app</figcaption>\n  </figure><p></p>\n<h2 id=\"a-palhacada-do-celular-no-desktop\" style=\"position:relative;\"><a href=\"#a-palhacada-do-celular-no-desktop\" aria-label=\"a palhacada do celular no desktop 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>A palhaçada do celular no desktop</h2>\n<p>Foi aqui que eu parei de me comportar como uma pessoa normal.</p>\n<p>Eu queria que <a href=\"https://musclog.app/app/\" target=\"_blank\" rel=\"noreferrer\">musclog.app/app</a>, no desktop, mostrasse o app funcionando dentro de uma moldura de celular. Um pouco porque fica bonito. Um pouco porque funciona como preview do produto. Mas principalmente porque, depois que a ideia entrou na minha cabeça, qualquer opção menos ridícula começou a parecer errada.</p>\n<p>O detalhe chato era que <code class=\"language-text\">/app</code> precisava parecer um celular, mas <code class=\"language-text\">/home</code> definitivamente não. Então só o roteamento não bastava. Eu precisava que o shell bruto de HTML soubesse, antes da hidratação, se o navegador deveria renderizar a gambiarra do celular fake ou não.</p>\n<p>Essa lógica mora em <code class=\"language-text\">app/+html.tsx</code>, dentro de um script no <code class=\"language-text\">&lt;head&gt;</code> do documento:</p>\n<div class=\"copy-code-block\"><button tabindex=\"3\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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>\n  <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\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>\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>\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>\n\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>\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>\n      <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token punctuation\">{</span>\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>\n      <span class=\"token punctuation\">}</span>\n    <span class=\"token punctuation\">}</span>\n\n    <span class=\"token function\">update</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\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>\n\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>\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>\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>\n      <span class=\"token function\">update</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\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>\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>\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>\n      <span class=\"token function\">update</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\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>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>A parte importante é a gambiarra em <code class=\"language-text\">pushState</code> / <code class=\"language-text\">replaceState</code>. A primeira renderização foi tranquila. A navegação client-side é que era a parte chata. Sem isso, dava pra sair de <code class=\"language-text\">/app</code> e continuar com o shell errado ali, como se o Chrome tivesse esquecido em que página estava.</p>\n<p>O shell HTML em si é basicamente o painel da landing, o app roteado, e a moldura do celular:</p>\n<div class=\"copy-code-block\"><button tabindex=\"4\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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\">&gt;</span></span><span class=\"token plain-text\">\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\">&gt;</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\">&gt;</span></span><span class=\"token plain-text\">\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\">/&gt;</span></span><span class=\"token plain-text\">\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\">&gt;</span></span><span class=\"token plain-text\">\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\">&gt;</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\">&gt;</span></span><span class=\"token plain-text\">\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>img</span>\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>\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>\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>\n      <span class=\"token attr-name\">aria-hidden</span>\n    <span class=\"token punctuation\">/&gt;</span></span><span class=\"token plain-text\">\n  </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">&gt;</span></span><span class=\"token plain-text\">\n</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>body</span><span class=\"token punctuation\">&gt;</span></span></code></pre></div></div>\n<p>Aí o CSS comete o crime:</p>\n<div class=\"copy-code-block\"><button tabindex=\"5\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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>\n  <span class=\"token selector\">.expo-web-root</span> <span class=\"token punctuation\">{</span>\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>\n    <span class=\"token property\">aspect-ratio</span><span class=\"token punctuation\">:</span> 1438 / 2976<span class=\"token punctuation\">;</span>\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>\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>\n    <span class=\"token property\">overflow</span><span class=\"token punctuation\">:</span> hidden<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token selector\">.expo-web-app-shell</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token property\">position</span><span class=\"token punctuation\">:</span> absolute<span class=\"token punctuation\">;</span>\n    <span class=\"token property\">left</span><span class=\"token punctuation\">:</span> 7.4409%<span class=\"token punctuation\">;</span>\n    <span class=\"token property\">top</span><span class=\"token punctuation\">:</span> 2.9906%<span class=\"token punctuation\">;</span>\n    <span class=\"token property\">right</span><span class=\"token punctuation\">:</span> 6.3282%<span class=\"token punctuation\">;</span>\n    <span class=\"token property\">bottom</span><span class=\"token punctuation\">:</span> 3.125%<span class=\"token punctuation\">;</span>\n    <span class=\"token property\">zoom</span><span class=\"token punctuation\">:</span> 0.85<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>Sim, eu medi a área transparente da tela dentro do PNG até o app encaixar direito. Não tem geometria elegante nenhuma por trás dessas porcentagens. Era só eu, uma moldura de celular, e muito empurra-pra-lá até o app parar de parecer torto.</p>\n<p>Nas rotas que não são <code class=\"language-text\">/app</code>, o espetáculo inteiro é desligado:</p>\n<div class=\"copy-code-block\"><button tabindex=\"6\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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,\n.hide-desktop-wrapper .expo-web-phone-frame</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">display</span><span class=\"token punctuation\">:</span> none <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token selector\">.hide-desktop-wrapper .expo-web-app-shell</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">position</span><span class=\"token punctuation\">:</span> static <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\n  <span class=\"token property\">width</span><span class=\"token punctuation\">:</span> 100% <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\n  <span class=\"token property\">height</span><span class=\"token punctuation\">:</span> auto <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\n  <span class=\"token property\">zoom</span><span class=\"token punctuation\">:</span> 1 <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\n  <span class=\"token property\">overflow</span><span class=\"token punctuation\">:</span> visible <span class=\"token important\">!important</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>Sim, usa <code class=\"language-text\">!important</code>. Isso é CSS de pré-hidratação cujo trabalho inteiro é manter a mentira de pé. Não estamos fazendo arquitetura refinada nessa camada.</p>\n<p></p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span class=\"gatsby-resp-image-wrapper\" style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \">\n      <span class=\"gatsby-resp-image-background-image\" 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;\"></span>\n  <img class=\"gatsby-resp-image-image\" alt=\"App web do Musclog no desktop renderizado dentro de uma moldura de celular ao lado da landing\" title=\"App web do Musclog no desktop renderizado dentro de uma moldura de celular ao lado da landing\" src=\"/static/49361b3283ae34ef30a950b13c7f5594/42a19/musclog-desktop-phone-frame-wrapper.png\" 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\" sizes=\"(max-width: 1024px) 100vw, 1024px\" style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\" loading=\"lazy\" decoding=\"async\">\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">App web do Musclog no desktop renderizado dentro de uma moldura de celular ao lado da landing</figcaption>\n  </figure><p></p>\n<h2 id=\"tres-pequenos-crimes\" style=\"position:relative;\"><a href=\"#tres-pequenos-crimes\" aria-label=\"tres pequenos 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>Três pequenos crimes</h2>\n<p><img src=\"/377ac4354ce553fccf98c5d8d1017e50/straight-to-jail.gif\" alt=\"Direto pra cadeia. Na hora.\"></p>\n<p>Depois que o shell funcionou, os incômodos menores começaram a aparecer como se tivessem marcado horário.</p>\n<h3 id=\"1-o-painel-da-landing-precisava-de-i18n-antes-da-hidratacao\" style=\"position:relative;\"><a href=\"#1-o-painel-da-landing-precisava-de-i18n-antes-da-hidratacao\" aria-label=\"1 o painel da landing precisava de i18n antes da hidratacao 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. O painel da landing precisava de i18n antes da hidratação</h3>\n<p>O texto do lado do celular mora em HTML cru, o que significa que React e i18n ainda estão dormindo quando a página renderiza pela primeira vez. Então, se eu quisesse que a página em português não desse um flash de inglês antes, eu tinha que remendar isso manualmente a partir do <code class=\"language-text\">localStorage</code>:</p>\n<div class=\"copy-code-block\"><button tabindex=\"7\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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>\n  <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\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>\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>\n\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>\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>\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>\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>\n      <span class=\"token punctuation\">}</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\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>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>Não é elegante. Também evita jogar um flashbang de texto em inglês numa página em português, então eu vou manter.</p>\n<h3 id=\"2-assets-em-html-cru-nao-recebem-a-ajuda-de-sempre-do-expo\" style=\"position:relative;\"><a href=\"#2-assets-em-html-cru-nao-recebem-a-ajuda-de-sempre-do-expo\" aria-label=\"2 assets em html cru nao recebem a ajuda de sempre do expo 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. Assets em HTML cru não recebem a ajuda de sempre do Expo</h3>\n<p>O PNG da moldura do celular e o QR code também são referenciados a partir de HTML cru, então aquela conveniência normal de assets do Expo não me ajudava muito ali. Resultado: eu precisei de um helper pra base URL:</p>\n<div class=\"copy-code-block\"><button tabindex=\"8\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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>\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>\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>\n    <span class=\"token keyword\">return</span> path<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\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>\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>\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>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>E como esses arquivos vivem fora do pipeline normal de assets do React Native, eu copio tudo pra <code class=\"language-text\">public/</code> antes de rodar dev e export. Esquece esse passo uma vez e o site imediatamente te lembra quem manda.</p>\n<h3 id=\"3-o-expo-export-as-vezes-precisava-de-apoio-emocional\" style=\"position:relative;\"><a href=\"#3-o-expo-export-as-vezes-precisava-de-apoio-emocional\" aria-label=\"3 o expo export as vezes precisava de apoio emocional 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. O <code class=\"language-text\">expo export</code> às vezes precisava de apoio emocional</h3>\n<p>Em alguns ambientes, <code class=\"language-text\">expo export --platform web</code> terminava com sucesso e depois só… continuava vivo sem motivo nenhum. Pasta <code class=\"language-text\">dist</code> lá. Arquivos gerados. Processo espiritualmente concluído, tecnicamente ainda pendurado.</p>\n<p>Então agora existe um script wrapper:</p>\n<div class=\"copy-code-block\"><button tabindex=\"9\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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>\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>\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>\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\">=&gt;</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>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>Não é glamouroso… mas resolve.</p>\n<h2 id=\"ai-os-modais-ficaram-esquisitos\" style=\"position:relative;\"><a href=\"#ai-os-modais-ficaram-esquisitos\" aria-label=\"ai os modais ficaram esquisitos 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>Aí os modais ficaram esquisitos</h2>\n<p></p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span class=\"gatsby-resp-image-wrapper\" style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \">\n      <span class=\"gatsby-resp-image-background-image\" 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;\"></span>\n  <img class=\"gatsby-resp-image-image\" alt=\"Modal do Musclog no desktop renderizado corretamente dentro da moldura do celular\" title=\"Modal do Musclog no desktop renderizado corretamente dentro da moldura do celular\" src=\"/static/a2af4c3fcf36eee559fddfb48a5143d1/42a19/musclog-desktop-modal-inside-phone-frame.png\" 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\" sizes=\"(max-width: 1024px) 100vw, 1024px\" style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\" loading=\"lazy\" decoding=\"async\">\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Modal do Musclog no desktop renderizado corretamente dentro da moldura do celular</figcaption>\n  </figure><p></p>\n<p>Claro que ficaram.</p>\n<p>Quando o app web mora dentro de um celular fake no desktop, o comportamento padrão de <code class=\"language-text\">Modal</code> no React Native Web começa a ficar ridículo bem rápido. Fazer portal direto pro <code class=\"language-text\">document.body</code> funciona bem quando o app é dono da página inteira. Funciona bem menos quando o app está visualmente recortado dentro de uma carcaça de celular e o modal decide do nada que agora pertence à janela inteira do navegador.</p>\n<p>Então eu adicionei um <code class=\"language-text\">WebModalShellProvider</code> lá em cima em <code class=\"language-text\">app/app/_layout.tsx</code>, com um host de overlay dentro do shell do celular:</p>\n<div class=\"copy-code-block\"><button tabindex=\"10\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token comment\">// context/WebModalShellContext.web.tsx</span>\n<span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\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\">&gt;</span></span><span class=\"token plain-text\">\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\">&gt;</span></span><span class=\"token plain-text\">\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\">&gt;</span></span><span class=\"token plain-text\">\n        </span><span class=\"token punctuation\">{</span>children<span class=\"token punctuation\">}</span><span class=\"token plain-text\">\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\">&gt;</span></span><span class=\"token plain-text\">\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">View</span></span>\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>\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>\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>\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>\n          position<span class=\"token operator\">:</span> <span class=\"token string\">'absolute'</span><span class=\"token punctuation\">,</span>\n          left<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n          right<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n          top<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n          bottom<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n          zIndex<span class=\"token operator\">:</span> <span class=\"token number\">1_000_000</span><span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">}</span><span class=\"token punctuation\">}</span></span>\n      <span class=\"token punctuation\">/&gt;</span></span><span class=\"token plain-text\">\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\">&gt;</span></span><span class=\"token plain-text\">\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\">&gt;</span></span>\n<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div></div>\n<p>Aí o <code class=\"language-text\">Modal.web.tsx</code> faz aquela coisa óbvia que só parece óbvia depois, alternando entre um portal dentro desse host e o modal normal do RN:</p>\n<div class=\"copy-code-block\"><button tabindex=\"11\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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>\n\n<span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>useShellPortal<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> <span class=\"token function\">createPortal</span><span class=\"token punctuation\">(</span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">View</span></span>\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>\n        position<span class=\"token operator\">:</span> <span class=\"token string\">'absolute'</span><span class=\"token punctuation\">,</span>\n        left<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n        right<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n        top<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n        bottom<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n        width<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span>\n        height<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span>\n      <span class=\"token punctuation\">}</span><span class=\"token punctuation\">}</span></span>\n    <span class=\"token punctuation\">&gt;</span></span><span class=\"token plain-text\">\n      </span><span class=\"token punctuation\">{</span>children<span class=\"token punctuation\">}</span><span class=\"token plain-text\">\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\">&gt;</span></span><span class=\"token punctuation\">,</span>\n    hostElement\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\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\">&gt;</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\">&gt;</span></span><span class=\"token punctuation\">;</span></code></pre></div></div>\n<p>E o hook compartilhado do overlay sabe se ele deve se comportar como um modal de viewport inteira ou como um modal de viewport de celular fake:</p>\n<div class=\"copy-code-block\"><button tabindex=\"12\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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>\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>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>isDesktopFrame<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span>\n      position<span class=\"token operator\">:</span> <span class=\"token string\">'absolute'</span><span class=\"token punctuation\">,</span>\n      top<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n      left<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n      right<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n      bottom<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n      width<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span>\n      height<span class=\"token operator\">:</span> <span class=\"token string\">'100%'</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span>\n    position<span class=\"token operator\">:</span> <span class=\"token string\">'fixed'</span><span class=\"token punctuation\">,</span>\n    top<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n    left<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n    right<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n    bottom<span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n    width<span class=\"token operator\">:</span> <span class=\"token string\">'100vw'</span><span class=\"token punctuation\">,</span>\n    height<span class=\"token operator\">:</span> <span class=\"token string\">'100dvh'</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>Isso resolveu a parte visual. Aí os pointer events decidiram que também queriam atenção.</p>\n<p>No native, <code class=\"language-text\">pointerEvents=\"box-none\"</code> é normal. No HTML, isso vira <code class=\"language-text\">pointer-events: box-none</code>, que não é CSS de verdade, então o navegador fica livre pra improvisar. Então agora existe uma regra defensiva pro host do overlay:</p>\n<div class=\"copy-code-block\"><button tabindex=\"13\" type=\"button\" class=\"MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary copy-code-button\"><span class=\"MuiButton-label\">Copiar</span></button><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>\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>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token selector\">.expo-web-app-shell #expo-web-modal-shell-host *</span> <span class=\"token punctuation\">{</span>\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>\n<span class=\"token punctuation\">}</span></code></pre></div></div>\n<p>Também existe um <code class=\"language-text\">useLayoutEffect</code> forçando a mesma coisa inline, porque a essa altura eu já não tinha mais interesse em descobrir quão elegante seria a solução correta.</p>\n<h2 id=\"eu-claramente-tenho-um-padrao\" style=\"position:relative;\"><a href=\"#eu-claramente-tenho-um-padrao\" aria-label=\"eu claramente tenho um padrao 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>Eu claramente tenho um padrão</h2>\n<p>Se você já lê este blog há algum tempo, nada disso deveria te surpreender.</p>\n<p>Eu já <a href=\"/pt-br/blog/coding/eu-criei-um-jogo-para-acessar-o-conteudo-do-meu-blog-com-phaser-e-react/\">transformei este blog num RPG top-down</a> porque o Konami Code merecia uma recompensa maior do que só um efeitinho de fundo.</p>\n<p>Depois eu <a href=\"/pt-br/blog/coding/meu-blog-tem-stories-agora-mas-nao-me-pergunte-por-que/\">coloquei Stories no blog</a> porque o app da minha operadora tinha Stories, e isso me irritou profundamente num nível suficiente pra eu transformar o problema em problema de todo mundo.</p>\n<p>Então sim, é claro que agora o meu app de fitness serve o site a partir do mesmo repo com Expo Router, e é claro que o app web no desktop roda dentro de uma foto gigante de um celular. Isso não é uma escalada surpreendente. É só continuidade.</p>\n<h2 id=\"por-que-eu-vou-manter-isso\" style=\"position:relative;\"><a href=\"#por-que-eu-vou-manter-isso\" aria-label=\"por que eu vou manter isso 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>Por que eu vou manter isso</h2>\n<p>Passando da piada, isso resolveu um imposto de manutenção bem real.</p>\n<p>As traduções são compartilhadas. Os design tokens são compartilhados. Os componentes são compartilhados. As páginas legais ficam do lado do produto que elas estão legalmente defendendo. Quando eu mudo texto, screenshot ou posicionamento de feature, eu não preciso lembrar qual repo guarda a versão pública e comportada do mesmo app.</p>\n<p>O deploy também ficou melhor. Um build. Um export. Um artefato. O site e o app web não têm como sair de sincronia a não ser que eu faça questão de separá-los, o que fica bem mais difícil quando eles literalmente são publicados juntos.</p>\n<p>E quando eu lancei o <a href=\"/pt-br/blog/coding/musclog-redesign-acompanhamento-nutricional-e-por-que-a-assinatura-do-seu-app-de-fitness-e-uma-enganacao/\">redesign do Musclog</a>, o site basicamente veio junto no embalo. Mesmo spacing, mesmas cores, screenshots atualizadas, mesmo PR. Exatamente o tipo de vitória chata de manutenção que faz uma decisão de arquitetura meio estranha parecer inteligente seis meses depois.</p>\n<h2 id=\"conclusao\" style=\"position:relative;\"><a href=\"#conclusao\" aria-label=\"conclusao 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>Conclusão</h2>\n<p>Eu poderia ter mantido o site num repo separado em Next.js como uma pessoa emocionalmente mais estável? Poderia.</p>\n<p>Eu ia continuar fazendo isso depois que o Expo Router deixou esse caminho aberto? Não.</p>\n<p>A parte engraçada é que o roteamento foi a parte sem graça. O Expo Router resolveu isso muito bem. O trabalho irritante estava em todas as mentirinhas de navegador ao redor: bloqueio de rota antes da hidratação, i18n em HTML cru, sincronização de assets, modais que sabiam em que shell estavam, esquisitice com pointer-events, e convencer o Chrome do desktop a acreditar temporariamente que era um celular.</p>\n<p>Ainda assim, valeu muito a pena.</p>\n<p>Se quiser fuçar o código, o <a href=\"https://github.com/blopa/musclog-app\" target=\"_blank\" rel=\"noreferrer\">Musclog é open-source no GitHub</a>. Se quiser usar, tá em <a href=\"https://musclog.app\" target=\"_blank\" rel=\"noreferrer\">musclog.app</a>. O site mora dentro do repo do app agora.</p>\n<p>Agora mora todo mundo aqui.</p>","otherLanguagesUrl":["/en/blog/coding/website-or-app-yes-hacky-expo-router-guide/"],"rss":{"title":"Site ou App? Sim. O guia gambiarrístico para sites feitos em Expo Router","description":"Site ou App? Sim. O guia gambiarrístico para sites feitos em Expo Router","date":"2026-04-25T00:00:00.000Z"},"images":["/static/f71a459f3034e47544de2f3ce309dd61/42a19/musclog-shared-router-website-home-1.png","/static/c50b08ecc21b0e02ed7774578063a5f6/42a19/musclog-shared-router-website-home-2.png","/static/49361b3283ae34ef30a950b13c7f5594/42a19/musclog-desktop-phone-frame-wrapper.png","/377ac4354ce553fccf98c5d8d1017e50/straight-to-jail.gif","/static/a2af4c3fcf36eee559fddfb48a5143d1/42a19/musclog-desktop-modal-inside-phone-frame.png"],"videos":[],"comments":[],"googleFormData":{"fvv":1,"pageHistory":0,"fbzx":"2341154569390836157","action":"e/1FAIpQLSeST_0jBnLKkEkXGpyx9LWrvV2a1-1F5dr-AcA4wn0BSRyPCw","title":"blog_comment","description":null,"fields":[{"label":"name","description":null,"type":"SHORT_ANSWER","id":"1953327618","required":true},{"label":"email","description":null,"type":"SHORT_ANSWER","id":"1309141965","required":false},{"label":"twitter","description":null,"type":"SHORT_ANSWER","id":"740186305","required":false},{"label":"comment","description":null,"type":"LONG_ANSWER","id":"1663940054","required":true},{"label":"post_path","description":null,"type":"SHORT_ANSWER","id":"1852628638","required":true}],"fieldsOrder":{"740186305":2,"1309141965":1,"1663940054":3,"1852628638":4,"1953327618":0}},"pageType":"blogPost","categoryImage":"/categories_coding.jpg/","slug":"/2026/2026-04-25-site-ou-app-sim-o-guia-gambiarrístico-para-sites-feitos-em-expo-router.pt-br/","locale":"pt-br","title":"Site ou App? Sim. O guia gambiarrístico para sites feitos em Expo Router","previous":{"excerpt":"One of the annoying little side quests of shipping an app is…","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"},"rawMarkdownBody":"\r\nOne of the annoying little side quests of shipping an app is that Google occasionally forces you to become a website owner too.\r\n\r\nBack when I [first wrote about Musclog in 2024](/en/blog/coding/musclog-leveraging-my-reactjs-experience-to-build-a-react-native-app/), 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.\r\n\r\nThat worked right until I had to touch the website again.\r\n\r\nNew screenshots? Other repo. New feature copy? Other repo. Legal page update? Other repo. Translation tweak? Other repo.\r\n\r\nNone 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.\r\n\r\nSo now I don't.\r\n\r\nMusclog's website lives inside the app repo. Same Expo Router project. Same deploy. [musclog.app](https://musclog.app/) is the public site, [musclog.app/app](https://musclog.app/app) is the actual web app, and Android + iOS still come from the same codebase.\r\n\r\nBest practice? Questionable. Convenient? Extremely.\r\n\r\n![Musclog public website rendered from the same Expo Router codebase as the app](../../uploads/blog/2026/04/musclog-shared-router-website-home-1.png)\r\n\r\n## Ok but why?\r\n\r\nBasically because Expo Router removed my last excuse.\r\n\r\nMusclog already used [Expo Router](https://expo.github.io/router/docs), so the app was already organized as file-based routes inside `app/`. Then I realized I could just create a route group called `(website)`, move the public pages there, keep the actual app under `app/app/*`, and stop pretending these were two different products.\r\n\r\nThe separate repo had become one of those architecture decisions that sounds clean in theory and then quietly charges you maintenance tax forever:\r\n\r\n- two PRs for one product change\r\n- two deployments\r\n- two places for translations\r\n- two places for legal pages\r\n- two places to forget something\r\n\r\nNo thanks.\r\n\r\n## The split\r\n\r\nThe funny part is that the routing itself ended up being the least cursed part of the whole thing:\r\n\r\n- `app/app/*` is the actual Musclog app. Workouts, nutrition, AI coach, all of it.\r\n- `app/(website)/*` is the public website. Landing page, privacy policy, terms, contact page, calculator.\r\n\r\nThe root route just checks the platform and sends you where you belong:\r\n\r\n```tsx\r\n// app/index.tsx\r\nimport { Redirect, useRouter } from 'expo-router';\r\nimport { useEffect } from 'react';\r\nimport { Platform } from 'react-native';\r\n\r\nexport default function Index() {\r\n  const router = useRouter();\r\n\r\n  useEffect(() => {\r\n    if (Platform.OS === 'web') {\r\n      router.replace('/home');\r\n    }\r\n  }, [router]);\r\n\r\n  if (Platform.OS === 'web') {\r\n    return null;\r\n  }\r\n\r\n  return <Redirect href=\"/app\" />;\r\n}\r\n```\r\n\r\nThat's it. On web, `/` becomes the website. On native, `/` becomes the app.\r\n\r\nThe website also gets its own lighter layout:\r\n\r\n```tsx\r\n// app/(website)/_layout.web.tsx\r\nimport { Slot } from 'expo-router';\r\n\r\nimport { WebsiteChrome } from '@/components/website/WebsiteChrome';\r\nimport { WebsiteProviders } from '@/components/website/WebsiteProviders';\r\n\r\nexport default function WebsiteLayout() {\r\n  return (\r\n    <WebsiteProviders>\r\n      <WebsiteChrome>\r\n        <Slot />\r\n      </WebsiteChrome>\r\n    </WebsiteProviders>\r\n  );\r\n}\r\n```\r\n\r\nThat 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.\r\n\r\nAnd if a native user somehow lands on a website-only route, the fix is beautifully blunt:\r\n\r\n```tsx\r\n// app/(website)/home.tsx\r\nimport { Redirect } from 'expo-router';\r\n\r\nexport default function Home() {\r\n  return <Redirect href=\"/app\" />;\r\n}\r\n```\r\n\r\nDone. Back to your macros.\r\n\r\n![Musclog public website rendered from the same Expo Router codebase as the app](../../uploads/blog/2026/04/musclog-shared-router-website-home-2.png)\r\n\r\n## The desktop phone nonsense\r\n\r\nThis is where I stopped behaving like a normal person.\r\n\r\nI wanted [musclog.app/app](https://musclog.app/app/) 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.\r\n\r\nThe annoying detail was that `/app` should look like a phone, but `/home` 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.\r\n\r\nThat logic lives in `app/+html.tsx`, inside a script in the document `<head>`:\r\n\r\n```tsx\r\nfunction landingPanelGate(base: string) {\r\n  try {\r\n    function update() {\r\n      const raw = window.location.pathname;\r\n      const path = (base && raw.startsWith(base) ? raw.slice(base.length) : raw) || '/';\r\n\r\n      if (!path.startsWith('/app')) {\r\n        document.documentElement.classList.add('hide-desktop-wrapper');\r\n      } else {\r\n        document.documentElement.classList.remove('hide-desktop-wrapper');\r\n      }\r\n    }\r\n\r\n    update();\r\n    window.addEventListener('popstate', update);\r\n\r\n    const origPush = history.pushState.bind(history);\r\n    history.pushState = function (...args) {\r\n      origPush(...args);\r\n      update();\r\n    };\r\n\r\n    const origReplace = history.replaceState.bind(history);\r\n    history.replaceState = function (...args) {\r\n      origReplace(...args);\r\n      update();\r\n    };\r\n  } catch (_) {}\r\n}\r\n```\r\n\r\nThe important part is the `pushState` / `replaceState` monkey patch. First paint was easy. Client-side navigation was the annoying bit. Without this, you could leave `/app` and keep the wrong shell around like Chrome had forgotten what page it was on.\r\n\r\nThe HTML shell itself is basically the landing panel, the routed app, and the phone frame:\r\n\r\n```tsx\r\n<body className=\"expo-web-body\">\r\n  <div className=\"expo-web-landing\">...</div>\r\n  <script dangerouslySetInnerHTML={{ __html: LANDING_I18N_SCRIPT }} />\r\n  <div className=\"expo-web-root\">\r\n    <div className=\"expo-web-app-shell\">{children}</div>\r\n    <img\r\n      className=\"expo-web-phone-frame\"\r\n      src={withExpoBaseUrl(PHONE_FRAME_SRC)}\r\n      alt=\"\"\r\n      aria-hidden\r\n    />\r\n  </div>\r\n</body>\r\n```\r\n\r\nThen CSS commits the crime:\r\n\r\n```css\r\n@media (min-width: 1024px) {\r\n  .expo-web-root {\r\n    --frame-h: min(100dvh, max(min(360px, 100dvh), 85dvh));\r\n    aspect-ratio: 1438 / 2976;\r\n    width: min(100vw, calc(var(--frame-h) * 1438 / 2976));\r\n    max-height: var(--frame-h);\r\n    overflow: hidden;\r\n  }\r\n\r\n  .expo-web-app-shell {\r\n    position: absolute;\r\n    left: 7.4409%;\r\n    top: 2.9906%;\r\n    right: 6.3282%;\r\n    bottom: 3.125%;\r\n    zoom: 0.85;\r\n  }\r\n}\r\n```\r\n\r\nYes, 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.\r\n\r\nOn routes that are not `/app`, the whole performance gets disabled:\r\n\r\n```css\r\n.hide-desktop-wrapper .expo-web-landing,\r\n.hide-desktop-wrapper .expo-web-phone-frame {\r\n  display: none !important;\r\n}\r\n\r\n.hide-desktop-wrapper .expo-web-app-shell {\r\n  position: static !important;\r\n  width: 100% !important;\r\n  height: auto !important;\r\n  zoom: 1 !important;\r\n  overflow: visible !important;\r\n}\r\n```\r\n\r\nYes, it uses `!important`. This is pre-hydration CSS whose entire job is keeping the lie intact. We are not doing refined architecture at this layer.\r\n\r\n![Musclog desktop web app rendered inside a phone frame next to the landing panel](../../uploads/blog/2026/04/musclog-desktop-phone-frame-wrapper.png)\r\n\r\n## Three tiny crimes\r\n\r\n![Straight to jail. Right away.](../../uploads/blog/2026/04/straight-to-jail.gif)\r\n\r\nOnce the shell worked, the smaller annoyances started lining up like they had booked appointments.\r\n\r\n### 1. The landing panel needed i18n before hydration\r\n\r\nThe 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 `localStorage`:\r\n\r\n```tsx\r\nfunction landingI18nPatcher(translations, storageKey) {\r\n  try {\r\n    let lang = localStorage.getItem(storageKey);\r\n    let s = (lang && translations[lang]) || translations['en-US'];\r\n\r\n    document.querySelectorAll('[data-landing-i18n]').forEach(function (el) {\r\n      let k = el.getAttribute('data-landing-i18n');\r\n      if (k && s[k]) {\r\n        el.textContent = s[k];\r\n      }\r\n    });\r\n  } catch (_) {}\r\n}\r\n```\r\n\r\nNot elegant. Also not flash-banging a Portuguese page with English text, so I'm keeping it.\r\n\r\n### 2. Raw HTML assets do not get Expo's usual help\r\n\r\nThe 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:\r\n\r\n```tsx\r\nfunction withExpoBaseUrl(path: string): string {\r\n  const base = process.env.EXPO_BASE_URL;\r\n  if (base == null || base === '') {\r\n    return path;\r\n  }\r\n\r\n  const basePath = String(base).replace(/^\\/+|\\/+$/g, '');\r\n  const normalized = path.startsWith('/') ? path : `/${path}`;\r\n  return `/${basePath}${normalized}`;\r\n}\r\n```\r\n\r\nAnd because those files live outside the React Native asset pipeline, I copy them into `public/` before dev and export. Forget that step once and the site immediately reminds you who is in charge.\r\n\r\n### 3. `expo export` occasionally needed emotional support\r\n\r\nIn some environments, `expo export --platform web` would successfully finish and then just stay alive for no reason. Dist folder there. Files generated. Process spiritually complete, technically still hanging around.\r\n\r\nSo now there is a wrapper script:\r\n\r\n```js\r\n// scripts/export-web-wrapper.js\r\nif (output.includes('Exported: dist')) {\r\n  console.log('[export-web-wrapper] Detected successful export. Forcing exit in 5s...');\r\n  setTimeout(() => process.exit(0), 5000);\r\n}\r\n```\r\n\r\nNot glamorous. Very effective.\r\n\r\n## Then modals got weird\r\n\r\n![Musclog desktop modal rendered correctly inside the phone frame](../../uploads/blog/2026/04/musclog-desktop-modal-inside-phone-frame.png)\r\n\r\nOf course they did.\r\n\r\nWhen the web app lives inside a fake phone on desktop, React Native Web's default `Modal` behavior starts looking ridiculous very quickly. Portaling straight to `document.body` 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.\r\n\r\nSo I added a `WebModalShellProvider` high in `app/app/_layout.tsx`, with an overlay host inside the phone shell:\r\n\r\n```tsx\r\n// context/WebModalShellContext.web.tsx\r\nreturn (\r\n  <WebModalShellContext.Provider value={{ hostElement }}>\r\n    <View style={{ flex: 1, minHeight: 0, height: '100%', width: '100%', position: 'relative' }}>\r\n      <View style={{ flex: 1, minHeight: 0, height: '100%', width: '100%' }} collapsable={false}>\r\n        {children}\r\n      </View>\r\n      <View\r\n        id=\"expo-web-modal-shell-host\"\r\n        ref={setHostRef}\r\n        collapsable={false}\r\n        style={{\r\n          position: 'absolute',\r\n          left: 0,\r\n          right: 0,\r\n          top: 0,\r\n          bottom: 0,\r\n          zIndex: 1_000_000,\r\n        }}\r\n      />\r\n    </View>\r\n  </WebModalShellContext.Provider>\r\n);\r\n```\r\n\r\nThen `Modal.web.tsx` does the obvious-not-obvious thing and switches between a portal inside that host and the normal RN modal:\r\n\r\n```tsx\r\nconst useShellPortal = Platform.OS === 'web' && isDesktopFrame && hostElement != null;\r\n\r\nif (useShellPortal) {\r\n  return createPortal(\r\n    <View\r\n      style={{\r\n        position: 'absolute',\r\n        left: 0,\r\n        right: 0,\r\n        top: 0,\r\n        bottom: 0,\r\n        width: '100%',\r\n        height: '100%',\r\n      }}\r\n    >\r\n      {children}\r\n    </View>,\r\n    hostElement\r\n  );\r\n}\r\n\r\nreturn <RNModal visible={visible}>{children}</RNModal>;\r\n```\r\n\r\nAnd the shared overlay hook knows whether it should behave like a full viewport modal or a fake-phone viewport modal:\r\n\r\n```tsx\r\nexport function useWebModalLayerStyle(options = {}) {\r\n  const isDesktopFrame = useWebDesktopPhoneFrame();\r\n\r\n  if (isDesktopFrame) {\r\n    return {\r\n      position: 'absolute',\r\n      top: 0,\r\n      left: 0,\r\n      right: 0,\r\n      bottom: 0,\r\n      width: '100%',\r\n      height: '100%',\r\n    };\r\n  }\r\n\r\n  return {\r\n    position: 'fixed',\r\n    top: 0,\r\n    left: 0,\r\n    right: 0,\r\n    bottom: 0,\r\n    width: '100vw',\r\n    height: '100dvh',\r\n  };\r\n}\r\n```\r\n\r\nThat fixed the visual part. Then pointer events decided they wanted attention too.\r\n\r\nOn native, `pointerEvents=\"box-none\"` is normal. On HTML, that turns into `pointer-events: box-none`, which is not real CSS, which means the browser gets to improvise. So there is now a defensive rule for the overlay host:\r\n\r\n```css\r\n.expo-web-app-shell #expo-web-modal-shell-host {\r\n  pointer-events: none !important;\r\n}\r\n\r\n.expo-web-app-shell #expo-web-modal-shell-host * {\r\n  pointer-events: auto !important;\r\n}\r\n```\r\n\r\nThere is also a `useLayoutEffect` forcing the same thing inline, because by that point I was no longer interested in finding out how polite the correct solution was.\r\n\r\n## Look, I have a type\r\n\r\nIf you've been reading this blog for a while, none of this should surprise you.\r\n\r\nI already [turned this blog into a top-down RPG](/en/blog/coding/i-made-a-top-down-game-version-of-my-blog-with-phaser-and-react/) because the Konami Code deserved a bigger payoff than just a background effect.\r\n\r\nThen I [added Stories to the blog](/en/blog/coding/my-blog-now-has-stories-and-im-not-sure-why/) because my internet provider's app had Stories, and something about that annoyed me deeply enough that I made it everybody else's problem.\r\n\r\nSo 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.\r\n\r\n## Why I'm keeping it\r\n\r\nPast the comedy value, this fixed a very real maintenance tax.\r\n\r\nTranslations 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.\r\n\r\nDeploy 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.\r\n\r\nAnd when I shipped the [Musclog redesign](/en/blog/coding/musclog-redesign-nutrition-tracking-and-why-your-fitness-app-subscription-is-a-scam/), 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.\r\n\r\n## Conclusion\r\n\r\nCould I have kept the website in a separate Next.js repo like a more emotionally stable person? Sure.\r\n\r\nWas I ever going to keep doing that once Expo Router made this possible? No.\r\n\r\nThe 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.\r\n\r\nStill worth it.\r\n\r\nIf you want to poke around the code, [Musclog is open-source on GitHub](https://github.com/blopa/musclog-app). If you want to use it, it's at [musclog.app](https://musclog.app). The website lives in the app repo now.\r\n\r\nEverybody lives here now.\r\n","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."}},"next":null,"language":"pt-br","intl":{"language":"pt-br","languages":["en","pt-br"],"messages":{"site_title":"pablo.gg","title":"Título","author":"@thepiratepablo","search_placeholder":"Buscar...","about":"Sobre","photos":"Fotos","archive":"Arquivo","contact":"Contato","close":"Fechar","contact_page":"Página de contato","see_more":"Veja mais posts","built_with":"Feito com ","buy_me_a_soda":"Me pague uma bebida","blog":"Blog","blog_posts":"Posts do blog","go_to_post":"Ir para o post","search":"Busca","loading":"Carregando...","search_results":"Resultados de busca","search_results_for":"{quantity} resultados de busca para: \"{query}\"","search_for_query":"Buscar por \"{query}\"","no_results":"Sem resultados","home":"Home","description":"Apenas mais um blog pessoal","go_back":"Voltar para home","thats_me":"Esse sou eu!","got_it":"Entendi!","check_it_out":"Confira!","we_are":"Faltam","e3":"E3","away_from_next_sgf":"para a Summer Game Fest 2026","away_from_next_gamescom":"para a Gamescom 2026","sgf_countdown":"Contador para Summer Game Fest","gamescom_countdown":"Contador para Gamescom","e3_paragraph_1":"Esta página já contou com uma contagem regressiva para o próximo evento E3, um momento que inúmeros jogadores e profissionais da indústria aguardavam ansiosamente a cada ano. E3 não era apenas um evento; era uma celebração da nossa paixão compartilhada por videogames, um lugar onde sonhos se realizavam e memórias eram feitas.","e3_paragraph_2":"Desde os anúncios eletrizantes até as demos práticas, a E3 era o coração do mundo dos jogos. Ela reunia pessoas de todos os cantos do globo, unidas pelo amor aos jogos. Para muitos, era uma chance de conhecer seus heróis, descobrir novos títulos e experimentar a emoção das últimas inovações em tecnologia de jogos.","e3_paragraph_3":"No entanto, à medida que o cenário dos jogos evoluiu, também evoluiu a maneira como nos conectamos e celebramos nossa paixão. Embora a E3 tenha chegado ao fim, o espírito de excitação e comunidade que ela promoveu continua vivo. Agora esperamos por novas maneiras de nos reunir, compartilhar nosso amor pelos jogos e criar novas memórias.","e3_paragraph_4":"Embora a contagem regressiva tenha acabado, o legado da E3 permanecerá para sempre em nossos corações, nos lembrando das jornadas incríveis que fizemos e dos laços que formamos ao longo do caminho.","sec":"Seg","secs":"Segs","min":"Min","mins":"Mins","hour":"Hora","hours":"Horas","day":"Dia","days":"Dias","month":"Mês","months":"Meses","year":"Ano","years":"Anos","recent_posts":"Posts recentes","email":"E-mail","twitter":"Twitter","name":"Nome","page":"Página","fill_this_want_reply":"Preencha isso se quiser que eu entre em contato com você","sorry_this_post_unavailable_language":"Desculpe, este post não está disponível no idioma que você escolheu","language":"Idioma","comment":"Comentário","comments":"Comentários","no_comments":"Nenhum comentário.","post_comment":"Publicar comentário","send_message":"Enviar mensagem","message":"Mensagem","post_a_comment":"Publicar um comentário","your_comment_submitted":"Seu comentário foi enviado com sucesso.","your_message_submitted":"Sua mensagem foi enviada com sucesso.","on":"em","ok":"Ok","copy":"Copiar","copied":"Copiado","photo_num":"Foto {num}","the_matrix_has_you":"The Matrix has you...","about_paragraph_1":"Entusiasta de tecnologia desde pequeno, sempre me interessei por computadores e videogames.","about_paragraph_2":"Me formei em Tecnologia da Informação na Faculdade Estácio de Sá e sempre procuro me informar sobre novas tecnologias e me envolver em novos projetos de desenvolvimento, alguns deles o código fonte pode ser encontrado no GitHub.","cookie_banner_consent":"Ao usar este site, você concorda com o uso de cookies para oferecer uma melhor experiência.","written_in":"Escrito em ","no_post_this_tag":"Nenhum post em Português contém essa tag.","tags":"Tags","tag_colon":"Tag: ","tags_colon":"Tags: ","posts_tagged":"Posts com a tag ","categories":"Categorias","category":"Categoria","category_colon":"Categoria: ","posts_on_category":"Posts na categoria ","related_posts":"Posts relacionados","read_time":"🕒 {time} min. de leitura","create_post":"Criar Post","show":"Mostrar","date":"Data","download":"Download","add_tag":"Adicionar Tag","hide_excerpt":"Esconder Excerpt","publish_on_medium":"Publicar no Medium","allow_comments":"Permitir Comentários","subtitle":"Subtítulo","you_must_be_truly_desperate":"Você deve estar muito desesperado para me pedir ajuda","game.game_title":"pablo.gg - O Jogo","game.next":"Próxima","game.ok":"Ok","game.loading_asset_colon":"Carregando asset:","game.loading":"Carregando...","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":"Placa","game.characters.book_01":"Livro","game.characters.home_page_city_sign":"Placa","game.characters.coding_category_city_sign_01":"Placa","game.characters.coding_category_city_sign_02":"Placa","game.characters.events_category_city_sign":"Placa","game.characters.funny_category_city_sign":"Placa","game.characters.gadgets_category_city_sign_01":"Placa","game.characters.gadgets_category_city_sign_02":"Placa","game.characters.games_category_city_sign":"Placa","game.characters.general_category_city_sign":"Placa","game.characters.tips_category_city_sign":"Placa","game.characters.collectibles_category_city_sign":"Placa","game.characters.sword":"Info","game.characters.push":"Info","game.gamepad.a_button":"Botão A","game.gamepad.b_button":"Botão B","game.gamepad.d_pad_left":"D-Pad Esquerda","game.gamepad.d_pad_up":"D-Pad Cima","game.gamepad.d_pad_right":"D-Pad Direita","game.gamepad.d_pad_down":"D-Pad Baixo","game.gamepad.start_button":"Botão Start","game.menu.start":"Começar","game.menu.exit":"Sair","game.menu.settings":"Configurações","game.start_menu.save_game":"Salvar Jogo","game.start_menu.exit":"Sair","game.game_over.game_over":"Game Over","game.game_over.retry":"Tentar Novamente","game.game_over.exit":"Sair","game.browse_posts.choose_a_post":"Escolha um post para ler","game.dialogs.npc_01.01":"Ei, você finalmente acordou!","game.dialogs.npc_01.02":"O quê, você não sabe onde está?","game.dialogs.npc_01.03":"Não seja bobo, você está na Cidade Página Inicial, lembra?","game.dialogs.npc_01.04":"Esta cidade foi fundada por Pablo Montenegro para ser o início de sua jornada","game.dialogs.npc_01.05":"Explore o mundo e encontre outras cidades onde você possa ler o conhecimento acumulado de nossa civilização...","game.dialogs.npc_01.06":"... algumas pessoas chamam de \"Posts do Blog\", não sei por quê...","game.dialogs.npc_02.01":"Tenha cuidado com os Slimes que vivem no mundo aberto.","game.dialogs.npc_02.02":"Pressione ESPAÇO para usar sua espada","game.dialogs.npc_02.03":"O que é ESPAÇO? Eu não faço ideia.","game.dialogs.npc_03.01":"Olá, bem vindo a nossa biblioteca","game.dialogs.npc_03.02":"Temos apenas um livro, que contém todas as publicações da categoria dessa cidade.","game.dialogs.npc_03.03":"Vá dar uma olhada!","game.dialogs.npc_04.01":"Eu gosto de caracóis","game.dialogs.npc_05.01":"Frases incompletas podem causar","game.dialogs.npc_06.01":"O vermelho é mais verde do que o roxo, com certeza.","game.dialogs.npc_07.01":"Ter barba é o novo não ter barba","game.dialogs.npc_08.01":"E aí","game.dialogs.npc_09.01":"\" - Cooper, o que está fazendo?\"\n\" - Atracando.\"","game.dialogs.npc_10.01":"Eu deveria comprar um barco","game.dialogs.npc_11.01":"Conhece a piada do não nem eu? Não? Nem eu!","game.dialogs.npc_12.01":"Eu limpo o banheiro e resgato a princesas, vida boa, certo?","game.dialogs.npc_13.01":"Queremos as ondas de rádio de volta","game.dialogs.npc_14.01":"Salve a líder de torcida, salve o mundo","game.dialogs.npc_15.01":"Olá, como vai?","game.dialogs.npc_15.02":"OK, tchau!","game.dialogs.npc_16.01":"Um canguru é realmente apenas um coelho com esteróides","game.dialogs.npc_17.01":"Pela 216ª vez, ele disse que pararia de beber refrigerante após esta última Coca","game.dialogs.npc_18.01":"Nada e tudo é possímpossivel","game.dialogs.npc_19.01":"Para uma cidade chamada \"Eventos\", não há muito acontecendo...","game.dialogs.npc_20.01":"Eu ouvi dizer que existe um jeito de empurrar alguns objetos neste jogo, mas não sei como.","game.dialogs.sign_01.01":"Parabéns, você pode ler isso!","game.dialogs.book_01.01":"Hey, obrigado por testar esse novo jeito bem esquisito de acessar o meu site","game.dialogs.book_01.02":"Este projeto não seria possível sem o incrível trabalho de muitas pessoas, como:","game.dialogs.book_01.03":"ArMM1998 - Pelos sprites dos personagens e os tilesets","game.dialogs.book_01.04":"PixElthen - Pelos sprites do slime","game.dialogs.book_01.05":"pixelartm - Pelos sprites do chapéu de pirata","game.dialogs.book_01.06":"jkjkke - Pela imagem da tela de Game Over","game.dialogs.book_01.07":"KnoblePersona - Pela imagem da tela do menu inicial","game.dialogs.book_01.08":"Min - Pelo sprite do livro aberto","game.dialogs.book_01.09":"E claro, ao Richard Davey por ter criado o Phaser.io!","game.dialogs.home_page_city_sign":"Cidade Página Inicial","game.dialogs.coding_category_city_sign.01":"Cidade da Categoria Programação","game.dialogs.coding_category_city_sign.02":"Cidade da Categoria Programação","game.dialogs.events_category_city_sign":"Cidade da Categoria Eventos","game.dialogs.funny_category_city_sign":"Cidade da Categoria Engraçado","game.dialogs.gadgets_category_city_sign.01":"Cidade da Categoria Eletrónicos","game.dialogs.gadgets_category_city_sign.02":"Cidade da Categoria Eletrónicos","game.dialogs.games_category_city_sign":"Cidade da Categoria Jogos","game.dialogs.general_category_city_sign":"Cidade da Categoria Geral","game.dialogs.tips_category_city_sign":"Cidade da Categoria Dicas","game.dialogs.collectibles_category_city_sign":"Cidade da Categoria Brinquedos","game.dialogs.sword_item_description":"Agora você pode atacar, pressione ESPAÇO para usar sua espada.","game.dialogs.push_item_description":"Agora você pode empurrar alguns objetos, pressione ESPAÇO na frente de um objeto para empurrá-lo.","zelda_timeline.title":"Timeline de Zelda","zelda_timeline.timeline_split":"Divisão da Timeline","zelda_timeline.timeline_unification":"Timeline Unificada","zelda_timeline.icons_from":"Os ícones usados nesta página são do zeldauniverse.net e game-icons.net","zelda_timeline.creation":"Criação","zelda_timeline.creation_of_land_sky":"A Criação da Terra e do Céu","zelda_timeline.goddess_hylia_and_sky_era":"Deusa Hylia e a Era do Céu","zelda_timeline.skyward_sword":"Skyward Sword","zelda_timeline.the_ancient_battle":"A Antiga Batalha e a reencarnação da Deusa Hylia","zelda_timeline.return_to_surface":"O retorno à superfície","zelda_timeline.era_of_chaos":"Era do caos","zelda_timeline.sacred_realm_sealed":"O Sacred Realm é selado","zelda_timeline.era_of_prosperity":"Era da Prosperidade","zelda_timeline.establishment_of_hyrule":"O Reino de Hyrule é estabelecido","zelda_timeline.force_era":"Era da Força","zelda_timeline.the_minish_cap":"The Minish Cap","zelda_timeline.rise_of_evil_vaati":"A Ascensão do Maligno Vaati","zelda_timeline.four_swords":"Four Swords","zelda_timeline.resurrection_of_vaati":"A Ressurreição de Vaati","zelda_timeline.era_of_the_hero_of_time":"Era do Herói do Tempo","zelda_timeline.hyrulean_civil_war":"Guerra Civil Hyruleana","zelda_timeline.ocarina_of_time":"Ocarina of Time","zelda_timeline.sacred_realm_becomes_dark_world":"O Sacred Realm se torna o Dark World","zelda_timeline.ganondorf_becomes_ganon":"Ganondorf se torna Ganon","zelda_timeline.hero_is_defeated":"O herói é derrotado","zelda_timeline.decline_of_last_hero":"O declínio de Hyrule e o último herói","zelda_timeline.the_imprisoning_war":"A Guerra do Aprisionamento","zelda_timeline.era_of_dark_and_light":"Era de Luz e Escuridão","zelda_timeline.a_link_to_the_past":"A Link to the Past","zelda_timeline.resurrection_of_ganon":"A Ressurreição de Ganon","zelda_timeline.resurrection_of_ganon_is_prevented":"A ressurreição de Ganon é evitada","zelda_timeline.links_awakening":"Link's Awakening","zelda_timeline.oracle_of_ages_and_seasons":"Oracle of Ages e 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":"Os Monarcas de Hyrule usam a Triforce","zelda_timeline.era_of_decline":"A Era do Declínio","zelda_timeline.tragedy_of_princess_zelda_1":"A Tragédia da Princesa 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 Derrotado","zelda_timeline.child_era":"Era Infantil","zelda_timeline.adult_era":"Era Adulta","zelda_timeline.sacred_realm_protected":"Sacred Realm está protegido","zelda_timeline.twilight_realm_and_legacy_of_hero":"O Twilight Realm e o legado do Herói","zelda_timeline.majoras_mask":"Majora's Mask","zelda_timeline.prince_of_thieves_is_executed":"O Príncipe dos Ladrões Ganondorf é executado","zelda_timeline.twilight_era":"A Era do Twilight","zelda_timeline.twilight_princess":"Twilight Princess","zelda_timeline.shadow_invasion":"A invasão das sombras","zelda_timeline.shadow_era":"A Era das Sombras","zelda_timeline.four_swords_adventures":"Four Swords Adventures","zelda_timeline.reincarnation_of_ganondorf":"A Reencarnação de Ganondorf","zelda_timeline.ganondorf_is_sealed":"Ganondorf é selado","zelda_timeline.hero_of_wind_and_new_world":"O Herói do Vento e um Novo Mundo","zelda_timeline.era_without_a_hero":"A era sem um herói","zelda_timeline.ganondorf_is_resurrected":"Ganondorf é ressuscitado","zelda_timeline.hyrule_is_sealed_and_flooded":"Hyrule é selado e então inundado","zelda_timeline.era_of_the_great_sea":"A Era do Great Sea","zelda_timeline.the_wind_waker":"The Wind Waker","zelda_timeline.era_of_the_great_voyage":"A Era da Grande Viagem","zelda_timeline.phantom_hourglass":"Phantom Hourglass","zelda_timeline.era_of_hyrule_rebirth":"A Era do Renascimento de Hyrule","zelda_timeline.new_continent_discovered":"Novo continente descoberto","zelda_timeline.new_hyrule_is_founded":"Um novo reino de Hyrule é fundado","zelda_timeline.spirit_tracks":"Spirit Tracks","zelda_timeline.evil_king_malladus_is_resurrected":"O Rei Maligno Malladus é ressuscitado","zelda_timeline.age_of_calamity":"Age of Calamity","zelda_timeline.breath_of_the_wild":"Breath of the Wild","zelda_timeline.era_of_the_wilds":"A Era dos Selvagens","zelda_timeline.calamity_ganon_is_sealed":"Calamity Ganon é Selado. A tecnologia é proibida, levando alguns Sheikah a formar o Yiga Clan","zelda_timeline.divine_beasts_are_cleansed":"As Bestas Divinas são purificadas e Calamity Ganon é Selado","zelda_timeline.tears_of_the_kingdom":"Tears of the Kingdom","zelda_timeline.hyrule_kingdom_is_teared_apart":"Ganondorf é ressuscitado (Será?)","blog_categories.games":"Jogos","blog_categories.general":"Geral","blog_categories.tips":"Dicas","blog_categories.events":"Eventos","blog_categories.coding":"Programação","blog_categories.funny":"Engraçado","blog_categories.collectibles":"Colecionáveis","blog_categories.gadgets":"Eletrónicos","e3_2012_photos.title":"E3 2012","e3_2012_photos.description":"Em junho de 2012 participei da E3 como imprensa para uma cobertura completa para o Nintendo Blast.","e3_2013_photos.title":"E3 2013","e3_2013_photos.description":"Em junho de 2013 participei da E3 como imprensa para uma cobertura completa para o Game Blast.","e3_2014_photos.title":"E3 2014","e3_2014_photos.description":"Em junho de 2014 participei da E3 como imprensa para uma cobertura completa para o Game Blast.","e3_2015_photos.title":"E3 2015","e3_2015_photos.description":"Em junho de 2015 participei da E3 como imprensa para uma cobertura completa para o Game Blast e Game Over TV.","e3_2017_photos.title":"E3 2017","e3_2017_photos.description":"Em junho de 2017 participei da E3 como imprensa para uma cobertura completa para o PlayReplay e Game Over TV.","e3_2019_photos.title":"E3 2019","e3_2019_photos.description":"Em junho de 2019 participei da E3 como imprensa para uma cobertura completa para o PlayReplay.","gamescom_2019_photos.title":"Gamescom 2019","gamescom_2019_photos.description":"Em agosto de 2019 participei da Gamescom como imprensa para uma cobertura completa para o PlayReplay.","san_francisco_2019_photos.title":"San Francisco 2019","san_francisco_2019_photos.description":"Em setembro de 2019, viajei para San Francisco para o show do Metallica S&M2.","forty_two_page.title":"Quarenta e Dois","forty_two_page.description":"Até mais, e obrigado pelos peixes!","projects_page.title":"Projetos","projects_page.description":"Aqui está uma lista de alguns dos meus projetos pessoais favoritos.","projects_page.gatsbyMaterialUiBlogDescription":"Um simples Gatsby Blog Starter com Material UI.","projects_page.contractBuilderDescription":"O Contract Builder é um projeto de código aberto gratuito que permite a qualquer pessoa manter e construir facilmente qualquer tipo de contrato (documentos legais, processos judiciais, aluguel, acordos, construção e assim por diante) usando o Google Spreadsheets. Este foi desenvolvido como um projeto pessoal para ajudar uma amiga que estava com dificuldades de gastar até uma hora para fazer um contrato personalizado, agora ela consegue fazer em menos de 5 minutos. Hooray!","projects_page.resumeBuilderDescription":"Resume Builder é um projeto de código aberto gratuito que permite a qualquer pessoa manter e construir facilmente qualquer tipo de currículo usando o Google Spreadsheets. Este foi desenvolvido como um projeto pessoal para ajudar um amigo que estava com dificuldades de gastar até uma hora para fazer um currículo personalizado.","projects_page.magentoChatbotDescription":"Com este módulo, você pode integrar totalmente a sua loja Magento com os aplicativos de chat mais populares do mercado. Isso significa que simplesmente instalando este módulo e alguns cliques você pode ter uma nova forma de mostrar e vender seus produtos aos seus clientes. Muito fácil de usar! Experimente agora, é GRÁTIS.","projects_page.jamStackSortenerDescription":"Este é um POC de um encurtador de URL básico desenvolvido com Gatsby.","projects_page.gotinhaDescription":"Sempre foi meu sonho fazer meu próprio jogo, e depois de experimentar o Unity alguns anos atrás, decidi tentar novamente com algo com o qual estou mais familiarizado: Javascript. Como desenvolvedor front-end, Javascript já é a linguagem com a qual escrevo a maior parte do meu código no trabalho e também em meus projetos pessoais, e após uma rápida pesquisa consegui encontrar o incrível PhaserJS Framework para construção de jogos web 2D.","notfound.title":"404: Não encontrado","notfound.header":"404 NÃO ENCONTRADO","notfound.description":"Desculpe, esta página parece não existir. Talvez os arquivos estejam incompletos?","seo_keywords.developer":"desenvolvedor","seo_keywords.development":"desenvolvimento","seo_keywords.javascript":"javascript","seo_keywords.es6":"es6","seo_keywords.e3":"e3","seo_keywords.sgf":"sgf","seo_keywords.gamescom":"gamescom","seo_keywords.countdown":"contador","seo_keywords.archive":"arquivo","seo_keywords.about_me":"sobre mim","seo_keywords.personal_blog":"blog pessoal","seo_keywords.personal_projects":"projetos pessoais","seo_keywords.travels":"viagens","seo_keywords.tips":"dicas","seo_keywords.lifehacks":"truques de produtividade","seo_keywords.reviews":"analises","seo_keywords.games":"games","seo_keywords.timeline":"linha do tempo","seo_keywords.photos":"fotos","cookie_law.we_use_cookies":"Usamos cookies para garantir que você obtenha a melhor experiência em nosso site. Ao usar nosso site, você concorda com nossa ","cookie_law.title":"Política de cookies","cookie_law.what_are_cookies":"O que são cookies?","cookie_law.what_are_cookies_text":"Como é prática comum em quase todos os sites profissionais, este site usa cookies, que são pequenos arquivos baixados para o seu computador, para melhorar a sua experiência. Esta página descreve quais informações eles coletam, como as usamos e por que às vezes precisamos armazenar esses cookies. Também compartilharemos como você pode evitar que esses cookies sejam armazenados, no entanto, isso pode diminuir ou 'quebrar' certos elementos da funcionalidade do site. Para obter mais informações gerais sobre cookies, leia ","cookie_law.what_are_cookies_more_info_url":"https://pt.wikipedia.org/wiki/Cookie_(informática)","cookie_law.how_we_use_cookies":"Como usamos cookies","cookie_law.how_we_use_cookies_text":"Usamos cookies por vários motivos detalhados abaixo. Infelizmente, na maioria dos casos, não há opções padrão da indústria para desabilitar cookies sem desabilitar completamente a funcionalidade e os recursos que eles adicionam a este site. Recomenda-se que você deixe todos os cookies se não tiver certeza se precisa deles ou não, caso sejam usados para fornecer um serviço que você usa.","cookie_law.disabling_cookies":"Desativando cookies","cookie_law.disabling_cookies_text":"Você pode impedir a configuração de cookies ajustando as configurações do seu navegador (consulte a Ajuda do navegador para saber como fazer isso). Esteja ciente de que a desativação de cookies afetará a funcionalidade deste e de muitos outros sites que você visita. A desativação dos cookies normalmente resultará na desativação de certas funcionalidades e recursos deste site. Portanto, é recomendável que você não desative os cookies.","cookie_law.the_cookies_we_set":"Os cookies que definimos","cookie_law.site_preferences_cookie":"Cookies de preferências do site","cookie_law.site_preferences_cookie_text":"Para lhe proporcionar uma excelente experiência neste site, fornecemos a funcionalidade para definir as suas preferências de funcionamento deste site quando o utiliza. Para lembrar suas preferências, precisamos definir cookies para que essas informações possam ser chamadas sempre que você interagir com uma página afetada por suas preferências.","cookie_law.third_party_cookies":"Cookies de terceiros","cookie_law.third_party_cookies_text":"Em alguns casos especiais, também usamos cookies fornecidos por terceiros confiáveis. A seção a seguir detalha quais cookies de terceiros você pode encontrar neste site.","cookie_law.third_party_cookies_item_1":"Este site usa o Google Analytics, que é uma das soluções de análise mais difundidas e confiáveis na web para nos ajudar a entender como você usa o site e como podemos melhorar sua experiência. Esses cookies podem rastrear coisas como quanto tempo você passa no site e as páginas que você visita para que possamos continuar a produzir conteúdo envolvente. Para obter mais informações sobre os cookies do Google Analytics, consulte a página oficial do Google Analytics.","cookie_law.third_party_cookies_item_2":"De vez em quando, testamos novos recursos e fazemos mudanças sutis na maneira como o site é fornecido. Quando ainda estamos testando novos recursos, esses cookies podem ser usados para garantir que você receba uma experiência consistente enquanto estiver no site, garantindo que entendemos quais otimizações nossos usuários mais apreciam.","cookie_law.more_information":"Mais informações","cookie_law.more_information_text":"Esperamos que isso tenha esclarecido as coisas para você e, conforme mencionado anteriormente, se há algo que você não tem certeza se precisa ou não, geralmente é mais seguro deixar os cookies ativados, caso eles interajam com um dos recursos que você usa em nosso site. No entanto, se você ainda estiver procurando por mais informações, entre em contato conosco através de nossa "},"routed":true,"originalPath":"/blog/coding/site-ou-app-sim-o-guia-gambiarrístico-para-sites-feitos-em-expo-router/","redirect":true,"redirectDefaultLanguageToRoot":false,"defaultLanguage":"en","fallbackLanguage":"","ignoredPaths":[]},"blogLocale":"pt-br"}},
    "staticQueryHashes": ["1156153307","1355482417","1591365477","1628619374","2127381735","2288279559","26159077","3566410298","3649515864","3847325417","3982724423","928834867"]}