<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Junat.live blog]]></title><description><![CDATA[ Empowering thousands for seamless transit. Learn about Junat.live, the real-time Finnish train schedule site.]]></description><link>https://blog.junat.live/</link><image><url>https://blog.junat.live/favicon.png</url><title>Junat.live blog</title><link>https://blog.junat.live/</link></image><generator>Ghost 5.75</generator><lastBuildDate>Fri, 08 May 2026 18:40:53 GMT</lastBuildDate><atom:link href="https://blog.junat.live/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Introducing the new geolocation experience]]></title><description><![CDATA[<p>Junat.live now provides a better experience for users using the geolocation feature to navigate to nearby stations. Previously the geolocation button would assume you want to go to the nearest station, which would sometimes lead to confusion. The new version makes less assumptions, but is still snappy if the</p>]]></description><link>https://blog.junat.live/introducing-the-new-geolocation-experience/</link><guid isPermaLink="false">65d3229bc0115300015880c1</guid><dc:creator><![CDATA[Jasper Nykänen]]></dc:creator><pubDate>Mon, 19 Feb 2024 10:05:28 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1580064339631-c938765a976f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fHZyJTIwdHJhaW58ZW58MHx8fHwxNzA4MzM2NzAzfDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1580064339631-c938765a976f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fHZyJTIwdHJhaW58ZW58MHx8fHwxNzA4MzM2NzAzfDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Introducing the new geolocation experience"><p>Junat.live now provides a better experience for users using the geolocation feature to navigate to nearby stations. Previously the geolocation button would assume you want to go to the nearest station, which would sometimes lead to confusion. The new version makes less assumptions, but is still snappy if the nearest station is where you want to go.</p><p>So what was wrong with the previous implementation? As said, it would assume the nearest station is the place to go to. What if you are in-between stations? Say you are in Linnanm&#xE4;ki and would like to see trains that depart from Helsinki. The distance between Linnanm&#xE4;ki and Helsinki is roughly 1.7 km, and the distance to Pasila is roughly 1.2 km as the crow flies. You might know that Pasila is closer, but you may also not. Calculating the trip with a straight line also affects the results. To patch this edge-case, for a while, Junat.live showed a notification every time you used the geolocation feature.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.junat.live/content/images/2024/02/Screenshot-2024-02-19-at-11.20.36.png" class="kg-image" alt="Introducing the new geolocation experience" loading="lazy"><figcaption><span style="white-space: pre-wrap;">The notification on old version of Junat.live</span></figcaption></figure><p>That seemed like a good way to allow users to get to the other nearby stations. What I failed to consider is that receiving a notification each time you click a button gets old really quick. Previously you could click a button, boom, nearest station, see departing trains, and continue your day after onboarding the train. All without having to be annoyed with a notification. Handling the edge-case of being in-between two stations was a must, but there&#x2019;s another. What if your position accuracy is not the best? Say your phone is near to dying and to conserve power your phone will stop using GPS. Instead, you get a rough estimate depending on either the nearest cell tower or WiFi modem. You could even be underground, where the accuracy can be affected by rocks.</p><p>That leads us to the revamped experience. Upon clicking the geolocation button instead of assuming the station, you now see stations listed by their distance to you. To avoid information overload, you&#x2019;ll see five stations at a time and can scroll for more. The new implementation covers both of the edge cases, but does not feel intrusive. I did some market research and some sites would open a new page with <em>all</em> stations listed, but that leads to a similar experience as just searching for the station you want to go to. Moreover, having the links at the bottom of the screen makes tapping them easier. It&#x2019;s the little details that make a feature either annoying or a joy to use.</p><figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://blog.junat.live/content/media/2024/02/geolocation_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://blog.junat.live/content/media/2024/02/geolocation.mp4" poster="https://img.spacergif.org/v1/332x640/0a/spacer.png" width="332" height="640" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://blog.junat.live/content/media/2024/02/geolocation_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container kg-video-hide">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:07</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure>]]></content:encoded></item><item><title><![CDATA[How Apple's anti-consumer changes affect Junat.live]]></title><description><![CDATA[The installable web app might stop working for iOS users in March.]]></description><link>https://blog.junat.live/apple-anti-consumer-changes/</link><guid isPermaLink="false">65c8b0dcc01153000158804c</guid><dc:creator><![CDATA[Jasper Nykänen]]></dc:creator><pubDate>Sun, 11 Feb 2024 13:05:30 GMT</pubDate><media:content url="https://blog.junat.live/content/images/2024/02/Frame-1.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-white"><div class="kg-callout-emoji">&#x26A0;&#xFE0F;</div><div class="kg-callout-text">The text is now outdated, and progressive web applications will continue to work the way they did priorly, <a href="https://developer.apple.com/support/dma-and-apps-in-the-eu?ref=blog.junat.live#8" rel="noreferrer">as confirmed by Apple</a>. PWAs are still not anywhere near what Android users are used to, and Apple is blocking third party browsers from installing PWAs. You&apos;re still limited to Safari (WebKit) and it might take a long time until new legalisation forces Apple to think about the user and not maintaining their walled garden.</div></div><img src="https://blog.junat.live/content/images/2024/02/Frame-1.png" alt="How Apple&apos;s anti-consumer changes affect Junat.live"><p>iOS 17.4 is due for release in March of 2024 and will enable developers not to use the App Store to distribute apps, a feature called side loading. Until these changes take effect, Apple has boasted a monopoly requiring developers to pay a yearly fee to use their App Store, taking 30% of all transactions made through the platform. Side loading allows downloading apps from the internet or even other app stores that might pop up. More excitingly, the Digital Markets Act forced Apple to allow other browser engines to function on the platform. Currently, you can only use WebKit, and browsers like Chrome and Firefox use it under the hood on iOS, although they both have their engines on other platforms.</p><p>That all sounds great on paper, but there&apos;s always a but with Apple. Apple will require a 0,5&#x20AC; fee for every download of a side-loaded app when the downloads exceed one million. And since other browser engines can work on iOS in the near future, Apple disabled progressive web application (PWA) support entirely. PWA is a web application you can install on your home screen that behaves similarly to a native app. This scorched earth campaign is what Germans used in the Lapland War. Instead of allowing PWAs to flourish with browser engines that give them full support (e.g., Blink by Google), Apple hardcoded a solution to prevent PWAs from working on iOS.</p><p>That said, the Junat.live PWA will probably stop working when iOS 17.4 reaches general availability. The change can already be seen on iOS 17.4 beta, available for users enrolled in the Apple Developer program. </p><hr><p><strong>17.2.2024</strong></p><p>Apple has confirmed that they will remove PWA functionality in iOS 17.4, and are blaming DMA for it. Apple is threatened by their walled garden getting new residents, so they are artificially limiting what a browser can do in iOS. </p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://open-web-advocacy.org/blog/its-official-apple-kills-web-apps-in-the-eu/?ref=blog.junat.live"><div class="kg-bookmark-content"><div class="kg-bookmark-title">It&#x2019;s Official, Apple Kills Web Apps in the EU - Open Web Advocacy</div><div class="kg-bookmark-description"></div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://open-web-advocacy.org/images/logo-192.png" alt="How Apple&apos;s anti-consumer changes affect Junat.live"><span class="kg-bookmark-author">Open Web Advocacy</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://open-web-advocacy.org/images/logo-192.png" alt="How Apple&apos;s anti-consumer changes affect Junat.live"></div></a></figure>]]></content:encoded></item><item><title><![CDATA[Year 2023 in review]]></title><description><![CDATA[<p>Last year was big for Junat.live: tons of bugs squashed, new features added and an explosive growth in userbase. Sentry was kind enough to sponsor us with a very generous plan to catch errors in production, and I installed Umami to see what features need prioritizing.&#xA0;</p><p>Privacy and</p>]]></description><link>https://blog.junat.live/year-2023-in-review/</link><guid isPermaLink="false">65a19393c4fa0c000148e849</guid><dc:creator><![CDATA[Jasper Nykänen]]></dc:creator><pubDate>Sat, 13 Jan 2024 16:09:17 GMT</pubDate><media:content url="https://blog.junat.live/content/images/2024/01/michael-pointner-9B0r-IHVTfg-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.junat.live/content/images/2024/01/michael-pointner-9B0r-IHVTfg-unsplash.jpg" alt="Year 2023 in review"><p>Last year was big for Junat.live: tons of bugs squashed, new features added and an explosive growth in userbase. Sentry was kind enough to sponsor us with a very generous plan to catch errors in production, and I installed Umami to see what features need prioritizing.&#xA0;</p><p>Privacy and ease of use have always been core principles in making Junat.live. Junat.live has been open source since day one, meaning anyone can inspect the code. But like many other websites, we have integrated with third-party services, which makes developing way more effortless, and having those services publicly available is just not possible. One of those third-party services includes our analytics service. Umami, a lesser-known analytics engine, was chosen because of its out-of-the-box GDPR compliance. Everything, and I mean&#xA0;<em>everything</em>, you do in Junat.live is anonymous. Things like location are not sent to servers, and that&apos;s our promise for years to come.&#xA0;</p><p>Speaking of ease of use, in late 2023, the filter option was added, meaning that you can see only trains that are relevant to you. Now you can view trains that depart from station A and stop at station B. You probably know where you&apos;re going, so let&apos;s streamline the experience and get you where you want to be faster &#x1F609;. We now also download less data and cache things more efficiently, meaning more speed and less data usage. You can now also save favorites to access the stations you use faster.&#xA0;</p><p>But before we get technical, numbers! Unfortunately, Umami was installed in late September, and the analytics data by Cloudflare could be more precise, so I will keep that private. We still have Google, which, by the way, now shows a nice photo next to Junat.live if you search for it:</p><figure class="kg-card kg-image-card"><img src="https://blog.junat.live/content/images/2024/01/image-1.png" class="kg-image" alt="Year 2023 in review" loading="lazy" width="661" height="126" srcset="https://blog.junat.live/content/images/size/w600/2024/01/image-1.png 600w, https://blog.junat.live/content/images/2024/01/image-1.png 661w"></figure><p>In December, we achieved 17 thousand clicks from Google, with a total of <strong>29594 </strong>views overall! Of which just over 11 thousand were unique users. Most people use Junat.live on mobile devices and Tikkurila was the most popular station for 2023. The below data is for October&#x2013;December 2023.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.junat.live/content/images/2024/01/image-2.png" class="kg-image" alt="Year 2023 in review" loading="lazy" width="1355" height="396" srcset="https://blog.junat.live/content/images/size/w600/2024/01/image-2.png 600w, https://blog.junat.live/content/images/size/w1000/2024/01/image-2.png 1000w, https://blog.junat.live/content/images/2024/01/image-2.png 1355w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Users distribute on a wide array of platforms</span></figcaption></figure><div class="kg-card kg-header-card kg-v2 kg-width-full kg-content-wide " style="background-color: #F0F0F0;" data-background-color="#F0F0F0">
            
            <div class="kg-header-card-content">
                
                <div class="kg-header-card-text kg-align-center">
                    <h2 id="lets-get-technical" class="kg-header-card-heading" style="color: #000000;" data-text-color="#000000"><span style="white-space: pre-wrap;">Lets get technical</span></h2>
                    
                    
                </div>
            </div>
        </div><p>If you have no programming experience, or haven&apos;t worked in the software industry, the text below will probably be hard to read. I&apos;ll try to keep the text simple enough, but you have been warned.</p><h2 id="the-big-refactor">The big refactor</h2><p>If you have ever built a website, you usually pick a way to style things and stick with it. CSS is not the most friendly thing to refactor or write in the first place. Much of the testing becomes manual, no matter how you try to automate things. The mistake was laid out early on in the design phase of the Junat.live saga: use hyped-up tools and experiment a little. It did not stand the test of time. In the autumn of 2023, I realized the styling engine Stitches.js I had chosen had been abandoned, and I contemplated options.</p><p>Generally, the way to go about unmaintained software is to ignore it. If it works, do not touch it. After all, Stitches.js got security updates, and the maintainers were transparent about their software&apos;s direction. The reason Stitches.js itself became excruciating to develop was it required a small runtime on the browser. That&apos;s because many frameworks are shifting towards server-side rendering, including the framework of choice for Junat.live: Next.js. Stitches.js maintainers could have rewritten it to require a build step to compile ahead of time (AOT), but many dynamic features that made the tool appealing in the first place would be lost. Due to AOT compilation being almost like a new product was created from scratch, the maintainers looked for new ones to take ownership of the sinking ship. Nobody who could fill the boots volunteered, and&#xA0;<a href="https://github.com/stitchesjs/stitches/discussions/1149?ref=blog.junat.live">the project was officially abandoned</a>.</p><p>Now, if I say that ignoring the ugly parts was an option, why didn&apos;t I do exactly that? Since Junat.live is built on Next.js and they introduced their app router, it seemed as if everything would be SSR first and client second, which is often the way to build websites. SPAs get a lot of love, but bots like our beloved Google Bot would rather not. Not only is the site not (as snappily) machine-readable, but you&apos;ll also have to bore users with loading states like skeleton UIs or spinners. A well-cached webpage generated AOT (like this blog post you&apos;re reading right now) jumps faster at users and requires fewer CPU cycles to render, so the website works like butter even on old Nokias &#x2013; not that old, mind you. Currently, most pages on Junat.live can&apos;t be server-side rendered as they fetch data from Digitraffic that limits requests to IP addresses. Nevertheless, having the option to server-side render pages and components seemed appealing considering the future. We might even move on from Digitraffic and use HSL&apos;s open data, which does not measure usage per IP address, and can then create the initial render on the server and hydrate live data after the fact.</p><p>Moving from Stitches.js to a more stable option was the way to go. I sat on this decision for weeks since I did not want to do a third rewrite. I chose Tailwind because it has gotten a lot of traction and has a large user base. It also supports design systems, which made the migration that much easier. The design system used for Junat.live is relatively primitive, including fonts, type scales, colors, spacing, and icons. Before I began rewriting, though, it was crucial to write automated tests first. Since most of the components in Junat.live are pure (do not contain side-effects), writing stories using Storybook seemed like a no-brainer. Storybook gives free documentation, an interface to see components without having to mock hard-to-reach states, and even integration tests. I ended up writing more tests than deleting and adding lines.</p><p>Tailwind is terse and hard to read, almost like regular expressions in readability if you don&apos;t have the proper tooling installed. Nonetheless, I did everything possible to make the code as readable as possible. After all, choosing Tailwind was not my preference as I had not used it before the rewrite and chose it because it&apos;s what most people use nowadays to generalize a little.</p><h2 id="onwards-on-to-the-next-adventure">Onwards on to the next adventure!</h2><p>Please no.</p><p>I got excited about the refactor and decided it was time to complicate things. Complicating things was not the goal but the effect. Junat.live is now deployed on Vercel, but it ran on a distributed cluster of Docker containers for a month or so. Not the way to go, not at all.</p><p>I&apos;m constantly learning new things and wanted to learn about container orchestration. I was sick of having to worry about each deployment. Not just a couple of times, changes worked on my machine, but not production. With container orchestration, you get things like A/B-, Blue/Green- and canary deployments for free, and rollbacks are as simple as pushing a button. Learning about container orchestration was a positive experience, and the happy path was fun to drift about. Sure, there were some complications, but nothing a couple of cups of coffee and a few screams could not fix. It was only after I had moved Junat.live to the cluster that the pain would start to kick in.</p><p>The container orchestration tool of choice was Nomad, not because it&apos;s the cool kid on the block, but because I did not need all the features and thus complexity a behemoth like Kubernetes (k8s for short) would bring. Sure, there are alternatives like Minikube and k3s, but they mostly require the same runtime environment as k8s, and one of the goals was to keep the costs low. I achieved this by deploying four virtual private servers and creating a single server node and three client nodes. Having a single server node is never a good idea because if the server node blows up, there&apos;s nothing to take its place. Usually, you&apos;d have at least three server nodes and multiple client nodes, but it was outside the budget for me. You&apos;d be partly right if you guessed that the server node quit.</p><p>The four horsemen were hosted by Oracle Cloud, which had hired an intern, Tommy. Tommy decided it was time to delete IP addresses from all shared instances in the UK, and Junat.live would suffer a few days of downtime. I made up Tommy, the intern, but a guess as good as any. How do you just straight up yeet IP addresses out of the window? Sure, they were ephemeral, but at best, they can only rotate, not just straight up stop existing. Oracle Cloud was not doing any favors here, but this mishap would be the least of my worries for weeks to come.</p><p>Now that I had configured Nomad and dockerized Junat.live, I had to make it visible. For discovery, at the time, you had to use a third-party reverse proxy or Consul since discovery was not built in. Nowadays, Nomad can act as a standalone reverse proxy, but too little too late. I set up Consul, but after some experimentation, it proved to be a dire effort. Consul is a large tool, sometimes even used with k8s, and the complexity started getting out of hand. The other popular alternative is Traefik, which, like Nomad, is built on Go. It had a nice API and some bizarre quirks I would only learn about later. The problem was that Traefik would not do its single job, which it&apos;s supposed to do well. Sometimes, the tool would just respond with a status code 404 and a custom error page. It would do this frequently but not constantly&#x2014; a recipe for disaster.</p><p>At this point, I was sick of having to maintain a cluster and just wanted to get back to developing features, which was when I finally adopted Vercel. Now, there are a few services running on a VPS, like this blog and analytics engine. Deployments are as simple as passing the continuous integration and merging to the main, compared to before, where I had to do those above, push to Docker Hub, and provision the image with Terraform.</p><p>After many headaches ensued from our little cloud adventure, having a managed cloud do the grunt work made the development velocity exponential. I would push to production more often, and if anything did something as minor as log to the console, I would know as I had set up Sentry as a last resort. Components would be e2e tested with Storybook, and most logic was covered with unit tests. I squashed some critical bugs like live-updating being restricted to 20 trains, which is now uncapped on our part. There were UX improvements like better error messages and informing of downtime if it were to happen. If Digitraffic components suffer an outage, you can now see the affected components right in Junat.live.</p><p>There are huge things planned for 2024, and I can&apos;t wait to share them with you.</p>]]></content:encoded></item></channel></rss>