<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Dev on Build in Public</title><link>https://build.ralphmayr.com/tags/dev/</link><description>Recent content in Dev on Build in Public</description><generator>Hugo</generator><language>en-us</language><copyright>©️ Ralph Mayr 2026</copyright><lastBuildDate>Tue, 07 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://build.ralphmayr.com/tags/dev/index.xml" rel="self" type="application/rss+xml"/><item><title>Supabase is my new favorite database</title><link>https://build.ralphmayr.com/posts/98-supabase-is-my-new-favorite-database/</link><pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/98-supabase-is-my-new-favorite-database/</guid><description>&lt;p&gt;Since the early days of poketto.me, I&amp;rsquo;ve been dissatisfied with my persistence architecture. I started with Google Cloud SQL, but I quickly realized that it was
&lt;a href="../6-cloudsql-is-prohibitively-expensive-at-least-for-small-projects/"&gt;too expensive&lt;/a&gt; for a small app. I switched to Firebase and
&lt;a href="../7-refactoring-legacy-code-let-the-ai-handle-it/"&gt;had Claude do the replatforming&lt;/a&gt;, if you remember. However, when I introduced full-text search, I had to
&lt;a href="../94-bigquerys-search-function-only-works-with-ascii-characters/"&gt;add BigQuery as a second database&lt;/a&gt; just for that because Firebase doesn&amp;rsquo;t have full-text indexing. The point is: It&amp;rsquo;s a lot of headaches for something that should be easy.&lt;/p&gt;</description></item><item><title>Stopping the scrape: Why I switched to the Wikimedia API</title><link>https://build.ralphmayr.com/posts/96-stopping-the-scrape-why-i-switched-to-the-wikimedia-api/</link><pubDate>Sat, 04 Oct 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/96-stopping-the-scrape-why-i-switched-to-the-wikimedia-api/</guid><description>&lt;p&gt;I&amp;rsquo;ve noticed a welcome uptick in users saving Wikipedia articles to poketto.me recently.&lt;/p&gt;
&lt;p&gt;But until now, the app treated Wikipedia just like any other website: it scraped the raw HTML. Turns out, for Wikipedia, that is far from ideal:&lt;/p&gt;
&lt;p&gt;🤯 Artifacts: The extracted content often included UI clutter like &amp;ldquo;Edit&amp;rdquo; buttons, navigation links, and &amp;ldquo;Citation missing&amp;rdquo; tags.&lt;/p&gt;
&lt;p&gt;📋 Rendering issues: The standard HTML → Markdown → HTML conversion pipeline introduced plenty of ugly formatting glitches specific to wikis.&lt;/p&gt;</description></item><item><title>Be careful when counting your whitespace</title><link>https://build.ralphmayr.com/posts/95-be-careful-when-counting-your-whitespace/</link><pubDate>Fri, 03 Oct 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/95-be-careful-when-counting-your-whitespace/</guid><description>&lt;p&gt;One of the great things about working on poketto.me is that I'm constantly learning about fascinating linguistic subtleties. For instance, while working on automatic content summaries and extracting key facts and figures, I came across an interesting issue with token counting in Chinese script.&lt;/p&gt;
&lt;p&gt;I had put a safeguard in place so that poketto.me would only attempt to summarize content longer than 100 words. This works well for German and English content, but when I tested the feature on an article published by Xinhua, a Chinese news agency, my code said the article had only about 12 words, which was obviously incorrect, so it didn't produce a summary.&lt;/p&gt;</description></item><item><title>BigQuery’s SEARCH function only works with ASCII characters</title><link>https://build.ralphmayr.com/posts/94-bigquerys-search-function-only-works-with-ascii-characters/</link><pubDate>Thu, 02 Oct 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/94-bigquerys-search-function-only-works-with-ascii-characters/</guid><description>&lt;p&gt;Admittedly, it may not have been my brightest idea to use &lt;strong&gt;BigQuery&lt;/strong&gt; as the search backend for &lt;strong&gt;poketto.me&lt;/strong&gt;. But since Firebase doesn&amp;rsquo;t have built-in full-text search, I would have needed to add another tool to my stack anyway. I figured BigQuery would be easier than managing something external like &lt;strong&gt;Apache Lucene&lt;/strong&gt; or &lt;strong&gt;Elasticsearch&lt;/strong&gt;. Plus, BigQuery has a built-in &lt;strong&gt;SEARCH(...)&lt;/strong&gt; function, so why not give it a try?&lt;/p&gt;
&lt;p&gt;As it turns out, &lt;strong&gt;SEARCH(...)&lt;/strong&gt; is really more of a token-based text analyzer than a true search function:&lt;br&gt;
1️⃣ It splits both the search term and the text into tokens (words).&lt;br&gt;
2️⃣ It matches &lt;em&gt;full&lt;/em&gt; tokens only.&lt;br&gt;
3️⃣ It returns just &lt;strong&gt;TRUE&lt;/strong&gt; or &lt;strong&gt;FALSE&lt;/strong&gt;&amp;mdash;no offsets, no match lengths.&lt;br&gt;
🤯 And worst of all: &lt;strong&gt;It only works with ASCII.&lt;/strong&gt; Yes. Like it&amp;rsquo;s 1979.&lt;/p&gt;</description></item><item><title>Angular data binding with arrays: Yes, it can work!</title><link>https://build.ralphmayr.com/posts/92-angular-data-binding-with-arrays-yes-it-can-work/</link><pubDate>Tue, 30 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/92-angular-data-binding-with-arrays-yes-it-can-work/</guid><description>&lt;p&gt;Angular is awesome. And data binding, in particular, has been a game changer for developing modern web apps. Something changes somewhere and&amp;mdash;magically&amp;mdash;every part of your UI that needs to respond does so.&lt;/p&gt;
&lt;p&gt;However, I&amp;rsquo;ve always struggled with one corner case: What if you&amp;rsquo;re binding to an array, and the change that occurs is that something gets added or removed, and you need to respond to the change programmatically (not just in your HTML template)?&lt;/p&gt;</description></item><item><title>How to fix the ominous Android Status Bar Issue</title><link>https://build.ralphmayr.com/posts/91-how-to-fix-the-ominous-android-status-bar-issue/</link><pubDate>Mon, 29 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/91-how-to-fix-the-ominous-android-status-bar-issue/</guid><description>&lt;p&gt;Remember
&lt;a href="../28-capacitor-android-status-bar/"&gt;Capacitor + Android Status Bar = 🤯&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;🪲 This bug has haunted me for months&amp;mdash;stalling the Android release of poketto.me and draining way too much mental energy. It&amp;rsquo;s one of those dreaded dev problems: no obvious solution, hard to debug, and endless rabbit holes.&lt;/p&gt;
&lt;p&gt;Eventually, I had to bite the bullet and dig in. Here&amp;rsquo;s what I found:&lt;/p&gt;
&lt;p&gt;💣 Issue #1: The Capacitor status bar plugin only half-works. There&amp;rsquo;s a StatusBar.setOverlaysWebView(true/false) API, but on modern Android versions it doesn&amp;rsquo;t behave as advertised. Why?&lt;/p&gt;</description></item><item><title>The memory consumption patterns of LangChain are… disturbing</title><link>https://build.ralphmayr.com/posts/88-the-memory-consumption-patterns-of-langchain-are-disturbing/</link><pubDate>Fri, 26 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/88-the-memory-consumption-patterns-of-langchain-are-disturbing/</guid><description>&lt;p&gt;As I said in
&lt;a href="../21-no-you-dont-have-to-learn-langchain/"&gt;No, you don&amp;rsquo;t have to learn LangChain&lt;/a&gt;, we shouldn&amp;rsquo;t get distracted by the artificial complexity introduced by our frameworks. LangChain is mostly a wrapper around the REST APIs of various LLM providers. Useful? Yes&amp;mdash;switching between models becomes easy.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s a mystery I can&amp;rsquo;t explain.&lt;/p&gt;
&lt;p&gt;When I added Gemini as a fallback to DeepSeek (see yesterday&amp;rsquo;s post about DeepSeek refusing to touch Chinese politics), I thought it would be straightforward:&lt;/p&gt;</description></item><item><title>DeepSeek really won’t touch anything related to Chinese politics</title><link>https://build.ralphmayr.com/posts/87-deepseek-really-wont-touch-anything-related-to-chinese-politics/</link><pubDate>Thu, 25 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/87-deepseek-really-wont-touch-anything-related-to-chinese-politics/</guid><description>&lt;p&gt;For most use cases in poketto.me, I&amp;rsquo;m pretty happy with #DeepSeek: it&amp;rsquo;s cheap, reliable, and the output quality matches any other LLM I&amp;rsquo;ve tried.&lt;/p&gt;
&lt;p&gt;But there&amp;rsquo;s one big caveat: anything related to Chinese politics can trigger an immediate refusal. Example: Right after the launch of poketto.me, a user tried saving an article about the September 3rd Beijing meeting between Xi Jinping, Vladimir Putin, Kim Jong Un, et al. (
&lt;a href="https://orf.at/stories/3404330/" target="_blank" rel="noopener noreferrer"&gt;https://orf.at/stories/3404330/&lt;/a&gt;)&lt;/p&gt;</description></item><item><title>You can customize the text selection menu in your Android app</title><link>https://build.ralphmayr.com/posts/84-you-can-customize-the-text-selection-menu-in-your-android-app/</link><pubDate>Mon, 22 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/84-you-can-customize-the-text-selection-menu-in-your-android-app/</guid><description>&lt;p&gt;As I mentioned in
&lt;a href="../77-in-place-dom-manipulation-thorny-as-ever/"&gt;In-place DOM manipulation: Thorny as ever&lt;/a&gt;, text selection in browsers can get tricky fast. For poketto.me, the challenge was even bigger: the web app runs inside a hosted WebView in an Android app.&lt;/p&gt;
&lt;p&gt;Mobile text selection already behaves differently from desktop, and the embedding scenario didn&amp;rsquo;t make things easier. After wrestling with a few awkward bugs, though, I found an elegant solution.&lt;/p&gt;
&lt;p&gt;On the web, when a user finishes selecting text, poketto.me immediately converts the selection into a highlight (default color). On Android, however, a tiny &lt;strong&gt;system context menu&lt;/strong&gt; pops up with entries like &lt;em&gt;Copy, Select All, Web Search,&lt;/em&gt; etc. At first this got in the way&amp;mdash;but it turns out you can &lt;strong&gt;customize this menu on a per-activity basis!&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>The Gemini API for Video Understanding is surprisingly good</title><link>https://build.ralphmayr.com/posts/83-the-gemini-api-for-video-understanding-is-surprisingly-good/</link><pubDate>Sun, 21 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/83-the-gemini-api-for-video-understanding-is-surprisingly-good/</guid><description>&lt;p&gt;As I mentioned in
&lt;a href="../82-geminis-url-context-feature-is-90-hype-10-value/"&gt;Gemini&amp;rsquo;s URL Context feature is 90% hype, 10% value&lt;/a&gt;, I was pretty disappointed with Gemini&amp;rsquo;s &amp;ldquo;URL Context&amp;rdquo; feature. But &amp;ldquo;Video Understanding&amp;rdquo;? That one actually works like a charm.&lt;/p&gt;
&lt;p&gt;How it works:&lt;/p&gt;
&lt;p&gt;👉 Provide a YouTube video link&lt;br&gt;
👉 Ask Gemini questions about the video&lt;br&gt;
👉 Get a structured response back&lt;/p&gt;
&lt;p&gt;For poketto.me, this unlocks a really neat feature: users can save any YouTube video in the app and either watch it later &lt;em&gt;or&lt;/em&gt; read a textual description of the video.&lt;/p&gt;</description></item><item><title>Gemini’s “URL Context” feature is 90% hype, 10% value</title><link>https://build.ralphmayr.com/posts/82-geminis-url-context-feature-is-90-hype-10-value/</link><pubDate>Sat, 20 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/82-geminis-url-context-feature-is-90-hype-10-value/</guid><description>&lt;p&gt;I&amp;rsquo;ll admit&amp;mdash;I was pretty excited when Google announced that the Gemini API would support a new &amp;ldquo;URL Context&amp;rdquo; tool. The idea: you could &amp;ldquo;ask&amp;rdquo; Gemini about the content of a specific web page, with Google handling all the heavy lifting.&lt;/p&gt;
&lt;p&gt;The
&lt;a href="https://www.youtube.com/watch?v=4-6WQl-Hls0" target="_blank" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; even shows a neat example: send Gemini two recipe URLs and prompt it to compare ingredients and cooking times. If it worked, this would&amp;rsquo;ve been a game-changer for poketto.me:&lt;/p&gt;</description></item><item><title>There is no cloud (it’s just somebody else’s computer)</title><link>https://build.ralphmayr.com/posts/80-there-is-no-cloud-its-just-somebody-elses-computer/</link><pubDate>Thu, 18 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/80-there-is-no-cloud-its-just-somebody-elses-computer/</guid><description>&lt;p&gt;&amp;hellip;and ultimately, that &amp;ldquo;someone&amp;rdquo; is going to send you a real invoice for actual money. In my case, I&amp;rsquo;m running poketto.me almost entirely on Google Cloud. While it&amp;rsquo;s not terribly expensive right now, it is a cost I had to factor into my pricing strategy.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s the good news: Google offers various programs to support early-stage startups! Check out
&lt;a href="https://cloud.google.com/startup?hl=en" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/startup?hl=en&lt;/a&gt; for details.&lt;/p&gt;
&lt;p&gt;Today, I&amp;rsquo;m happy to share that poketto.me made it into the &amp;ldquo;START&amp;rdquo; tier of that program, which means: no Google Cloud bills in my mailbox for the foreseeable future!&lt;/p&gt;</description></item><item><title>In-place DOM manipulation: Thorny as ever 🥀</title><link>https://build.ralphmayr.com/posts/77-in-place-dom-manipulation-thorny-as-ever/</link><pubDate>Mon, 15 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/77-in-place-dom-manipulation-thorny-as-ever/</guid><description>&lt;p&gt;The &amp;ldquo;highlights&amp;rdquo; feature in the poketto.me reader looks deceptively simple: select a text range, and it gets highlighted in yellow. Clicking a highlight opens a small context menu to change its color or delete it.&lt;/p&gt;
&lt;p&gt;But the implementation? Much thornier than you&amp;rsquo;d think. Why?&lt;/p&gt;
&lt;p&gt;1️⃣ Browser selections are relative to visible text, not the underlying DOM tree of the rendered HTML.&lt;br&gt;
2️⃣ A selection is defined by the text plus its start offset (characters before the selection) and end offset.&lt;br&gt;
3️⃣ To do anything useful with a selection (beyond copying text to the clipboard), you have to map it back to its position in the DOM tree.&lt;br&gt;
4️⃣ That &amp;ldquo;mapping&amp;rdquo; is, to some extent, impossible to get perfectly right. A selection can span multiple nodes, start and end at different levels of depth, and include irrelevant nodes.&lt;/p&gt;</description></item><item><title>Posthog also works well for feature flagging</title><link>https://build.ralphmayr.com/posts/76-posthog-also-works-well-for-feature-flagging/</link><pubDate>Sun, 14 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/76-posthog-also-works-well-for-feature-flagging/</guid><description>&lt;p&gt;Since adopting
&lt;a href="https://posthog.com/" target="_blank" rel="noopener noreferrer"&gt;Posthog&lt;/a&gt; for user analytics at poketto.me, I&amp;rsquo;ve grown pretty fond of the tool. Beyond the
&lt;a href="../68-product-analytics-posthog-is-my-tool-of-choice/"&gt;basics&lt;/a&gt; and
&lt;a href="../72-product-analytics-is-more-than-dau-and-wau/"&gt;advanced insights&lt;/a&gt;, I&amp;rsquo;m now also using it for feature flagging.&lt;/p&gt;
&lt;p&gt;Fine-grained control (and easy rollback) of new features or major changes is becoming increasingly important as the poketto.me user base grows. Why?&lt;/p&gt;
&lt;p&gt;🤕 In a B2C app, you can&amp;rsquo;t count on users to complain when something breaks&amp;mdash;they&amp;rsquo;ll just leave. Especially for early-stage products, every irritated user is a missed opportunity.&lt;/p&gt;</description></item><item><title>Firefox extensions (mostly) work on Firefox on Android as well</title><link>https://build.ralphmayr.com/posts/74-firefox-extensions-mostly-work-on-firefox-on-android-as-well/</link><pubDate>Fri, 12 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/74-firefox-extensions-mostly-work-on-firefox-on-android-as-well/</guid><description>&lt;p&gt;A poketto.me Firefox user recently pointed out that Firefox on Android also supports browser extensions&amp;ndash; but that the poketto.me extension didn&amp;rsquo;t appear on Mozilla&amp;rsquo;s Add-Ons page there.&lt;/p&gt;
&lt;p&gt;Turns out: Any modern browser extension built on the standard APIs (Chrome, Firefox, Edge) can also run in Firefox on Android. But when publishing, you have to explicitly test and target Android.&lt;/p&gt;
&lt;p&gt;Technically, the extension &amp;ldquo;just works&amp;rdquo;&amp;mdash;as long as you&amp;rsquo;re not doing anything exotic with the APIs. But aesthetically, there&amp;rsquo;s one big caveat.&lt;/p&gt;</description></item><item><title>trafilatura’s image extraction is a bit too cautious for my taste</title><link>https://build.ralphmayr.com/posts/73-trafilaturas-image-extraction-is-a-bit-too-cautious-for-my-taste/</link><pubDate>Thu, 11 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/73-trafilaturas-image-extraction-is-a-bit-too-cautious-for-my-taste/</guid><description>&lt;p&gt;A poketto.me user recently filed a curious bug: They had saved a page that clearly contained images &amp;mdash; but in the reader view, no images showed up.&lt;/p&gt;
&lt;p&gt;I expected some quirky HTML. But when I checked, the &amp;lt;img&amp;gt; tags looked perfectly normal (see Exhibit A). Yet, after passing the HTML through trafilatura (which I use to convert HTML to Markdown), the images had simply vanished.&lt;/p&gt;
&lt;p&gt;🔎 The culprit? Trafilatura is very cautious with images. It only accepts &amp;lt;img src=...&amp;gt; URLs that end with a known extension (.jpg, .png, .gif, &amp;hellip;). The site in question served images from URLs without extensions &amp;mdash; so trafilatura just ignored them.&lt;/p&gt;</description></item><item><title>Product analytics is more than DAU and WAU</title><link>https://build.ralphmayr.com/posts/72-product-analytics-is-more-than-dau-and-wau/</link><pubDate>Wed, 10 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/72-product-analytics-is-more-than-dau-and-wau/</guid><description>&lt;p&gt;
&lt;a href="../68-product-analytics-posthog-is-my-tool-of-choice/"&gt;Recently&lt;/a&gt;, I wrote about adopting Posthog for poketto.me. At first, I thought I&amp;rsquo;d use it for the basics:&lt;/p&gt;
&lt;p&gt;📆 Daily &amp;amp; weekly active users (DAU/WAU)&lt;br&gt;
📎 Core events (URLs saved, links shared, etc.)&lt;br&gt;
🚨 Error tracking and alerting&lt;/p&gt;
&lt;p&gt;But then I realized: analytics can do much more. In fact, Posthog replaced one of my home-grown tools &amp;mdash; my &amp;ldquo;podcast heuristic accuracy guestimator.&amp;rdquo; Let me explain.&lt;/p&gt;
&lt;p&gt;When a user adds content to their podcast feed, poketto.me has to gauge three things:&lt;/p&gt;</description></item><item><title>Entitlements are easy (until they’re not)</title><link>https://build.ralphmayr.com/posts/71-entitlements-are-easy-until-theyre-not/</link><pubDate>Tue, 09 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/71-entitlements-are-easy-until-theyre-not/</guid><description>&lt;p&gt;Early-stage products are all about uncertainty. With poketto.me, I started by building something I wanted to use &amp;mdash; and gave it away for free. Then came early adopters asking for features, and eventually I began experimenting with monetizable &amp;ldquo;premium&amp;rdquo; features: personalized podcasts, news aggregation, summaries, contextualization, etc.&lt;/p&gt;
&lt;p&gt;That third bucket quickly gave me headaches. I needed a way to put usage guardrails around these features:&lt;/p&gt;
&lt;p&gt;👉 to separate free from premium,&lt;br&gt;
👉 to distinguish &amp;ldquo;beta&amp;rdquo; from production-ready,&lt;br&gt;
👉 and to keep my future monetization options open.&lt;/p&gt;</description></item><item><title>A room without a dustbin will never be clean</title><link>https://build.ralphmayr.com/posts/70-a-room-without-a-dustbin-will-never-be-clean/</link><pubDate>Mon, 08 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/70-a-room-without-a-dustbin-will-never-be-clean/</guid><description>&lt;p&gt;This is a roundabout way of saying: make &amp;ldquo;good&amp;rdquo; behavior the easy choice. But there&amp;rsquo;s an interesting backstory to the saying.&lt;/p&gt;
&lt;p&gt;I come from a family of manual laborers. My dad, all of my uncles, and most of my many cousins work in trades ranging from carpentry to plumbing to house painting. And one of the first things any good craftsman does when setting up at a new site? Installing a dustbin.&lt;/p&gt;</description></item><item><title>Debugging your Stripe integration is easy with Stripe CLI</title><link>https://build.ralphmayr.com/posts/69-debugging-your-stripe-integration-is-easy-with-stripe-cli/</link><pubDate>Sun, 07 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/69-debugging-your-stripe-integration-is-easy-with-stripe-cli/</guid><description>&lt;p&gt;I put off building checkout and payments for the premium version of poketto.me for way too long. The thought of integrating with Stripe &amp;mdash; keeping &amp;ldquo;their&amp;rdquo; and &amp;ldquo;my&amp;rdquo; data in sync, handling callbacks, connecting the UI, smoothing out user flows&amp;hellip; it always felt like &lt;em&gt;too much&lt;/em&gt; to tackle on any given day.&lt;/p&gt;
&lt;p&gt;But once I committed to a GA date for the premium version, I had to bite the bullet.&lt;/p&gt;</description></item><item><title>Product analytics? PostHog is my tool of choice!</title><link>https://build.ralphmayr.com/posts/68-product-analytics-posthog-is-my-tool-of-choice/</link><pubDate>Sat, 06 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/68-product-analytics-posthog-is-my-tool-of-choice/</guid><description>&lt;p&gt;Event tracking and analytics is one of those cross-cutting topics I mentioned back in
&lt;a href="../27-you-dont-need-to-bring-out-the-big-guns-right-away-but-its-good-to-know-them-anyway/"&gt;You don&amp;rsquo;t need to bring out the big guns right away (but it&amp;rsquo;s good to know them anyway)&lt;/a&gt;: in the beginning, it doesn&amp;rsquo;t really matter if or how you do it. Often, you can just hack something together and move on to more important things. For the longest time, for example, poketto.me just had a hard-coded email notification that let me know whenever a new user signed up. That was enough to give me a general sense of what was going on.&lt;/p&gt;</description></item><item><title>Your browser extension doesn’t necessarily need that many permissions</title><link>https://build.ralphmayr.com/posts/66-your-browser-extension-doesnt-necessarily-need-that-many-permissions/</link><pubDate>Thu, 04 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/66-your-browser-extension-doesnt-necessarily-need-that-many-permissions/</guid><description>&lt;p&gt;The other week, I was building a neat little feature for the poketto.me browser extensions (Chrome, Firefox, and Edge): when you save a web page, the extension should capture not just the URL, but the entire content you&amp;rsquo;re looking at in that moment.&lt;/p&gt;
&lt;p&gt;🤨Why does this matter?&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re signed in on a page (say, with your New York Times subscription), poketto.me can capture the full article &amp;mdash; just as you, the paying subscriber, see it. Otherwise, poketto.me tries to fetch the article in the background&amp;hellip; but it will run straight into the paywall (see TIL #59).&lt;/p&gt;</description></item><item><title>Always suspect your own code first</title><link>https://build.ralphmayr.com/posts/64-always-suspect-your-own-code-first/</link><pubDate>Tue, 02 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/64-always-suspect-your-own-code-first/</guid><description>&lt;p&gt;I physically winced when a beta tester of the poketto.me Android app reported this bug:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Sometimes, adding new tags to a Save doesn&amp;rsquo;t work. I open the tagging dialog, type a new tag, click &amp;lsquo;save&amp;rsquo; &amp;mdash; the input field clears, but the new tag is nowhere to be seen.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I was able to reproduce the issue on my phone &lt;em&gt;and&lt;/em&gt; in an Android emulator.&lt;br&gt;
Gut reaction: &lt;em&gt;&amp;ldquo;Ah, must be the WebView. WebViews always behave differently than a normal browser. This will be a mess.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Social media “previews” are tricky (part 2)</title><link>https://build.ralphmayr.com/posts/63-social-media-previews-are-tricky-part-2/</link><pubDate>Mon, 01 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/63-social-media-previews-are-tricky-part-2/</guid><description>&lt;p&gt;Yesterday, I described my elegant solution to the social media &amp;ldquo;preview&amp;rdquo; problem: Direct the share URL to the Python backend, render a skeleton HTML page with machine-readable metadata, and send real browsers to the Angular app&amp;rsquo;s proper &amp;ldquo;read&amp;rdquo; page.&lt;/p&gt;
&lt;p&gt;But&amp;hellip; how exactly do you redirect the user?&lt;/p&gt;
&lt;p&gt;My first, naive idea was to use the age-old HTML redirect tag:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;meta http-equiv=&amp;quot;refresh&amp;quot; content=&amp;quot;0; url=http://example.com/&amp;quot; \&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;My assumption: social media bots wouldn&amp;rsquo;t follow these &amp;ldquo;semantic&amp;rdquo; redirects, but they would follow HTTP redirects (which they definitely do). This worked perfectly &amp;mdash; except for LinkedIn 😅&lt;/p&gt;</description></item><item><title>Social media “previews” are tricky (part 1)</title><link>https://build.ralphmayr.com/posts/62-social-media-previews-are-tricky-part-1/</link><pubDate>Sun, 31 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/62-social-media-previews-are-tricky-part-1/</guid><description>&lt;p&gt;When you add a link to a website to post on Bluesky, Facebook, or LinkedIn, your post will often contain a &amp;ldquo;preview&amp;rdquo; of the target page. That&amp;rsquo;s great &amp;mdash; it makes the post more appealing and more likely to get clicks. But how do social media platforms fetch the exact content they embed in that preview? That&amp;rsquo;s&amp;hellip; a tiny rabbit hole.&lt;/p&gt;
&lt;p&gt;Enter: the &amp;ldquo;Share&amp;hellip;&amp;rdquo; feature of poketto.me. You can share any saved content with others via poketto.me. Anyone who opens the link gets the same distraction-free reading experience, whether or not they use poketto.me.&lt;/p&gt;</description></item><item><title>Firefox and Chrome (finally!) support the same extension API (and so does Microsoft Edge)</title><link>https://build.ralphmayr.com/posts/61-firefox-and-chrome-finally-support-the-same-extension-api-and-so-does-microsoft-edge/</link><pubDate>Sat, 30 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/61-firefox-and-chrome-finally-support-the-same-extension-api-and-so-does-microsoft-edge/</guid><description>&lt;p&gt;I hadn&amp;rsquo;t actually planned to build a Firefox extension for poketto.me. But among the first wave of &amp;ldquo;Pocket Converts&amp;rdquo; &amp;mdash; users who turned to poketto.me after reading about it as an alternative to Pocket &amp;mdash; several asked for a Firefox extension. So, I decided to do a quick technical feasibility check.&lt;/p&gt;
&lt;p&gt;As it turns out: The Chrome extension works in Firefox without a single code change!&lt;/p&gt;
&lt;p&gt;Apparently, there are a few APIs that Firefox doesn&amp;rsquo;t support (see:
&lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs%29" target="_blank" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs)&lt;/a&gt;, but luckily, I didn&amp;rsquo;t rely on any of those.&lt;/p&gt;</description></item><item><title>Don’t generalize too soon. But do generalize.</title><link>https://build.ralphmayr.com/posts/60-dont-generalize-too-soon-but-do-generalize/</link><pubDate>Fri, 29 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/60-dont-generalize-too-soon-but-do-generalize/</guid><description>&lt;p&gt;It&amp;rsquo;s generally good practice among developers to break things down into small, independent, reusable components. But, like all good things, it can become problematic when taken too far too soon.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example from poketto.me:&lt;/p&gt;
&lt;p&gt;A few weeks ago, I introduced colored tags. Users can choose one of eight colors for each tag to help them quickly find what they&amp;rsquo;re looking for and organize their tags visually. (Personally, I use one color for all location-related tags, another for tech topics, etc.)&lt;/p&gt;</description></item><item><title>The occasional toast can’t hurt</title><link>https://build.ralphmayr.com/posts/58-the-occasional-toast-cant-hurt/</link><pubDate>Wed, 27 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/58-the-occasional-toast-cant-hurt/</guid><description>&lt;p&gt;Some of the designers and frontend devs I&amp;rsquo;ve worked with may remember my rants against toast notifications:&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Why do I need a success message for every action? I expect things to work. Only tell me when something breaks.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;But&amp;hellip; I&amp;rsquo;ll admit it: Sometimes toasts do have their merit.&lt;/p&gt;
&lt;p&gt;Two examples from poketto.me:&lt;/p&gt;
&lt;p&gt;🔹 Copy to clipboard (Podcast feed URL):&lt;/p&gt;
&lt;p&gt;When users click the &amp;ldquo;copy&amp;rdquo; button, the action happens instantly. But without any feedback, it feels&amp;hellip; awkward.&lt;/p&gt;</description></item><item><title>Multi-threaded TTS: A bad idea</title><link>https://build.ralphmayr.com/posts/57-multi-threaded-tts-a-bad-idea/</link><pubDate>Tue, 26 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/57-multi-threaded-tts-a-bad-idea/</guid><description>&lt;p&gt;Running text-to-speech in the cloud is fun&amp;mdash;until it isn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;Early on, I didn&amp;rsquo;t think much about thread safety. During my own testing, rarely would more than one TTS task be running in parallel, so there were no big issues. But once more users started using the feature, strange bugs popped up:&lt;/p&gt;
&lt;p&gt;Errors like &amp;ldquo;Assertion srcIndex &amp;lt; srcSelectDimSize failed&amp;rdquo; started showing up in the logs&amp;mdash;and worse, once triggered, the entire Cloud Run instance would become unusable until a redeploy.&lt;/p&gt;</description></item><item><title>Object.assign(...) in JavaScript helps with asynchronous updates</title><link>https://build.ralphmayr.com/posts/52-object-assign-in-javascript-helps-with-asynchronous-updates/</link><pubDate>Thu, 21 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/52-object-assign-in-javascript-helps-with-asynchronous-updates/</guid><description>&lt;p&gt;In
&lt;a href="../51-websockets-more-than-tech-vanity/"&gt;WebSockets: More than tech vanity&lt;/a&gt;, I talked about the implicit user value of asynchronous UIs. Today, here&amp;rsquo;s a neat little JavaScript trick I hadn&amp;rsquo;t been aware of that makes implementing them a bit easier: &lt;code&gt;Object.assign(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the situation:&lt;/p&gt;
&lt;p&gt;Your Angular frontend initially fetches a list of items from the backend. Thanks to Angular&amp;rsquo;s powerful data binding, many components reference properties of these objects. Later, the backend sends an updated version of one of these items via WebSocket. What now?&lt;/p&gt;</description></item><item><title>WebSockets: More than tech vanity</title><link>https://build.ralphmayr.com/posts/51-websockets-more-than-tech-vanity/</link><pubDate>Wed, 20 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/51-websockets-more-than-tech-vanity/</guid><description>&lt;p&gt;As I said in
&lt;a href="../18-socket-science-isnt-rocket-science-or-websockets-flask-socketio/"&gt;Socket science isn&amp;rsquo;t rocket science&lt;/a&gt;, WebSockets are quite easy to use with Python + Flask + Socket.IO on the backend and Angular + Socket.IO + RxJS on the frontend. But why bother? Dealing with asynchronous requests is more of a hassle than not. And for a &amp;ldquo;boring&amp;rdquo; app like poketto.me, aren&amp;rsquo;t synchronous HTTP requests/responses good enough?&lt;/p&gt;
&lt;p&gt;Here are two use cases where, despite the app&amp;rsquo;s apparent simplicity, asynchronous backend/frontend communication adds real value:&lt;/p&gt;</description></item><item><title>Prompt engineering: A task best left to the machines</title><link>https://build.ralphmayr.com/posts/50-prompt-engineering-a-task-best-left-to-the-machines/</link><pubDate>Tue, 19 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/50-prompt-engineering-a-task-best-left-to-the-machines/</guid><description>&lt;p&gt;Under the hood, poketto.me makes heavy use of LLMs. The podcast feature is a great example: Users can turn any web content into a podcast, but often that content isn&amp;rsquo;t well-suited for listening. LLMs are great at optimizing this&amp;mdash;simplifying complex sentences, turning headlines into enumerations, describing images verbally, etc.&lt;/p&gt;
&lt;p&gt;But the challenge: How do you craft a single, generic prompt that works across all types of content and runs unsupervised via the API?&lt;/p&gt;</description></item><item><title>Extracting Favicons: There’s No Bulletproof Way</title><link>https://build.ralphmayr.com/posts/47-extracting-favicons-theres-no-bulletproof-way/</link><pubDate>Sat, 16 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/47-extracting-favicons-theres-no-bulletproof-way/</guid><description>&lt;p&gt;A &lt;strong&gt;favicon&lt;/strong&gt; is that tiny icon you see next to a site name in your browser tab or bookmarks bar. It's one of those small UX elements that quietly plays a big role in how we recognize and visually differentiate websites.&lt;/p&gt;
&lt;p&gt;In poketto.me, I wanted to bring favicons into play for a couple of UI elements&amp;mdash;particularly when managing your &lt;strong&gt;saved news sources&lt;/strong&gt;. Seeing a little logo beside each source makes skimming, scanning, and organizing much more intuitive than reading domain names alone.&lt;/p&gt;</description></item><item><title>GCS Caching Can Be a Pain in the Neck</title><link>https://build.ralphmayr.com/posts/46-gcs-caching-can-be-a-pain-in-the-neck/</link><pubDate>Fri, 15 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/46-gcs-caching-can-be-a-pain-in-the-neck/</guid><description>&lt;p&gt;I love GCS (Google Cloud Storage). It&amp;rsquo;s a simple, robust, and powerful solution for storing files online and accessing them either programmatically or via HTTP. Storage is dirt cheap&amp;mdash;especially if you don&amp;rsquo;t need global replication or sophisticated backups. And you can even turn a GCS bucket into an HTTPS-secured, internet-facing web server for static websites.
&lt;a href="https://poketto.me" target="_blank" rel="noopener noreferrer"&gt;https://poketto.me&lt;/a&gt;, for example, runs on that architecture.&lt;/p&gt;
&lt;p&gt;Another good use case: the #podcast feature in poketto.me. Naturally, the generated MP3 files need to live somewhere, and storing them in a database or serving them through my Python web server would be&amp;hellip; silly. So I push the generated files to a GCS bucket, and all is well: HTTPS-secured, fast, and compatible with any podcast client in the world.&lt;/p&gt;</description></item><item><title>The ABCD rule doesn’t cut it anymore</title><link>https://build.ralphmayr.com/posts/44-the-abcd-rule-doesnt-cut-it-anymore/</link><pubDate>Wed, 13 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/44-the-abcd-rule-doesnt-cut-it-anymore/</guid><description>&lt;p&gt;There was a time when the golden rule of consumer app development was as simple as &lt;strong&gt;ABCD&lt;/strong&gt;: &lt;strong&gt;Always Be Collecting Data.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The strategy?&lt;br&gt;
1️⃣ Grow your user base as fast as possible.&lt;br&gt;
2️⃣ Track every interaction, every event, every click.&lt;br&gt;
3️⃣ Figure out how to &lt;em&gt;monetize the data&lt;/em&gt; &amp;mdash; usually through targeted advertising, if you couldn&amp;rsquo;t think of anything more creative.&lt;/p&gt;
&lt;p&gt;But that game is changing. Consumers are more privacy-aware than ever. Regulators &amp;mdash; especially in the EU, California, Japan, and a few other regions &amp;mdash; have stepped in. And both founders and investors are realizing that &lt;em&gt;data-harvesting at scale&lt;/em&gt; is not a sustainable or ethical business model.&lt;/p&gt;</description></item><item><title>You Don’t Need an nl2br Pipe</title><link>https://build.ralphmayr.com/posts/43-you-dont-need-an-nl2br-pipe/</link><pubDate>Tue, 12 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/43-you-dont-need-an-nl2br-pipe/</guid><description>&lt;p&gt;This one&amp;rsquo;s a pretty common hassle: You&amp;rsquo;ve got text with line breaks (encoded as \n or sometimes \r\n &amp;mdash; thanks, Microsoft!), but when rendering that text on a webpage, those line breaks are nowhere to be seen. Naturally, browsers collapse &amp;lsquo;source&amp;rsquo; line breaks and ignore them.&lt;/p&gt;
&lt;p&gt;The usual reaction? Reach for an nl2br utility &amp;mdash; like
&lt;a href="https://www.npmjs.com/package/nl2br-pipe" target="_blank" rel="noopener noreferrer"&gt;nl2br-pipe&lt;/a&gt;&amp;mdash;to manually replace \n with &amp;lt;br /&amp;gt; tags. It&amp;rsquo;s a well-known workaround in web dev.&lt;/p&gt;</description></item><item><title>Parsing and Serializing XML Is (Still) a Pain in the Neck</title><link>https://build.ralphmayr.com/posts/40-parsing-and-serializing-xml-is-still-a-pain-in-the-neck/</link><pubDate>Sat, 09 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/40-parsing-and-serializing-xml-is-still-a-pain-in-the-neck/</guid><description>&lt;p&gt;It&amp;rsquo;s 2025, and I still can&amp;rsquo;t believe I have to say this &amp;mdash; but handling XML, especially in Python, remains frustratingly painful.&lt;/p&gt;
&lt;p&gt;Take this example: For the upcoming podcast feature of poketto.me, the app will generate a personalized podcast feed for each user, populated with text-to-speech versions of their saved content. Users can subscribe to their custom feed in any podcast app&amp;mdash;pretty handy.&lt;/p&gt;
&lt;p&gt;The feed itself isn&amp;rsquo;t complex: just an XML file hosted on a web server (in my case, a GCS bucket) containing metadata and links to episode MP3s. It just needs to comply with Apple&amp;rsquo;s
&lt;a href="https://podcasters.apple.com/support/823-podcast-requirements" target="_blank" rel="noopener noreferrer"&gt;Podcast RSS Feed Requirements&lt;/a&gt; so podcast clients can parse it correctly.&lt;br&gt;
Sounds simple, right?&lt;/p&gt;</description></item><item><title>Consistency Beats Accuracy (Part 2)</title><link>https://build.ralphmayr.com/posts/39-consistency-beats-accuracy-part-2/</link><pubDate>Fri, 08 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/39-consistency-beats-accuracy-part-2/</guid><description>&lt;p&gt;The bigger your codebase grows, the more important it becomes to stay consistent &amp;mdash; in naming things and in how you use features of your programming language.&lt;/p&gt;
&lt;p&gt;Take input parameters and variables, for example. Is the ID of a &lt;strong&gt;Save&lt;/strong&gt; object sometimes named save_id, other times saveId, and occasionally just id? Is the function to load it called load_save(...), get_save(...), or fetch_save(...)? Or maybe it&amp;rsquo;s load_save(...) for saves, but get_settings(...) and fetch_tag(...) elsewhere?&lt;br&gt;
If so, confusion is only a matter of time.&lt;/p&gt;</description></item><item><title>Google Requires a New versionCode for Every App Bundle You Publish</title><link>https://build.ralphmayr.com/posts/37-google-requires-a-new-versioncode-for-every-app-bundle-you-publish/</link><pubDate>Wed, 06 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/37-google-requires-a-new-versioncode-for-every-app-bundle-you-publish/</guid><description>&lt;p&gt;When publishing a new version of your Android app through the Google Play Console, you &lt;em&gt;must&lt;/em&gt; increment the versionCode in your app&amp;rsquo;s build.gradle file. This is required even if the versionName (the human-readable version string) stays the same.&lt;/p&gt;
&lt;p&gt;I found this a bit confusing at first &amp;mdash; but the rule is simple: Before you build the bundle you upload to Google Play, make sure you&amp;rsquo;ve updated the versionCode. If you don&amp;rsquo;t, your upload will be rejected immediately.&lt;/p&gt;</description></item><item><title>Mind the Gap — flex-gap!</title><link>https://build.ralphmayr.com/posts/33-mind-the-gap-flex-gap/</link><pubDate>Sat, 02 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/33-mind-the-gap-flex-gap/</guid><description>&lt;p&gt;I use flex-based layouts a lot in the #Angular frontend of poketto.me. But I never realized there&amp;rsquo;s a neat little property called gap (or flex-gap) that lets you define spacing between flex items directly.&lt;/p&gt;
&lt;p&gt;For the longest time, I worked around this with clumsy constructs where I&amp;rsquo;d set margins on child elements &amp;mdash; and then unset them on the last child:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.container {
display: flex;
flex-direction: column;
.child {
margin-bottom: 1rem;
&amp;amp;:last-child {
margin-bottom: unset;
}
}
}
```
When really, the world could be so much simpler:
```
.container {
display: flex;
flex-direction: column;
gap: 1rem;
}
```
No more margin hacks.
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Firebase as very peculiar limits, or: You don’t want to test in production 🙄</title><link>https://build.ralphmayr.com/posts/30-firebase-as-very-peculiar-limits-or-you-dont-want-to-test-in-production/</link><pubDate>Wed, 30 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/30-firebase-as-very-peculiar-limits-or-you-dont-want-to-test-in-production/</guid><description>&lt;p&gt;My &amp;ldquo;user admin&amp;rdquo; section for poketto.me is quite simple: it lists all registered users, shows when they first (and most recently) used the app, and lets me assign &amp;ldquo;entitlements&amp;rdquo; (features like the experimental podcasts and newsfeeds). Works perfectly well on my development instance &amp;mdash; but in production, it kept crashing.&lt;/p&gt;
&lt;p&gt;Turns out: with Firebase, querying documents where a field value is `in` a list of values isn&amp;rsquo;t very smart &amp;mdash; the list may only contain up to **30 (!) values**.&lt;/p&gt;</description></item><item><title>Capacitor + Android Status Bar = 🤯</title><link>https://build.ralphmayr.com/posts/28-capacitor-android-status-bar/</link><pubDate>Mon, 28 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/28-capacitor-android-status-bar/</guid><description>&lt;p&gt;The Android status bar (that is, the small bar on top of the screen that shows the current time, the batter status, and notification icons) is really, really weird.&lt;/p&gt;
&lt;p&gt;It turns out, Capacitor will, by default, make your app a &amp;ldquo;fullscreen&amp;rdquo; app: It&amp;rsquo;ll render your MainActivity (the one that hosts the WebView which in turn hosts your web app) in such a way that the status bar isn&amp;rsquo;t there at all&amp;ndash;on most devices. On some devices, however, the status bar will be visible, but transparently overlay your app 🤨And, to add insult to injury: It&amp;rsquo;ll change the font color of the status bar to white, so that if you rapp comes with a white background, all the user sees are some weird optical artifacts at the top of their screen.&lt;/p&gt;</description></item><item><title>You don’t need to bring out the big guns right away (but it’s good to know them anyway)</title><link>https://build.ralphmayr.com/posts/27-you-dont-need-to-bring-out-the-big-guns-right-away-but-its-good-to-know-them-anyway/</link><pubDate>Sun, 27 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/27-you-dont-need-to-bring-out-the-big-guns-right-away-but-its-good-to-know-them-anyway/</guid><description>&lt;p&gt;It&amp;rsquo;s surprisingly hard to settle on a &amp;ldquo;fit-for-purpose&amp;rdquo; technology and tool stack for a modern SaaS / Cloud app.&lt;/p&gt;
&lt;p&gt;First of all, there are the technical decisions:&lt;/p&gt;
&lt;p&gt;🔀 Which frontend, backend, and persistence stack do you use? Angular vs. React, Java vs. Python, Spring Boot vs. Rails vs. Django vs. Flask, MongoDB vs. Firebase vs. MySQL vs. Postgres&amp;hellip;&lt;/p&gt;
&lt;p&gt;🔀 Do you run it on AWS (Amazon), GCP (Google Cloud), or Azure (Microsoft)?&lt;/p&gt;</description></item><item><title>GMail’s spam filter is pretty weird</title><link>https://build.ralphmayr.com/posts/22-gmails-spam-filter-is-pretty-weird/</link><pubDate>Tue, 22 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/22-gmails-spam-filter-is-pretty-weird/</guid><description>&lt;p&gt;I thought I was being clever when I implemented sign-in via email + one-time token for poketto.me:&lt;/p&gt;
&lt;p&gt;✅ No passwords to store&lt;/p&gt;
&lt;p&gt;✅ No reliance on external login providers like Google or Facebook&lt;/p&gt;
&lt;p&gt;And I had two reliable email services to send those tokens: Hetzner, via their all-inclusive web + mail hosting package I&amp;rsquo;m using for
&lt;a href="https://ralphmayr.com" target="_blank" rel="noopener noreferrer"&gt;ralphmayr.com&lt;/a&gt;, and Zoho Mail, as a lightweight, email-only solution I&amp;rsquo;m using for poketto.me.&lt;/p&gt;
&lt;p&gt;On paper, everything looked fine: The SMTP server accepted the mail, and the message was sent. Simple, right? Wrong&amp;mdash;especially when Gmail is on the receiving end.&lt;/p&gt;</description></item><item><title>No, you don’t have to learn LangChain</title><link>https://build.ralphmayr.com/posts/21-no-you-dont-have-to-learn-langchain/</link><pubDate>Mon, 21 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/21-no-you-dont-have-to-learn-langchain/</guid><description>&lt;p&gt;...or LangGraph, or LlamaIndex, or RAG, or whatever new AI-hype framework is trending this week in order build an AI-powered app.&lt;/p&gt;
&lt;p&gt;More often than not, these frameworks are just &lt;em&gt;wrappers&lt;/em&gt; around basic functionality&amp;mdash;in this case, calling an API. And the layers of abstraction they introduce can make even simple things (&amp;ldquo;prompt an LLM&amp;rdquo;) feel unnecessarily complex.&lt;/p&gt;
&lt;p&gt;Take RAG, for example. All it really does is frontload your prompt with additional context. That&amp;rsquo;s it. In practice, it boils down to concatenating a few strings&amp;mdash;something you can do in five lines of code. But LangChain adds layer upon layer of custom methods, config objects, routing logic, etc., that often just get in the way.&lt;/p&gt;</description></item><item><title>Feedback is key 🔑</title><link>https://build.ralphmayr.com/posts/19-feedback-is-key/</link><pubDate>Sat, 19 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/19-feedback-is-key/</guid><description>&lt;p&gt;Back in my corporate days, I didn&amp;rsquo;t always give feedback the attention it deserves. But building poketto.me as a solo endeavour has reminded me just how crucial it really is.&lt;/p&gt;
&lt;p&gt;Working alone has its perks: you can move fast, make bold decisions, and follow your own vision. But it also comes with pitfalls:&lt;/p&gt;
&lt;p&gt;➡️ You get blindsided by your own past choices&lt;br&gt;
➡️ You can waste time iterating on suboptimal ideas&lt;br&gt;
➡️ You miss what&amp;rsquo;s obvious to others&lt;/p&gt;</description></item><item><title>Socket science isn’t rocket science! 🚀 Or: WebSockets + Flask + SocketIO = ❤️</title><link>https://build.ralphmayr.com/posts/18-socket-science-isnt-rocket-science-or-websockets-flask-socketio/</link><pubDate>Fri, 18 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/18-socket-science-isnt-rocket-science-or-websockets-flask-socketio/</guid><description>&lt;p&gt;For some reason, async communication between a web app backend and the browser causes more anxiety than it should (guilty 🙋). I&amp;rsquo;ve had my fair share of headaches with WebSocket frameworks before, but for poketto.me, I tried a much simpler stack&amp;mdash;and was pleasantly surprised:&lt;/p&gt;
&lt;p&gt;➡️http backend: &lt;strong&gt;Flask&lt;/strong&gt; (
&lt;a href="https://flask.palletsprojects.com/en/stable/" target="_blank" rel="noopener noreferrer"&gt;https://flask.palletsprojects.com/en/stable/&lt;/a&gt;)&lt;br&gt;
➡️websocket backend: &lt;strong&gt;Flask-Socketio&lt;/strong&gt; (
&lt;a href="https://flask-socketio.readthedocs.io/en/latest/" target="_blank" rel="noopener noreferrer"&gt;https://flask-socketio.readthedocs.io/en/latest/&lt;/a&gt;)&lt;br&gt;
➡️websocket frontend: &lt;strong&gt;socketio-client&lt;/strong&gt; (
&lt;a href="https://www.npmjs.com/package/socket.io-client" target="_blank" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/socket.io-client&lt;/a&gt;)&lt;/p&gt;
&lt;h4 id="-backend-handling-connections-is-dead-simple"&gt;&lt;strong&gt;🔌 Backend: Handling connections is dead simple&lt;/strong&gt;&lt;/h4&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;@socketio.on(&amp;#39;connect&amp;#39;)
def handle_connect():
_, user_id = authenticate()
sid = request.sid
connected_clients[user_id] = sid
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="-sending-data-to-a-connected-client"&gt;&lt;strong&gt;📤 Sending data to a connected client&lt;/strong&gt;&lt;/h4&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sid = connected_clients.get(user_id)\
if sid:
socketio.emit(&amp;#39;message&amp;#39;, {
&amp;#39;message_type&amp;#39;: &amp;#39;save_changed&amp;#39;,
&amp;#39;data&amp;#39;: {
&amp;#39;id&amp;#39;: save_id,
&amp;#39;state&amp;#39;: &amp;#39;archived&amp;#39;
}
}, to=sid)
#### **🖥️ Frontend: Receiving messages is just as easy**
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;const socket = io(environment.WS_BASE_URL, {&lt;/p&gt;</description></item><item><title>Designing for mobile after the fact is painful</title><link>https://build.ralphmayr.com/posts/17-designing-for-mobile-after-the-fact-is-painful/</link><pubDate>Thu, 17 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/17-designing-for-mobile-after-the-fact-is-painful/</guid><description>&lt;p&gt;I&amp;rsquo;ll admit it: when I built the frontend for [poketto.me, I focused mostly on desktop browsers&amp;mdash;except for the reading experience, which was always meant to be mobile-first. But optimizing the rest of the UI for smaller screens later on? A &lt;strong&gt;real hassle&lt;/strong&gt;&amp;mdash;and one that could&amp;rsquo;ve been avoided had I truly embraced &lt;em&gt;mobile-first&lt;/em&gt; design from the start.&lt;/p&gt;
&lt;p&gt;Here are a few key lessons the hard way taught me:&lt;/p&gt;
&lt;p&gt;📱 *&lt;em&gt;Thumb-friendly design matters*&lt;/em&gt;
The
&lt;a href="https://www.zilliondesigns.com/blog/infographics/mobile-app-design-thumb-friendly/" target="_blank" rel="noopener noreferrer"&gt;Rule of Thumbs&lt;/a&gt; is real:&lt;/p&gt;</description></item><item><title>Material’s tooltip interferes with touch-scrolling on mobile</title><link>https://build.ralphmayr.com/posts/16-materials-tooltip-interferes-with-touch-scrolling-on-mobile/</link><pubDate>Wed, 16 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/16-materials-tooltip-interferes-with-touch-scrolling-on-mobile/</guid><description>&lt;p&gt;As I mentioned the other day, I love Angular Material. However, I was quite surprised to discover that the matTooltip directive can negatively impact the user experience on mobile 🤯&lt;/p&gt;
&lt;p&gt;I noticed that users couldn&amp;rsquo;t 'drag to scroll' on poketto.me on their phones (in both the Android app and the browser) when they started to drag particular elements on the screen, such as the save title images.&lt;/p&gt;
&lt;p&gt;After a lot of analysis and debugging, I discovered that the matTooltip directive, which I use to render tooltips displaying the full save title in case it is abbreviated, sets touch-action: none; on that element. This means: On a mobile device, users can't start scrolling the page with that element as an anchor. It feels totally weird!&lt;/p&gt;</description></item><item><title>Cloud Build beats GitLab CI for my use case.</title><link>https://build.ralphmayr.com/posts/15-cloud-build-beats-gitlab-ci-for-my-use-case/</link><pubDate>Tue, 15 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/15-cloud-build-beats-gitlab-ci-for-my-use-case/</guid><description>&lt;p&gt;When I set up my personal blog,
&lt;a href="https://ralphmayr.com" target="_blank" rel="noopener noreferrer"&gt;ralphpmayr.com&lt;/a&gt;, years ago, I opted for a GitLab CI pipeline to build and deploy it. However, for poketto.me, I was looking for something faster, cheaper and more closely integrated with the Google Cloud ecosystem.&lt;/p&gt;
&lt;p&gt;I opted for Google's own CloudBuild and discovered that it integrates seamlessly with GitLab.com. In GitLab, all you need to do is create two API keys (one for read access and one for edit access), configure these in Google Cloud and CloudBuild will then be able to fetch and build any GitLab project.&lt;/p&gt;</description></item><item><title>The process to get an app into Google Play is… byzantine</title><link>https://build.ralphmayr.com/posts/14-the-process-to-get-an-app-into-google-play-is-byzantine/</link><pubDate>Mon, 14 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/14-the-process-to-get-an-app-into-google-play-is-byzantine/</guid><description>&lt;p&gt;🏗️First, of course, you actually got to build your app. Then you register at the &lt;strong&gt;Google Play Console&lt;/strong&gt; (and fork over $25). Then you provide your name, contact details, etc. and then the fun starts: You need official ID (drivers license, passport), proof of residence (&amp;ldquo;Meldezettel&amp;rdquo; in Austria), proof that you own an Android device, install the Google Play Console App on that device, and verify your contact phone number.&lt;/p&gt;</description></item><item><title>"Token used too early" — the weirdest Google Sign-In error.</title><link>https://build.ralphmayr.com/posts/13-token-used-too-early-the-weirdest-google-sign-in-error/</link><pubDate>Sun, 13 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/13-token-used-too-early-the-weirdest-google-sign-in-error/</guid><description>&lt;p&gt;I&amp;rsquo;ve had my ups and downs with &amp;ldquo;Sign in with Google&amp;rdquo;:&lt;/p&gt;
&lt;p&gt;⬆️It&amp;rsquo;s simple and works well on the web&lt;/p&gt;
&lt;p&gt;⬇️It&amp;rsquo;s a complete hassle inside a hosted WebView in a native mobile app&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s something really funny: I local clock is &lt;em&gt;ahead&lt;/em&gt; of Google's &amp;mdash; sometimes by as little as a few milliseconds &amp;ndash; the sign in call will fail with &amp;ldquo;Token used too early.&amp;rdquo; 🤯&lt;/p&gt;
&lt;p&gt;✅ Solution: resync your system clock (in my case, my MacBook with Apple's time server).&lt;/p&gt;</description></item><item><title>WebView ↔ Native app communication is easier than you think.</title><link>https://build.ralphmayr.com/posts/12-webview-native-app-communication-is-easier-than-you-think/</link><pubDate>Sat, 12 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/12-webview-native-app-communication-is-easier-than-you-think/</guid><description>&lt;p&gt;Even without relying on the utilities provided by Capacitor, you can easily pass data back and forth between your web app and its native counterpart:&lt;/p&gt;
&lt;p&gt;➡️In JavaScript / TypeScript you can register global functions on the window object. The native code can call these via executeJavaScript() on the WebView.&lt;/p&gt;
&lt;p&gt;➡️In the other direction, the native code can register a JavaScript interface on the&lt;/p&gt;
&lt;p&gt;WebView:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;webView.addJavascriptInterface(new Object() {
@JavascriptInterface
public void onSessionId(String value) {
SaveUrlHandler.instance.setSessionId(value);
}
@JavascriptInterface
public void closeApp() {
MainActivity.this.finish();
}
}, &amp;#34;Android&amp;#34;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;which the JavaScript / TypeScript code can call whenever it wants:&lt;/p&gt;</description></item><item><title>Running text-to-speech in the #Cloud is harder than you would think (part three)</title><link>https://build.ralphmayr.com/posts/10-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-three/</link><pubDate>Thu, 10 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/10-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-three/</guid><description>&lt;p&gt;So, after finally setting up a dedicated virtual machine (VM) to run my text-to-speech workloads and wiring up all the build and deployment scripts, I got a bit excited. Could I reduce the TTS latency even further if the VM had GPU power?&lt;/p&gt;
&lt;p&gt;In theory: Yes. In practice: Google doesn't give you access to their GPUs straight away. There&amp;rsquo;s a special quota setting for VM instances with GPUs, and by default that&amp;rsquo;s set to zero. As a regular user, you cannot increase this without contacting Google Cloud Support.&lt;/p&gt;</description></item><item><title>Running text-to-speech in the #Cloud is harder than you would think (part two)</title><link>https://build.ralphmayr.com/posts/9-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-two/</link><pubDate>Wed, 09 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/9-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-two/</guid><description>&lt;p&gt;Do you remember when I mentioned the difficulty of running 🐸 CoquiTTS in the cloud yesterday? My first experiment was to run it directly in my Cloud Run backend service. In theory, this could have worked, but you'll never guess why it failed in practice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;x86 CPUs&lt;/strong&gt;. Really. Like the ones we had in our computers in the 90s. How did I figure this out? After taking a horribly long time to start up, the TTS service failed with a message saying that it was running on an 'incompatible' CPU architecture. Specifically, 32-bit x86 CPUs.&lt;/p&gt;</description></item><item><title>Running text-to-speech in the #Cloud is harder than you would think (part one)</title><link>https://build.ralphmayr.com/posts/8-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-one/</link><pubDate>Tue, 08 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/8-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-one/</guid><description>&lt;p&gt;For the podcast automation feature that I&amp;rsquo;m planning for a future version of poketto.me, I&amp;rsquo;ve been experimenting with various text-to-speech solutions. The easiest and highest-quality approach would have been the ElevenLabs API. However, considering the &amp;ldquo;throwaway&amp;rdquo; nature of these audio files &amp;ndash; most of which would only be listened to once by one person &amp;ndash; and the cost structure that this would introduce, I desperately need a cheaper approach.&lt;/p&gt;
&lt;p&gt;The Python library 🐸 CoquiTTS is pretty awesome: There are many different models to choose from, ranging from 'super low latency' to 'high quality' (including voice cloning). Therefore, poketto.me users could choose from many different voices, and from a commercial perspective, I could set different price points for different levels of quality and latency. However, they all require significant computing power to function.&lt;/p&gt;</description></item><item><title>Refactoring “legacy” code? Let the AI handle it!</title><link>https://build.ralphmayr.com/posts/7-refactoring-legacy-code-let-the-ai-handle-it/</link><pubDate>Mon, 07 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/7-refactoring-legacy-code-let-the-ai-handle-it/</guid><description>&lt;p&gt;For reasons outlined in yesterday's post, I had to switch poketto.me from #CloudSQL (MySQL) to a completely different database architecture: Firebase 🔥&lt;/p&gt;
&lt;p&gt;At that stage, the Python backend code base wasn&amp;rsquo;t huge, but it was already fairly substantial. It included CRUD operations for several entities, as well as some basic lookup logic. Rewriting the whole thing would have taken me at least half a day.&lt;/p&gt;
&lt;p&gt;Instead, I asked my good friend #Claude to take care of things. And, to my surprise, the result worked straight away! 🎁The &amp;ldquo;dorp in&amp;rdquo; replacement generated by the AI immediately passed my unit tests, and also the chatbot&amp;rsquo;s instructions for how to set up and configure #Firebase were actually useful.&lt;/p&gt;</description></item><item><title>CloudSQL is prohibitively expensive (at least for small projects)</title><link>https://build.ralphmayr.com/posts/6-cloudsql-is-prohibitively-expensive-at-least-for-small-projects/</link><pubDate>Sun, 06 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/6-cloudsql-is-prohibitively-expensive-at-least-for-small-projects/</guid><description>&lt;p&gt;When I started setting up the cloud infrastructure for &lt;strong&gt;poketto.me&lt;/strong&gt;, I didn&amp;rsquo;t give much thought to costs. I thought it was such a small project that it just wouldn&amp;rsquo;t matter. I launched a #&lt;strong&gt;CloudSQL&lt;/strong&gt; (MySQL) database with pretty much the default settings and was quite happy with it &amp;ndash; until I checked the billing dashboard a couple of days later and realised that I was already spending almost €4 per day on the database alone. 120 euros per month just for a few MySQL tables? That couldn&amp;rsquo;t be right.&lt;/p&gt;</description></item><item><title>There’s no “npx cap remove” 🤦‍♂️</title><link>https://build.ralphmayr.com/posts/5-theres-no-npx-cap-remove/</link><pubDate>Sat, 05 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/5-theres-no-npx-cap-remove/</guid><description>&lt;p&gt;#Capacitor comes with a user-friendly command line interface. To add a new mobile platform to your project, simply run &amp;ldquo;&lt;strong&gt;npx cap add [android | ios]&lt;/strong&gt;&amp;rdquo;. And to remove one? Exactly &amp;mdash; you guessed it: &lt;strong&gt;'npx cap remove&lt;/strong&gt;...' But: That command isn&amp;rsquo;t implemented &amp;ndash; for understandable reasons. The interesting thing is, though, that it's &amp;quot;plausible&amp;quot; that it would be there, right?. So it's not surprising that #Claude insists it exists.&lt;/p&gt;
&lt;p&gt;This once again highlights a major issue with LLMs that I just can&amp;rsquo;t shut up about: Just because what the chatbot says sounds 'plausible' doesn't mean it's correct. In the case of AI-assisted coding, that&amp;rsquo;s not such a big deal &amp;ndash; you, the developer, will eventually realise that the AI was wrong. But what about the many other use cases where we blindly trust the AI and put whatever it says into action? 🤔&lt;/p&gt;</description></item><item><title>Multi-threaded webservers in Python: A rabbit hole you don’t want to get into.</title><link>https://build.ralphmayr.com/posts/4-multi-threaded-webservers-in-python-a-rabbit-hole-you-dont-want-to-get-into/</link><pubDate>Fri, 04 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/4-multi-threaded-webservers-in-python-a-rabbit-hole-you-dont-want-to-get-into/</guid><description>&lt;p&gt;Put simply, serving web requests properly in Python is not easy. poketto.me uses a fairly basic off-the-shelf stack (#&lt;strong&gt;Flask&lt;/strong&gt; as a web framework and #&lt;strong&gt;SocketIO&lt;/strong&gt; for websocket communication), and you would never guess the issues you could encounter with it. For starters, Flask comes with a built-in web server (&amp;ldquo;werkzeug&amp;rdquo;), which is convenient for development, but absolutely not for production (it even warns you in bright red).&lt;/p&gt;
&lt;p&gt;🧵It runs on a single thread &amp;ndash; I'm not kidding. This means it can only handle one web request at a time. If that request involves any actual work, such as extracting web content, it cannot handle any other requests at the same time.&lt;/p&gt;</description></item><item><title>Building a Chrome Extension is easier than I thought — but still a bit of a hassle</title><link>https://build.ralphmayr.com/posts/3-building-a-chrome-extension-is-easier-than-i-thought-but-still-a-bit-of-a-hassle/</link><pubDate>Thu, 03 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/3-building-a-chrome-extension-is-easier-than-i-thought-but-still-a-bit-of-a-hassle/</guid><description>&lt;p&gt;To my surprise, building a Chrome Extension is &lt;em&gt;technically&lt;/em&gt; quite straightforward. You need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A bit of HTML (the popup UI)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A bit of JavaScript (your logic)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;And a $5 one-time fee to publish in the Chrome Web Store&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The
&lt;a href="https://developer.chrome.com/docs/extensions/reference/api" target="_blank" rel="noopener noreferrer"&gt;Chrome Extensions API&lt;/a&gt; is pretty minimal, but it covers all the basics:&lt;/p&gt;
&lt;p&gt;✅ You can persist data (e.g. auth tokens)&lt;br&gt;
✅ You can query open tabs, grab the current URL&lt;br&gt;
✅ You can even inject JavaScript into pages &amp;mdash; though this last bit triggers stricter review from Google, especially if you request broad access (like &amp;quot;&amp;lt;all_urls&amp;gt;&amp;quot;).&lt;/p&gt;</description></item><item><title>Extracting web content is still… messy.</title><link>https://build.ralphmayr.com/posts/2-extracting-web-content-is-still-messy/</link><pubDate>Wed, 02 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/2-extracting-web-content-is-still-messy/</guid><description>&lt;p&gt;You can talk about autonomous #AIAgents roaming the web and performing all kinds of tasks 'just as a human would' as much as you like, but technically, some of the very basics are still lacking. For example, there is no free, easy-to-use, off-the-shelf solution for extracting web content.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what I mean: Think of poketto.me as a very basic 'agent': you tell it to save a URL, and then it talks to the website 'on your behalf' to access its content. For this use case, the &lt;strong&gt;Newspaper3k&lt;/strong&gt; Python library is pretty good: it teases out structured metadata, but occasionally misses basic things like the content language. To retrieve the actual content, &lt;strong&gt;Trafilatura&lt;/strong&gt; (
&lt;a href="https://github.com/adbar/trafilatura" target="_blank" rel="noopener noreferrer"&gt;https://github.com/adbar/trafilatura&lt;/a&gt;) appears to be the best option at the moment. However, even that doesn't work well with all sites. For edge cases, I actually had to fall back on parsing the raw HTML myself using Beautiful Soup (
&lt;a href="https://beautiful-soup-4.readthedocs.io/en/latest/%29" target="_blank" rel="noopener noreferrer"&gt;https://beautiful-soup-4.readthedocs.io/en/latest/)&lt;/a&gt;. (And yes, I&amp;rsquo;m sending that through an LLM later to streamline the content so all the tiny formatting issues Trafilatura introduces get smoothed out again.)&lt;/p&gt;</description></item><item><title>Hybrid apps &amp; social logins: tread carefully.</title><link>https://build.ralphmayr.com/posts/1-hybrid-apps-social-logins-tread-carefully/</link><pubDate>Tue, 01 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/1-hybrid-apps-social-logins-tread-carefully/</guid><description>&lt;p&gt;Remember when I talked about &amp;ldquo;Sign in with Google?&amp;rdquo; That works really well on the web, but: Once you&amp;rsquo;re wrapping your web app into a native mobile app, things turn very ugly very soon. In a hosted WebView, Google won&amp;rsquo;t let you render the sign in button to begin with &amp;ndash; unless you override the WebView&amp;rsquo;s user agent string. After that, you&amp;rsquo;re still screwed if you rely on the default sign in workflow as this would open a popup window from which the user then can&amp;rsquo;t navigate back into your app.&lt;/p&gt;</description></item></channel></rss>