<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pedro Martins</title><description>Pedro Martins — Product Engineer, Full-Stack Developer &amp; AI Engineer. Building products end-to-end with TypeScript, Laravel, and AI.</description><link>https://nikuscs.com/</link><language>en-us</language><item><title>The 10x Trap: Why AI Made Me Work More, Not Less</title><link>https://nikuscs.com/blog/15-ai-we-are-working-more-when-we-should-work-less/</link><guid isPermaLink="true">https://nikuscs.com/blog/15-ai-we-are-working-more-when-we-should-work-less/</guid><description>AI was supposed to give us back our time. Instead we ship 10x more code, review 10x more PRs, and end up working longer hours. Here&apos;s why.</description><pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Callout from &quot;@/components/Callout.astro&quot;;&lt;/p&gt;
&lt;h2&gt;1. Introduction&lt;/h2&gt;
&lt;p&gt;If you are reading this, you already know most of what&apos;s going on with AI and you follow the scene quite closely. We all thought AI would give us back our time, and life would be a bit easier right? AI would do the boring parts, automate the repetitive work, let us focus on the fun stuff. The list goes on and on.&lt;/p&gt;
&lt;p&gt;Well, yeah, not quite. After 2 years of using AI daily, i must admit i&apos;ve been actually working &lt;strong&gt;more&lt;/strong&gt;, not less. Keep in mind not everything is negative, i&apos;ve been doing more projects, more open source, learning more, so its not all bad. But the hours? The hours went up.&lt;/p&gt;
&lt;p&gt;How about you? Do you feel like you&apos;re working more with AI? I know its hard to keep up with the &quot;San Francisco mindset&quot; so you don&apos;t feel left behind, but is there a better way? Lets dig in.&lt;/p&gt;
&lt;p&gt;&amp;lt;Callout type=&quot;info&quot;&amp;gt;
&lt;a href=&quot;https://www.scientificamerican.com/article/why-developers-using-ai-are-working-longer-hours/&quot;&gt;Scientific American (March 2026)&lt;/a&gt; reported that out-of-hours code commits rose &lt;strong&gt;19.6%&lt;/strong&gt; among engineers using AI, and engineers scored &lt;strong&gt;17% lower&lt;/strong&gt; on comprehension quizzes compared to the control group. We are working more AND understanding less. Something is off.
&amp;lt;/Callout&amp;gt;&lt;/p&gt;
&lt;p&gt;So lets talk about why this is happening, what i&apos;ve been feeling on my own machine, and where i think we&apos;re heading.&lt;/p&gt;
&lt;h2&gt;2. The Parallelism Trap 🪤&lt;/h2&gt;
&lt;p&gt;Well, if you are a developer you&apos;re probably using some agentic workflow tool like [[Cursor|https://cursor.com]], [[Claude Code|https://claude.com/claude-code]], [[Superset|https://superset.sh]], [[Conductor|https://conductor.build]], [[Codex|https://github.com/openai/codex]], or similar. These tools let you multiplex your workflow across multiple worktrees, multiple projects, and multiple agents all running in parallel. Looks crazy right? Are we in the future? Yes we are!&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;But there is a catch (there is always a catch, right?). While doing all this can be a &lt;strong&gt;massive productivity boost&lt;/strong&gt; if you planned things perfectly beforehand, it also increases the amount of attention and context you need to hold in your valuable brain.&lt;/p&gt;
&lt;p&gt;This often leads to a lot of context switching, and your mental bandwidth is constantly getting drained. You will probably:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Feel like you have done more, when you actually have done less.&lt;/li&gt;
&lt;li&gt;Stop paying attention to details, flow control, code quality, possible bugs, etc.&lt;/li&gt;
&lt;li&gt;End up feeling way more tired at the end of the day, because there is a lot to ingest at all times.&lt;/li&gt;
&lt;li&gt;Make poor architecture decisions without realising it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;Callout type=&quot;info&quot;&amp;gt;
The &lt;a href=&quot;https://dora.dev/dora-report-2025/&quot;&gt;2025 DORA Report&lt;/a&gt; backs this up, developers using AI interact with &lt;strong&gt;67.4% more PR contexts&lt;/strong&gt; per day, work restarts are &lt;strong&gt;up 13.8%&lt;/strong&gt;, and &lt;strong&gt;26% more in-progress tasks&lt;/strong&gt; show no activity for 7+ days. Parallelism has a real cost.
&amp;lt;/Callout&amp;gt;&lt;/p&gt;
&lt;p&gt;Personally, i&apos;ve been doing a maximum of 2 worktrees per project and 2 to 3 agents per worktree. The only exception where i scale more is when its repetitive work, like implementing a test suite for a bunch of modules at the same time.&lt;/p&gt;
&lt;p&gt;BUT there is still a catch, lets check on the next section!&lt;/p&gt;
&lt;h2&gt;3. The Review Bottleneck 🔍&lt;/h2&gt;
&lt;p&gt;So, given all the above, you&apos;re producing code like crazy, and a lot of companies have been sold this dream of &quot;10x productivity&quot;. But well, not quite the 10x we were thinking of.&lt;/p&gt;
&lt;p&gt;Because we produce more code now (and the [[GitHub Octoverse 2025 report|https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/]] shows this clearly, &lt;strong&gt;nearly 1 billion commits in 2025&lt;/strong&gt;, up &lt;strong&gt;25.1% year over year&lt;/strong&gt;, with ~100 million commits in August alone), it means developers (mediors and seniors) are also now reviewing way more code. So not only are we writing more code, we are also reviewing more code. Yeah, &lt;strong&gt;MORE work for us!!!&lt;/strong&gt; 😅&lt;/p&gt;
&lt;p&gt;And if you want the scariest datapoint, there&apos;s a &lt;a href=&quot;https://coremention.com/blog/claude-code-tracker/&quot;&gt;public tracker&lt;/a&gt; showing that [[Claude Code|https://claude.com/claude-code]] alone is responsible for around &lt;strong&gt;9.7% of all public GitHub commits&lt;/strong&gt; as of mid-March 2026, up from 4% in February. AI agent PRs jumped from roughly &lt;strong&gt;4M in September 2025 to 17M in March 2026&lt;/strong&gt;. Someone still has to read all of that.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;Callout type=&quot;info&quot;&amp;gt;
According to &lt;a href=&quot;https://www.faros.ai/blog/ai-software-engineering&quot;&gt;Faros AI research&lt;/a&gt;, developers are completing &lt;strong&gt;21% more tasks&lt;/strong&gt;, &lt;strong&gt;98% more PRs merged&lt;/strong&gt;, but PR review time is &lt;strong&gt;up 91%&lt;/strong&gt; and PRs are &lt;strong&gt;154% larger&lt;/strong&gt;. The &lt;a href=&quot;https://stackoverflow.co/company/press/archive/stack-overflow-2025-developer-survey/&quot;&gt;2025 Stack Overflow Developer Survey&lt;/a&gt; also found that &lt;strong&gt;45% of devs&lt;/strong&gt; say debugging AI-generated code is the most time-consuming part of their day, and &lt;strong&gt;66%&lt;/strong&gt; are frustrated by AI output that is &quot;almost right, but not quite&quot;.
&amp;lt;/Callout&amp;gt;&lt;/p&gt;
&lt;p&gt;Companies are also not helping here, because they are doing massive layoffs thinking a developer can now do the work of 3 developers. When in fact, in my opinion, they are just putting more work on the shoulders of the remaining developers.&lt;/p&gt;
&lt;p&gt;Eventually those developers will either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get burned out and leave the company.&lt;/li&gt;
&lt;li&gt;Start letting sloppy code pass, because they are under pressure to deliver and can&apos;t properly review all of it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And this is simply because we don&apos;t fully trust AI code yet, but lets check on the next section!&lt;/p&gt;
&lt;h2&gt;4. The Trust Gap 🤖&lt;/h2&gt;
&lt;p&gt;We are getting to the point where models are becoming more and more capable of doing the work of a developer (or so we think). As time goes by, i believe in about 2 years models will be able to produce actually good code that we can somewhat trust a bit more.&lt;/p&gt;
&lt;p&gt;But lets not forget that AI is still JUST a next-token predictor. There&apos;s a lot of things that aren&apos;t there yet, models still have a &quot;tunnel vision&quot; about your project, they create unnecessary helpers, and a bunch of other things we call &lt;strong&gt;slop&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Developers currently use codebase indexers, semantic search, and greps to power up the AI workflow, but its still not enough. If you&apos;re solo vibing with AI for coding, you already know you&apos;re playing a gambling game every time you throw a new session in. There&apos;s a good chance you get something &lt;strong&gt;NICE&lt;/strong&gt;, and also a good chance you get something &lt;strong&gt;BAD&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That&apos;s why we humans still need to keep a close eye on the output, not just blindly trust it. The ones doing that are the so-called &lt;strong&gt;vibe coders&lt;/strong&gt;, the same ones who will then cry 3 days later that their [[Supabase|https://supabase.com]] credentials got leaked, or can&apos;t figure out why the website is not working, burning a zillion hours and tokens trying to understand what went wrong.&lt;/p&gt;
&lt;p&gt;And yes, you can use [[Replit|https://replit.com]], [[CodeRabbit|https://coderabbit.ai]], or pass your code to 10 different AI models, and all of them will still let slop code pass. As of today, at least.&lt;/p&gt;
&lt;p&gt;According to the &lt;a href=&quot;https://stackoverflow.co/company/press/archive/stack-overflow-2025-developer-survey/&quot;&gt;2025 Stack Overflow Developer Survey&lt;/a&gt; (49,000+ devs across 177 countries), &lt;strong&gt;84% of developers&lt;/strong&gt; use or plan to use AI tools, but only &lt;strong&gt;29% trust the output&lt;/strong&gt;, down from 40% the year before. Even worse, just &lt;strong&gt;3% say they &quot;highly trust&quot;&lt;/strong&gt; it, and the more experienced the developer, the lower the trust (senior devs report the highest &lt;strong&gt;&quot;highly distrust&quot;&lt;/strong&gt; rate at 20%). So yeah, we might not be writing as much code ourselves, but we still need to &lt;strong&gt;understand code&lt;/strong&gt;, and &lt;strong&gt;how to validate it&lt;/strong&gt;. Thats on us.&lt;/p&gt;
&lt;h2&gt;5. Wrap Up 🧘&lt;/h2&gt;
&lt;p&gt;AI is still one of the best things happening at the moment, no doubt. But lets not forget (including myself) that there is still life outside the terminal, don&apos;t burn yourself out, its ok to take a break and do a bit less.&lt;/p&gt;
&lt;p&gt;Lets also try to spread the word about this. Its all new, not only for us developers but also for companies and managers thinking AI will magically solve every problem and that they can &quot;tokenmaxx&quot; their way to success. The tool doesn&apos;t fix broken systems, it amplifies them. If your team ships fast and reviews slow, AI won&apos;t save you, it will just expose the cracks.&lt;/p&gt;
&lt;p&gt;We developers are here to stay for a few more years at least! Stay positive, keep learning, keep building, and keep sharing your knowledge with the community. Thank you! 🙏&lt;/p&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Scientific American (March 2026), Why developers using AI are working longer hours|https://www.scientificamerican.com/article/why-developers-using-ai-are-working-longer-hours/]]&lt;/li&gt;
&lt;li&gt;[[HBR (February 2026), AI Doesn&apos;t Reduce Work, It Intensifies It|https://hbr.org/2026/02/ai-doesnt-reduce-work-it-intensifies-it]]&lt;/li&gt;
&lt;li&gt;[[Stack Overflow (February 2026), Closing the AI trust gap for developers|https://stackoverflow.blog/2026/02/18/closing-the-developer-ai-trust-gap/]]&lt;/li&gt;
&lt;li&gt;[[2025 Stack Overflow Developer Survey, Press Release|https://stackoverflow.co/company/press/archive/stack-overflow-2025-developer-survey/]]&lt;/li&gt;
&lt;li&gt;[[Larridin, Developer Productivity Benchmarks 2026|https://larridin.com/developer-productivity-hub/developer-productivity-benchmarks-2026]]&lt;/li&gt;
&lt;li&gt;[[GitHub Octoverse 2025, 986M commits and AI leads TypeScript to #1|https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/]]&lt;/li&gt;
&lt;li&gt;[[CoreMention, Claude Code GitHub Commit Share Tracker|https://coremention.com/blog/claude-code-tracker/]]&lt;/li&gt;
&lt;li&gt;[[Faros AI (July 2025), The AI Productivity Paradox Research Report|https://www.faros.ai/blog/ai-software-engineering]]&lt;/li&gt;
&lt;li&gt;[[O&apos;Reilly, AI Is Writing Our Code Faster Than We Can Verify It|https://www.oreilly.com/radar/ai-is-writing-our-code-faster-than-we-can-verify-it/]]&lt;/li&gt;
&lt;li&gt;[[2025 DORA Report, State of AI-assisted Software Development|https://dora.dev/dora-report-2025/]]&lt;/li&gt;
&lt;li&gt;[[VentureBeat, 43% of AI-generated code changes need debugging in production|https://venturebeat.com/technology/43-of-ai-generated-code-changes-need-debugging-in-production-survey-finds]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;Are developers actually working more hours since adopting AI coding tools?&quot;, answer: &quot;Recent data suggests yes. Scientific American reported in March 2026 that out-of-hours code commits rose 19.6% among engineers using AI, and 96% of frequent AI users now work evenings or weekends a few times a month or more. The 2025 DORA report also shows developers interacting with 67.4% more pull request contexts per day, which correlates with higher cognitive load even when individual tasks feel faster.&quot; },
{ question: &quot;What is the AI code review bottleneck?&quot;, answer: &quot;AI lets developers generate and merge pull requests much faster, but human review time has not scaled at the same rate. Faros AI research shows PR volume up 98% and PR size up 154%, while review time per PR is up 91%. According to the 2025 Stack Overflow Developer Survey, 45% of developers say debugging AI-generated code is now the most time-consuming part of their day.&quot; },
{ question: &quot;How much do developers currently trust AI-generated code?&quot;, answer: &quot;According to the 2025 Stack Overflow Developer Survey of 49,000+ developers, 84% use or plan to use AI tools but only 29% trust the output, down from 40% in 2024. Just 3% say they highly trust it, and 46% actively distrust AI output. Senior developers are the most skeptical group, with the highest highly-distrust rate at around 20%.&quot; },
{ question: &quot;How does the DORA 2025 report describe AI&apos;s impact on teams?&quot;, answer: &quot;The 2025 DORA report found that AI adoption among developers rose to 90%, and over 80% say AI improved their productivity. However, organizational delivery metrics remained flat or slightly dropped, suggesting AI amplifies existing team practices rather than fixing broken ones on its own.&quot; },
{ question: &quot;What are practical ways to avoid burnout when working with AI agents?&quot;, answer: &quot;Common approaches include limiting the number of parallel worktrees and agents per session, reserving parallel execution for repetitive work like test generation, treating AI output as a draft that requires review, and protecting dedicated deep-work time away from notifications and agent dashboards.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>ai</category><category>productivity</category><category>burnout</category><category>developer-experience</category><category>workflow</category><author>Pedro Martins</author></item><item><title>Inside Mobile Apps - The Reverse Engineering Rabbit Hole</title><link>https://nikuscs.com/blog/14-inside-mobile-apps-reverse-engineering-rabbit-hole/</link><guid isPermaLink="true">https://nikuscs.com/blog/14-inside-mobile-apps-reverse-engineering-rabbit-hole/</guid><description>How i used to reverse engineer mobile apps for fun, from rooting devices and bypassing certificate pinning to extracting signing keys and reading raw API traffic.</description><pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import CodeTabs from &quot;@/components/CodeTabs.astro&quot;;
import CodeTab from &quot;@/components/CodeTab.astro&quot;;
import Callout from &quot;@/components/Callout.astro&quot;;&lt;/p&gt;
&lt;h2&gt;1. Introduction&lt;/h2&gt;
&lt;p&gt;Part of my life was also investigating mobile apps &amp;amp; their APIs, sometimes for security reasons &amp;amp; curiosity, and sometimes to do a few projects that required &lt;strong&gt;private APIs&lt;/strong&gt; that were not publicly available. In this post i&apos;ll be using Instagram as the main example, since it was the app i spent the most time poking at, but the techniques apply to pretty much any mobile app.&lt;/p&gt;
&lt;p&gt;While im not super proud of it, and it could as well be considered &quot;illegal&quot; or &quot;Black Hat&quot; practices, it was a great learning experience and a lot of fun to do. I also got a lot of help from the community &amp;amp; some of the greatest engineers in the scene.&lt;/p&gt;
&lt;p&gt;&amp;lt;Callout type=&quot;warning&quot;&amp;gt;
I&apos;m not affiliated with Instagram, im not a professional Penetration Tester or anything related to security, im just a hobbyist. The contents of this post are already old and probably not working anymore, still the process is the same for most mobile apps. Please do not try this at home, and do not use this for any illegal purposes. &lt;strong&gt;THIS IS FOR EDUCATIONAL PURPOSES ONLY.&lt;/strong&gt;
&amp;lt;/Callout&amp;gt;&lt;/p&gt;
&lt;h2&gt;2. Glossary &amp;amp; Terminology&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reverse Engineering&lt;/strong&gt; - Deconstructing a compiled app to understand its inner workings — reading decompiled code, tracing network calls, and figuring out undocumented behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jailbreak&lt;/strong&gt; - Removing Apple&apos;s security restrictions on iOS devices, giving you root-level access and the ability to run unsigned code and tweaks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root&lt;/strong&gt; - The Android equivalent of jailbreaking — gaining superuser (root) access to the OS, bypassing manufacturer restrictions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JEB&lt;/strong&gt; - A professional-grade decompiler and debugger for Android APKs. Turns compiled bytecode back into readable Java/Kotlin-like code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frida&lt;/strong&gt; - A dynamic instrumentation toolkit that lets you inject JavaScript into running apps on both Android and iOS — hook functions, intercept calls, and modify behavior at runtime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Certificate Pinning&lt;/strong&gt; - A security mechanism where an app only trusts specific certificates for its server connections, rejecting even valid CA-signed certs. This makes intercepting API traffic significantly harder.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. iOS vs Android: Two Very Different Worlds&lt;/h2&gt;
&lt;p&gt;&amp;lt;Callout type=&quot;warning&quot;&amp;gt;
Rooting or jailbreaking your device can &lt;strong&gt;brick it&lt;/strong&gt;, void your warranty, and compromise your security. You may lose data, break OTA updates, or lock yourself out permanently. Always use a secondary device you&apos;re ok with losing, never your daily driver. Back up everything before you start.
&amp;lt;/Callout&amp;gt;&lt;/p&gt;
&lt;h3&gt;3.1. Root (Android)&lt;/h3&gt;
&lt;p&gt;Usually when you want to reverse engineer a mobile app, you may want to &lt;strong&gt;jailbreak or root&lt;/strong&gt; the device (in case you want to check the network traffic and get an easy way to explore the app internals at runtime).&lt;/p&gt;
&lt;p&gt;For me Android is much easier, since rooting is way more common due to Android being &quot;open source&quot;. Phones like &lt;strong&gt;Google Pixel&lt;/strong&gt; and others are really easy to root with tools like [[Magisk|https://github.com/topjohnwu/Magisk]].&lt;/p&gt;
&lt;p&gt;You may also find on [[XDA forums|https://xdaforums.com/]] images for different devices, and how to root them. Keep in mind that rooting will: void your warranty in certain cases like Samsung devices, compromise your security, and you may not be able to use some features of the device. Apps might also detect if you are rooted and block you from using some features or using the app at all (like Pokemon Go), requiring you to have a bunch of workarounds to use the app.&lt;/p&gt;
&lt;h3&gt;3.2. Jailbreak (iOS)&lt;/h3&gt;
&lt;p&gt;But thats about Android, iOS is wayyyy more tricky to get root access also known as &quot;jailbreaking&quot;. Back in the days, iPhones were somehow easier to jailbreak, the community was there, we had tools like &lt;strong&gt;Cydia&lt;/strong&gt;, those were the golden days for iOS. But with the years, Apple has been cracking hard on iOS security and also hardware security, demotivating developers from the jailbreaking process, and even some of them being hired by Apple itself to work on the other side. This is one of the reasons why iOS is one of the &lt;strong&gt;safest devices&lt;/strong&gt; that you can use in 2026.&lt;/p&gt;
&lt;p&gt;So here for iOS you have a few options in 2026:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Be on an old iOS version and wait for the community to release a jailbreak for it, but this also means that you will be using a legacy version of the OS and that might cause some red flags to the apps, since 90% of the people usually do update to latest iOS versions.&lt;/li&gt;
&lt;li&gt;[[VPPhone|https://github.com/Lakr233/vphone-cli]] - A recently released tool that allows you to run virtual iOS devices on your machine, and then you can jailbreak it.&lt;/li&gt;
&lt;li&gt;[[Corellium|https://corellium.com/]] - An online iOS emulator that you can use to jailbreak your device &amp;amp; test for security vulnerabilities.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. The Toolkit: JEB, Frida &amp;amp; Friends&lt;/h2&gt;
&lt;p&gt;Ok, we got the first part done, assuming you have your device ready to work, now its time to explore a few tools.&lt;/p&gt;
&lt;p&gt;For iOS and Android (or anything really) you can use &lt;strong&gt;decompilers&lt;/strong&gt; to explore binary/object code and get a better understanding of the app itself, while apps that care about security will make life harder for you, by having obfuscated code, tampering protection, and the list goes on. You may want to use this to find a few &lt;strong&gt;magic strings&lt;/strong&gt; across the codebase ex: &lt;code&gt;signature_key&lt;/code&gt; or whatever you see in the requests (more on that later at section 7).&lt;/p&gt;
&lt;h3&gt;4.1. JEB &amp;amp; IDA Pro&lt;/h3&gt;
&lt;p&gt;These are the two heavy hitters when it comes to &lt;strong&gt;static analysis&lt;/strong&gt;, taking an app apart without running it.&lt;/p&gt;
&lt;p&gt;[[JEB|https://www.pnfsoftware.com/]] is purpose-built for Android. You throw an APK at it and it decompiles the Dalvik bytecode back into readable Java/Kotlin. The UI lets you navigate classes, cross-reference method calls, and rename obfuscated symbols as you figure out what they do. For something like Instagram, you&apos;d open the APK, search for strings like &lt;code&gt;x-ig-signature&lt;/code&gt; or &lt;code&gt;signature_key&lt;/code&gt;, and then trace backwards through the code to understand how they&apos;re generated. JEB also has a built-in debugger, so you can set breakpoints on a rooted device and step through the code live.&lt;/p&gt;
&lt;p&gt;[[IDA Pro|https://hex-rays.com/ida-pro/]] is the industry standard for native code, think C/C++ compiled to ARM. Instagram (and most large apps) ship performance-critical and security-sensitive code as native &lt;code&gt;.so&lt;/code&gt; libraries. When you see a Java method calling into &lt;code&gt;libinstagram.so&lt;/code&gt; via JNI, that&apos;s where IDA comes in. It disassembles the binary into ARM assembly and tries to reconstruct higher-level structures. It&apos;s harder to read than JEB&apos;s Java output, but it&apos;s where the &lt;strong&gt;real secrets hide&lt;/strong&gt;, crypto routines, signature generation, anti-tampering checks.&lt;/p&gt;
&lt;p&gt;In practice you use both together: JEB to get the big picture and find entry points, IDA to dig into the native layers where the interesting logic lives.&lt;/p&gt;
&lt;p&gt;Here is a small set of notes on how i used IDA Pro to get the Instagram Signature Key for iOS.&lt;/p&gt;
&lt;p&gt;&amp;lt;CodeTabs&amp;gt;
&amp;lt;CodeTab label=&quot;ida-pro-steps.sh&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. The iphone must be Jailbroken with Cydia, OpenSSH, and Mobile Substrate
# 2. Download and Install Clutch 2.0 for Cydia on the following repository:
#    http://cydia.iphonecake.com/
# 3. Connect to your Phone via SSH and type Clutch2,
#    it should show the apps installed and their IDs
# 4. Dump the IPA:
Clutch2 -d 1  # (use the ID of your app)
# 5. Check the path where the IPA got extracted,
#    usually at /private/var/mobile/Documents/Dumped/com.xxxxx
# 6. Copy the IPA file to your desktop and rename to .zip to extract it
# 7. Find the file at Payload/AppName.app/AppName (no extension)
#    and drag &amp;amp; drop it into IDA Pro
# 8. Wait for the automatic analysis to finish
# 9. In the functions window, Right Click -&amp;gt; &quot;Quick Filter&quot; -&amp;gt; type &quot;hmac&quot;
# 10. Find NSSString(HMAC)HMACWithSecret
# 11. Find the line of code above CCHmacInit — it should be &quot;sub_xxxx&quot;
# 12. Double click to check the sub function,
#     find the lower16/upper16 and navigate to that string
# 13. Select the main string and go to Edit -&amp;gt; Export Data
# 14. Put the HEX data in a script to decode the signing key:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/CodeTab&amp;gt;
&amp;lt;CodeTab label=&quot;decode-key.php&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$encryptedKey = pack(&quot;H*&quot;, &quot;AABBCCDD...&quot;);  // hex data exported from IDA
$hexChars     = array(52, 102, 97, 49, 48, 57, 98, 50, 99, 54, 100, 56, 101, 51, 55, 53);
$index        = array(244, 216, 173, 177, 178, 179, 184, 187, 159, 102, 106, 115, 124, 89, 13, 59);
$signingKey   = &quot;&quot;;
$keyMap       = array_combine($index, $hexChars);
$encKeyLen    = strlen($encryptedKey);

for ($i = 0; $i &amp;lt; $encKeyLen; $i++) {
    $signingKey .= pack(&apos;C&apos;, $keyMap[ord($encryptedKey[$i])]);
}

printf(&quot;SigningKey: %s\n&quot;, $signingKey);
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/CodeTab&amp;gt;
&amp;lt;/CodeTabs&amp;gt;&lt;/p&gt;
&lt;p&gt;Fun right? Ofc you have to figure out first, what are the actual &quot;sub_xxxx&quot; functions, and so on, i&apos;ll not be covering those in this post.&lt;/p&gt;
&lt;h3&gt;4.2. Frida&lt;/h3&gt;
&lt;p&gt;If JEB and IDA are about reading dead code, [[Frida|https://frida.re/]] is about messing with &lt;strong&gt;living code&lt;/strong&gt;. It&apos;s a dynamic instrumentation toolkit, you attach it to a running app and inject JavaScript that can hook any function, intercept arguments, change return values, and basically rewrite the app&apos;s behavior on the fly.&lt;/p&gt;
&lt;p&gt;The workflow is simple: you run &lt;code&gt;frida -U -f com.example.app -l your_script.js&lt;/code&gt; on a rooted/jailbroken device connected via USB (&lt;code&gt;-U&lt;/code&gt;), and Frida spawns the app with your script injected. From there, your script has full access to the app&apos;s memory, symbols, and Objective-C/Java runtime.&lt;/p&gt;
&lt;p&gt;In practice, Frida is incredibly versatile, you can use it to &lt;strong&gt;extract encryption keys&lt;/strong&gt; from memory, log function arguments and return values to understand internal logic, dump decrypted payloads, or bypass security checks like certificate pinning and root/jailbreak detection.&lt;/p&gt;
&lt;p&gt;Here are two Frida scripts i used back in the day. The first one bypasses certificate pinning at multiple layers so you can intercept the app&apos;s HTTPS traffic with a proxy like [[Charles|https://www.charlesproxy.com/]] or [[mitmproxy|https://mitmproxy.org/]]. The second one hooks the HMAC signing process to extract the signing key straight from memory, the dynamic counterpart to the IDA Pro approach from section 4.1.&lt;/p&gt;
&lt;p&gt;&amp;lt;CodeTabs&amp;gt;
&amp;lt;CodeTab label=&quot;ssl-pinning-bypass.js&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Frida script to bypass SSL/TLS certificate pinning on iOS.
 * Targets multiple layers: custom HTTP framework verification, internal config
 * flags, OpenSSL-based callbacks, and Apple&apos;s Security framework.
 *
 * Usage: frida -U -f com.example.app -l ssl-pinning-bypass.js
 */

// ——————————————————————————————————————————————
// 1. Bypass the custom HTTP framework&apos;s TLS verification
// ——————————————————————————————————————————————
// Some apps use their own HTTP framework with a custom certificate
// verification step that runs before the standard SSL checks.
// We find it by symbol name and replace it to always succeed.
function bypassCustomTLSVerification() {
  var matches = DebugSymbol.findFunctionsMatching(&quot;*validateCertChain*&quot;);

  if (matches.length !== 1) {
    console.log(&quot;[!] Expected exactly one validateCertChain symbol, found &quot; + matches.length);
    return;
  }

  var original = new NativeFunction(
    matches[0], &quot;int&quot;,
    [&quot;uint64&quot;, &quot;pointer&quot;, &quot;pointer&quot;, &quot;pointer&quot;, &quot;pointer&quot;, &quot;pointer&quot;, &quot;pointer&quot;]
  );

  Interceptor.replace(matches[0], new NativeCallback(function (
    _flag, _x509ctx, _str, _failCb, _successCb, _clock, _trace
  ) {
    // Call original but swap the fail callback with the success callback,
    // then force return 1 (success) regardless
    original(_flag, _x509ctx, _str, _successCb, _successCb, _clock, _trace);
    return 1;
  }, &quot;int&quot;, [&quot;uint64&quot;, &quot;pointer&quot;, &quot;pointer&quot;, &quot;pointer&quot;, &quot;pointer&quot;, &quot;pointer&quot;, &quot;pointer&quot;]));

  console.log(&quot;[+] Custom TLS verification bypassed&quot;);
}

bypassCustomTLSVerification();

// ——————————————————————————————————————————————
// 2. Disable internal TLS/SSL config flags
// ——————————————————————————————————————————————
// The app&apos;s internal config has several flags that enable enhanced
// TLS features (custom TLS stack, SSL session caching, cert compression).
// We force all of them to return 0 (disabled).

var configFlags = [
  &quot;persistentSSLCacheEnabled&quot;,
  &quot;crossDomainSSLCacheEnabled&quot;,
  &quot;customTLSEnabled&quot;,
  &quot;customTLSPersistentCacheEnabled&quot;,
  &quot;quicEarlyDataEnabled&quot;,
  &quot;tlsEarlyDataEnabled&quot;,
  &quot;enableCertCompression&quot;,
];

function disableInternalTLSConfig() {
  var resolver = new ApiResolver(&quot;objc&quot;);

  for (var i = 0; i &amp;lt; configFlags.length; i++) {
    var flag = configFlags[i];
    var results = resolver.enumerateMatchesSync(
      &quot;-[InternalTLSConfig &quot; + flag + &quot;]&quot;
    );

    if (results.length &amp;lt; 1) {
      console.log(&quot;[!] Could not find getter for &quot; + flag);
      continue;
    }

    Interceptor.attach(results[0][&quot;address&quot;], {
      onLeave: function (retval) {
        retval.replace(0);
      },
    });

    console.log(&quot;[+] &quot; + flag + &quot; disabled&quot;);
  }
}

disableInternalTLSConfig();

// ——————————————————————————————————————————————
// 3. Neutralize OpenSSL certificate callbacks
// ——————————————————————————————————————————————
// The underlying SSL library provides multiple hooks for cert
// verification. We replace them all with no-ops so the app
// never registers its custom pinning callbacks.

function neutralizeSSLCallbacks() {
  var callbackNames = [
    &quot;SSL_CTX_set_cert_verify_callback&quot;,
    &quot;SSL_CTX_set_cert_verify_result_callback&quot;,
    &quot;SSL_CTX_set_verify&quot;,
    &quot;SSL_set_verify&quot;,
    &quot;SSL_set_cert_cb&quot;,
    &quot;SSL_CTX_set_cert_cb&quot;,
    &quot;X509_STORE_CTX_set_verify_cb&quot;,
  ];

  for (var i = 0; i &amp;lt; callbackNames.length; i++) {
    var name = callbackNames[i];
    var addresses = DebugSymbol.findFunctionsNamed(name);

    for (var j = 0; j &amp;lt; addresses.length; j++) {
      Interceptor.replace(
        addresses[j],
        new NativeCallback(function () {
          return;
        }, &quot;void&quot;, [])
      );
    }

    console.log(&quot;[+] &quot; + name + &quot; neutralized (&quot; + addresses.length + &quot; instances)&quot;);
  }
}

neutralizeSSLCallbacks();

// ——————————————————————————————————————————————
// 4. Override Apple&apos;s SecTrustEvaluate
// ——————————————————————————————————————————————
// Last line of defense — Apple&apos;s own Security framework.
// We hook SecTrustEvaluate to always write kSecTrustResultProceed
// to the result pointer and return success (0 = errSecSuccess).

function overrideSecTrustEvaluate() {
  var ptr = Module.findExportByName(&quot;Security&quot;, &quot;SecTrustEvaluate&quot;);

  if (ptr === null) {
    console.log(&quot;[!] SecTrustEvaluate not found&quot;);
    return;
  }

  var original = new NativeFunction(ptr, &quot;int&quot;, [&quot;pointer&quot;, &quot;pointer&quot;]);

  Interceptor.replace(ptr, new NativeCallback(function (trust, result) {
    original(trust, result);
    // kSecTrustResultProceed = 1
    Memory.writeU8(result, 1);
    return 0; // errSecSuccess
  }, &quot;int&quot;, [&quot;pointer&quot;, &quot;pointer&quot;]));

  console.log(&quot;[+] SecTrustEvaluate bypassed&quot;);
}

overrideSecTrustEvaluate();

console.log(&quot;\n[*] All SSL pinning bypasses active. Ready to intercept traffic.\n&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/CodeTab&amp;gt;
&amp;lt;CodeTab label=&quot;extract-signing-key.js&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Hooks the app&apos;s HMAC signing method and the underlying SHA256 calls
 * to extract the HMAC key material (ipad/opad XOR&apos;d key) from memory.
 *
 * Usage: frida -U -f com.example.app -l extract-signing-key.js
 *        Then trigger a request in the app (e.g. try to log in).
 */

var currentThreadId = 0;
var requestBody = &quot;&quot;;
var sha256CallCount = 0;
var hasShownSignedBody = false;
var lastDump = &quot;&quot;;

function extractSigningKey() {
  var resolver = new ApiResolver(&quot;objc&quot;);
  var matches = resolver.enumerateMatchesSync(&quot;-[NSString computeHMAC:]&quot;);

  if (matches.length !== 1) {
    console.log(&quot;[!] Expected exactly one computeHMAC method, found &quot; + matches.length);
    return;
  }

  Interceptor.attach(matches[0][&quot;address&quot;], {
    onEnter: function (args) {
      if (currentThreadId === 0) {
        currentThreadId = Process.getCurrentThreadId();
        requestBody = ObjC.Object(args[0]);

        // Now hook the underlying SHA256 update function to capture key material
        var moduleResolver = new ApiResolver(&quot;module&quot;);
        var sha256Matches = moduleResolver.enumerateMatchesSync(&quot;exports:*!CC_SHA256_Update&quot;);

        for (var i = 0; i &amp;lt; sha256Matches.length; i++) {
          Interceptor.attach(sha256Matches[i][&quot;address&quot;], {
            onEnter: function (args) {
              if (currentThreadId === Process.getCurrentThreadId()) {
                var dump = hexdump(args[1], { length: 0x40, ansi: false, header: false });

                if (dump !== lastDump) {
                  sha256CallCount++;

                  // HMAC internally does two rounds of hashing:
                  // 1st call: ipad XOR&apos;d with the key
                  // 3rd call: opad XOR&apos;d with the key
                  // From these two values you can recover the original key
                  if (sha256CallCount === 1) {
                    console.log(&quot;[*] ipad_xor_key =\n&quot; + dump);
                  }
                  if (sha256CallCount === 3) {
                    console.log(&quot;[*] opad_xor_key =\n&quot; + dump);
                  }

                  lastDump = dump;
                }
              }
            },
          });
          console.log(&quot;[+] CC_SHA256_Update hooked at &quot; + sha256Matches[i][&quot;address&quot;]);
        }
      }
    },

    onLeave: function (retval) {
      if (Process.getCurrentThreadId() === currentThreadId &amp;amp;&amp;amp; !hasShownSignedBody) {
        console.log(&quot;[*] Signed body example: &quot; + ObjC.Object(retval) + &quot;.&quot; + requestBody);
        hasShownSignedBody = true;
        Interceptor.detachAll();
      }
    },
  });

  console.log(&quot;[+] HMAC signing method hooked&quot;);
  console.log(&quot;[*] Now trigger a request in the app (e.g. try to log in)...\n&quot;);
}

extractSigningKey();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/CodeTab&amp;gt;
&amp;lt;/CodeTabs&amp;gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;SSL pinning bypass&lt;/strong&gt; works in 4 layers because apps like these don&apos;t just pin certificates in one place, they stack multiple verification mechanisms on top of each other. If you only bypass one, the others will still reject your proxy&apos;s certificate. You need to get &lt;strong&gt;all of them&lt;/strong&gt;. Once this script is running, you can point the device&apos;s traffic through a proxy and finally see the actual API requests in cleartext.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;key extraction script&lt;/strong&gt; hooks the HMAC signing function and listens for the underlying &lt;code&gt;CC_SHA256_Update&lt;/code&gt; calls. The output gives you the &lt;code&gt;ipad&lt;/code&gt; and &lt;code&gt;opad&lt;/code&gt; XOR&apos;d keys, the two halves of the HMAC construction. Since HMAC XORs the key with known constants (&lt;code&gt;0x36&lt;/code&gt; for ipad, &lt;code&gt;0x5c&lt;/code&gt; for opad), recovering the original key is trivial, just XOR it back. The static IDA approach from section 4.1 requires you to reverse engineer the key obfuscation in the binary, while this dynamic approach just waits for the app to decrypt the key itself and grabs it from memory.&lt;/p&gt;
&lt;h2&gt;5. The Cat and Mouse Game&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;So you rooted your device, installed Frida, and you&apos;re ready to go. You open the app and... it crashes. Or it opens but refuses to load. Or it works but all the requests fail. Welcome to the &lt;strong&gt;cat and mouse game&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;5.1. Root &amp;amp; Jailbreak Detection&lt;/h3&gt;
&lt;p&gt;Big apps like Instagram don&apos;t just sit there and let you poke around. They actively check if your device is compromised. On Android, Google&apos;s &lt;strong&gt;Play Integrity API&lt;/strong&gt; (formerly SafetyNet) lets apps verify the device hasn&apos;t been tampered with. On iOS, apps check for common jailbreak artifacts like Cydia, &lt;code&gt;/etc/apt&lt;/code&gt;, or the ability to write to system paths.&lt;/p&gt;
&lt;p&gt;The thing is, these checks are well known and the community has built workarounds for most of them. On Android, Magisk has &lt;strong&gt;Zygisk&lt;/strong&gt; modules (previously MagiskHide) that hide root from specific apps by running them in an isolated environment where root binaries and modified system files are invisible. On iOS, tweaks like [[Shadow|https://github.com/jjolano/shadow]] or [[A-Bypass|https://repo.co.kr/]] patch the detection routines at runtime.&lt;/p&gt;
&lt;p&gt;But here&apos;s the catch, apps update their detection methods constantly. You might get it working today and then an app update two weeks later breaks everything again. Its a never-ending cycle.&lt;/p&gt;
&lt;h3&gt;5.2. Frida Detection&lt;/h3&gt;
&lt;p&gt;This one is sneaky. Some apps specifically look for &lt;strong&gt;Frida itself&lt;/strong&gt;. They&apos;ll scan for the &lt;code&gt;frida-server&lt;/code&gt; process, check if the default Frida port (27042) is open, look for Frida&apos;s agent library in the app&apos;s memory, or even scan for known Frida strings in loaded modules.&lt;/p&gt;
&lt;p&gt;There are a few ways around this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rename frida-server&lt;/strong&gt; to something random, some detection just looks for the process name&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Frida Gadget&lt;/strong&gt; instead of frida-server. Instead of attaching to the app from outside, you embed Frida directly into the app as a library (&lt;code&gt;.dylib&lt;/code&gt; on iOS, &lt;code&gt;.so&lt;/code&gt; on Android). This avoids the server entirely and is way harder to detect&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timing your injection&lt;/strong&gt;, attach after the detection code has already run, or hook the detection functions themselves before they execute (yeah, using Frida to bypass Frida detection, its recursive like that)&lt;/li&gt;
&lt;li&gt;Use [[frida-stealth|https://github.com/alphaSeclab/anti-debug]] or community scripts that patch known detection patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In my experience, Frida Gadget was the most reliable approach for heavily protected apps. It takes more setup (you need to repackage the app with the gadget library embedded), but once its in there, the app doesn&apos;t know the difference.&lt;/p&gt;
&lt;h2&gt;6. Reading the Requests&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Now that you got everything that you need (or you should by this time) you can continue your journey to explore the app&apos;s &lt;strong&gt;HTTP layer&lt;/strong&gt;. You can use tools like [[Wireshark|https://www.wireshark.org/]], [[Proxyman|https://proxyman.io/]], [[Fiddler|https://www.telerik.com/fiddler]], [[Charles|https://www.charlesproxy.com/]], [[mitmproxy|https://mitmproxy.org/]] to capture the network traffic and analyze the data. I usually use mitmproxy when i need flexibility to hook into the request (filter, etc) and Proxyman when i need something really quick to capture. Take the one that fits your needs best.&lt;/p&gt;
&lt;p&gt;For apps that don&apos;t have &lt;strong&gt;Certificate Pinning&lt;/strong&gt;, usually the process is as simple as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get the certificate from the proxy app and install it on your mobile device as a trusted certificate.&lt;/li&gt;
&lt;li&gt;Set the WiFi settings to proxy to your proxy server like: &lt;code&gt;192.168.1.100:8080&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;You can try opening Google or any site and it shouldn&apos;t be blocked, if it is, you may have to check the certificate again.&lt;/li&gt;
&lt;li&gt;Open the app and start clicking around and see the requests being logged. Keep in mind that for some apps you will get a &lt;strong&gt;TON&lt;/strong&gt; of requests, most of them about telemetry, analytics, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you&apos;re capturing traffic, the trick is knowing what to look for. Most of what you&apos;ll see is noise, telemetry pings, analytics batches, ad SDK calls, config fetches. The interesting stuff is usually the &lt;strong&gt;authenticated API calls&lt;/strong&gt;, look for custom headers like &lt;code&gt;x-ig-signature&lt;/code&gt;, &lt;code&gt;x-ig-app-id&lt;/code&gt;, or whatever the app uses for request signing. Pay attention to the request body format too, some apps send JSON, others use protobuf or custom binary formats. If the body looks like gibberish, it&apos;s probably signed or encrypted, and thats where the signing key from section 4 comes into play.&lt;/p&gt;
&lt;p&gt;A good workflow is to &lt;strong&gt;filter by domain&lt;/strong&gt; first (in Instagram&apos;s case, &lt;code&gt;i.instagram.com&lt;/code&gt;), then sort by endpoint to see which APIs are being called. Login flows, feed loading, story viewing, each one hits different endpoints with different payloads. Once you map out the main ones, you start to see the patterns.&lt;/p&gt;
&lt;h2&gt;7. Data Samples &amp;amp; Privacy Concerns&lt;/h2&gt;
&lt;p&gt;Well, im betting here that if you do happen to actually get to see some of the mainstream apps like Instagram, Twitter, TikTok etc you will be surprised by the amount of data that is being sent to their servers. Its actually &lt;strong&gt;INSANE&lt;/strong&gt;! This is why most of these apps are locked down and try to make it as hard as possible for the regular user to see this data. Some of the data i saw across multiple apps includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User Location Data&lt;/strong&gt; (GPS, IP Address, etc)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Device Hardware Data&lt;/strong&gt; including: CPU, RAM, Free RAM, Storage, Battery Percentage (YES!)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SIM Card&lt;/strong&gt; - Carrier Name, Country, etc&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time spent&lt;/strong&gt; on the App, time spent on each photo/video, clicking points, scrolls, etc&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time spent with the finger&lt;/strong&gt; over a certain photo, video or post&lt;/li&gt;
&lt;li&gt;If your phone is &lt;strong&gt;rooted or not&lt;/strong&gt;, time since you &quot;booted&quot; the device, also first boot, etc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The list goes on and on, while this data could be used for internal analytics, it can also be used for &lt;strong&gt;tracking, fingerprinting &amp;amp; ads&lt;/strong&gt;. Remember the famous sentence, if its free, you are the product? Yeah, we often forget about it, but when you get to see the actual data, you gonna start thinking twice about it.&lt;/p&gt;
&lt;p&gt;Here&apos;s a redacted sample of a single telemetry batch captured from just opening the Instagram app and navigating to the login screen, this is what gets sent &lt;strong&gt;before you even type anything&lt;/strong&gt;. You will get probably 10s of these batches per minute &amp;amp; everytime you wake up your device or make it sleep.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. Conclusion&lt;/h2&gt;
&lt;p&gt;Reverse engineering mobile apps is one of those things that teaches you more about software than most courses ever will. You learn how apps really work under the hood, how security is (or isn&apos;t) implemented, and just how much data flows between your phone and someone else&apos;s servers without you ever knowing.&lt;/p&gt;
&lt;p&gt;I hope this post gave you a decent overview of the process and the tools involved. Whether you&apos;re into security research, building tools that interact with private APIs, or just curious about what your favourite apps are doing behind the scenes, the skills are the same. Root/jailbreak, decompile, hook, intercept, analyze.&lt;/p&gt;
&lt;p&gt;Just remember, use this knowledge responsibly. The line between &quot;security research&quot; and &quot;getting yourself in trouble&quot; is thinner than you think. Stay curious, stay ethical, and don&apos;t do anything stupid.&lt;/p&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Magisk|https://github.com/topjohnwu/Magisk]] - Root your Android device&lt;/li&gt;
&lt;li&gt;[[VPPhone|https://github.com/Lakr233/vphone-cli]] - Virtual iOS devices&lt;/li&gt;
&lt;li&gt;[[Corellium|https://corellium.com/]] - Online iOS emulator&lt;/li&gt;
&lt;li&gt;[[JEB|https://www.pnfsoftware.com/]] - Android APK decompiler &amp;amp; debugger&lt;/li&gt;
&lt;li&gt;[[IDA Pro|https://hex-rays.com/ida-pro/]] - Industry standard disassembler for native code&lt;/li&gt;
&lt;li&gt;[[Frida|https://frida.re/]] - Dynamic instrumentation toolkit&lt;/li&gt;
&lt;li&gt;[[Wireshark|https://www.wireshark.org/]] - Network protocol analyzer&lt;/li&gt;
&lt;li&gt;[[Proxyman|https://proxyman.io/]] - macOS HTTP debugging proxy&lt;/li&gt;
&lt;li&gt;[[Charles|https://www.charlesproxy.com/]] - HTTP proxy / monitor&lt;/li&gt;
&lt;li&gt;[[mitmproxy|https://mitmproxy.org/]] - Open-source interactive HTTPS proxy&lt;/li&gt;
&lt;li&gt;[[Fiddler|https://www.telerik.com/fiddler]] - Web debugging proxy&lt;/li&gt;
&lt;li&gt;[[XDA Forums|https://xdaforums.com/]] - Android development community&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;Is reverse engineering mobile apps legal?&quot;, answer: &quot;It depends on your jurisdiction and intent. Security research and interoperability are often protected under laws like the DMCA&apos;s exemptions or the EU&apos;s reverse engineering provisions. However, using it to access unauthorized data, violate terms of service, or harm others can be illegal. Always check your local laws and only do this in controlled, ethical environments.&quot; },
{ question: &quot;What is the difference between static and dynamic analysis?&quot;, answer: &quot;Static analysis means examining the app&apos;s code without running it, using tools like JEB or IDA Pro to decompile and read the source. Dynamic analysis means interacting with the app while it&apos;s running, using tools like Frida to hook functions, intercept data, and modify behavior in real-time. In practice you often use both together.&quot; },
{ question: &quot;How does certificate pinning work and why do apps use it?&quot;, answer: &quot;Certificate pinning is when an app hardcodes or embeds the specific certificates it trusts for server connections, instead of relying on the device&apos;s certificate store. This prevents man-in-the-middle attacks where someone could intercept traffic using a proxy with a custom certificate. Large apps often implement multiple layers of pinning for extra security.&quot; },
{ question: &quot;What kind of data do mobile apps collect about users?&quot;, answer: &quot;Many mainstream apps collect extensive telemetry including device hardware specs, location data, SIM card info, battery percentage, screen interactions (scroll depth, tap coordinates, time spent viewing content), root/jailbreak status, and boot times. This data is typically used for analytics, ad targeting, and device fingerprinting.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>reverse-engineering</category><category>mobile</category><category>security</category><category>instagram</category><author>Pedro Martins</author></item><item><title>Bookmarkjar ®</title><link>https://nikuscs.com/projects/07-bookmarkjar/</link><guid isPermaLink="true">https://nikuscs.com/projects/07-bookmarkjar/</guid><description>AI-powered bookmark manager that saves, organizes, and lets you search your bookmarks with semantic understanding. Find faster, organize less.</description><pubDate>Sat, 14 Feb 2026 00:00:00 GMT</pubDate><category>ai</category><category>bookmarks</category><category>productivity</category><category>search</category><category>chrome-extension</category><author>Pedro Martins</author></item><item><title>Prompt Book</title><link>https://nikuscs.com/projects/09-prompt-book/</link><guid isPermaLink="true">https://nikuscs.com/projects/09-prompt-book/</guid><description>Store, search &amp; instantly copy AI prompts from your macOS menu bar — built with Tauri v2 + React.</description><pubDate>Wed, 11 Feb 2026 00:00:00 GMT</pubDate><category>ai</category><category>macos</category><category>productivity</category><category>tauri</category><category>prompt-engineering</category><author>Pedro Martins</author></item><item><title>i18n with TanStack Start: A Complete Guide 2026</title><link>https://nikuscs.com/blog/13-tanstackstart-i18n/</link><guid isPermaLink="true">https://nikuscs.com/blog/13-tanstackstart-i18n/</guid><description>Learn how to properly implement internationalization in TanStack Start. From project setup and routing strategies to SSR hydration and language detection—everything you need to build a production-ready multilingual app.</description><pubDate>Tue, 27 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Introduction 🌍&lt;/h2&gt;
&lt;p&gt;Everyone hates doing i18n for apps, usually its way faster to just hardcode the strings in the code and call it a day, and thats totally valid depending on the project and how fast you need to get it done.&lt;/p&gt;
&lt;p&gt;But specially today if you want to market your product to other countries that are not so &quot;English speaking&quot; friendly you might want to consider doing i18n, not only its a great benefit for your users, but also for your SEO and marketing. Even if you want to start just with English, having the strings in a single place might be a good idea to avoid duplication and keep your codebase clean.&lt;/p&gt;
&lt;p&gt;BUT, this seems like a daunting task, and usually its a pain to implement in all frameworks that i have tried, specially with the routing logic.&lt;/p&gt;
&lt;h2&gt;2. Requirements 📋&lt;/h2&gt;
&lt;p&gt;You might think that this is a simple task right? Or that is probably a package that will just work out of the box? Well, yes and no. i18n can be tricky to implement in any framework, mostly because of the routing logic.&lt;/p&gt;
&lt;p&gt;So here is what i usually want as a good i18n implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single source of truth for the strings for any language&lt;/li&gt;
&lt;li&gt;Quickly add new languages with mostly zero effort.&lt;/li&gt;
&lt;li&gt;NO prefix for the default language, and prefix for the other languages.&lt;/li&gt;
&lt;li&gt;Ability to ignore certain routes to not be prefixed at all ( ex: /dashboard )&lt;/li&gt;
&lt;li&gt;Small in Bundle size, and fast to load.&lt;/li&gt;
&lt;li&gt;Support SSR, Async loading of the strings.&lt;/li&gt;
&lt;li&gt;Ability to store in cookies or local storage, accept the language from the browser or from the user. Choose the best strategy for the job for your use case.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. How frameworks handle locale prefixes 🛤️&lt;/h2&gt;
&lt;p&gt;Before diving into strategies, let&apos;s see how different frameworks approach optional locale prefixes in routes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Laravel&lt;/strong&gt; handles this with route groups, but it&apos;s not as simple as it looks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Route::group([&apos;prefix&apos; =&amp;gt; &apos;{locale?}&apos;, &apos;where&apos; =&amp;gt; [&apos;locale&apos; =&amp;gt; &apos;es|fr|de&apos;]], function () {
    Route::get(&apos;/about&apos;, [PageController::class, &apos;about&apos;]);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;{locale?}&lt;/code&gt; is optional and the &lt;code&gt;where&lt;/code&gt; constraint ensures only valid locales match. But here&apos;s the catch: &lt;code&gt;/random-page&lt;/code&gt; would 404 because &lt;code&gt;random-page&lt;/code&gt; doesn&apos;t match the constraint—but you still need middleware to actually set the default locale when no prefix is provided, and you need to be careful with route ordering.&lt;/p&gt;
&lt;p&gt;[[TanStack Router|https://tanstack.com/router]] handles this with optional path parameters using the &lt;code&gt;{-$param}&lt;/code&gt; syntax:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/routes/{-$locale}.tsx
export const Route = createFileRoute(&quot;/{-$locale}&quot;)({
  beforeLoad: ({ params }) =&amp;gt; {
    const locale = params.locale;
    if (locale &amp;amp;&amp;amp; !isValidLocale(locale)) {
      throw notFound();
    }
  },
  component: LocalizedLayout,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-&lt;/code&gt; prefix makes the param optional. So &lt;code&gt;/{-$locale}/about&lt;/code&gt; matches both &lt;code&gt;/about&lt;/code&gt; (no locale) and &lt;code&gt;/es/about&lt;/code&gt; (with locale). This is exactly what we need for section 4.3.&lt;/p&gt;
&lt;p&gt;The key difference from Laravel: you need to validate in &lt;code&gt;beforeLoad&lt;/code&gt; that the param is actually a valid locale, otherwise &lt;code&gt;/random-page&lt;/code&gt; would be interpreted as a locale instead of a 404.&lt;/p&gt;
&lt;h2&gt;4. Prefixing or not to prefix? 🤔&lt;/h2&gt;
&lt;p&gt;This is one of the most important decisions you&apos;ll make, and it affects SEO, user experience, and your routing complexity. There are typically three strategies:&lt;/p&gt;
&lt;h3&gt;4.1 Always prefix all languages (&lt;code&gt;/en/about&lt;/code&gt;, &lt;code&gt;/es/about&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;This is the &quot;easy&quot; approach from a routing perspective—every language gets a prefix, no exceptions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The problem?&lt;/strong&gt; Your root &lt;code&gt;/&lt;/code&gt; becomes useless. You need a redirect to &lt;code&gt;/en&lt;/code&gt; (or detect the language and redirect). This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extra redirect hop = slower initial load&lt;/li&gt;
&lt;li&gt;Google sees &lt;code&gt;/&lt;/code&gt; as a redirect, not your actual content&lt;/li&gt;
&lt;li&gt;Your &quot;main&quot; URLs look ugly: &lt;code&gt;example.com/en/pricing&lt;/code&gt; instead of &lt;code&gt;example.com/pricing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Users sharing links always share the prefixed version&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From an SEO perspective, Google handles redirects fine, but you&apos;re essentially wasting your cleanest URL (&lt;code&gt;/&lt;/code&gt;) on a 302/301 redirect instead of actual content.&lt;/p&gt;
&lt;h3&gt;4.2 Never prefix (detect via cookies/headers)&lt;/h3&gt;
&lt;p&gt;Clean URLs everywhere! Just &lt;code&gt;/about&lt;/code&gt; for everyone, and you detect the language from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Accept-Language&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;A cookie from a previous visit&lt;/li&gt;
&lt;li&gt;User&apos;s account settings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why this is terrible for SEO:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Google crawls your site without cookies and with &lt;code&gt;Accept-Language: en&lt;/code&gt;. So Google only ever sees your English content. Your Spanish pages? They don&apos;t exist to search engines. You can&apos;t have proper &lt;code&gt;hreflang&lt;/code&gt; tags pointing to different URLs because... there are no different URLs.&lt;/p&gt;
&lt;p&gt;Also, CDN caching becomes a nightmare. Same URL, different content based on headers? You need &lt;code&gt;Vary: Accept-Language&lt;/code&gt; headers and your cache hit rate drops significantly.&lt;/p&gt;
&lt;h3&gt;4.3 No prefix for default, prefix for others (&lt;code&gt;/about&lt;/code&gt;, &lt;code&gt;/es/about&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;This is the sweet spot, but also the hardest to implement correctly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/about&lt;/code&gt; → English (default, no prefix)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/es/about&lt;/code&gt; → Spanish&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/fr/about&lt;/code&gt; → French&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why it&apos;s hard:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Your router needs to handle two different URL patterns for the same page. In most frameworks, routes are defined once. Now you need:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/about           → AboutPage (lang: en)
/es/about        → AboutPage (lang: es)
/fr/about        → AboutPage (lang: fr)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means your routing logic needs to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check if the first segment is a known locale&lt;/li&gt;
&lt;li&gt;If yes, extract it and route to the rest of the path&lt;/li&gt;
&lt;li&gt;If no, assume default locale and route normally&lt;/li&gt;
&lt;li&gt;Handle links generation differently based on target language&lt;/li&gt;
&lt;li&gt;Not break when someone adds a page called &lt;code&gt;/essays&lt;/code&gt; (is that a locale or a page?)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Most i18n libraries either don&apos;t support this, or require you to duplicate every route definition. TanStack Router makes this manageable, but it&apos;s still not trivial.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it&apos;s worth it:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your primary audience gets clean URLs&lt;/li&gt;
&lt;li&gt;Full SEO support with proper &lt;code&gt;hreflang&lt;/code&gt; tags&lt;/li&gt;
&lt;li&gt;Each language has its own indexable URL&lt;/li&gt;
&lt;li&gt;No redirect tax on your most important pages&lt;/li&gt;
&lt;li&gt;CDN caching works perfectly (different URL = different cache entry)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the strategy we&apos;ll implement in this guide.&lt;/p&gt;
&lt;h2&gt;5. Redirecting the user to the correct language 🔀&lt;/h2&gt;
&lt;p&gt;Once you got the previous section sorted out we will need to decide, what we do when the user first visits our website?&lt;/p&gt;
&lt;p&gt;Let&apos;s say a Spanish-speaking user lands on &lt;code&gt;example.com/about&lt;/code&gt; (your English default). Should you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redirect them to &lt;code&gt;/es/about&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Show English and let them switch manually?&lt;/li&gt;
&lt;li&gt;Something in between?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&apos;s no universally correct answer. Let&apos;s break down the options:&lt;/p&gt;
&lt;h3&gt;5.1 Redirect based on &lt;code&gt;Accept-Language&lt;/code&gt; header&lt;/h3&gt;
&lt;p&gt;The browser sends an &lt;code&gt;Accept-Language&lt;/code&gt; header with every request (e.g., &lt;code&gt;es-ES,es;q=0.9,en;q=0.8&lt;/code&gt;). You can read this server-side and redirect accordingly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Feels magical—users see their language immediately&lt;/li&gt;
&lt;li&gt;No user action required&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SEO disaster waiting to happen.&lt;/strong&gt; Googlebot sends &lt;code&gt;Accept-Language: en&lt;/code&gt;. If you redirect based on this header, Google might never index your default language pages properly. You could end up with Google indexing &lt;code&gt;/es/about&lt;/code&gt; as your canonical when you wanted &lt;code&gt;/about&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CDN caching breaks.&lt;/strong&gt; Same URL, different redirects based on headers. You need &lt;code&gt;Vary: Accept-Language&lt;/code&gt; which tanks your cache hit rate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VPN and travel users get wrong language.&lt;/strong&gt; Someone from Spain traveling in Germany might get German. Someone using a US VPN gets English even though they want Spanish.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Corporate/shared browsers lie.&lt;/strong&gt; Many corporate machines are set to &lt;code&gt;en-US&lt;/code&gt; regardless of the actual user.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The gotcha:&lt;/strong&gt; If you MUST redirect based on &lt;code&gt;Accept-Language&lt;/code&gt;, only do it on the FIRST visit and immediately set a cookie. Never redirect if a cookie already exists. And never, ever redirect if the user explicitly navigated to a prefixed URL—they chose that language.&lt;/p&gt;
&lt;h3&gt;5.2 Redirect based on cookie (returning visitors)&lt;/h3&gt;
&lt;p&gt;Store the user&apos;s language preference in a cookie when they:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manually switch languages&lt;/li&gt;
&lt;li&gt;Visit a prefixed URL (implicit preference)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On subsequent visits to non-prefixed URLs, redirect based on this cookie.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Respects explicit user choice&lt;/li&gt;
&lt;li&gt;Works great for returning visitors&lt;/li&gt;
&lt;li&gt;No header-based guessing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First-time visitors always see default language&lt;/li&gt;
&lt;li&gt;Cookie might be stale (user changed preference on another device)&lt;/li&gt;
&lt;li&gt;Still has CDN caching implications if you redirect server-side&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The gotcha:&lt;/strong&gt; Be careful with the cookie scope. If you set it on &lt;code&gt;/es&lt;/code&gt;, it might not be readable on &lt;code&gt;/&lt;/code&gt;. Always set cookies on the root path with &lt;code&gt;path=/&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;5.3 No redirect, just show the content&lt;/h3&gt;
&lt;p&gt;User lands on &lt;code&gt;/about&lt;/code&gt;? Show English. User lands on &lt;code&gt;/es/about&lt;/code&gt;? Show Spanish. No automatic redirects ever.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SEO-perfect.&lt;/strong&gt; Google sees exactly what you want it to see. No redirect chains, no confusion.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CDN-perfect.&lt;/strong&gt; Every URL is deterministic. Cache everything aggressively.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable.&lt;/strong&gt; Users always get what the URL says.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shareable links work correctly.&lt;/strong&gt; When someone shares &lt;code&gt;/about&lt;/code&gt;, everyone sees English regardless of their browser settings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First-time visitors might see the &quot;wrong&quot; language&lt;/li&gt;
&lt;li&gt;Requires clear language switcher UI&lt;/li&gt;
&lt;li&gt;Some users expect automatic detection&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The gotcha:&lt;/strong&gt; If you go this route, make your language switcher VERY visible. Don&apos;t bury it in a footer dropdown. Consider a banner for detected language mismatch: &quot;This page is available in Español →&quot;&lt;/p&gt;
&lt;h3&gt;5.4 Hybrid approach (recommended)&lt;/h3&gt;
&lt;p&gt;Here&apos;s what I recommend for most production apps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Never redirect on page load based on headers alone&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Store preference in a cookie when user switches language or visits a prefixed URL&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On root &lt;code&gt;/&lt;/code&gt; only (not other pages), consider a soft redirect for returning users with a cookie&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Show a non-intrusive language suggestion banner for first-time visitors&lt;/strong&gt; when their &lt;code&gt;Accept-Language&lt;/code&gt; doesn&apos;t match the current page&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│ 🌐 This page is available in Español. [Switch] [✕]     │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clean SEO (no redirects on content pages)&lt;/li&gt;
&lt;li&gt;Good UX for returning visitors&lt;/li&gt;
&lt;li&gt;Helpful hint for first-time visitors without being aggressive&lt;/li&gt;
&lt;li&gt;Full CDN cacheability (banner can be client-side rendered)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.5 What about localStorage?&lt;/h3&gt;
&lt;p&gt;Don&apos;t use &lt;code&gt;localStorage&lt;/code&gt; for language preference. It&apos;s not accessible server-side, so you can&apos;t use it for SSR. You&apos;d end up with a flash of wrong language on every page load while the client-side code fixes it. Cookies are the way to go.&lt;/p&gt;
&lt;h3&gt;5.6 TL;DR - Quick comparison&lt;/h3&gt;
&lt;p&gt;&amp;lt;Table
headers={[&quot;Strategy&quot;, &quot;SEO&quot;, &quot;CDN Cache&quot;, &quot;UX&quot;, &quot;Complexity&quot;]}
rows={[
[&quot;Accept-Language redirect&quot;, &quot;❌ Bad&quot;, &quot;❌ Bad&quot;, &quot;✅ Great&quot;, &quot;Medium&quot;],
[&quot;Cookie redirect&quot;, &quot;⚠️ OK&quot;, &quot;⚠️ OK&quot;, &quot;✅ Great&quot;, &quot;Medium&quot;],
[&quot;No redirect&quot;, &quot;✅ Perfect&quot;, &quot;✅ Perfect&quot;, &quot;⚠️ OK&quot;, &quot;Low&quot;],
[&quot;Hybrid&quot;, &quot;✅ Great&quot;, &quot;✅ Great&quot;, &quot;✅ Great&quot;, &quot;High&quot;],
]}
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My preference:&lt;/strong&gt; No redirect on public pages. The URL dictates the language, period. But I still save the user&apos;s preference to a cookie when they switch languages or visit a prefixed URL—this comes in handy for authenticated areas like &lt;code&gt;/dashboard&lt;/code&gt; where SEO doesn&apos;t matter and you can freely use the cookie to serve the right language without URL prefixes.&lt;/p&gt;
&lt;h2&gt;6. TanStack Start i18n Integration 🔧&lt;/h2&gt;
&lt;p&gt;Now that we&apos;ve covered the theory—prefixing strategies, redirect approaches, and SEO considerations—let&apos;s build the actual implementation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why a custom solution?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Most i18n libraries focus on translations: loading strings, pluralization, formatting. They assume you&apos;ll figure out the routing yourself. The few that do handle routing often make assumptions that don&apos;t match our requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They want to prefix all languages (we don&apos;t want &lt;code&gt;/en&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;They don&apos;t support ignored paths (we need &lt;code&gt;/dashboard&lt;/code&gt; without prefixes)&lt;/li&gt;
&lt;li&gt;They don&apos;t integrate well with TanStack Router&apos;s rewrite system&lt;/li&gt;
&lt;li&gt;They want to control the middleware layer in ways that conflict with TanStack Start&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So we&apos;re building a lightweight integration layer on top of [[use-intl|https://github.com/amannn/next-intl]]. The translation library handles translations. We handle everything else: routing, URL rewriting, cookie management, and the glue between server and client.&lt;/p&gt;
&lt;p&gt;Here&apos;s what we&apos;re building:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;packages/i18n/
├── src/
│   ├── core/
│   │   ├── shared.ts    # Config, types, utilities (used by both)
│   │   ├── client.ts    # URL rewriting, locale detection
│   │   └── server.ts    # Middleware, cookie handling
│   ├── messages/
│   │   ├── en.ts        # English translations
│   │   ├── pt.ts        # Portuguese translations
│   │   └── index.ts     # Message registry
│   ├── entry.client.ts  # Client-side exports
│   └── entry.server.ts  # Server-side exports
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s break it down piece by piece.&lt;/p&gt;
&lt;h3&gt;6.1 Why a Separate Package?&lt;/h3&gt;
&lt;p&gt;Before we dive into code, let&apos;s talk about project structure. In our monorepo, we keep i18n in a separate package: &lt;code&gt;packages/i18n&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why not just a folder in the app?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Server and client need completely different code paths:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server code imports &lt;code&gt;@tanstack/react-start/server&lt;/code&gt; (Node-only APIs)&lt;/li&gt;
&lt;li&gt;Client code uses &lt;code&gt;window.location&lt;/code&gt;, &lt;code&gt;document.cookie&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Mixing them causes bundler errors or bloated client bundles&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By creating a package with separate entry points, we keep things clean:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;@app/i18n&quot;,
  &quot;exports&quot;: {
    &quot;./client&quot;: &quot;./src/entry.client.ts&quot;,
    &quot;./server&quot;: &quot;./src/entry.server.ts&quot;,
    &quot;./messages&quot;: &quot;./src/messages/index.ts&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the app imports exactly what it needs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// In client code
import { getCurrentLocale, localizeUrl } from &apos;@app/i18n/client&apos;

// In server code (middleware)
import { handleLocaleMiddleware } from &apos;@app/i18n/server&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The bundler never pulls server code into the client bundle. No runtime errors, no bloat.&lt;/p&gt;
&lt;h3&gt;6.2 Why &lt;code&gt;use-intl&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;There are many i18n libraries out there: [[i18next|https://www.i18next.com/]], [[react-intl|https://formatjs.io/docs/react-intl/]], [[lingui|https://lingui.dev/]], [[paraglide|https://inlang.com/m/gerre34r/library-inlang-paraglideJs]]. We went with [[use-intl|https://github.com/amannn/next-intl]] for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lightweight&lt;/strong&gt; — ~2kb gzipped, minimal footprint&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript-first&lt;/strong&gt; — Full type inference for translation keys&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSR-friendly&lt;/strong&gt; — No hydration mismatches when used correctly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simple API&lt;/strong&gt; — Just &lt;code&gt;useTranslations()&lt;/code&gt; and you&apos;re done&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import { useTranslations } from &apos;use-intl&apos;

function SignInPage() {
  const t = useTranslations(&apos;auth&apos;)

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{t(&apos;signIn&apos;)}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{t(&apos;noAccount&apos;)}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The messages are plain objects, easy to maintain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const en = {
  auth: {
    signIn: &apos;Sign In&apos;,
    signUp: &apos;Sign Up&apos;,
    noAccount: &quot;Don&apos;t have an account?&quot;,
  },
  common: {
    loading: &apos;Loading...&apos;,
    error: &apos;An error occurred&apos;,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing fancy, nothing magical. Just objects and functions.&lt;/p&gt;
&lt;h3&gt;6.3 What About Paraglide?&lt;/h3&gt;
&lt;p&gt;[[Paraglide.js|https://inlang.com/m/gerre34r/library-inlang-paraglideJs]] is an excellent i18n solution worth considering. It offers compile-time extraction, a tiny runtime, and great DX. Check out their [[documentation|https://inlang.com/]] for the latest features.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (January 27, 2026):&lt;/strong&gt; Paraglide now has official support for multiple frameworks including TanStack Start, SvelteKit, Astro. Their routing integration supports flexible strategies like no-prefix for default language and ignored paths—exactly what we needed for this project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why we went with &lt;code&gt;use-intl&lt;/code&gt; for this guide:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When we originally wrote this guide, we chose &lt;code&gt;use-intl&lt;/code&gt; (from the [[next-intl|https://next-intl.dev/]] author) because we were already familiar with its patterns and wanted to demonstrate how to build custom routing integration from scratch. It&apos;s a solid library with good community support.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;That said, Paraglide is a great choice for TanStack Start projects.&lt;/strong&gt; If you prefer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automatic string extraction from code&lt;/li&gt;
&lt;li&gt;Compile-time optimizations for the smallest possible bundle&lt;/li&gt;
&lt;li&gt;Official framework adapters that handle routing for you&lt;/li&gt;
&lt;li&gt;Excellent TypeScript support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...then Paraglide might be the better fit for your project. Both libraries will get the job done well.&lt;/p&gt;
&lt;h3&gt;6.4 The Shared Foundation&lt;/h3&gt;
&lt;p&gt;Everything starts with &lt;code&gt;shared.ts&lt;/code&gt;—the config and utilities used by both server and client.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// packages/i18n/src/core/shared.ts

export const defaultLocale = &apos;en&apos;
export const supportedLocales = [&apos;en&apos;, &apos;pt&apos;] as const
export const LOCALE_COOKIE = &apos;locale&apos;

// Paths that bypass locale handling entirely
export const ignoredPathsRegex = /^\/(?:api|rpc|dashboard)(?:\/|$)/

export type Locale = (typeof supportedLocales)[number]

export function isValidLocale(locale: string | undefined): locale is Locale {
  return supportedLocales.includes(locale as Locale)
}

export function shouldIgnorePath(pathname: string): boolean {
  return ignoredPathsRegex.test(pathname)
}

export function extractLocaleFromPath(pathname: string): Locale | null {
  const match = /^\/([a-z]{2})(?:\/|$)/.exec(pathname)
  const locale = match?.[1]
  return locale &amp;amp;&amp;amp; isValidLocale(locale) &amp;amp;&amp;amp; locale !== defaultLocale ? locale : null
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;ignoredPathsRegex&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Some routes shouldn&apos;t have locale prefixes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/api/*&lt;/code&gt; — API endpoints don&apos;t need localization in the URL&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/rpc/*&lt;/code&gt; — Same for RPC handlers&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dashboard/*&lt;/code&gt; — Authenticated area, SEO doesn&apos;t matter, reads from cookie&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This regex lets us skip locale handling for these paths entirely.&lt;/p&gt;
&lt;h3&gt;6.5 TanStack Router URL Rewriting&lt;/h3&gt;
&lt;p&gt;Here&apos;s where the magic happens. TanStack Router has a &lt;code&gt;rewrite&lt;/code&gt; option that transforms URLs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/web/src/application/router.tsx
import { deLocalizeUrl, localizeUrl } from &apos;@app/i18n/client&apos;

const router = createTanStackRouter({
  routeTree,
  rewrite: {
    input: ({ url }) =&amp;gt; deLocalizeUrl(url),
    output: ({ url }) =&amp;gt; localizeUrl(url),
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What does this do?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;input&lt;/code&gt; (deLocalizeUrl): Strips the locale prefix before routing. User visits &lt;code&gt;/es/about&lt;/code&gt;, router sees &lt;code&gt;/about&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;output&lt;/code&gt; (localizeUrl): Adds the locale prefix when generating links. &lt;code&gt;&amp;lt;Link to=&quot;/about&quot;&amp;gt;&lt;/code&gt; becomes &lt;code&gt;/es/about&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why do we need this?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Without rewriting, you&apos;d have two problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Every route would need to handle both &lt;code&gt;/about&lt;/code&gt; and &lt;code&gt;/es/about&lt;/code&gt; explicitly&lt;/li&gt;
&lt;li&gt;All your &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; components would generate wrong URLs&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The rewrite functions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function deLocalizeUrl(url: URL): URL {
  if (shouldIgnorePath(url.pathname)) return url

  const locale = extractLocaleFromPath(url.pathname)
  if (locale) {
    const newUrl = new URL(url)
    newUrl.pathname = url.pathname.replace(`/${locale}`, &apos;&apos;) || &apos;/&apos;
    return newUrl
  }
  return url
}

export function localizeUrl(url: URL): URL {
  if (shouldIgnorePath(url.pathname)) return url

  const locale = getCurrentLocale()
  if (locale === defaultLocale) return url

  const newUrl = new URL(url)
  newUrl.pathname = `/${locale}${url.pathname === &apos;/&apos; ? &apos;&apos; : url.pathname}`
  return newUrl
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice how both functions respect &lt;code&gt;shouldIgnorePath()&lt;/code&gt;. Links to &lt;code&gt;/dashboard&lt;/code&gt; stay as &lt;code&gt;/dashboard&lt;/code&gt;, never &lt;code&gt;/es/dashboard&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;6.6 The Layout Route&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;/{-$locale}&lt;/code&gt; layout route is where we validate the locale param:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/web/src/routes/localized.layout.tsx
import { isValidLocale } from &apos;@app/i18n/client&apos;
import { createFileRoute, notFound, Outlet } from &apos;@tanstack/react-router&apos;

export const Route = createFileRoute(&apos;/{-$locale}&apos;)({
  beforeLoad: ({ params }) =&amp;gt; {
    const locale = params.locale
    if (locale &amp;amp;&amp;amp; !isValidLocale(locale)) {
      throw notFound()
    }
  },
  component: LocalizedLayout,
})

function LocalizedLayout() {
  return &amp;lt;Outlet /&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why do we need both this AND server middleware?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;They handle different things:&lt;/p&gt;
&lt;p&gt;&amp;lt;Table
headers={[&quot;Concern&quot;, &quot;Layout Route&quot;, &quot;Server Middleware&quot;]}
rows={[
[&quot;Validate /xyz/about → 404&quot;, &quot;✅&quot;, &quot;❌&quot;],
[&quot;Redirect /en/about → /about&quot;, &quot;❌&quot;, &quot;✅&quot;],
[&quot;Sync cookie from URL&quot;, &quot;❌&quot;, &quot;✅&quot;],
[&quot;Strip locale from /es/dashboard&quot;, &quot;❌&quot;, &quot;✅&quot;],
]}
/&amp;gt;&lt;/p&gt;
&lt;p&gt;The layout route runs inside React, after routing. It can throw &lt;code&gt;notFound()&lt;/code&gt; but can&apos;t do redirects before HTML is sent. Server middleware runs before React, so it handles redirects and cookie syncing.&lt;/p&gt;
&lt;h3&gt;6.7 Server Middleware&lt;/h3&gt;
&lt;p&gt;The server middleware handles everything that must happen before React renders:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// packages/i18n/src/core/server.ts

export function handleLocaleMiddleware(request: Request): LocaleMiddlewareResult {
  const url = new URL(request.url)
  const pathname = url.pathname

  // Skip for ignored paths
  if (shouldIgnorePath(pathname)) {
    return {}
  }

  // Redirect /en/* to /* (default locale shouldn&apos;t have prefix)
  if (pathname.startsWith(`/${defaultLocale}/`) || pathname === `/${defaultLocale}`) {
    url.pathname = pathname.replace(`/${defaultLocale}`, &apos;&apos;) || &apos;/&apos;
    return { redirect: Response.redirect(url.toString(), 301) }
  }

  const urlLocale = extractLocaleFromPath(pathname)

  if (urlLocale) {
    // Strip locale from ignored paths: /es/dashboard → /dashboard
    const strippedPath = pathname.replace(`/${urlLocale}`, &apos;&apos;) || &apos;/&apos;
    if (shouldIgnorePath(strippedPath)) {
      url.pathname = strippedPath
      return { redirect: Response.redirect(url.toString(), 301) }
    }

    // Sync cookie when URL has explicit locale
    const cookieLocale = parseLocaleCookie(request.headers.get(&apos;cookie&apos;))
    if (urlLocale !== cookieLocale) {
      return { setCookie: { name: LOCALE_COOKIE, value: urlLocale } }
    }
  }

  return {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Integration with TanStack Start:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/web/src/application/server.ts
import { handleLocaleMiddleware, createCookieHeader } from &apos;@app/i18n/server&apos;
import handler, { createServerEntry } from &apos;@tanstack/react-start/server-entry&apos;

export default createServerEntry({
  async fetch(request) {
    const { redirect, setCookie } = handleLocaleMiddleware(request)

    if (redirect) {
      return redirect
    }

    const response = await handler.fetch(request)

    if (setCookie) {
      const cookieValue = createCookieHeader(setCookie.name, setCookie.value)
      response.headers.append(&apos;Set-Cookie&apos;, cookieValue)
    }

    return response
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What this handles:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;/en/about&lt;/code&gt; → 301 redirect to &lt;code&gt;/about&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/es/dashboard&lt;/code&gt; → 301 redirect to &lt;code&gt;/dashboard&lt;/code&gt; (ignored path)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/es/about&lt;/code&gt; → Sets cookie to &lt;code&gt;es&lt;/code&gt; if not already set&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/about&lt;/code&gt; → No action needed&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.8 Client-Side Locale Detection&lt;/h3&gt;
&lt;p&gt;Getting the current locale needs to work identically on server (SSR) and client. We use TanStack&apos;s &lt;code&gt;createIsomorphicFn()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// packages/i18n/src/core/client.ts
import { getRequest } from &apos;@tanstack/react-start/server&apos;
import { createIsomorphicFn } from &apos;@tanstack/react-start&apos;

export const getCurrentLocale = createIsomorphicFn()
  .server(() =&amp;gt; {
    const request = getRequest()
    const url = new URL(request.url)

    // Dashboard reads from cookie
    if (shouldIgnorePath(url.pathname)) {
      return parseLocaleCookie(request.headers.get(&apos;cookie&apos;)) ?? defaultLocale
    }
    // Public pages read from URL
    return extractLocaleFromPath(url.pathname) ?? defaultLocale
  })
  .client(() =&amp;gt; {
    // Dashboard reads from cookie
    if (shouldIgnorePath(window.location.pathname)) {
      return parseLocaleCookie(document.cookie) ?? defaultLocale
    }
    // Public pages read from URL
    return extractLocaleFromPath(window.location.pathname) ?? defaultLocale
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why the split logic?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Public pages&lt;/strong&gt; (&lt;code&gt;/about&lt;/code&gt;, &lt;code&gt;/es/about&lt;/code&gt;): Locale comes from URL. This is the source of truth for SEO.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dashboard&lt;/strong&gt; (&lt;code&gt;/dashboard&lt;/code&gt;): No locale in URL, so we read from cookie. User&apos;s preference persists across the authenticated area.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is how we satisfy our original requirement: public pages use URL prefixes for SEO, but dashboard ignores prefixes and uses cookies.&lt;/p&gt;
&lt;h3&gt;6.9 The Provider Setup&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;IntlProvider&lt;/code&gt; wraps the app and provides translations via React context:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/web/src/components/context/context.providers.tsx
import { IntlProvider, getClientLocale } from &apos;@app/i18n/client&apos;
import { useSuspenseQuery } from &apos;@tanstack/react-query&apos;
import { useRouterState } from &apos;@tanstack/react-router&apos;

export function Providers({ children }: { children: ReactNode }) {
  const routerPathname = useRouterState({ select: (s) =&amp;gt; s.location.pathname })
  const locale = getClientLocale(routerPathname)

  const { data: messages } = useSuspenseQuery(
    rpc.i18n.messages.queryOptions({
      input: locale,
      staleTime: Infinity,
      gcTime: Infinity,
    })
  )

  return (
    &amp;lt;IntlProvider locale={locale} messages={messages}&amp;gt;
      {children}
    &amp;lt;/IntlProvider&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;useRouterState&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The provider needs to react to route changes. When navigating from &lt;code&gt;/about&lt;/code&gt; to &lt;code&gt;/es/about&lt;/code&gt;, the locale changes and we need to fetch Spanish messages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why async loading via RPC?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Messages are loaded on-demand per locale. English users never download Spanish strings. With &lt;code&gt;staleTime: Infinity&lt;/code&gt;, messages are cached forever (until page refresh).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Setting the &lt;code&gt;lang&lt;/code&gt; attribute:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/web/src/routes/root.tsx
function RootDocument({ children }: { children: ReactNode }) {
  const locale = getCurrentLocale()
  return (
    &amp;lt;html lang={locale}&amp;gt;
      &amp;lt;head&amp;gt;&amp;lt;HeadContent /&amp;gt;&amp;lt;/head&amp;gt;
      &amp;lt;body&amp;gt;{children}&amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.10 Type-Safe Localized Links&lt;/h3&gt;
&lt;p&gt;Regular &lt;code&gt;&amp;lt;Link to=&quot;/about&quot;&amp;gt;&lt;/code&gt; works but loses type safety for localized routes. We created a &lt;code&gt;LocalizedLink&lt;/code&gt; component:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/web/src/components/misc/localized-link.tsx
import { Link } from &apos;@tanstack/react-router&apos;
import type { FileRouteTypes } from &apos;@/application/routes.tree&apos;

type LocalizedFullPaths = Extract&amp;lt;FileRouteTypes[&apos;to&apos;], `/{-$locale}${string}`&amp;gt;

type StripLocalePrefix&amp;lt;T extends string&amp;gt; = T extends &apos;/{-$locale}&apos;
  ? &apos;/&apos;
  : T extends `/{-$locale}${infer Rest}`
  ? Rest
  : never

export type LocalizedTo = StripLocalePrefix&amp;lt;LocalizedFullPaths&amp;gt;

export function LocalizedLink&amp;lt;TTo extends LocalizedTo&amp;gt;({
  to,
  ...props
}: LocalizedLinkProps&amp;lt;TTo&amp;gt;) {
  const fullPath = to === &apos;/&apos; ? &apos;/{-$locale}&apos; : `/{-$locale}${to}`
  return &amp;lt;Link to={fullPath} {...props} /&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Type-safe! Only accepts routes that exist under /{-$locale}/*
&amp;lt;LocalizedLink to=&quot;/auth/sign-up&quot;&amp;gt;Sign up&amp;lt;/LocalizedLink&amp;gt;

// TypeScript error: &quot;/invalid-route&quot; doesn&apos;t exist
&amp;lt;LocalizedLink to=&quot;/invalid-route&quot;&amp;gt;Nope&amp;lt;/LocalizedLink&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The router&apos;s &lt;code&gt;rewrite.output&lt;/code&gt; handles adding the actual locale prefix. You write &lt;code&gt;/auth/sign-up&lt;/code&gt;, users see &lt;code&gt;/es/auth/sign-up&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;6.11 Adding a New Language&lt;/h3&gt;
&lt;p&gt;With everything in place, adding a new language is trivial:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Create the message file:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// packages/i18n/src/messages/es.ts
const es = {
  auth: {
    signIn: &apos;Iniciar Sesión&apos;,
    signUp: &apos;Registrarse&apos;,
    noAccount: &apos;¿No tienes una cuenta?&apos;,
  },
  common: {
    loading: &apos;Cargando...&apos;,
    error: &apos;Ocurrió un error&apos;,
  },
}

export default es
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Add to supported locales:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// packages/i18n/src/core/shared.ts
export const supportedLocales = [&apos;en&apos;, &apos;pt&apos;, &apos;es&apos;] as const
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Register in messages index:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// packages/i18n/src/messages/index.ts
import en from &apos;./en&apos;
import pt from &apos;./pt&apos;
import es from &apos;./es&apos;

export const messages: Record&amp;lt;Locale, Messages&amp;gt; = { en, pt, es }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it. No route changes, no middleware updates, no config files. The routing, links, and cookie handling all just work.&lt;/p&gt;
&lt;h2&gt;7. Wrapping Up 🎉&lt;/h2&gt;
&lt;p&gt;We built a complete i18n solution for [[TanStack Start|https://tanstack.com/start]] that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ No prefix for default language (&lt;code&gt;/about&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ Prefix for other languages (&lt;code&gt;/es/about&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ Ignored paths read from cookie (&lt;code&gt;/dashboard&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ SEO-friendly with proper URLs per language&lt;/li&gt;
&lt;li&gt;✅ Type-safe translations and links&lt;/li&gt;
&lt;li&gt;✅ SSR-compatible with no hydration issues&lt;/li&gt;
&lt;li&gt;✅ Easy to add new languages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key insight: i18n isn&apos;t just about translations. It&apos;s about routing, redirects, cookies, and making all these pieces work together. By building on [[use-intl|https://github.com/amannn/next-intl]] and [[TanStack Router|https://tanstack.com/router]]&apos;s rewrite system, we get a solution that&apos;s both flexible and maintainable. Now its time to get that SEO score up!&lt;/p&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;How do I implement i18n in TanStack Start with TanStack Router&apos;s rewrite system?&quot;, answer: &quot;Use TanStack Router&apos;s rewrite option with deLocalizeUrl (strips locale prefix before routing) and localizeUrl (adds prefix when generating links). Create a layout route with the optional {-$locale} parameter to validate locale codes. This way /es/about is internally routed as /about, and all Link components automatically generate locale-prefixed URLs.&quot; },
{ question: &quot;How does use-intl compare to Paraglide for i18n in TanStack Start?&quot;, answer: &quot;use-intl (from the next-intl author) is a lightweight ~2kb library with a simple useTranslations API and SSR support. Paraglide.js offers compile-time string extraction, smaller runtime bundles, and now has official TanStack Start support with built-in routing integration. use-intl requires manual routing setup while Paraglide can handle routing automatically through its framework adapters.&quot; },
{ question: &quot;How does TanStack Router handle optional locale prefixes in routes?&quot;, answer: &quot;TanStack Router uses the {-$locale} syntax for optional path parameters. A route defined as /{-$locale}/about matches both /about (no locale, uses default) and /es/about (with locale). The beforeLoad hook validates that the parameter is an actual locale code and throws notFound() for invalid values like /random-page.&quot; },
{ question: &quot;How should a TanStack Start app handle locale detection and cookies for i18n?&quot;, answer: &quot;Use server middleware to redirect /en/* to /* (default locale should not have a prefix), sync a locale cookie when users visit prefixed URLs, and strip locale prefixes from ignored paths like /dashboard. On the client, use createIsomorphicFn to detect locale from the URL on public pages and from cookies on dashboard pages where SEO does not matter.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>tanstack</category><category>tanstack-start</category><category>i18n</category><category>react</category><category>typescript</category><category>vite</category><author>Pedro Martins</author></item><item><title>Browser Clutch</title><link>https://nikuscs.com/projects/08-browser-clutch/</link><guid isPermaLink="true">https://nikuscs.com/projects/08-browser-clutch/</guid><description>A browser picker for macOS. Route URLs to different browsers based on app or domain rules. Open work links in Chrome, personal in Safari — you decide.</description><pubDate>Sat, 24 Jan 2026 00:00:00 GMT</pubDate><category>macos</category><category>swift</category><category>productivity</category><category>menu-bar-app</category><author>Pedro Martins</author></item><item><title>Twenty Years Building for the Web: From PHP&apos;s Simple Server Rendering to TypeScript&apos;s Modern Complexity</title><link>https://nikuscs.com/blog/12-20-years-building-for-the-web-php-to-typescript/</link><guid isPermaLink="true">https://nikuscs.com/blog/12-20-years-building-for-the-web-php-to-typescript/</guid><description>A personal journey through two decades of web development. From mixing PHP in templates to managing fullstack TypeScript. Exploring the trade-offs, the complexity, and why there&apos;s no clear winner.</description><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Introduction&lt;/h2&gt;
&lt;p&gt;I have been a PHP &amp;amp; Laravel Developer since I wrote my first lines of code almost &lt;strong&gt;&lt;em&gt;2 decades ago&lt;/em&gt;&lt;/strong&gt;. I always loved doing things for the web, share it, and see it online. Languages like C++, Java, Python never really appealed to me because they were mostly used for backend or visual &quot;offline&quot; projects. I wanted to &lt;strong&gt;build for the web&lt;/strong&gt;, for people to see and use.&lt;/p&gt;
&lt;p&gt;If you are reading this and you are old enough, you know how good it was to mix and match PHP on top of your templates &amp;amp; files, interpolate variables, and just call it a day. HTML was there for you, SIMPLE! I miss those days honestly! And it was totally &quot;safe&quot; &lt;strong&gt;cough&lt;/strong&gt; &lt;strong&gt;cough&lt;/strong&gt;. 😅&lt;/p&gt;
&lt;p&gt;As we move on over the years we start to take care of more things: SQL injection, XSS, CSRF. We continue our journey to be more secure, more performant, more maintainable, more scalable. The list goes on.&lt;/p&gt;
&lt;p&gt;Then we start to see &quot;frameworks&quot; like Yii, CodeIgniter, Symfony, Laravel, etc. which were more &quot;modular&quot; and &quot;opinionated&quot; but still had a lot of flexibility and control over the code. This way we could benefit from their already done functionalities, but still have the ability to customize and extend them to our own needs. How amazing is that?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php 
$title = &apos;Hello World&apos;;
$description = &apos;This is a description&apos;;
$items = mysqli_query($conn, &quot;SELECT * FROM items&quot;);
?&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;{$title}&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;{$description}&amp;lt;/p&amp;gt;
    &amp;lt;ul&amp;gt;
        &amp;lt;?php foreach ($items as $item): ?&amp;gt;
            &amp;lt;li&amp;gt;{$item[&apos;name&apos;]}&amp;lt;/li&amp;gt;
        &amp;lt;?php endforeach; ?&amp;gt;
    &amp;lt;/ul&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. PHP + Laravel Golden Age&lt;/h2&gt;
&lt;p&gt;There was a time when Laravel offered the &lt;strong&gt;BEST developer experience&lt;/strong&gt; you could find (and still does!). Why? Because we had little to no JavaScript on our projects. Things just worked.&lt;/p&gt;
&lt;p&gt;We&apos;d use [[jQuery|https://jquery.com/]] + [[Bootstrap|https://getbootstrap.com/]], render Blade templates, make a few Ajax calls, and call it a day. Database, Queues, Events. &lt;strong&gt;&lt;em&gt;Everything in ONE language and ONE framework&lt;/em&gt;&lt;/strong&gt;. No mental overhead, no context switching, no learning multiple ecosystems. Just PHP and Laravel.&lt;/p&gt;
&lt;p&gt;We might have &lt;strong&gt;peaked here&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Routes
Route::get(&apos;/&apos;, [ItemController::class, &apos;index&apos;]);
// Controller
public function index()
{
    $title = &apos;Hello World&apos;;
    $description = &apos;This is a description&apos;;
    $items = Item::all();
    return view(&apos;index&apos;, compact(&apos;title&apos;, &apos;items&apos;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;{{ $title }}&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;{{ $description }}&amp;lt;/p&amp;gt;
    &amp;lt;ul&amp;gt;
        @foreach ($items as $item)
            &amp;lt;li&amp;gt;{{ $item[&apos;name&apos;] }}&amp;lt;/li&amp;gt;
        @endforeach
    &amp;lt;/ul&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. The Rise of Javascript&lt;/h2&gt;
&lt;p&gt;But the world never stops. For good or bad, we got pushed into doing MORE on the client side. More JavaScript! Interactive websites became the &quot;standard&quot;. While jQuery lasted a long time, frameworks like [[React|https://react.dev/]], [[Vue|https://vuejs.org/]], and [[Angular|https://angular.dev/]] took over the frontend world. The reasoning? Browsers could handle the heavy rendering, freeing up server resources for more requests.&lt;/p&gt;
&lt;p&gt;This brought the era of APIs: JSON, GraphQL, REST. Now you needed to write API endpoints to serve data to different clients, not just browsers. Developers split into camps. Backend folks handled APIs and business logic, while Frontend devs crafted interactive UIs, real-time dashboards, and modern web applications.&lt;/p&gt;
&lt;p&gt;JavaScript was still manageable at this point. We had task runners like [[Gulp|https://gulpjs.com/]] and [[Grunt|https://gruntjs.com/]] to bundle, compress, and minify our files into single or multiple outputs. Simple enough.&lt;/p&gt;
&lt;p&gt;But as you&apos;ll see in the next section, things were about to get way more complicated. 😬&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Gulp file example
gulp.task(&apos;build&apos;, function() {
    return gulp.src(&apos;src/**/*.js&apos;)
        .pipe(gulp.dest(&apos;dist&apos;))
        .pipe(uglify())
        .pipe(rename({ suffix: &apos;.min&apos; }))
        .pipe(gulp.dest(&apos;dist&apos;));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Frontend Complexity with Javascript&lt;/h2&gt;
&lt;p&gt;Well, you think everything was wonderful, right? 😅 Not really! Engineers always find ways to complicate things (job security, am I right?).&lt;/p&gt;
&lt;p&gt;With the rise of JavaScript came a &lt;strong&gt;&lt;em&gt;tsunami of complexity&lt;/em&gt;&lt;/strong&gt;. We needed to figure out bundling, optimization, rendering strategies, and package distribution. Enter [[Webpack|https://webpack.js.org/]], [[Rollup|https://rollupjs.org/]], [[Parcel|https://parceljs.org/]], and later [[Vite|https://vitejs.dev/]]. And with them? The legendary &lt;strong&gt;&lt;code&gt;node_modules&lt;/code&gt; folder&lt;/strong&gt;! The heaviest object in the universe, deeper than the Mariana Trench! 🌊&lt;/p&gt;
&lt;p&gt;But why all this complexity? Gulp and Grunt were just task runners that merged and minified files. Modern bundlers are smarter (and way more complicated): code splitting, tree shaking, dead code elimination, minification, source maps, hot module replacement. The list goes on and on and on.&lt;/p&gt;
&lt;p&gt;And don&apos;t forget the browser wars! You had to support Chrome, Firefox, Safari, and god forbid... IE11. Your code needed transpiling (hello [[Babel|https://babeljs.io/]]!), polyfills for older browsers, and different builds for different targets. What worked in Chrome might break in Safari. Fun times!&lt;/p&gt;
&lt;p&gt;The build process went from &lt;strong&gt;&quot;save and refresh&quot;&lt;/strong&gt; to &lt;strong&gt;&lt;em&gt;&quot;save, wait 30 seconds for Webpack to rebuild, pray it doesn&apos;t error, then refresh.&quot;&lt;/em&gt;&lt;/strong&gt; The DX we had with PHP&apos;s instant feedback? Gone. Welcome to the world of build times and configuration hell.&lt;/p&gt;
&lt;p&gt;Webpack configs became 500+ lines of cryptic magic nobody understood. Just copy-paste from StackOverflow and pray. NPM brought its own drama: the left-pad incident that broke the internet, package hijacking, security vulnerabilities 15 levels deep in dependencies you didn&apos;t know existed. 💀&lt;/p&gt;
&lt;p&gt;And the tooling fatigue! Every 6 months a new bundler claiming to be &quot;10x faster&quot;: Webpack → Parcel → Snowpack → Vite → Turbopack → ??? Even CSS got complicated: plain files → Sass/Less → CSS Modules → CSS-in-JS → [[Tailwind|https://tailwindcss.com/]]. Styling became a build step!&lt;/p&gt;
&lt;p&gt;The framework wars didn&apos;t help: React vs Vue vs Angular vs Svelte. Each with their own ecosystem, build tools, and &quot;best practices&quot; that changed yearly. Exhausting! 😮‍💨&lt;/p&gt;
&lt;h2&gt;5. Node.js Enters the Chat&lt;/h2&gt;
&lt;p&gt;Then came [[Node.js|https://nodejs.org/]]. Ryan Dahl took Chrome&apos;s V8 engine and brought JavaScript to the server. The pitch was seductive: &lt;strong&gt;&lt;em&gt;&quot;Write JavaScript everywhere! Share code between frontend and backend! One language for everything&quot;&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;[[Express.js|https://expressjs.com/]] made it simple to build APIs. Later came [[Fastify|https://fastify.dev/]] for performance, [[NestJS|https://nestjs.com/]] for structure (heavily inspired by Angular), and countless other frameworks. The ecosystem exploded. NPM became the largest package registry in the world. JavaScript was no longer just a browser language. It was a legitimate backend contender.&lt;/p&gt;
&lt;p&gt;For me as a Laravel and PHP developer, this created an interesting dilemma. Do we keep Laravel &amp;amp; PHP for the backend while still benefiting from the JS ecosystem for the frontend? That&apos;s what I have done for a few years till now and still do. We have clear boundaries between the two, and we can still benefit from the JS ecosystem for the frontend.&lt;/p&gt;
&lt;p&gt;But we also get the overhead of managing two different ecosystems, two different languages, two different &quot;build&quot; tools, and so on. Not very bueno!&lt;/p&gt;
&lt;p&gt;On the other side, the promise of &quot;JavaScript everywhere&quot; was appealing, but it came with trade-offs. Node&apos;s async/callback hell (before async/await), different paradigms, and the lack of Laravel&apos;s batteries-included approach meant you were building everything from scratch or stitching together dozens of packages. This is where the complexity really starts to kick in.&lt;/p&gt;
&lt;h3&gt;5.1 The SSR Complexity&lt;/h3&gt;
&lt;p&gt;With Node.js came the possibility of Server-Side Rendering (SSR). Remember how simple PHP was? Write your template, render on the server, send HTML to the browser. Done.&lt;/p&gt;
&lt;p&gt;SSR in JavaScript? Not so simple. Now you need to render React/Vue on the server, send HTML to the browser, then &lt;strong&gt;&quot;hydrate&quot;&lt;/strong&gt; it on the client so it becomes interactive. Sounds easy? Well, you need to handle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server and client builds (two separate bundles)&lt;/li&gt;
&lt;li&gt;Hydration mismatches (server HTML doesn&apos;t match client)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;window is not defined&lt;/code&gt; errors everywhere&lt;/li&gt;
&lt;li&gt;Memory leaks on the server&lt;/li&gt;
&lt;li&gt;Data fetching on both server and client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Meta-frameworks like [[Next.js|https://nextjs.org/]] and [[Nuxt|https://nuxt.com/]] eventually made this easier, but the complexity is still there under the hood. What used to be simple server rendering in PHP became a complex dance of server/client coordination. We gained interactivity but lost simplicity. 😅&lt;/p&gt;
&lt;h2&gt;6. Laravel Inertia JS - Bridging the Gap&lt;/h2&gt;
&lt;p&gt;Laravel wasn&apos;t going to sit still while the JavaScript world took over. [[Inertia.js|https://inertiajs.com]] by Jonathan Reinink is a clever solution that lets you build modern single-page applications using classic server-side routing and controllers. &lt;strong&gt;No need to build a separate API&lt;/strong&gt;. No REST endpoints. No GraphQL schemas. 🎯&lt;/p&gt;
&lt;p&gt;Inertia acts as the glue between your Laravel backend and your React, Vue, or Svelte frontend. You return data from your controllers, and Inertia handles the client-side rendering. It feels like traditional server-side rendering but with the interactivity of an SPA. You get the best of both worlds: Laravel&apos;s elegant backend with modern JavaScript frameworks on the frontend.&lt;/p&gt;
&lt;p&gt;For many Laravel developers, this is the sweet spot. You could keep your PHP expertise, maintain your existing Laravel patterns, and still deliver the modern UX that clients demanded. No need to rewrite everything in Node.js. No need to maintain two separate applications. Just Laravel doing what it does best, with JavaScript sprinkled on top where it matters.&lt;/p&gt;
&lt;p&gt;I&apos;m a BIG fan of InertiaJS, being one of the early adopters of the framework, contributing to the community and helping others to get started with it. If you have any questions about InertiaJS, feel free to join our [[Discord Server|https://discord.gg/inertiajs]] and ask away! ❤️&lt;/p&gt;
&lt;p&gt;Here&apos;s a quick example of how Inertia works:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Laravel Controller
class UserController extends Controller
{
    public function show(User $user)
    {
        return Inertia::render(&apos;Users/Show&apos;, [
            &apos;user&apos; =&amp;gt; $user,
            &apos;posts&apos; =&amp;gt; $user-&amp;gt;posts()-&amp;gt;latest()-&amp;gt;get(),
        ]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// React Component (Users/Show.tsx)
import { Head } from &apos;@inertiajs/react&apos;

// Option 1: No types 😢
interface Props {
  user: any
  posts: any[]
}

// Option 2: Manual types - but you need to keep them in sync! 🔄
interface User {
  id: number
  name: string
  email: string
}

interface Post {
  id: number
  title: string
  content: string
}

interface PropsTyped {
  user: User
  posts: Post[]
}

export default function Show({ user, posts }: PropsTyped) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Head title={user.name} /&amp;gt;
      &amp;lt;h1&amp;gt;{user.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{user.email}&amp;lt;/p&amp;gt;
      
      &amp;lt;div&amp;gt;
        {posts.map(post =&amp;gt; (
          &amp;lt;div key={post.id}&amp;gt;
            &amp;lt;h2&amp;gt;{post.title}&amp;lt;/h2&amp;gt;
          &amp;lt;/div&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple and elegant! But notice the problem? You can write your own types manually, but now you need to &lt;strong&gt;&lt;em&gt;keep them in sync&lt;/em&gt;&lt;/strong&gt; with your backend. Add a field in your Laravel model? You need to remember to update the TypeScript interface. Rename a property? Better not forget to change it in both places! This manual sync is error-prone and tedious. 😓&lt;/p&gt;
&lt;p&gt;But how do we keep things typesafe automatically? How do we close the gap between our PHP Backend and our Javascript frontend STILL being typesafe? Let&apos;s jump into typescript section for a while and come back to this topic later.&lt;/p&gt;
&lt;h2&gt;7. TypeScript - JavaScript That Scales&lt;/h2&gt;
&lt;p&gt;In 2012, Microsoft released [[TypeScript|https://www.typescriptlang.org/]], led by Anders Hejlsberg (the creator of C#). The pitch? &lt;strong&gt;JavaScript with optional static typing&lt;/strong&gt;. At first, many developers were skeptical. &quot;Why would I need types in JavaScript? It&apos;s supposed to be flexible!&quot;&lt;/p&gt;
&lt;p&gt;But as JavaScript applications grew larger and more complex, the cracks started to show. Refactoring was terrifying. You&apos;d rename a property and hope you caught all the references. Runtime errors that could&apos;ve been caught at compile time. No autocomplete or IntelliSense worth mentioning. The developer experience was rough for large codebases.&lt;/p&gt;
&lt;p&gt;TypeScript changed that. Suddenly you had type safety, interfaces, generics, and incredible tooling. Your IDE could catch errors before you even ran the code. Refactoring became safe. Large teams could work on the same codebase without stepping on each other&apos;s toes.&lt;/p&gt;
&lt;p&gt;The adoption was gradual but steady. Angular 2+ went all-in on TypeScript. React added first-class support. Vue 3 was rewritten in TypeScript. [[Next.js|https://nextjs.org/]], [[Remix|https://remix.run/]], NestJS. All TypeScript by default. Even Node.js now has built-in TypeScript support with type stripping.&lt;/p&gt;
&lt;p&gt;Today, TypeScript isn&apos;t just popular, &lt;strong&gt;&lt;em&gt;it&apos;s the default&lt;/em&gt;&lt;/strong&gt;. Most new JavaScript projects start with TypeScript. It&apos;s become what JavaScript should have been from the start: a language that scales from small scripts to massive applications. For me coming from typed languages like PHP (with its type hints), TypeScript feels like coming home.&lt;/p&gt;
&lt;p&gt;But it&apos;s not perfect. Typescript also brings additional complexity to the table with false sense of security and type safety, while bloating part of your codebase with types everywhere :p&lt;/p&gt;
&lt;h2&gt;8. Full Stack Laravel + Typescript = The perfect combo?&lt;/h2&gt;
&lt;p&gt;While working with Inertia, I felt the need to also have my frontend fully typesafe. So when I changed a DTO or a Request/Response, it would reflect on the frontend that the property was not there anymore or it was changed. How cool is that for developers?&lt;/p&gt;
&lt;p&gt;This is where [[Laravel Data|https://spatie.be/docs/laravel-data]] by Spatie comes in. It pairs perfect with [[Spatie Typescript Transformer|https://github.com/spatie/laravel-typescript-transformer]], so everytime you change your Data Object it would &lt;strong&gt;generate a typescript file&lt;/strong&gt; with all your definitions and types for your data objects that you can use in your frontend! Yay! We got it right? Yeah partially!&lt;/p&gt;
&lt;p&gt;Here is an example of a Data Object in Laravel → Laravel Data → Typescript Transformer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
namespace App\Data;

use Closure;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Lazy;

/**
 * The data shared from the server to the client
 * when the user is logged in, includes wallet,
 * notifications, and everything about the user
 */
final class UserAuthenticatedData extends Data
{
    public function __construct(
        public readonly ?UserData $user,
        public readonly ?WalletData $wallet,
        public readonly Lazy|int|Closure $notifications_count = 0,
    ) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// UserAuthenticatedData.ts
export interface UserAuthenticatedData {
    user: UserData | null;
    wallet: WalletData | null;
    notifications_count: number | Lazy&amp;lt;number&amp;gt; | Closure;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.ts
import laravel from &apos;laravel-vite-plugin&apos;;
import { run } from &apos;vite-plugin-run&apos;
export default defineConfig(({ isSsrBuild }) =&amp;gt; {
    return {
    plugins: [
      laravel({
        input: [
          &apos;resources/application/main.tsx&apos;,
          &apos;resources/application/css/app.css&apos;
        ],
        ssr: &apos;resources/application/ssr.tsx&apos;,
        refresh: true,
      }),
      run({
        silent: true,
        input: [
          {
            name: &apos;Generate TypeScript Types&apos;,
            run: [&apos;php&apos;, &apos;artisan&apos;, &apos;typescript:transform&apos;],
            pattern: [
              &apos;app/**/*Data.php&apos;,
              &apos;app/**/Enums/**/*.php&apos;,
              &apos;app/**/Http/Requests/**/*Request.php&apos;,
              &apos;app/**/Http/Responses/**/*Response.php&apos;,
              &apos;app/**/Http/Views/**/*View.php&apos;,
            ],
            onFileChanged: () =&amp;gt; console.info(&apos;[Generators] Generating TypeScript Types 🎨&apos;)
          },
        ]
      })
    ],
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But this also comes at a cost. While you could use the generated types, you will also be required to create a Laravel Data DTO or a form request for EVERY piece that you send to the frontend. This could quickly become a PITA = Overhead! Also it&apos;s not out-of-the-box. You would still need to setup all the packages, transforms, get vite dev server to work, etc.&lt;/p&gt;
&lt;p&gt;But at least, we got types and the best of both worlds! We have closed the gap between our PHP Backend and our Javascript frontend and we can still benefit from the JS ecosystem for the frontend. But we STILL not there yet in terms of developer experience!&lt;/p&gt;
&lt;p&gt;There are also a few issues with this approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We get only types, what if we want to [[Zod|https://zod.dev/]] Schemas or [[Standard Schemas|https://github.com/standard-schema/standard-schema]]? humm&lt;/li&gt;
&lt;li&gt;What if we need to derive certain types? Omit, Pick, etc? We would still need to write types on the frontend.&lt;/li&gt;
&lt;li&gt;How about Generics? We can&apos;t use them in PHP because we don&apos;t really have generics. So we would need to type the generics on the DTOS with some weird string syntax that is not typesafe :(&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Well we got to a point here. While it&apos;s not a perfect solution, for me it&apos;s a good sweet spot if you wanna keep yourself in the Laravel ecosystem and still benefit from the JS ecosystem for the frontend. I would love to see an official &quot;solution&quot; from Laravel to make this even better. Let&apos;s hope so!&lt;/p&gt;
&lt;h2&gt;9. FullStack Typescript&lt;/h2&gt;
&lt;p&gt;Well, I have tried a ton of JS/TS full stack approaches in the past 3 years in attempt to mimic our good beloved Laravel. While it&apos;s not possible to fully mimic it, I have found a few approaches that are very close to Laravel in terms of DX and developer experience.&lt;/p&gt;
&lt;p&gt;While it&apos;s easy to fall into the hype train of certain frameworks and tools specially if you are active on [[X|https://x.com]] (twitter), I have tried to stay away from it and focus on trying out myself.&lt;/p&gt;
&lt;p&gt;Here is what I have tried so far and what I have found:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;[[Next.js|https://nextjs.org/]]&lt;/strong&gt;: The #1 Framework for React, Huge Ecosystem, great docs, avg DX. But Next lost me with all the unnecessary complexity with the RSC approach, feels like a step back from the &quot;old&quot; React. Also a bit of vendor lock-in with Vercel, while some might say otherwise, that&apos;s a fact.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Hono|https://hono.dev/]]/[[Elysia|https://elysiajs.com/]]&lt;/strong&gt;: Both are GREAT! You can build a really nice API in no time, fully typed, OpenAPI spec compliant, Swagger Docs ready for you, it&apos;s fast, and it&apos;s simple yet powerful.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Tanstack Start|https://tanstack.com/start]]&lt;/strong&gt;: The goat of the frameworks for me, super flexible, fully typed end to end, doesn&apos;t get in your way, easy mental model, great community &amp;amp; fully open-source to the core.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Vue|https://vuejs.org/]]/[[Nuxt|https://nuxt.com/]]&lt;/strong&gt;: Pretty amazing also, I would honestly pick Vue/Nuxt any day over React if the ecosystem was the same. Here I said it :p&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So my current sweet spot is Tanstack Start + Elysia for the API with RPC calls to the backend. This way we can have the best of both worlds, fully typed end to end. You still have an API if you need it, still can call it from the frontend, full SSR without any overhead, streaming, server actions, one single language, one single codebase in a monorepo using [[Turborepo|https://turbo.build/]], etc.&lt;/p&gt;
&lt;p&gt;Here&apos;s a quick example of how beautiful the DX is with Elysia + Treaty RPC:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Backend API with Elysia
import { Elysia, t } from &apos;elysia&apos;

const app = new Elysia()
  .get(&apos;/users/:id&apos;, async ({ params }) =&amp;gt; {
    const user = await db.user.findUnique({ 
      where: { id: params.id } 
    })
    return { user }
  }, {
    params: t.Object({
      id: t.String()
    })
  })
  .post(&apos;/users&apos;, async ({ body }) =&amp;gt; {
    const user = await db.user.create({ data: body })
    return { user }
  }, {
    body: t.Object({
      name: t.String(),
      email: t.String()
    })
  })
  .listen(3000)

export type App = typeof app
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Frontend with Treaty RPC in Tanstack Router - Fully typed!
import { createFileRoute } from &apos;@tanstack/react-router&apos;
import { treaty } from &apos;@elysiajs/eden&apos;
import type { App } from &apos;./server&apos;

const api = treaty&amp;lt;App&amp;gt;(&apos;localhost:3000&apos;)

export const Route = createFileRoute(&apos;/users/$id&apos;)({
  beforeLoad: async ({ params }) =&amp;gt; {
    // Autocomplete and type safety everywhere! 🎉
    const { data } = await api.users({ id: params.id }).get()
    //     ^? { user: User }
    
    return { user: data.user }
  },
  component: UserProfile
})

function UserProfile() {
  const { user } = Route.useLoaderData()
  //      ^? User - fully typed!
  
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{user.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{user.email}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what I mean by &lt;strong&gt;&lt;em&gt;fully typed end to end&lt;/em&gt;&lt;/strong&gt;. Change the backend, and your frontend immediately knows about it. No code generation, no build steps, just pure TypeScript magic! The data flows from your API through your router loader directly into your components, all with perfect type safety.&lt;/p&gt;
&lt;p&gt;What do I miss here from Laravel? Queues we got to install [[BullMQ|https://docs.bullmq.io/]] or [[Trigger.dev|https://trigger.dev/]], Auth with [[better-auth|https://www.better-auth.com/]], etc. So you need to glue different pieces together to get the same DX, but once your &quot;boilerplate&quot; is done, I would say it&apos;s a tie!&lt;/p&gt;
&lt;h2&gt;10. PHP ( Laravel ) vs Javascript/Typescript&lt;/h2&gt;
&lt;p&gt;This is a tough one, because both ecosystems are great and I can&apos;t really stand with one side or another. Laravel ecosystem is perfect, and while smaller vs Javascript, this is not a problem if you see it from a different perspective. Because it&apos;s not fragmented like Javascript, you will get first party support for most of the things you need but you will also miss some things or get a few things &quot;late&quot; compared to Javascript.&lt;/p&gt;
&lt;p&gt;I&apos;ll keep this short and give my honest opinion on both ecosystems: ⚖️&lt;/p&gt;
&lt;h3&gt;10.1 PHP ( Laravel ) Pros ✅&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Batteries included: Queues, Events, Jobs, Notifications, ORMs, Auth, Mail etc&lt;/li&gt;
&lt;li&gt;Top-tier Documentation&lt;/li&gt;
&lt;li&gt;Amazing DX &amp;amp; tooling: [[Telescope|https://laravel.com/docs/telescope]], [[Horizon|https://laravel.com/docs/horizon]], [[Forge|https://forge.laravel.com/]], [[Vapor|https://vapor.laravel.com/]], etc&lt;/li&gt;
&lt;li&gt;Scales pretty well with large projects&lt;/li&gt;
&lt;li&gt;PHP is mature and very organized in terms of code structure and best practices.&lt;/li&gt;
&lt;li&gt;Dependency Injection is a first class citizen, you can easily inject dependencies into your controllers, services, etc&lt;/li&gt;
&lt;li&gt;If you embrace the framework and its patterns, you will be able to build very maintainable and scalable applications, buttery smooth!&lt;/li&gt;
&lt;li&gt;[[Eloquent|https://laravel.com/docs/eloquent]] is THE ORM!&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10.2 PHP ( Laravel ) Cons ❌&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;It&apos;s still PHP, so you still get that gap if you wanna use Javascript for the frontend.&lt;/li&gt;
&lt;li&gt;Things tend to come a bit &quot;late&quot; compared to Javascript, say AI tools, etc. Example: [[Prism|https://github.com/echolabsdev/prism]] is amazing but it&apos;s not yet comparable to [[Vercel AI SDK|https://sdk.vercel.ai/]] for example.&lt;/li&gt;
&lt;li&gt;Serverless is not as mature as Javascript, but it&apos;s getting there&lt;/li&gt;
&lt;li&gt;Local Env is still meh, you need to rely on [[Herd|https://herd.laravel.com/]], Brew to install PHP, nginx, fpm etc. You could also go with &lt;code&gt;php artisan serve&lt;/code&gt;, but yeah, you get the point here.&lt;/li&gt;
&lt;li&gt;Smaller community means also less packages, etc.&lt;/li&gt;
&lt;li&gt;No generics in PHP, we need to rely on [[PHPStan|https://phpstan.org/]] to get that perfect static analysis and type safety.&lt;/li&gt;
&lt;li&gt;While you can, if you go out of the &quot;laravel-way&quot; you will be a bit on your own. If that&apos;s the case use Symfony, there I said it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10.3 Javascript/Typescript Pros ✅&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Huge Ecosystem: Millions of packages, frameworks, tools, libraries, etc.&lt;/li&gt;
&lt;li&gt;AI tools are first party in Javascript + Python, so we get them first here&lt;/li&gt;
&lt;li&gt;Universal, with correct shared code and setup, you can share parts of your codebase between frontend and backend. Example types, little utilities, etc. This is a huge advantage!&lt;/li&gt;
&lt;li&gt;Deployment + Serverless is more mature than Laravel, you can deploy to [[Vercel|https://vercel.com/]], [[Netlify|https://www.netlify.com/]], AWS, GCP, etc. First party support for most of the platforms, unlike PHP :(&lt;/li&gt;
&lt;li&gt;One file is enough to start with, example with [[Bun|https://bun.sh]] + index.ts you&apos;re ready to go!&lt;/li&gt;
&lt;li&gt;Fully typed end to end with typescript, zod, standard schemas, etc.&lt;/li&gt;
&lt;li&gt;HMR is really nice, get instant feedback while you change a file.&lt;/li&gt;
&lt;li&gt;SSR, Sockets, Streaming out of the box&lt;/li&gt;
&lt;li&gt;More jobs and higher salaries (could be wrong here but that&apos;s what we see so far)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10.4 Javascript/Typescript Cons ❌&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Fragmented Ecosystem: Hard to pick a winner sometimes, lots of options also makes it harder to choose and learn.&lt;/li&gt;
&lt;li&gt;New boy in the block is common, new framework pops up and then a year later it&apos;s gone, risky takes.&lt;/li&gt;
&lt;li&gt;Composability is a double edged sword, you can build very modular and reusable code, but you can also build very complex and hard to maintain code if you don&apos;t do it right.&lt;/li&gt;
&lt;li&gt;Harder to do Dependency Injection, tools like [[Effect|https://effect.website/]] will probably solve this, but still babies in the scene :p&lt;/li&gt;
&lt;li&gt;NPM is great but the latest supply chain attacks and vulnerabilities are a bit of a pain, so we need to be careful with what we install and what we use.&lt;/li&gt;
&lt;li&gt;Multiple runtimes, you can use Bun, [[Node.js|https://nodejs.org/]], [[Deno|https://deno.com/]], but also pitfalls when using one over the other.&lt;/li&gt;
&lt;li&gt;Error Handling &amp;amp; Stack traces are a pain compared to PHP.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;11. Conclusion&lt;/h2&gt;
&lt;p&gt;Like many said, we should avoid framework dramas. At the end of the day being an engineer, you should be able to &lt;strong&gt;&lt;em&gt;pick the right tool for the job&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If I would still need a quick app or dashboard I would probably pick Laravel + [[Filament|https://filamentphp.com/]] because it gets the job done fast and easy. But if I would need a more complex application with fancy UI &amp;amp; realtime capabilities I would probably pick Tanstack Start + Elysia.&lt;/p&gt;
&lt;p&gt;I really hope PHP will get more &quot;modern&quot; features like Generics and gets closer to the Javascript DX. Also I would love if the Javascript ecosystem would focus on being more mature and laravel-like for a defacto standard (I see you [[AdonisJS|https://adonisjs.com/]]!) 👀&lt;/p&gt;
&lt;p&gt;At the end, both ecosystems are amazing, and we should be grateful to have such great tools at our disposal. Keep learning, keep building, and most importantly, keep having fun!&lt;/p&gt;
&lt;h2&gt;12. Credits &amp;amp; Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Laravel|https://laravel.com/]]&lt;/li&gt;
&lt;li&gt;[[Inertia.js|https://inertiajs.com]]&lt;/li&gt;
&lt;li&gt;[[Laravel Data|https://spatie.be/docs/laravel-data]]&lt;/li&gt;
&lt;li&gt;[[Spatie Typescript Transformer|https://github.com/spatie/laravel-typescript-transformer]]&lt;/li&gt;
&lt;li&gt;[[Filament|https://filamentphp.com/]]&lt;/li&gt;
&lt;li&gt;[[PHPStan|https://phpstan.org/]]&lt;/li&gt;
&lt;li&gt;[[Telescope|https://laravel.com/docs/telescope]]&lt;/li&gt;
&lt;li&gt;[[Horizon|https://laravel.com/docs/horizon]]&lt;/li&gt;
&lt;li&gt;[[Forge|https://forge.laravel.com/]]&lt;/li&gt;
&lt;li&gt;[[Vapor|https://vapor.laravel.com/]]&lt;/li&gt;
&lt;li&gt;[[Eloquent|https://laravel.com/docs/eloquent]]&lt;/li&gt;
&lt;li&gt;[[Herd|https://herd.laravel.com/]]&lt;/li&gt;
&lt;li&gt;[[jQuery|https://jquery.com/]]&lt;/li&gt;
&lt;li&gt;[[Bootstrap|https://getbootstrap.com/]]&lt;/li&gt;
&lt;li&gt;[[React|https://react.dev/]]&lt;/li&gt;
&lt;li&gt;[[Vue|https://vuejs.org/]]&lt;/li&gt;
&lt;li&gt;[[Angular|https://angular.dev/]]&lt;/li&gt;
&lt;li&gt;[[Gulp|https://gulpjs.com/]]&lt;/li&gt;
&lt;li&gt;[[Grunt|https://gruntjs.com/]]&lt;/li&gt;
&lt;li&gt;[[Webpack|https://webpack.js.org/]]&lt;/li&gt;
&lt;li&gt;[[Rollup|https://rollupjs.org/]]&lt;/li&gt;
&lt;li&gt;[[Parcel|https://parceljs.org/]]&lt;/li&gt;
&lt;li&gt;[[Vite|https://vitejs.dev/]]&lt;/li&gt;
&lt;li&gt;[[Babel|https://babeljs.io/]]&lt;/li&gt;
&lt;li&gt;[[Tailwind|https://tailwindcss.com/]]&lt;/li&gt;
&lt;li&gt;[[TypeScript|https://www.typescriptlang.org/]]&lt;/li&gt;
&lt;li&gt;[[Node.js|https://nodejs.org/]]&lt;/li&gt;
&lt;li&gt;[[Express.js|https://expressjs.com/]]&lt;/li&gt;
&lt;li&gt;[[Fastify|https://fastify.dev/]]&lt;/li&gt;
&lt;li&gt;[[NestJS|https://nestjs.com/]]&lt;/li&gt;
&lt;li&gt;[[Next.js|https://nextjs.org/]]&lt;/li&gt;
&lt;li&gt;[[Nuxt|https://nuxt.com/]]&lt;/li&gt;
&lt;li&gt;[[Remix|https://remix.run/]]&lt;/li&gt;
&lt;li&gt;[[Bun|https://bun.sh]]&lt;/li&gt;
&lt;li&gt;[[Deno|https://deno.com/]]&lt;/li&gt;
&lt;li&gt;[[Tanstack Start|https://tanstack.com/start]]&lt;/li&gt;
&lt;li&gt;[[Elysia|https://elysiajs.com/]]&lt;/li&gt;
&lt;li&gt;[[Hono|https://hono.dev/]]&lt;/li&gt;
&lt;li&gt;[[Zod|https://zod.dev/]]&lt;/li&gt;
&lt;li&gt;[[Standard Schemas|https://github.com/standard-schema/standard-schema]]&lt;/li&gt;
&lt;li&gt;[[BullMQ|https://docs.bullmq.io/]]&lt;/li&gt;
&lt;li&gt;[[Trigger.dev|https://trigger.dev/]]&lt;/li&gt;
&lt;li&gt;[[better-auth|https://www.better-auth.com/]]&lt;/li&gt;
&lt;li&gt;[[Effect|https://effect.website/]]&lt;/li&gt;
&lt;li&gt;[[Vercel AI SDK|https://sdk.vercel.ai/]]&lt;/li&gt;
&lt;li&gt;[[Prism|https://github.com/echolabsdev/prism]]&lt;/li&gt;
&lt;li&gt;[[Vercel|https://vercel.com/]]&lt;/li&gt;
&lt;li&gt;[[Netlify|https://www.netlify.com/]]&lt;/li&gt;
&lt;li&gt;[[AdonisJS|https://adonisjs.com/]]&lt;/li&gt;
&lt;li&gt;[[Turborepo|https://turbo.build/]]&lt;/li&gt;
&lt;li&gt;[[InertiaJS Discord|https://discord.gg/inertiajs]]&lt;/li&gt;
&lt;li&gt;[[Laracasts|https://laracasts.com/]]&lt;/li&gt;
&lt;li&gt;[[TanStack Discord|https://discord.gg/tanstack]]&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>laravel</category><category>php</category><category>javascript</category><category>typescript</category><category>tanstack</category><category>nextjs</category><category>bun</category><category>vite</category><category>react</category><category>vue</category><author>Pedro Martins</author></item><item><title>The restless mind of thinkers, how obsession, curiosity, and chaos can be a gift</title><link>https://nikuscs.com/blog/11-the-restless-mind-of-thinkers/</link><guid isPermaLink="true">https://nikuscs.com/blog/11-the-restless-mind-of-thinkers/</guid><description>For the thinkers who can&apos;t switch off, where obsession meets creation, and chaos becomes the spark for something greater.</description><pubDate>Sat, 11 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Introduction&lt;/h2&gt;
&lt;p&gt;You know the feeling when your brain just &lt;strong&gt;can&apos;t shut up&lt;/strong&gt;? Or time is never enough for what you want to learn or do? Well, that&apos;s my story as well. While this article might look weird or out of place, it&apos;s a story of how &lt;strong&gt;obsession, curiosity, and chaos&lt;/strong&gt; can be a gift sometimes, but also a curse.&lt;/p&gt;
&lt;p&gt;Did it happen that you close your laptop but you&apos;re still coding in your mind endlessly? Recently I finished watching [[Queen&apos;s Gambit|https://www.imdb.com/title/tt10048342/]] and I could relate myself to it, but unfortunately I can&apos;t visualize [[VSCode|https://code.visualstudio.com/]] or [[PHPStorm|https://www.jetbrains.com/phpstorm/]] on the roof of my room. Instead, I can plan, architect, design, and build something in my mind. Maybe prepare what I&apos;m doing the next day? Solve problems in my mind or just think about what I will learn next?&lt;/p&gt;
&lt;p&gt;Is this a problem? Or is it just the art of being a thinker &amp;amp; the love for learning? Well, I don&apos;t know, but do whatever drives you forward. If it makes you happy, it&apos;s a gift. 🧠&lt;/p&gt;
&lt;h2&gt;2. The Storm&lt;/h2&gt;
&lt;p&gt;Sometimes it starts with something small, a random idea that hits you while you&apos;re doing something completely unrelated. Maybe in the shower, maybe drinking coffee, or even taking a shit, and suddenly you need to open your laptop or grab your phone to search for something. It&apos;s like a spark that won&apos;t leave you alone.&lt;/p&gt;
&lt;p&gt;Did you ever want to stop a movie because your brain was somewhere else and all you wanted to do was pause and get that database schema aligned, fearing you might not have the same clean thinking tomorrow? Or have you started to refactor a little bit of your code and ended up in a pile of code, [[GitHub|https://github.com]] issues, pull requests, and an amazing deep dive into the codebase? Well, that&apos;s fun!&lt;/p&gt;
&lt;p&gt;I know it sounds chaotic, but that chaos is also where &lt;strong&gt;the best ideas are born&lt;/strong&gt;.
It&apos;s the moment where logic meets obsession and suddenly, you&apos;re not forcing creativity, but instead &lt;strong&gt;you&apos;re riding it&lt;/strong&gt;. You don&apos;t even feel like you&apos;re working anymore.&lt;/p&gt;
&lt;p&gt;Yeah, it drains us sometimes. But honestly? I live for those moments. That feeling when everything clicks, when you&apos;re in that unstoppable flow, that&apos;s what keeps me coming back.
Maybe that&apos;s what the creative storm really is: proof that you care deeply about what you&apos;re doing. It&apos;s messy, it&apos;s loud, it&apos;s exhausting, but it&apos;s real, and for me that&apos;s where the real magic happens.&lt;/p&gt;
&lt;h2&gt;3. Learning - The Real Oxygen&lt;/h2&gt;
&lt;p&gt;I don&apos;t really &quot;decide&quot; to learn, it just happens. I&apos;ll be scrolling on [[X|https://x.com]], or watching a video, and something grabs me. Next thing I know, I&apos;ve got ten tabs open, half a [[Notion|https://notion.so]] page filled with notes, and a new course I probably won&apos;t finish (but at least I started it).&lt;/p&gt;
&lt;p&gt;For me it&apos;s not about needing to know everything, it&apos;s about not being able to ignore curiosity. I feel restless if I go too long without learning something new. Some people relax by watching a show or going out. I relax by discovering, by figuring out how something works, by knowing the internals of something.&lt;/p&gt;
&lt;p&gt;I know it sounds intense, but for me, &lt;strong&gt;learning feels like breathing&lt;/strong&gt;. It resets me. Even when I&apos;m tired, if I find something that sparks my curiosity, it gives me energy again. It&apos;s like my mind goes, &quot;Oh well! Finally, something to chew on.&quot;
Sometimes I wonder if I&apos;m chasing too much: new tools, new frameworks, new ideas. But then I remember that &lt;strong&gt;curiosity is what keeps me moving&lt;/strong&gt;. It&apos;s what keeps me alive.&lt;/p&gt;
&lt;p&gt;Is learning just a skill? Or is it a pulse?&lt;/p&gt;
&lt;h2&gt;4. AI - Infinite Learning&lt;/h2&gt;
&lt;p&gt;Something that completely changed how I learn was realizing how much AI can help me stay curious forever. A lot of people see AI as a shortcut or a threat, but I see it as a partner. It&apos;s not here to replace thinking, it&apos;s here to remove friction.&lt;/p&gt;
&lt;p&gt;When I get stuck, I no longer waste hours searching or waiting for the right moment. I can just ask, explore, and keep moving. Whether it&apos;s debugging code, understanding a concept, or brainstorming an idea, AI helps me stay in motion. It keeps curiosity alive. 🤖&lt;/p&gt;
&lt;p&gt;The best part is that it never judges. You can ask the same question ten times in ten different ways until it clicks. You can go deep into topics that used to feel out of reach and actually understand them at your own pace.&lt;/p&gt;
&lt;p&gt;AI doesn&apos;t make you less human, it makes you &lt;strong&gt;more capable&lt;/strong&gt; of focusing on what truly matters: creating, learning, and thinking. It keeps the fire of curiosity burning and turns every question into a path forward.&lt;/p&gt;
&lt;p&gt;We finally live in a time where &lt;strong&gt;learning never really ends&lt;/strong&gt;, and that might be the greatest gift of all.&lt;/p&gt;
&lt;p&gt;There are no excuses anymore!&lt;/p&gt;
&lt;h2&gt;5. Stopping = Weakness?&lt;/h2&gt;
&lt;p&gt;One thing I&apos;ve learned the hard way: stopping doesn&apos;t mean you&apos;ve lost momentum or you are weak per se.
I used to think that if I wasn&apos;t working, learning, or creating, I was falling behind. That if I stopped, someone else out there was already doing the thing I just paused. It&apos;s easy to fall for the trap, especially if you are on [[X|https://x.com]] or [[Bluesky|https://bsky.app]] where you see the community moving forward with a zillion ideas getting implemented. That mindset pushed me forward for a while, until it started to eat me alive.&lt;/p&gt;
&lt;p&gt;There are nights when I&apos;m deep in a problem, exhausted, but still pushing because I feel like I should finish it. And every time I do that, I end up making twice as many mistakes and rewriting everything the next day. Funny thing, when I finally step away, take a walk, smoke a cigarette, or even just sleep on it, the answer shows up like it was waiting for me to shut up for five minutes. Step back to go forward!&lt;/p&gt;
&lt;p&gt;It took me a long time to realize that &lt;strong&gt;rest isn&apos;t weakness, it&apos;s strategy&lt;/strong&gt;.
The brain keeps working in the background even when we stop forcing it. Sometimes &lt;strong&gt;stepping away is the most productive thing&lt;/strong&gt; you can do.&lt;/p&gt;
&lt;p&gt;Now, when I feel that inner voice telling me to push harder, I try to ask myself: am I pushing forward, or just running on fumes? If it&apos;s the latter, I shut it down and let things breathe. Because real growth doesn&apos;t come from nonstop motion, it comes from rhythm.&lt;/p&gt;
&lt;p&gt;Stopping isn’t giving up. It’s making space for your next move.&lt;/p&gt;
&lt;h2&gt;6. Burnout or Laziness?&lt;/h2&gt;
&lt;p&gt;This one is confusing because they often look the same. You feel unmotivated, you slow down, and you start wondering what’s wrong with you. But burnout and laziness come from two very different places.&lt;/p&gt;
&lt;p&gt;When I&apos;m burned out, I still want to work. I still care. I just can&apos;t get my mind to click on something. My focus is gone, my brain feels heavy, and even simple things start to feel impossible. &lt;strong&gt;That&apos;s not lack of effort, that&apos;s just your system shutting down&lt;/strong&gt; after running too long. It&apos;s your body saying &quot;enough.&quot;&lt;/p&gt;
&lt;p&gt;Laziness is different. Laziness happens when I could do something, but I choose not to. When I tell myself I’m “resting” or “waiting for inspiration,” but really I’m just avoiding the hard parts of the process. Learning is uncomfortable. Growth feels awkward. And sometimes people disguise that fear as burnout because it sounds better than admitting they don’t want to struggle.&lt;/p&gt;
&lt;p&gt;It took me a while to admit that to myself. There were times I said I was tired, but deep down I was just scared to start. Scared to be bad at something new, scared to open that documentation, or scared because Jared from [[Bun|https://bun.sh]] knows [[Zig|https://ziglang.org/]] and I would never get there! lol&lt;/p&gt;
&lt;p&gt;Now I try to ask myself two simple questions:&lt;/p&gt;
&lt;p&gt;Am I tired because I’ve given my all?&lt;/p&gt;
&lt;p&gt;Or am I avoiding something because it’s hard?&lt;/p&gt;
&lt;p&gt;If I’m tired, I rest. If I’m avoiding, I start. Even small progress is better than waiting for the “right moment.”&lt;/p&gt;
&lt;p&gt;Because &lt;strong&gt;real burnout needs healing, but laziness needs honesty&lt;/strong&gt;. One asks for recovery, the other asks for courage.&lt;/p&gt;
&lt;h2&gt;7. Curiosity &amp;gt; Talent&lt;/h2&gt;
&lt;p&gt;I’ve never seen myself as naturally talented. Most of the things I know today came from falling into rabbit holes, breaking stuff, and figuring out how to fix it again. Curiosity did what talent couldn’t.&lt;/p&gt;
&lt;p&gt;I&apos;m a self-taught developer, and all I did was to start toying around with small [[OGame|https://lobby.ogame.gameforge.com/]] Scripts back in the 2000s, and here we are today, still learning, still exploring, still building. Sometimes it&apos;s easier to just sit down and watch a football match or a movie than to actually build something.&lt;/p&gt;
&lt;p&gt;Talent helps, sure, but curiosity never runs out. It doesn&apos;t care if you&apos;re the smartest person in the room. It just wants to understand. That&apos;s what keeps you moving forward when others stop. The people who grow the fastest aren&apos;t always the most gifted, they&apos;re the ones who keep asking &quot;why&quot; long after everyone else has moved on.&lt;/p&gt;
&lt;p&gt;I’ve met people way more talented than me who gave up because learning got hard or slow. But curiosity doesn’t care about perfect conditions. It doesn’t wait for motivation. It just pulls you in until you find your own way through.&lt;/p&gt;
&lt;p&gt;That&apos;s the thing for me: &lt;strong&gt;talent might give you a head start, but curiosity keeps you in the race&lt;/strong&gt;. It&apos;s what makes you explore one more idea, read one more page, or test one more version of something that still doesn&apos;t work.&lt;/p&gt;
&lt;p&gt;Curiosity isn&apos;t about being the best. &lt;strong&gt;It&apos;s about staying interested&lt;/strong&gt;.
And if you stay interested long enough, you eventually become the best at what you love, not because of talent, but because &lt;strong&gt;you never stopped learning&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;8. There are people around you&lt;/h2&gt;
&lt;p&gt;When you are obsessed with learning and building, it is easy to disappear into your own world. You start chasing progress so hard that you forget life is happening around you. I have been there and I&apos;m still there sometimes, skipping calls, ignoring texts, telling people &quot;I&apos;m busy&quot; for weeks that turn into months. It is not intentional, it is just that the mind gets stuck in its own loop of goals and ideas. ❤️&lt;/p&gt;
&lt;p&gt;But every time I take a break and meet a friend, talk to family, or just share a meal, I realize how much I have been missing. Those small moments recharge something that no tutorial, project, or bit of progress ever could.&lt;/p&gt;
&lt;p&gt;It is not about choosing between your passion and the people around you, it is about remembering that both can exist together. The same curiosity that drives you to learn can also help you connect. Ask people about their stories, listen, pay attention. Sometimes the best ideas and the deepest motivation come from those simple conversations.&lt;/p&gt;
&lt;p&gt;At the end of the day, &lt;strong&gt;success means nothing if you have no one to share it with&lt;/strong&gt;. The people who care about you are part of your journey too. They remind you to laugh, to slow down, and to stay human.&lt;/p&gt;
&lt;p&gt;Keep learning, keep building, but remember to look up once in a while. Send that message. Call that friend. The world outside your thoughts matters just as much as the one you are creating inside them.&lt;/p&gt;
&lt;p&gt;IMPORTANT: Re-read this twice please.&lt;/p&gt;
&lt;h2&gt;9. Conclusion&lt;/h2&gt;
&lt;p&gt;At the end of the day, the restless mind isn&apos;t something to fix. It is something to understand and live with. Curiosity, obsession, chaos, they are not flaws, they are signals that you care deeply about what you do.&lt;/p&gt;
&lt;p&gt;If you feel tired, rest. If you feel stuck, learn. If you feel alone, reach out. The balance is never perfect, and it never will be. But maybe that&apos;s the point. The storm, the silence, the breakthroughs, the people, it&apos;s all part of the same rhythm. ✨&lt;/p&gt;
&lt;p&gt;So keep thinking, keep learning, and keep creating.
&lt;strong&gt;Not to be the best, but to stay alive in the process of becoming.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PS: This article was brought to you in a late night, by a restless mind.&lt;/p&gt;
</content:encoded><category>mindset</category><category>creativity</category><category>chaos</category><category>obsession</category><category>curiosity</category><author>Pedro Martins</author></item><item><title>Programming with AI using Claude Code, Roo Code &amp; Cursor 🤖</title><link>https://nikuscs.com/blog/10-ai-workflow-with-claude-code-roocode-gpt/</link><guid isPermaLink="true">https://nikuscs.com/blog/10-ai-workflow-with-claude-code-roocode-gpt/</guid><description>Curious about my workflow using AI? Let&apos;s dive into Claude Code, Roo Code &amp; Cursor and discover how these tools supercharge my development process! 🚀</description><pubDate>Wed, 27 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Callout from &quot;@components/Callout.astro&quot;;
import Button from &quot;@components/Button.astro&quot;;&lt;/p&gt;
&lt;h2&gt;1. Introduction ✍️&lt;/h2&gt;
&lt;p&gt;It feels like ages ago, but the &quot;first&quot; release of [[ChatGPT|https://openai.com/chatgpt]] by [[OpenAI|https://openai.com]] was just a year and a half ago—crazy, isn&apos;t it? 🤯 While some people still deny or try to avoid AI because they don&apos;t yet see much value in it, I was actually one of the first, among my small group of developers, to embrace it. At the beginning, some of them actually laughed at me, but now &lt;strong&gt;all&lt;/strong&gt; of them use some sort of AI daily 😂&lt;/p&gt;
&lt;p&gt;I was &lt;strong&gt;super motivated&lt;/strong&gt; since day one by how even the early versions of ChatGPT could help me out with repetitive tasks. Remember converting a PHP array to JavaScript manually? Crazy, isn&apos;t it? Just paste it in GPT and ask! Remember wasting hours finding that missing comma that made your project break so hard? Those days are &lt;strong&gt;long gone&lt;/strong&gt;! 🎉&lt;/p&gt;
&lt;p&gt;So let&apos;s &lt;strong&gt;deep dive&lt;/strong&gt; into part of my workflow using AI—how I use it, how I approach it, and how I set up my tools. I&apos;ll probably miss some topics, but I&apos;ll try to keep this article up-to-date.&lt;/p&gt;
&lt;h2&gt;2. Tools 🛠️&lt;/h2&gt;
&lt;p&gt;It&apos;s 2025, August, and we have a couple of tools available for us to use. There are a &lt;strong&gt;TON&lt;/strong&gt; of them available, but honestly, I don&apos;t have &quot;time&quot; yet to test them all. Some of them are also prototypes or not well-polished yet, so I&apos;ll use whatever I feel is polished and saves my time! ⚡&lt;/p&gt;
&lt;p&gt;I&apos;ll focus on 3-4 important tools that I use daily: [[Cursor|https://cursor.com]], [[Claude Code|https://claude.ai/code]], [[Roo Code|https://roocode.com]] (Cline) &amp;amp; [[GPT|https://openai.com]].&lt;/p&gt;
&lt;h3&gt;2.1. Cursor 💻&lt;/h3&gt;
&lt;p&gt;[[Cursor|https://cursor.com]] was &lt;strong&gt;AMAZING&lt;/strong&gt; when it was released, but the VC money needs to be paid at some point 💸. I can&apos;t deny that I really &lt;strong&gt;ENJOYED&lt;/strong&gt; the first days of Cursor, when it was still used by a small subset of people, where we actually had the chance to even debate prices with the CEO himself! As time went by and with the &quot;Vibe Coding&quot; hype that was going on [[Twitter|https://twitter.com]], it was a matter of time before the price was raised, and they would start capping people who were abusing the free and paid tiers.&lt;/p&gt;
&lt;p&gt;[[Cursor|https://cursor.com]] is my main editor at the moment, and I still believe it&apos;s a good tool for the price that we pay, even with the 500 requests per month, but unfortunately, it doesn&apos;t last me for the whole month if I use it daily.&lt;/p&gt;
&lt;p&gt;Here&apos;s what I &lt;strong&gt;love&lt;/strong&gt; about Cursor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Autocomplete&lt;/strong&gt; - For me, this is worth $10 alone. I was previously using [[Supermaven|https://supermaven.com]] (now part of Cursor too) and was paying $10 per month for it, so yeah, the Tab + Autocomplete is just &lt;strong&gt;out of this world&lt;/strong&gt; and there&apos;s no competition so far. [[GitHub Copilot|https://github.com/features/copilot]] is nice, but Cursor tab is just &lt;strong&gt;nuts&lt;/strong&gt; 🔥&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI + Agent UI&lt;/strong&gt; - This is really nice. While Claude Code is a nice agent, I love being able to simply drag files, tag files, see what we edited, accept changes, and so on. Some people are nerdy enough to say you could do everything in a terminal, yeah, but we&apos;re in 2025—let&apos;s use UIs too! They&apos;re great and save us time 💫&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VSCode Ecosystem&lt;/strong&gt; - Being able to use Cursor with [[VSCode|https://code.visualstudio.com]] extensions is just great. You still benefit from the &lt;strong&gt;HUGE&lt;/strong&gt; VSCode ecosystem. No brainer for me 🧠&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent&lt;/strong&gt; - Okay-ish. While there&apos;s still a lot of room to improve, the Agent looks okay and does the job most of the time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codebase Indexing + Docs&lt;/strong&gt; - This is a really nice feature! You can index your codebase and your docs, so when you ask or delegate a task, it actually knows exactly how to search &quot;files&quot; and &quot;docs&quot; to find the best answer, making it way better instead of including all the codebase/files in the context.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I &lt;strong&gt;don&apos;t like&lt;/strong&gt; about Cursor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The price&lt;/strong&gt; - 500 requests that include tool calls and whatnot might be gone fast if you push it too much. Use your requests wisely! 💸&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No support&lt;/strong&gt; (or partial support) for your own models/API keys. While this isn&apos;t a big deal for everyone, I would love to see this happening at some point, but it&apos;s not likely to happen soon since it&apos;s their core business.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A bit slow&lt;/strong&gt; and laggy sometimes 🐌&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wish there was better support&lt;/strong&gt; for multiple mode switching and sub-tasks, etc., so we could use the context in a smarter way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context cap&lt;/strong&gt; - Cursor charges more for the &quot;Max&quot; model, which are basically the same models but with higher context.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I believe after the backlash from the community following the price raise, Cursor started to listen more to the community, and they&apos;re actually trying to ship some nice features these days, so let&apos;s keep an eye on this one 👀.&lt;/p&gt;
&lt;h3&gt;2.2. Claude Code 🧠&lt;/h3&gt;
&lt;p&gt;Well, well, this is the &lt;strong&gt;big boy&lt;/strong&gt; as of today (time of this post). [[Anthropic|https://anthropic.com]] really &lt;strong&gt;nailed it&lt;/strong&gt; with [[Claude Code|https://claude.ai/code]]! I must admit, initially I didn&apos;t like the idea of having it in the terminal and was actually expecting a VSCode extension, but I was wrong—this is actually a &lt;strong&gt;really nice tool&lt;/strong&gt;! 🎯&lt;/p&gt;
&lt;p&gt;[[Anthropic|https://anthropic.com]] has been leading the AI coding models for a while now, and they did it again with Claude Code + [[Claude Sonnet|https://www.anthropic.com/claude]]. I would say most of the time it does the tasks that I ask &lt;strong&gt;perfectly&lt;/strong&gt;, with very few exceptions.&lt;/p&gt;
&lt;p&gt;Here&apos;s what I &lt;strong&gt;love&lt;/strong&gt; about Claude Code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Minimalist&lt;/strong&gt; - Being in a terminal fits well. It can work with &quot;any&quot; IDE because it&apos;s not an actual addon, so it just works—it&apos;s fast and easy to use ⚡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can be installed anywhere&lt;/strong&gt; you want. Need to code while traveling or at the coffee shop? You bet! Install Claude Code on your old MacBook or [[Raspberry Pi|https://www.raspberrypi.org]] and you&apos;re good to go! (I actually do this behind a [[Tailscale|https://tailscale.com]] VPN) 🏖️&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent&lt;/strong&gt; - I&apos;m still not sure what they did here with Claude Code, but the agent is just &lt;strong&gt;so nice&lt;/strong&gt;. I actually ran the same task with Sonnet 4 in different tools, and Claude Code did it better most of the time. They have some black magic behind it 🪄&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Unlimited&quot;&lt;/strong&gt; - They claim unlimited for the max plan. If you do &quot;fair&quot; usage, you could probably use this all day long without being rate limited. Just make sure you edit your button colors yourself and don&apos;t delegate it to the agent... &lt;strong&gt;PLEASE&lt;/strong&gt;! 😅&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I &lt;strong&gt;don&apos;t like&lt;/strong&gt; about Claude Code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nothing?&lt;/strong&gt; - Nothing really. I&apos;m not sure what to say here. Its just great!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A UI please?&lt;/strong&gt; - I would love to see a UI for Claude Code.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3. Roo Code 🦘&lt;/h3&gt;
&lt;p&gt;[[Roo Code|https://roocode.com]] (fork of [[Cline|https://github.com/cline/cline]]) is a VSCode Extension that&apos;s getting more and more popular, and for good reason. It&apos;s &lt;strong&gt;open-source&lt;/strong&gt;, it doesn&apos;t opine about the models, &lt;strong&gt;fully customizable&lt;/strong&gt;, and it has a really nice agent as well! 🎨&lt;/p&gt;
&lt;p&gt;Here&apos;s what I &lt;strong&gt;love&lt;/strong&gt; about Roo Code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Open-source&lt;/strong&gt; - Yeah, you can actually contribute and help shape the future of Roo Code, fork it, or do whatever you want with it! 🔓&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customizable&lt;/strong&gt; - You can customize the agent rules, prompts, create new modes, and so on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bring your own keys!&lt;/strong&gt; - This is really nice! You can plug your [[OpenRouter|https://openrouter.ai]] keys, [[Anthropic|https://anthropic.com]] keys, and even connect it to Claude Code!! 🔑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mode Switching&lt;/strong&gt; - This is really a &lt;strong&gt;killer feature&lt;/strong&gt; I would like to see in Cursor. You can basically Plan in the Architect mode, make it perfect, and then it will switch automatically to the Code mode and start executing the task. Here you have the chance to actually use nice models for the planning and then switch to a cheaper model for the actual coding—you could save some nice money here! 💰&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tasks&lt;/strong&gt; - Roo Code can also &quot;divide&quot; a bigger scope into multiple small tasks while still retaining part of the context of what it has done and what&apos;s remaining to do. This is really nice and avoids polluting the context with too much stuff when you&apos;re trying to implement a feature that contains a lot of things to do. It can fully go &lt;strong&gt;auto-pilot mode&lt;/strong&gt;, do the small tasks, switch back, and continue where it left off with mostly zero effort! Really nice feature here! 🚗&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codebase Indexing&lt;/strong&gt; - Recently added, you can index your codebase with [[Qdrant|https://qdrant.tech/]]. It will be like Cursor in this case.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost Control&lt;/strong&gt; - Since you bring your own keys, you can control the cost, how much you spend, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I &lt;strong&gt;don&apos;t like&lt;/strong&gt; about Roo Code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The UI&lt;/strong&gt; still needs some work—it could be a bit more polished, but it&apos;s getting better, and hey, it&apos;s &lt;strong&gt;open-source&lt;/strong&gt;, okay? We can contribute to it! 🛠️&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Laggy&lt;/strong&gt; sometimes 🐌&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4. OpenAI - GPT 🤖&lt;/h3&gt;
&lt;p&gt;While I don&apos;t use [[OpenAI Codex|https://openai.com/codex]] or [[GPT|https://openai.com/gpt-4]] often for coding or hard-core coding, I tend to use GPT in the browser to investigate quick things, quick questions, let it &quot;web-search&quot; for me topics that I&apos;m curious about, compare choices like frameworks, libraries, and so on. I would say it&apos;s my new &quot;[[Google|https://google.com]]&quot; 😄&lt;/p&gt;
&lt;h3&gt;2.5. Why not VSCode, Copilot, Devin, Windsurf, etc? 🤷‍♂️&lt;/h3&gt;
&lt;p&gt;Well, at some point you gotta pick what you&apos;re comfortable with and stick with something. If you keep jumping between a zillion tools, you won&apos;t be actually productive. So nothing against other tools—I&apos;m sharing here what I use personally and what I think is the best fit for me.&lt;/p&gt;
&lt;p&gt;I also created [[Days Since Last VSCode Fork|https://dayssincelastvscodefork.com/]] if you&apos;re curious about how long it&apos;s been since the last time some VC-backed company forked VSCode 😂&lt;/p&gt;
&lt;h3&gt;2.6. Picking the right tool—how do I do it? 🎯&lt;/h3&gt;
&lt;p&gt;Well, usually I do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; - Actual features (important ones), bootstrap projects, and ideas. Bug finding, complex tasks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cursor&lt;/strong&gt; - Quick tasks in a file or two, trying to save up some requests, or when I need to tag a few files and I&apos;m bored to actually do it on Claude Code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Roo Code&lt;/strong&gt; - When I want a proper plan + execution and I want to explore some ideas while giving Claude Code some time to breathe.&lt;/li&gt;
&lt;li&gt;Often I could delegate different tasks (as long as they don&apos;t touch the same files) to Claude Code + Cursor + Roo Code at the same time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. Prompting 📝&lt;/h2&gt;
&lt;p&gt;This is a &lt;strong&gt;very important&lt;/strong&gt; topic! Most of the time, people are lazy enough to prompt. It&apos;s known that if you don&apos;t prompt correctly, you will probably also get bad results. LLMs are &lt;strong&gt;not magic&lt;/strong&gt;—they need context, and you need to clearly explain what you want to achieve and what&apos;s your goal with a specific task. Sometimes you need to give it a little &quot;hint&quot; to point it in the right direction, and that makes a &lt;strong&gt;lot of difference&lt;/strong&gt; in the results! 🎯&lt;/p&gt;
&lt;p&gt;Some people debate whether you waste more time prompting and battling the AI while you could do the code yourself—that&apos;s correct! It happened to all of us. Sometimes you just wish you would have written the code yourself and you would probably spend less time, but I also think, how much time did I actually save? I would probably end up &lt;strong&gt;net positive&lt;/strong&gt;! 😛&lt;/p&gt;
&lt;p&gt;If you&apos;re lazy, ensure that you have some prompts saved somewhere that you could easily copy and paste or get ideas from! 💡&lt;/p&gt;
&lt;p&gt;Here are some examples of basic prompts I do often:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; Ensure methods are single worded lower case if possible. If we are in &quot;Browser&quot;, you dont need a method called &quot;browserCleanup&quot; -&amp;gt; cleanup() simple!!!
- You will be taking advantage of typescript return types and things that are infered by typescript without needing to define the return types unless its really needed.
- You will ensure best pratices for async patterns and promise patterns in JS, so avoiding awaits in for loops and prefer Promise.all()
- Avoid redundant Variables
- Ensure we dont do .push for example in async patterns that could lead to weird behaviour.
- Organize the types, avoid nesting types and extract smaller pieces like Arrays or Array of Objects into a new type to ensure we can read the types better.
- Dont create unecessary functions, unless we really need. Keep it inline whenever possible.
- Avoid more then 3 nested ?? null coalesce for example.
- Prefer ?? over || 
- Document methods with what they do, but dont write the params types on the signature. Keep it short.
- Remove any useless comments in the code that provide no value
- Avoid esle if and prefer early returns.
- If you need a lot of ifs, probably use ts-pattern package so we can use a &quot;match&quot;
- Lint and typecheck the file in question to ensure it gets passed the check tests
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;You will streamline this extractor, in a standard way so every other extract would follow the same patterns.

Here are some rules:

- Ensure if the map is complex, to see if we could streamline it
- attempt to avoid for loops, prefer promisses
- If always follow the same pattern for returning errors for example create a createError() for example.
- We want the following api clean!
- If needed you can create a file for each mapper, in this case create a folder next to the instagram, leaving the crawler a minimal &quot;interface&quot;:
-- src/crawlers/api/github-crawler.ts
-- src/integrations/github/github-post.ts
-- src/integrations/github/github-profile.ts
With the way above, the code is really clean and each extractor as its own place.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s about it—you don&apos;t need to write a full and super-duper detailed prompt, just the minimum effort to get the job done.&lt;/p&gt;
&lt;h2&gt;4. Project Rules 📋&lt;/h2&gt;
&lt;p&gt;Well, with 3 different tools and probably more to come during this year, and all of them having their own rules file (which is a &lt;strong&gt;bad idea&lt;/strong&gt;, to be honest), we want to make sure that our project rules are the same for all of them at all times.&lt;/p&gt;
&lt;p&gt;You probably already know what project rules are, but they basically define the &quot;base&quot; instructions that you want AI to follow in each conversation/agent/session. This will guarantee that you will get greater results, and AI will follow (hopefully 😅) these rules.&lt;/p&gt;
&lt;p&gt;I really hope [[agents.md|https://agents.md/]] will be a thing in the future so we can stop this madness of having to create a rules file for each tool. If just &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;vite.config.ts&lt;/code&gt;, &lt;code&gt;.eslintrc&lt;/code&gt;, &lt;code&gt;.prettierrc&lt;/code&gt;, etc., are not enough—just 3 more files, bro! &lt;strong&gt;PLEASE&lt;/strong&gt;! 😩&lt;/p&gt;
&lt;p&gt;To overcome this (and because sometimes you don&apos;t want to ask your teammates to add 30 files and folders to &lt;code&gt;.gitignore&lt;/code&gt;, or you&apos;re scared they will fire you because you&apos;re using AI tools), you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add a global gitignore that will ignore the &lt;code&gt;.roo&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, and &lt;code&gt;.cursor&lt;/code&gt; for every project you touch.&lt;/li&gt;
&lt;li&gt;Or you can convince them to use [[Ruler|https://github.com/intellectronica/ruler]], a nice tool that will apply your rules to all other &quot;tools&quot; that you might want. So you have a single file that is kept in &quot;sync&quot; with all other tools.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&apos;s my config for Ruler:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .ruler/ruler.toml
[agents.claude]
enabled = true
output_path = &quot;CLAUDE.md&quot;

[agents.cursor]
enabled = true
output_path = &quot;.cursor/rules/global.mdc&quot;

[agents.cline]
enabled = true
output_path = &quot;.roo/rules/global.md&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;---
description: My Monorepo - Project Structure &amp;amp; Rules
globs: **/*
alwaysApply: true
---
# My Monorepo - Project Structure &amp;amp; Rules 📑

## Overview

A Turbo monorepo for a web app to save and search stuff from various sources using AI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then on your package.json you can add the following command to apply the rules:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
  &quot;ruler&quot;: &quot;ruler apply --agents claude,cursor,cline&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it! When you change your base instructions, you can just run &lt;code&gt;bun ruler&lt;/code&gt; and it will apply the rules to all other tools.&lt;/p&gt;
&lt;h3&gt;4.1. Generating Project Rules ⚙️&lt;/h3&gt;
&lt;p&gt;This is probably &quot;basic&quot; for most people (probably). You should use &lt;code&gt;/claude init&lt;/code&gt; or ask Cursor to generate the rules for you, &lt;strong&gt;BUT&lt;/strong&gt; never forget to give it a look and tweak it to your own taste and needs. Sometimes these rules are quite generic and contain a lot of stuff that is probably useless for your project. Tokens are &lt;strong&gt;not free&lt;/strong&gt;, so you should be smart about it! 💸&lt;/p&gt;
&lt;p&gt;Here&apos;s a small example of what I usually do:&lt;/p&gt;
&lt;p&gt;Psst. This is a secret project I&apos;m releasing soon! 🤫&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
description: Bookmarks Monorepo - Project Structure &amp;amp; Rules
globs: **/*
alwaysApply: true
---

# Bookmarks Monorepo - Project Structure &amp;amp; Rules 📑

## Overview

A Turbo monorepo for a web app to save and search bookmarks from various sources (Twitter, GitHub, etc.) using AI

## Monorepo Structure

- **Package Manager**: Bun (v1.2.20)
- **Build System**: Turbo + Vite (using rolldown-vite)
- **Frontend Framework**: React 19 + TanStack Router/Start
- **Database**: PostgreSQL + Drizzle ORM
- **Styling**: Tailwind CSS v4 + Shadcn UI
- **Search**: Meilisearch
- **Background Jobs**: Trigger.dev
- **Real-time**: Pusher/Laravel Echo (WebSockets)
- **AI**: Vercel AI SDK, OpenAI
- **Authentication**: Better Auth
- **Storage**: S3/R2 compatible storage

### Root Level

- `turbo.json` - Turbo pipeline configuration with task dependencies
- `package.json` - Root workspace with Turbo scripts and version overrides
- `bun.lock` - Bun lockfile for dependencies

### Applications (`apps/`)

- `apps/web/` - Main bookmarks web application (React + TanStack Start)
- `apps/browser-extension/` - Browser extension for bookmarks (active development)

### Shared Packages (`packages/`)

- `packages/ai/` - AI utilities and prompt building
- `packages/attempt/` - Error handling utilities with tryCatch patterns
- `packages/cli/` - Command line interface utilities
- `packages/crawler/` - Web crawling functionality
- `packages/database/` - Drizzle ORM schema and migrations
- `packages/env/` - Environment variable validation
- `packages/eslint-config/` - Shared ESLint configuration with custom rules
- `packages/indexer/` - Meilisearch indexing utilities
- `packages/integrations/` - External service integrations (GitHub, Twitter)
- `packages/logger/` - Logging utilities
- `packages/monitoring/` - Performance and error monitoring
- `packages/sockets/` - WebSocket authentication and types
- `packages/storage/` - File storage abstractions (S3, R2, local)
- `packages/types/` - Shared TypeScript types and Zod schemas
- `packages/typescript-config/` - Shared TypeScript configurations (base + react)
- `packages/ui/` - Shadcn UI components and hooks
- `packages/utils/` - General utility functions

## Web App Structure (`apps/web/src/`)

src/
├── components/    # React UI components
├── console.ts     # CLI commands entry point
├── css/          # Tailwind CSS styles
├── entry-points/ # App entry points (client, server, router)
├── hooks/        # React hooks
├── jobs/         # Trigger.dev background jobs
├── lib/          # Utilities and configurations
├── prompts/      # AI prompt templates
├── routes/       # TanStack Router route components
├── routes.ts     # Route configuration
└── services/     # Business logic and API layer

## Tech Stack

- **Frontend**: React 19 + TanStack Router/Start
- **Database**: Drizzle + Postgres
- **Styling**: Tailwind CSS v4 + Shadcn
- **Package Manager**: Bun
- **AI**: Vercel AI SDK, Meilisearch
- **Build System**: Turbo + Vite
- **Jobs**: Trigger.dev
- **Architecture**: Laravel inspired MVC

## Key Commands

- `bun run build` - Build all packages
- `bun run dev` - Run main app in development
- `bun run lint:fix` - Lint all packages with shared config
- `bun run typecheck` - Run typescript Checks
- `bun lint:fix &amp;amp;&amp;amp; bun typecheck`- Run both

## Package Dependencies

- Apps use workspace packages via `workspace:*` protocol
- TypeScript version consistency enforced via root overrides
- ESLint configuration shared across all packages
- Caching handled by Turbo in `.turbo/` folder

## Development Rules

### TypeScript &amp;amp; Types

- **Zod Types**: Use `z.infer&amp;lt;typeof Schema&amp;gt;` for Zod-derived types
- **Imports**: Use `import * as` for namespace imports (services, Radix/Shadcn)
- **Type Safety**: Prefer type inference, interfaces over types, no TS enums
- **Coercion**: Use `z.coerce.number()` for ID fields that may come as strings
- **Return Types**: Let TypeScript infer return types when possible. Only add explicit return types when:
  - External APIs return `any` (chrome/browser APIs, fetch responses)
  - Type inference breaks due to complex logic
  - Public API methods that need documented return types
- **Type Organization**: Extract shared types to dedicated files in `src/types/` folder
- **No ANY**: Never use `any` type except when dealing with external API responses where typing is impossible

### Code Organization

- **Services**: Export plain functions, use classes sparingly
- **Actions**: TanStack Server Functions with `attempt.try()` error handling
- **Components**: Extend component props with `ComponentProps&amp;lt;typeof Component&amp;gt;`
- **File Naming**: kebab-case for files/directories
- **Method Naming**: Use single-word method names when possible (e.g., `sync()` instead of `syncBookmarks()`)
- **Business Logic**: Keep business logic out of React components - use service classes/functions
- **State Management**: Centralize state operations within service methods, not in components
- **Utilities**: Prefer static methods within service classes over separate utility files

### Error Handling

- **Result Pattern**: Use `attempt.try()` for [error,data] patterns instead of try/catch
- **Client Errors**: Set `client: true` in attempt options for user-facing errors
- **Custom Errors**: Use `ErrorException(&apos;ERROR_CODE&apos;)` for known error types
- **Error Mapping**: Provide error mappers in attempt options

### Code Style

- **Early Returns**: Always prefer early returns over nested conditions
- **No Nested Ternaries**: Keep conditionals readable
- **Destructuring**: Destructure props and options at function start
- **Nullish Coalescing**: Use `??` over `||` for nullable values
- **Pattern Matching**: Use `ts-pattern` for complex conditionals
- **React Conditionals**: Use ternary operators (`condition ? &amp;lt;Component /&amp;gt; : null`) instead of `&amp;amp;&amp;amp;` pattern to avoid leaked values
- **Comments**: Remove useless comments, keep code self-documenting
- **Function Organization**: Remove unused methods, inline browser API calls when single-use
- **Generic Functions**: Use generic types with proper constraints for reusable utilities (e.g., `sort&amp;lt;T extends Bookmark&amp;gt;(bookmarks: T[])`)
- **No Comments**: Do not add comments unless explicitly requested by the user

### Database

- **Query Builder**: Use Drizzle ORM with `db.query` syntax
- **Transactions**: Support optional `tx` parameter for transaction support
- **Relations**: Use `with` clause for eager loading related data
- **Validation**: Always question new columns, prefer defaults over null-checking

### Performance

- **Parallel Processing**: Use `Promise.all()` for concurrent operations
- **Batching**: Batch database queries and API calls when possible
- **Caching**: Cache expensive operations with appropriate TTL

### Package Manager

- **Bun**: Use `bun` for all package management tasks

### Key Dependencies

- **Zod v4**: All validation (`import { z } from &apos;zod/v4&apos;`)
- **TanStack**: Router, Query, Start for full-stack patterns
- **Drizzle ORM**: Database with type safety (`db.query` syntax)
- **ts-pattern**: Functional pattern matching with `match()`
- **react-hook-form**: Form management with Zod validation
- **Motion**: Animations (`motion/react`)
- **Radix UI**: Headless components via `radix-ui`
- **CVA**: Component styling variants

### Database Schema Workflow

- **Tables**: Define in `packages/database/src/schema.ts`
- **Schemas**: Mirror in `packages/types/src/database.schema.ts` with Zod
- **Sync**: Keep database and Zod schemas synchronized
- **Relations**: Export tables and relations separately

### Tasks &amp;amp; Jobs

- Interate on lints, first do everything then on the end run `bun lint:fix &amp;amp;&amp;amp; bun typecheck`
- Dont attempt to run tests or anything unless asked, ask me to test and ill do it and paste the output
- Once your are done with a task, please also give a double &quot;check&quot; to make sure everything is done and working as expected

### Development Server &amp;amp; Debugging

🚨 **CRITICAL**: **NEVER** run `bun run build` or `bun run dev` commands when debugging or pair programming! This interferes with the user&apos;s development workflow.

**Rules for Development Server:**

- **User controls the dev server** - Always ask the user to start/stop development servers
- **Never build for production** during debugging - it creates cached files that break HMR
- **Ask for logs** - Request console output, error logs, or debugging information from the user
- **Let user test** - Ask user to test functionality and paste results/logs
- **HMR workflows** - Respect that users may be running Hot Module Reload and your builds can break their workflow

**When debugging browser extensions:**

- Ask user to reload extension in browser after code changes
- Request specific console logs from extension devtools vs browser console
- Never run build commands - let the user&apos;s dev server handle file updates

**Correct debugging approach:**

❌ bun run build
❌ bun run dev
✅ &quot;Can you start the dev server with bun run dev?&quot;
✅ &quot;Please reload the extension and paste the console logs&quot;
✅ &quot;Can you test this and share what logs you see?&quot;


#### Naming Conventions

- **Files/Directories**: kebab-case (e.g., `user-service.ts`)
- **Classes**: PascalCase (e.g., `UserService`)
- **Variables/Methods**: camelCase (e.g., `getUserById`)
- **Database Tables**: snake_case (e.g., `user_bookmarks`)
- **Database Columns**: camelCase with snake_case for timestamps (e.g., `userId`, `created_at`)
- **Service Methods**: Single-word names when possible (e.g., `sync()`, `get()`, `set()`, `clear()`)
- **Integration Methods**: Domain validation (`isValid()`), data operations (`sync()`, `fetch()`, `parse()`)
- **Avoid Verbose Names**: Prefer `twitter()` over `syncTwitterBookmarks()`, `browser()` over `syncBrowserBookmarks()`
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. Controlling the Output and Code Quality 🎨&lt;/h2&gt;
&lt;p&gt;Well, as we&apos;ve seen before, we can do a lot to make our output better, instruct AI better, and try to save time. While this could look like it&apos;s really easy to get on the right track, after you&apos;re done with a setup that you like, you&apos;ll probably just copy-paste or tweak to other projects or tasks that you&apos;re doing daily.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BUT&lt;/strong&gt;, while AI can do a lot of code, it&apos;s important to keep monitoring the output and code quality. We are not &quot;vibe coding&quot; here—we&apos;re actually trying to get AI to do creative work, help us save time, and unblock real-world problems. If you&apos;re not getting the results you want, try switching your prompt, the way you ask, or the way you instruct AI to do something. That&apos;s often the problem. It could also happen that AI models are being nerfed, and you should just go touch grass 😛 or actually do code, because that&apos;s what you&apos;re paid for? lol 😂&lt;/p&gt;
&lt;h2&gt;6. What you should not do 🚫&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Small Tasks&lt;/strong&gt; - If you want to change a color or a font-size, please just do it yourself, or at dont cry when your Cursor/Claude requests are gone, if you did you deserve to pay for it:p&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Know when when to stop session/chat&lt;/strong&gt; - There is limited context, so if you feeling like your doing too much on a session/chat, try to start a fresh one!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stuck in Endless Loop&lt;/strong&gt; - If you feel like AI is not giving you the results you want, dont get mad! Breath a bit, start a new chat and try to approach it in a different way, explain different things, give more context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dont stop being a programmer&lt;/strong&gt; - You are programmer, so please be creative, while its nice to get AI by your side, dont forget to be creative, understand what your code is doing, what it should do, and how you want to shape it, you are in control!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. Conclusion 🎉&lt;/h2&gt;
&lt;p&gt;Well, that&apos;s it for now! Just wanted to brain dump some thoughts and ideas, and workflows that I use daily. Things are changing, so it&apos;s highly likely that this will be outdated in a few months, but I hope you enjoyed this article and that you&apos;ll find it useful.&lt;/p&gt;
&lt;p&gt;OH, this article was NOT written by AI btw lol! I just fixed typos sorry for that!&lt;/p&gt;
&lt;p&gt;If you&apos;re on X or Bluesky make sure to give me a follow! [[@nikuscs|https://x.com/nikuscs]] or [[@nikus|https://bsky.app/profile/nikuscs.bsky.social]] 🐦&lt;/p&gt;
&lt;h2&gt;7. Resources 📚&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[@anthropic|https://anthropic.com]]&lt;/li&gt;
&lt;li&gt;[[Claude Code|https://claude.ai/code]]&lt;/li&gt;
&lt;li&gt;[[Roo Code|https://roocode.com]]&lt;/li&gt;
&lt;li&gt;[[Cursor|https://cursor.com]]&lt;/li&gt;
&lt;li&gt;[[OpenAI GPT|https://openai.com]]&lt;/li&gt;
&lt;li&gt;[[Tailscale|https://tailscale.com]]&lt;/li&gt;
&lt;li&gt;[[Qdrant|https://qdrant.tech]]&lt;/li&gt;
&lt;li&gt;[[OpenRouter|https://openrouter.ai]]&lt;/li&gt;
&lt;li&gt;[[Ruler|https://github.com/intellectronica/ruler]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;How do Claude Code, Cursor, and Roo Code compare for AI-assisted development?&quot;, answer: &quot;Claude Code is a terminal-based agent that excels at complex autonomous tasks and multi-file operations. Cursor is a VS Code fork with Tab autocomplete and a visual agent UI, best for quick edits and file-focused work. Roo Code (Cline fork) is a VS Code extension that supports multiple AI providers via OpenRouter, offers mode switching between Architect and Code modes, and allows task decomposition.&quot; },
{ question: &quot;How does Roo Code&apos;s mode switching and task system work?&quot;, answer: &quot;Roo Code lets you plan in Architect mode with a capable model, then automatically switches to Code mode with a cheaper model for execution. Its task system divides large features into smaller sub-tasks while retaining context across them, allowing it to work in auto-pilot mode. You can also bring your own API keys through OpenRouter for cost control.&quot; },
{ question: &quot;How does Ruler help sync project rules across AI coding tools?&quot;, answer: &quot;Ruler is a tool that reads a single rules file and generates the correct format for multiple AI tools: CLAUDE.md for Claude Code, .cursor/rules/ for Cursor, and .roo/rules/ for Roo Code. This avoids maintaining separate rule files for each tool and keeps all tools in sync with one command like &apos;bun ruler&apos;.&quot; },
{ question: &quot;How should I split tasks between Claude Code, Cursor, and Roo Code?&quot;, answer: &quot;Use Claude Code for important features, bootstrapping projects, complex bug finding, and multi-file refactors. Use Cursor for quick edits in one or two files and when you need visual diff review. Use Roo Code when you want structured plan-then-execute workflows or when exploring ideas. You can run tasks in parallel across all three as long as they don&apos;t touch the same files.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>ai</category><category>claude</category><category>roocode</category><category>cursor</category><category>workflow</category><author>Pedro Martins</author></item><item><title>Generate Dynamic SEO Images with Vercel OG (Satori) for Tanstack Start, Astro &amp; Laravel</title><link>https://nikuscs.com/blog/09-dynamic-seo-images-vercel-og-satori/</link><guid isPermaLink="true">https://nikuscs.com/blog/09-dynamic-seo-images-vercel-og-satori/</guid><description>In this article, we&apos;ll explore how to create dynamic SEO images for Tanstack Start, Astro or Laravel using Vercel OG (Satori).</description><pubDate>Wed, 30 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Callout from &quot;@components/Callout.astro&quot;;
import Button from &quot;@components/Button.astro&quot;;&lt;/p&gt;
&lt;h2&gt;1. Introduction 🚀&lt;/h2&gt;
&lt;p&gt;Not too long ago, we created static SEO images for our websites (and that&apos;s still valid!). If you&apos;re in the SEO game and it matters to you, having an &lt;code&gt;og:image&lt;/code&gt; is essential - especially when sharing your website on social media platforms, Discord, Slack, and other platforms.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;og:image&lt;/code&gt; should be the &quot;main&quot; image of the page or what you want to highlight, usually related to the page content. If none is provided, search engines like Google will take the first image found on the page and use it as the preview for SERP results.&lt;/p&gt;
&lt;p&gt;But you&apos;re probably here for a reason 😌 - to generate dynamic SEO images or on-demand images, right?&lt;/p&gt;
&lt;p&gt;[[GitHub|https://github.com]] was one of the first to implement this, generating dynamic SEO images on-demand that show current repository stats like forks, stars, and issues, plus Pull Request information and text in real-time. This was amazing because sharing these on Slack or Discord would instantly show what the current Pull Request is about, giving you a clear view in seconds!&lt;/p&gt;
&lt;p&gt;Thankfully, [[@shuding|https://github.com/shuding]] 🐐 created [[Satori|https://github.com/vercel/satori]] / [[@vercel/og|https://www.npmjs.com/package/@vercel/og]], a library to generate dynamic SEO images using HTML, JSX, CSS, and even [[Tailwind CSS|https://tailwindcss.com]] support.&lt;/p&gt;
&lt;p&gt;In this article, we&apos;ll explore how to implement this in TanStack Start and Astro, but the same principles can be applied to any JavaScript framework.&lt;/p&gt;
&lt;p&gt;Let&apos;s jump into the code:&lt;/p&gt;
&lt;h2&gt;2. Creating an Image Generator 🎨&lt;/h2&gt;
&lt;p&gt;Since this can run in any framework, we&apos;ll abstract our logic away from the framework and create a generic image generator.
In this example, I&apos;ll showcase what I&apos;ve used for my Astro blog, but you can tweak it to your own needs and add more information.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { CollectionEntry, CollectionKey } from &quot;astro:content&quot;;
import { SITE } from &quot;@consts&quot;;
import { ImageResponse } from &apos;@vercel/og&apos;;
import fs from &apos;node:fs&apos;
import path from &apos;node:path&apos;
import { encode } from &quot;blurhash&quot;;
import { getPixels } from &quot;@unpic/pixels&quot;;
import { blurhashToCssGradientString } from &quot;@unpic/placeholder&quot;;

/**
 * Loads a font from Google Fonts
 * @param font - The font name
 * @param text - The text to load the font for
 * @param weight - The font weight to load
 * @returns The font data
 */
async function loadFont(font: string, text: string, weight: number) {
  const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}&amp;amp;text=${encodeURIComponent(text)}`
  const css = await (await fetch(url)).text()
  const resource = css.match(/src: url\((.+)\) format\(&apos;(opentype|truetype)&apos;\)/)
  if (resource) {
    const response = await fetch(resource[1])
    if (response.status == 200) {
      return await response.arrayBuffer()
    }
  }
  throw new Error(&apos;failed to load font data&apos;)
}


/**
 * Generates a SEO image for a blog post or project
 * @param title - The title of the blog post or project
 * @param text - The text to display in the image
 * @param description - The description of the blog post or project
 * @param tags - The tags of the blog post or project
 * @param image - The image to display in the image
 * @returns The SEO image
 */
export interface SeoImageGeneratorProps {
  title: string;
  text: string
  image?: {
    src: string
    width: number
    height: number
    format: &apos;png&apos; | &apos;jpg&apos; | &apos;jpeg&apos; | &apos;webp&apos; | &apos;tiff&apos; | &apos;gif&apos; | &apos;svg&apos; | &apos;avif&apos;
  };
}

export async function generateSeoImage({
  title,
  image,
  text
}: SeoImageGeneratorProps) {

  const assetsPrefix = process.env.NODE_ENV === &apos;development&apos; ? &apos;./public&apos; : &apos;./dist&apos;
  const siteImage = fs.readFileSync(path.resolve(assetsPrefix, &apos;static/avatar.png&apos;))

  const siteImageElement = {
    type: &apos;img&apos;,
    props: {
      tw: &apos;w-6 h-6 rounded-full mr-2&apos;,
      src: siteImage.buffer,
    },
  };

  const siteTitleElement = {
    type: &apos;div&apos;,
    props: {
      tw: &apos; opacity-70&apos;,
      style: {
        fontFamily: &apos;Geist&apos;,
      },
      children: SITE.TITLE,
    },
  }

  const separatorElement = {
    type: &apos;div&apos;,
    props: {
      tw: &apos;px-2 opacity-70&apos;,
      children: &apos;•&apos;,
    },
  };

  const siteSubtitleElement = {
    type: &apos;div&apos;,
    props: {
      tw: &apos;opacity-70&apos;,
      style: {
        fontFamily: &apos;Geist&apos;,
      },
      children: title,
    },
  };

  const siteImageAndTitleContainer = {
    type: &apos;div&apos;,
    props: {
      tw: &apos;flex items-center justify-start w-full mb-2&apos;,
      children: [siteImageElement, siteTitleElement, separatorElement, siteSubtitleElement],
    },
  };
 
  let imageElement = null
  let shouldBlur = false
  if (image &amp;amp;&amp;amp; shouldBlur) {
    const img = fs.readFileSync(
      process.env.NODE_ENV === &apos;development&apos;
        ? path.resolve(image.src.replace(/\?.*/, &apos;&apos;).replace(&apos;/@fs&apos;, &apos;&apos;),)
        : path.resolve(image.src.replace(&apos;/&apos;, &apos;dist/&apos;)),
    );
    const imgData = await getPixels(new Uint8Array(img));
    const data = Uint8ClampedArray.from(imgData.data);
    const blurhash = encode(data, imgData.width, imgData.height, 4, 4);
    const gradient = blurhashToCssGradientString(blurhash, 5, 5);

    imageElement = {
      type: &apos;div&apos;,
      props: {
        tw: &apos;w-full h-full absolute inset-0 rotate-90&apos;,
        style: {
          opacity: 0.07,
          background: gradient,
        },
      },
    };
  }

  const gradientElement = {
    type: &apos;div&apos;,
    props: {
      style: {
        background: &apos;radial-gradient(circle at bottom right, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0))&apos;,
      },
      tw: &apos;w-full h-full absolute flex inset-0 opacity-40&apos;,
    },
  };

  const mainTextElement = {
    type: &apos;div&apos;,
    props: {
      tw: &apos;text-5xl leading-none text-left&apos;,
      style: {
        fontFamily: &apos;Geist&apos;,
        fontWeight: &apos;400&apos;,
      },
      children: text,
    },
  };

  const containerElement = {
    type: &apos;div&apos;,
    props: {
      tw: &apos;flex flex-col items-center justify-center max-w-3xl w-full&apos;,
      children: [
        siteImageAndTitleContainer,
        {
          type: &apos;div&apos;,
          props: {
            tw: &apos;shrink flex w-full&apos;,
            children: [
              mainTextElement,
            ],
          },
        },
      ],
    },
  }

  const footerElement = {
    type: &apos;div&apos;,
    props: {
      tw: &apos;absolute right-[15px] bottom-[15px] flex items-center justify-center opacity-70 text-xs&apos;,
      children: [
        SITE.URL_SHORT,
      ],
    },
  }

  const html = {
    type: &apos;div&apos;,
    props: {
      tw: &apos;relative w-full h-full flex flex-col items-center justify-center relative relative text-white&apos;,
      style: {
        background: &apos;#0a0a0a&apos;,
        fontFamily: &apos;Geist&apos;,
        fontSmoothing: &apos;antialiased&apos;,
      },
      children: [
        imageElement,
        gradientElement,
        containerElement,
        footerElement,
      ],
    },
  } as any


  const weights = [100, 200, 300, 400, 900] as const

  const fontConfigs = [
    ...weights.map(weight =&amp;gt; ({ name: &apos;Geist&apos;, weight })),
    ...weights.map(weight =&amp;gt; ({ name: &apos;Geist Mono&apos;, weight })),
  ]

  const fonts = await Promise.all(
    fontConfigs.map(async ({ name, weight }) =&amp;gt; ({
      name,
      data: await loadFont(name, text, weight),
      style: &apos;normal&apos; as const,
      weight,
    }))
  )

  return new ImageResponse(html, {
    width: 1200,
    height: 630,
    fonts,
    debug: false,
  })
}

/**
 * Generates a SEO image for a blog post or project
 * @param entry - The entry to generate the SEO image for
 * @param type - The type of the entry
 * @returns The SEO image
 */
export interface SeoImageGeneratorForContentProps {
  entry: CollectionEntry&amp;lt;&quot;blog&quot;&amp;gt; | CollectionEntry&amp;lt;&quot;projects&quot;&amp;gt;;
  type: CollectionKey
}

export async function generateSeoImageForContent({ entry, type }: SeoImageGeneratorForContentProps) {
  const {
    title: text,
    ...rest
  } = entry.data;

  const title = {
    blog: &apos;Blog&apos;,
    projects: &apos;Projects&apos;,
  }[type]

  return generateSeoImage({
    title,
    text,
    image: entry.data.cover,
    ...rest,
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s break down what we&apos;ve done in this file to keep it simple and maintainable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create multiple elements, we could inline everything, but to keep it easy to manage iv went this way&lt;/li&gt;
&lt;li&gt;Use tailwindcss classes to style the elements ( keep in mind not every utility is supported, but most of them are )&lt;/li&gt;
&lt;li&gt;We can also pull and attach images from the entry data, like the cover image, a logo or favicon if needed.&lt;/li&gt;
&lt;li&gt;Blurhash to generate a gradient background for the image, this is optional, but it looks nice and gives a nice touch to the image without looking always the same color/style.&lt;/li&gt;
&lt;li&gt;Loading Fonts from Google Fonts directly, we using Geist here. You can also load local fonts if needed.&lt;/li&gt;
&lt;li&gt;Finally, we return the ImageResponse, which is the image that will be used as the SEO image.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is all it takes to create a generic image generator, you can tweak it to your own needs and get more information going on.&lt;/p&gt;
&lt;p&gt;Now lets see how we serve this image, probably the easiest part!&lt;/p&gt;
&lt;h2&gt;3. Serving the Image 🚀&lt;/h2&gt;
&lt;h3&gt;3.1. TanStack Start&lt;/h3&gt;
&lt;p&gt;With [[@tanstack|TanStack Start|https://tanstack.com/start]], you&apos;ll want to create a server route to serve the image.&lt;/p&gt;
&lt;p&gt;For a blog post, we could create a route under &lt;code&gt;src/routes/blog/$slug[.]png.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you&apos;re using virtual routes, don&apos;t forget to add it to your routes configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/routes/blog/$slug[.]png.ts
import { generateSeoImageForContent } from &apos;@/lib/seo-image-generator&apos;

export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ request, params }) =&amp;gt; {
    // Call your database to get article by the slug for example
    const article = await db.query.articles.findFirst({where: and(eq(tables.articles.slug, params.slug))})

    if (!article) {
      return new Response(&apos;Article not found&apos;, { status: 404 })
    }
    
    return await generateSeoImageForContent({ entry: article, type: &apos;blog&apos; })
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it! We have a server route that generates the image and returns it as a response.
We&apos;ll explore below what we can do to optimize this further.&lt;/p&gt;
&lt;h3&gt;3.2. Astro&lt;/h3&gt;
&lt;p&gt;With [[@astro|Astro|https://astro.build]], you&apos;ll want to create a server route to serve the image.&lt;/p&gt;
&lt;p&gt;For a blog post, we could create a route under &lt;code&gt;src/pages/blog/[slug].png.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { getCollection, type CollectionEntry } from &apos;astro:content&apos;
import type { APIRoute } from &apos;astro&apos;
import { generateSeoImageForContent } from &quot;@lib/seo-image-generator&quot;

type MaybeCollectionEntry = CollectionEntry&amp;lt;&quot;blog&quot;&amp;gt; | CollectionEntry&amp;lt;&quot;projects&quot;&amp;gt;

export async function getStaticPaths() {
	const posts = await getCollection(&apos;blog&apos;)
	return posts.map((post) =&amp;gt; ({
		params: { id: post.id },
		props: post,
	}))
}

export const GET: APIRoute = async ({ props }) =&amp;gt; {
	const image = await generateSeoImageForContent({
		entry: props as MaybeCollectionEntry,
		type: &apos;blog&apos;,
	})
	return image
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it! When you generate your Astro site, you&apos;ll have a route that generates the image and copies your images to the &lt;code&gt;dist&lt;/code&gt; folder.&lt;/p&gt;
&lt;h3&gt;3.3. Laravel / PHP&lt;/h3&gt;
&lt;p&gt;While this could be more challenging in PHP land, there are a few options. We can still achieve this with [[@laravel|Laravel|https://laravel.com]].
In this case, we&apos;ll create a Bun script to generate the image, then use the [[Spatie Media Library|https://spatie.be/docs/laravel-medialibrary]] to store the image.&lt;/p&gt;
&lt;p&gt;For a blog post, we could create an action under &lt;code&gt;app/Actions/GenerateBlogPostOGImageAction.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\Actions;

use App\Enums\MediaCollectionEnum;
use App\Enums\System\FilesystemDiskEnum;
use App\Models\Media;
use App\Models\BlogPost;
use Illuminate\Support\Str;
use Spatie\MediaLibrary\MediaCollections\Exceptions\FileDoesNotExist;
use Spatie\MediaLibrary\MediaCollections\Exceptions\FileIsTooBig;
use Symfony\Component\Process\Process;

final class GenerateBlogPostOGImageAction
{
    /**
     * @throws FileDoesNotExist
     * @throws FileIsTooBig
     */
    public function handle(BlogPost $blogPost): void
    {
        // Remove any existing og images
        // @phpstan-ignore-next-line
        $blogPost-&amp;gt;getMedia(MediaCollectionEnum::OGImage-&amp;gt;value)-&amp;gt;each(fn (Media $media) =&amp;gt; $media-&amp;gt;delete());

        $avatar = $blogPost-&amp;gt;getFirstMedia(MediaCollectionEnum::Avatar-&amp;gt;value)?-&amp;gt;getUrl();
        $temporaryFile = tempnam(sys_get_temp_dir(), MediaCollectionEnum::OGImage-&amp;gt;value);

        $parameters = [
            config(&apos;app.bun_path&apos;, &apos;bun&apos;), base_path(&apos;scripts/og-image.ts&apos;),
            &apos;--output&apos;, $temporaryFile,
            &apos;--title&apos;, $blogPost-&amp;gt;title,
            &apos;--description&apos;, $blogPost-&amp;gt;description,
            ...($avatar ? [&apos;--avatar&apos;, $avatar] : []),
        ];

        $process = new Process($parameters, env: [
            &apos;NODE_TLS_REJECT_UNAUTHORIZED&apos; =&amp;gt; &apos;0&apos;,
        ]);

        $process-&amp;gt;run();
        if ($process-&amp;gt;isSuccessful()) {
            $blogPost
                -&amp;gt;addMedia($temporaryFile)
                -&amp;gt;usingName($blogPost-&amp;gt;uuid)
                -&amp;gt;usingFileName($blogPost-&amp;gt;uuid.&apos;.png&apos;)
                -&amp;gt;toMediaCollection(MediaCollectionEnum::OGImage-&amp;gt;value, FilesystemDiskEnum::OGImages-&amp;gt;value);
        } else {
            logger($process-&amp;gt;getErrorOutput());
        }
        @unlink($temporaryFile);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Optimizing &amp;amp; Performance ⚡&lt;/h2&gt;
&lt;p&gt;While this is great and super fast, and we&apos;ve got our dynamic image sorted out, sometimes we can optimize this further - especially if the image doesn&apos;t change often.&lt;/p&gt;
&lt;p&gt;With Astro, you can use the &lt;code&gt;getStaticPaths()&lt;/code&gt; function to generate the paths for images. When you build to a static site, Astro will generate these images beforehand, so no further requests will be made to the server.&lt;/p&gt;
&lt;p&gt;For TanStack Start with a more &quot;dynamic&quot; approach, I&apos;d recommend a few strategies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Generate or cache the image&lt;/strong&gt; on the server - serve cached version if it already exists&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set caching headers&lt;/strong&gt; so the image is cached longer in the browser&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a CDN&lt;/strong&gt; to cache the image globally for faster delivery&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. Conclusion&lt;/h2&gt;
&lt;p&gt;That&apos;s about it! I hope you enjoyed this article, and if you have any questions, feel free to reach out to me on [[@nikuscs|https://x.com/nikuscs]].&lt;/p&gt;
&lt;p&gt;Thanks for reading! Special thanks to [[@shuding|https://github.com/shuding]] for creating [[Satori|https://github.com/vercel/satori]] 🙏&lt;/p&gt;
&lt;h2&gt;6. Resources 📚&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[@shuding|https://github.com/shuding]]&lt;/li&gt;
&lt;li&gt;[[satori|https://github.com/vercel/satori]]&lt;/li&gt;
&lt;li&gt;[[@vercel/og|https://www.npmjs.com/package/@vercel/og]]&lt;/li&gt;
&lt;li&gt;[[@tanstack|TanStack Start|https://tanstack.com/start]]&lt;/li&gt;
&lt;li&gt;[[@astro|Astro|https://astro.build]]&lt;/li&gt;
&lt;li&gt;[[@laravel|Laravel|https://laravel.com]]&lt;/li&gt;
&lt;li&gt;[[@spatie|Spatie Media Library|https://spatie.be/docs/laravel-medialibrary]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;How do I generate dynamic OG images with Satori and @vercel/og?&quot;, answer: &quot;Use @vercel/og&apos;s ImageResponse to render HTML/JSX element trees with Tailwind-like styles into 1200x630px PNG images. Create a generic image generator function that builds element objects with tw classes, loads custom fonts from Google Fonts as ArrayBuffers, and returns the ImageResponse. This works in any JavaScript framework including TanStack Start, Astro, and Laravel via Bun scripts.&quot; },
{ question: &quot;How do I serve dynamic OG images in TanStack Start and Astro?&quot;, answer: &quot;In TanStack Start, create a server route like /blog/$slug[.]png.ts that queries your data and calls the image generator. In Astro, create a route at /blog/[slug].png.ts using getStaticPaths to pre-generate images at build time. Both approaches return the ImageResponse directly, and Astro&apos;s static generation means no server-side rendering is needed after build.&quot; },
{ question: &quot;How can I generate OG images for a Laravel application using Satori?&quot;, answer: &quot;Create a Bun script that uses Satori/@vercel/og to generate the image, then call it from a Laravel Action using Symfony Process with parameters like title and description. Store the generated image using Spatie Media Library, which handles file management and serving. This bridges the PHP and JavaScript ecosystems for image generation.&quot; },
{ question: &quot;How do I load custom Google Fonts in Satori for OG images?&quot;, answer: &quot;Fetch the Google Fonts CSS URL with the desired font family and weight, extract the font file URL from the CSS response using a regex match on the src declaration, then fetch the font file as an ArrayBuffer. Pass the fonts array to ImageResponse with name, data, style, and weight properties for each variant you need.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>tanstack-start</category><category>astro</category><category>vercel</category><category>satori</category><category>og-image</category><category>seo</category><category>vercel-og</category><category>laravel</category><author>Pedro Martins</author></item><item><title>WebSockets with Pusher - Real-time Events, Notifications with Laravel &amp; TanStack Start</title><link>https://nikuscs.com/blog/08-websockets-pusher/</link><guid isPermaLink="true">https://nikuscs.com/blog/08-websockets-pusher/</guid><description>Learn how to implement WebSockets with Pusher-compatible servers on both server-side and client-side, using TanStack Start and Laravel</description><pubDate>Mon, 21 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Callout from &quot;@components/Callout.astro&quot;;
import Button from &quot;@components/Button.astro&quot;;&lt;/p&gt;
&lt;h2&gt;1. Introduction&lt;/h2&gt;
&lt;p&gt;WebSockets are an excellent way to build real-time applications, but they can be tricky to implement. It&apos;s been a while since I&apos;ve used WebSockets in my projects, especially with [[Laravel|https://laravel.com/]] and [[@tanstack|TanStack Start|https://tanstack.com/start]], so I thought it would be a good idea to write about it.&lt;/p&gt;
&lt;p&gt;If you&apos;re using [[Laravel|https://laravel.com/]] like I did for a long time, dispatching jobs and notifying users in real-time can provide a nice UX improvement, and it&apos;s probably the only &quot;clean&quot; way to handle this. Of course, you could always use polling, but that&apos;s not the best solution in the long run. Let&apos;s explore how to implement this in Laravel (PHP) and [[@tanstack|TanStack Start|https://tanstack.com/start]] (JavaScript).&lt;/p&gt;
&lt;h2&gt;2. WebSocket Servers ⚡&lt;/h2&gt;
&lt;p&gt;While you could build your own WebSocket servers, sometimes it&apos;s just not worth the effort—you want to ship your app without worrying about maintaining another service.&lt;/p&gt;
&lt;p&gt;There are several solutions you can choose from. I&apos;m biased toward Pusher because of my Laravel background, but that doesn&apos;t mean you can&apos;t use other solutions. In this article, I&apos;ll explore the Pusher solution, but other solutions like [[socket.io|https://socket.io/]] are also excellent, and I might update this article in the future.&lt;/p&gt;
&lt;p&gt;Here are a few you can pick from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[[Pusher|https://pusher.com/]] ( Paid as a service )&lt;/li&gt;
&lt;li&gt;[[Sockudo|https://sockudo.app/]] ( Pusher compatible, Open source, self-hosted, Rust )&lt;/li&gt;
&lt;li&gt;[[Soketi|https://soketi.app/]] ( Pusher compatible, Open source, self-hosted )&lt;/li&gt;
&lt;li&gt;[[Socket.io|https://socket.io/]] ( Open source, self-hosted )&lt;/li&gt;
&lt;li&gt;[[Partykit|https://partykit.io/]] ( Open source, self-hosted )&lt;/li&gt;
&lt;li&gt;[[Laravel Reverb|https://reverb.laravel.com/]] ( Pusher compatible, Laravel specific, Open source )&lt;/li&gt;
&lt;li&gt;[[Crossws|https://unjs.io/packages/crossws/]] ( Open source, barebones )&lt;/li&gt;
&lt;li&gt;[[uWebSockets|https://github.com/uNetworking/uWebSockets]] ( Open source, barebones )&lt;/li&gt;
&lt;li&gt;[[ws|https://github.com/websockets/ws]] ( Open source, barebones )&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are tradeoffs to each solution, and you should pick the one that fits your needs best. For me, most of the time I don&apos;t need super high performance or blazing-fast solutions, so I would just pick Pusher-compatible solutions because they&apos;re easy to integrate, include authentication, and you can use the Pusher JS SDK and call it a day.&lt;/p&gt;
&lt;p&gt;I&apos;ll explore some self-hosted solutions here, but you can also use Pusher as a service—it&apos;s an excellent solution if you&apos;re not interested in self-hosting.&lt;/p&gt;
&lt;h3&gt;2.1 - Laravel Reverb 📡&lt;/h3&gt;
&lt;p&gt;[[Laravel Reverb|https://reverb.laravel.com/]] is a Pusher-compatible solution for Laravel. If you&apos;re a Laravel user, it&apos;s probably the best solution for you, as it pairs perfectly with your current deployment pipeline and integrates with Laravel.&lt;/p&gt;
&lt;p&gt;I&apos;ll cover the basic installation and setup here, but you can find more information in the [[Laravel Reverb documentation|https://laravel.com/docs/12.x/reverb]].&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php artisan install:broadcasting
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In your environment file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;php artisan reverb:start --host=&quot;0.0.0.0&quot; --port=8080 --hostname=&quot;laravel.test&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yes, it should be this easy! 🎉&lt;/p&gt;
&lt;h3&gt;2.2 - Sockudo 🦀&lt;/h3&gt;
&lt;p&gt;[[Sockudo|https://sockudo.app/]] is a new project written in Rust. It&apos;s a Pusher-compatible solution that&apos;s very easy to set up because it ships as a single binary—you can just download it and run it. While I previously used [[Soketi|https://soketi.app/]], it seems Soketi has been abandoned, so for me this is the best solution right now for a &quot;plug-and-play&quot; approach, especially if you&apos;re in the JavaScript ecosystem where Reverb isn&apos;t an option.&lt;/p&gt;
&lt;p&gt;First, grab the latest release from the [[Sockudo GitHub|https://github.com/RustNSparks/sockudo/releases]] and run it as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sockudo --config=./config.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s an example &lt;code&gt;config.json&lt;/code&gt; I use locally. For production, you should use environment variables. Please refer to the [[Sockudo documentation|https://sockudo.app/guide/configuration/app-manager.html#environment-variables-for-app-manager]] for more information about setting up default apps.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;debug&quot;: true,
  &quot;adapter&quot;: {
    &quot;driver&quot;: &quot;redis&quot;,
    &quot;redis&quot;: {
      &quot;requests_timeout&quot;: 5000,
      &quot;prefix&quot;: &quot;sockets_&quot;,
      &quot;redis_pub_options&quot;: {
        &quot;url&quot;: &quot;redis://127.0.0.1:6379&quot;
      },
      &quot;redis_sub_options&quot;: {
        &quot;url&quot;: &quot;redis://127.0.0.1:6379&quot;
      }
    }
  },
  &quot;app_manager&quot;: {
    &quot;driver&quot;: &quot;memory&quot;,
    &quot;array&quot;: {
      &quot;apps&quot;: [
        {
          &quot;id&quot;: &quot;my-app-id&quot;,
          &quot;key&quot;: &quot;my-app-key&quot;,
          &quot;secret&quot;: &quot;my-app-secret&quot;,
          &quot;max_connections&quot;: &quot;10000&quot;,
          &quot;enable_client_messages&quot;: true,
          &quot;max_client_events_per_second&quot;: &quot;200&quot;,
          &quot;enabled&quot;: true
        }
      ]
    }
  },
  &quot;cache&quot;: {
    &quot;driver&quot;: &quot;redis&quot;,
    &quot;redis&quot;: {
      &quot;redis_options&quot;: {
        &quot;url&quot;: &quot;redis://127.0.0.1:6379&quot;,
        &quot;prefix&quot;: &quot;sockets_cache&quot;
      }
    }
  },
  &quot;port&quot;: 6001
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Server-Side Implementation 🛠️&lt;/h2&gt;
&lt;p&gt;Now that we have our Pusher service up and running, we can implement the server-side components like authentication, event dispatching, and more.&lt;/p&gt;
&lt;h3&gt;3.1 - Server-Side with Laravel 🐘&lt;/h3&gt;
&lt;p&gt;If you&apos;re using Laravel, this is provided for you. You can use Laravel Events and implement traits like &lt;code&gt;ShouldBroadcast&lt;/code&gt; and &lt;code&gt;ShouldBroadcastNow&lt;/code&gt; on your events. Let&apos;s explore how to do it.&lt;/p&gt;
&lt;p&gt;If you ran the &lt;code&gt;php artisan install:broadcasting&lt;/code&gt; command, you should have a &lt;code&gt;BroadcastingServiceProvider&lt;/code&gt; and a route file called &lt;code&gt;channels.php&lt;/code&gt; that handles authentication for different channels.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

use App\Models\User;
use App\Services\Cart\CartService;
use Illuminate\Support\Facades\Broadcast;

/*
|--------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
|
| Here you may register all of the event broadcasting channels that your
| application supports. The given channel authorization callbacks are
| used to check if an authenticated user can listen to the channel.
|
*/

Broadcast::routes([&apos;middleware&apos; =&amp;gt; [&apos;web&apos;]]);

Broadcast::channel(&apos;App.Models.User.{id}&apos;, function (User $user, $id) {
    return $user-&amp;gt;id === (int) $id;
});

// Example of a channel for a cart
// In this case a guest-authenticated middleware should be used.
Broadcast::channel(&apos;App.Models.Cart.{uuid}&apos;, function (User $user, string $uuid) {
    return (new CartService)-&amp;gt;get()-&amp;gt;uuid === $uuid;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.1.1 - Dispatching Events &amp;amp; Notifications 📢&lt;/h4&gt;
&lt;p&gt;Now that we have our authentication endpoint and channels set up, we can start dispatching events and notifications. I&apos;ll cover how to do this in JavaScript later, but for now let&apos;s explore how to do it in PHP.&lt;/p&gt;
&lt;p&gt;This simple event broadcasts to the currently logged-in user with the event &lt;code&gt;refresh:xxxx&lt;/code&gt;, where &lt;code&gt;xxx&lt;/code&gt; can be anything you want—a table ID, cart ID, dialog ID, etc.&lt;/p&gt;
&lt;p&gt;&amp;lt;CodeTabs&amp;gt;
&amp;lt;CodeTab label=&quot;RefreshUIEvent.php&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\Events;

use App\Helpers\Broadcast;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;

final class RefreshUIEvent implements ShouldBroadcastNow
{
    use Dispatchable;
    use InteractsWithSockets;
    use Queueable;
    use SerializesModels;

    public function __construct(
        protected User $user,
        protected string $uniqueKey,
    ) {}

    public function broadcastOn(): PrivateChannel
    {
        return [
            new PrivateChannel(&apos;user.&apos;.$this-&amp;gt;user-&amp;gt;id),
        ];
    }

    public function broadcastAs(): string
    {
        return &apos;refresh:&apos;.$this-&amp;gt;uniqueKey;
    }

    public function broadcastWith(): array
    {
        return [];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/CodeTab&amp;gt;
&amp;lt;CodeTab label=&quot;usage.php&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use App\Events\RefreshUIEvent;

event(new RefreshUIEvent($user, &apos;my-unique-key&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/CodeTab&amp;gt;
&amp;lt;/CodeTabs&amp;gt;&lt;/p&gt;
&lt;p&gt;We can then dispatch this event from anywhere in our codebase, like a job, a controller, a service, etc.&lt;/p&gt;
&lt;p&gt;There&apos;s much more to cover here, but nothing beats diving into the [[Laravel Broadcasting documentation|https://laravel.com/docs/12.x/broadcasting#main-content]]. You can implement this in many ways—handy aliases, presence channels, and more. I want to keep this article focused on the WebSocket implementation, so I won&apos;t cover all the other features.&lt;/p&gt;
&lt;h3&gt;3.2 - Server-Side with TanStack Start (JavaScript) ⚡&lt;/h3&gt;
&lt;p&gt;While Laravel does all the heavy lifting for us (and we&apos;re spoiled), in JavaScript land we need to do a bit more work. But fear not! This is where this article and the community shine.&lt;/p&gt;
&lt;p&gt;First, let&apos;s see how we can mimic Laravel&apos;s authentication part by creating a simple auth service. I&apos;ll try to replicate Laravel&apos;s channel registration pattern that we&apos;re familiar with: &lt;code&gt;user.{userId}&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;authenticator.channel&amp;lt;UserChannelParams&amp;gt;(&apos;user.{userId}&apos;, (user, params) =&amp;gt; {
  //logger.info(&apos;🔑 Authenticating user&apos;, user, params)
  return Number(user.id) === Number(params.userId)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following code is a bit complex but does the job and ensures several things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Validates pattern syntax and matches channel names to patterns&lt;/li&gt;
&lt;li&gt;Supports presence channels and private channels in a Pusher-like interface&lt;/li&gt;
&lt;li&gt;Uses [[Better Auth|https://better-auth.com/]] to get user sessions and inject user data into channels&lt;/li&gt;
&lt;li&gt;That&apos;s probably all we need for now, but you can extend it to your needs!&lt;/li&gt;
&lt;li&gt;Compatible with [[Laravel Echo|https://github.com/laravel/echo]]&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// src/services/sockets/sockets.auth.ts
import { logger } from &apos;@bookmarks/logger&apos;
import { auth } from &apos;@/services/auth/auth.better-auth&apos;
import type { SocketsChannelConfig, SocketsChannelParams, SocketsAuthCallback, SocketsMatchedChannelInfo, SocketsAuthOptions, SocketsAuthResponse, SocketsAuthErrorResponse, SocketsChannelPrefix, SocketsAuthUser } from &apos;./sockets.types&apos;
import type Pusher from &apos;pusher&apos;

interface StoredPatternInfo&amp;lt;
  TParams extends SocketsChannelParams,
  TPresenceData extends Pusher.PresenceChannelData
&amp;gt; {
  config: SocketsChannelConfig&amp;lt;TParams, TPresenceData&amp;gt;;
  patternParts: string[];
}

export class SocketAuthenticator&amp;lt;TUser extends SocketsAuthUser = SocketsAuthUser&amp;gt; {

  private channelPatternsByLength = new Map&amp;lt;number, StoredPatternInfo&amp;lt;any, any&amp;gt;[]&amp;gt;()

  private options: Required&amp;lt;SocketsAuthOptions&amp;gt;

  constructor(opts?: SocketsAuthOptions) {

    if (!opts?.pusher) {
      throw new Error(&apos;Pusher instance is required&apos;)
    }

    this.options = {
      privateChannelsPrefix: opts.privateChannelsPrefix ?? &apos;private-&apos;,
      presenceChannelsPrefix: opts.presenceChannelsPrefix ?? &apos;presence-&apos;,
      pusher: opts.pusher
    }
  }

  /**
   * Register a channel pattern with its auth callback
   */
  public channel&amp;lt;
    TParams extends SocketsChannelParams = SocketsChannelParams,
    TPresenceData extends Pusher.PresenceChannelData = Pusher.PresenceChannelData
  &amp;gt;(
    name: string,
    callback: SocketsAuthCallback&amp;lt;TParams, TPresenceData&amp;gt;,
  ): void {
    const normalizedName = name.replace(/^\.+|\.+$/g, &apos;&apos;)

    // Validate pattern syntax
    this.validatePattern(normalizedName)

    const config: SocketsChannelConfig&amp;lt;TParams, TPresenceData&amp;gt; = { pattern: normalizedName, callback }
    const patternParts = normalizedName.split(&apos;.&apos;).filter(Boolean)

    const entry: StoredPatternInfo&amp;lt;TParams, TPresenceData&amp;gt; = {
      config,
      patternParts,
    }

    const len = patternParts.length
    if (!this.channelPatternsByLength.has(len)) {
      this.channelPatternsByLength.set(len, [])
    }
    const entries = this.channelPatternsByLength.get(len)
    if (entries) {
      logger.info(`✅ Registered auth callback for channel pattern: ${normalizedName} (Segments: ${len})`)
      entries.push(entry)
    }
  }

  /**
   * Clear all registered patterns (useful for testing)
   */
  public clearChannels(): void {
    this.channelPatternsByLength.clear()
  }

  /**
   * Validate pattern syntax
   */
  private validatePattern(pattern: string): void {
    // Check for balanced braces
    const braces = pattern.match(/[{}]/g) ?? []
    if (braces.length % 2 !== 0) {
      throw new Error(`❌ Invalid pattern syntax: unbalanced braces in &quot;${pattern}&quot;`)
    }

    // Check for empty parameter names
    const paramMatches = pattern.match(/\{[^}]*\}/g) ?? []
    for (const param of paramMatches) {
      if (param === &apos;{}&apos;) {
        throw new Error(`❌ Invalid pattern syntax: empty parameter in &quot;${pattern}&quot;`)
      }
    }

    // Check for invalid characters in parameter names
    for (const param of paramMatches) {
      const paramName = param.slice(1, -1)
      if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(paramName)) {
        throw new Error(`❌ Invalid pattern syntax: invalid parameter name &quot;${paramName}&quot; in &quot;${pattern}&quot;`)
      }
    }
  }

  /**
   * Handle the socket auth request
   */
  public async handle(request: Request): Promise&amp;lt;SocketsAuthResponse&amp;gt; {
    const user = await this.session(request)

    if (!user) {
      return this.forbidden(&apos;❌ Unauthorized: user not found, banned or invalid&apos;)
    }

    const formData = await request.formData()
    const socketId = formData.get(&apos;socket_id&apos;) as string | null
    const channelName = formData.get(&apos;channel_name&apos;) as string | null

    if (!socketId) {
      return this.error(&apos;Socket ID is required&apos;)
    }

    // We dont have a channel but we have a user, i guess its ok.
    if (!channelName) {
      const userData = this.getUserPresenceData(user)
      const response = this.options.pusher.authenticateUser(socketId, userData)
      return this.ok(response)
    }

    // Attempt to match a channel for the given channel name prefixed
    const channel = this.matchChannel(channelName)

    if (!channel) {
      logger.warn(`❌ Forbidden: Channel ${channelName} (socket: ${socketId}) doesn&apos;t exist`)
      return this.forbidden(&apos;Channel not found or invalid&apos;)
    }

    const {
      config,
      params,
      prefix,
    } = channel

    try {
      // Here we call the provided registered callback to see if the user is authorized to join the channel
      // Similar to how laravel does it.
      const result = await config.callback(
        user as Pusher.UserChannelData,
        params,
        socketId,
        channelName,
      )

      // A special case here for the presence channel, where we can return additional data, but also the user presence data
      if (prefix === this.options.presenceChannelsPrefix &amp;amp;&amp;amp; typeof result === &apos;object&apos; &amp;amp;&amp;amp; result !== null) {
        const response = this.options.pusher.authorizeChannel(socketId, channelName, {
          ...result,
          ...this.getUserPresenceData(user),
        })
        return this.ok(response)
      }

      // If the callback returns true, we authorize the channel
      if (result === true) {
        logger.info(`✅ Authorized channel &quot;${channelName}&quot;. User: ${user.id}, Socket: ${socketId}, Pattern: &quot;${config.pattern}&quot;`)
        const response = this.options.pusher.authorizeChannel(socketId, channelName)
        return this.ok(response)
      }

      logger.warn(`❌ Forbidden: Access denied by callback for channel &quot;${channelName}&quot;. User: ${user.id}, Socket: ${socketId}, Pattern: &quot;${config.pattern}&quot;`)
      return this.forbidden(&apos;Access denied&apos;)
    } catch (error) {
      logger.error(`❌ Error in channel auth callback for &quot;${config.pattern}&quot; (channel: ${channelName}):`, error)
      return this.error(&apos;Internal server error during auth callback&apos;)
    }
  }

  /**
   * Resolve the user session from the request ( in this case better-auth )
   */
  private async session(request: Request) {
    const authSession = await auth.api.getSession({ headers: request.headers })

    if (!authSession?.session || !authSession.user.id) {
      return null
    }
    return authSession.user as unknown as TUser
  }

  /**
   * Get the user presence data
   */
  private getUserPresenceData(user: TUser) {
    return {
      id: String(user.id),
      user_info: { name: user.name ?? user.username ?? &apos;Anonymous&apos; },
    }
  }

  /**
   * Return a forbidden error
   */
  private forbidden(message: string): SocketsAuthErrorResponse {
    return { body: { message }, status: 403 }
  }

  /**
   * Return an error
   */
  private error(message: string): SocketsAuthErrorResponse {
    return { body: { message }, status: 500 }
  }

  /**
   * Return a success response
   */
  private ok(response: Pusher.ChannelAuthResponse): SocketsAuthResponse {
    return { body: response, status: 200 }
  }

  /**
   * Match the channel name to the channel config
   */
  private matchChannel&amp;lt;
    TParams extends SocketsChannelParams,
    TPresenceData extends Pusher.PresenceChannelData
  &amp;gt;(name: string): SocketsMatchedChannelInfo&amp;lt;TParams, TPresenceData&amp;gt; | null {

    let prefix: SocketsChannelPrefix | null = null
    let channelToMatch: string

    if (name.startsWith(this.options.privateChannelsPrefix)) {
      prefix = this.options.privateChannelsPrefix as SocketsChannelPrefix
      channelToMatch = name.substring(this.options.privateChannelsPrefix.length)
    } else if (name.startsWith(this.options.presenceChannelsPrefix)) {
      prefix = this.options.presenceChannelsPrefix as SocketsChannelPrefix
      channelToMatch = name.substring(this.options.presenceChannelsPrefix.length)
    } else {
      return null
    }

    if (!channelToMatch || !prefix) {
      return null
    }

    const match = this.findChannelMatch&amp;lt;TParams, TPresenceData&amp;gt;(channelToMatch)

    if (!match) {
      return null
    }

    return {
      config: match.config,
      params: match.params,
      prefix,
      name,
    }
  }

  /**
   * Find matching channel pattern for the given channel name
   */
  private findChannelMatch&amp;lt;
    TParams extends SocketsChannelParams,
    TPresenceData extends Pusher.PresenceChannelData
  &amp;gt;(channelToMatch: string): { config: SocketsChannelConfig&amp;lt;TParams, TPresenceData&amp;gt;; params: TParams } | null {
    const requestParts = channelToMatch.split(&apos;.&apos;).filter(Boolean)
    const relevantPatternEntries = this.channelPatternsByLength.get(requestParts.length)

    if (!relevantPatternEntries) {
      return null
    }

    for (const entry of relevantPatternEntries) {
      const { config, patternParts } = entry

      const params: SocketsChannelParams = {}
      let match = true

      for (let i = 0; i &amp;lt; patternParts.length; i++) {
        const patternPart = patternParts[i]
        const requestPart = requestParts[i]

        if (patternPart.startsWith(&apos;{&apos;) &amp;amp;&amp;amp; patternPart.endsWith(&apos;}&apos;)) {
          const paramName = patternPart.substring(1, patternPart.length - 1)
          params[paramName] = requestPart
        } else if (patternPart !== requestPart) {
          match = false
          break
        }
      }

      if (match) {
        return {
          config,
          params: params as TParams,
        }
      }
    }
    return null
  }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are the type definitions for the auth service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { UserChannelData } from &apos;pusher&apos;
import type Pusher from &apos;pusher&apos;

export interface SocketsAuthOptions {
  pusher: Pusher
  privateChannelsPrefix?: string
  presenceChannelsPrefix?: string
}

export interface SocketsAuthUser {
  id: string | number
  [key: string]: any
}

export interface SocketsPresenceChannelData {
  user_id: string | number
  user_info?: Record&amp;lt;string, any&amp;gt;
}

export type SocketsChannelParams = Record&amp;lt;string, any&amp;gt;

export type SocketsChannelPrefix = &apos;private-&apos; | &apos;presence-&apos;

export type SocketsAuthCallback&amp;lt;
  TParams extends SocketsChannelParams = SocketsChannelParams,
  TPresenceData extends SocketsPresenceChannelData = SocketsPresenceChannelData,
  TUser extends UserChannelData = UserChannelData,
&amp;gt; = (
  user: TUser,
  params: TParams,
  socketId: string,
  channelName: string,
) =&amp;gt; Promise&amp;lt;boolean | TPresenceData&amp;gt; | boolean | TPresenceData

export interface SocketsChannelConfig&amp;lt;
  TParams extends SocketsChannelParams = SocketsChannelParams,
  TPresenceData extends SocketsPresenceChannelData = SocketsPresenceChannelData,
&amp;gt; {
  pattern: string
  callback: SocketsAuthCallback&amp;lt;TParams, TPresenceData&amp;gt;
}

export interface SocketsMatchedChannelInfo&amp;lt;
  TParams extends SocketsChannelParams = SocketsChannelParams,
  TPresenceData extends SocketsPresenceChannelData = SocketsPresenceChannelData,
&amp;gt; {
  config: SocketsChannelConfig&amp;lt;TParams, TPresenceData&amp;gt;
  params: TParams
  prefix: SocketsChannelPrefix
  name: string
}

export interface SocketsAuthResponse {
  body: any
  status: number
}

export interface SocketsAuthErrorResponse {
  body: {
    message: string
  }
  status: number
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Great! Now that we can authenticate channels, let&apos;s also have an entry point. Every time we call the WebSocket server, we need to ensure our callback is registered somewhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: Make sure this is done only once!&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Pusher from &apos;pusher&apos;
import { env } from &apos;@/env&apos;
import { SocketAuthenticator } from &apos;@/services/sockets/sockets.auth&apos;
import type { User } from &apos;@/types&apos;

export const pusher = new Pusher({
  appId: env.PUSHER_APP_ID ?? &apos;&apos;,
  key: env.PUSHER_APP_KEY ?? &apos;&apos;,
  secret: env.PUSHER_APP_SECRET ?? &apos;&apos;,
  host: env.PUSHER_HOST ?? &apos;&apos;,
  port: env.PUSHER_PORT ?? &apos;&apos;,
  useTLS: env.PUSHER_SCHEME === &apos;https&apos;,
  cluster: &apos;mt1&apos;,
})

export const authenticator = new SocketAuthenticator&amp;lt;User&amp;gt;({ pusher })

interface UserChannelParams {
  userId: string
}

authenticator.channel&amp;lt;UserChannelParams&amp;gt;(&apos;user.{userId}&apos;, (user, params) =&amp;gt; {
  //logger.info(&apos;🔑 Authenticating user&apos;, user, params)
  return Number(user.id) === Number(params.userId)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let&apos;s ensure we also have an endpoint like Laravel&apos;s. I&apos;ll place this in a file called &lt;code&gt;broadcast.auth.ts&lt;/code&gt; in our API routes. This file ensures that when the frontend connects to the WebSocket server, it will be authenticated and can join channels (regardless of which channel it is).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/routes/api/broadcast.auth.ts
import { json } from &apos;@tanstack/react-start&apos;
import { authenticator } from &apos;@/services/sockets/sockets.server&apos;

export const ServerRoute = createServerFileRoute().methods({
  POST: async ({ request }) =&amp;gt; {
    const { body, status } = await authenticator.handle(request)
    return json(body, { status })
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can finally dispatch demo events to connected clients. Here&apos;s a simple example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { pusher } from &apos;@/services/sockets/sockets.server&apos;
const userId = &apos;1&apos;
await pusher.trigger(`private-user.${userId}`, &apos;article.created&apos;, { articleId: &apos;1&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example and in my apps, I use this to trigger events from [[Trigger.dev|https://trigger.dev]] jobs. When a job finishes, I can notify the user and invalidate the [[@tanstack|TanStack Start|https://tanstack.com/start]] router cache to reload the results.&lt;/p&gt;
&lt;h2&gt;4. Client-Side Implementation 🎯&lt;/h2&gt;
&lt;p&gt;Now that we have our server-side implementation fairly complete, let&apos;s see how to implement the client-side part. For simplicity, I&apos;ll use Laravel Echo. You could use the Pusher JS SDK if you prefer—just make sure to adjust the authentication and channel prefixes accordingly.&lt;/p&gt;
&lt;h3&gt;4.1 - Client-Side with Laravel Echo &amp;amp; React ⚛️&lt;/h3&gt;
&lt;p&gt;In this example, I&apos;ll use a React Context Provider to ensure we can wrap our Layout with it, so the socket connection is available everywhere, connects once, and is available for registering new listeners anywhere.&lt;/p&gt;
&lt;p&gt;Let&apos;s break down the code before we start, so you can understand what&apos;s happening:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a Context Provider&lt;/li&gt;
&lt;li&gt;Initialize the Echo client once&lt;/li&gt;
&lt;li&gt;Clean up channels/connections when the component unmounts&lt;/li&gt;
&lt;li&gt;Store the connection state in context&lt;/li&gt;
&lt;li&gt;Provide a clean way to join new channels&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import { createContext, useContext, useState, useRef, useEffect } from &apos;react&apos;
import type { ReactNode, Dispatch, SetStateAction } from &apos;react&apos;
import { env } from &apos;@/env&apos;
import type Echo from &apos;laravel-echo&apos;

interface SocketOptions {
  encrypted?: boolean;
}

type EchoType = Echo&amp;lt;&apos;reverb&apos; | &apos;pusher&apos;&amp;gt;

interface SocketState {
  connected: boolean;
  reconnecting: boolean;
  socketId: string | null;
  channels: string[];
}

interface SocketContextType {
  client: EchoType | null;
  connect: () =&amp;gt; void;
  disconnect: () =&amp;gt; void;
  addChannel: (channel: string) =&amp;gt; void;
  removeChannel: (channel: string) =&amp;gt; void;
  state: SocketState;
  setState: Dispatch&amp;lt;SetStateAction&amp;lt;SocketState&amp;gt;&amp;gt;;
}

const SocketContext = createContext&amp;lt;SocketContextType | null&amp;gt;(null)

export interface SocketProviderProps {
  children: ReactNode
  options?: SocketOptions
}

export function SocketProvider({
  children,
}: SocketProviderProps) {

  const [state, setState] = useState&amp;lt;SocketState&amp;gt;({
    connected: false,
    reconnecting: false,
    socketId: null,
    channels: [],
  })

  const clientRef = useRef&amp;lt;EchoType | null&amp;gt;(null)
  const didBoundInitialEvents = useRef(false)

  const initializeClient = async () =&amp;gt; {
    if (typeof window !== &apos;undefined&apos;) {
      // @ts-expect-error
      window.Pusher = await import(&apos;pusher-js&apos;).then(m =&amp;gt; m.default)
    }

    const { default: Echo } = await import(&apos;laravel-echo&apos;)

    clientRef.current = new Echo({
      broadcaster: &apos;pusher&apos;,
      key: env.VITE_PUSHER_APP_KEY,
      wsHost: env.VITE_PUSHER_HOST,
      wsPort: env.VITE_PUSHER_PORT ? Number(env.VITE_PUSHER_PORT) : 6001,
      wssPort: env.VITE_PUSHER_PORT ? Number(env.VITE_PUSHER_PORT) : 6001,
      disableStats: true,
      cluster: &apos;mt1&apos;,
      forceTLS: env.VITE_PUSHER_SCHEME === &apos;https&apos;,
      encrypted: env.VITE_PUSHER_ENCRYPTED === &apos;true&apos;,
      enabledTransports: [&apos;ws&apos;, &apos;wss&apos;],
      authEndpoint: &apos;/api/broadcast/auth&apos;,
    })

  }

  const bindEvents = () =&amp;gt; {
    if (didBoundInitialEvents.current || !clientRef.current) return

    const client = clientRef.current
    setState(prev =&amp;gt; ({
      ...prev,
      connected: client.connector.pusher.connection.state === &apos;connected&apos;,
      socketId: client.socketId() ?? null
    }))

    client.connector.pusher.connection.bind(&apos;connected&apos;, () =&amp;gt; {
      setState(prev =&amp;gt; ({
        ...prev,
        connected: true,
        reconnecting: false,
        socketId: client.socketId() ?? null
      }))

      console.info(&apos;⚡ Socket was Connected with ID: &apos;, client.socketId())
    })

    client.connector.pusher.connection.bind(&apos;disconnected&apos;, () =&amp;gt; {
      setState(prev =&amp;gt; ({
        ...prev, connected: false,
        reconnecting: false,
        socketId: null
      }))

      console.info(&apos;⚡ Socket Disconnected&apos;)
    })

    client.connector.pusher.connection.bind(&apos;reconnecting&apos;, (attemptNumber: number) =&amp;gt; {
      setState(prev =&amp;gt; ({
        ...prev, connected: false,
        reconnecting: true,
        socketId: null
      }))

      console.info(&apos;⚡ Socket is Trying to reconnect&apos;, attemptNumber)
    })

    didBoundInitialEvents.current = true
  }

  const unbindEvents = () =&amp;gt; {
    if (!clientRef.current) {
      return
    }
    const client = clientRef.current
    client.connector.pusher.connection.unbind(&apos;connected&apos;)
    client.connector.pusher.connection.unbind(&apos;disconnected&apos;)
    client.connector.pusher.connection.unbind(&apos;reconnecting&apos;)
    didBoundInitialEvents.current = false
  }

  const connect = async () =&amp;gt; {
    if (!clientRef.current) {
      await initializeClient()
    }

    if (state.connected) {
      return
    }

    if (!clientRef.current) {
      return
    }

    const client = clientRef.current

    if (client.connector.pusher.connection.state === &apos;connected&apos;) {
      bindEvents()
      return
    }

    client.connect()
    bindEvents()
  }

  const disconnect = () =&amp;gt; {
    if (!clientRef.current || !state.connected) return
    clientRef.current.disconnect()
    unbindEvents()
  }

  const addChannel = (channel: string) =&amp;gt; {
    setState(prev =&amp;gt; ({
      ...prev,
      channels: prev.channels.includes(channel) ? prev.channels : [...prev.channels, channel],
    }))
  }

  const removeChannel = (channel: string) =&amp;gt; {
    setState(prev =&amp;gt; ({
      ...prev,
      channels: prev.channels.filter(c =&amp;gt; c !== channel),
    }))
  }

  useEffect(
    () =&amp;gt; {
      void connect()
      return () =&amp;gt; disconnect()
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    []
  )

  const contextValue: SocketContextType = {
    state,
    setState,
    client: clientRef.current,
    connect,
    disconnect,
    addChannel,
    removeChannel,
  }

  return (
    &amp;lt;SocketContext.Provider value={contextValue} &amp;gt;
      {children}
    &amp;lt;/SocketContext.Provider&amp;gt;
  )
}

export const useSockets = () =&amp;gt; {
  const context = useContext(SocketContext)
  if (!context) {
    throw new Error(&apos;useSockets must be used within a SocketProvider&apos;)
  }
  return context
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wrap your main layout with the &lt;a href=&quot;SocketProvider&quot;&gt;&lt;code&gt;SocketProvider&lt;/code&gt;&lt;/a&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;SocketProvider&amp;gt;
  &amp;lt;Layout&amp;gt;
    &amp;lt;Outlet /&amp;gt;
  &amp;lt;/Layout&amp;gt;
&amp;lt;/SocketProvider&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can use the &lt;a href=&quot;useSockets&quot;&gt;&lt;code&gt;useSockets&lt;/code&gt;&lt;/a&gt; hook to get the socket client and join channels. Here&apos;s a demo of how to join a private channel and listen for events:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { client } = useSockets()

const channelRef = useRef&amp;lt;any&amp;gt;(null)
const onArticleCreated = (e: any) =&amp;gt; {
  void router.invalidate()
  toast.success(&apos;Your Article was created! ✅&apos;)
}

useEffect(() =&amp;gt; {
  if (!client) return

  channelRef.current = client.private(`user.${userId}`)
  channelRef.current.listen(&apos;article.created&apos;, onArticleCreated)

  return () =&amp;gt; {
    channelRef.current?.stopListening(&apos;article.created&apos;, onArticleCreated)
  }
}, [client, userId, onArticleCreated])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach is easily portable to other frameworks like Vue, Solid, etc. You can also create &quot;online&quot; and &quot;offline&quot; components next to user profile components to show connection status indicators. Just make sure to get the state from the context—it&apos;s reactive and should be available everywhere.&lt;/p&gt;
&lt;h2&gt;5. Conclusion 🎉&lt;/h2&gt;
&lt;p&gt;I hope you enjoyed this article and learned something new! While we could dive deeper and explore many more features, I wanted to keep this article focused on the WebSocket implementation. Please make sure to read the documentation for anything mentioned here, as there are still some edge cases to cover!&lt;/p&gt;
&lt;h2&gt;6. Resources 📚&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Laravel Broadcasting Documentation|https://laravel.com/docs/12.x/broadcasting#main-content]]&lt;/li&gt;
&lt;li&gt;[[Laravel Echo Documentation|https://laravel.com/docs/12.x/broadcasting#main-content]]&lt;/li&gt;
&lt;li&gt;[[@tanstack|TanStack Start Documentation|https://tanstack.com/start/docs/getting-started/installation]]&lt;/li&gt;
&lt;li&gt;[[Pusher Documentation|https://pusher.com/docs]]&lt;/li&gt;
&lt;li&gt;[[Better Auth Documentation|https://better-auth.com/docs]]&lt;/li&gt;
&lt;li&gt;[[Trigger.dev|https://trigger.dev]]&lt;/li&gt;
&lt;li&gt;[[Better Auth|https://better-auth.com]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;How do I implement Pusher-compatible WebSockets in TanStack Start?&quot;, answer: &quot;Create a SocketAuthenticator class that validates channel subscriptions using pattern matching (similar to Laravel&apos;s channel registration). Set up a server route at /api/broadcast/auth that handles Pusher authentication requests. On the client, use Laravel Echo with pusher-js in a React Context Provider to manage connections, and use the useSockets hook to join private channels and listen for events.&quot; },
{ question: &quot;How do Sockudo, Soketi, and Laravel Reverb compare as Pusher alternatives?&quot;, answer: &quot;Sockudo is a Rust-based Pusher-compatible server that ships as a single binary — easy to run and actively maintained. Soketi is a Node.js-based alternative but appears abandoned. Laravel Reverb is Laravel-specific and integrates directly with Laravel&apos;s broadcasting system. All three implement the Pusher protocol, so you can use the same pusher-js client SDK and Laravel Echo.&quot; },
{ question: &quot;How does Laravel Echo work with TanStack Start for real-time events?&quot;, answer: &quot;Laravel Echo connects to a Pusher-compatible server (Pusher, Sockudo, Soketi, or Laravel Reverb) and handles channel authentication via your auth endpoint. In TanStack Start, wrap your app with a SocketProvider that initializes Echo, manages connection state, and provides a useSockets hook. Components use client.private() to join channels and .listen() to react to server-dispatched events.&quot; },
{ question: &quot;How can I use WebSockets with Trigger.dev and TanStack Start?&quot;, answer: &quot;Dispatch Pusher events from Trigger.dev background jobs when they complete, using the pusher.trigger() method on a private user channel. On the TanStack Start client, listen for these events via Laravel Echo and invalidate TanStack Router cache to reload results, giving users real-time feedback when async jobs finish.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>tanstack-start</category><category>pusher</category><category>websockets</category><category>tanstack</category><category>laravel</category><category>sockudo</category><category>soketi</category><author>Pedro Martins</author></item><item><title>Astro - Power your blog with Markdown View, Copy buttons to LLMS + Voice Resumes</title><link>https://nikuscs.com/blog/07-astro-copy-llms-button/</link><guid isPermaLink="true">https://nikuscs.com/blog/07-astro-copy-llms-button/</guid><description>In this article, I&apos;ll show you how to add a view &amp; copy markdown, OpenAI &amp; Claude button to your Astro blog, but also how to add a voice resume of the content to your blog.</description><pubDate>Thu, 17 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Callout from &quot;@components/Callout.astro&quot;;&lt;/p&gt;
&lt;h2&gt;1. Introduction&lt;/h2&gt;
&lt;p&gt;I&apos;ve been watching many documentation websites and blogs make it easier for LLMs/AI to read content or copy and paste to your favorite AI tools - a great signal for the future of documentation websites.&lt;/p&gt;
&lt;p&gt;I first saw this on [[Mintlify|https://mintlify.com/]] and thought it was a great idea, but never bothered implementing it. Since I&apos;m refactoring my blog to the latest Astro version and saw the following tweet, I took the chance to implement it here.&lt;/p&gt;
&lt;p&gt;&amp;lt;blockquote class=&quot;twitter-tweet&quot;&amp;gt;&amp;lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&amp;gt;having this in your docs is incredibly high signal &amp;lt;a href=&quot;https://t.co/Z5HtOGEhu3&quot;&amp;gt;pic.twitter.com/Z5HtOGEhu3&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;— Nathan Covey (@nathan_covey) &amp;lt;a href=&quot;https://twitter.com/nathan_covey/status/1945233405395775691?ref_src=twsrc%5Etfw&quot;&amp;gt;July 15, 2025&amp;lt;/a&amp;gt;&amp;lt;/blockquote&amp;gt; &amp;lt;script async src=&quot;https://platform.twitter.com/widgets.js&quot; charset=&quot;utf-8&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p&gt;We&apos;ll also add automatic voice generation of the content, so your readers can listen to a short summary. Without further delay, let&apos;s get started.&lt;/p&gt;
&lt;h2&gt;2. Add Markdown View &amp;amp; Copy buttons to your blog&lt;/h2&gt;
&lt;p&gt;While it looks easy at first, this was quite a challenge because I didn&apos;t know Astro internals well enough to make it work. I&apos;ll explain the steps I took and how you can do it too.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My Astro blog uses MDX with custom components - how would I convert them to pure markdown?&lt;/li&gt;
&lt;li&gt;Should I just crawl my own page and convert it to markdown from HTML? That would also bring headers and other elements not related to content.&lt;/li&gt;
&lt;li&gt;How could I render the content in Astro without the runtime while preserving plugins I add for Remark, Shiki, etc., so it wouldn&apos;t be a pain to maintain?&lt;/li&gt;
&lt;li&gt;What current tools do we have to convert HTML to Markdown?&lt;/li&gt;
&lt;li&gt;Astro MDX renders to HTML, so I still need to convert it to markdown.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After digging around, I found a way to do it (probably not the best but it works! 🥲) and I&apos;ll explain it in the following sections.&lt;/p&gt;
&lt;h3&gt;2.1. MDX to Markdown&lt;/h3&gt;
&lt;p&gt;This is where I struggled the most. Crawling my own website felt heavy and I was looking for a &quot;cleaner&quot; way. I could also attempt to parse my raw MDX and &quot;traverse&quot; the tree to convert it to markdown, but I asked myself:
This should be doable with Astro only, right? Well, yes, but it&apos;s still experimental!&lt;/p&gt;
&lt;p&gt;First I found this article: &lt;a href=&quot;https://codetv.dev/blog/mdx-to-rss-astro&quot;&gt;The terrible things I did to Astro to render MDX content in my RSS feed&lt;/a&gt;, which was somewhat what I needed, but we still needed a lot of boilerplate to make it work. However, at the end of the article I found a link to an &lt;a href=&quot;https://github.com/withastro/roadmap/issues/533&quot;&gt;Astro RFC&lt;/a&gt; that was exactly what I needed! Lucky me, this was already pushed as an experimental feature.&lt;/p&gt;
&lt;p&gt;Let&apos;s check the [[Astro Container Reference|https://docs.astro.build/en/reference/container-reference/]] and see how we can use it to render the content. Based on this, here&apos;s an example utility I created:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// lib/markdown/to-markdown.ts
import { getEntry, type CollectionEntry } from &quot;astro:content&quot;;
import { experimental_AstroContainer as AstroContainer, type AstroContainerUserConfig } from &quot;astro/container&quot;;
import { astroConfig } from &quot;../../../astro.config.mjs&quot;;
import reactRenderer from &quot;@astrojs/react/server.js&quot;;
import vueRenderer from &quot;@astrojs/vue/server.js&quot;;
import mdxRenderer from &quot;@astrojs/mdx/server.js&quot;;
import { render } from &quot;astro:content&quot;;
import turndown from &quot;turndown&quot;;
import turndownPluginGfm from &quot;@joplin/turndown-plugin-gfm&quot;;
import CraftBox from &quot;@components/CraftBox.astro&quot;;

export interface ToMarkdownProps {
  post: CollectionEntry&amp;lt;&quot;blog&quot;&amp;gt;;
  request: Request;
}

export async function toMarkdown({ post, request }: ToMarkdownProps) {

  const container = await AstroContainer.create({
    astroConfig: astroConfig as AstroContainerUserConfig,
  });

  container.addServerRenderer({ renderer: vueRenderer, name: &quot;@astrojs/vue&quot; });
  container.addServerRenderer({ renderer: mdxRenderer, name: &quot;@astrojs/mdx&quot; });
  container.addServerRenderer({ renderer: reactRenderer, name: &quot;@astrojs/react&quot; });
  container.addClientRenderer({ name: &quot;@astrojs/react&quot;, entrypoint: &quot;@astrojs/react/client.js&quot; })

  const entry = await getEntry(&quot;blog&quot;, post.id);

  if (!entry || entry.data.draft) {
    throw new Error(&quot;Post not found or it is a draft&quot;);
  }

  const { Content } = await render(entry);

  const html = await container.renderToString(Content, {
    request,
    props: {
      components: {
        CraftBox,
      }
    }
  });

  const turndownService = new turndown({ codeBlockStyle: &quot;fenced&quot; });

  // Parse Inline links
  turndownService.addRule(&quot;inlineLink&quot;, {
    filter: function (node: any, options: any) {
      return (
        options.linkStyle === &quot;inlined&quot; &amp;amp;&amp;amp;
        node.nodeName === &quot;A&quot; &amp;amp;&amp;amp;
        node.getAttribute(&quot;href&quot;)
      );
    },
    replacement: function (content: any, node: any) {
      var href = node.getAttribute(&quot;href&quot;).trim();
      var title = node.title ? &apos; &quot;&apos; + node.title + &apos;&quot;&apos; : &quot;&quot;;
      return &quot;[&quot; + content.trim() + &quot;](&quot; + href + title + &quot;)\n&quot;;
    },
  });

  // Parse Fenced Code Blocks
  turndownService.addRule(&apos;fencedCodeBlock&apos;, {
    filter: function (node: any, options: any) {
      return (
        options.codeBlockStyle === &apos;fenced&apos; &amp;amp;&amp;amp;
        node.nodeName === &apos;PRE&apos; &amp;amp;&amp;amp;
        node.firstChild &amp;amp;&amp;amp;
        node.firstChild.nodeName === &apos;CODE&apos;
      )
    },
    replacement: function (content: any, node: any, options: any) {
      var language = node.getAttribute(&quot;data-language&quot;) || &quot;&quot;
      return (
        &quot;\n\n&quot; + options.fence + language + &quot;\n&quot; +
        node.firstChild.textContent +
        &quot;\n&quot; + options.fence + &quot;\n\n&quot;
      )
    }
  })

  // Use Github flavored markdown
  turndownService.use(turndownPluginGfm.gfm);

  // Convert HTML to Markdown
  const markdown = turndownService.turndown(html);
 return markdown;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So what&apos;s happening here? Let&apos;s break it down in easy steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Astro was previously using the runtime to render content, and the APIs were too tight to the runtime, so we could not use it to render content outside of the Astro context.&lt;/li&gt;
&lt;li&gt;Astro released the Astro Container, which is a way to render content outside of the Astro context, but it was still experimental.&lt;/li&gt;
&lt;li&gt;We pull our Astro config, create the container with the config, and add the necessary renderers to make it work.&lt;/li&gt;
&lt;li&gt;We get our blog entry and use the &lt;code&gt;render&lt;/code&gt; function to get the content component.&lt;/li&gt;
&lt;li&gt;Finally we render the content to HTML like Astro would do in a normal context, passing our custom components to the context and request if needed.&lt;/li&gt;
&lt;li&gt;Once we have the HTML, we can use the &lt;code&gt;turndown&lt;/code&gt; library to convert it to markdown.&lt;/li&gt;
&lt;li&gt;We create small inline rules to convert the inline links and fenced code blocks to markdown and retain the snippet&apos;s original language.&lt;/li&gt;
&lt;li&gt;Finally we use GitHub flavored markdown to convert the HTML to markdown and return it as a string.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yay! We did MDX → HTML → Markdown, now things just got easier!&lt;/p&gt;
&lt;h3&gt;2.2. Serving the Markdown&lt;/h3&gt;
&lt;p&gt;If you&apos;re on Astro, this is probably a boring part. Simply add a new route next to your blog post route. Like so: &lt;code&gt;src/pages/blog/[id].md.ts&lt;/code&gt; and add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { getCollection, type CollectionEntry } from &apos;astro:content&apos;
import type { APIRoute } from &apos;astro&apos;
import { toMarkdown } from &quot;@lib/markdown/to-markdown&quot;;

export async function getStaticPaths() {
	const posts = await getCollection(&apos;blog&apos;)
	return posts.map((post) =&amp;gt; ({
		params: { id: post.id },
		props: post,
	}))
}

export const GET: APIRoute = async ({ props, request }) =&amp;gt; {
	try {
		const markdown = await toMarkdown({ post: props as CollectionEntry&amp;lt;&quot;blog&quot;&amp;gt;, request });
		return new Response(markdown, {
			headers: {
				&apos;Content-Type&apos;: &apos;text/markdown; charset=utf-8&apos;,
			},
		});
	} catch (error) {
		console.error(error);
		return new Response(&apos;Error&apos;, { status: 500 });
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should be more than enough to serve the markdown version of your blog post - simply add the &lt;code&gt;.md&lt;/code&gt; extension to your blog post route and it should just work!&lt;/p&gt;
&lt;h3&gt;2.3. Add the buttons to your blog post&lt;/h3&gt;
&lt;p&gt;Now that we have the markdown version of our blog post, we can add the buttons to it. For this I used the [[shadcn/ui|https://ui.shadcn.com/]] dropdown menu component.&lt;/p&gt;
&lt;p&gt;&amp;lt;Callout icon=&quot;☝️&quot; type=&quot;info&quot;&amp;gt;
I&apos;ve removed the icons to keep it simple, but you can add them back if you want. Also there&apos;s an issue with the dropdown menu with Astro Transitions - watch the &lt;a href=&quot;https://github.com/withastro/astro/issues/10863&quot;&gt;issue&lt;/a&gt; for more details.
&amp;lt;/Callout&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useState, type SVGProps } from &quot;react&quot;;
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from &quot;./ui/dropdown-menu&quot;;

interface BlogActionsDropdownReactProps {
  url: string;
}

export default function BlogActionsDropdownReact({ url }: BlogActionsDropdownReactProps) {
  const [copyStatus, setCopyStatus] = useState&amp;lt;string&amp;gt;(&quot;Copy Markdown&quot;);

  const [dropdownOpen, setDropdownOpen] = useState(false);

  const fetchMarkdown = async () =&amp;gt; {
    try {
      const response = await fetch(url);
      if (response.ok) {
        const content = await response.text();
        return content;
      }
    } catch (error) {
      console.error(&quot;Failed to fetch markdown content:&quot;, error);
    }
  };


  const handleAskGPT = () =&amp;gt; {
    const content = encodeURIComponent(&apos;Please read the contents from the following link so i can ask question about it: &apos; + url);
    const chatGptUrl = `https://chatgpt.com/?q=${content}`;
    window.open(chatGptUrl, &quot;_blank&quot;, &quot;noopener,noreferrer&quot;);
  };

  const handleAskClaude = () =&amp;gt; {
    const content = encodeURIComponent(&apos;Please read the contents from the following link so i can ask question about it: &apos; + url);
    const chatClaudeUrl = `https://claude.ai/new?q=${content}`;
    window.open(chatClaudeUrl, &quot;_blank&quot;, &quot;noopener,noreferrer&quot;);
  };

  const handleCopyMarkdown = async () =&amp;gt; {
    const markdownContent = await fetchMarkdown();
    if (!markdownContent) {
      setCopyStatus(&quot;Failed&quot;);
      return;
    }
    try {
      await navigator.clipboard.writeText(markdownContent);
      setCopyStatus(&quot;Copied!&quot;);
      setTimeout(() =&amp;gt; {
        setCopyStatus(&quot;Copy Markdown&quot;);
      }, 2000);
    } catch (error) {
      console.error(&quot;Failed to copy to clipboard:&quot;, error);
    }
  };

  const handleViewMarkdown = () =&amp;gt; {
    window.open(url, &quot;_blank&quot;, &quot;noopener,noreferrer&quot;);
  };

  return (
    &amp;lt;DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}&amp;gt;
      &amp;lt;DropdownMenuTrigger
        onClick={() =&amp;gt; setDropdownOpen((val) =&amp;gt; !val)}
        className=&quot;not-prose group relative flex w-fit flex-nowrap rounded-sm border border-black/15 py-1.5 px-2 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white text-sm items-center justify-center gap-1.5&quot;&amp;gt;
        &amp;lt;div className=&quot;flex items-center justify-center gap-1.5 divide-x divide-border&quot;&amp;gt;
          &amp;lt;div className=&quot;flex items-center justify-center gap-1.5 pr-1.5&quot;&amp;gt;
            &amp;lt;IconCopy className=&quot;size-3&quot; /&amp;gt;
            &amp;lt;span&amp;gt;Copy Page&amp;lt;/span&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;div className=&quot;flex items-center justify-center gap-1.5&quot;&amp;gt;
            &amp;lt;IconChevronDown className=&quot;size-3 transition-transform duration-300 ease-in-out group-data-[state=open]:rotate-180&quot; /&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/DropdownMenuTrigger&amp;gt;
      &amp;lt;DropdownMenuContent align=&quot;end&quot; className=&quot;min-w-[8rem]&quot;&amp;gt;
        &amp;lt;DropdownMenuItem onClick={handleAskGPT}&amp;gt;
          &amp;lt;IconOpenai className=&quot;size-3&quot; /&amp;gt;
          &amp;lt;span&amp;gt;Ask GPT&amp;lt;/span&amp;gt;
        &amp;lt;/DropdownMenuItem&amp;gt;
        &amp;lt;DropdownMenuItem onClick={handleAskClaude}&amp;gt;
          &amp;lt;IconClaude className=&quot;size-3&quot; /&amp;gt;
          &amp;lt;span&amp;gt;Ask Claude&amp;lt;/span&amp;gt;
        &amp;lt;/DropdownMenuItem&amp;gt;
        &amp;lt;DropdownMenuItem onClick={handleCopyMarkdown}&amp;gt;
          &amp;lt;span&amp;gt;{copyStatus}&amp;lt;/span&amp;gt;
        &amp;lt;/DropdownMenuItem&amp;gt;
        &amp;lt;DropdownMenuItem onClick={handleViewMarkdown}&amp;gt;
          &amp;lt;span&amp;gt;View Markdown&amp;lt;/span&amp;gt;
        &amp;lt;/DropdownMenuItem&amp;gt;
      &amp;lt;/DropdownMenuContent&amp;gt;
    &amp;lt;/DropdownMenu&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s it! With this in place, you can add the button to your blog post page, and it should just work!&lt;/p&gt;
&lt;p&gt;We have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Copy markdown to your clipboard&lt;/li&gt;
&lt;li&gt;Serve the Markdown version of your blog post&lt;/li&gt;
&lt;li&gt;Add a link to GPT &amp;amp; Claude to ask questions about the content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is how it looks like:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;3. Voice Summary&lt;/h2&gt;
&lt;p&gt;Now that we have the markdown version of our blog post, it&apos;s time to add a voice summary. While this is probably overkill, I had some fun doing it, so I&apos;ll share it with you.&lt;/p&gt;
&lt;p&gt;This involves 4 steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Summarize the raw content of the blog post using [[OpenAI|https://openai.com/]] &amp;amp; [[ai-sdk|https://ai-sdk.com/]]&lt;/li&gt;
&lt;li&gt;Generate the voice summary using [[ElevenLabs|https://elevenlabs.io/]] or [[Murf|https://murf.ai/]]&lt;/li&gt;
&lt;li&gt;Generate &amp;amp; save the voice summary to a file&lt;/li&gt;
&lt;li&gt;Add the voice summary to your blog post&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.1 - Summarize the raw content of the blog post&lt;/h3&gt;
&lt;p&gt;This is probably the easiest part if you&apos;re familiar with [[Vercel|https://vercel.com/]] SDK, so I&apos;ll just jump right into the code.
I&apos;ve tweaked the prompt to output the summary in a more natural way - feel free to tweak it to your liking or suggest how I can make it better.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// lib/voice/summarizer.ts
import { generateObject } from &apos;ai&apos;;
import { createOpenAI } from &apos;@ai-sdk/openai&apos;;
import { z } from &apos;zod&apos;;
import { env } from &apos;../../env&apos;;

export interface SummaryOptions {
  tone?: &apos;professional&apos; | &apos;casual&apos; | &apos;engaging&apos;;
  focusAreas?: string[];
  includeKeyPoints?: boolean;
}

export interface Metadata {
  title: string;
  description?: string;
  tags?: string[];
}

export async function summarize(
  mdxContent: string,
  metadata: Metadata,
  options: SummaryOptions = {}
) {

  const {
    tone = &apos;engaging&apos;,
    focusAreas = [],
  } = options;

  const openaiProvider = createOpenAI({ apiKey: env.OPENAI_API_KEY });
  const model = openaiProvider(&apos;gpt-4o-mini&apos;);

  const systemPrompt = `You are Albert, an expert summarizer. 
Your task is to:
1. Create a concise, engaging summary that flows well when spoken aloud.
2. Maintain the key technical concepts and insights
3. Use natural speech patterns and transitions
4. Keep the ${tone} tone throughout
5. MAXIMUM 3000 characters AND 250 words.
${focusAreas.length &amp;gt; 0 ? `6. Focus particularly on: ${focusAreas.join(&apos;, &apos;)}` : &apos;&apos;}

The summary should be suitable for text-to-speech conversion and provide value to listeners who want to quickly understand the main points of the article, but without compromising the quality of the content.

You may use the following format to add pauses: [pause 1s]

Here are some important rules to following:

- Make sure you dont miss any important comparisons or insights, like if we talk about Frameworks, make sure you mention them correctly.
- While code snippets are important, you can can skip the code and explain briefly what they do.
- Always talk in third person, like you are the narrotor for nikuscs blog post.
- Dont use fancy words, keep the language simple, natural and easy to understand by the readers.
- Users will not &quot;listen&quot; the article, they will read it and listen to the summary.
- Avoid adding weird or special characters to the summary like: In this insightful blog post titled \&quot;Tresjs + Vue - Holographic Sticker,\&quot;
- Attempt to replace characters like: Tresjs + Vue - Holographic Sticker with natural words like: Tresjs and Vue - Holographic Sticker. ( This is a good example but be smart about other types thay maybe follow)
- If twitter/x URLS or other links are in the content, attempt to mention them in the summary by their names.
- For Frameworks or weird namings or words you can ( but not always need to ) use the pronunciation dictionary as you see fit like: &quot;React&quot; -&amp;gt; &quot;Ree Act&quot;, only if it makes sense.

Please summarize the following blog post:

**Title:** ${metadata.title}
${metadata.description ? `**Description:** ${metadata.description}` : &apos;&apos;}
${metadata.tags ? `**Tags:** ${metadata.tags.join(&apos;, &apos;)}` : &apos;&apos;}

**Content:**
${mdxContent}
`;

  try {
    const { object } = await generateObject({
      model,
      prompt: systemPrompt,
      schema: z.object({
        summary: z.string().describe(&apos;A natural, conversational summary suitable for audio that flows well when spoken aloud. MAXIMUM 3000 characters AND 250 words.&apos;),
      }),
    });

    const summary = object.summary;
    const originalLength = mdxContent.length;
    const compressionRatio = ((originalLength - summary.length) / originalLength) * 100;

    return {
      summary,
      originalLength,
      summaryLength: summary.length,
      compressionRatio: Math.round(compressionRatio * 100) / 100,
    };
  } catch (error) {
    console.error(&apos;❌ Error summarizing content:&apos;, error);
    throw new Error(`Failed to summarize content: ${error instanceof Error ? error.message : &apos;Unknown error&apos;}`);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 - Generate the voice summary&lt;/h3&gt;
&lt;p&gt;Initially I was thinking about using ElevenLabs, but I found they&apos;re now subscription-based. While I used most of the free credits while testing, I didn&apos;t feel like subscribing to ONE more AI service just for generating casual voice summaries.
So I implemented ElevenLabs, but also Murf which offers a pay-as-you-go model.&lt;/p&gt;
&lt;p&gt;It&apos;s clear to me that ElevenLabs outputs are much better, but for this case, it serves the purpose.&lt;/p&gt;
&lt;p&gt;&amp;lt;Callout icon=&quot;☝️&quot; type=&quot;info&quot;&amp;gt;
Check the Murf implementation for more details, as it contains a pronunciation dictionary for the most common words in tech - this was also a fun challenge to implement.
&amp;lt;/Callout&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// lib/voice/client.elevenlabs.ts
export interface ElevenLabsVoiceOptions {
  voiceId?: string;
  modelId?: string;
  stability?: number;
  similarityBoost?: number;
  style?: number;
  useSpeakerBoost?: boolean;
}

export interface VoiceSettings {
  stability: number;
  similarity_boost: number;
  style?: number;
  use_speaker_boost?: boolean;
}

export class ElevenLabsClient {
  private readonly apiKey: string;
  private readonly baseUrl = &apos;https://api.elevenlabs.io/v1&apos;;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async generate(
    text: string,
    options: ElevenLabsVoiceOptions = {}
  ): Promise&amp;lt;ArrayBuffer&amp;gt; {
    const {
      voiceId = &apos;JBFqnCBsd6RMkjVDRZzb&apos;,
      modelId = &apos;eleven_turbo_v2_5&apos;,
      stability = 0.5,
      similarityBoost = 0.5,
      style = 0,
      useSpeakerBoost = true,
    } = options;

    const url = `${this.baseUrl}/text-to-speech/${voiceId}`;

    const requestBody = {
      text,
      model_id: modelId,
      voice_settings: {
        stability,
        similarity_boost: similarityBoost,
        style,
        use_speaker_boost: useSpeakerBoost,
      },
    };

    try {
      const response = await fetch(url, {
        method: &apos;POST&apos;,
        headers: {
          &apos;Accept&apos;: &apos;audio/mpeg&apos;,
          &apos;Content-Type&apos;: &apos;application/json&apos;,
          &apos;xi-api-key&apos;: this.apiKey,
        },
        body: JSON.stringify(requestBody),
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`ElevenLabs API error: ${response.status} ${response.statusText} - ${errorText}`);
      }

      const arrayBuffer = await response.arrayBuffer();
      return arrayBuffer;
    } catch (error) {
      console.error(&apos;❌ Error generating speech with ElevenLabs:&apos;, error);
      throw new Error(`ElevenLabs TTS failed: ${error instanceof Error ? error.message : &apos;Unknown error&apos;}`);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// lib/voice/client.murf.ts
export interface MurfVoiceOptions {
  voiceId?: string;
  speed?: number;
  pitch?: number;
  volume?: number;
  emphasis?: &apos;strong&apos; | &apos;moderate&apos; | &apos;reduced&apos;;
  pauseLength?: number;
  style?: string;
}

export interface MurfSpeechRequest {
  text: string;
  voiceId: string;
  audioFormat?: &apos;mp3&apos; | &apos;wav&apos; | &apos;flac&apos;;
  sampleRate?: number;
  bitRate?: number;
  speed?: number;
  pitch?: number;
  volume?: number;
  emphasis?: &apos;strong&apos; | &apos;moderate&apos; | &apos;reduced&apos;;
  pauseLength?: number;
  style?: string;
  pronunciationDictionary?: Record&amp;lt;string, { type: string; pronunciation: string }&amp;gt;;
}

export interface MurfSpeechResponse {
  audioFile: string;
  contentType?: string;
  duration?: number;
  size?: number;
}

export class MurfClient {
  private readonly apiKey: string;
  private readonly baseUrl = &apos;https://api.murf.ai/v1&apos;;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async generate(
    text: string,
    options: MurfVoiceOptions = {}
  ): Promise&amp;lt;ArrayBuffer&amp;gt; {

    const {
      voiceId = &apos;en-US-charles&apos;,
      speed = 0.8,
      pitch = 1.0,
      volume = 1.0,
      emphasis = &apos;moderate&apos;,
      pauseLength = 0.5,
      style = &apos;calm&apos;
    } = options;

    const requestBody: MurfSpeechRequest = {
      text,
      voiceId,
      speed,
      pitch,
      volume,
      emphasis,
      pauseLength,
      style,
      pronunciationDictionary: {
        &apos;js&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;jay ess&quot; },
        &apos;ts&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;tee ess&quot; },
        &apos;html&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;H T M L&quot; },
        &apos;css&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;C S S&quot; },
        &apos;json&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;jay son&quot; },
        &apos;yaml&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;yammel&quot; },
        &apos;vue&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;view&quot; },
        &apos;nuxt&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;nuhkst&quot; },
        &apos;react&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;ree act&quot; },
        &apos;angular&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;ang you lar&quot; },
        &apos;svelte&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;svelt&quot; },
        &apos;vite&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;vite&quot; },
        &apos;tailwind&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;tail wind&quot; },
        &apos;shadcn&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;shad sea en&quot; },
        &apos;threejs&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;three jay ess&quot; },
        &apos;nestjs&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;nest jay ess&quot; },
        &apos;nextjs&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;next jay ess&quot; },
        &apos;next.js&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;next jay ess&quot; },
        &apos;vue.js&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;view jay ess&quot; },
        &apos;laravel&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;la ruh vell&quot; },
        &apos;express&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;ex press&quot; },
        &apos;tsx&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;T S X&quot; },
        &apos;jsx&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;J S X&quot; },
        &apos;dom&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;dee oh em&quot; },
        &apos;cpu&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;C P U&quot; },
        &apos;gpu&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;G P U&quot; },
        &apos;sdk&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;S D K&quot; },
        &apos;api&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;A P I&quot; },
        &apos;cli&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;see ell eye&quot; },
        &apos;gh&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;gee aitch&quot; },
        &apos;ci&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;C I&quot; },
        &apos;cd&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;C D&quot; },
        &apos;http&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;H T T P&quot; },
        &apos;https&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;H T T P S&quot; },
        &apos;ai&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;A I&quot; },
        &apos;ml&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;M L&quot; },
        &apos;ui&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;you eye&quot; },
        &apos;ux&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;you ex&quot; },
        &apos;sql&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;sequel&quot; },
        &apos;npm&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;N P M&quot; },
        &apos;pnpm&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;P N P M&quot; },
        &apos;yarn&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;yarn&quot; },
        &apos;tresjs&apos;: { type: &quot;SAY_AS&quot;, pronunciation: &quot;tres jay ess&quot; },
      },
    };


    try {
      const response = await fetch(`${this.baseUrl}/speech/generate`, {
        method: &apos;POST&apos;,
        headers: {
          &apos;Content-Type&apos;: &apos;application/json&apos;,
          &apos;api-key&apos;: this.apiKey,
          &apos;Accept&apos;: &apos;application/json&apos;,
        },
        body: JSON.stringify(requestBody),
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Murf API error: ${response.status} - ${errorText}`);
      }

      const data: MurfSpeechResponse = await response.json();
      const audioResponse = await fetch(data.audioFile);

      if (!audioResponse.ok) {
        throw new Error(`Failed to download audio: ${audioResponse.status}`);
      }
      const audioBuffer = await audioResponse.arrayBuffer();
      return audioBuffer;
    } catch (error) {
      console.error(&apos;❌ Error generating speech with Murf:&apos;, error);
      throw new Error(`Murf TTS failed: ${error instanceof Error ? error.message : &apos;Unknown error&apos;}`);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, we have both clients. Now let&apos;s create the TTS entry point that would pick one or another.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// lib/voice/tts.ts
import { ElevenLabsClient } from &apos;./client.elevenlabs&apos;;
import { MurfClient } from &apos;./client.murf&apos;;
import { env } from &apos;../../env&apos;;

export type TTSProvider = &apos;elevenlabs&apos; | &apos;murf&apos;;

export interface TTSOptions {
  provider?: TTSProvider;
  voiceId?: string;
  speed?: number;
  pitch?: number;
  volume?: number;
}

export interface TTSResult {
  audioBuffer: ArrayBuffer;
  provider: TTSProvider;
  size: number;
}

export async function generate(
  text: string,
  options: TTSOptions = {}
): Promise&amp;lt;TTSResult&amp;gt; {
  const { provider = &apos;murf&apos; } = options;

  console.log(`🎤 Generating TTS with ${provider}...`);

  let audioBuffer: ArrayBuffer;

  try {
    switch (provider) {
      case &apos;murf&apos;:
        const murfClient = new MurfClient(env.MURF_API_KEY);
        audioBuffer = await murfClient.generate(text, {
          voiceId: options.voiceId ?? &apos;en-US-charles&apos;,
          speed: options.speed ?? 1.0,
          pitch: options.pitch ?? 1.0,
          volume: options.volume ?? 1.0,
        });
        break;

      case &apos;elevenlabs&apos;:
        const elevenLabsClient = new ElevenLabsClient(env.ELEVENLABS_API_KEY);
        audioBuffer = await elevenLabsClient.generate(text, options);
        break;

      default:
        throw new Error(`Unsupported TTS provider: ${provider}`);
    }

    return {
      audioBuffer,
      provider,
      size: audioBuffer.byteLength,
    };
  } catch (error) {
    console.error(`❌ TTS generation failed with ${provider}:`, error);
    throw new Error(`TTS generation failed: ${error instanceof Error ? error.message : &apos;Unknown error&apos;}`);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 - Generate &amp;amp; Save the voice summary to a file&lt;/h3&gt;
&lt;p&gt;So we have both clients implemented. Now let&apos;s generate the voice summary for each of our blog posts.
For simplicity, I created a command to generate them, skipping the ones that already exist. This is done before I run the deploy for each article I write.&lt;/p&gt;
&lt;p&gt;This file will take care of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Parse the MDX files in the blog directory&lt;/li&gt;
&lt;li&gt;Call the summarizer&lt;/li&gt;
&lt;li&gt;Pass the summary to the TTS client&lt;/li&gt;
&lt;li&gt;Save the voice summary to a file in the public directory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can add it to your &lt;code&gt;package.json&lt;/code&gt; scripts if needed like: &lt;code&gt;voice:generate&lt;/code&gt; with &lt;code&gt;bun run src/lib/voice/command.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// lib/voice/command.ts
#!/usr/bin/env bun
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from &apos;fs&apos;;
import { join, dirname } from &apos;path&apos;;
import { fileURLToPath } from &apos;url&apos;;
import { generate } from &apos;./tts&apos;;
import { summarize } from &apos;./summarizer&apos;;

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const BLOG_DIR = join(__dirname, &apos;../../../src/content/blog&apos;);
const AUDIO_DIR = join(__dirname, &apos;../../../public/audio/blog&apos;);

interface BlogPost {
  id: string;
  data: {
    title: string;
    description: string;
    date: string;
    draft?: boolean;
    tags?: string[];
    cover?: string;
  };
}

function parseMDX(content: string): BlogPost[&apos;data&apos;] {
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
  if (!frontmatterMatch) {
    throw new Error(&apos;No frontmatter found&apos;);
  }

  const frontmatter = frontmatterMatch[1];
  const data: any = {};

  // Parse key-value pairs
  const lines = frontmatter.split(&apos;\n&apos;);
  for (const line of lines) {
    const trimmedLine = line.trim();
    if (!trimmedLine || trimmedLine.startsWith(&apos;#&apos;)) continue;

    if (trimmedLine.endsWith(&apos;:&apos;)) {
      continue;
    }

    if (trimmedLine.startsWith(&apos;- &apos;)) {
      const value = trimmedLine.slice(2).trim();
      if (!data.tags) data.tags = [];
      data.tags.push(value);
      continue;
    }

    const match = trimmedLine.match(/^(\w+):\s*(.*)$/);
    if (match) {
      const [, key, value] = match;
      if (key === &apos;draft&apos;) {
        data[key] = value === &apos;true&apos;;
      } else {
        data[key] = value.replace(/^[&quot;&apos;]|[&quot;&apos;]$/g, &apos;&apos;);
      }
    }
  }

  return data;
}

function getContent(): BlogPost[] {
  const posts: BlogPost[] = [];
  const blogDirs = readdirSync(BLOG_DIR, { withFileTypes: true })
    .filter(dirent =&amp;gt; dirent.isDirectory())
    .map(dirent =&amp;gt; dirent.name);

  for (const dirName of blogDirs) {
    const postPath = join(BLOG_DIR, dirName, &apos;index.mdx&apos;);
    if (existsSync(postPath)) {
      try {
        const content = readFileSync(postPath, &apos;utf-8&apos;);
        const data = parseMDX(content);
        posts.push({
          id: dirName,
          data
        });
      } catch (error) {
        console.warn(`⚠️  Failed to parse ${dirName}:`, error);
      }
    }
  }

  return posts;
}

async function generateAudioForPost(post: BlogPost): Promise&amp;lt;void&amp;gt; {
  const audioFilePath = join(AUDIO_DIR, `${post.id}.mp3`);

  if (existsSync(audioFilePath) &amp;amp;&amp;amp; !process.argv.includes(&apos;--force&apos;)) {
    console.log(`⏭️  Skipping ${post.id} - audio file already exists`);
    return;
  }

  try {
    console.log(`🎙️ Processing: ${post.data.title}`);

    const mdx = readFileSync(join(BLOG_DIR, post.id, &apos;index.mdx&apos;), &apos;utf-8&apos;);
    const summary = await summarize(
      mdx,
      {
        title: post.data.title,
        description: post.data.description,
        tags: post.data.tags,
      },
      { tone: &apos;engaging&apos; }
    );

    console.log(`🤖 AI Summary Result:`, {
      originalLength: mdx.length,
      summaryLength: summary.summaryLength,
      compressionRatio: Math.round(((mdx.length - summary.summaryLength) / mdx.length) * 100),
      summary: summary.summary,
    });

    const audio = await generate(summary.summary, { provider: &apos;murf&apos; });

    writeFileSync(audioFilePath, new Uint8Array(audio.audioBuffer));

    console.log(`✅ Generated: ${audioFilePath} (${(audio.size / 1024 / 1024).toFixed(2)} MB)`);

  } catch (error: any) {
    console.error(`❌ Error processing ${post.id}:`, error.message);
    if (process.argv.includes(&apos;--continue-on-error&apos;)) {
      console.log(&apos;⏩ Continuing with next post...&apos;);
      return;
    }
    throw error;
  }
}

async function main() {
  console.log(&apos;🎙️ Starting AI-powered voice generation for blog posts...&apos;);
  if (!existsSync(AUDIO_DIR)) {
    mkdirSync(AUDIO_DIR, { recursive: true });
    console.log(&apos;📁 Created audio directory:&apos;, AUDIO_DIR);
  }

  try {
    const posts = getContent();
    console.log(`📚 Found ${posts.length} blog posts`);
    for (const post of posts) {
      await generateAudioForPost(post);
    }
    console.log(&apos;✅ Voice generation completed successfully!&apos;);
  } catch (error) {
    console.error(&apos;❌ Error during voice generation:&apos;, error);
    process.exit(1);
  }
}

main().catch(console.error);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 - Add the voice summary to your blog post&lt;/h3&gt;
&lt;p&gt;Now that we have the voice file generated, we can add it to our blog post. A simple component to do this would be:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
interface Props {
  postId: string;
  title?: string;
}

const { postId } = Astro.props;
const audioUrl = `/audio/blog/${postId}.mp3`;
---

&amp;lt;div class=&quot;ring-input/80 my-6 rounded-lg p-3 ring&quot;&amp;gt;
  &amp;lt;div class=&quot;flex flex-col items-start justify-start gap-1&quot;&amp;gt;
    &amp;lt;div class=&quot;text-muted-foreground text-xs font-thin&quot;&amp;gt;Want a quick summary of this post? Tune in 🎧&amp;lt;/div&amp;gt;
    &amp;lt;audio controls preload=&quot;metadata&quot; class=&quot;h-8 w-full self-start&quot; src={audioUrl}&amp;gt;Your browser does not support the audio element.&amp;lt;/audio&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it! Now you also have a voice summary of your blog post, and you can add it to your blog post page.&lt;/p&gt;
&lt;p&gt;Here is how it looks like:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;4. Conclusion&lt;/h2&gt;
&lt;p&gt;This was a fun challenge to work on, and I hope you enjoyed it. If you have any questions, feel free to ask me on [[Twitter|https://x.com/nikuscs]].
Astro is indeed a great framework for blogging, and I&apos;m glad to see it&apos;s getting more traction!&lt;/p&gt;
&lt;h2&gt;5. Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Astro Container Reference|https://docs.astro.build/en/reference/container-reference/]]&lt;/li&gt;
&lt;li&gt;[[Astro RFC|https://github.com/withastro/roadmap/issues/533]]&lt;/li&gt;
&lt;li&gt;[[Turndown|https://github.com/domchristie/turndown]]&lt;/li&gt;
&lt;li&gt;[[Turndown Plugin GFM|https://github.com/laurent22/joplin/tree/dev/packages/turndown-plugin-gfm]]&lt;/li&gt;
&lt;li&gt;[[ElevenLabs|https://elevenlabs.io/]]&lt;/li&gt;
&lt;li&gt;[[Murf|https://murf.ai/]]&lt;/li&gt;
&lt;li&gt;[[Vercel SDK|https://vercel.com/docs/sdk/overview]]&lt;/li&gt;
&lt;li&gt;[[ai-sdk|https://ai-sdk.com/]]&lt;/li&gt;
&lt;li&gt;[[shadcn/ui|https://ui.shadcn.com/]]&lt;/li&gt;
&lt;li&gt;[[codetv.dev|https://codetv.dev/blog/mdx-to-rss-astro]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;How do I convert Astro MDX content to Markdown for LLM copy buttons?&quot;, answer: &quot;Use the experimental Astro Container API to render your MDX content to HTML outside the normal Astro runtime, passing custom components and renderers. Then use the Turndown library with the GFM plugin to convert the HTML back to clean Markdown, adding custom rules for inline links and fenced code blocks to preserve the original language annotations.&quot; },
{ question: &quot;How does the Astro Container API help with MDX-to-Markdown conversion?&quot;, answer: &quot;The Astro Container API (experimental_AstroContainer) lets you render content components outside the normal Astro context. You create a container with your astro config, add server renderers for React, Vue, and MDX, then call renderToString with your content component. This gives you the HTML output that Turndown can convert to Markdown.&quot; },
{ question: &quot;How do ElevenLabs and Murf compare for blog voice summaries?&quot;, answer: &quot;ElevenLabs produces higher quality voice output but uses a subscription model. Murf offers a pay-as-you-go model with features like a pronunciation dictionary for tech terms (e.g., mapping &apos;vue&apos; to &apos;view&apos;, &apos;nuxt&apos; to &apos;nuhkst&apos;). Both accept text input and return audio buffers. The choice depends on whether you prefer quality (ElevenLabs) or flexible pricing (Murf).&quot; },
{ question: &quot;How do I generate AI voice summaries for Astro blog posts?&quot;, answer: &quot;Create a build script that reads your MDX files, summarizes each post using OpenAI&apos;s GPT via the ai-sdk, then passes the summary to a TTS provider like ElevenLabs or Murf. Save the audio files to your public directory and add an HTML audio player component to your blog post template. Run the script before each deploy to generate audio for new posts.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>astro</category><category>markdown</category><category>openai</category><category>claude</category><category>button</category><author>Pedro Martins</author></item><item><title>Tanstack Start - A deep dive into THE React Framework</title><link>https://nikuscs.com/blog/06-tanstack-start-deep-dive/</link><guid isPermaLink="true">https://nikuscs.com/blog/06-tanstack-start-deep-dive/</guid><description>A deep dive into one of the most emerging React Frameworks in 2025, Tanstack Start, and how it can be used to build modern web applications.</description><pubDate>Mon, 14 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Introduction&lt;/h2&gt;
&lt;p&gt;React Vite starter is a great way to learn React, but it&apos;s no longer recommended for production use. When building real-world applications, you&apos;ll want a more robust framework that provides routing, state management, image optimization, SSR, and other features out of the box.&lt;/p&gt;
&lt;p&gt;The React ecosystem currently has three main frameworks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[[Next.js|https://nextjs.org]]&lt;/li&gt;
&lt;li&gt;[[React Router 7|https://reactrouter.com/]]&lt;/li&gt;
&lt;li&gt;[[TanStack Start|https://tanstack.com/start]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&apos;s explore TanStack Start and why it might be perfect for your next project. &lt;em&gt;Disclaimer: I&apos;m not affiliated with TanStack - just a fan of their work!&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;2. What is TanStack Start? 🔧&lt;/h2&gt;
&lt;p&gt;From their documentation: &quot;Full-stack React and Solid framework powered by TanStack Router SSR, Streaming, Server Functions, API Routes, bundling and more powered by TanStack Router and Vite.&quot;&lt;/p&gt;
&lt;p&gt;That&apos;s right! If you&apos;re in the React ecosystem, you probably know about TanStack Query, TanStack Table, TanStack Router, and recently TanStack Form. Most of these are built by the same team and by [[@tannerlinsley]] himself.&lt;/p&gt;
&lt;p&gt;TanStack Start aims to bring the &quot;server&quot; side of the equation with SSR, Streaming, Server Functions, API Routes, and many more features. You can finally build a full-stack application with a single framework. About 90% of TanStack Start is powered by TanStack Router, leaving just the server bits to the &quot;start&quot; implementation.&lt;/p&gt;
&lt;h2&gt;3. Why TanStack Start vs Next.js / React Router 7? ⚡&lt;/h2&gt;
&lt;p&gt;Tech choices are hard, and you need to choose the right tool for the job. Here are my reasons for picking TanStack Start over Next.js or React Router 7:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React Router 7&apos;s / Remix direction&lt;/strong&gt;: While React Router 7 powers a big part of the ecosystem alongside Next.js, and promisses to keep it up to date are there, Remix ( previously known as React Router ) seems to be stepping back from &quot;React&quot; - read &lt;a href=&quot;https://x.com/remix_run/status/1927729823805255925&quot;&gt;this post&lt;/a&gt; for more info. This makes me unconfortable with so many decisions / rebrands taking place in the past few years.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation clarity&lt;/strong&gt;: React Router 7 lacks proper documentation and examples. It feels complex to understand and implement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RSC complexity&lt;/strong&gt;: Next.js placed their bets on RSC and a new React paradigm. While RSC is great, it adds extra complexity to how we build React applications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance trade-offs&lt;/strong&gt;: Next.js server-side rendering feels slower compared to traditional SPAs and needs more powerful machines to run (we&apos;re pushing workload to the server instead of the client).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vite ecosystem&lt;/strong&gt;: This is personal preference, but the [[Vite|https://vite.dev]] ecosystem is huge, fast, and has better DX compared to Webpack/Turbopack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type safety&lt;/strong&gt;: TanStack Router is simply the best type-safe router for React - query params, context, everything is fully typed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. Core Architecture &amp;amp; Philosophy&lt;/h2&gt;
&lt;p&gt;TanStack Start represents a paradigm shift in how we think about full-stack React applications. While frameworks like Next.js have embraced a &quot;server-first&quot; approach with React Server Components, TanStack Start takes a fundamentally different path: &lt;strong&gt;client-first architecture with powerful server capabilities&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;4.1 Client-First vs Server-First Philosophy&lt;/h3&gt;
&lt;p&gt;The key differentiator lies in TanStack Start&apos;s philosophy:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;While other frameworks continue to compromise on the client-side application experience we&apos;ve cultivated as a front-end community over the years, TanStack Start stays true to the client-side first developer experience, while providing a full-featured server-side capable system that won&apos;t make you compromise on user experience.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Initial navigation&lt;/strong&gt;: Server-side rendering for fast initial page loads and SEO&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subsequent navigation&lt;/strong&gt;: Client-side routing for instant, SPA-like experiences&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data loading&lt;/strong&gt;: Isomorphic loaders that run on server during SSR, client during navigation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interactivity&lt;/strong&gt;: Rich client-side interactions without server-side compromises&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 Isomorphic Loaders: The Game Changer 🎯&lt;/h3&gt;
&lt;p&gt;TanStack Start&apos;s most innovative feature is its &lt;strong&gt;isomorphic loaders&lt;/strong&gt;. Unlike traditional meta-frameworks where server loaders and client state exist in separate worlds, TanStack Start bridges this gap elegantly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const Route = createFileRoute(&apos;/posts&apos;)({
  component: Page,
  validateSearch: zodValidator(PostSearchParamsSchema),
  loaderDeps: ({ search }) =&amp;gt; ({ search }),
  search: {
    middlewares: [stripSearchParams(PostSearchParamsDefaultValues)],
  },
  loader: async ({ deps, context }) =&amp;gt; context.queryClient.ensureQueryData(postsQueryOptions(deps.search)),
  head: () =&amp;gt; ({
    meta: createMetaPage({
      title: &apos;Posts&apos;,
      description: &apos;Your personal post collection&apos;,
    }),
  }),
})

/**
 * Page
 */
function Page() {
  const results = Route.useLoaderData()
  return (
    &amp;lt;div className=&quot;flex flex-col gap-4&quot;&amp;gt;
      &amp;lt;div className=&quot;grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4&quot;&amp;gt;
        {results.records.map((post) =&amp;gt; &amp;lt;PostCard post={post} key={post.id} /&amp;gt;)}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This solves the &quot;impedance mismatch&quot; problem where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server loaders blindly re-fetch all data on navigation ( can pair with TanStack Query )&lt;/li&gt;
&lt;li&gt;Client state gets ignored by server-side logic&lt;/li&gt;
&lt;li&gt;You end up requesting data you already have cached&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 Server Functions: Not Your Typical RPCs&lt;/h3&gt;
&lt;p&gt;TanStack Start introduces &lt;code&gt;createServerFn&lt;/code&gt; - a unique approach to server-side logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const likePost = createServerFn({ method: &apos;POST&apos; })
  .middleware([authedMiddleware])
  .validator(LikePostSchema)
  .handler(async ({ data }) =&amp;gt; {
    if (!data.postId) {
      throw new Error(&apos;Post is not valid or doesnt exist anymore&apos;)
    }

    const headers = getHeaders()
    await auth.api.likePost({
      headers: headers as HeadersInit,
      body: {
        postId: data.postId,
      },
    })

    return { message: &apos;Post liked successfully&apos; }
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Call from anywhere&lt;/strong&gt;: Components, hooks, loaders - full flexibility&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type-safe end-to-end&lt;/strong&gt;: Input validation with inference&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No API layer needed&lt;/strong&gt;: Direct function calls that proxy to server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic serialization&lt;/strong&gt;: Complex data types handled seamlessly&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4 TanStack Query Integration&lt;/h3&gt;
&lt;p&gt;Unlike other frameworks, TanStack Start seamlessly integrates with TanStack Query for sophisticated caching:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const postsQuery = useQuery({
  queryKey: [&apos;posts&apos;],
  queryFn: () =&amp;gt; getPostsServerFn()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This enables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Smart caching&lt;/strong&gt;: Only fetch what&apos;s not already cached&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background updates&lt;/strong&gt;: Stale-while-revalidate patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimistic updates&lt;/strong&gt;: Immediate UI feedback&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared state&lt;/strong&gt;: Between server and client seamlessly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For more info, check out the [[TanStack Query documentation|https://tanstack.com/query/latest/docs/framework/react/overview]].&lt;/p&gt;
&lt;h3&gt;4.5 Type Safety Throughout&lt;/h3&gt;
&lt;p&gt;TanStack Start builds on TanStack Router&apos;s foundation of &lt;strong&gt;100% inferred TypeScript&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Routes&lt;/strong&gt;: Fully typed path parameters and search params&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Navigation&lt;/strong&gt;: Type-safe links with autocomplete&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server functions&lt;/strong&gt;: Input/output type inference&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data flow&lt;/strong&gt;: From server to client with full type preservation&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.6 The Architecture in Practice&lt;/h3&gt;
&lt;p&gt;Here&apos;s how it all comes together:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Initial Load&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server renders page with data from loaders&lt;/li&gt;
&lt;li&gt;Client receives HTML + serialized data&lt;/li&gt;
&lt;li&gt;Hydration with full state restoration&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Client Navigation&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Router intercepts navigation&lt;/li&gt;
&lt;li&gt;Loaders run on client with cache awareness&lt;/li&gt;
&lt;li&gt;Only missing data gets fetched&lt;/li&gt;
&lt;li&gt;Instant page updates&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Server Interactions&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server functions called like regular functions&lt;/li&gt;
&lt;li&gt;Automatic proxy to server via fetch&lt;/li&gt;
&lt;li&gt;Type safety maintained throughout&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This architecture enables the &lt;strong&gt;best of both worlds&lt;/strong&gt;: fast initial loads like traditional SSR with rich client-side experiences like SPAs, without the typical compromises or complexity.&lt;/p&gt;
&lt;h2&gt;5. Routing: File-Based vs Virtual Routes 🛣️&lt;/h2&gt;
&lt;p&gt;Just like Next.js, you can define file-based routes - check the full documentation &lt;a href=&quot;https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But the real power comes with virtual routes - this is where the DX starts to shine! Even better, you can mix and match file-based routes with virtual routes.&lt;/p&gt;
&lt;p&gt;Here&apos;s an example of a virtual route:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { rootRoute, route, index } from &apos;@tanstack/virtual-file-routes&apos;

export const routes = rootRoute(&apos;root.tsx&apos;, [
  index(&apos;landing/index.tsx&apos;),
  // Private pages
  route(&apos;/dashboard&apos;, &apos;dashboard/layout.tsx&apos;, [
    //  Bookmarks
    route(&apos;/posts&apos;, &apos;dashboard/posts/layout.tsx&apos;, [
      index(&apos;dashboard/posts/index.tsx&apos;),
      route(&apos;/create&apos;, &apos;dashboard/posts/create.tsx&apos;),
      route(&apos;/$id&apos;, &apos;dashboard/posts/view.tsx&apos;),
    ]),
    // User Area
    route(&apos;/user&apos;, [
      // Teams
      route(&apos;/teams&apos;, [
        route(&apos;$slug&apos;, [route(&apos;/switch&apos;, &apos;dashboard/user/switch-team.tsx&apos;)]),
      ]),
      route(&apos;/logout&apos;, &apos;dashboard/user/logout.tsx&apos;),
    ]),
  ]),
  // Public pages
  route(&apos;/auth&apos;, &apos;auth/layout.tsx&apos;, [
    route(&apos;/sign-in&apos;, &apos;auth/sign-in.tsx&apos;),
  ]),
  // Api
  route(&apos;/api&apos;, [
    // BetterAuth
    route(&apos;/auth&apos;, [route(&apos;$&apos;, &apos;api/auth.ts&apos;)]),
  ]),
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. Middleware 🔧&lt;/h2&gt;
&lt;p&gt;You probably know what middlewares are. While they come &quot;for free&quot; in other frameworks like [[Laravel|https://laravel.com]] and [[Nuxt|https://nuxt.com]], they&apos;re not always easy to implement in React.&lt;/p&gt;
&lt;p&gt;Thankfully, TanStack Start has a very nice way to define middleware for server functions and routes. Let&apos;s explore them.&lt;/p&gt;
&lt;h3&gt;6.1 Server Functions &amp;amp; Request Middleware 🔒&lt;/h3&gt;
&lt;p&gt;This ensures that the server function is only called if the middleware passes or doesn&apos;t throw an exception. Here&apos;s a demo example for better auth:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function authMiddlewareHandler(request: Request | null | undefined): Promise&amp;lt;AuthMaybeSessionContext&amp;gt; {

  const context: AuthMaybeSessionContext = {
    isAuthenticated: false,
    session: null,
    user: null,
    organization: null,
    organizations: [],
  }

  if (!request) {
    return context
  }

  const authSession = await auth.api.getSession({
    headers: request.headers,
  })

  // Add authenticated data if session exists
  if (authSession?.session &amp;amp;&amp;amp; authSession.user) {
    Object.assign(context, {
      isAuthenticated: true,
      session: authSession.session as unknown as Session,
      user: authSession.user as unknown as User,
      organization: organization as Organization,
      organizations: organizations as Organization[],
    })
  }

  return context
}

/**
 * This middleware is used to authenticate the user.
 * It sets the session in the context.
 */
export const authMiddleware = createMiddleware({ type: &apos;function&apos; })
  .server(async ({ next }) =&amp;gt; {
    const request = getWebRequest()
    const context = await authMiddlewareHandler(request)
    return next({
      context,
    })
  })

/**
 * This middleware is used to authenticate the user.
 * It sets the session in the context.
 */
export const authWebMiddleware = createMiddleware({ type: &apos;request&apos; })
  .server(async ({ next }) =&amp;gt; {
    const request = getWebRequest()
    const context = await authMiddlewareHandler(request)
    return next({
      context,
    })
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.2 Router &quot;Middleware&quot; 🚦&lt;/h3&gt;
&lt;p&gt;While TanStack Router doesn&apos;t have the concept of middleware, users often use the &lt;code&gt;beforeLoad&lt;/code&gt; hook to achieve the same effect.&lt;/p&gt;
&lt;p&gt;Here are examples of how to pass context from &quot;root.tsx&quot; into a gated route:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// root.tsx
export const Route = createRootRouteWithContext&amp;lt;RouterContext&amp;gt;()({
  beforeLoad: async ({ context }) =&amp;gt; {
    try {
      const authContext = await context.queryClient.ensureQueryData(authQueryOptions())
      return {
        ...authContext,
      }
    } catch (error) {
      logger.error(&apos;Unable to load the auth context&apos;, error)
      return {}
    }
  },
})

// dashboard/layout.tsx
export const Route = createFileRoute(&apos;/dashboard/_layout&apos;)({
  component: Layout,
  beforeLoad: async ({ context }) =&amp;gt; {
    const { isAuthenticated } = context
    if (!isAuthenticated || !context.session || !context.user || !context.organization || !context.organizations) {
      throw redirect({
        to: &apos;/auth/sign-in&apos;
      })
    }
    return AuthSessionContextSchema.parse(context)
  }
})

// Server route middleware
export const ServerRoute = createServerFileRoute().methods((api) =&amp;gt; ({
  GET: api
    .middleware([authMiddleware])
    .handler(async ({ request }) =&amp;gt; {
      return new Response(&apos;Hello, World! from &apos; + request.url)
    }),
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wait, what&apos;s happening here? Let&apos;s break it down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We use &lt;code&gt;beforeLoad&lt;/code&gt; on the &lt;code&gt;root.tsx&lt;/code&gt; route to load the auth context (whether the user is logged in or not)&lt;/li&gt;
&lt;li&gt;The QueryClient fetches the auth context from &lt;strong&gt;server side&lt;/strong&gt; and persists it in the query cache&lt;/li&gt;
&lt;li&gt;The context is then passed to child routes, in this case the &lt;code&gt;dashboard/layout.tsx&lt;/code&gt; route&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;dashboard/layout.tsx&lt;/code&gt; route uses the &lt;code&gt;beforeLoad&lt;/code&gt; hook to check if the user is authenticated - if not, we redirect to the login page&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;AuthSessionContextSchema&lt;/code&gt; is a Zod schema that validates the context and ensures it&apos;s valid &amp;amp; fully typed&lt;/li&gt;
&lt;li&gt;You can repeat this process for any route you want (even nested ones)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7. Server &amp;amp; Client Boundaries 🔐&lt;/h2&gt;
&lt;p&gt;This is a crucial concept to understand and a major security concern for full-stack applications in the JavaScript ecosystem. Blurring the lines between server &amp;amp; client provides good DX, but it also opens up security concerns if you don&apos;t know what you&apos;re doing.&lt;/p&gt;
&lt;p&gt;A missed server import into the client can lead to your server application logic or even environment variables being exposed to the client!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next.js&lt;/strong&gt; handles this elegantly with the &lt;code&gt;use &apos;client&apos;&lt;/code&gt; directive at the top of files - anything not tagged with &lt;code&gt;client&lt;/code&gt; is treated as server code, ensuring no server code executes on the client.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;React Router 7 &amp;amp; SvelteKit&lt;/strong&gt; use file naming conventions to determine if a file is server or client: &lt;code&gt;post.server.tsx&lt;/code&gt; or &lt;code&gt;post.client.tsx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;While TanStack Start doesn&apos;t YET have a built-in way to handle this, you can use the Vite Plugin [[vite-env-only|https://github.com/pcattori/vite-env-only]] to ensure that files and directories matching your patterns don&apos;t get imported into the client bundle. Here&apos;s an example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &apos;vite&apos;
import { denyImports } from &apos;vite-env-only&apos;

export default defineConfig({
  plugins: [
    // .. other plugins,
    denyImports({
        client: {
          specifiers: [
            &quot;fs&quot;, /^node:/, &quot;drizzle-orm&quot;, &quot;postgres&quot;,
            &quot;node-ray&quot;, &quot;Buffer&quot;, &quot;process&quot;, &quot;path&quot;, &quot;os&quot;,
            &quot;url&quot;, &quot;crypto&quot;, &quot;stream&quot;, &quot;net&quot;,
          ],
          files: [
            &quot;**/.server/*&quot;,
            &quot;**/*.server.*&quot;,
            &quot;src/console/*&quot;,
            &quot;src/services/*&quot;,
            &quot;src/console/*&quot;,
            &quot;src/jobs/*&quot;
          ],
        },
      }),
  ]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. Deployment 🚀&lt;/h2&gt;
&lt;p&gt;TanStack being based on [[Vite|https://vite.dev]], [[h3|https://h3.dev]] &amp;amp; [[Nitro|https://nitro.unjs.io]] (at least for now), makes it a perfect fit for deployment on almost any platform!&lt;/p&gt;
&lt;p&gt;All you have to do is toggle your &lt;code&gt;target&lt;/code&gt; in your &lt;code&gt;vite.config.ts&lt;/code&gt; file, and you&apos;re good to go!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.ts
import { tanstackStart } from &apos;@tanstack/react-start/plugin/vite&apos;
import { defineConfig } from &apos;vite&apos;

// Targets can be : cloudflare-module, vercel, netlify, bun, node, etc.
export default defineConfig({
  plugins: [tanstackStart({ target: &apos;vercel&apos; })],
  // ... other vite config
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don&apos;t forget to check the documentation - sometimes you need to tweak the config to make it work on your platform of choice.&lt;/p&gt;
&lt;h2&gt;9. Conclusion ✨&lt;/h2&gt;
&lt;p&gt;TanStack Start is a powerful meta-framework that&apos;s changing the game in the React ecosystem. If you haven&apos;t tried it yet, you should - grab some coffee and start building!&lt;/p&gt;
&lt;p&gt;You can join their &lt;a href=&quot;https://discord.com/invite/WrRKjPJ&quot;&gt;TanStack Discord&lt;/a&gt; to get help, ask questions, and get updates on the framework.&lt;/p&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;What is TanStack Start and how does it differ from Next.js?&quot;, answer: &quot;TanStack Start is a full-stack React framework built on TanStack Router, Vite, and Nitro. Unlike Next.js which uses a server-first approach with React Server Components, TanStack Start takes a client-first architecture: SSR for initial loads, then client-side routing for SPA-like navigation. It avoids RSC complexity while offering server functions, streaming, and full type safety.&quot; },
{ question: &quot;How do TanStack Start&apos;s isomorphic loaders work with TanStack Query?&quot;, answer: &quot;TanStack Start&apos;s loaders run on the server during SSR and on the client during navigation. By integrating with TanStack Query via ensureQueryData, loaders only fetch data that is not already cached. This enables stale-while-revalidate patterns, optimistic updates, and smart caching — solving the problem of frameworks blindly re-fetching all data on every navigation.&quot; },
{ question: &quot;How does TanStack Router handle middleware and authentication?&quot;, answer: &quot;TanStack Router uses beforeLoad hooks as route-level middleware. You can load auth context in root.tsx, pass it down via route context, and check authentication in child routes like dashboard layouts. For server functions, TanStack Start provides createMiddleware with type-safe context passing. Both approaches support Zod schema validation for the context.&quot; },
{ question: &quot;How does TanStack Start handle deployment across different platforms?&quot;, answer: &quot;TanStack Start uses Vite and Nitro under the hood, allowing deployment to multiple platforms by changing the target in vite.config.ts. Supported targets include Vercel, Cloudflare, Netlify, Bun, and Node.js. This avoids the vendor lock-in that exists with Next.js and Vercel.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>react</category><category>tanstack</category><category>tanstack-start</category><category>framework</category><category>react-query</category><category>react-router</category><author>Pedro Martins</author></item><item><title>Next vs Nuxt, which one is better in 2025?</title><link>https://nikuscs.com/blog/03-nuxt-vs-next-2024/</link><guid isPermaLink="true">https://nikuscs.com/blog/03-nuxt-vs-next-2024/</guid><description>Recently I deep-dived into Nuxt and Next.js, and I wanted to share my thoughts on the top 2 meta-frameworks for Vue and React, and how they compare to each other.</description><pubDate>Thu, 16 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;With more than 15 years of experience in web development, I&apos;ve seen many frameworks come and go, but [[Nuxt|https://nuxt.com]] and [[Next.js|https://nextjs.org]] are here to stay. In this article, I&apos;ll compare the two frameworks and share my thoughts on which one is better in 2025.&lt;/p&gt;
&lt;h2&gt;React vs Vue&lt;/h2&gt;
&lt;p&gt;The first choice (which I won&apos;t cover in this post) is picking between React or Vue. Both are great frameworks, but I&apos;ll assume you&apos;ve already chosen one for your project.&lt;/p&gt;
&lt;p&gt;At the time of this post, both have great communities and are very popular, so you can&apos;t go wrong with either - React being more popular and Vue being more beginner-friendly.&lt;/p&gt;
&lt;p&gt;In this article, I won&apos;t cover basic features since both frameworks support many things. I&apos;ll focus on the high-level features instead.&lt;/p&gt;
&lt;h2&gt;1. Next.js 🚀&lt;/h2&gt;
&lt;p&gt;I recently tried Next.js for a personal project, and while I&apos;m biased towards Vue, I have to say that Next.js is a great framework with many features and a great community.&lt;/p&gt;
&lt;p&gt;But not everything is perfect. I tried hard to ignore all the DX that Vue gives me to embrace the Next.js + React hype train. I researched medium to large Next.js projects on [[GitHub|https://github.com]] to learn more about the framework.&lt;/p&gt;
&lt;p&gt;I read the documentation, learned the best practices, and watched tutorials &amp;amp; videos to get familiar with the framework, but that wasn&apos;t enough. Coming from [[Laravel|https://laravel.com]], we&apos;re REALLY spoiled with the DX that Vue or Laravel gives us.&lt;/p&gt;
&lt;p&gt;Let&apos;s go through the pros and cons of Next.js:&lt;/p&gt;
&lt;h3&gt;1.1 Pros ✅&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Great, large &amp;amp; active community&lt;/li&gt;
&lt;li&gt;Solid documentation&lt;/li&gt;
&lt;li&gt;Out-of-the-box TypeScript support&lt;/li&gt;
&lt;li&gt;SSR, SSG, ISR capabilities&lt;/li&gt;
&lt;li&gt;Great concept for Server &amp;amp; Client Components - really loved this feature&lt;/li&gt;
&lt;li&gt;Huge React ecosystem that Next.js inherits&lt;/li&gt;
&lt;li&gt;Pages routing is simple and easy to understand&lt;/li&gt;
&lt;li&gt;Minimalistic/bare bones - you can add only what you need&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 Cons ❌&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Vendor lock-in - no matter what you say, Vercel is the way to go for Next.js&lt;/li&gt;
&lt;li&gt;Webpack 😞 - really slow on dev &amp;amp; build, most of the time compared to a Nuxt project&lt;/li&gt;
&lt;li&gt;Middleware - no concept of multiple middlewares, you have to use a single middleware for all routes or implement your own middleware stack&lt;/li&gt;
&lt;li&gt;i18n - while there are a few &quot;demos&quot;, none are actual real-world examples, and the documentation is lacking. External packages exist but they&apos;re HEAVILY complicated to implement in my opinion&lt;/li&gt;
&lt;li&gt;Server &amp;amp; Client components - while a great concept, it&apos;s really hard to understand and implement, you end up prop drilling a lot of data&lt;/li&gt;
&lt;li&gt;Server &amp;amp; Client lines blurred - you have to be really careful with what you&apos;re doing, and you need to know what&apos;s running on the server vs client. A little mistake could be a big problem&lt;/li&gt;
&lt;li&gt;Node.js support for Middleware - this is a BIG one. While Next.js advocates that Middleware should not run Node.js but their &quot;Edge&quot; server, there are many cases where you need to run Node.js code in Middleware, and this isn&apos;t supported by Next.js at the time of this post. This is REALLY necessary for basic stuff like auth checks against databases&lt;/li&gt;
&lt;li&gt;Dev Experience - while the DX isn&apos;t bad, it&apos;s not as good as Nuxt. There&apos;s no proper way to debug your code. React DevTools with Next.js is a HUGE pile of nested contexts&lt;/li&gt;
&lt;li&gt;Unstable - it seems like there are many breaking changes on every release, and the community isn&apos;t happy about it. There are many issues on GitHub about this&lt;/li&gt;
&lt;li&gt;Lack of documentation on how to deploy other than Vercel&lt;/li&gt;
&lt;li&gt;Somewhat slow - while SSR is good, it seems slower than traditional SPAs at first load&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. Nuxt 💚&lt;/h2&gt;
&lt;p&gt;After my mission with Next.js, I decided to replicate the same project, but this time using Nuxt. I had most of my components already done with Shadcn, and I could easily transfer a lot of code-base from Next.js.&lt;/p&gt;
&lt;p&gt;Nuxt instantly provides a better DX than Next.js. Can&apos;t deny - after a few weeks of React, I was really happy to be back on Vue!&lt;/p&gt;
&lt;p&gt;With my struggles from Next.js in mind, here&apos;s a breakdown for Nuxt:&lt;/p&gt;
&lt;h3&gt;2.1 Pros ✅&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Great, large &amp;amp; active community - Vue is growing a lot, and Nuxt is growing with it&lt;/li&gt;
&lt;li&gt;Lots of composables out-of-the-box, like &lt;code&gt;useFetch&lt;/code&gt;, &lt;code&gt;useMeta&lt;/code&gt;, &lt;code&gt;useHead&lt;/code&gt;, &lt;code&gt;useState&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Great documentation - really easy to understand and follow&lt;/li&gt;
&lt;li&gt;Amazing plugin &amp;amp; modules system - you can add many features with a single line of code&lt;/li&gt;
&lt;li&gt;Out-of-the-box support for stacked middlewares - attach them &amp;amp; use them&lt;/li&gt;
&lt;li&gt;Nuxt i18n just works - with a few clicks you can have a multi-language website, no stress!&lt;/li&gt;
&lt;li&gt;Top-notch DX - install modules directly from the Nuxt DevTools, check OpenGraph images, dependencies, preview builds... just amazing!&lt;/li&gt;
&lt;li&gt;Great hooks for deduping &amp;amp; caching around [[ofetch|https://github.com/unjs/ofetch]]&lt;/li&gt;
&lt;li&gt;Amazing universal support from [[UnJS|https://unjs.io]] ecosystem&lt;/li&gt;
&lt;li&gt;No vendor lock-in - you can deploy to Vercel, Cloudflare, etc.&lt;/li&gt;
&lt;li&gt;Middleware supports Node.js &amp;amp; edge runtimes&lt;/li&gt;
&lt;li&gt;Vue reactivity system &amp;amp; bindings are just great!&lt;/li&gt;
&lt;li&gt;Support for &quot;Islands&quot; - Server &amp;amp; Client components are really easy to understand and implement&lt;/li&gt;
&lt;li&gt;Configuration in one place - no need to search for many files, everything is in &lt;code&gt;nuxt.config.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Easier to implement auth modules&lt;/li&gt;
&lt;li&gt;Auto-imports for components, composables, etc. - just write your business logic, no need to import stuff&lt;/li&gt;
&lt;li&gt;Vite bundler - it&apos;s really fast, and the DX is just amazing + the ecosystem&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 Cons ❌&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Confusing sometimes - because there are many hooks, composables, etc., sometimes you don&apos;t know what to use or what&apos;s the correct one for a specific case&lt;/li&gt;
&lt;li&gt;Documentation is still lacking some &quot;best practices&quot; for some features&lt;/li&gt;
&lt;li&gt;Unstable sometimes - while auto-imports are a good DX, they also create buggy code, and it&apos;s really hard to find the source of the bug sometimes&lt;/li&gt;
&lt;li&gt;Server Components are not yet polished - you can&apos;t mix and match client &amp;amp; server components like Next.js (yet)&lt;/li&gt;
&lt;li&gt;Lack of methods to inject configurations from user-land &amp;amp; modules (still better than Next.js, which has none)&lt;/li&gt;
&lt;li&gt;Unclear what composables &amp;amp; features you can use on server &amp;amp; client, leading again to unexpected bugs &amp;amp; hard to debug issues&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Winner 🏆&lt;/h2&gt;
&lt;p&gt;While both frameworks are great, the winner for me is clearly Nuxt. The DX is just amazing, and the features are great compared to Next.js.&lt;/p&gt;
&lt;p&gt;I could probably do the same project 10x faster with Nuxt than with Next.js. One of the biggest concerns for me is also the vendor lock-in for Vercel. While many people try to deny and argue against that, for me it&apos;s crystal clear that Next.js doesn&apos;t have/doesn&apos;t want to make these changes, resulting in a lot of drama on Twitter &amp;amp; Reddit.&lt;/p&gt;
&lt;p&gt;On the other hand, React has a huge ecosystem, and if you&apos;re already using React, Next.js is a great choice. You may also consider [[Remix|https://remix.run]] or [[TanStack Start|https://tanstack.com/start]] as alternatives.&lt;/p&gt;
&lt;h2&gt;Conclusion 🎯&lt;/h2&gt;
&lt;p&gt;In the past years, many meta-frameworks appeared and some of them are here to stay. For me, there&apos;s no perfect one yet. Nuxt has good potential to be the best one, but there&apos;s still a lot of work to be done, not only in Nuxt but also in the [[Vue|https://vuejs.org]] ecosystem.&lt;/p&gt;
&lt;p&gt;I&apos;m still a big fan of starting my own projects with vanilla Vue or [[React|https://react.dev]], and calling it a day - bring your own plugins, your own features, and your own DX. But if you&apos;re in a hurry or want a quick MVP, both are great choices.&lt;/p&gt;
&lt;p&gt;One big concern I also have with full-stack JavaScript frameworks is that while they provide a really flexible way to build applications with a single language &amp;amp; code-base, they can also create the inverse effect.&lt;/p&gt;
&lt;p&gt;For instance, including or importing a server module in a client module could potentially leak important data like API keys, database connections, so &lt;strong&gt;ALWAYS&lt;/strong&gt; make sure you double-check your code before deploying it to production.&lt;/p&gt;
&lt;p&gt;For me, [[Rails|https://rubyonrails.org]]/Laravel + Vue/React is still the best way to go, but if you&apos;re looking for a quick MVP or a small project, why not give it a go?&lt;/p&gt;
&lt;p&gt;Stay safe, and happy coding!&lt;/p&gt;
&lt;h2&gt;Links 🔗&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Nuxt|https://nuxt.com/]]&lt;/li&gt;
&lt;li&gt;[[Next.js|https://nextjs.org/]]&lt;/li&gt;
&lt;li&gt;[[Remix|https://remix.run/]]&lt;/li&gt;
&lt;li&gt;[[TanStack Start|https://tanstack.com/start]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;What are the key differences between Nuxt and Next.js in 2025?&quot;, answer: &quot;Nuxt is built on Vue with Vite for faster dev builds, auto-imports, built-in modules, and stacked middleware support. Next.js is built on React with Webpack/Turbopack, a larger ecosystem, and React Server Components. Nuxt has no vendor lock-in, while Next.js is tightly coupled with Vercel for optimal deployment.&quot; },
{ question: &quot;How does Nuxt handle i18n and middleware compared to Next.js?&quot;, answer: &quot;Nuxt offers built-in stacked middleware and Nuxt i18n that works with minimal configuration. Next.js uses a single middleware file for all routes and lacks proper built-in i18n — external packages exist but are complex to implement. This is a significant DX difference for multilingual applications.&quot; },
{ question: &quot;What alternatives to Nuxt and Next.js exist for React developers?&quot;, answer: &quot;React developers can consider Remix (React Router 7) for a more traditional approach, or TanStack Start for a Vite-based full-stack framework with excellent type safety. Both offer different trade-offs compared to Next.js, including no vendor lock-in and simpler mental models.&quot; },
{ question: &quot;How does Nuxt&apos;s Vite-based tooling compare to Next.js&apos;s Webpack/Turbopack?&quot;, answer: &quot;Nuxt uses Vite, which provides significantly faster development builds and hot module replacement. Next.js relies on Webpack with Turbopack as an emerging alternative. The Vite ecosystem also benefits from the UnJS library suite. Build speed differences are most noticeable during development, while production performance is comparable.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>nextjs</category><category>nuxt</category><category>vue</category><category>react</category><category>meta-framework</category><author>Pedro Martins</author></item><item><title>Unraid, the perfect NAS, Streaming Server &amp; Netflix-like service for your home &amp; office</title><link>https://nikuscs.com/blog/02-unraid-streaming-server/</link><guid isPermaLink="true">https://nikuscs.com/blog/02-unraid-streaming-server/</guid><description>In this article, we will explore a little bit about Unraid, what it is, how it works, and how you can use it to create your own Netflix like service, NAS or file system for your home or office.</description><pubDate>Sun, 05 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Callout from &quot;@components/Callout.astro&quot;;
import Button from &quot;@components/Button.astro&quot;;&lt;/p&gt;
&lt;p&gt;If you arrived here, you probably enjoy having a home server, NAS, or homelab. You might have heard about Unraid and are curious about what it is, how it works, and how you can use it to create your own Netflix-like service, NAS, or file system for your home or office.&lt;/p&gt;
&lt;h2&gt;Contents&lt;/h2&gt;
&lt;h2&gt;1 - Why Unraid?&lt;/h2&gt;
&lt;p&gt;While Unraid is a pure OS that doesn&apos;t dictate specific hardware requirements, you might have heard of Synology, QNAP, or FreeNAS. While these are great solutions, they&apos;re not as flexible as Unraid and are often more expensive with hardware restrictions and slower performance.&lt;/p&gt;
&lt;p&gt;If you&apos;re looking for a more flexible, faster, and cost-effective solution, Unraid is the way to go! Here are some advantages of Unraid:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;: Run Unraid on almost any hardware and expand as needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Create fast NAS or streaming servers with excellent performance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Affordable licensing starting at $60, compatible with most hardware&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Community&lt;/strong&gt;: Active community with extensive help and support resources&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-intrusive&lt;/strong&gt;: Runs from USB without modifying your drives, allowing portability to other systems&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugins&lt;/strong&gt;: Extensive plugin ecosystem for customization and expansion&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;: Built-in Docker support for running containerized applications&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2 - Hardware Requirements&lt;/h2&gt;
&lt;p&gt;Unraid is based on Linux and runs on almost any hardware. Here are the recommended hardware requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU&lt;/strong&gt;: 64-bit processor running 1 GHz or higher&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAM&lt;/strong&gt;: While not RAM-intensive, 4GB minimum is recommended&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: Flexible storage options; minimum 1TB HDD recommended for streaming&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSD&lt;/strong&gt;: At least one SSD for cache and scratch disk operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network&lt;/strong&gt;: 1Gbps connection recommended for streaming&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Graphics&lt;/strong&gt;: Optional for basic use; 4K-capable GPU recommended for media server transcoding&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3 - The Basic Setup&lt;/h2&gt;
&lt;p&gt;After installing and setting up the OS, configure your shares, users, and permissions. Here are the key configurations I&apos;ve set up (detailed tutorials are available online):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shares&lt;/strong&gt;: Separate shares for Movies, TV Shows, Music, and Photos&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Users&lt;/strong&gt;: Individual user accounts for family members with appropriate access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permissions&lt;/strong&gt;: Share-specific permissions controlling user access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Essential Plugins&lt;/strong&gt;: &lt;a href=&quot;compose-manager&quot;&gt;&lt;code&gt;Compose.Manager&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;nerdtools&quot;&gt;&lt;code&gt;NerdTools&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;gpu-stats&quot;&gt;&lt;code&gt;GPU Stats&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;nvidia-driver&quot;&gt;&lt;code&gt;Nvidia Driver&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;unassigned-devices&quot;&gt;&lt;code&gt;Unassigned Devices&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;user-scripts&quot;&gt;&lt;code&gt;User Scripts&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System Settings&lt;/strong&gt;: NFS for macOS compatibility, UPS configuration, energy monitoring, notifications, Docker, VMs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unraid Connect&lt;/strong&gt;: Remote server management and notifications&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4 - Docker &amp;amp; Containers&lt;/h2&gt;
&lt;p&gt;One of the greatest advantages of a Docker-centric OS is running containerized applications that can be wiped without affecting the host OS. This allows complete setup recreation without data loss.&lt;/p&gt;
&lt;p&gt;Here&apos;s my Docker configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Configuration Storage&lt;/strong&gt;: Store all app configurations in a backed-up folder (e.g., &lt;code&gt;/mnt/disk3/docker_data&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker Image Size&lt;/strong&gt;: Set appropriate docker.img file size (I use 100GB)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage Location&lt;/strong&gt;: Store docker.img on fast SSD storage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Management Interface&lt;/strong&gt;: Use Unraid&apos;s native UI instead of Portainer for better integration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service Organization&lt;/strong&gt;: Group services by responsibility (media server, dev tools, etc.) in separate docker-compose files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Port Management&lt;/strong&gt;: Organize ports to avoid conflicts; use reverse proxy when needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network Organization&lt;/strong&gt;: Maintain clean networks for container communication and internal DNS&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5 - External Access&lt;/h2&gt;
&lt;p&gt;With your server configured, you&apos;ll want external access to run it headlessly at home. While &lt;strong&gt;Unraid Connect&lt;/strong&gt; provides server management, it doesn&apos;t grant access to your services.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Verify your internet provider allows hosting services. Some providers block ports, requiring VPN or VPS solutions.&lt;/p&gt;
&lt;p&gt;Obtain your &lt;strong&gt;public IP&lt;/strong&gt; and ensure you have either a static IP or dynamic DNS setup for domain-based access.&lt;/p&gt;
&lt;h3&gt;5.1 - Domain &amp;amp; Cloudflare&lt;/h3&gt;
&lt;p&gt;I recommend purchasing a memorable domain for easy access from your devices. I use [[Cloudflare]] for DNS and domain management due to these benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free Service&lt;/strong&gt;: No cost for DNS management with unlimited domains&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security Features&lt;/strong&gt;: DDoS protection, WAF, and other security tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global Performance&lt;/strong&gt;: Worldwide data centers for faster service delivery&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Powerful API&lt;/strong&gt;: Automation capabilities for domain management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic Updates&lt;/strong&gt;: Automatic subdomain creation and IP address updates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Setup process: Purchase domain → Configure Cloudflare DNS → Point domain to your public IP.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: This assumes basic domain setup knowledge. Let me know if you need a detailed tutorial.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;5.2 - Reverse Proxy&lt;/h3&gt;
&lt;p&gt;With your domain pointing to your public IP, you&apos;ll want subdomain access instead of port-based access.&lt;/p&gt;
&lt;p&gt;This is where a reverse proxy like &lt;strong&gt;NGINX&lt;/strong&gt; becomes essential!&lt;/p&gt;
&lt;p&gt;We&apos;ll use [[Nginx Proxy Manager|https://nginxproxymanager.com/]] to manage subdomains, ports, and SSL certificate generation.&lt;/p&gt;
&lt;p&gt;Here is a sample YAML file to get you started:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;3&apos;
services:
  app:
    image: &apos;jc21/nginx-proxy-manager:latest&apos;
    restart: unless-stopped
    ports:
      - &apos;3245:80&apos; 
      - &apos;3246:81&apos;
      - &apos;3247:443&apos;
    volumes:
      - /mnt/disk3/docker_data/nginx_manager/data:/data
      - /mnt/disk3/docker_data/nginx_manager/letsencrypt:/etc/letsencrypt
    networks:
      - media_server_default
networks:
  media_server_default:
    external: true
  whisper_default:
    external: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the container starts, access the UI at &lt;code&gt;192.168.1.2:3246&lt;/code&gt; to configure services and subdomains.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Local access only&lt;/strong&gt;: Configure router port forwarding to enable external access:&lt;/p&gt;
&lt;p&gt;Navigate to your router&apos;s IPv4 Port Forwarding settings and add these rules (adjust for your NAS IP):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;80&lt;/code&gt; → &lt;code&gt;3245&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;443&lt;/code&gt; → &lt;code&gt;3247&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This enables domain-based service access instead of ports.&lt;/p&gt;
&lt;h2&gt;How the Traffic Flow Works 🌐&lt;/h2&gt;
&lt;p&gt;Here&apos;s the simplified request flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Domain&lt;/strong&gt; → &lt;strong&gt;Cloudflare&lt;/strong&gt; (DNS + protection)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt; → &lt;strong&gt;Public IP&lt;/strong&gt; (your home router)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browser&lt;/strong&gt; → &lt;strong&gt;Router&lt;/strong&gt; (ports 80/443)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Router&lt;/strong&gt; → &lt;strong&gt;Unraid Server&lt;/strong&gt; (ports 3245/3247)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt; → &lt;strong&gt;Service&lt;/strong&gt; (based on subdomain)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Adding Services&lt;/h2&gt;
&lt;p&gt;Access services via subdomains by mapping them in Nginx Proxy Manager. For example, mapping &lt;code&gt;bazar.domain.com&lt;/code&gt; to your local &lt;code&gt;bazar&lt;/code&gt; container:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Docker container names are case-sensitive.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Repeat this process for all services. The combination of Cloudflare and reverse proxy allows you to secure services behind firewalls or password protection.&lt;/p&gt;
&lt;h2&gt;6 - Media Server - Plex, Jellyfin &amp;amp; Emby&lt;/h2&gt;
&lt;p&gt;With your server configured, you can install a media server for streaming movies, TV shows, music, and more. You can either use your own media or download content from the internet. &lt;strong&gt;Please ensure you have proper rights to download and stream any content.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;2.1&quot;
services:
  transmission:
    image: lscr.io/linuxserver/transmission:latest
    container_name: transmission
    environment:
      - PUID=99
      - PGID=100
      - UMASK=000
      - TZ=Europe/London
    volumes:
      - /mnt/disk3/docker_data/transmission/data:/config
      - /mnt/disk1/library/shows:/shows
      - /mnt/disk2/library/movies:/movies
      - /mnt/disk1/library/documentaries:/documentaries
      - /mnt/disk3/downloads:/downloads
    ports:
      - 9091:9091
      - 51413:51413
      - 51413:51413/udp
    restart: unless-stopped

  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    environment:
      - PUID=99
      - PGID=100
      - UMASK=000
      - TZ=Europe/London
    volumes:
      - /mnt/disk3/docker_data/sonarr/data:/config
      - /mnt/disk1/library/shows:/shows
      - /mnt/disk2/library/movies:/movies
      - /mnt/disk1/library/documentaries:/documentaries
      - /mnt/disk3/downloads:/downloads
      - /mnt/disk3/downloads:/config/downloads
    ports:
      - 8989:8989
    restart: unless-stopped

      
  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    environment:
      - PUID=99
      - PGID=100
      - UMASK=000
      - TZ=Europe/London
    volumes:
      - /mnt/disk3/docker_data/radarrr/data:/config
      - /mnt/disk1/library/shows:/shows
      - /mnt/disk2/library/movies:/movies
      - /mnt/disk1/library/documentaries:/documentaries
      - /mnt/disk3/downloads:/downloads
      - /mnt/disk3/downloads:/config/downloads
    ports:
      - 7878:7878
    restart: unless-stopped

  bazarr:
    image: lscr.io/linuxserver/bazarr:latest
    container_name: bazarr
    environment:
      - PUID=99
      - PGID=100
      - UMASK=000
      - TZ=Europe/London
    volumes:
      - /mnt/disk3/docker_data/bazarr/data:/config
      - /mnt/disk1/library/shows:/shows
      - /mnt/disk2/library/movies:/movies
      - /mnt/disk1/library/documentaries:/documentaries
    ports:
      - 6767:6767
    restart: unless-stopped

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    environment:
      - PUID=99
      - PGID=100
      - UMASK=000
      - TZ=Europe/London
    volumes:
      - /mnt/disk3/docker_data/prowlarr/data:/config
      - /mnt/disk1/library/shows:/shows
      - /mnt/disk2/library/movies:/movies
      - /mnt/disk1/library/documentaries:/documentaries
      - /mnt/disk3/downloads:/downloads
    ports:
      - 9696:9696
    restart: unless-stopped

  sabnzbd:
    image: lscr.io/linuxserver/sabnzbd:latest
    container_name: sabnzbd
    environment:
      - PUID=99
      - PGID=100
      - UMASK=000
      - TZ=Europe/London
    volumes:
      - /mnt/disk3/docker_data/sabnzbd/data:/config
      - /mnt/disk3/downloads:/config/downloads
      - /mnt/disk3/downloads:/downloads
    ports:
      - 8329:8080
    restart: unless-stopped

  nzbget:
    image: lscr.io/linuxserver/nzbget:latest
    container_name: nzbget
    environment:
      - PUID=99
      - PGID=100
      - UMASK=000
      - TZ=Europe/London
    volumes:
      - /mnt/disk3/docker_data/nzbget/data:/config
      - /mnt/disk1/library/shows:/shows
      - /mnt/disk2/library/movies:/movies
      - /mnt/disk1/library/documentaries:/documentaries
      - /mnt/disk3/downloads:/downloads
    ports:
      - 6789:6789
    restart: unless-stopped

  overseerr:
    image: sctx/overseerr:latest
    container_name: overseerr
    environment:
      - LOG_LEVEL=debug
      - TZ=Europe/London
      - PORT=5055
      - PUID=99
      - PGID=100
      - UMASK=000
    ports:
      - 5055:5055
    volumes:
      - /mnt/disk3/docker_data/overseerr/data:/app/config
      - /mnt/disk1/library/shows:/shows
      - /mnt/disk2/library/movies:/movies
      - /mnt/disk1/library/documentaries:/documentaries
      - /mnt/disk3/downloads:/downloads
    restart: unless-stopped

  tautulli:
    image: lscr.io/linuxserver/tautulli:latest
    container_name: tautulli
    environment:
      - PUID=99
      - PGID=100
      - MASK=000
      - TZ=Europe/London
    volumes:
      - /mnt/disk3/docker_data/tautulli/data:/config
    ports:
      - 8181:8181
    restart: unless-stopped

  organizr:
    container_name: organizr
    hostname: organizr
    image: organizr/organizr:latest

    ports:
        - 8333:80
    volumes:
        - /mnt/disk3/docker_data/organizrr/data:/config
    environment:
        - PUID=99
        - PGID=100
        - TZ=Europe/London
    restart: unless-stopped

  maintainerr:
    image: jorenn92/maintainerr:latest
    container_name: maintainerr
    volumes:
      - /mnt/disk3/docker_data/maintainerr/data:/opt/data
    environment:
      - PUID=99
      - PGID=100
      - TZ=Europe/London
    ports:
      - 8154:80
    restart: unless-stopped
  wizarr:
    image: ghcr.io/wizarrrr/wizarr:latest
    container_name: wizarr
    ports:
      - 5690:5690
    volumes:
      - /mnt/disk3/docker_data/wizarr/database:/data/database
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.1.1 - Plex 📺&lt;/h3&gt;
&lt;p&gt;[[Plex]] is one of the best media servers available, offering extensive features and broad device compatibility.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Easy setup and intuitive interface&lt;/li&gt;
&lt;li&gt;Regular UI updates and improvements&lt;/li&gt;
&lt;li&gt;Pre-installed apps on most TVs for easy friend/family sharing&lt;/li&gt;
&lt;li&gt;Integrates with Netflix, Amazon Prime, etc. for unified library management&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Limited plugin ecosystem (mostly legacy)&lt;/li&gt;
&lt;li&gt;Account dependency - Plex controls your access&lt;/li&gt;
&lt;li&gt;Premium features require Plex Pass subscription&lt;/li&gt;
&lt;li&gt;Mobile app requires payment for video playback (affects sharing)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6.1.2 - Jellyfin 🆓&lt;/h4&gt;
&lt;p&gt;[[Jellyfin]] is a completely free and open-source media server with extensive features and broad device compatibility.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Truly free and open-source&lt;/li&gt;
&lt;li&gt;Clean UI with regular updates&lt;/li&gt;
&lt;li&gt;Complete server ownership and control&lt;/li&gt;
&lt;li&gt;Service integrations for unified library management&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More technical setup required&lt;/li&gt;
&lt;li&gt;Manual app installation needed (limited pre-installed availability)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6.1.3 - Emby 💰&lt;/h4&gt;
&lt;p&gt;[[Emby]] is a popular media server that transitioned from open-source to a freemium model. Like Plex, premium features require payment.&lt;/p&gt;
&lt;p&gt;While more &quot;free&quot; than Plex, the closed-source nature is a drawback.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rich plugin ecosystem and advanced features&lt;/li&gt;
&lt;li&gt;More configuration options than Plex&lt;/li&gt;
&lt;li&gt;Free mobile app&lt;/li&gt;
&lt;li&gt;Self-hosted with data ownership&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Closed-source model&lt;/li&gt;
&lt;li&gt;Dated UI design (lacks modern aesthetics)&lt;/li&gt;
&lt;li&gt;Premium feature paywall&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6.1.4 - Best Media Server in 2025? 🏆&lt;/h4&gt;
&lt;p&gt;For home sharing with friends and family, &lt;strong&gt;Plex&lt;/strong&gt; remains the top choice. It&apos;s the most user-friendly option with extensive features. Since most viewing happens on TVs rather than mobile devices, the mobile app cost becomes less significant. A one-time Plex Pass purchase offers excellent value.&lt;/p&gt;
&lt;h3&gt;6.2 - The *Arr Stack: Automation Suite 🤖&lt;/h3&gt;
&lt;p&gt;With your server and media server ready, let&apos;s explore automating content acquisition using specialized Docker containers from the community.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why multiple applications?&lt;/strong&gt; Each tool serves a specific purpose and excels at its designated task. This modular approach offers several benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Specialized functionality&lt;/strong&gt;: Series downloading differs from movies and music&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Independent development&lt;/strong&gt;: Each tool evolves at its own pace&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;: Mix and match tools for your specific needs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reliability&lt;/strong&gt;: No single point of failure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;Callout icon=&quot;☝️&quot; type=&quot;danger&quot;&amp;gt;
Please keep in mind that this tutorial will not cover on how to download movies, tv shows, music, etc. Please ensure you have the rights to download and stream the content that you are downloading.
Subject to any applicable laws, you are responsible for your own actions.
&amp;lt;/Callout&amp;gt;&lt;/p&gt;
&lt;h3&gt;6.3 - *Arr Stack Capabilities 🎯&lt;/h3&gt;
&lt;p&gt;The *Arr software suite manages your media libraries and automates content acquisition, feeding new content directly to your media server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Search&lt;/strong&gt;: Find content across multiple indexers (Usenet, torrents)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Download&lt;/strong&gt;: Automated content acquisition&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organization&lt;/strong&gt;: Powerful renaming with proper metadata&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quality Control&lt;/strong&gt;: Target specific qualities (4K, 1080p, 720p)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduling&lt;/strong&gt;: Track and schedule downloads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Application Roles:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;[[Radarr]]&lt;/strong&gt;: Movie management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Sonarr]]&lt;/strong&gt;: TV show management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Prowlarr]]&lt;/strong&gt;: Indexer management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Bazarr]]&lt;/strong&gt;: Subtitle management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Tdarr]]&lt;/strong&gt;: Media transcoding&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.4 - Content Acquisition Methods 📥&lt;/h3&gt;
&lt;p&gt;Here&apos;s an overview of the two main content acquisition methods (research Reddit communities for detailed information):&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Usenet:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Distributed network since the 1980s&lt;/li&gt;
&lt;li&gt;Fast, secure, and difficult to track&lt;/li&gt;
&lt;li&gt;Requires: Provider + Indexer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Torrents:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Peer-to-peer network&lt;/li&gt;
&lt;li&gt;Fast but easily trackable&lt;/li&gt;
&lt;li&gt;Requires: Tracker + VPN (recommended)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Indexers:&lt;/strong&gt; Search engines for both Usenet and torrent content&lt;/p&gt;
&lt;p&gt;The *Arr software automates downloads from both sources based on your configuration.&lt;/p&gt;
&lt;h3&gt;6.5 - Usenet vs Torrents in 2025 ⚖️&lt;/h3&gt;
&lt;p&gt;While torrents remain viable, I&apos;ve shifted to Usenet for primary content acquisition due to several factors:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Torrent Challenges:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Quality content frequently removed&lt;/li&gt;
&lt;li&gt;Private tracker invite requirements&lt;/li&gt;
&lt;li&gt;Seeding obligations for ratio maintenance&lt;/li&gt;
&lt;li&gt;Provider dependency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Usenet Advantages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paid service reliability&lt;/li&gt;
&lt;li&gt;No seeding requirements&lt;/li&gt;
&lt;li&gt;Consistent availability&lt;/li&gt;
&lt;li&gt;Enhanced security&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I prefer paying for quality Usenet providers and indexers for reliable, fast, and secure downloads.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: This article won&apos;t recommend specific providers. Research Reddit communities to find services matching your needs.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;6.6 - Localized Content &amp;amp; Subtitles 🌍&lt;/h3&gt;
&lt;p&gt;While most online media is available in English, localized content for family viewing often requires subtitles. Audio localization is more challenging, though you can configure preferences in Bazarr or Radarr.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[[Bazarr]] Capabilities:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Downloads subtitles from [[OpenSubtitles.org|https://opensubtitles.org]]&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Generation&lt;/strong&gt;: Uses [[@ahmetoner/whisper-asr-webservice]] AI for subtitle creation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Translation&lt;/strong&gt;: Converts English subtitles to other languages (generally effective)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&apos;s my automation script for subtitle translation on Unraid:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json
import os
from urllib import request, error

# ------------------
# Credentials
# ------------------
TRANSLATION_FILE_PATH = &apos;./translation_attempts.json&apos;
API_KEY = &apos;YOURKEY&apos;
API_BASE_URL = &apos;https://YOURBAZARURL.com/api&apos;
COMMON_HEADERS = {
    &apos;Accept&apos;: &apos;application/json&apos;,
    &apos;X-API-KEY&apos;: API_KEY,
    &apos;User-Agent&apos;: &apos;curl/8.4.0&apos;
}

# ------------------
# Make a request
# ------------------
def make_request(url, method=&apos;GET&apos;):
    req = request.Request(url, method=method, headers=COMMON_HEADERS)
    try:
        with request.urlopen(req) as response:
            if response.status == 200 or response.status == 204:
                return json.loads(response.read().decode()) if method == &apos;GET&apos; else True
            else:
                print(f&quot;Failed request at {url}, Status code: {response.status}&quot;)
                return None
    except error.HTTPError as e:
        #print(f&quot;HTTP error: {e.code} - {e.reason}&quot;)
        return None
    except error.URLError as e:
        #print(f&quot;URL error: {e.reason}&quot;)
        return None
    
# ------------------
# Load The attempts
# ------------------
def load_translation_attempts():
    if os.path.exists(TRANSLATION_FILE_PATH):
        with open(TRANSLATION_FILE_PATH, &apos;r&apos;) as file:
            return json.load(file)
    return {}

# ------------------
# Save The Attempts
# ------------------
def save_translation_attempts(attempts):
    with open(TRANSLATION_FILE_PATH, &apos;w&apos;) as file:
        json.dump(attempts, file, indent=4)

# ------------------
# Get all series IDs
# ------------------
def get_series_ids():
    data = make_request(f&quot;{API_BASE_URL}/series?start=0&amp;amp;length=-1&quot;)
    return [series[&apos;sonarrSeriesId&apos;] for series in data[&apos;data&apos;]] if data else []

# ------------------
# Get subtitle paths for multiple series
# ------------------
def get_subtitle_paths_for_multiple_series(series_ids):
    # Constructing the query string for multiple series IDs
    series_ids_query = &apos;&amp;amp;&apos;.join([f&apos;seriesid%5B%5D={id}&apos; for id in series_ids])
    episode_url = f&quot;{API_BASE_URL}/episodes?{series_ids_query}&quot;

    data = make_request(episode_url)
    subtitle_info = []
    if data:
        for episode in data[&apos;data&apos;]:
            english_subtitles = [sub for sub in episode[&apos;subtitles&apos;] if sub[&apos;code2&apos;] == &apos;en&apos;]
            portuguese_subtitles = [sub for sub in episode[&apos;subtitles&apos;] if sub[&apos;code2&apos;] == &apos;pt&apos;]
            if english_subtitles and not portuguese_subtitles:
                subtitle_info.extend([{
                    &apos;path&apos;: sub[&apos;path&apos;],
                    &apos;id&apos;: episode[&apos;sonarrEpisodeId&apos;],
                    &apos;type&apos;: &apos;episode&apos;
                } for sub in english_subtitles if sub[&apos;path&apos;] is not None])
    return subtitle_info

# ------------------
# Get subtitle paths for movies
# ------------------
def get_movie_subtitle_paths():
    data = make_request(f&quot;{API_BASE_URL}/movies?start=0&amp;amp;length=-1&quot;)
    subtitle_info = []
    if data:
        for movie in data[&apos;data&apos;]:
            english_subtitles = [sub for sub in movie[&apos;subtitles&apos;] if sub[&apos;code2&apos;] == &apos;en&apos;]
            portuguese_subtitles = [sub for sub in movie[&apos;subtitles&apos;] if sub[&apos;code2&apos;] == &apos;pt&apos;]
            if english_subtitles and not portuguese_subtitles:
                subtitle_info.extend([{
                    &apos;path&apos;: sub[&apos;path&apos;],
                    &apos;id&apos;: movie[&apos;radarrId&apos;],
                    &apos;type&apos;: &apos;movie&apos;
                } for sub in english_subtitles if sub[&apos;path&apos;] is not None])
    return subtitle_info

# ------------------
# Translate subtitle
# ------------------
# Update the translate_subtitle function
def translate_subtitle(subtitle_info, language=&apos;pt&apos;):
    subtitle_path = subtitle_info[&apos;path&apos;]
    translation_attempts = load_translation_attempts()

    if translation_attempts.get(subtitle_path, 0) &amp;gt;= 3:
        print(f&quot;Skipping translation for {subtitle_path} due to multiple failures.&quot;)
        return

    if make_request(f&quot;{API_BASE_URL}/subtitles?action=translate&amp;amp;language={language}&amp;amp;path={request.quote(subtitle_path)}&amp;amp;type={subtitle_info[&apos;type&apos;]}&amp;amp;id={subtitle_info[&apos;id&apos;]}&quot;, &apos;PATCH&apos;):
        print(f&quot;Translate: Subtitle translated: {subtitle_path}&quot;)
    else:
        print(f&quot;Translate: Failed to translate subtitle: {subtitle_path}&quot;)
        translation_attempts[subtitle_path] = translation_attempts.get(subtitle_path, 0) + 1
        save_translation_attempts(translation_attempts)

# ------------------
# Translate Series
# ------------------
def translate_series():
    # Usage
    all_series_ids = get_series_ids()

    # Check if there are any series IDs, exit if not
    if not all_series_ids:
        print(&quot;Series: No series found to process.&quot;)
        #exit(0)

    all_subtitles_info = get_subtitle_paths_for_multiple_series(all_series_ids)

    # Check if there are any subtitle paths, exit if not
    if not all_subtitles_info:
        print(&quot;Series: No subtitle paths found to translate.&quot;)
        #exit(0)

    # Translate 10 Subtitles
    subtitles_to_translate = all_subtitles_info[:20]

    # Translate the selected subtitles
    for subtitle in subtitles_to_translate:
        translate_subtitle(subtitle)

# ------------------
# Translate Movies
# ------------------
def translate_movies():
    # Usage
    all_subtitles_info = get_movie_subtitle_paths()

    # Check if there are any subtitle paths, exit if not
    if not all_subtitles_info:
        print(&quot;Movies: No subtitle paths found to translate.&quot;)
        #exit(0)

    # Translate 10 Subtitles
    subtitles_to_translate = all_subtitles_info[:20]

    # Translate the selected subtitles
    for subtitle in subtitles_to_translate:
        translate_subtitle(subtitle)

# ------------------
# Main
# ------------------
if __name__ == &apos;__main__&apos;:
    translate_series()
    translate_movies()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.7 - Transcoding &amp;amp; Optimization 🎬&lt;/h3&gt;
&lt;p&gt;For advanced streaming optimization, research transcoding and optimal streaming formats. No perfect video/audio format exists - it depends on your use case and playback hardware.&lt;/p&gt;
&lt;p&gt;While platforms like Netflix convert content into multiple codecs for different users, home servers can&apos;t afford this luxury of storing multiple formats per movie.&lt;/p&gt;
&lt;p&gt;Choose formats that best fit your specific needs. Transcoding serves two purposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Space saving&lt;/strong&gt;: Smaller file sizes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Faster streaming&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6.8 - Format Selection 📋&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;HEVC + H.265&lt;/strong&gt;: Modern format for 2015+ hardware
&lt;strong&gt;MP4 + H.264&lt;/strong&gt;: Widely compatible with modern and legacy devices&lt;/p&gt;
&lt;p&gt;While &lt;strong&gt;H.265&lt;/strong&gt; offers better compression for 2025 streaming, it requires newer hardware. For family sharing, ensure devices support H.265, otherwise Plex will transcode to &lt;strong&gt;H.264&lt;/strong&gt;, increasing hardware usage and power consumption.&lt;/p&gt;
&lt;h4&gt;6.9 - Transcoding &amp;amp; Processing Pipeline 🔄&lt;/h4&gt;
&lt;p&gt;Here&apos;s what you need to know about transcoding and video processing software.&lt;/p&gt;
&lt;p&gt;Configure Radarr and Sonarr to prefer specific formats to minimize re-processing needs.&lt;/p&gt;
&lt;p&gt;&amp;lt;Callout icon=&quot;☝️&quot; type=&quot;info&quot;&amp;gt;
A budget NVIDIA GPU is recommended for efficient transcoding. CPU transcoding is resource-intensive and consumes more energy.
&amp;lt;/Callout&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File Processing Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compression&lt;/strong&gt;: Reduce disk space usage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audio Management&lt;/strong&gt;: Remove unwanted language tracks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subtitle Control&lt;/strong&gt;: Remove unnecessary subtitle tracks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Format Conversion&lt;/strong&gt;: Convert audio formats (5.1 to 2.1)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subtitle Extraction&lt;/strong&gt;: Extract subtitles for translation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track Organization&lt;/strong&gt;: Organize audio and subtitle track order&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All processing relies on &lt;strong&gt;[[FFmpeg]]&lt;/strong&gt; - consider supporting this amazing project!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tool Comparison:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[[Tdarr|https://home.tdarr.io/]]&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advanced features&lt;/li&gt;
&lt;li&gt;Popular community choice&lt;/li&gt;
&lt;li&gt;Open source with freemium model&lt;/li&gt;
&lt;li&gt;Complex UI that can be confusing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;[[Fileflows|https://fileflows.com/]]&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Superior user interface&lt;/li&gt;
&lt;li&gt;Built-in plugins and tools&lt;/li&gt;
&lt;li&gt;Responsive smaller community&lt;/li&gt;
&lt;li&gt;Intuitive flow building&lt;/li&gt;
&lt;li&gt;Easy multi-node setup&lt;/li&gt;
&lt;li&gt;Better Docker integration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Results&lt;/strong&gt;: Saved approximately &lt;strong&gt;3TB&lt;/strong&gt; from my current library!&lt;/p&gt;
&lt;p&gt;For large transcoding workloads, deploy multiple nodes working in parallel for simultaneous content processing.&lt;/p&gt;
&lt;h3&gt;6.10 - Advanced Media Server Features 🚀&lt;/h3&gt;
&lt;p&gt;Enhance your media server setup with these additional tools for a better library experience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;[[Tautulli|https://tautulli.com/]]&lt;/strong&gt;: Monitor Plex server performance, user statistics, and transcoding metrics&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Overseerr|https://overseerr.dev/]]&lt;/strong&gt;: Enable friends and family to request content with approval workflows for automatic downloads&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Kometa|https://github.com/Kometa-Team/Kometa]]&lt;/strong&gt;: Plex Meta Manager for enhanced metadata, custom categories, library organization, and popular content lists&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Wizarr|https://wizarr.dev/]]&lt;/strong&gt;: Streamlined user invitation system with permission management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Organizr|https://organizr.app/]]&lt;/strong&gt;: Unified dashboard for all your services with clean UI access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[[Maintainerr|https://github.com/maintainerr/maintainerr]]&lt;/strong&gt;: Automatically manage content lifecycle - remove unwatched or completed content&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7 - Energy Efficiency ⚡&lt;/h2&gt;
&lt;p&gt;Energy saving is often overlooked but crucial for high-end servers running 24/7. Here are effective power management strategies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Disk Spindown&lt;/strong&gt;: Automatically spin down unused drives to reduce power consumption&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sleep Mode&lt;/strong&gt;: Enable server sleep mode with wake-on-demand capabilities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU Power Management&lt;/strong&gt;: Configure power scaling based on usage patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU Power Management&lt;/strong&gt;: Optimize NVIDIA GPU power states when idle&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
# check for driver
command -v nvidia-smi &amp;amp;&amp;gt; /dev/null || { echo &amp;gt;&amp;amp;2 &quot;nvidia driver is not installed you will need to install this from community applications ... exiting.&quot;; exit 1; }
echo &quot;Nvidia drivers are installed&quot;
echo
echo &quot;I can see these Nvidia gpus in your server&quot;
echo
nvidia-smi --list-gpus 
echo
echo &quot;-------------------------------------------------------------&quot;
# set persistence mode for gpus ( When persistence mode is enabled the NVIDIA driver remains loaded even when no active processes, 
# stops modules being unloaded therefore stops settings changing when modules are reloaded
nvidia-smi --persistence-mode=1
#query power state
gpu_pstate=$(nvidia-smi --query-gpu=&quot;pstate&quot; --format=csv,noheader);
#query running processes by pid using gpu
gpupid=$(nvidia-smi --query-compute-apps=&quot;pid&quot; --format=csv,noheader);
#check if pstate is zero and no processes are running by checking if any pid is in string
if [ &quot;$gpu_pstate&quot; == &quot;P0&quot; ] &amp;amp;&amp;amp; [ -z &quot;$gpupid&quot; ]; then
echo &quot;No pid in string so no processes are running&quot;
fuser -kv /dev/nvidia*
echo &quot;Power state is&quot;
echo &quot;$gpu_pstate&quot; # show what power state is
else
echo &quot;Power state is&quot; 
echo &quot;$gpu_pstate&quot; # show what power state is
fi
echo
echo &quot;-------------------------------------------------------------&quot;
echo
echo &quot;Power draw is now&quot;
# Check current power draw of GPU
nvidia-smi --query-gpu=power.draw --format=csv
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8 - Data Protection: Backups &amp;amp; Parity 🛡️&lt;/h2&gt;
&lt;p&gt;Unraid&apos;s &lt;strong&gt;Parity&lt;/strong&gt; system protects your data differently from traditional RAID. Parity offers more flexibility and allows expansion as needed.&lt;/p&gt;
&lt;p&gt;For detailed information about Parity, RAID, and backups, consult the [[Unraid FAQ|https://docs.unraid.net/legacy/FAQ/Parity/]].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My Backup Strategy&lt;/strong&gt;: I use [[Duplicacy|http://duplicacy.com]] for cloud backups with easy restoration capabilities. Currently storing backups with [[Backblaze|https://www.backblaze.com/]] for their excellent service quality and competitive pricing.&lt;/p&gt;
&lt;h2&gt;9 - Expanding Your Unraid Capabilities 🚀&lt;/h2&gt;
&lt;p&gt;The possibilities are endless with your NAS and Unraid setup. Run virtually any application including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Personal cloud storage (Dropbox alternative)&lt;/li&gt;
&lt;li&gt;LLM inference servers&lt;/li&gt;
&lt;li&gt;DNS ad-blocking servers&lt;/li&gt;
&lt;li&gt;Development environments&lt;/li&gt;
&lt;li&gt;Home automation systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unraid&apos;s Docker support enables running almost any containerized application with simple expansion capabilities.&lt;/p&gt;
&lt;p&gt;The community app ecosystem grows daily, providing countless applications installable with just a few clicks.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;10 - Conclusion 🎬&lt;/h2&gt;
&lt;p&gt;We&apos;ve explored Unraid fundamentals, setup procedures, and how to create your own Netflix-like service, NAS, or office file system. The configuration and tweaking process has been enjoyable while learning about Docker, Linux, and microservices.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: Some information may contain inaccuracies as I&apos;m sharing personal learning experiences. This article aims to help you start your own NAS and Unraid journey.&lt;/p&gt;
&lt;h2&gt;11 - My Hardware Specifications 💻&lt;/h2&gt;
&lt;p&gt;Here&apos;s my current hardware setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU&lt;/strong&gt;: Intel® Core™ i7-8700 @ 3.20GHz&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAM&lt;/strong&gt;: 16GB DDR4&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU&lt;/strong&gt;: RTX A2000&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard&lt;/strong&gt;: Gigabyte Z390 DESIGNARE-CF&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network&lt;/strong&gt;: 1Gbps (1GB Download / 500MB Upload)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UPS&lt;/strong&gt;: APC Back-UPS Pro 900&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: 2x 10TB WDC WD101KRYZ drives&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache&lt;/strong&gt;: 2x 1TB Samsung SSD 970 EVO Plus&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Do you need this much?&lt;/strong&gt; No! I repurposed existing hardware for this project. You can run the same setup with much less powerful hardware.&lt;/p&gt;
&lt;h2&gt;12 - References &amp;amp; Links 🔗&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Unraid|https://unraid.net/]]&lt;/li&gt;
&lt;li&gt;[[Usenet Indexers|https://www.reddit.com/r/Usenet/wiki/indexers/]]&lt;/li&gt;
&lt;li&gt;[[Usenet Providers|https://www.reddit.com/r/usenet/wiki/providers]]&lt;/li&gt;
&lt;li&gt;[[LinuxServer Images|https://docs.linuxserver.io/images/docker-adguardhome-sync/]]&lt;/li&gt;
&lt;li&gt;[[Fileflows|https://fileflows.com/]]&lt;/li&gt;
&lt;li&gt;[[Best Movies Lists|https://mdblist.com/]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;How does Unraid compare to Synology and QNAP for a home server?&quot;, answer: &quot;Unraid runs from a USB drive on almost any hardware without modifying your drives, supports mixed drive sizes, and has built-in Docker support. Synology and QNAP are proprietary solutions tied to specific hardware. Unraid starts at $60 and offers more flexibility for homelabs, while Synology and QNAP provide a more turnkey experience.&quot; },
{ question: &quot;What are the differences between Plex, Jellyfin, and Emby for an Unraid media server?&quot;, answer: &quot;Plex has polished apps pre-installed on most TVs but requires a Plex Pass for some features. Jellyfin is fully free and open-source with complete server ownership but needs manual app installation. Emby offers a rich plugin ecosystem but is closed-source with a freemium model. All three support transcoding and multiple user profiles on Unraid.&quot; },
{ question: &quot;What is the *Arr stack and how does it work with Unraid?&quot;, answer: &quot;The *Arr stack is a suite of Docker containers for media automation: Radarr manages movies, Sonarr handles TV shows, Prowlarr manages indexers, Bazarr downloads subtitles, and Tdarr handles transcoding. Each runs as a separate container on Unraid, feeding content to your Plex or Jellyfin media server.&quot; },
{ question: &quot;How do Tdarr and Fileflows compare for media transcoding on Unraid?&quot;, answer: &quot;Tdarr is the more popular choice with advanced features and an open-source freemium model, but has a complex UI. Fileflows offers a more intuitive flow-building interface, better Docker integration, and easier multi-node setup. Both use FFmpeg under the hood and can save significant disk space by converting media to formats like H.265.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>unraid</category><category>streaming-server</category><category>nas</category><category>netflix-like-service</category><category>homelab</category><author>Pedro Martins</author></item><item><title>Roast ®</title><link>https://nikuscs.com/projects/06-pleaseroast/</link><guid isPermaLink="true">https://nikuscs.com/projects/06-pleaseroast/</guid><description>A funny AI-powered social media profile roaster to try our AI skills :p</description><pubDate>Wed, 01 Jan 2025 00:00:00 GMT</pubDate><category>ai</category><category>social-media</category><category>marketing</category><category>automation</category><category>viral</category><author>Pedro Martins</author></item><item><title>Flavorly ®</title><link>https://nikuscs.com/projects/04-flavorly/</link><guid isPermaLink="true">https://nikuscs.com/projects/04-flavorly/</guid><description>Open-source contributions to Laravel, Javascripts, Vue, React, etc.</description><pubDate>Sun, 01 Dec 2024 00:00:00 GMT</pubDate><category>open-source</category><category>javascript</category><category>vue</category><category>react</category><category>laravel</category><category>tailwind</category><category>typescript</category><category>php</category><author>Pedro Martins</author></item><item><title>Vue 3 &amp; React - Ticker, Marquee, Carousel Component</title><link>https://nikuscs.com/blog/05-ticker/</link><guid isPermaLink="true">https://nikuscs.com/blog/05-ticker/</guid><description>A example of a Ticker, Marquee or Carousel component using Vue 3 and React, support hover slowdown, pause on hover, and more.</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import DemoReact from &quot;./demo.tsx&quot;;&lt;/p&gt;
&lt;p&gt;&amp;lt;CraftBox&amp;gt;
&amp;lt;div class=&quot;flex items-center justify-center min-h-[600px]&quot;&amp;gt;
&amp;lt;DemoReact client:load /&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/CraftBox&amp;gt;&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;I fell in love with [[Framer|https://framer.com]] Marquees when they launched and wanted to integrate them into one of my projects. However, I encountered several issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In React and Vue, I couldn&apos;t find packages/components that met my requirements&lt;/li&gt;
&lt;li&gt;Existing solutions weren&apos;t polished enough for my design standards&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here are my specific requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hover slowdown&lt;/strong&gt;: Mouse hover should reduce animation speed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pause on hover&lt;/strong&gt;: Complete animation pause when hovering&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auto-cloning&lt;/strong&gt;: Clone items if insufficient content to fill the container&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Endless repeat&lt;/strong&gt;: Seamless infinite loop&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge fading&lt;/strong&gt;: Smooth fade effects on container edges&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Touch support&lt;/strong&gt;: Mobile-friendly interactions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-directional&lt;/strong&gt;: Both vertical and horizontal orientations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Render only when visible (intersection observer)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You might wonder: why not use a carousel? Carousels and marquees serve different purposes. Carousels display a few items at once with navigation controls, while marquees show continuous flowing content—perfect for news feeds, stock tickers, or sliding content displays.&lt;/p&gt;
&lt;p&gt;While you could theoretically achieve this with pure CSS, I encountered issues with that approach. CSS animations caused inconsistent behavior, especially during hover slowdown effects.&lt;/p&gt;
&lt;p&gt;After deeper investigation, I decided to build custom Marquee/Ticker components for both React and Vue.&lt;/p&gt;
&lt;h2&gt;Technical Challenges 🛠️&lt;/h2&gt;
&lt;p&gt;Here are the key challenges I faced while building this component:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DOM Measurement Timing&lt;/strong&gt;: We must measure parent and child dimensions on the next tick since the DOM isn&apos;t ready immediately. This calculation determines how many duplicates we need for seamless scrolling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Framework Differences&lt;/strong&gt;: Slots work differently in Vue and React, requiring separate handling strategies for each framework&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pixel-Perfect Animation&lt;/strong&gt;: Creating a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame&quot;&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;&lt;/a&gt; ticker that loops seamlessly without visible jumps—precision is critical for smooth user experience&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vue 3 Implementation 🟢&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import type { ComputedRef, CSSProperties } from &apos;vue&apos;
import { useFade } from &apos;@/utils/fade-mask&apos;

/**
 * Some notes:
 * - We cannot apply any kind of transition to the ticker because it will break the animation ( say tailwindcss classes )
 * - Types imported from vue are not working properly so we must keep styles inline
 */

export interface SizingOptions {
	widthType: boolean
	heightType: boolean
}

export interface FadeOptions {
	content: boolean
	overflow: boolean
	width: number
	alpha: number
	inset: number
}

export interface PaddingOptions {
	padding: number
	perSide: boolean
	top: number
	right: number
	bottom: number
	left: number
}

export interface TickerProps {
	gap?: number | string
	speed?: number | string
	hoverFactor?: number | string
	direction?: string | boolean | &apos;left&apos; | &apos;right&apos; | &apos;top&apos; | &apos;bottom&apos;
	alignment?: string | &apos;flex-start&apos; | &apos;center&apos; | &apos;flex-end&apos;
	sizingOptions?: SizingOptions
	fadeOptions?: FadeOptions
	paddingOptions?: PaddingOptions
}

defineOptions({
	inheritAttrs: false,
})

const props = withDefaults(defineProps&amp;lt;TickerProps&amp;gt;(), {
	gap: 10,
	speed: 20,
	hoverFactor: 3,
	direction: &apos;right&apos;,
	alignment: &apos;center&apos;,
	paddingOptions: () =&amp;gt; ({
		padding: 10,
		perSide: false,
		top: 0,
		right: 0,
		bottom: 0,
		left: 0
	}),
	sizingOptions: () =&amp;gt; ({
		widthType: true,
		heightType: true
	}),
	fadeOptions: () =&amp;gt; ({
		content: true,
		overflow: false,
		width: 50,
		alpha: 0,
		inset: 0
	}),
})

const containerStyle = {
	display: &apos;flex&apos;,
	width: &apos;100%&apos;,
	height: &apos;100%&apos;,
	maxWidth: &apos;100%&apos;,
	maxHeight: &apos;100%&apos;,
	placeItems: &apos;center&apos;,
	margin: 0,
	padding: 0,
	listStyleType: &apos;none&apos;,
	textIndent: &apos;none&apos;
}

// Utility: Wrap Utility
const wrap = (min: number, max: number, v: number) =&amp;gt; {
	const rangeSize = max - min
	return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min
}

// The main container ref
const container = ref&amp;lt;HTMLElement&amp;gt;()
// The UL container that contains the children
const containerItems = ref&amp;lt;HTMLElement&amp;gt;()
// Stores the Refs for the original elements
const elementsOriginal = shallowRef()
// Stores the Refs of the cloned elements
const elementsCloned = shallowRef()
// Checks if its hovered or not
const isHovered = ref(false)

// Amount of duplicates required to fill the container and provide a smooth animation
const duplicateBy = ref(1)

// Sizes of Container
const sizes = reactive({
	parent: 0,
	children: 0,
})

// First and last child to calculate the size properly
const firstChild = computed(() =&amp;gt; unrefElement(elementsOriginal.value[0])) as unknown as ComputedRef&amp;lt;HTMLElement&amp;gt;
const lastChild = computed(() =&amp;gt; unrefElement(elementsOriginal.value[elementsOriginal.value.length - 1])) as unknown as ComputedRef&amp;lt;HTMLElement&amp;gt;

// Get the padding on each side if required
const containerPadding = computed(() =&amp;gt; props.paddingOptions.perSide
	? `${props.paddingOptions.top}px ${props.paddingOptions.right}px ${props.paddingOptions.bottom}px ${props.paddingOptions.left}px`
	: `${props.paddingOptions.padding}px`
)

// Check if the container has children
const hasItems = computed(() =&amp;gt; {
	if (!containerItems.value) {
		return false
	}

	return containerItems.value.children?.length &amp;gt; 0
})

// Checks if the container is horizontal or vertical
const isHorizontal = computed(() =&amp;gt; props.direction === &apos;left&apos; || props.direction === &apos;right&apos;)

// Checks the direction of the animation
const direction = computed(() =&amp;gt; props.direction)

// The speed of the animation
const speed = computed(() =&amp;gt; Number(props.speed))

// The value to animate to, this a very important value because it determines the end of the animation
// Usually this is the size of the children + the size of the parent so lets say 3x multiplier
const animateToValue = computed(() =&amp;gt; sizes.children + sizes.children * Math.round(sizes.parent / sizes.children))

// Value that holds the main animation translate value
const xOrY = ref&amp;lt;number&amp;gt;(0)

// The initial time of the animation &amp;amp; time related values
const timeInitial = ref&amp;lt;number | null&amp;gt;(null)
const timePrevious = ref&amp;lt;number | null&amp;gt;(null)

// Store the frame of the animation via requestAnimationFrame
const animationFrame = ref&amp;lt;number | null&amp;gt;(null)

// Fade Options
const fadeMask = useFade(isHorizontal.value ? &apos;horizontal&apos; : &apos;vertical&apos;, props.fadeOptions)

// Since vue resets the manual styles on update, we will use reactive styles
const reactiveStyles = computed(() =&amp;gt; ({
	...containerStyle,
	gap: `${props.gap}px`,
	placeItems: props.alignment,
	position: &apos;relative&apos;,
	flexDirection: isHorizontal.value ? &apos;row&apos; : &apos;column&apos;,
	willChange: &apos;transform&apos;,
	top: props.direction === &apos;bottom&apos; ? `-${animateToValue.value}px` || 0 : undefined,
	left: props.direction === &apos;right&apos; ? `-${animateToValue.value}px` || 0 : undefined,
	perspective: &apos;1000px&apos;,
	backfaceVisibility: &apos;hidden&apos;,
})) as unknown as CSSProperties

/**
 * Measures the container and children
 */
const measure = () =&amp;gt; {
	// Must have items, container and first child
	if (!hasItems.value || !container.value || !containerItems.value) {
		return
	}

	// Must have first and last child
	if (!firstChild.value || !lastChild.value) {
		console.warn(&apos;First and last child must be present. Ensure the ticker has only one child and that child has one root&apos;)
		return
	}

	// Get the size of the parent
	const parentLength = isHorizontal.value ? container.value.offsetWidth : container.value.offsetHeight

	// Get the size of the first child
	const start = firstChild.value
		? (isHorizontal.value ? firstChild.value.offsetLeft : firstChild.value.offsetTop)
		: 0

	// Get The size of the last child
	const end = lastChild.value
		? (isHorizontal.value ? lastChild.value.offsetLeft + lastChild.value.offsetWidth : lastChild.value.offsetTop + lastChild.value.offsetHeight)
		: 0

	// Get the size of the children with the gap in mind
	const childrenLength = end - start + Number(props.gap)

	// Finally set the sizes
	sizes.parent = parentLength
	sizes.children = childrenLength

	if (sizes.parent &amp;lt;= 0 || sizes.children &amp;lt;= 0) {
		// console.warn(&apos;Unable to get parent or child size. Ensure the ticker has only one child and that child has one root&apos;)
		return
	}

	// Also update the duplication
	const duplicateResult = Math.round(sizes.parent / sizes.children * 2) + 1

	// If value is different and more then 0 and less then 200
	if (duplicateResult !== duplicateBy.value &amp;amp;&amp;amp; duplicateResult &amp;gt; 0 &amp;amp;&amp;amp; duplicateResult &amp;lt;= 200) {
		duplicateBy.value = duplicateResult
	}
}

const ticker = (animationTiming: number) =&amp;gt; {
	if (!containerItems.value) {
		return
	}

	// Set initial time if it&apos;s null
	if (timeInitial.value === null) {
		timeInitial.value = animationTiming
	}

	// Adjust time elapsed
	animationTiming -= timeInitial.value
	const timeSinceLast = timePrevious.value === null ? 0 : animationTiming - timePrevious.value
	let delta = timeSinceLast * (speed.value / 1e3)

	// If its hover we want to reduce the speed, so we divide by the hover factor
	if (isHovered.value) {
		delta = delta / Number(props.hoverFactor)
	}

	// Wrap the value so it doesn&apos;t go over the max
	xOrY.value = wrap(0, animateToValue.value, xOrY.value + delta)

	// Apply transform based on direction
	if (direction.value === &apos;left&apos;) {
		containerItems.value.style.transform = `translate3d(-${xOrY.value}px, 0, 0)`
	}

	if (direction.value === &apos;right&apos;) {
		containerItems.value.style.transform = `translate3d(${xOrY.value}px, 0, 0)`
	}

	if (direction.value === &apos;top&apos;) {
		containerItems.value.style.transform = `translate3d(0, -${xOrY.value}px, 0)`
	}

	if (direction.value === &apos;bottom&apos;) {
		containerItems.value.style.transform = `translate3d(0, ${xOrY.value}px, 0)`
	}

	// Point the previous time to the current time
	timePrevious.value = animationTiming
	// Request the next frame
	animationFrame.value = requestAnimationFrame(ticker)
}

/**
 * Starts the animation
 */
const animationStart = () =&amp;gt; {
	if (!containerItems.value) {
		return
	}
	// Initial Frame Request
	animationFrame.value = requestAnimationFrame(ticker)
}

/**
 * Stops the animation
 */
const animationStop = () =&amp;gt; animationFrame.value &amp;amp;&amp;amp; cancelAnimationFrame(animationFrame.value)

// Intersection Observer
const { stop: stopIntersectionObserver } = useIntersectionObserver(
	container,
	([{ isIntersecting }]) =&amp;gt; {
		if (isIntersecting) {
			animationStart()
		} else {
			animationStop()
		}
	}
)

// Await next tick to ensure the DOM is ready
tryOnMounted(() =&amp;gt; {
	nextTick(() =&amp;gt; {
		measure()
		animationStart()
	})
})

// Clear the animation frame on unmount
onUnmounted(() =&amp;gt; {
	animationStop()
	stopIntersectionObserver()
})

defineExpose({
	measure,
	translate: xOrY
})

&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
	&amp;lt;div
		ref=&quot;container&quot;
		data-ticker=&quot;true&quot;
		:class=&quot;{
			&apos;overflow-hidden&apos;: !props.fadeOptions.overflow,
			&apos;overflow-visible&apos;: props.fadeOptions.overflow,
		}&quot;
		:style=&quot;{
			...containerStyle,
			padding: containerPadding,
			maskImage: props.fadeOptions.content ? fadeMask : undefined,
		}&quot;
	&amp;gt;
		&amp;lt;ul
			ref=&quot;containerItems&quot;
			:style=&quot;reactiveStyles&quot;
			@mouseenter=&quot;isHovered = true&quot;
			@mouseleave=&quot;isHovered = false&quot;
		&amp;gt;
			&amp;lt;template
				v-for=&quot;(children, i) in $slots.default?.()[0]?.children ?? []&quot;
				:key=&quot;i&quot;
			&amp;gt;
				&amp;lt;x-ticker-item&amp;gt;
					&amp;lt;component
						:is=&quot;children&quot;
						ref=&quot;elementsOriginal&quot;
					/&amp;gt;
				&amp;lt;/x-ticker-item&amp;gt;
			&amp;lt;/template&amp;gt;
			&amp;lt;template
				v-for=&quot;z in duplicateBy&quot;
				:key=&quot;z&quot;
			&amp;gt;
				&amp;lt;template
					v-for=&quot;(children, i) in $slots.default?.()[0]?.children ?? []&quot;
					:key=&quot;i&quot;
				&amp;gt;
					&amp;lt;x-ticker-item&amp;gt;
						&amp;lt;component
							:is=&quot;children&quot;
							ref=&quot;elementsCloned&quot;
						/&amp;gt;
					&amp;lt;/x-ticker-item&amp;gt;
				&amp;lt;/template&amp;gt;
			&amp;lt;/template&amp;gt;
		&amp;lt;/ul&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;React Implementation ⚛️&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&apos;use client&apos;

import type { CSSProperties, ReactNode, RefObject, JSX, ReactElement, HTMLAttributes } from &apos;react&apos;
import { Children, useLayoutEffect, useEffect, useRef, useMemo, createRef, useState, useCallback, cloneElement } from &apos;react&apos;
import { RenderTarget } from &apos;framer&apos;
import { useAnimationFrame, useReducedMotion, LayoutGroup } from &apos;framer-motion&apos;
import { debounce } from &apos;lodash-es&apos;
import { wrap } from &apos;popmotion&apos;

interface SizingOptions {
  widthType: boolean;
  heightType: boolean;
}

interface FadeOptions {
  fadeContent: boolean;
  overflow: boolean;
  fadeWidth: number;
  fadeAlpha: number;
  fadeInset: number;
}

interface TransitionControl {
  type: string;
  ease: string;
  duration: number;
}

interface TickerProps extends HTMLAttributes&amp;lt;&apos;div&apos;&amp;gt;{
  slots: ReactElement[];
  gap: number;
  padding: number;
  paddingPerSide?: boolean;
  paddingTop?: number;
  paddingRight?: number;
  paddingBottom?: number;
  paddingLeft?: number;
  speed?: number;
  hoverFactor?: number;
  direction?: string | boolean | &apos;left&apos; | &apos;right&apos; | &apos;top&apos; | &apos;bottom&apos;;
  alignment?: string | &apos;flex-start&apos; | &apos;center&apos; | &apos;flex-end&apos;;
  sizingOptions?: SizingOptions;
  fadeOptions?: FadeOptions;
  transitionControl?: TransitionControl;
  style?: CSSProperties;
}

/* Placeholder Styles */
const containerStyle = {
  display: &apos;flex&apos;,
  width: &apos;100%&apos;,
  height: &apos;100%&apos;,
  maxWidth: &apos;100%&apos;,
  maxHeight: &apos;100%&apos;,
  placeItems: &apos;center&apos;,
  margin: 0,
  padding: 0,
  listStyleType: &apos;none&apos;,
  textIndent: &apos;none&apos;
}

/* Clamp function, used for fadeInset */
const clamp = (num: number, min: number, max: number) =&amp;gt; Math.min(Math.max(num, min), max)

export default function Ticker({
  gap = 10,
  padding = 10,
  paddingPerSide,
  paddingTop,
  paddingRight,
  paddingBottom,
  paddingLeft,
  speed = 10,
  hoverFactor = 5,
  direction = true,
  alignment = &apos;center&apos;,
  sizingOptions = {
    widthType: true,
    heightType: true
  },
  fadeOptions = {
    fadeContent: true,
    overflow: false,
    fadeWidth: 100,
    fadeAlpha: 0,
    fadeInset: 0
  },
  style,
  slots,
  className,
}: TickerProps) {

  const {
    fadeContent,
    overflow,
    fadeWidth,
    fadeInset,
    fadeAlpha
  } = fadeOptions

  const {
    widthType,
    heightType
  } = sizingOptions

  const paddingValue = paddingPerSide ? `${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px` : `${padding}px`

  /* Checks */
  const isCanvas = RenderTarget.current() === RenderTarget.canvas
  const hasChildren = Children.count(slots) &amp;gt; 0
  const isHorizontal = direction === &apos;left&apos; || direction === &apos;right&apos;

  /* Empty state */
  if (!hasChildren || !slots) {
    throw new Error(&apos;You must add at least one child to the Ticker component.&apos;)
  }

  /* Refs and State */
  const parentRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null)
  const childrenRef = useMemo&amp;lt;[RefObject&amp;lt;HTMLDivElement&amp;gt;, RefObject&amp;lt;HTMLDivElement&amp;gt;]&amp;gt;(() =&amp;gt; [createRef(), createRef()], [])
  const [size, setSize] = useState&amp;lt;{ parent: number; children: number }&amp;gt;({
    parent: 0,
    children: 0
  })

  /* Arrays */
  let clonedChildren: ReactNode[] | JSX.Element[]
  let dupedChildren: ReactNode[] | JSX.Element[] = []

  /* Duplicate value */
  let duplicateBy = 0
  let opacity = 0

  if (isCanvas) {
    duplicateBy = 20
    opacity = 1
  }

  if (!isCanvas &amp;amp;&amp;amp; hasChildren &amp;amp;&amp;amp; size.parent) {
    duplicateBy = Math.round(size.parent / size.children * 2) + 1
    opacity = 1
  }

  /* Measure parent and child */
  const measure = useCallback(() =&amp;gt; {
    if (hasChildren &amp;amp;&amp;amp; parentRef.current) {
      const parentLength = isHorizontal ? parentRef.current.offsetWidth : parentRef.current.offsetHeight
      const start = childrenRef[0].current ? isHorizontal ? childrenRef[0].current.offsetLeft : childrenRef[0].current.offsetTop : 0
      const end = childrenRef[1].current ? isHorizontal ? childrenRef[1].current.offsetLeft + childrenRef[1].current.offsetWidth : childrenRef[1].current.offsetTop + childrenRef[1].current.offsetHeight : 0
      const childrenLength = end - start + gap
      setSize({
        parent: parentLength,
        children: childrenLength
      })
    }
  }, [childrenRef, gap, hasChildren, isHorizontal])

  /* Add refs to first and last child */
  useLayoutEffect(() =&amp;gt; {
    !isCanvas &amp;amp;&amp;amp; measure()
  }, [isCanvas, measure])

  /**
  * Track whether this is the initial resize event. By default this will fire on mount,
  * which we do in the useEffect. We should only fire it on subsequent resizes.
  */
  let initialResize = useRef(true)

  useEffect(() =&amp;gt; {
    if (isCanvas || !parentRef.current) {
      return
    }

    const handleResize = debounce(() =&amp;gt; {
      if (!initialResize.current) {
        measure()
      }
      initialResize.current = false
    }, 1500) // 300 ms debounce time

    window.addEventListener(&apos;resize&apos;, handleResize)

    return () =&amp;gt; {
      window.removeEventListener(&apos;resize&apos;, handleResize)
    }
  }, [isCanvas, measure])

  // @ts-ignore
  clonedChildren = Children.map(slots, (child, index) =&amp;gt; {
    let selectedRef = null
    if (index === 0) {
      selectedRef = childrenRef[0]
    }

    if (index === slots.length - 1) {
      selectedRef = childrenRef[1]
    }

    return (
      &amp;lt;LayoutGroup inherit=&quot;id&quot;&amp;gt;
        &amp;lt;li
          style={{ display: &apos;contents&apos; }}
        &amp;gt;
          {cloneElement(child, {
            key: `cloned-child-${index}`,
            ref: selectedRef,
            style: {
              ...(child.props?.style),
              width: widthType ? child.props?.width : &apos;100%&apos;,
              height: heightType ? child.props?.height : &apos;100%&apos;,
              flexShrink: 0,
            },
          }, child.props?.children)}
        &amp;lt;/li&amp;gt;
      &amp;lt;/LayoutGroup&amp;gt;
    )
  })

  for (let i = 0; i &amp;lt; duplicateBy; i++) {
    dupedChildren = [
      ...dupedChildren,
      ...Children.map(slots, (child, childIndex) =&amp;gt; {
        return (
          &amp;lt;LayoutGroup inherit=&quot;id&quot; key={`duped-child-${i}-${childIndex}`}&amp;gt;
            &amp;lt;li style={{ display: &apos;contents&apos; }}&amp;gt;
              {cloneElement(child, {
                style: {
                  ...child.props.style,
                  width: widthType ? child.props.width : &apos;100%&apos;,
                  height: heightType ? child.props.height : &apos;100%&apos;,
                  flexShrink: 0
                }
              }, child.props.children)}
            &amp;lt;/li&amp;gt;
          &amp;lt;/LayoutGroup&amp;gt;
        )
      })
    ]
  }

  const animateToValue = size.children + size.children * Math.round(size.parent / size.children)
  const transformRef = useRef&amp;lt;HTMLUListElement&amp;gt;(null)
  const initialTime = useRef&amp;lt;number| null&amp;gt;(null)
  const prevTime = useRef&amp;lt;number | null&amp;gt;(null)
  const xOrY = useRef(0)
  const isHover = useRef(false)
  const isReducedMotion = useReducedMotion()

  useAnimationFrame(t =&amp;gt; {
    if (!transformRef.current || !animateToValue || isReducedMotion) {
      return
    }

    /**
     * In case this animation is delayed from starting because we&apos;re running a bunch
     * of other work, we want to set an initial time rather than counting from 0.
     * That ensures that if the animation is delayed, it starts from the first frame
     * rather than jumping.
     */
    if (initialTime.current === null) {
      initialTime.current = t
    }

    t = t - initialTime.current
    const timeSince = prevTime.current === null ? 0 : t - prevTime.current
    let delta = timeSince * (speed / 1e3)

    if (isHover.current) {
      delta *= hoverFactor
    }

    xOrY.current += delta
    xOrY.current = wrap(0, animateToValue, xOrY.current)
    /* Direction */

    if (direction === &apos;left&apos;) {
      transformRef.current.style.transform = `translateX(-${xOrY.current}px)`
    }

    if (direction === &apos;right&apos;) {
      transformRef.current.style.transform = `translateX(${xOrY.current}px)`
    }

    if (direction === &apos;top&apos;) {
      transformRef.current.style.transform = `translateY(-${xOrY.current}px)`
    }

    if (direction === &apos;bottom&apos;) {
      transformRef.current.style.transform = `translateY(${xOrY.current}px)`
    }

    prevTime.current = t
  })

  /* Fades */
  const fadeDirection = isHorizontal ? &apos;to right&apos; : &apos;to bottom&apos;
  const fadeWidthStart = fadeWidth / 2
  const fadeWidthEnd = 100 - fadeWidth / 2
  const fadeInsetStart = clamp(fadeInset, 0, fadeWidthStart)
  const fadeInsetEnd = 100 - fadeInset
  const fadeMask = `linear-gradient(${fadeDirection}, rgba(0, 0, 0, ${fadeAlpha}) ${fadeInsetStart}%, rgba(0, 0, 0, 1) ${fadeWidthStart}%, rgba(0, 0, 0, 1) ${fadeWidthEnd}%, rgba(0, 0, 0, ${fadeAlpha}) ${fadeInsetEnd}%)`

  return (
    &amp;lt;section
      className={className}
      data-ticker={true}
      ref={parentRef}
      style={{
        ...containerStyle,
        opacity,
        padding: paddingValue,
        WebkitMaskImage: fadeContent ? fadeMask : undefined,
        MozMaskImage: fadeContent ? fadeMask : undefined,
        maskImage: fadeContent ? fadeMask : undefined,
        overflow: overflow ? &apos;visible&apos; : &apos;hidden&apos;,
      } as CSSProperties}
    &amp;gt;
      &amp;lt;ul
        className={&apos;transform-gpu&apos;}
        ref={transformRef}
        style={{
          ...containerStyle,
          gap,
          top: direction === &apos;bottom&apos; ? -animateToValue || 0 : undefined,
          left: direction === &apos;right&apos; ? -animateToValue || 0 : undefined,
          placeItems: alignment,
          position: &apos;relative&apos;,
          flexDirection: isHorizontal ? &apos;row&apos; : &apos;column&apos;,
          willChange: &apos;transform&apos;,
          ...style,
        }}
        onMouseEnter={() =&amp;gt; (isHover.current = true)}
        onMouseLeave={() =&amp;gt; (isHover.current = false)}
      &amp;gt;
        &amp;lt;&amp;gt;
          {clonedChildren}
          {dupedChildren}
        &amp;lt;/&amp;gt;
      &amp;lt;/ul&amp;gt;
    &amp;lt;/section&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Fade Utility 🎨&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import type { MaybeRef } from &apos;vue&apos;
import { computed } from &apos;vue&apos;

export interface FadeOptions {
	content: boolean
	overflow: boolean
	width: number
	alpha: number
	inset: number
}

function clamp(value: number, min: number, max: number): number {
	return Math.min(Math.max(value, min), max)
}

// The useFade composable function
export function useFade(orientation: MaybeRef&amp;lt;&apos;horizontal&apos; | &apos;vertical&apos; | undefined&amp;gt;, fadeOptions: FadeOptions) {
	const orientationValue = toValue(orientation)
	const isHorizontal = computed(() =&amp;gt; orientationValue === &apos;horizontal&apos;)
	const fadeDirection = computed(() =&amp;gt; isHorizontal.value ? &apos;to right&apos; : &apos;to bottom&apos;)
	const fadeWidthStart = computed(() =&amp;gt; fadeOptions.width / 2)
	const fadeWidthEnd = computed(() =&amp;gt; 100 - fadeOptions.width / 2)
	const fadeInsetStart = computed(() =&amp;gt; clamp(fadeOptions.inset, 0, fadeWidthStart.value))
	const fadeInsetEnd = computed(() =&amp;gt; 100 - fadeOptions.inset)
	return computed(() =&amp;gt; `linear-gradient(${fadeDirection.value}, rgba(0, 0, 0, ${fadeOptions.alpha}) ${fadeInsetStart.value}%, rgba(0, 0, 0, 1) ${fadeWidthStart.value}%, rgba(0, 0, 0, 1) ${fadeWidthEnd.value}%, rgba(0, 0, 0, ${fadeOptions.alpha}) ${fadeInsetEnd.value}%)`)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage Tips 💡&lt;/h2&gt;
&lt;p&gt;Both implementations support the same core features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Direction&lt;/strong&gt;: &lt;code&gt;&apos;left&apos;&lt;/code&gt;, &lt;code&gt;&apos;right&apos;&lt;/code&gt;, &lt;code&gt;&apos;top&apos;&lt;/code&gt;, &lt;code&gt;&apos;bottom&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speed Control&lt;/strong&gt;: Adjustable animation speed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hover Effects&lt;/strong&gt;: Slowdown and pause on hover&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fade Options&lt;/strong&gt;: Customizable edge fading&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Built-in intersection observer&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Key Differences&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Vue Implementation&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Uses Vue 3 Composition API&lt;/li&gt;
&lt;li&gt;Leverages Vue&apos;s reactivity system&lt;/li&gt;
&lt;li&gt;Template-based rendering with slots&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;React Implementation&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Uses React hooks and [[Framer Motion|https://framer.com/motion]]&lt;/li&gt;
&lt;li&gt;Imperative ref-based DOM manipulation&lt;/li&gt;
&lt;li&gt;JSX with children cloning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both versions provide smooth, performant marquee animations perfect for modern web applications! 🚀&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Motion|https://motion.dev]]&lt;/li&gt;
&lt;li&gt;[[Framer|https://framer.com]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;How do I build a ticker or marquee component in Vue 3 and React?&quot;, answer: &quot;In Vue 3, use the Composition API with requestAnimationFrame to animate a container of cloned slot children, measuring parent and child dimensions on nextTick to calculate duplicates. In React, use Framer Motion&apos;s useAnimationFrame and popmotion&apos;s wrap function for the animation loop. Both implementations clone children to fill the container and use IntersectionObserver for performance.&quot; },
{ question: &quot;What is the difference between a marquee component and a carousel?&quot;, answer: &quot;A marquee or ticker displays continuously flowing content in an infinite loop, ideal for news feeds or sliding displays. A carousel shows a few items at once with navigation controls to move between pages. Marquees animate via requestAnimationFrame for smooth continuous movement, while carousels use discrete page transitions.&quot; },
{ question: &quot;How does the React ticker implementation use Framer Motion?&quot;, answer: &quot;The React implementation uses Framer Motion&apos;s useAnimationFrame hook for the animation loop, the LayoutGroup component for consistent layout during cloning, and popmotion&apos;s wrap function to seamlessly loop the animation value. It also uses useReducedMotion to respect accessibility preferences, and RenderTarget to handle Framer canvas vs browser rendering.&quot; },
{ question: &quot;Why use requestAnimationFrame instead of CSS animations for a marquee?&quot;, answer: &quot;Pure CSS marquee animations cause inconsistent behavior during hover slowdown effects. A requestAnimationFrame-based ticker provides precise speed interpolation when hovering (via a hoverFactor divisor), seamless infinite looping with a wrap function, and performance optimization through IntersectionObserver to pause when off-screen.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>vue</category><category>react</category><category>component</category><category>ticker</category><category>marquee</category><category>carousel</category><author>Pedro Martins</author></item><item><title>Tresjs + Vue - Holographic Sticker</title><link>https://nikuscs.com/blog/04-threejs-holo-sticker/</link><guid isPermaLink="true">https://nikuscs.com/blog/04-threejs-holo-sticker/</guid><description>A simple sticker effect using Tresjs for Vue as part of my 3D experiments with TreeJS</description><pubDate>Sun, 01 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import DemoVue from &quot;./demo.vue&quot;;
import DemoReact from &quot;./demo.tsx&quot;;&lt;/p&gt;
&lt;p&gt;Recently I saw a cool holographic sticker effect on Twitter made by &lt;a href=&quot;https://twitter.com/shinework/status/1775922778631245998&quot;&gt;Baptiste Adrien&lt;/a&gt;, and I decided to take a stab at replicating it using Vue and TresJS.
It turned out to be actually harder than I thought, but I&apos;m happy with the results so far.&lt;/p&gt;
&lt;p&gt;One of the things that still bugs me is that the React community has way more active contributors and libraries for ThreeJS, while Vue is still catching up.
I tried reaching out to the [[TresJS|https://tresjs.org]] Discord for help to see if I could find my way around it, but didn&apos;t have any luck.&lt;/p&gt;
&lt;p&gt;One of the challenges of this effect is the &lt;strong&gt;MeshTransmissionMaterial&lt;/strong&gt;, which is not available in TresJS. So I had to use the &lt;strong&gt;MeshPhysicalMaterial&lt;/strong&gt; from [[pmndrs/drei-vanilla|https://github.com/pmndrs/drei-vanilla]] and try to achieve the same effect using the available materials.
But not everything is lost - TresJS provides a good abstraction around ThreeJS, and I could get a result closer to the React version.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;MeshTransmissionMaterial&lt;/strong&gt; makes the base mesh transparent so it can reflect the background and items inside it, creating the desired effect.
With little knowledge about shaders and 3D, this was indeed a fun journey to learn more about it.&lt;/p&gt;
&lt;h2&gt;Vue + TresJS&lt;/h2&gt;
&lt;p&gt;The following demo uses TresJS + Vue. Notice that the badge absorbs way more light than the React version - I&apos;m still unsure why.&lt;/p&gt;
&lt;p&gt;&amp;lt;CraftBox&amp;gt;
&amp;lt;div class=&quot;flex items-center justify-center&quot;&amp;gt;
&amp;lt;DemoVue client:only=&quot;vue&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/CraftBox&amp;gt;&lt;/p&gt;
&lt;h3&gt;Vue implementation&lt;/h3&gt;
&lt;p&gt;But let&apos;s see the actual code! Here is the main scene for the Vue version.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { OrbitControls, Environment, useGLTF } from &quot;@tresjs/cientos&quot;;
import { TresCanvas } from &quot;@tresjs/core&quot;;
import Stickers from &quot;./sticker.vue&quot;;
import MeshTransmissionMaterial from &quot;./material.vue&quot;;
const { nodes } = await useGLTF(&quot;/assets/three-model.glb&quot;);
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;relative size-[300px] lg:size-[600px]&quot;&amp;gt;
    &amp;lt;TresCanvas style=&quot;width: 100%; height: 100%&quot; :alpha=&quot;true&quot; :antialias=&quot;true&quot; power-preference=&quot;high-performance&quot;
      :shadows=&quot;true&quot;&amp;gt;
      &amp;lt;suspense&amp;gt;
        &amp;lt;TresMesh :geometry=&quot;nodes.Object_4.geometry&quot; :rotation=&quot;[Math.PI / 2, 0, 0]&quot; :cast-shadow=&quot;true&quot;
          :receive-shadow=&quot;true&quot;&amp;gt;
          &amp;lt;MeshTransmissionMaterial /&amp;gt;
        &amp;lt;/TresMesh&amp;gt;
      &amp;lt;/suspense&amp;gt;

      &amp;lt;suspense&amp;gt;
        &amp;lt;stickers /&amp;gt;
      &amp;lt;/suspense&amp;gt;

      &amp;lt;TresAmbientLight :intensity=&quot;1.5&quot; /&amp;gt;
      &amp;lt;TresPerspectiveCamera visible :position=&quot;[0, 0, 6]&quot; :fov=&quot;45&quot; /&amp;gt;
      &amp;lt;OrbitControls :target=&quot;[0, 0, 0]&quot; /&amp;gt;
      &amp;lt;suspense&amp;gt;
        &amp;lt;Environment :files=&quot;&apos;/assets/three-env.hdr&apos;&quot; :blur=&quot;0&quot; :background=&quot;false&quot; /&amp;gt;
      &amp;lt;/suspense&amp;gt;
    &amp;lt;/TresCanvas&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here you can find the stickers inside the main scene:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
  import { useTexture } from &quot;@tresjs/core&quot;;
  import { DoubleSide } from &quot;three&quot;;
  const texture = await useTexture([&quot;/assets/three-sticker.png&quot;]);
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;suspense&amp;gt;
    &amp;lt;TresMesh&amp;gt;
      &amp;lt;TresPlaneGeometry /&amp;gt;
      &amp;lt;TresMeshPhysicalMaterial :map=&quot;texture&quot; :transparent=&quot;true&quot; :clearcoat=&quot;1&quot; :roughness=&quot;1&quot; :metalness=&quot;0.8&quot; :side=&quot;DoubleSide&quot; /&amp;gt;
    &amp;lt;/TresMesh&amp;gt;
  &amp;lt;/suspense&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here&apos;s the Transmission Material - a bit messy and buggy. If you&apos;re reading this and have a better solution, please let&apos;s get in touch! :p&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { shallowRef, onMounted, nextTick } from &quot;vue&quot;;
import { MeshTransmissionMaterial, MeshDiscardMaterial } from &quot;@pmndrs/vanilla&quot;;
import { useFBO } from &quot;@tresjs/cientos&quot;;
import { useRenderLoop, useTresContext } from &quot;@tresjs/core&quot;;
import { BackSide, DoubleSide } from &quot;three&quot;;
import type { TresObject } from &quot;@tresjs/core&quot;;
import type { Camera, Texture, WebGLRenderTarget } from &quot;three&quot;;
import type { Ref } from &quot;vue&quot;;

const MeshTransmissionMaterialClass = shallowRef();
const { extend, scene, renderer, camera } = useTresContext();
const parent = shallowRef&amp;lt;TresObject&amp;gt;();
const backside = true;
const backsideThickness = 0;
const thickness = 0;
const backsideEnvMapIntensity = 0;
const fboResolution = 1500;

extend({ MeshTransmissionMaterial });

/**
 * Finds the parent mesh using the specified material UUID.
 *
 * @param {THREE.Scene} scene - The Three.js scene to search.
 * @param {string} materialUuid - The UUID of the material.
 * @returns {THREE.Mesh | undefined} - The mesh using the material, or undefined if not found.
 */
function findMeshByMaterialUuid(scene: TresObject, materialUuid: string): TresObject {
  let foundMesh;

  scene.traverse((object: TresObject) =&amp;gt; {
    if (object.isMesh &amp;amp;&amp;amp; object.material &amp;amp;&amp;amp; object.material.uuid === materialUuid) {
      foundMesh = object;
    }
  });

  return foundMesh as unknown as TresObject;
}

const discardMaterial = new MeshDiscardMaterial();

const { onLoop } = useRenderLoop();

onMounted(async () =&amp;gt; {
  await nextTick();
  parent.value = findMeshByMaterialUuid(scene.value as unknown as TresObject, MeshTransmissionMaterialClass.value.uuid);
});

const fboBack = useFBO({
  width: fboResolution,
  height: fboResolution,
}) as unknown as Ref&amp;lt;WebGLRenderTarget&amp;lt;Texture&amp;gt;&amp;gt;;
const fboMain = useFBO({
  width: fboResolution,
  height: fboResolution,
}) as unknown as Ref&amp;lt;WebGLRenderTarget&amp;lt;Texture&amp;gt;&amp;gt;;

let oldBg;
let oldEnvMapIntensity;
let oldTone;

onLoop(({ elapsed }) =&amp;gt; {
  MeshTransmissionMaterialClass.value.time = elapsed;

  if (MeshTransmissionMaterialClass.value.buffer === fboMain.value.texture) {
    if (parent.value) {
      // Save defaults
      oldTone = renderer.value.toneMapping;
      oldBg = scene.value.background;
      oldEnvMapIntensity = MeshTransmissionMaterialClass.value.envMapIntensity;

      parent.value.material = discardMaterial;

      if (backside) {
        // Render into the backside buffer
        renderer.value.setRenderTarget(fboBack.value);
        renderer.value.render(scene.value, camera.value as Camera);
        // And now prepare the material for the main render using the backside buffer
        parent.value.material = MeshTransmissionMaterialClass.value;
        parent.value.material.thickness = backsideThickness;
        parent.value.material.side = BackSide;
        parent.value.material.envMapIntensity = backsideEnvMapIntensity;
      }

      // Render into the main buffer
      renderer.value.setRenderTarget(fboMain.value);
      renderer.value.render(scene.value, camera.value as Camera);

      parent.value.material = MeshTransmissionMaterialClass.value;
      parent.value.material.thickness = thickness;
      parent.value.material.side = DoubleSide;
      // TODO: For some reason this makes the material really shinny, and i dont know why.
      // parent.value.material.buffer = fboMain.value.texture
      parent.value.material.envMapIntensity = oldEnvMapIntensity;

      // Set old state back
      scene.value.background = oldBg;
      renderer.value.setRenderTarget(null);
      renderer.value.toneMapping = oldTone;
    }
  }
});

defineExpose({
  root: MeshTransmissionMaterialClass,
  constructor: MeshTransmissionMaterial,
});
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;TresMeshTransmissionMaterial ref=&quot;MeshTransmissionMaterialClass&quot; :buffer=&quot;fboMain.texture&quot; :transmission=&quot;0&quot;
    :_transmission=&quot;1&quot; :anisotropic-blur=&quot;0.1&quot; :thickness=&quot;0&quot; :side=&quot;DoubleSide&quot; /&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;React Three Fiber Implementation&lt;/h2&gt;
&lt;p&gt;And here you can find the same demo but with React Three Fiber. While the Vue implementation looks really close to this one,
I cannot seem to get the same results. Maybe I&apos;m missing some configuration, or it&apos;s just the way React Three Fiber handles the materials and defaults.&lt;/p&gt;
&lt;p&gt;&amp;lt;CraftBox&amp;gt;
&amp;lt;div class=&quot;flex items-center justify-center&quot;&amp;gt;
&amp;lt;DemoReact client:only=&quot;react&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/CraftBox&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Environment, MeshTransmissionMaterial, OrbitControls, useGLTF, useTexture } from &apos;@react-three/drei&apos;
import { Canvas } from &apos;@react-three/fiber&apos;
import { DoubleSide } from &apos;three&apos;

const Stickers = ({ texturePath }: { texturePath: string }) =&amp;gt; {
  const texture = useTexture(texturePath)
  return (
    &amp;lt;&amp;gt;
      &amp;lt;mesh&amp;gt;
        &amp;lt;planeGeometry /&amp;gt;
        &amp;lt;meshPhysicalMaterial
          map={texture}
          transparent
          clearcoat={1}
          roughness={0}
          side={DoubleSide}
        /&amp;gt;
      &amp;lt;/mesh&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export interface SceneProps {
  modelPath: string
  texturePath: string
}

export const Scene = ({ modelPath, texturePath }: SceneProps) =&amp;gt; {
  const { nodes } = useGLTF(modelPath)

  return (
    &amp;lt;div className=&quot;relative size-[300px] lg:size-[600px]&quot;&amp;gt;
      &amp;lt;Canvas
        shadows
        camera={{ position: [0, 0, 6], fov: 45 }}
      &amp;gt;
        &amp;lt;OrbitControls
          target={[0, 0, 0]}
        /&amp;gt;
        &amp;lt;group dispose={null}&amp;gt;
          &amp;lt;mesh
            castShadow
            receiveShadow
            geometry={nodes.Object_4.geometry}
            rotation={[Math.PI / 2, 0, 0]}
          &amp;gt;
            &amp;lt;MeshTransmissionMaterial /&amp;gt;
          &amp;lt;/mesh&amp;gt;
        &amp;lt;/group&amp;gt;

        &amp;lt;Stickers texturePath={texturePath} /&amp;gt;

        &amp;lt;ambientLight intensity={2} /&amp;gt;
        &amp;lt;Environment preset=&quot;city&quot; blur={0} /&amp;gt;
      &amp;lt;/Canvas&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once again, such a nice challenge and learning experience! I hope you enjoyed this post, and if you have any suggestions or tips, please let me know!
All credits to Baptiste Adrien and Vercel for the amazing idea!&lt;/p&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Hybridly|https://hybridly.dev/]]&lt;/li&gt;
&lt;li&gt;[[TresJS|https://docs.tresjs.org]]&lt;/li&gt;
&lt;li&gt;[[Vue|https://vuejs.org]]&lt;/li&gt;
&lt;li&gt;[[Three.js|https://threejs.org]]&lt;/li&gt;
&lt;li&gt;[[React Three Fiber|https://docs.pmnd.rs/react-three-fiber]]&lt;/li&gt;
&lt;li&gt;[[Baptiste Adrien|https://twitter.com/shinework/status/1775922778631245998]]&lt;/li&gt;
&lt;li&gt;[[pmndrs/drei-vanilla|https://github.com/pmndrs/drei-vanilla]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;How do I create a holographic sticker effect with TresJS and Vue?&quot;, answer: &quot;Load a 3D model with useGLTF, apply MeshPhysicalMaterial for the sticker texture, and use a custom MeshTransmissionMaterial port from drei-vanilla for the glass-like refraction. TresJS doesn&apos;t have MeshTransmissionMaterial built in, so you need to extend the renderer and handle FBO rendering manually via useRenderLoop.&quot; },
{ question: &quot;How does TresJS compare to React Three Fiber for Three.js development?&quot;, answer: &quot;TresJS is a Vue-based abstraction for Three.js using the Cientos helper library, while React Three Fiber is the React equivalent with the drei ecosystem. React Three Fiber has more community-contributed materials like MeshTransmissionMaterial available out of the box. TresJS provides a Vue-friendly API but has a smaller ecosystem for advanced materials.&quot; },
{ question: &quot;What is MeshTransmissionMaterial and how does it differ from MeshPhysicalMaterial?&quot;, answer: &quot;MeshTransmissionMaterial from the pmndrs/drei library simulates glass-like transparency with refraction, making meshes transparent while reflecting backgrounds and internal objects. MeshPhysicalMaterial is a standard Three.js material with clearcoat and roughness properties. The transmission material produces more realistic glass effects but requires FBO rendering setup.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>threejs</category><category>vue</category><category>experiments</category><category>3d</category><author>Pedro Martins</author></item><item><title>Speed up any WordPress site in 6 easy steps</title><link>https://nikuscs.com/blog/01-speed-up-wordpress/</link><guid isPermaLink="true">https://nikuscs.com/blog/01-speed-up-wordpress/</guid><description>In this article we will explore some quick ways to optimize WordPress websites and make them load faster.</description><pubDate>Sun, 21 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;[[WordPress|https://wordpress.org]] is an amazing tool that has powered millions of websites worldwide for years.&lt;/p&gt;
&lt;p&gt;When building a WordPress website for yourself or a client, it&apos;s important to understand that WordPress was initially created for blogging! Everything else that adds extra features to the WordPress core comes in the format of &quot;plugins&quot; - small collections of PHP, CSS, and JavaScript files that integrate into your website. But if you&apos;re a bit of a geek, you probably already know all this, so let&apos;s move forward on how you can optimize your WordPress website using 6 essential techniques.&lt;/p&gt;
&lt;h2&gt;Contents&lt;/h2&gt;
&lt;h2&gt;1 — Cloudflare 🚒&lt;/h2&gt;
&lt;p&gt;The first step everyone should be using is [[Cloudflare|https://cloudflare.com]]! Cloudflare is a DNS manager and CDN that brings many useful features that people aren&apos;t aware of. When creating websites for clients or myself, I always make sure Cloudflare is a requirement!&lt;/p&gt;
&lt;p&gt;Here we&apos;ll introduce some basic settings you should enable and configure on the Cloudflare website to improve your WordPress website.&lt;/p&gt;
&lt;p&gt;Cloudflare offers 3 different ways of SSL configuration. You should always make sure your hosting/VPS provider gives you at least LetsEncrypt free SSL certificates (if not, you should switch ASAP! [[Laravel Forge|https://forge.laravel.com]] might be a good choice to provision your VPS).&lt;/p&gt;
&lt;p&gt;We&apos;ll provide screenshots of our standard configuration for WordPress setups:&lt;/p&gt;
&lt;h3&gt;1.1 — HTTPS / SSL ✅&lt;/h3&gt;
&lt;p&gt;In this step, make sure a certificate is already installed on your VPS/hosting. If you&apos;re unable to generate a certificate, temporarily turn off Cloudflare SSL and try again.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;You can now move forward to the next tab:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;1.2 — Firewall Rules 🔥&lt;/h3&gt;
&lt;p&gt;The second step on Cloudflare is to set up some basic firewall rules. This will keep most automated bots and brute-force attackers away from your website, ensuring these attacks don&apos;t affect your server&apos;s performance. The great advantage over using plugins like iTheme Security or Wordfence is that Cloudflare acts at the DNS level, so these attacks will never reach you because they&apos;re blocked at a higher level. Keep in mind that the rules we&apos;ll show here are basic - some websites may require more or fewer tweaks based on your needs.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;At this point, you can also add more rules based on User Agent, URL, and so on. Some of the most common rules to keep attackers away:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User Agent contains: &lt;code&gt;masscan/1.0&lt;/code&gt;, &lt;code&gt;python-requests&lt;/code&gt;, &lt;code&gt;WPScan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;URL contains: &lt;code&gt;backups&lt;/code&gt;, &lt;code&gt;.bak&lt;/code&gt;, &lt;code&gt;backup&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;setup.php&lt;/code&gt;, &lt;code&gt;phpmyadmin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hit Save and you&apos;re ready to go! This will ensure that anyone (yes, even yourself) will need to solve a captcha when accessing any page that contains &lt;code&gt;wp-admin&lt;/code&gt; or &lt;code&gt;wp-login&lt;/code&gt; in the URL.&lt;/p&gt;
&lt;p&gt;Why is this so effective? Because hackers often try to brute force those standard pages by default, so you&apos;ll save a lot of CPU and RAM by keeping them away!&lt;/p&gt;
&lt;h2&gt;2 — WP Rocket 🚀&lt;/h2&gt;
&lt;p&gt;I&apos;ve tried many different plugins for WordPress caching, minification, and other performance optimizations - [[W3 Total Cache|https://wordpress.org/plugins/w3-total-cache/]], [[WP Super Cache|https://wordpress.org/plugins/wp-super-cache/]], and so on. But I always found trouble at some point: some required extensive configuration, breaking styles, JavaScript, and more.&lt;/p&gt;
&lt;p&gt;I&apos;m a big fan of &quot;keep it simple,&quot; so here&apos;s where [[WP Rocket|https://wp-rocket.me]] shines! With a few clicks and toggles, you can get your website optimized without much technical knowledge, while being compatible with most themes without any issues. Some nice features we use in WP Rocket:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cache / Separated mobile cache&lt;/li&gt;
&lt;li&gt;Heartbeat Control (this really has a huge impact on CPU)&lt;/li&gt;
&lt;li&gt;Lazy Load &amp;amp; Image Optimization&lt;/li&gt;
&lt;li&gt;Merge / Minify JavaScript&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Keep in mind that if you&apos;re using Cloudflare to minify your CSS/HTML/JavaScript, you don&apos;t really need to toggle the minify options - otherwise, you&apos;ll be doing the same optimization twice. At this point, I prefer to let Cloudflare handle it. You also need to be careful and add URLs you want to exclude from caching. By default, [[WooCommerce|https://woocommerce.com]] URLs are ignored, but others such as wish lists should be excluded manually.&lt;/p&gt;
&lt;h2&gt;3 — Redis, Redis &amp;amp; Redis! 🚤&lt;/h2&gt;
&lt;p&gt;Yes! You might think it&apos;s overkill, and yes it could be if your site doesn&apos;t have major traffic. But if your website runs WooCommerce and has a lot of products or heavy traffic, a nice add-on is [[Redis Object Cache|https://wordpress.org/plugins/redis-cache/]].&lt;/p&gt;
&lt;p&gt;Redis Object Cache also has an enterprise version that claims to add even more performance (I haven&apos;t tried it myself). If you don&apos;t know what [[Redis|https://redis.io]] is, you can read more on Google. I won&apos;t go into detail here, but basically Redis is a fast object cache available for many languages and platforms. The port for PHP is called predis or phpredis, which should be available as a PHP extension on your hosting provider or via Composer.&lt;/p&gt;
&lt;p&gt;When installed on WordPress through the WP Redis addon, it ensures that repeated queries and objects will be blazing fast as they&apos;re cached in memory, so you&apos;ll save some heavy queries to your MySQL database.&lt;/p&gt;
&lt;p&gt;If all goes well, after some time you should see metrics like the following screenshot:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;4 — Picking the Right Theme 🎨&lt;/h2&gt;
&lt;p&gt;There&apos;s no doubt that there are tons of themes available on the WordPress market. But this is a decision you should take very seriously - don&apos;t rush into picking the first theme that looks good to you. A bad theme (in terms of programming) can ruin not only your server but your user experience.&lt;/p&gt;
&lt;p&gt;Often programmers take shortcuts and don&apos;t really look at their code quality. Most users don&apos;t understand how to evaluate a good theme when purchasing, so here we&apos;ll give you a few tips to find out if the theme is well-made and fast!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Look at HTML markup organization&lt;/strong&gt;: Often bad markup and styling means the developer didn&apos;t put in much time, which likely means they don&apos;t care about what&apos;s under the hood.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check CSS and JavaScript load&lt;/strong&gt;: Inspect the code to see how much CSS and JavaScript are being included. The more JavaScript and CSS loaded, the more bloat and slower your site will be. Keep it clean with fewer dependencies.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Test responsive design&lt;/strong&gt;: Try the theme in responsive mode or on your phone before you buy it. Often developers make themes look good on desktop, but the mobile experience could be really bad. It&apos;s always good to check how it looks on mobile - if you find little issues here and there, the developer might not be the best and didn&apos;t put much effort into the mobile-first concept. This means you should probably avoid it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check reviews&lt;/strong&gt;: This is really important! If you visit [[ThemeForest|https://themeforest.net]], you can look at comments and reviews to see if other people who purchased the theme are having issues. If there are many comments/questions/problems, it usually means the theme is buggy - otherwise people wouldn&apos;t complain or call out the developer/agency so much.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Give preference to Elementor Page Builder&lt;/strong&gt;: Why? Because it&apos;s the modern page editor for WordPress. [[WP Bakery|https://wpbakery.com]] is outdated (not officially, but in my opinion), while [[Elementor|https://elementor.com]] is a lightweight editor that provides flexibility when customizing your pages in the future. Visual Composer (formerly WP Bakery) gave me some really bad memories, so I don&apos;t recommend it personally.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5 — Find Good Hosting &amp;amp; Prefer a VPS 🏗️&lt;/h2&gt;
&lt;p&gt;With more bloat every day in WordPress Core and plugins around it, WordPress websites tend to be CPU-heavy. Often customers on shared hosting will struggle with very slow response times. The reason is that most shared hosting providers cap your CPU usage because the actual CPU is being used by thousands of other customers. For this reason, I&apos;d avoid using WordPress (especially if paired with WooCommerce) on shared hosting if you want to keep your website blazing fast.&lt;/p&gt;
&lt;p&gt;My recommendation goes for a small/medium VPS. You don&apos;t need to be a sysadmin to set up a VPS - you can use services like [[Laravel Forge|https://forge.laravel.com]] or [[Ploi.io|https://ploi.io]] to provision your VPS in a few minutes with very little effort. Keep in mind that VPS won&apos;t include any cPanel (unless you pay for it).&lt;/p&gt;
&lt;p&gt;There are also some nice solutions for managed hosting dedicated to WordPress, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[[WP Engine|https://wpengine.com]]&lt;/li&gt;
&lt;li&gt;[[Kinsta|https://kinsta.com]]&lt;/li&gt;
&lt;li&gt;[[SiteGround|https://www.siteground.com]]&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6 — Battle with the Unknown &amp;amp; Debug 👻&lt;/h2&gt;
&lt;p&gt;Sometimes you may have done everything but you still don&apos;t know why your site is buggy or slow! This could be happening for many reasons that we&apos;ll describe below. Of course, we can&apos;t cover everything, but these tools will be really handy when it comes to debugging and tracking what could possibly be making your WordPress website slow!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Disable plugins one by one&lt;/strong&gt; (Standard engineer fix! 😃) - keep testing if you notice any difference after disabling a certain plugin.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[[Query Monitor|https://wordpress.org/plugins/query-monitor/]]&lt;/strong&gt;: This is one of the best plugins when it comes to debugging. It will tell you about queries running, slow queries, requests going out, and a lot of useful info. For instance, you can check what plugins are hitting your database really hard, causing your website to be slow. The Swiss Army knife for WordPress owners &amp;amp; maintainers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[[Snitch|https://wordpress.org/plugins/snitch/]]&lt;/strong&gt;: What I love personally about this plugin is you can keep track of what&apos;s going OUT from your WordPress website and block certain files/URLs. This is pretty nice because you can block constant plugins from slowing down your website by sending requests for updates every minute, blocking URLs if they&apos;re taking too long to respond, and block &amp;amp; track analytics being sent without your knowledge.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Verify file integrity&lt;/strong&gt; with plugins like [[Wordfence|https://wordpress.org/plugins/wordfence/]]. This is really handy if you believe some of your WordPress core files were changed. Sometimes hackers hide deep in WordPress core files, so it&apos;s always good to run a quick check every few weeks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Keep your plugins up-to-date&lt;/strong&gt;. Often plugins bring performance improvements, so it&apos;s always good to keep them updated - just make sure it won&apos;t break anything and always perform a backup first!&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Disable WP_CRON&lt;/strong&gt;: The WordPress cron is triggered every time a user hits a page (when necessary). It&apos;s good practice to disable &lt;code&gt;WP_CRON&lt;/code&gt; whenever possible and instead use Crontab available on most servers running cPanel or Linux. This ensures no extra workload is being called unnecessarily.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I hope you enjoyed this article - leave your comment and clap on! 👏&lt;/p&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;How does Cloudflare help speed up a WordPress website?&quot;, answer: &quot;Cloudflare operates at the DNS level, providing CDN caching, SSL management, and firewall rules that block bots and brute-force attempts before they reach your server. By adding rules for wp-admin and wp-login URLs, attackers are stopped at the DNS level, saving significant CPU and RAM on your hosting.&quot; },
{ question: &quot;How does WP Rocket compare to other WordPress caching plugins?&quot;, answer: &quot;WP Rocket provides caching, minification, lazy loading, and heartbeat control with minimal configuration compared to W3 Total Cache or WP Super Cache. It works well with most themes out of the box and offers a simpler setup process, though you should avoid double-minifying if Cloudflare already handles minification.&quot; },
{ question: &quot;How does Redis Object Cache improve WordPress performance?&quot;, answer: &quot;Redis Object Cache stores repeated database queries and objects in memory, reducing the load on your MySQL database. This is especially useful for WooCommerce sites with many products or heavy traffic. The WordPress plugin connects to Redis via the predis or phpredis PHP extension.&quot; },
{ question: &quot;How can Query Monitor and Snitch help debug a slow WordPress site?&quot;, answer: &quot;Query Monitor shows slow database queries, outgoing requests, and which plugins are hitting your database hardest. Snitch tracks outgoing requests from your WordPress site and lets you block plugins that constantly send update requests. Together they help identify which plugins or queries are causing performance issues.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>wordpress</category><category>speed-optimization</category><author>Pedro Martins</author></item><item><title>Building a Proxy Forwarder with 3Proxy, Squid &amp; Laravel</title><link>https://nikuscs.com/blog/00-proxy-forward-and-laravel-dashboard/</link><guid isPermaLink="true">https://nikuscs.com/blog/00-proxy-forward-and-laravel-dashboard/</guid><description>Learn how to build a secure proxy forwarder using 3Proxy and Squid with Laravel. We&apos;ll cover proxy setup, authentication, and request redirection with a complete dashboard solution.</description><pubDate>Tue, 26 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently I needed to re-route, transform, and redirect proxies. The challenge was: how could I hide my real proxy behind another proxy by redirecting requests? Or could I set up a simple authentication method? In this article we&apos;ll cover:&lt;/p&gt;
&lt;h2&gt;Key Concepts &amp;amp; Terminology 📚&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Inbound Port&lt;/strong&gt; — Port that accepts incoming traffic, typically used on your device (ex: 192.168.111.2:8080)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outbound Port&lt;/strong&gt; — Port that redirects traffic to the destination server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inbound IP/Domain&lt;/strong&gt; — The IP or domain that receives connections from your device&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outbound IP/Domain&lt;/strong&gt; — The destination IP or domain that receives the redirected traffic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Template Variables&lt;/strong&gt; — Wildcards like &lt;code&gt;{{outbound_port}}&lt;/code&gt; or &lt;code&gt;{{xxxx}}&lt;/code&gt; that get replaced dynamically by scripts (PHP str_replace) with actual ports and IPs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Architecture Overview 🏗️&lt;/h2&gt;
&lt;p&gt;The diagram below shows how we mask the original proxy by creating a forwarding proxy that redirects traffic to the real proxy server.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Squid Proxy Setup 🦑&lt;/h2&gt;
&lt;h3&gt;What is Squid?&lt;/h3&gt;
&lt;p&gt;[[Squid|https://www.squid-cache.org]] is one of the most widely used and reliable proxy solutions. It provides proxying, caching, authentication, user groups, and more. I didn&apos;t have time to explore all features, but at least it&apos;s not made by Russians! 😄 (joking of course).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Installation Guide:&lt;/strong&gt; Learn how to install Squid &lt;a href=&quot;https://phoenixnap.com/kb/setup-install-squid-proxy-server-ubuntu&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Documentation for Squid can be sparse online. What I do know is that Squid relies on a single configuration file:&lt;/p&gt;
&lt;h3&gt;Squid Configuration Template 📝&lt;/h3&gt;
&lt;p&gt;Here&apos;s a sample Squid configuration file showing how to set up a proxy. You can find more about the configuration &lt;a href=&quot;http://www.squid-cache.org/Doc/config/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The key difference is this template uses placeholders that [[Laravel|https://laravel.com]] (or any language) can replace dynamically:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;header_replace Accept-Encoding gzip
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwd
auth_param basic credentialsttl 999 minutes
auth_param basic casesensitive on
auth_param basic realm easyshit
acl ncsa proxy_auth REQUIRED
follow_x_forwarded_for deny all
forwarded_for delete
via off

# Proxy Redirect Line
{{lines}}

# Squid Default Config
acl SSL_ports port 443
acl Safe_ports port 80		# http
acl Safe_ports port 21		# ftp
acl Safe_ports port 443		# https
acl Safe_ports port 70		# gopher
acl Safe_ports port 210		# wais
acl Safe_ports port 1025-65535	# unregistered ports
acl Safe_ports port 280		# http-mgmt
acl Safe_ports port 488		# gss-http
acl Safe_ports port 591		# filemaker
acl Safe_ports port 777		# multiling http
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost manager
http_access deny manager
http_access allow localhost
http_access allow ncsa
coredump_dir /var/spool/squid
refresh_pattern ^ftp:		1440	20%	10080
refresh_pattern ^gopher:	1440	0%	1440
refresh_pattern -i (/cgi-bin/|\?) 0	0%	0
refresh_pattern (Release|Packages(.gz)*)$      0       20%     2880
refresh_pattern .		0	20%	4320

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Single Proxy Redirect Configuration&lt;/h3&gt;
&lt;p&gt;Here&apos;s the configuration for a single proxy redirect line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Authentication for User with Port {{inbound_port}} with Reference #{{reference}}
# The user is being routed to {{outgoing_ip}}:{{outgoing_port}}
{{permissions}}
http_port {{inbound_port}} name=port_{{inbound_port}}
acl port_{{inbound_port}}_acl myportname port_{{inbound_port}}
always_direct deny port_{{inbound_port}}_acl
never_direct allow port_{{inbound_port}}_acl
cache_peer {{outgoing_ip}} parent {{outgoing_port}} 0 no-query {{outgoing_auth}} name=proxy{{inbound_port}}
cache_peer_access proxy{{inbound_port}} allow port_{{inbound_port}}_acl
cache_peer_access proxy{{inbound_port}} deny all
http_access allow localhost auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More information can be found on the [[Squid Documentation|http://www.squid-cache.org/Doc/]] page, but I found it confusing, so I searched extensively to get this proper config. Thanks also to some friends in the proxy business who lent me a hand 🙂.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This configuration uses wildcards that will be replaced dynamically.&lt;/p&gt;
&lt;h2&gt;3Proxy Setup ⚙️&lt;/h2&gt;
&lt;h3&gt;How 3Proxy Works&lt;/h3&gt;
&lt;p&gt;Unlike Squid, [[3proxy|https://github.com/z3APA3A/3proxy]] runs as a simple bash process that serves as our connection listener. A process runs on the machine as the connection listener. When we no longer need it, we simply kill the process and the proxy/routing stops - this reduces management overhead significantly.&lt;/p&gt;
&lt;p&gt;The examples below show:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;HTTP to HTTP proxy&lt;/li&gt;
&lt;li&gt;HTTP to SOCKS5 proxy&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Remember: wildcards in &lt;code&gt;{{}}&lt;/code&gt; format get replaced with actual values via PHP scripts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Installation Guide:&lt;/strong&gt; Learn how to install 3proxy &lt;a href=&quot;https://github.com/SnoyIatK/3proxy&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Simply run/kill this process to start/stop proxy routing.&lt;/p&gt;
&lt;h3&gt;3Proxy Configuration Template 📄&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# Subscription: {{ $SUBSCRIPTION_REFERENCE }}
# Proxy: {{ $PROXY_REFERENCE }}
# Allocated port: {{ $ALLOCATED_PORT }}
# The user is being routed to {{ $ORIGINAL_HOST }}:{{ $ORIGINAL_PORT }}

daemon
pidfile /var/run/{{ $PID }}.pid
nserver {{ $PROXY3_DNS_1 }}
nserver {{ $PROXY3_DNS_2 }}
nscache 65536
nscache6 65536
timeouts 1 5 30 60 180 1800 15 60
deny * * 127.0.0.1,192.168.1.1
# Admin for this proxy, should be common for all users
auth strong
users {{ $PROXY3_ADMIN_USERNAME }}:CL:{{ $PROXY3_ADMIN_PASSWORD }}
allow {{ $PROXY3_ADMIN_USERNAME }} * * *
flush

# Build the user line
maxconn {{ $MAX_CONNECTIONS }}
auth strong
users {{ $ALLOCATED_USERNAME }}:CL:{{ $ALLOCATED_PASSWORD }}
allow {{ $ALLOCATED_USERNAME }} * {{ $ALLOWED_DOMAINS }} 80-88,8080-8088,443 HTTP,HTTPS
parent 1000 {{ $ORIGINAL_PROTOCOL }} {{ $ORIGINAL_HOST }} {{ $ORIGINAL_PORT }} {{ $ORIGINAL_USERNAME }} {{ $ORIGINAL_PASSWORD }}
proxy -4 -olSO_REUSEADDR,SO_REUSEPORT -ocTCP_TIMESTAMPS,TCP_NODELAY -osTCP_NODELAY -n -a -p{{ $ALLOCATED_PORT }}
flush
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Auto-Start Script 🔄&lt;/h3&gt;
&lt;p&gt;A small bash script can automate the process startup. The &lt;code&gt;{{proxy3_bin}}&lt;/code&gt; is the path to your 3proxy binary and the &lt;code&gt;{{proxy_config}}&lt;/code&gt; is the path to your .conf file (mentioned above). We also call Laravel Artisan command to verify the proxy is still valid. Our backend handles the necessary commands to delete config files and terminate processes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh
if ps -aux | grep &quot;3[p]roxy&quot; | grep &quot;{{proxy3_bin_grep}} {{proxy3_config}}&quot; &amp;gt; /dev/null 2&amp;gt;&amp;amp;1;
then
echo &apos;Running {{reference}}&apos;;
else
{{proxy3_bin}} {{proxy3_config}} &amp;amp;
fi
/usr/bin/php -f {{artisan}} check:order {{reference}}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Laravel Helper Class 🔧&lt;/h2&gt;
&lt;h3&gt;Squid &amp;amp; 3Proxy Helper&lt;/h3&gt;
&lt;p&gt;The following PHP Class is just a helper I&apos;ve created to automatically update/generate config files on demand (from database in this case). This doesn&apos;t work out of the box and requires additional configuration. I&apos;m providing just a proof of concept, not the complete code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
/**
  * Replaces the keys and generates the user rules
  *
  * @return mixed
  * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
  */
private function getProxyConfig(): string
{
    $keys = [
        # Inbound
        &apos;{{inbound_port}}&apos; =&amp;gt; $this-&amp;gt;getInbound()-&amp;gt;getPort(),
        &apos;{{inbound_username}}&apos; =&amp;gt; $this-&amp;gt;getInbound()-&amp;gt;getAuthUsername(),
        &apos;{{inbound_password}}&apos; =&amp;gt; $this-&amp;gt;getInbound()-&amp;gt;getAuthPassword(),
        &apos;{{inbound_allowed_urls}}&apos; =&amp;gt; &apos;*&apos;, // Comma delimited instagram.com,*.instagram.com
        # Outbound
        &apos;{{outgoing_ip}}&apos; =&amp;gt; $this-&amp;gt;getOutbound()-&amp;gt;getIpOrHostName(),
        &apos;{{outgoing_port}}&apos; =&amp;gt; $this-&amp;gt;getOutbound()-&amp;gt;getPort(),
        &apos;{{outgoing_protocol}}&apos; =&amp;gt; $this-&amp;gt;getOutbound()-&amp;gt;getProtocol(),
        # Other
        &apos;{{pid}}&apos; =&amp;gt; random_number(100000, 10000000),
        &apos;{{max_connections}}&apos; =&amp;gt; 10000,
        &apos;{{name_server_1}}&apos; =&amp;gt; config(&apos;app.proxy3-dns-server-2&apos;),
        &apos;{{name_server_2}}&apos; =&amp;gt; config(&apos;app.proxy3-dns-server-2&apos;),
        &apos;{{reference}}&apos; =&amp;gt; $this-&amp;gt;reference,
        &apos;{{admin_username}}&apos; =&amp;gt; config(&apos;app.proxy3-admin-username&apos;),
        &apos;{{admin_password}}&apos; =&amp;gt; config(&apos;app.proxy3-admin-password&apos;),
    ];

    # Check if there is auth
    if ($this-&amp;gt;outbound-&amp;gt;hasAuthentication() &amp;amp;&amp;amp; $this-&amp;gt;outbound-&amp;gt;getAuth() !== null) {
        $keys[&apos;{{outgoing_username}}&apos;] = $this-&amp;gt;outbound-&amp;gt;getAuthUsername();
        $keys[&apos;{{outgoing_password}}&apos;] = $this-&amp;gt;outbound-&amp;gt;getAuthPassword();
    } else {
        $keys[&apos;{{outgoing_username}}&apos;] = &apos;&apos;;
        $keys[&apos;{{outgoing_password}}&apos;] = &apos;&apos;;
    }
    if($this-&amp;gt;inbound-&amp;gt;getProtocol() === Parser::PROTOCOL_SOCKS5){
        return replace($keys, self::getConfigTemplateSocks5());
    }
    return replace($keys, self::getConfigTemplate());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Result 🎉&lt;/h2&gt;
&lt;p&gt;Here&apos;s a small example of an internal Laravel app we built to manage everything. Keep in mind that some code might NOT be optimized as this is a demo project for the sole purpose of testing 3proxy and squid. Feel free to enhance the database logic, helpers, and documentation. If you have any questions feel free to ask!&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion 🎯&lt;/h2&gt;
&lt;p&gt;We&apos;ve learned how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use Squid &amp;amp; proxy config files (more or less), setup, and how it works&lt;/li&gt;
&lt;li&gt;Build simple proxy redirecting using 3proxy from HTTP → HTTP and HTTP → SOCKS5&lt;/li&gt;
&lt;li&gt;Automate all this logic using a back-end (Laravel &amp;amp; PHP)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Proxies can be used for numerous reasons such as: keeping your connection safe, bypassing blocked websites in your country, crawling, etc! If you liked this article please clap or comment below!&lt;/p&gt;
&lt;p&gt;&amp;lt;FAQ items={[
{ question: &quot;How does a proxy forwarder with Squid and 3Proxy work?&quot;, answer: &quot;A proxy forwarder sits between your client and the destination server, masking the real proxy behind a forwarding proxy. Squid handles this with a config file that defines cache peers and ACL rules, while 3Proxy runs as a lightweight process that can convert between HTTP and SOCKS5 protocols. Both use template variables that can be replaced dynamically by a backend.&quot; },
{ question: &quot;What is the difference between Squid and 3Proxy for proxy forwarding?&quot;, answer: &quot;Squid is a full-featured proxy server with caching, authentication via ncsa_auth, and detailed ACL configuration through a single config file. 3Proxy is a lightweight multi-protocol proxy that runs as a simple process — you start it to route traffic and kill it to stop. Squid suits more complex setups, while 3Proxy excels at quick HTTP-to-HTTP or HTTP-to-SOCKS5 forwarding.&quot; },
{ question: &quot;How can Laravel automate Squid and 3Proxy configuration?&quot;, answer: &quot;Laravel can dynamically generate proxy config files by replacing template variables (like ports, IPs, and credentials) using str_replace on config templates stored in the database. A PHP helper class pulls inbound/outbound settings, generates the config, and manages processes via shell commands — providing a dashboard for creating, monitoring, and removing proxy routes.&quot; },
{ question: &quot;Can 3Proxy convert HTTP traffic to SOCKS5?&quot;, answer: &quot;Yes, 3Proxy supports protocol conversion between HTTP and SOCKS5. You configure it to listen for HTTP connections on one port and forward them through a SOCKS5 proxy on another, using the parent directive with the appropriate protocol flag in the config file.&quot; },
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>proxy</category><category>squid</category><category>laravel</category><category>3proxy</category><category>dashboard</category><author>Pedro Martins</author></item><item><title>Vanilla Components ®</title><link>https://nikuscs.com/projects/02-vanilla-components/</link><guid isPermaLink="true">https://nikuscs.com/projects/02-vanilla-components/</guid><description>A lightweight, flexible &amp; customizable UI library for Vue 3, styled with Tailwind CSS.</description><pubDate>Fri, 01 Sep 2023 00:00:00 GMT</pubDate><category>vue</category><category>components</category><category>ui</category><category>design-system</category><author>Pedro Martins</author></item><item><title>INIDGIT ®</title><link>https://nikuscs.com/projects/03-indigit/</link><guid isPermaLink="true">https://nikuscs.com/projects/03-indigit/</guid><description>A 360º agency specialized in Web applications, Websites, Marketing &amp; Digital Strategy</description><pubDate>Thu, 01 Dec 2022 00:00:00 GMT</pubDate><category>web-development</category><category>marketing</category><category>digital-strategy</category><category>websites</category><category>web-applications</category><author>Pedro Martins</author></item><item><title>Pokemon Accounts ®</title><link>https://nikuscs.com/projects/05-pokemonaccounts/</link><guid isPermaLink="true">https://nikuscs.com/projects/05-pokemonaccounts/</guid><description>A Marketplace to buy &amp; sell Pokémon GO accounts, with more than 10,000 users.</description><pubDate>Sat, 01 Jan 2022 00:00:00 GMT</pubDate><category>web-development</category><category>marketing</category><category>digital-strategy</category><category>websites</category><category>web-applications</category><author>Pedro Martins</author></item><item><title>IGERSLIKE ®</title><link>https://nikuscs.com/projects/01-igerslike/</link><guid isPermaLink="true">https://nikuscs.com/projects/01-igerslike/</guid><description>A full-featured plataform for Social Media Automation &amp; Viral Marketing Services.</description><pubDate>Wed, 20 Jan 2021 00:00:00 GMT</pubDate><category>social-media</category><category>automation</category><category>marketing</category><category>viral</category><author>Pedro Martins</author></item></channel></rss>