{
    "componentChunkName": "component---src-templates-blog-post-jsx",
    "path": "/en/blog/general/youll-own-nothing-and-youll-be-happy-part-1/",
    "result": {"data":{"site":{"siteMetadata":{"siteUrl":"https://pablo.gg"}},"markdownRemark":{"id":"83fac172-3aef-52cb-8ac3-d33830cd8bb2","excerpt":"I have the same photo library in Google Photos and in a bunch of hard drives inside my apartment. This is not a workflow I recommend to normal people. This is…","html":"<p>I have the same photo library in <a href=\"https://photos.google.com/\" target=\"_blank\" rel=\"noreferrer\">Google Photos</a> and in a bunch of hard drives inside my apartment. This is not a workflow I recommend to normal people. This is hoarder infrastructure.</p>\n<p>I do it because I don’t trust “access” the same way I trust “I have the file right here”. If you’ve ever moved countries, changed ecosystems, or watched one tech company eat another one, you know the difference matters. Convenience is nice. Ownership is nicer.</p>\n<p>And yet the whole industry spent the last decade trying to convince us those are basically the same thing. They are not. That whole <a href=\"https://en.wikipedia.org/wiki/You%27ll_own_nothing_and_be_happy\" target=\"_blank\" rel=\"noreferrer\">“you’ll own nothing, and you’ll be happy”</a> line was supposed to sound futuristic. Instead it became product strategy.</p>\n<h2 id=\"hoarder-mode\" style=\"position:relative;\"><a href=\"#hoarder-mode\" aria-label=\"hoarder mode 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>Hoarder mode</h2>\n<p>I am, objectively, the ideal customer for physical media.</p>\n<p>I like shelves. I like cartridges. I like hard drives. I like knowing that if a company wakes up on some random Tuesday and decides to kill a service, merge a platform, rename a plan, or add AI to something that used to just work, my stuff is still my stuff.</p>\n<p>This doesn’t mean I’m living in a bunker made of GameCube discs. I use Google Photos. I pay for Spotify. I stream movies. I live in the real world. But I don’t confuse convenience with control.</p>\n<p>So yes, all my photos are in Google Photos. And yes, all my photos are also stored locally. Because when a company says “don’t worry, it’s in the cloud”, what I hear is “future Pablo, this is your problem now.” (Also: future Pablo, link the degoogling post here when you finally write it.)</p>\n<h2 id=\"concord-was-the-cleanest-version-of-the-problem\" style=\"position:relative;\"><a href=\"#concord-was-the-cleanest-version-of-the-problem\" aria-label=\"concord was the cleanest version of the problem 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>Concord was the cleanest version of the problem</h2>\n<p><a href=\"https://en.wikipedia.org/wiki/Concord_(video_game)\" target=\"_blank\" rel=\"noreferrer\">Concord</a> was such a disaster it almost looped back into being interesting.</p>\n<p>Sony launched it, it bombed, refunded everyone, and then removed the digital version from players’ accounts. Depending on how you experienced it, the install either disappeared from the console or turned into a dead icon you couldn’t do anything with. Same result. A thing people had “bought” stopped existing the moment Sony decided the experiment was over.</p>\n<p>Yes, people got the money back. No, that is not the point.</p>\n<p>The point is that Sony showed, with zero subtlety, what a digital purchase actually is. Not ownership. Permission. Temporary, revocable, account-bound permission.</p>\n<p>Now compare that with <a href=\"https://en.wikipedia.org/wiki/P.T._(video_game)\" target=\"_blank\" rel=\"noreferrer\">P.T.</a>. Konami pulled it from the PlayStation Store after the Kojima breakup, which already sucked, but if you had downloaded it before that, it stayed on your console. People held on to those PS4s like they were carrying cursed treasure. That was bad enough already. Concord managed to make it worse.</p>\n<p>The old version of digital was “you can’t buy it anymore”. The new version is “you can’t even keep the useless dead file you already had”. Amazing progress.</p>\n<h2 id=\"anime-people-already-learned-this-lesson\" style=\"position:relative;\"><a href=\"#anime-people-already-learned-this-lesson\" aria-label=\"anime people already learned this lesson 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>Anime people already learned this lesson</h2>\n<p>Gamers are not even the first victims here. Anime fans got a speedrun version of the same thing when Funimation got folded into Crunchyroll.</p>\n<p>People had digital copies tied to Funimation that simply did not make the trip. Not “please wait while we migrate your library”. Not “we’re sorting out licensing details”. Just gone. Crunchyroll’s own support page says those digital copies are not available there anymore.</p>\n<p>Back in the DVD days, this problem did not exist. You bought the disc, threw away the plastic wrap, put it on a shelf, and that was the end of the negotiation. The publisher could disappear, get acquired, or explode in a corporate merger and your movie would still be sitting there, ready to go.</p>\n<p>That is what ownership looks like. Boring. Reliable. Beautiful.</p>\n<h2 id=\"the-subscription-math-is-fake-good\" style=\"position:relative;\"><a href=\"#the-subscription-math-is-fake-good\" aria-label=\"the subscription math is fake good 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 subscription math is fake-good</h2>\n<p>Game Pass and PS Plus are the kind of deal that sounds amazing until you stop looking at the monthly price and start looking at the years.</p>\n<p>Let’s say you pay around $200 a year for a premium game subscription. In ten years, that is $2,000. And what do you have at the end of it? A receipt history and some fond memories, I guess.</p>\n<p>You do not have a shelf of games. You do not have something you can resell. You do not have the specific 2026 version of that game before three balance patches and two monetization experiments turned it into something else. The second you stop paying, the whole library evaporates.</p>\n<p>People hear this and say “yeah but I don’t replay games”. Fair. Until the service gets worse.</p>\n<p>Because that is the real checkmate here. The trap is not that you personally need to own every movie or every game forever. The trap is that once enough people stop owning things, companies get to make the service worse and you have nowhere good to go. More ads, higher prices, smaller catalog, worse support, more lock-in. The usual enshittification arc.</p>\n<p>If you want a stupidly specific example, look at the 3DS. <a href=\"https://en.wikipedia.org/wiki/Pok%C3%A9mon_Shuffle\" target=\"_blank\" rel=\"noreferrer\">Pokemon Shuffle</a> was a real Nintendo game on a real Nintendo handheld and now, legally, it may as well be smoke for anyone who didn’t grab it in time. <a href=\"https://zelda.fandom.com/wiki/The_Legend_of_Zelda:_Four_Swords_Anniversary_Edition\" target=\"_blank\" rel=\"noreferrer\">The Legend of Zelda: Four Swords Anniversary Edition</a> was free and still managed to become unobtainable. Free. Gone. Nintendo somehow found a way to make zero euros expire.</p>\n<h2 id=\"nintendo-is-now-importing-digital-nonsense-into-physical-media\" style=\"position:relative;\"><a href=\"#nintendo-is-now-importing-digital-nonsense-into-physical-media\" aria-label=\"nintendo is now importing digital nonsense into physical media 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>Nintendo is now importing digital nonsense into physical media</h2>\n<p>Nintendo used to be the company of “blow on the cartridge and try again.” Very physical. Very stupid. Very honest.</p>\n<p>Now we get stuff like <em>Super Mario 3D All-Stars</em> being sold with artificial scarcity so the second-hand price climbs to around €130 for no good reason beyond Nintendo deciding FOMO should have a retail box. I already wrote about how this kind of pricing logic works in the collectibles world <a href=\"/en/blog/collectibles/why-are-some-plastic-toys-so-expensive-a-sneaky-peek-on-how-pricing-works/\">here</a>. It is annoying there too, but at least nobody pretends it is pro-consumer.</p>\n<p>Then came the Switch 2 game-key cards. Nintendo’s own support page literally says the card does not contain the full game data. It is the key that lets you download the actual game.</p>\n<p>So now even when you buy the thing in a store, take the box home, and insert the card in the console, you may still be buying a permission slip. Physical-looking DRM. A cartridge cosplay.</p>\n<p>Nintendo says you only need the internet the first time, which is better than always-online garbage, sure. But the preservation problem is still right there. If the real game lives on a server first, that physical product has an expiration date hiding inside it.</p>\n<p>Cartridges used to be the whole point. Now sometimes they’re just a receipt with extra steps.</p>\n<h2 id=\"your-phone-is-also-turning-into-a-frontend\" style=\"position:relative;\"><a href=\"#your-phone-is-also-turning-into-a-frontend\" aria-label=\"your phone is also turning into a frontend 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>Your phone is also turning into a frontend</h2>\n<p>I miss when phones had features that actually lived inside the phone.</p>\n<p>My old Nokia had Wi-Fi and could do some smart things, but most of the time it was just a solid little brick that worked offline and minded its own business. Now every launch event is full of magical features that depend on some cloud service quietly doing the real work somewhere else.</p>\n<p>That means the feature is not really part of the device. It is part of a service attached to the device.</p>\n<p>The day the company kills that service, moves it behind a subscription tier, or decides only the new model deserves it, your very fancy hardware suddenly forgets how to do the thing from the commercial. Congrats on the purchase. You bought a screen for a server.</p>\n<h2 id=\"i-am-not-a-purist-just-tired\" style=\"position:relative;\"><a href=\"#i-am-not-a-purist-just-tired\" aria-label=\"i am not a purist just tired 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>I am not a purist, just tired</h2>\n<p>I am not telling you to cancel every subscription, throw your phone in a canal, and live off ripped DVDs and local MP3s like it’s 2007. I still make compromises. Everybody does.</p>\n<p>But I do think a few rules still make sense:</p>\n<ul>\n<li>buy physical games when you can</li>\n<li>keep local backups of photos and files you actually care about</li>\n<li>prefer one-time purchases when the product does not genuinely need a monthly bill</li>\n<li>reject digital assets whenever you can</li>\n<li>be suspicious when a company sells “convenience” by removing your options</li>\n</ul>\n<p>Also, physical media is not just better for preservation. It is often better for your wallet. You can buy a used game, finish it, and sell it to the next guy. Try doing that with a PSN license.</p>\n<p>Part 2 is going deeper into software subscriptions specifically, because that rabbit hole gets even dumber. Fitness apps, Adobe, “AI features”, apps charging monthly fees to write text into SQLite, all the usual modern nonsense. I already touched that nerve a bit in my <a href=\"/en/blog/coding/musclog-redesign-nutrition-tracking-and-why-your-fitness-app-subscription-is-a-scam/\">Musclog post about nutrition tracking and scammy fitness app subscriptions</a>, but I have more to say.</p>\n<p>For now, that’s the whole rant: cloud stuff is convenient, subscriptions are sometimes unavoidable, and ownership still matters. Especially when the companies telling you it doesn’t are the exact ones with a financial incentive to make sure you never really keep anything again.</p>\n<p>See you in part 2.</p>","fields":{"postHashId":"Z2VuZXJhbHRydWVudWxsMjAyNi0wNS0xNlQwMDowMDowMC4wMDBa","slug":"/2026/2026-05-16-youll-own-nothing-and-youll-be-happy-part-1.en/","path":"/blog/general/youll-own-nothing-and-youll-be-happy-part-1/","locale":"en"},"readingTime":{"minutes":7.41},"frontmatter":{"path":"youll-own-nothing-and-youll-be-happy-part-1","allowComments":true,"title":"You'll own nothing and you'll be happy - Part 1","date":"2026-05-16T00:00:00.000Z","categories":["general"],"tags":["subscriptions","digital-ownership","games","physical-media","enshittification"],"hideExcerpt":false,"subtitle":"Cloud convenience is cute until somebody decides your purchase no longer exists"}},"categoryImage":{"childImageSharp":{"original":{"width":1920,"height":1080,"src":"/static/categories_general-2e01bc51fc29da41beb68a86d5ec193f.jpg"}}}},"pageContext":{"postHashId":"Z2VuZXJhbHRydWVudWxsMjAyNi0wNS0xNlQwMDowMDowMC4wMDBa","relatedPosts":[{"fields":{"postHashId":"Z2FtZXN0cnVlbnVsbDIwMjQtMTAtMjFUMDA6MDA6MDAuMDAwWg==","slug":"/2024/2024-10-21-a-guide-to-retro-game-shopping-in-seoul.en/","path":"/blog/games/a-guide-to-retro-game-shopping-in-seoul/","locale":"en"},"frontmatter":{"tags":["games","shopping","seoul","korea","action figures","retro games","video games"],"categories":["games"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2024-10-21T00:00:00.000Z","id":null,"path":"a-guide-to-retro-game-shopping-in-seoul","show":true,"title":"A guide to retro game shopping in Seoul","hideExcerpt":false,"subtitle":"A guide to the best places to buy retro games and action figures in Seoul."}},{"fields":{"postHashId":"Z2FtZXN0cnVlbnVsbDIwMjItMDktMjdUMDA6MDA6MDAuMDAwWg==","slug":"/2022/2022-09-27-e3-2023-dates-revealed.en/","path":"/blog/games/e3-2023-dates-revealed/","locale":"en"},"frontmatter":{"tags":["e3","los angeles","games","nintendo","sony","microsoft"],"categories":["games"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2022-09-27T00:00:00.000Z","id":null,"path":"e3-2023-dates-revealed","show":true,"title":"E3 2023 dates revealed","hideExcerpt":false,"subtitle":"LET'S GOOOOOOOO"}},{"fields":{"postHashId":"Z2FtZXN0cnVlbnVsbDIwMjItMDYtMDFUMDA6MDA6MDAuMDAwWg==","slug":"/2022/2022-06-01-10-years-anniversary-of-my-first-e3.en/","path":"/blog/games/10-years-anniversary-of-my-first-e3/","locale":"en"},"frontmatter":{"tags":["e3","e3 2012","games","los angeles","lacc","convention center","nintendo","sony","microsoft","gameblast","media","journalism"],"categories":["games"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2022-06-01T00:00:00.000Z","id":null,"path":"10-years-anniversary-of-my-first-e3","show":true,"title":"10 years anniversary of my first E3","hideExcerpt":false,"subtitle":"10 years ago today I was embarking on a journey to my first E3 conference."}},{"fields":{"postHashId":"Z2FtZXN0cnVlbnVsbDIwMjEtMTEtMThUMDA6MDA6MDAuMDAwWg==","slug":"/2021/2021-11-18-how-to-load-assets-asynchronously-with-phaser-3.en/","path":"/blog/games/how-to-load-assets-asynchronously-with-phaser-3/","locale":"en"},"frontmatter":{"tags":["coding","games","javascript","phaser","phaser 3","asynchronous","rexrainbow","phaser loader"],"categories":["games"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2021-11-18T00:00:00.000Z","id":null,"path":"how-to-load-assets-asynchronously-with-phaser-3","show":true,"title":"How to load assets asynchronously with Phaser 3","hideExcerpt":false,"subtitle":"Asynchronous asset loading in Phaser 3's preload function."}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDIxLTEwLTEyVDAwOjAwOjAwLjAwMFo=","slug":"/2021/2021-10-12-i-made-a-top-down-game-version-of-my-blog-with-phaser-and-react.en/","path":"/blog/coding/i-made-a-top-down-game-version-of-my-blog-with-phaser-and-react/","locale":"en"},"frontmatter":{"tags":["coding","games","javascript","phaser","phaser 3","react","blog","rpg","rpg maker","top-down"],"categories":["coding"],"allowComments":true,"publishOnMedium":null,"cover":null,"date":"2021-10-12T00:00:00.000Z","id":null,"path":"i-made-a-top-down-game-version-of-my-blog-with-phaser-and-react","show":true,"title":"I made a top-down game version of my blog with Phaser and React","hideExcerpt":null,"subtitle":"Learn how to create a top-down RPG game using Phaser and React, including integrating with Gatsby and creating the game map with Tiled"}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDIxLTEwLTA4VDAwOjAwOjAwLjAwMFo=","slug":"/2021/2021-10-08-how-to-create-a-top-down-rpg-maker-like-game-with-phaser-js-and-react.en/","path":"/blog/coding/how-to-create-a-top-down-rpg-maker-like-game-with-phaser-js-and-react/","locale":"en"},"frontmatter":{"tags":["coding","games","javascript","phaser","phaser 3","react","dialog box","rpg","rpg maker"],"categories":["coding"],"allowComments":true,"publishOnMedium":null,"cover":null,"date":"2021-10-08T00:00:00.000Z","id":null,"path":"how-to-create-a-top-down-rpg-maker-like-game-with-phaser-js-and-react","show":true,"title":"How to create a top-down RPG Maker like game with Phaser JS and React","hideExcerpt":null,"subtitle":"From RPG Maker dreams to Phaser reality: a game dev journey."}},{"fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDIxLTEwLTA2VDAwOjAwOjAwLjAwMFo=","slug":"/2021/2021-10-06-creating-a-dialog-box-with-react-for-a-phaser-game.en/","path":"/blog/coding/creating-a-dialog-box-with-react-for-a-phaser-game/","locale":"en"},"frontmatter":{"tags":["coding","games","javascript","phaser","phaser 3","react","dialog box","rpg","rpg maker","material ui"],"categories":["coding"],"allowComments":true,"publishOnMedium":null,"cover":null,"date":"2021-10-06T00:00:00.000Z","id":null,"path":"creating-a-dialog-box-with-react-for-a-phaser-game","show":true,"title":"Creating a dialog box with React for a Phaser game","hideExcerpt":null,"subtitle":"Phaser game UI: Blend canvas and DOM."}},{"fields":{"postHashId":"Z2FtZXN0cnVlbnVsbDIwMjEtMTAtMDJUMDA6MDA6MDAuMDAwWg==","slug":"/2021/2021-10-02-automatically-update-the-embedded-tileset-for-all-your-tiled-game-with-a-nodejs-script.en/","path":"/blog/games/automatically-update-the-embedded-tileset-for-all-your-tiled-game-with-a-nodejs-script/","locale":"en"},"frontmatter":{"tags":["coding","games","javascript","phaser","phaser 3","nodejs","node","tiled","map","tilemap","tileset"],"categories":["games"],"allowComments":true,"publishOnMedium":null,"cover":null,"date":"2021-10-02T00:00:00.000Z","id":null,"path":"automatically-update-the-embedded-tileset-for-all-your-tiled-game-with-a-nodejs-script","show":true,"title":"Automatically update the embedded Tileset for all your Tiled maps a Node.js script","hideExcerpt":null,"subtitle":"Load Tiled maps in Phaser JS easily."}},{"fields":{"postHashId":"Z2FtZXN0cnVlbnVsbDIwMjEtMDktMDlUMDA6MDA6MDAuMDAwWg==","slug":"/2021/2021-09-09-creating-a-mobile-build-of-my-phaser-js-game-game-devlog-23.en/","path":"/blog/games/creating-a-mobile-build-of-my-phaser-js-game-game-devlog-23/","locale":"en"},"frontmatter":{"tags":["coding","games","javascript","phaser","phaser 3","game devlog","gamedev","skate platformer","super ollie vs pebble corp","webpack","tiled","mobile","android","mobile build"],"categories":["games"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2021-09-09T00:00:00.000Z","id":null,"path":"creating-a-mobile-build-of-my-phaser-js-game-game-devlog-23","show":true,"title":"Creating a mobile build of my Phaser JS game - Game Devlog #23","hideExcerpt":false,"subtitle":"Porting Super Ollie to mobile with virtual controls."}},{"fields":{"postHashId":"Z2FtZXN0cnVlbnVsbDIwMjEtMDgtMjhUMDA6MDA6MDAuMDAwWg==","slug":"/2021/2021-08-28-creating-moving-platforms-for-my-phaser-js-game-game-devlog-22.en/","path":"/blog/games/creating-moving-platforms-for-my-phaser-js-game-game-devlog-22/","locale":"en"},"frontmatter":{"tags":["coding","games","javascript","phaser","phaser 3","game devlog","gamedev","skate platformer","super ollie vs pebble corp","webpack","tiled","moving platforms","platforms"],"categories":["games"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2021-08-28T00:00:00.000Z","id":null,"path":"creating-moving-platforms-for-my-phaser-js-game-game-devlog-22","show":true,"title":"Creating moving platforms for my Phaser JS game - Game Devlog #22","hideExcerpt":false,"subtitle":"Super Ollie: Making moving platforms, finally."}}],"alternativeHtml":"<p>I have the same photo library in <a href=\"https://photos.google.com/\" target=\"_blank\" rel=\"noreferrer\">Google Photos</a> and in a bunch of hard drives inside my apartment. This is not a workflow I recommend to normal people. This is hoarder infrastructure.</p>\n<p>I do it because I don’t trust “access” the same way I trust “I have the file right here”. If you’ve ever moved countries, changed ecosystems, or watched one tech company eat another one, you know the difference matters. Convenience is nice. Ownership is nicer.</p>\n<p>And yet the whole industry spent the last decade trying to convince us those are basically the same thing. They are not. That whole <a href=\"https://en.wikipedia.org/wiki/You%27ll_own_nothing_and_be_happy\" target=\"_blank\" rel=\"noreferrer\">“you’ll own nothing, and you’ll be happy”</a> line was supposed to sound futuristic. Instead it became product strategy.</p>\n<h2 id=\"hoarder-mode\" style=\"position:relative;\"><a href=\"#hoarder-mode\" aria-label=\"hoarder mode 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>Hoarder mode</h2>\n<p>I am, objectively, the ideal customer for physical media.</p>\n<p>I like shelves. I like cartridges. I like hard drives. I like knowing that if a company wakes up on some random Tuesday and decides to kill a service, merge a platform, rename a plan, or add AI to something that used to just work, my stuff is still my stuff.</p>\n<p>This doesn’t mean I’m living in a bunker made of GameCube discs. I use Google Photos. I pay for Spotify. I stream movies. I live in the real world. But I don’t confuse convenience with control.</p>\n<p>So yes, all my photos are in Google Photos. And yes, all my photos are also stored locally. Because when a company says “don’t worry, it’s in the cloud”, what I hear is “future Pablo, this is your problem now.” (Also: future Pablo, link the degoogling post here when you finally write it.)</p>\n<h2 id=\"concord-was-the-cleanest-version-of-the-problem\" style=\"position:relative;\"><a href=\"#concord-was-the-cleanest-version-of-the-problem\" aria-label=\"concord was the cleanest version of the problem 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>Concord was the cleanest version of the problem</h2>\n<p><a href=\"https://en.wikipedia.org/wiki/Concord_(video_game)\" target=\"_blank\" rel=\"noreferrer\">Concord</a> was such a disaster it almost looped back into being interesting.</p>\n<p>Sony launched it, it bombed, refunded everyone, and then removed the digital version from players’ accounts. Depending on how you experienced it, the install either disappeared from the console or turned into a dead icon you couldn’t do anything with. Same result. A thing people had “bought” stopped existing the moment Sony decided the experiment was over.</p>\n<p>Yes, people got the money back. No, that is not the point.</p>\n<p>The point is that Sony showed, with zero subtlety, what a digital purchase actually is. Not ownership. Permission. Temporary, revocable, account-bound permission.</p>\n<p>Now compare that with <a href=\"https://en.wikipedia.org/wiki/P.T._(video_game)\" target=\"_blank\" rel=\"noreferrer\">P.T.</a>. Konami pulled it from the PlayStation Store after the Kojima breakup, which already sucked, but if you had downloaded it before that, it stayed on your console. People held on to those PS4s like they were carrying cursed treasure. That was bad enough already. Concord managed to make it worse.</p>\n<p>The old version of digital was “you can’t buy it anymore”. The new version is “you can’t even keep the useless dead file you already had”. Amazing progress.</p>\n<h2 id=\"anime-people-already-learned-this-lesson\" style=\"position:relative;\"><a href=\"#anime-people-already-learned-this-lesson\" aria-label=\"anime people already learned this lesson 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>Anime people already learned this lesson</h2>\n<p>Gamers are not even the first victims here. Anime fans got a speedrun version of the same thing when Funimation got folded into Crunchyroll.</p>\n<p>People had digital copies tied to Funimation that simply did not make the trip. Not “please wait while we migrate your library”. Not “we’re sorting out licensing details”. Just gone. Crunchyroll’s own support page says those digital copies are not available there anymore.</p>\n<p>Back in the DVD days, this problem did not exist. You bought the disc, threw away the plastic wrap, put it on a shelf, and that was the end of the negotiation. The publisher could disappear, get acquired, or explode in a corporate merger and your movie would still be sitting there, ready to go.</p>\n<p>That is what ownership looks like. Boring. Reliable. Beautiful.</p>\n<h2 id=\"the-subscription-math-is-fake-good\" style=\"position:relative;\"><a href=\"#the-subscription-math-is-fake-good\" aria-label=\"the subscription math is fake good 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 subscription math is fake-good</h2>\n<p>Game Pass and PS Plus are the kind of deal that sounds amazing until you stop looking at the monthly price and start looking at the years.</p>\n<p>Let’s say you pay around $200 a year for a premium game subscription. In ten years, that is $2,000. And what do you have at the end of it? A receipt history and some fond memories, I guess.</p>\n<p>You do not have a shelf of games. You do not have something you can resell. You do not have the specific 2026 version of that game before three balance patches and two monetization experiments turned it into something else. The second you stop paying, the whole library evaporates.</p>\n<p>People hear this and say “yeah but I don’t replay games”. Fair. Until the service gets worse.</p>\n<p>Because that is the real checkmate here. The trap is not that you personally need to own every movie or every game forever. The trap is that once enough people stop owning things, companies get to make the service worse and you have nowhere good to go. More ads, higher prices, smaller catalog, worse support, more lock-in. The usual enshittification arc.</p>\n<p>If you want a stupidly specific example, look at the 3DS. <a href=\"https://en.wikipedia.org/wiki/Pok%C3%A9mon_Shuffle\" target=\"_blank\" rel=\"noreferrer\">Pokemon Shuffle</a> was a real Nintendo game on a real Nintendo handheld and now, legally, it may as well be smoke for anyone who didn’t grab it in time. <a href=\"https://zelda.fandom.com/wiki/The_Legend_of_Zelda:_Four_Swords_Anniversary_Edition\" target=\"_blank\" rel=\"noreferrer\">The Legend of Zelda: Four Swords Anniversary Edition</a> was free and still managed to become unobtainable. Free. Gone. Nintendo somehow found a way to make zero euros expire.</p>\n<h2 id=\"nintendo-is-now-importing-digital-nonsense-into-physical-media\" style=\"position:relative;\"><a href=\"#nintendo-is-now-importing-digital-nonsense-into-physical-media\" aria-label=\"nintendo is now importing digital nonsense into physical media 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>Nintendo is now importing digital nonsense into physical media</h2>\n<p>Nintendo used to be the company of “blow on the cartridge and try again.” Very physical. Very stupid. Very honest.</p>\n<p>Now we get stuff like <em>Super Mario 3D All-Stars</em> being sold with artificial scarcity so the second-hand price climbs to around €130 for no good reason beyond Nintendo deciding FOMO should have a retail box. I already wrote about how this kind of pricing logic works in the collectibles world <a href=\"/en/blog/collectibles/why-are-some-plastic-toys-so-expensive-a-sneaky-peek-on-how-pricing-works/\">here</a>. It is annoying there too, but at least nobody pretends it is pro-consumer.</p>\n<p>Then came the Switch 2 game-key cards. Nintendo’s own support page literally says the card does not contain the full game data. It is the key that lets you download the actual game.</p>\n<p>So now even when you buy the thing in a store, take the box home, and insert the card in the console, you may still be buying a permission slip. Physical-looking DRM. A cartridge cosplay.</p>\n<p>Nintendo says you only need the internet the first time, which is better than always-online garbage, sure. But the preservation problem is still right there. If the real game lives on a server first, that physical product has an expiration date hiding inside it.</p>\n<p>Cartridges used to be the whole point. Now sometimes they’re just a receipt with extra steps.</p>\n<h2 id=\"your-phone-is-also-turning-into-a-frontend\" style=\"position:relative;\"><a href=\"#your-phone-is-also-turning-into-a-frontend\" aria-label=\"your phone is also turning into a frontend 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>Your phone is also turning into a frontend</h2>\n<p>I miss when phones had features that actually lived inside the phone.</p>\n<p>My old Nokia had Wi-Fi and could do some smart things, but most of the time it was just a solid little brick that worked offline and minded its own business. Now every launch event is full of magical features that depend on some cloud service quietly doing the real work somewhere else.</p>\n<p>That means the feature is not really part of the device. It is part of a service attached to the device.</p>\n<p>The day the company kills that service, moves it behind a subscription tier, or decides only the new model deserves it, your very fancy hardware suddenly forgets how to do the thing from the commercial. Congrats on the purchase. You bought a screen for a server.</p>\n<h2 id=\"i-am-not-a-purist-just-tired\" style=\"position:relative;\"><a href=\"#i-am-not-a-purist-just-tired\" aria-label=\"i am not a purist just tired 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>I am not a purist, just tired</h2>\n<p>I am not telling you to cancel every subscription, throw your phone in a canal, and live off ripped DVDs and local MP3s like it’s 2007. I still make compromises. Everybody does.</p>\n<p>But I do think a few rules still make sense:</p>\n<ul>\n<li>buy physical games when you can</li>\n<li>keep local backups of photos and files you actually care about</li>\n<li>prefer one-time purchases when the product does not genuinely need a monthly bill</li>\n<li>reject digital assets whenever you can</li>\n<li>be suspicious when a company sells “convenience” by removing your options</li>\n</ul>\n<p>Also, physical media is not just better for preservation. It is often better for your wallet. You can buy a used game, finish it, and sell it to the next guy. Try doing that with a PSN license.</p>\n<p>Part 2 is going deeper into software subscriptions specifically, because that rabbit hole gets even dumber. Fitness apps, Adobe, “AI features”, apps charging monthly fees to write text into SQLite, all the usual modern nonsense. I already touched that nerve a bit in my <a href=\"/en/blog/coding/musclog-redesign-nutrition-tracking-and-why-your-fitness-app-subscription-is-a-scam/\">Musclog post about nutrition tracking and scammy fitness app subscriptions</a>, but I have more to say.</p>\n<p>For now, that’s the whole rant: cloud stuff is convenient, subscriptions are sometimes unavoidable, and ownership still matters. Especially when the companies telling you it doesn’t are the exact ones with a financial incentive to make sure you never really keep anything again.</p>\n<p>See you in part 2.</p>","otherLanguagesUrl":["/pt-br/blog/general/voce-nao-tera-nada-e-sera-feliz-parte-1/"],"rss":{"title":"You'll own nothing and you'll be happy - Part 1","description":"You'll own nothing and you'll be happy - Part 1","date":"2026-05-16T00:00:00.000Z"},"images":[null],"videos":[],"comments":[],"googleFormData":{"fvv":1,"pageHistory":0,"fbzx":"8917922801126273338","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_general.jpg/","slug":"/2026/2026-05-16-youll-own-nothing-and-youll-be-happy-part-1.en/","locale":"en","title":"You'll own nothing and you'll be happy - Part 1","previous":{"excerpt":"Uma das side quests mais chatas de publicar um app na Google…","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"},"rawMarkdownBody":"\r\nUma 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.\r\n\r\nLá em 2024, quando eu [escrevi pela primeira vez sobre o Musclog](/pt-br/blog/coding/musclog-aproveitando-minha-experiencia-com-reactjs-para-criar-um-app-em-react-native/), 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.\r\n\r\nFuncionou perfeitamente até o dia em que eu precisei mexer no site de novo.\r\n\r\nScreenshots novas? Outro repo. Texto de feature nova? Outro repo. Atualização de página legal? Outro repo. Ajustezinho de tradução? Outro repo.\r\n\r\nNada 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.\r\n\r\nEntão agora eu não faço mais isso.\r\n\r\nO site do Musclog mora dentro do repo do app. Mesmo projeto com Expo Router. Mesmo deploy. [musclog.app](https://musclog.app/) é o site público, [musclog.app/app](https://musclog.app/app) é o app web de verdade, e Android + iOS continuam saindo da mesma base de código.\r\n\r\nBoa prática? Questionável. Conveniente? Demais.\r\n\r\n![Site público do Musclog renderizado pela mesma base de código com Expo Router do app](../../uploads/blog/2026/04/musclog-shared-router-website-home-1.png)\r\n\r\n## Ok mas por quê?\r\n\r\nBasicamente porque o Expo Router arrancou a minha última desculpa.\r\n\r\nO Musclog já usava [Expo Router](https://expo.github.io/router/docs), então o app já estava organizado em rotas baseadas em arquivos dentro da pasta `app/`. Aí eu percebi que podia simplesmente criar um grupo de rotas chamado `(website)`, mover as páginas públicas pra lá, deixar o app de verdade em `app/app/*`, e parar de fingir que eu estava lidando com dois produtos diferentes.\r\n\r\nO 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:\r\n\r\n- dois PRs pra uma mudança num produto\r\n- dois deploys\r\n- dois lugares pras traduções\r\n- dois lugares pras páginas legais\r\n- dois lugares pra esquecer alguma coisa\r\n\r\nSem condições.\r\n\r\n## A divisão\r\n\r\nA parte engraçada é que o roteamento em si acabou sendo a parte menos amaldiçoada dessa história:\r\n\r\n- `app/app/*` é o Musclog de verdade. Treinos, nutrição, coach com IA, tudo.\r\n- `app/(website)/*` é o site público. Landing page, política de privacidade, termos, contato, calculadora.\r\n\r\nA rota raiz só checa a plataforma e te joga pra onde você pertence:\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\nÉ isso. No web, `/` vira o site. No native, `/` vira o app.\r\n\r\nO site também ganha o próprio layout mais leve:\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\nIsso 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.\r\n\r\nE se algum usuário nativo cair numa rota exclusiva do site, a correção é maravilhosamente direta:\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\nPronto. Volta pros macros.\r\n\r\n![Site público do Musclog renderizado pela mesma base de código com Expo Router do app](../../uploads/blog/2026/04/musclog-shared-router-website-home-2.png)\r\n\r\n## A palhaçada do celular no desktop\r\n\r\nFoi aqui que eu parei de me comportar como uma pessoa normal.\r\n\r\nEu queria que [musclog.app/app](https://musclog.app/app/), 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.\r\n\r\nO detalhe chato era que `/app` precisava parecer um celular, mas `/home` 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.\r\n\r\nEssa lógica mora em `app/+html.tsx`, dentro de um script no `<head>` do documento:\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\nA parte importante é a gambiarra em `pushState` / `replaceState`. A primeira renderização foi tranquila. A navegação client-side é que era a parte chata. Sem isso, dava pra sair de `/app` e continuar com o shell errado ali, como se o Chrome tivesse esquecido em que página estava.\r\n\r\nO shell HTML em si é basicamente o painel da landing, o app roteado, e a moldura do celular:\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\nAí o CSS comete o 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\nSim, 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.\r\n\r\nNas rotas que não são `/app`, o espetáculo inteiro é desligado:\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\nSim, usa `!important`. Isso é CSS de pré-hidratação cujo trabalho inteiro é manter a mentira de pé. Não estamos fazendo arquitetura refinada nessa camada.\r\n\r\n![App web do Musclog no desktop renderizado dentro de uma moldura de celular ao lado da landing](../../uploads/blog/2026/04/musclog-desktop-phone-frame-wrapper.png)\r\n\r\n## Três pequenos crimes\r\n\r\n![Direto pra cadeia. Na hora.](../../uploads/blog/2026/04/straight-to-jail.gif)\r\n\r\nDepois que o shell funcionou, os incômodos menores começaram a aparecer como se tivessem marcado horário.\r\n\r\n### 1. O painel da landing precisava de i18n antes da hidratação\r\n\r\nO 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 `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\nNão é elegante. Também evita jogar um flashbang de texto em inglês numa página em português, então eu vou manter.\r\n\r\n### 2. Assets em HTML cru não recebem a ajuda de sempre do Expo\r\n\r\nO 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:\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\nE como esses arquivos vivem fora do pipeline normal de assets do React Native, eu copio tudo pra `public/` antes de rodar dev e export. Esquece esse passo uma vez e o site imediatamente te lembra quem manda.\r\n\r\n### 3. O `expo export` às vezes precisava de apoio emocional\r\n\r\nEm alguns ambientes, `expo export --platform web` terminava com sucesso e depois só... continuava vivo sem motivo nenhum. Pasta `dist` lá. Arquivos gerados. Processo espiritualmente concluído, tecnicamente ainda pendurado.\r\n\r\nEntão agora existe um script wrapper:\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\nNão é glamouroso... mas resolve.\r\n\r\n## Aí os modais ficaram esquisitos\r\n\r\n![Modal do Musclog no desktop renderizado corretamente dentro da moldura do celular](../../uploads/blog/2026/04/musclog-desktop-modal-inside-phone-frame.png)\r\n\r\nClaro que ficaram.\r\n\r\nQuando o app web mora dentro de um celular fake no desktop, o comportamento padrão de `Modal` no React Native Web começa a ficar ridículo bem rápido. Fazer portal direto pro `document.body` 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.\r\n\r\nEntão eu adicionei um `WebModalShellProvider` lá em cima em `app/app/_layout.tsx`, com um host de overlay dentro do shell do celular:\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\nAí o `Modal.web.tsx` faz aquela coisa óbvia que só parece óbvia depois, alternando entre um portal dentro desse host e o modal normal do RN:\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\nE 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:\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\nIsso resolveu a parte visual. Aí os pointer events decidiram que também queriam atenção.\r\n\r\nNo native, `pointerEvents=\"box-none\"` é normal. No HTML, isso vira `pointer-events: box-none`, 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:\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\nTambém existe um `useLayoutEffect` 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.\r\n\r\n## Eu claramente tenho um padrão\r\n\r\nSe você já lê este blog há algum tempo, nada disso deveria te surpreender.\r\n\r\nEu já [transformei este blog num RPG top-down](/pt-br/blog/coding/eu-criei-um-jogo-para-acessar-o-conteudo-do-meu-blog-com-phaser-e-react/) porque o Konami Code merecia uma recompensa maior do que só um efeitinho de fundo.\r\n\r\nDepois eu [coloquei Stories no blog](/pt-br/blog/coding/meu-blog-tem-stories-agora-mas-nao-me-pergunte-por-que/) 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.\r\n\r\nEntã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.\r\n\r\n## Por que eu vou manter isso\r\n\r\nPassando da piada, isso resolveu um imposto de manutenção bem real.\r\n\r\nAs 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.\r\n\r\nO 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.\r\n\r\nE quando eu lancei o [redesign do Musclog](/pt-br/blog/coding/musclog-redesign-acompanhamento-nutricional-e-por-que-a-assinatura-do-seu-app-de-fitness-e-uma-enganacao/), 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.\r\n\r\n## Conclusão\r\n\r\nEu poderia ter mantido o site num repo separado em Next.js como uma pessoa emocionalmente mais estável? Poderia.\r\n\r\nEu ia continuar fazendo isso depois que o Expo Router deixou esse caminho aberto? Não.\r\n\r\nA 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.\r\n\r\nAinda assim, valeu muito a pena.\r\n\r\nSe quiser fuçar o código, o [Musclog é open-source no GitHub](https://github.com/blopa/musclog-app). Se quiser usar, tá em [musclog.app](https://musclog.app). O site mora dentro do repo do app agora.\r\n\r\nAgora mora todo mundo aqui.\r\n","frontmatter":{"tags":["expo","expo router","react native","musclog","website","monorepo","javascript","typescript"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2026-04-25T00:00:00.000Z","id":null,"path":"site-ou-app-sim-o-guia-gambiarrístico-para-sites-feitos-em-expo-router","show":true,"title":"Site ou App? Sim. O guia gambiarrístico para sites feitos em Expo Router","hideExcerpt":false,"subtitle":"O roteamento foi fácil. O celular fake no navegador foi a parte amaldiçoada."}},"next":{"excerpt":"Eu tenho a mesma biblioteca de fotos no Google Photos e em…","html":"<p>Eu tenho a mesma biblioteca de fotos no <a href=\"https://photos.google.com/\" target=\"_blank\" rel=\"noreferrer\">Google Photos</a> e em um monte de HDs espalhados pela minha casa. Não é um workflow que eu recomendo pra pessoas normais. Isso aqui já é infraestrutura de acumulador.</p>\n<p>Eu faço isso porque eu não confio em “acesso” do mesmo jeito que eu confio em “o arquivo tá aqui na minha mão”. Se você já mudou de país, trocou de ecossistema, ou viu uma empresa de tecnologia engolir a outra, você sabe que essa diferença importa. Conveniência é legal. Propriedade é melhor.</p>\n<p>E ainda assim a indústria inteira passou a última década tentando convencer a gente de que as duas coisas são basicamente iguais. Não são. Aquela frase <a href=\"https://en.wikipedia.org/wiki/You%27ll_own_nothing_and_be_happy\" target=\"_blank\" rel=\"noreferrer\">“you’ll own nothing, and you’ll be happy”</a> supostamente era pra soar futurista. Em vez disso, virou estratégia de produto.</p>\n<h2 id=\"modo-acumulador\" style=\"position:relative;\"><a href=\"#modo-acumulador\" aria-label=\"modo acumulador 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>Modo acumulador</h2>\n<p>Eu sou, objetivamente, o cliente ideal de mídia física.</p>\n<p>Eu gosto de prateleira. Gosto de cartucho. Gosto de HD. Gosto de saber que se uma empresa acordar numa terça-feira qualquer e decidir matar um serviço, fundir uma plataforma, renomear um plano, ou enfiar IA numa coisa que antes simplesmente funcionava, as minhas coisas continuam sendo minhas.</p>\n<p>Isso não quer dizer que eu moro dentro de um bunker feito de discos de GameCube. Eu uso Google Photos. Eu pago Spotify. Eu vejo filme por streaming. Eu vivo no mundo real. Mas eu não confundo conveniência com controle.</p>\n<p>Então sim, todas as minhas fotos estão no Google Photos. E sim, todas as minhas fotos também estão guardadas localmente. Porque quando uma empresa fala “relaxa, tá na nuvem”, o que eu escuto é “Pablo do futuro, esse problema agora é teu.” (Aliás: Pablo do futuro, coloca aqui o link do post de degoogling quando você finalmente escrever isso.)</p>\n<h2 id=\"concord-foi-a-versao-mais-limpa-do-problema\" style=\"position:relative;\"><a href=\"#concord-foi-a-versao-mais-limpa-do-problema\" aria-label=\"concord foi a versao mais limpa do problema 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>Concord foi a versão mais limpa do problema</h2>\n<p><a href=\"https://en.wikipedia.org/wiki/Concord_(video_game)\" target=\"_blank\" rel=\"noreferrer\">Concord</a> foi um desastre tão grande que quase deu a volta e ficou interessante.</p>\n<p>A Sony lançou, o jogo flopou, reembolsou todo mundo e depois removeu a versão digital da conta dos jogadores. Dependendo de como isso bateu no seu console, a instalação ou sumiu de vez ou virou um ícone morto que não servia pra nada. Mesmo resultado. Uma coisa que as pessoas tinham “comprado” deixou de existir no momento em que a Sony decidiu que o experimento tinha acabado.</p>\n<p>Sim, as pessoas receberam o dinheiro de volta. Não, esse não é o ponto.</p>\n<p>O ponto é que a Sony mostrou, com zero sutileza, o que uma compra digital realmente é. Não é propriedade. É permissão. Permissão temporária, revogável, amarrada à sua conta.</p>\n<p>Agora compara isso com <a href=\"https://en.wikipedia.org/wiki/P.T._(video_game)\" target=\"_blank\" rel=\"noreferrer\">P.T.</a>. A Konami tirou o jogo da PlayStation Store depois da treta com o Kojima, o que já foi ruim o bastante, mas se você tinha baixado antes disso, ele continuava no seu console. A galera segurou aqueles PS4 como se estivesse carregando um tesouro amaldiçoado. Já era ruim o suficiente. O Concord conseguiu piorar.</p>\n<p>A versão antiga do digital era “você não consegue mais comprar”. A versão nova é “você nem pode manter o arquivo inútil e morto que já tinha”. Progresso incrível.</p>\n<h2 id=\"o-pessoal-do-anime-ja-aprendeu-essa-licao\" style=\"position:relative;\"><a href=\"#o-pessoal-do-anime-ja-aprendeu-essa-licao\" aria-label=\"o pessoal do anime ja aprendeu essa licao 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>O pessoal do anime já aprendeu essa lição</h2>\n<p>Os gamers nem são as primeiras vítimas aqui. O pessoal do anime recebeu uma versão speedrun da mesma coisa quando a Funimation foi engolida pela Crunchyroll.</p>\n<p>Tinha gente com cópias digitais presas na Funimation que simplesmente não fizeram a viagem. Não foi “aguarde enquanto migramos sua biblioteca”. Não foi “estamos resolvendo detalhes de licenciamento”. Foi só: sumiu. A própria página de suporte da Crunchyroll diz que essas cópias digitais não estão mais disponíveis lá.</p>\n<p>Na época do DVD esse problema não existia. Você comprava o disco, jogava o plástico fora, colocava na estante e pronto, a negociação tinha acabado ali. A distribuidora podia desaparecer, ser comprada, ou explodir numa fusão corporativa qualquer que o filme continuaria ali, esperando por você.</p>\n<p>É isso que propriedade parece. Chato. Confiável. Bonito.</p>\n<h2 id=\"a-matematica-das-assinaturas-parece-boa\" style=\"position:relative;\"><a href=\"#a-matematica-das-assinaturas-parece-boa\" aria-label=\"a matematica das assinaturas parece boa 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 matemática das assinaturas parece boa</h2>\n<p>Game Pass e PS Plus são o tipo de oferta que parece incrível até você parar de olhar o preço mensal e começar a olhar os anos.</p>\n<p>Digamos que você pague uns €200 por ano numa assinatura premium de jogos. Em dez anos, isso dá €2000. E o que você tem no fim? Um histórico de recibos e algumas boas lembranças, eu acho.</p>\n<p>Você não tem uma prateleira de jogos. Você não tem algo que pode revender. Você não tem aquela versão específica de 2016 do jogo, antes de três patches de balanceamento e dois experimentos de monetização transformarem ele em outra coisa. No segundo em que você para de pagar, a biblioteca inteira evapora.</p>\n<p>As pessoas ouvem isso e falam “ah, mas eu não rejogo jogo”. Justo. Até o serviço piorar.</p>\n<p>Porque esse é o xeque-mate de verdade. A armadilha não é que você pessoalmente precisa possuir cada filme ou cada jogo pra sempre. A armadilha é que, quando gente suficiente para de possuir as coisas, as empresas ganham o direito de piorar o serviço e você fica sem um lugar decente pra correr. Mais anúncios, preços maiores, catálogo menor, suporte pior, mais lock-in. O arco clássico da enshittification.</p>\n<p>Se você quiser um exemplo estupidamente específico, olha pro 3DS. <a href=\"https://en.wikipedia.org/wiki/Pok%C3%A9mon_Shuffle\" target=\"_blank\" rel=\"noreferrer\">Pokemon Shuffle</a> era um jogo real da Nintendo num portátil real da Nintendo e agora, legalmente falando, virou fumaça pra quem não pegou a tempo. <a href=\"https://zelda.fandom.com/wiki/The_Legend_of_Zelda:_Four_Swords_Anniversary_Edition\" target=\"_blank\" rel=\"noreferrer\">The Legend of Zelda: Four Swords Anniversary Edition</a> era grátis e ainda assim conseguiu se tornar inalcançável. Grátis. Sumiu. A Nintendo de algum jeito conseguiu dar prazo de validade até pra zero euros.</p>\n<h2 id=\"a-nintendo-agora-esta-importando-a-palhacada-digital-pra-midia-fisica\" style=\"position:relative;\"><a href=\"#a-nintendo-agora-esta-importando-a-palhacada-digital-pra-midia-fisica\" aria-label=\"a nintendo agora esta importando a palhacada digital pra midia fisica 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 Nintendo agora está importando a palhaçada digital pra mídia física</h2>\n<p>A Nintendo costumava ser a empresa do “assopra o cartucho e tenta de novo”. Muito físico. Muito burro. Muito honesto.</p>\n<p>Agora a gente tem coisa tipo <em>Super Mario 3D All-Stars</em> sendo vendido com escassez artificial pro preço de segunda mão subir pra uns €130 sem motivo nenhum além de a Nintendo ter decidido que o FOMO merecia uma caixa de varejo. Eu já escrevi sobre como essa lógica de preço funciona no mundo dos colecionáveis <a href=\"/pt-br/blog/collectibles/por-que-alguns-brinquedos-de-plastico-sao-tao-caros-uma-espiada-nos-bastidores-dos-precos/\">aqui</a>. Lá também é irritante, mas pelo menos ninguém finge que isso é pró-consumidor.</p>\n<p>Depois vieram os game-key cards do Switch 2. A própria página de suporte da Nintendo literalmente diz que o cartão não contém os dados completos do jogo. Ele é a chave que deixa você baixar o jogo de verdade.</p>\n<p>Então agora, mesmo quando você compra a coisa na loja, leva a caixa pra casa e enfia o cartão no console, ainda pode estar comprando um comprovante de permissão. DRM com cara de mídia física. Cosplay de cartucho.</p>\n<p>A Nintendo diz que você só precisa de internet na primeira vez, o que é melhor do que o lixo always-online, beleza. Mas o problema de preservação continua ali, intacto. Se o jogo real mora primeiro num servidor, esse produto físico já vem com uma data de validade escondida dentro dele.</p>\n<p>Cartucho costumava ser o ponto inteiro da coisa. Agora às vezes ele é só um recibo com etapas extras.</p>\n<h2 id=\"seu-celular-tambem-esta-virando-um-frontend\" style=\"position:relative;\"><a href=\"#seu-celular-tambem-esta-virando-um-frontend\" aria-label=\"seu celular tambem esta virando um frontend 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>Seu celular também está virando um frontend</h2>\n<p>Eu sinto falta da época em que os celulares tinham funções que realmente moravam dentro do celular.</p>\n<p>Meu Nokia velho tinha Wi-Fi e conseguia fazer umas coisas espertas, mas na maior parte do tempo ele era só um tijolinho sólido que funcionava offline e cuidava da própria vida. Agora todo evento de lançamento vem cheio de feature mágica que depende de algum serviço na nuvem fazendo o trabalho de verdade em outro lugar.</p>\n<p>Isso quer dizer que a funcionalidade não faz parte do aparelho de verdade. Ela faz parte de um serviço grudado no aparelho.</p>\n<p>No dia em que a empresa matar esse serviço, enfiar ele atrás de um plano de assinatura, ou decidir que só o modelo novo merece a feature, o seu hardware caríssimo vai simplesmente esquecer como fazer a coisa do comercial. Parabéns pela compra. Você comprou uma tela pra um servidor.</p>\n<h2 id=\"eu-nao-sou-purista-so-to-cansado\" style=\"position:relative;\"><a href=\"#eu-nao-sou-purista-so-to-cansado\" aria-label=\"eu nao sou purista so to cansado 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 não sou purista, só tô cansado</h2>\n<p>Eu não tô te mandando cancelar toda assinatura, jogar o celular num canal e viver de DVD ripado e MP3 local como se fosse 2007. Eu também faço concessões. Todo mundo faz.</p>\n<p>Mas eu acho que algumas regras ainda fazem sentido:</p>\n<ul>\n<li>compre jogos físicos quando der</li>\n<li>mantenha backup local das fotos e arquivos com que você realmente se importa</li>\n<li>prefira compra única quando o produto não precisa de verdade de uma cobrança mensal</li>\n<li>rejeite ativos digitais sempre que puder</li>\n<li>desconfie quando uma empresa vende “conveniência” removendo suas opções</li>\n</ul>\n<p>Além disso, mídia física não é só melhor pra preservação. Muitas vezes ela também é melhor pro seu bolso. Você compra um jogo usado, termina, e vende pro próximo cara. Tenta fazer isso com uma licença da PSN.</p>\n<p>A parte 2 vai entrar mais fundo especificamente nas assinaturas de software, porque esse buraco fica ainda mais idiota. App de fitness, Adobe, “features de IA”, aplicativo cobrando mensalidade pra escrever texto em SQLite, todo o pacote clássico da maluquice moderna. Eu já encostei um pouco nesse nervo no meu <a href=\"/pt-br/blog/coding/musclog-redesign-acompanhamento-nutricional-e-por-que-a-assinatura-do-seu-app-de-fitness-e-uma-enganacao/\">post do Musclog sobre acompanhamento nutricional e assinaturas picaretas de apps de fitness</a>, mas ainda tenho mais coisa pra falar.</p>\n<p>Por enquanto, o desabafo é esse: coisa na nuvem é conveniente, assinatura às vezes é inevitável, e propriedade ainda importa. Principalmente quando as empresas te dizendo que isso não importa são exatamente as que têm incentivo financeiro pra garantir que você nunca mais guarde nada de verdade.</p>\n<p>Te vejo na parte 2.</p>","fields":{"postHashId":"Z2VuZXJhbHRydWVudWxsMjAyNi0wNS0xNlQwMDowMDowMC4wMDBa","slug":"/2026/2026-05-16-voce-nao-tera-nada-e-sera-feliz-parte-1.pt-br/","path":"/blog/general/voce-nao-tera-nada-e-sera-feliz-parte-1/","locale":"pt-br"},"rawMarkdownBody":"\r\nEu tenho a mesma biblioteca de fotos no [Google Photos](https://photos.google.com/) e em um monte de HDs espalhados pela minha casa. Não é um workflow que eu recomendo pra pessoas normais. Isso aqui já é infraestrutura de acumulador.\r\n\r\nEu faço isso porque eu não confio em \"acesso\" do mesmo jeito que eu confio em \"o arquivo tá aqui na minha mão\". Se você já mudou de país, trocou de ecossistema, ou viu uma empresa de tecnologia engolir a outra, você sabe que essa diferença importa. Conveniência é legal. Propriedade é melhor.\r\n\r\nE ainda assim a indústria inteira passou a última década tentando convencer a gente de que as duas coisas são basicamente iguais. Não são. Aquela frase [\"you'll own nothing, and you'll be happy\"](https://en.wikipedia.org/wiki/You%27ll_own_nothing_and_be_happy) supostamente era pra soar futurista. Em vez disso, virou estratégia de produto.\r\n\r\n## Modo acumulador\r\n\r\nEu sou, objetivamente, o cliente ideal de mídia física.\r\n\r\nEu gosto de prateleira. Gosto de cartucho. Gosto de HD. Gosto de saber que se uma empresa acordar numa terça-feira qualquer e decidir matar um serviço, fundir uma plataforma, renomear um plano, ou enfiar IA numa coisa que antes simplesmente funcionava, as minhas coisas continuam sendo minhas.\r\n\r\nIsso não quer dizer que eu moro dentro de um bunker feito de discos de GameCube. Eu uso Google Photos. Eu pago Spotify. Eu vejo filme por streaming. Eu vivo no mundo real. Mas eu não confundo conveniência com controle.\r\n\r\nEntão sim, todas as minhas fotos estão no Google Photos. E sim, todas as minhas fotos também estão guardadas localmente. Porque quando uma empresa fala \"relaxa, tá na nuvem\", o que eu escuto é \"Pablo do futuro, esse problema agora é teu.\" (Aliás: Pablo do futuro, coloca aqui o link do post de degoogling quando você finalmente escrever isso.)\r\n\r\n## Concord foi a versão mais limpa do problema\r\n\r\n[Concord](https://en.wikipedia.org/wiki/Concord_(video_game)) foi um desastre tão grande que quase deu a volta e ficou interessante.\r\n\r\nA Sony lançou, o jogo flopou, reembolsou todo mundo e depois removeu a versão digital da conta dos jogadores. Dependendo de como isso bateu no seu console, a instalação ou sumiu de vez ou virou um ícone morto que não servia pra nada. Mesmo resultado. Uma coisa que as pessoas tinham \"comprado\" deixou de existir no momento em que a Sony decidiu que o experimento tinha acabado.\r\n\r\nSim, as pessoas receberam o dinheiro de volta. Não, esse não é o ponto.\r\n\r\nO ponto é que a Sony mostrou, com zero sutileza, o que uma compra digital realmente é. Não é propriedade. É permissão. Permissão temporária, revogável, amarrada à sua conta.\r\n\r\nAgora compara isso com [P.T.](https://en.wikipedia.org/wiki/P.T._(video_game)). A Konami tirou o jogo da PlayStation Store depois da treta com o Kojima, o que já foi ruim o bastante, mas se você tinha baixado antes disso, ele continuava no seu console. A galera segurou aqueles PS4 como se estivesse carregando um tesouro amaldiçoado. Já era ruim o suficiente. O Concord conseguiu piorar.\r\n\r\nA versão antiga do digital era \"você não consegue mais comprar\". A versão nova é \"você nem pode manter o arquivo inútil e morto que já tinha\". Progresso incrível.\r\n\r\n## O pessoal do anime já aprendeu essa lição\r\n\r\nOs gamers nem são as primeiras vítimas aqui. O pessoal do anime recebeu uma versão speedrun da mesma coisa quando a Funimation foi engolida pela Crunchyroll.\r\n\r\nTinha gente com cópias digitais presas na Funimation que simplesmente não fizeram a viagem. Não foi \"aguarde enquanto migramos sua biblioteca\". Não foi \"estamos resolvendo detalhes de licenciamento\". Foi só: sumiu. A própria página de suporte da Crunchyroll diz que essas cópias digitais não estão mais disponíveis lá.\r\n\r\nNa época do DVD esse problema não existia. Você comprava o disco, jogava o plástico fora, colocava na estante e pronto, a negociação tinha acabado ali. A distribuidora podia desaparecer, ser comprada, ou explodir numa fusão corporativa qualquer que o filme continuaria ali, esperando por você.\r\n\r\nÉ isso que propriedade parece. Chato. Confiável. Bonito.\r\n\r\n## A matemática das assinaturas parece boa\r\n\r\nGame Pass e PS Plus são o tipo de oferta que parece incrível até você parar de olhar o preço mensal e começar a olhar os anos.\r\n\r\nDigamos que você pague uns €200 por ano numa assinatura premium de jogos. Em dez anos, isso dá €2000. E o que você tem no fim? Um histórico de recibos e algumas boas lembranças, eu acho.\r\n\r\nVocê não tem uma prateleira de jogos. Você não tem algo que pode revender. Você não tem aquela versão específica de 2016 do jogo, antes de três patches de balanceamento e dois experimentos de monetização transformarem ele em outra coisa. No segundo em que você para de pagar, a biblioteca inteira evapora.\r\n\r\nAs pessoas ouvem isso e falam \"ah, mas eu não rejogo jogo\". Justo. Até o serviço piorar.\r\n\r\nPorque esse é o xeque-mate de verdade. A armadilha não é que você pessoalmente precisa possuir cada filme ou cada jogo pra sempre. A armadilha é que, quando gente suficiente para de possuir as coisas, as empresas ganham o direito de piorar o serviço e você fica sem um lugar decente pra correr. Mais anúncios, preços maiores, catálogo menor, suporte pior, mais lock-in. O arco clássico da enshittification.\r\n\r\nSe você quiser um exemplo estupidamente específico, olha pro 3DS. [Pokemon Shuffle](https://en.wikipedia.org/wiki/Pok%C3%A9mon_Shuffle) era um jogo real da Nintendo num portátil real da Nintendo e agora, legalmente falando, virou fumaça pra quem não pegou a tempo. [The Legend of Zelda: Four Swords Anniversary Edition](https://zelda.fandom.com/wiki/The_Legend_of_Zelda:_Four_Swords_Anniversary_Edition) era grátis e ainda assim conseguiu se tornar inalcançável. Grátis. Sumiu. A Nintendo de algum jeito conseguiu dar prazo de validade até pra zero euros.\r\n\r\n## A Nintendo agora está importando a palhaçada digital pra mídia física\r\n\r\nA Nintendo costumava ser a empresa do \"assopra o cartucho e tenta de novo\". Muito físico. Muito burro. Muito honesto.\r\n\r\nAgora a gente tem coisa tipo *Super Mario 3D All-Stars* sendo vendido com escassez artificial pro preço de segunda mão subir pra uns €130 sem motivo nenhum além de a Nintendo ter decidido que o FOMO merecia uma caixa de varejo. Eu já escrevi sobre como essa lógica de preço funciona no mundo dos colecionáveis [aqui](/pt-br/blog/collectibles/por-que-alguns-brinquedos-de-plastico-sao-tao-caros-uma-espiada-nos-bastidores-dos-precos/). Lá também é irritante, mas pelo menos ninguém finge que isso é pró-consumidor.\r\n\r\nDepois vieram os game-key cards do Switch 2. A própria página de suporte da Nintendo literalmente diz que o cartão não contém os dados completos do jogo. Ele é a chave que deixa você baixar o jogo de verdade.\r\n\r\nEntão agora, mesmo quando você compra a coisa na loja, leva a caixa pra casa e enfia o cartão no console, ainda pode estar comprando um comprovante de permissão. DRM com cara de mídia física. Cosplay de cartucho.\r\n\r\nA Nintendo diz que você só precisa de internet na primeira vez, o que é melhor do que o lixo always-online, beleza. Mas o problema de preservação continua ali, intacto. Se o jogo real mora primeiro num servidor, esse produto físico já vem com uma data de validade escondida dentro dele.\r\n\r\nCartucho costumava ser o ponto inteiro da coisa. Agora às vezes ele é só um recibo com etapas extras.\r\n\r\n## Seu celular também está virando um frontend\r\n\r\nEu sinto falta da época em que os celulares tinham funções que realmente moravam dentro do celular.\r\n\r\nMeu Nokia velho tinha Wi-Fi e conseguia fazer umas coisas espertas, mas na maior parte do tempo ele era só um tijolinho sólido que funcionava offline e cuidava da própria vida. Agora todo evento de lançamento vem cheio de feature mágica que depende de algum serviço na nuvem fazendo o trabalho de verdade em outro lugar.\r\n\r\nIsso quer dizer que a funcionalidade não faz parte do aparelho de verdade. Ela faz parte de um serviço grudado no aparelho.\r\n\r\nNo dia em que a empresa matar esse serviço, enfiar ele atrás de um plano de assinatura, ou decidir que só o modelo novo merece a feature, o seu hardware caríssimo vai simplesmente esquecer como fazer a coisa do comercial. Parabéns pela compra. Você comprou uma tela pra um servidor.\r\n\r\n## Eu não sou purista, só tô cansado\r\n\r\nEu não tô te mandando cancelar toda assinatura, jogar o celular num canal e viver de DVD ripado e MP3 local como se fosse 2007. Eu também faço concessões. Todo mundo faz.\r\n\r\nMas eu acho que algumas regras ainda fazem sentido:\r\n\r\n- compre jogos físicos quando der\r\n- mantenha backup local das fotos e arquivos com que você realmente se importa\r\n- prefira compra única quando o produto não precisa de verdade de uma cobrança mensal\r\n- rejeite ativos digitais sempre que puder\r\n- desconfie quando uma empresa vende \"conveniência\" removendo suas opções\r\n\r\nAlém disso, mídia física não é só melhor pra preservação. Muitas vezes ela também é melhor pro seu bolso. Você compra um jogo usado, termina, e vende pro próximo cara. Tenta fazer isso com uma licença da PSN.\r\n\r\nA parte 2 vai entrar mais fundo especificamente nas assinaturas de software, porque esse buraco fica ainda mais idiota. App de fitness, Adobe, \"features de IA\", aplicativo cobrando mensalidade pra escrever texto em SQLite, todo o pacote clássico da maluquice moderna. Eu já encostei um pouco nesse nervo no meu [post do Musclog sobre acompanhamento nutricional e assinaturas picaretas de apps de fitness](/pt-br/blog/coding/musclog-redesign-acompanhamento-nutricional-e-por-que-a-assinatura-do-seu-app-de-fitness-e-uma-enganacao/), mas ainda tenho mais coisa pra falar.\r\n\r\nPor enquanto, o desabafo é esse: coisa na nuvem é conveniente, assinatura às vezes é inevitável, e propriedade ainda importa. Principalmente quando as empresas te dizendo que isso não importa são exatamente as que têm incentivo financeiro pra garantir que você nunca mais guarde nada de verdade.\r\n\r\nTe vejo na parte 2.\r\n","frontmatter":{"tags":["general","propriedade-digital","jogos","midia-fisica","enshittification"],"categories":["general"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2026-05-16T00:00:00.000Z","id":null,"path":"voce-nao-tera-nada-e-sera-feliz-parte-1","show":true,"title":"Você não terá nada e será feliz - Parte 1","hideExcerpt":false,"subtitle":"A conveniência da nuvem é bonitinha até alguém decidir que sua compra não existe mais"}},"language":"en","intl":{"language":"en","languages":["en","pt-br"],"messages":{"site_title":"pablo.gg","title":"Title","author":"@thepiratepablo","search_placeholder":"Search...","about":"About","photos":"Photos","archive":"Archive","contact":"Contact","close":"Close","contact_page":"Contact page","see_more":"See more posts","built_with":"Built with ","buy_me_a_soda":"Buy me a soda","blog":"Blog","blog_posts":"Blog posts","go_to_post":"Go to post","search":"Search","loading":"Loading...","search_results":"Search results","search_results_for":"{quantity} search results for: \"{query}\"","search_for_query":"Search for \"{query}\"","no_results":"No results","home":"Home","description":"Yet another developer personal blog","go_back":"Go back to the homepage","thats_me":"That's me!","got_it":"Got it!","check_it_out":"Check it out!","we_are":"We are","e3":"E3","away_from_next_sgf":"away from Summer Game Fest 2026","away_from_next_gamescom":"away from Gamescom 2026","sgf_countdown":"Summer Game Fest Countdown","gamescom_countdown":"Gamescom Countdown","e3_paragraph_1":"This page once featured a countdown to the next E3 event, a moment that countless gamers and industry professionals looked forward to each year. E3 was not just an event; it was a celebration of our shared passion for video games, a place where dreams were realized, and memories were made.","e3_paragraph_2":"From the electrifying announcements to the hands-on demos, E3 was the heartbeat of the gaming world. It brought together people from all corners of the globe, united by their love for games. For many, it was a chance to meet their heroes, discover new titles, and experience the thrill of the latest innovations in gaming technology.","e3_paragraph_3":"However, as the gaming landscape has evolved, so too has the way we connect and celebrate our passion. While E3 has come to an end, the spirit of excitement and community it fostered lives on. We now look forward to new ways of coming together, sharing our love for games, and creating new memories.","e3_paragraph_4":"Though the countdown is gone, the legacy of E3 will forever remain in our hearts, reminding us of the incredible journeys we've taken and the bonds we've formed along the way.","sec":"Sec","secs":"Secs","min":"Min","mins":"Mins","hour":"Hour","hours":"Hours","day":"Day","days":"Days","month":"Month","months":"Months","year":"Year","years":"Years","recent_posts":"Recent posts","email":"Email","twitter":"Twitter","name":"Name","page":"Page","fill_this_want_reply":"Fill this in if you want me to contact you back","sorry_this_post_unavailable_language":"Sorry, this post is not available in the language you picked","language":"Language","comment":"Comment","comments":"Comments","no_comments":"No comments yet.","post_comment":"Post comment","send_message":"Send message","message":"Message","post_a_comment":"Post a comment","your_comment_submitted":"Your comment has been successfully submitted.","your_message_submitted":"Your message has been successfully submitted.","on":"on","ok":"Ok","copy":"Copy","copied":"Copied","photo_num":"Photo {num}","the_matrix_has_you":"The Matrix has you...","about_paragraph_1":"A technology enthusiast from an early age, I have always been interested in computers and video games.","about_paragraph_2":"I graduated in Information Technology at Estácio de Sá University and always try to find out about new technologies and get involved in new development projects, some of which the code can be found on GitHub.","cookie_banner_consent":"By using this website you agree to our use of cookies to deliver a better experience.","written_in":"Written in ","no_post_this_tag":"No post in English include this tag.","tags":"Tags","tag_colon":"Tag: ","tags_colon":"Tags: ","posts_tagged":"Posts tagged with ","categories":"Categories","category":"Category","category_colon":"Category: ","posts_on_category":"Posts on category ","related_posts":"Related posts","read_time":"🕒 {time} min. read","create_post":"Create Post","show":"Show","date":"Date","download":"Download","add_tag":"Add Tag","hide_excerpt":"Hide Excerpt","publish_on_medium":"Publish on Medium","allow_comments":"Allow Comments","subtitle":"Subtitle","you_must_be_truly_desperate":"You must be truly desperate to come to me for help","game.game_title":"pablo.gg - The Game","game.next":"Next","game.ok":"Ok","game.loading_asset_colon":"Loading asset:","game.loading":"Loading...","game.characters.npc_01":"Frost","game.characters.npc_02":"Gavin","game.characters.npc_03":"Giles","game.characters.npc_04":"Godfrey","game.characters.npc_05":"Hugh","game.characters.npc_06":"Ivar","game.characters.npc_07":"Leopold","game.characters.npc_08":"Lucian","game.characters.npc_09":"Gumercindo","game.characters.npc_10":"Mr. Flower","game.characters.npc_11":"Maxim","game.characters.npc_12":"Milo","game.characters.npc_13":"Otto","game.characters.npc_14":"Palmer","game.characters.npc_15":"Quentin","game.characters.npc_16":"Sebastian","game.characters.npc_17":"Neville","game.characters.npc_18":"Cassian","game.characters.npc_19":"Balthasar","game.characters.npc_20":"Jasper","game.characters.sign_01":"Sign","game.characters.book_01":"Book","game.characters.home_page_city_sign":"Sign","game.characters.coding_category_city_sign_01":"Sign","game.characters.coding_category_city_sign_02":"Sign","game.characters.events_category_city_sign":"Sign","game.characters.funny_category_city_sign":"Sign","game.characters.gadgets_category_city_sign_01":"Sign","game.characters.gadgets_category_city_sign_02":"Sign","game.characters.games_category_city_sign":"Sign","game.characters.general_category_city_sign":"Sign","game.characters.tips_category_city_sign":"Sign","game.characters.collectibles_category_city_sign":"Sign","game.characters.sword":"Info","game.characters.push":"Info","game.gamepad.a_button":"A Button","game.gamepad.b_button":"B Button","game.gamepad.d_pad_left":"D-Pad Left","game.gamepad.d_pad_up":"D-Pad Up","game.gamepad.d_pad_right":"D-Pad Right","game.gamepad.d_pad_down":"D-Pad Down","game.gamepad.start_button":"Start Button","game.menu.start":"Start","game.menu.exit":"Exit","game.menu.settings":"Settings","game.game_over.game_over":"Game Over","game.game_over.retry":"Retry","game.game_over.exit":"Exit","game.start_menu.save_game":"Save Game","game.start_menu.exit":"Exit","game.browse_posts.choose_a_post":"Choose a Post to read","game.dialogs.npc_01.01":"Hey, you're finally awake!","game.dialogs.npc_01.02":"What, you don't know where you are?","game.dialogs.npc_01.03":"Don't be silly, you're at the Home Page City, remember?","game.dialogs.npc_01.04":"This city was founded by Pablo Montenegro to be the start of your journey","game.dialogs.npc_01.05":"Go explore the world and find other cities where you can read the accumulated knowledge of our civilization...","game.dialogs.npc_01.06":"... some people call it \"Blog Posts\", I don't know why...","game.dialogs.npc_02.01":"Be careful with the Slimes that live in the wild.","game.dialogs.npc_02.02":"Press SPACE to use your sword","game.dialogs.npc_02.03":"What is SPACE? I have no idea.","game.dialogs.npc_03.01":"Hello, welcome to our library","game.dialogs.npc_03.02":"We only have one book, which contains all of this city category publications.","game.dialogs.npc_03.03":"Go check it out!","game.dialogs.npc_04.01":"I like snails","game.dialogs.npc_05.01":"Incomplete sentences may cause some","game.dialogs.npc_06.01":"Red is greener than purple, for sure.","game.dialogs.npc_07.01":"Having a beard is the new not having a beard","game.dialogs.npc_08.01":"Sup","game.dialogs.npc_09.01":"\" - Cooper, what are you doing?\"\n\" - Docking.\"","game.dialogs.npc_10.01":"I should buy a boat","game.dialogs.npc_11.01":"Have you heard the me neither joke? Me neither.","game.dialogs.npc_12.01":"I clean toilet and rescue princesses, good life, yes?","game.dialogs.npc_13.01":"We want the airwaves back","game.dialogs.npc_14.01":"Save the cheerleader, save the world","game.dialogs.npc_15.01":"Hello, how are you?","game.dialogs.npc_15.02":"Ok bye!","game.dialogs.npc_16.01":"A kangaroo is really just a rabbit on steroids","game.dialogs.npc_17.01":"For the 216th time, he said he would quit drinking soda after this last Coke","game.dialogs.npc_18.01":"Nothing and everything is possimpible","game.dialogs.npc_19.01":"For a city called \"Events\", there isn't much going on...","game.dialogs.npc_20.01":"I have heard that there's a way to push some objects in this game, but I don't know how to do it.","game.dialogs.sign_01.01":"Congrats, you can read this!","game.dialogs.book_01.01":"Hey, thanks for trying out this very weird way to access my website","game.dialogs.book_01.02":"This project wouldn't be possible without the awesome open-source work of many people, like:","game.dialogs.book_01.03":"ArMM1998 - For the characters sprites and tilesets","game.dialogs.book_01.04":"PixElthen - For the slime sprites","game.dialogs.book_01.05":"pixelartm - For the pirate hat sprites","game.dialogs.book_01.06":"jkjkke - For the Game Over screen background","game.dialogs.book_01.07":"KnoblePersona - For the Main Menu screen background","game.dialogs.book_01.08":"Min - For the open book sprite","game.dialogs.book_01.09":"And of course, Richard Davey for making Phaser.io!","game.dialogs.home_page_city_sign":"City of Home Page","game.dialogs.coding_category_city_sign.01":"City of Coding Category","game.dialogs.coding_category_city_sign.02":"City of Coding Category","game.dialogs.events_category_city_sign":"City of Events Category","game.dialogs.funny_category_city_sign":"City of Funny Category","game.dialogs.gadgets_category_city_sign.01":"City of Gadgets Category","game.dialogs.gadgets_category_city_sign.02":"City of Gadgets Category","game.dialogs.games_category_city_sign":"City of Games Category","game.dialogs.general_category_city_sign":"City of General Category","game.dialogs.tips_category_city_sign":"City of Tips Category","game.dialogs.collectibles_category_city_sign":"City of Collectibles Category","game.dialogs.sword_item_description":"You can now attack, press SPACE to use your sword.","game.dialogs.push_item_description":"Now you can push some objects, press SPACE in front of a object to use push it.","zelda_timeline.title":"Zelda Timeline","zelda_timeline.timeline_split":"Timeline Split","zelda_timeline.timeline_unification":"Timeline Unification","zelda_timeline.icons_from":"The icons used on this page are from zeldauniverse.net and game-icons.net","zelda_timeline.creation":"Creation","zelda_timeline.creation_of_land_sky":"The Creation of the Land and Sky","zelda_timeline.goddess_hylia_and_sky_era":"Goddess Hylia and Sky Era","zelda_timeline.skyward_sword":"Skyward Sword","zelda_timeline.the_ancient_battle":"The Ancient Battle and the reincarnation of the Goddess Hylia","zelda_timeline.return_to_surface":"Return to the surface","zelda_timeline.era_of_chaos":"Era of Chaos","zelda_timeline.sacred_realm_sealed":"The Sacred Realm is Sealed","zelda_timeline.era_of_prosperity":"Era of Prosperity","zelda_timeline.establishment_of_hyrule":"The Hyrule Kingdom is established","zelda_timeline.force_era":"Force Era","zelda_timeline.the_minish_cap":"The Minish Cap","zelda_timeline.rise_of_evil_vaati":"The Rise of the Evil Vaati","zelda_timeline.four_swords":"Four Swords","zelda_timeline.resurrection_of_vaati":"The Resurrection of Vaati","zelda_timeline.era_of_the_hero_of_time":"Era of the Hero of Time","zelda_timeline.hyrulean_civil_war":"Hyrulean Civil War","zelda_timeline.ocarina_of_time":"Ocarina of Time","zelda_timeline.sacred_realm_becomes_dark_world":"The Sacred Realm Becomes the Dark World","zelda_timeline.ganondorf_becomes_ganon":"Ganondorf becomes Ganon","zelda_timeline.hero_is_defeated":"Hero is defeated","zelda_timeline.decline_of_last_hero":"The Decline of Hyrule and the Last Hero","zelda_timeline.the_imprisoning_war":"The Imprisoning War","zelda_timeline.era_of_dark_and_light":"Era of Light and Dark","zelda_timeline.a_link_to_the_past":"A Link to the Past","zelda_timeline.resurrection_of_ganon":"The Resurrection of Ganon","zelda_timeline.resurrection_of_ganon_is_prevented":"The Resurrection of Ganon is prevented","zelda_timeline.links_awakening":"Link's Awakening","zelda_timeline.oracle_of_ages_and_seasons":"Oracle of Ages and Oracle of Seasons","zelda_timeline.a_link_between_worlds":"A Link Between Worlds","zelda_timeline.tri_force_heroes":"Tri Force Heroes","zelda_timeline.the_gold_era":"The Gold Era","zelda_timeline.monarchs_of_hyrule_use_triforce":"The Monarchs of Hyrule Use the Triforce","zelda_timeline.era_of_decline":"The Era of Decline","zelda_timeline.tragedy_of_princess_zelda_1":"The Tragedy of Princess Zelda I","zelda_timeline.the_legend_of_zelda":"The Legend of Zelda","zelda_timeline.adventure_of_link":"Adventure of Link","zelda_timeline.hero_defeated":"Hero Defeated","zelda_timeline.child_era":"Child Era","zelda_timeline.adult_era":"Adult Era","zelda_timeline.sacred_realm_protected":"The Sacred Realm Remains Protected","zelda_timeline.twilight_realm_and_legacy_of_hero":"The Twilight Realm and the Legacy of the Hero","zelda_timeline.majoras_mask":"Majora's Mask","zelda_timeline.prince_of_thieves_is_executed":"The Demon Thief, Ganondorf, Is Executed","zelda_timeline.twilight_era":"The Twilight Era","zelda_timeline.twilight_princess":"Twilight Princess","zelda_timeline.shadow_invasion":"The Shadow Invasion","zelda_timeline.shadow_era":"The Shadow Era","zelda_timeline.four_swords_adventures":"Four Swords Adventures","zelda_timeline.reincarnation_of_ganondorf":"The Reincarnation of Ganondorf","zelda_timeline.ganondorf_is_sealed":"Ganondorf is sealed","zelda_timeline.hero_of_wind_and_new_world":"The Hero of Wind and a New World","zelda_timeline.era_without_a_hero":"The Era without a Hero","zelda_timeline.ganondorf_is_resurrected":"Ganondorf is Resurrected","zelda_timeline.hyrule_is_sealed_and_flooded":"Hyrule is sealed and then flooded","zelda_timeline.era_of_the_great_sea":"The Era of the Great Sea","zelda_timeline.the_wind_waker":"The Wind Waker","zelda_timeline.era_of_the_great_voyage":"The Era of the Great Voyage","zelda_timeline.phantom_hourglass":"Phantom Hourglass","zelda_timeline.era_of_hyrule_rebirth":"The Era of Hyrule's Rebirth","zelda_timeline.new_continent_discovered":"New Continent Discovered","zelda_timeline.new_hyrule_is_founded":"A New Hyrule Kingdom is founded","zelda_timeline.spirit_tracks":"Spirit Tracks","zelda_timeline.evil_king_malladus_is_resurrected":"The Evil King Malladus is Resurrected","zelda_timeline.age_of_calamity":"Age of Calamity","zelda_timeline.breath_of_the_wild":"Breath of the Wild","zelda_timeline.era_of_the_wilds":"Era of the Wilds","zelda_timeline.calamity_ganon_is_sealed":"Calamity Ganon is Sealed. Technology is banned, leading some Sheikah to form the Yiga Clan","zelda_timeline.divine_beasts_are_cleansed":"The Divine Beasts are cleansed and Calamity Ganon is Sealed","zelda_timeline.tears_of_the_kingdom":"Tears of the Kingdom","zelda_timeline.hyrule_kingdom_is_teared_apart":"Ganondorf is Resurrected (I think?)","blog_categories.games":"Games","blog_categories.general":"General","blog_categories.tips":"Tips","blog_categories.events":"Events","blog_categories.coding":"Coding","blog_categories.funny":"Funny","blog_categories.collectibles":"Collectibles","blog_categories.gadgets":"Gadgets","forty_two_page.title":"Forty Two","forty_two_page.description":"So long and thanks for all the fish!","projects_page.title":"Projects","projects_page.description":"Here is a list of some of my favorites personal projects.","projects_page.gatsbyMaterialUiBlogDescription":"A simple Gatsby Blog Starter with Material UI.","projects_page.contractBuilderDescription":"Contract Builder is a free open-source project that allows anyone to easily maintain and build any kind of contract (legal documents, lawsuit, rent, agreements, construction and so on) using Google Spreadsheets. This was develop as a personal project to help a friend who was struggling spending up to an hour to make a custom contract, now she is able to do it in less than 5 minutes. Hooray!","projects_page.resumeBuilderDescription":"Resume Builder is a free open-source project that allows anyone to easily maintain and build any kind of resume using Google Spreadsheets. This was develop as a personal project to help a friend who was struggling spending up to an hour to make a custom resumes.","projects_page.magentoChatbotDescription":"With this module you can fully integrate your Magento store with the most popular chat apps in the market. This means that by simply installing this module and a few clicks you can have a new way to show and sell your products to your clients. Very easy to use! Try now, it's FREE.","projects_page.jamStackSortenerDescription":"This is a basic URL shortener POC build with Gatsby.","projects_page.gotinhaDescription":"It was always my dream to make my own game, and after trying Unity a couple years ago, I decided to try it again with something I'm more familiar with: Javascript. As a frontend developer, Javascript is already the language that I write most of my code at work and also in my personal projects, and after a quick search I was able to find the amazing PhaserJS Framework for building 2D web games.","e3_2012_photos.title":"E3 2012","e3_2012_photos.description":"On June 2012 I attended E3 as a media press for a full coverage for Nintendo Blast.","e3_2013_photos.title":"E3 2013","e3_2013_photos.description":"On June 2013 I attended E3 as a media press for a full coverage for Game Blast.","e3_2014_photos.title":"E3 2014","e3_2014_photos.description":"On June 2014 I attended E3 as a media press for a full coverage for Game Blast.","e3_2015_photos.title":"E3 2015","e3_2015_photos.description":"On June 2015 I attended E3 as a media press for a full coverage for Game Blast and Game Over TV.","e3_2017_photos.title":"E3 2017","e3_2017_photos.description":"On June 2012 I attended E3 as a media press for a full coverage for PlayReplay and Game Over TV.","e3_2019_photos.title":"E3 2019","e3_2019_photos.description":"On June 2019 I attended E3 as a media press for a full coverage for PlayReplay.","gamescom_2019_photos.title":"Gamescom 2019","gamescom_2019_photos.description":"On August 2019 I attended Gamescom as a media press for a full coverage for PlayReplay.","san_francisco_2019_photos.title":"San Francisco 2019","san_francisco_2019_photos.description":"On September 2019 I've traveled to San Francisco for the Metallica S&M2 concert.","notfound.title":"404: Not found","notfound.header":"404 NOT FOUND","notfound.description":"Sorry, this page doesn't seem to exist. Perhaps the archives are incomplete?","seo_keywords.developer":"developer","seo_keywords.development":"development","seo_keywords.javascript":"javascript","seo_keywords.es6":"es6","seo_keywords.e3":"e3","seo_keywords.sgf":"sgf","seo_keywords.gamescom":"gamescom","seo_keywords.countdown":"countdown","seo_keywords.archive":"archive","seo_keywords.about_me":"about me","seo_keywords.personal_blog":"personal blog","seo_keywords.personal_projects":"personal projects","seo_keywords.travels":"travels","seo_keywords.tips":"tips","seo_keywords.lifehacks":"lifehacks","seo_keywords.reviews":"reviews","seo_keywords.games":"games","seo_keywords.timeline":"timeline","seo_keywords.photos":"photos","cookie_law.we_use_cookies":"We use cookies to ensure you get the best experience on our website. By using our website you agree to our ","cookie_law.title":"Cookie policy","cookie_law.what_are_cookies":"What are cookies?","cookie_law.what_are_cookies_text":"As is common practice with almost all professional websites this site uses cookies, which are tiny files that are downloaded to your computer, to improve your experience. This page describes what information they gather, how we use it and why we sometimes need to store these cookies. We will also share how you can prevent these cookies from being stored however this may downgrade or 'break' certain elements of the sites functionality. For more general information on cookies, please read ","cookie_law.what_are_cookies_more_info_url":"https://en.wikipedia.org/wiki/HTTP_cookie","cookie_law.how_we_use_cookies":"How we use cookies","cookie_law.how_we_use_cookies_text":"We use cookies for a variety of reasons detailed below. Unfortunately in most cases there are no industry standard options for disabling cookies without completely disabling the functionality and features they add to this site. It is recommended that you leave on all cookies if you are not sure whether you need them or not in case they are used to provide a service that you use.","cookie_law.disabling_cookies":"Disabling cookies","cookie_law.disabling_cookies_text":"You can prevent the setting of cookies by adjusting the settings on your browser (see your browser Help for how to do this). Be aware that disabling cookies will affect the functionality of this and many other websites that you visit. Disabling cookies will usually result in also disabling certain functionality and features of the this site. Therefore it is recommended that you do not disable cookies.","cookie_law.the_cookies_we_set":"The cookies we set","cookie_law.site_preferences_cookie":"Site preferences cookies","cookie_law.site_preferences_cookie_text":"In order to provide you with a great experience on this site we provide the functionality to set your preferences for how this site runs when you use it. In order to remember your preferences we need to set cookies so that this information can be called whenever you interact with a page is affected by your preferences.","cookie_law.third_party_cookies":"Third party cookies","cookie_law.third_party_cookies_text":"In some special cases we also use cookies provided by trusted third parties. The following section details which third party cookies you might encounter through this site.","cookie_law.third_party_cookies_item_1":"This site uses Google Analytics which is one of the most widespread and trusted analytics solution on the web for helping us to understand how you use the site and ways that we can improve your experience. These cookies may track things such as how long you spend on the site and the pages that you visit so we can continue to produce engaging content. For more information on Google Analytics cookies, see the official Google Analytics page.","cookie_law.third_party_cookies_item_2":"From time to time we test new features and make subtle changes to the way that the site is delivered. When we are still testing new features these cookies may be used to ensure that you receive a consistent experience whilst on the site whilst ensuring we understand which optimisations our users appreciate the most.","cookie_law.more_information":"More information","cookie_law.more_information_text":"Hopefully that has clarified things for you and as was previously mentioned if there is something that you aren't sure whether you need or not it's usually safer to leave cookies enabled in case it does interact with one of the features you use on our site. However if you are still looking for more information then you can contact us through our "},"routed":true,"originalPath":"/blog/general/youll-own-nothing-and-youll-be-happy-part-1/","redirect":true,"redirectDefaultLanguageToRoot":false,"defaultLanguage":"en","fallbackLanguage":"","ignoredPaths":[]},"blogLocale":"en"}},
    "staticQueryHashes": ["1156153307","1355482417","1591365477","1628619374","2127381735","2288279559","26159077","3566410298","3649515864","3847325417","3982724423","928834867"]}