<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rusu Ionut</title>
    <description>The latest articles on DEV Community by Rusu Ionut (@johnrusu).</description>
    <link>https://dev.to/johnrusu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F144753%2Ffed5f112-6526-409a-83c9-24b4234d3f54.jpg</url>
      <title>DEV Community: Rusu Ionut</title>
      <link>https://dev.to/johnrusu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/johnrusu"/>
    <language>en</language>
    <item>
      <title>Why I Ditched VS Code for Zed (And How It Accelerated My Full-Stack Work)</title>
      <dc:creator>Rusu Ionut</dc:creator>
      <pubDate>Wed, 03 Jun 2026 17:27:00 +0000</pubDate>
      <link>https://dev.to/johnrusu/why-i-ditched-vs-code-for-zed-and-how-it-accelerated-my-full-stack-work-4fo8</link>
      <guid>https://dev.to/johnrusu/why-i-ditched-vs-code-for-zed-and-how-it-accelerated-my-full-stack-work-4fo8</guid>
      <description>&lt;p&gt;For the past few years, VS Code was my comfortable, heavily customized home. It had my extensions, my themes, and a decade's worth of muscle memory. But recently, as my stack evolved heavily into &lt;strong&gt;Express.js, Vue 3, and Tailwind CSS&lt;/strong&gt;, I started noticing the micro-frictions. &lt;/p&gt;

&lt;p&gt;VS Code is an industrial powerhouse, but it is built on Electron. Under the hood, it’s running a wrapped version of Google Chrome. When you are deeply indexing thousands of dependencies inside &lt;code&gt;node_modules&lt;/code&gt;, running real-time frontend dev servers, and layering on AI code completions, those micro-stutters and cursor lags start to compound.&lt;/p&gt;

&lt;p&gt;Two weeks ago, I completely cut the cord and switched to &lt;strong&gt;Zed&lt;/strong&gt;—the open-source, GPU-accelerated code editor written from scratch in Rust.&lt;/p&gt;

&lt;p&gt;Here is the real, unvarnished story of why I made the jump, the brutal financial reality check I ran into with token billing, and how this layout shift dramatically accelerated development on my main production project, &lt;strong&gt;&lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;recipe-finder.org&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Blazing Speed (The Rust Difference)
&lt;/h2&gt;

&lt;p&gt;The very first thing you notice when you open Zed is that it doesn't "launch"—it just appears. Because it's compiled natively in Rust and uses your computer's graphics card to render text, everything from file searching (&lt;code&gt;Ctrl + P&lt;/code&gt;) to global project searching is instantaneous. &lt;/p&gt;

&lt;p&gt;When working on the codebase for &lt;strong&gt;&lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;recipe-finder.org&lt;/a&gt;&lt;/strong&gt;, jumping back and forth between a complex Express backend route and a Vue frontend single-file component used to feel like dragging a heavy cart. In Zed, switching tabs and editing massive lines of code feels buttery smooth. It completely eliminated the cursor lag that used to break my typing flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Perfect Out-of-the-Box Tooling for Vue &amp;amp; Tailwind
&lt;/h2&gt;

&lt;p&gt;I was worried that leaving VS Code's massive ecosystem of 50,000+ extensions would break my stack. It didn't. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS:&lt;/strong&gt; Zed has &lt;em&gt;native&lt;/em&gt; support built right in. As I write utility classes inside my &lt;code&gt;.vue&lt;/code&gt; templates, autocomplete popups appear flawlessly. Hovering over a class instantly shows the raw CSS output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vue.js:&lt;/strong&gt; A quick trip to Zed's extension marketplace (&lt;code&gt;Ctrl + Shift + X&lt;/code&gt;) to grab the official Vue extension hooked up Volar (the Vue Language Server) seamlessly. Full type-checking, diagnostics, and scoped styling worked instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Express.js:&lt;/strong&gt; Because Zed indexes &lt;code&gt;node_modules&lt;/code&gt; instantly in the background without locking up the editor UI thread, my Node/Express server setup became incredibly responsive to work with.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Token Price Shock (And How I Fixed It)
&lt;/h2&gt;

&lt;p&gt;If you use Zed’s native AI panel on the right side, you need to understand how it bills. Zed Pro gives you a $5 monthly token credit and then switches to pay-as-you-go raw API costs +10%. &lt;/p&gt;

&lt;p&gt;Within my first hour, &lt;strong&gt;I burned through $2.63 in just four questions.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;I had massive sticker shock until I realized how LLMs work under the hood. It’s called the &lt;strong&gt;Context Snowball Effect&lt;/strong&gt;. Every time you type a follow-up question in the right-hand panel, Zed bundles the &lt;em&gt;entire chat history&lt;/em&gt; plus your &lt;em&gt;active code files&lt;/em&gt; and flings them back to the model. You aren't paying for small questions; you are paying the AI to re-read your entire file over and over again from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: The Ultimate Hybrid Workflow
&lt;/h3&gt;

&lt;p&gt;Instead of paying for metered usage-based billing with Zed-hosted models, I leveraged a brilliant feature built directly into the editor: &lt;strong&gt;I plugged in my existing $39 GitHub Copilot subscription.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By pressing &lt;code&gt;Ctrl + Shift + P&lt;/code&gt; and running &lt;code&gt;copilot: sign in&lt;/code&gt;, I authorized my GitHub account. Then, I switched my right-side AI panel dropdown provider to &lt;strong&gt;"GitHub Copilot Chat"&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Now, I get the flat-rate predictability of Copilot (ask 500 questions a day, leave massive Vue files attached, no extra fees) combined with the lightning-fast, zero-lag editing environment of Zed. It’s the ultimate setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Accelerated Recipe-Finder.org
&lt;/h2&gt;

&lt;p&gt;We’ve been sprinting hard to push out a massive referral marketing campaign on &lt;strong&gt;&lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;recipe-finder.org&lt;/a&gt;&lt;/strong&gt;: a brand-new invite portal titled &lt;strong&gt;"Get a FREE Month of Premium!"&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;This required writing a specialized absolute overlay over our main Vuetify cards to handle asynchronous loading states smoothly. In my old workflow, tweaking the custom Vue deep selectors (&lt;code&gt;:deep(.v-loader)&lt;/code&gt;) and alignment styling while waiting for code completions to catch up was tedious. &lt;/p&gt;

&lt;p&gt;With Zed docked perfectly to my preferences—with the terminal and project panels pushed over to the right side just like VS Code, and the minimap file preview pinned to &lt;code&gt;always&lt;/code&gt;—the cycle time dropped to zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- A snippet of the optimized portal cards we shipped under the new setup --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;v-card&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"premium-invite-card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;v-card-title&amp;gt;&lt;/span&gt;Get a FREE Month of Premium!&lt;span class="nt"&gt;&amp;lt;/v-card-title&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Referral portal components --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/v-card&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nc"&gt;.premium-invite-card&lt;/span&gt; &lt;span class="nd"&gt;:deep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.v-loader&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--v-theme-surface&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The combination of instant file-saving formatting, snappy inline AI predictions, and zero-latency UI rendering allowed us to prototype, style, and ship the invite portal features significantly faster than before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Final Verdict
&lt;/h2&gt;

&lt;p&gt;If your engineering workflow depends on hyper-niche VS Code extensions (like complex graphical visual debuggers or proprietary database managers), keep VS Code close by. &lt;/p&gt;

&lt;p&gt;But if you are a full-stack developer working in the JavaScript/Node ecosystem, handling reactive frontends, and writing code alongside AI assistants all day, &lt;strong&gt;do not sleep on Zed&lt;/strong&gt;. Connect your existing Copilot key, clean up your layout, and enjoy a text editor that finally keeps up with the speed of your brain.&lt;/p&gt;

&lt;p&gt;Have you tried switching to Zed yet, or are you staying loyal to VS Code? Let me know your setup below!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>vue</category>
      <category>node</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How I built a Social Recipe Extractor that turns short-form video links into structured recipes</title>
      <dc:creator>Rusu Ionut</dc:creator>
      <pubDate>Wed, 13 May 2026 15:38:18 +0000</pubDate>
      <link>https://dev.to/johnrusu/how-i-built-a-social-recipe-extractor-that-turns-short-form-video-links-into-structured-recipes-11j6</link>
      <guid>https://dev.to/johnrusu/how-i-built-a-social-recipe-extractor-that-turns-short-form-video-links-into-structured-recipes-11j6</guid>
      <description>&lt;p&gt;I recently built a feature for &lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;https://recipe-finder.org&lt;/a&gt; that takes a TikTok, Instagram, or YouTube cooking link and turns it into a structured recipe.&lt;/p&gt;

&lt;p&gt;The goal was simple: recipe inspiration lives on social platforms, but the actual cooking steps are usually buried inside captions, spoken instructions, or fast cuts. I wanted a workflow where a user pastes a link and gets back something usable: a recipe title, ingredient list, instructions, and a cleaner format for the rest of the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Flow
&lt;/h3&gt;

&lt;p&gt;The core user journey is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user pastes a supported social video URL.&lt;/li&gt;
&lt;li&gt;The frontend sends that URL to a protected backend endpoint.&lt;/li&gt;
&lt;li&gt;The backend normalizes the link, fetches available source metadata and text, and runs an extraction pass.&lt;/li&gt;
&lt;li&gt;The result is converted into a structured recipe object.&lt;/li&gt;
&lt;li&gt;The app returns a clean recipe view, keeps recent imports in history, and can reuse cached results for repeated links.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What makes this interesting isn't just the form input—it's the cleanup step between raw social content and something a user can actually cook from. &lt;/p&gt;

&lt;p&gt;Here is a look under the hood at how it's built using Vue 3, TypeScript, and Node.js/Express.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Frontend Entry Point
&lt;/h3&gt;

&lt;p&gt;The feature lives behind authentication and opens as a dedicated page in the app. The page itself stays simple and hands the main workflow to a dedicated &lt;code&gt;Social Recipe Extractor&lt;/code&gt; component. &lt;/p&gt;

&lt;p&gt;To keep the UI predictable and sleek (opting for a dark mode aesthetic with vibrant orange accents for active states), the component manages four main states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The pasted social link&lt;/li&gt;
&lt;li&gt;  Loading and error feedback&lt;/li&gt;
&lt;li&gt;  The extracted recipe result&lt;/li&gt;
&lt;li&gt;  Recent import history and usage state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the input on the left, the result on the right, and prior imports available without mixing feature logic across multiple pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Backend-Owned Extraction Flow
&lt;/h3&gt;

&lt;p&gt;The frontend does not parse social content directly. Instead, it sends the link to a backend route dedicated to social recipe extraction. &lt;/p&gt;

&lt;p&gt;That backend layer is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Validating the request&lt;/li&gt;
&lt;li&gt;  Normalizing the URL into a canonical form&lt;/li&gt;
&lt;li&gt;  Checking whether a usable cached import already exists&lt;/li&gt;
&lt;li&gt;  Fetching source metadata and extracted text from the target platform&lt;/li&gt;
&lt;li&gt;  Transforming unstructured content into a recipe-shaped response&lt;/li&gt;
&lt;li&gt;  Returning a stable payload the frontend can render safely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the right ownership boundary because platform parsing, rate control, caching, and extraction quality all belong on the server. The frontend only ever deals with a typed response.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Structured Extraction
&lt;/h3&gt;

&lt;p&gt;The most important design choice was ensuring the pipeline doesn't stop at "grab some text from the page." It pushes the output into a highly structured shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Recipe title&lt;/li&gt;
&lt;li&gt;  Prep time&lt;/li&gt;
&lt;li&gt;  Ingredients&lt;/li&gt;
&lt;li&gt;  Instructions&lt;/li&gt;
&lt;li&gt;  Normalized ingredients&lt;/li&gt;
&lt;li&gt;  Source metadata (platform, thumbnail, title, author)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because a structured result is much easier to reuse than a raw text dump. It allows the data to power future features without rebuilding the extraction logic each time.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Normalization and Cleanup
&lt;/h3&gt;

&lt;p&gt;Social content is inherently messy. Titles contain hashtags, descriptions are noisy, and ingredient mentions are often incomplete or informal. &lt;/p&gt;

&lt;p&gt;The implementation handles this by cleaning and normalizing content before &lt;em&gt;and&lt;/em&gt; after extraction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;URL normalization&lt;/strong&gt; ensures equivalent links map to the same import record.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Title cleanup&lt;/strong&gt; prevents social-platform suffixes and noisy text from leaking into the recipe title.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Whitespace and escaped-text cleanup&lt;/strong&gt; is applied before extraction.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Ingredient normalization&lt;/strong&gt; makes the output more consistent for downstream product use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This cleanup layer is where a lot of the actual product quality is realized. &lt;/p&gt;

&lt;h3&gt;
  
  
  5. Caching and History
&lt;/h3&gt;

&lt;p&gt;Two product decisions make the feature feel significantly faster and more useful:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Cached imports&lt;/strong&gt; prevent unnecessary repeat processing for the same normalized link.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Import history&lt;/strong&gt; gives users a lightweight library of recently extracted recipes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The extractor isn't just a one-shot tool; it becomes a reusable workflow inside the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Premium-Friendly Usage Model
&lt;/h3&gt;

&lt;p&gt;Finally, the feature returns usage information along with the extraction result. That allows the UI to immediately show whether the user can continue importing, how much of their current allowance is used, and when an upgrade prompt should appear. &lt;/p&gt;

&lt;p&gt;This is a much better user experience than hard-failing late in the flow, as the interface can explain the state before the user hits a dead end.&lt;/p&gt;




&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;This is one of those features where the product value comes purely from reducing friction. People already save recipes from social media. The real improvement is turning that saved link into something searchable, reusable, and easier to cook from. &lt;/p&gt;

&lt;p&gt;If I were extending it further, the next logical step would be connecting the extracted, structured recipe directly into grocery list generation, smart meal planning, and nutrition analysis.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>vue</category>
      <category>node</category>
      <category>fullstack</category>
    </item>
    <item>
      <title>Recipe-Finder.org is Officially on Android! 📱🍲</title>
      <dc:creator>Rusu Ionut</dc:creator>
      <pubDate>Wed, 13 May 2026 14:56:29 +0000</pubDate>
      <link>https://dev.to/johnrusu/recipe-finderorg-is-officially-on-android-4e16</link>
      <guid>https://dev.to/johnrusu/recipe-finderorg-is-officially-on-android-4e16</guid>
      <description>&lt;p&gt;Hey DEV community! 👋&lt;/p&gt;

&lt;p&gt;If you've ever used &lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;recipe-finder.org&lt;/a&gt; to figure out what to cook with that random assortment of ingredients left in your fridge, I've got some exciting news. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We've officially gone mobile!&lt;/strong&gt; 🎉&lt;/p&gt;

&lt;p&gt;While the web version has been fantastic, we realized early on that dragging a laptop into the kitchen—or trying to keep a mobile browser awake while your hands are covered in flour—isn't exactly the ultimate user experience. &lt;/p&gt;

&lt;h3&gt;
  
  
  Why We Built the App
&lt;/h3&gt;

&lt;p&gt;Moving from the web to a native mobile environment solved a few core problems for us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Kitchen Experience:&lt;/strong&gt; Native controls allow us to keep the screen awake while you're actively following a recipe. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick Access:&lt;/strong&gt; No more digging through a sea of browser tabs. Your saved recipes and grocery lists are now just a tap away on your home screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better Performance:&lt;/strong&gt; Snappier ingredient searches, smoother scrolling, and a much cleaner UI optimized specifically for your phone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Transitioning this project from a web-only platform to an Android application was a great learning experience. (I plan to write a follow-up post doing a deeper dive into the tech stack, the architecture we chose, and the hurdles we faced along the way). &lt;/p&gt;

&lt;p&gt;But for today, I'm just incredibly excited to share this milestone with you all!&lt;/p&gt;

&lt;h3&gt;
  
  
  Give It a Spin
&lt;/h3&gt;

&lt;p&gt;If you're an Android user, you can grab the new app directly from our site here:&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://recipe-finder.org/android-app" rel="noopener noreferrer"&gt;Download Recipe-Finder for Android&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd absolutely love to hear your feedback. If you catch any bugs, have feature requests, or just want to tell me what you're cooking for dinner tonight, drop a comment below!&lt;/p&gt;

</description>
      <category>android</category>
      <category>vue</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From Modal to Full Page: How We Refactored a Vue 3 Recipe Detail View</title>
      <dc:creator>Rusu Ionut</dc:creator>
      <pubDate>Sun, 03 May 2026 19:51:54 +0000</pubDate>
      <link>https://dev.to/johnrusu/from-modal-to-full-page-how-we-refactored-a-vue-3-recipe-detail-view-hef</link>
      <guid>https://dev.to/johnrusu/from-modal-to-full-page-how-we-refactored-a-vue-3-recipe-detail-view-hef</guid>
      <description>&lt;p&gt;One of the longest-lived technical decisions in our recipe finder app was showing recipe details inside a dialog modal. It worked — until it didn't. Here's how we migrated from a bloated modal to a clean, SEO-friendly full page, what we cut along the way, and what the app looks like now.&lt;/p&gt;

&lt;p&gt;Demo:&lt;br&gt;
&lt;a href="https://recipe-finder.org/recipe/644488-german-rhubarb-cake-with-meringue" rel="noopener noreferrer"&gt;https://recipe-finder.org/recipe/644488-german-rhubarb-cake-with-meringue&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Old Approach: Everything in a Modal
&lt;/h2&gt;

&lt;p&gt;The original setup opened a &lt;code&gt;&amp;lt;v-dialog&amp;gt;&lt;/code&gt; when a user clicked a recipe card. The modal held the entire recipe detail UI: ingredients, nutrition, videos, AI chef, grocery import, recipe scaler — all of it. The logic for opening it, fetching the recipe, and handling deep-link slugs lived inside &lt;code&gt;HomePage.vue&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Old: HomePage.vue controlled everything --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;RecipeDetailsModal&lt;/span&gt; &lt;span class="na"&gt;:is-open=&lt;/span&gt;&lt;span class="s"&gt;"isRecipeModalOpen"&lt;/span&gt; &lt;span class="na"&gt;:recipe=&lt;/span&gt;&lt;span class="s"&gt;"selectedRecipeDetails"&lt;/span&gt; &lt;span class="na"&gt;:loading=&lt;/span&gt;&lt;span class="s"&gt;"loadingRecipeDetails"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;close=&lt;/span&gt;&lt;span class="s"&gt;"closeModal"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The problem was that &lt;code&gt;HomePage.vue&lt;/code&gt; had become a god component. It managed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The search form and results&lt;/li&gt;
&lt;li&gt;Cuisine carousel&lt;/li&gt;
&lt;li&gt;Recipe of the day&lt;/li&gt;
&lt;li&gt;Recent recipes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;And&lt;/strong&gt; the modal open/close state, slug parsing, and detail fetch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of that, because everything was in a modal, the URL never changed. Users couldn't share a link to a specific recipe, Google couldn't index the content, and the Back button did nothing useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  The New Approach: Dedicated Route + Page
&lt;/h2&gt;

&lt;p&gt;We created &lt;code&gt;/recipe/:slug&lt;/code&gt; as a proper route and moved the recipe detail logic into a standalone &lt;code&gt;RecipeDetailPage.vue&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// router/index.ts&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/recipe/:slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/pages/RecipeDetailPage.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Slugs are derived from the recipe ID and title, making them human-readable and stable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// utils/index.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toRecipeSlug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;^-|-$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extractRecipeIdFromSlug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What We Cut From HomePage.vue
&lt;/h2&gt;

&lt;p&gt;Once the page was independent, we stripped &lt;code&gt;HomePage.vue&lt;/code&gt; of everything modal-related. Gone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;isRecipeModalOpen&lt;/code&gt; ref&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;selectedRecipeDetails&lt;/code&gt; and &lt;code&gt;loadingRecipeDetails&lt;/code&gt; state&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;openRecipeModal&lt;/code&gt; / &lt;code&gt;closeModal&lt;/code&gt; handlers&lt;/li&gt;
&lt;li&gt;The slug-watching &lt;code&gt;watch&lt;/code&gt; that re-fetched on URL change&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;RecipeDetailsModal&amp;gt;&lt;/code&gt; import and component registration&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;handleOpenRecipeDetails&lt;/code&gt; function passed down through three component layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result was &lt;code&gt;HomePage.vue&lt;/code&gt; shrinking by roughly &lt;strong&gt;40%&lt;/strong&gt; in script size. It now does one thing: show the search form and results.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Page Layout Looks Like
&lt;/h2&gt;

&lt;p&gt;The page uses a standard Vuetify two-column grid — main content on the left, sticky sidebar on the right (desktop only). On mobile, the sidebar collapses and the tools surface inline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;v-row&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Left: main content --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;v-col&lt;/span&gt; &lt;span class="na"&gt;cols=&lt;/span&gt;&lt;span class="s"&gt;"12"&lt;/span&gt; &lt;span class="na"&gt;lg=&lt;/span&gt;&lt;span class="s"&gt;"8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;v-img&lt;/span&gt; &lt;span class="na"&gt;:src=&lt;/span&gt;&lt;span class="s"&gt;"recipe.image"&lt;/span&gt; &lt;span class="na"&gt;cover&lt;/span&gt; &lt;span class="na"&gt;rounded=&lt;/span&gt;&lt;span class="s"&gt;"lg"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-6"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"recipe-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ recipe.title }}&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- chips, rating, action buttons, summary, ingredients, instructions, nutrition --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/v-col&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Right: sticky sidebar (desktop only) --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;v-col&lt;/span&gt; &lt;span class="na"&gt;cols=&lt;/span&gt;&lt;span class="s"&gt;"12"&lt;/span&gt; &lt;span class="na"&gt;lg=&lt;/span&gt;&lt;span class="s"&gt;"4"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"d-none d-lg-flex flex-column"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sidebar-sticky"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!-- Recipe Tools card --&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!-- AI Cooking Chef card --&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!-- Nutrition Snapshot card --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/v-col&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/v-row&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sidebar cards use a glass morphism style that matches the rest of the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.glass-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;circle&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="nb"&gt;top&lt;/span&gt; &lt;span class="nb"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;163&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;40%&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;165deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.07&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Mobile: Icon Buttons + Bottom Sheet AI
&lt;/h2&gt;

&lt;p&gt;On mobile, the page can't show a sidebar. We solved this in two ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action buttons&lt;/strong&gt; become icon-only on small screens, matching how the modal used to look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Mobile: icon-only --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"d-flex d-sm-none gap-2 mb-6 align-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;v-btn&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt; &lt;span class="na"&gt;variant=&lt;/span&gt;&lt;span class="s"&gt;"tonal"&lt;/span&gt; &lt;span class="na"&gt;size=&lt;/span&gt;&lt;span class="s"&gt;"small"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click.stop=&lt;/span&gt;&lt;span class="s"&gt;"handleToggleFavoriteRecipe"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;v-icon&amp;gt;&lt;/span&gt;{{ isFavorited ? 'mdi-heart' : 'mdi-heart-outline' }}&lt;span class="nt"&gt;&amp;lt;/v-icon&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/v-btn&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;v-btn&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt; &lt;span class="na"&gt;variant=&lt;/span&gt;&lt;span class="s"&gt;"tonal"&lt;/span&gt; &lt;span class="na"&gt;size=&lt;/span&gt;&lt;span class="s"&gt;"small"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"shareRecipe"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;v-icon&amp;gt;&lt;/span&gt;{{ isCopied ? 'mdi-check' : 'mdi-share-variant' }}&lt;span class="nt"&gt;&amp;lt;/v-icon&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/v-btn&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;v-btn&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt; &lt;span class="na"&gt;variant=&lt;/span&gt;&lt;span class="s"&gt;"tonal"&lt;/span&gt; &lt;span class="na"&gt;size=&lt;/span&gt;&lt;span class="s"&gt;"small"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"printRecipe"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;v-icon&amp;gt;&lt;/span&gt;mdi-printer&lt;span class="nt"&gt;&amp;lt;/v-icon&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/v-btn&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Desktop: text buttons --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"d-none d-sm-flex flex-wrap gap-2 mb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;v-btn&lt;/span&gt; &lt;span class="na"&gt;prepend-icon=&lt;/span&gt;&lt;span class="s"&gt;"mdi-heart-outline"&lt;/span&gt; &lt;span class="na"&gt;variant=&lt;/span&gt;&lt;span class="s"&gt;"tonal"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-action-btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Add to Favorites
  &lt;span class="nt"&gt;&amp;lt;/v-btn&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The AI chef&lt;/strong&gt; becomes a bottom sheet triggered by a text button inline with the ingredient tools (no floating action button cluttering the screen):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Ingredient row: Scale / Analyze / Grocery / AI Chef --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;v-btn&lt;/span&gt; &lt;span class="na"&gt;color=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt; &lt;span class="na"&gt;variant=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;size=&lt;/span&gt;&lt;span class="s"&gt;"small"&lt;/span&gt;
  &lt;span class="na"&gt;prepend-icon=&lt;/span&gt;&lt;span class="s"&gt;"mdi-robot-excited"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"showAiSheet = true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  AI Chef
&lt;span class="nt"&gt;&amp;lt;/v-btn&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;v-bottom-sheet&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"showAiSheet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- full AI interface --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/v-bottom-sheet&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  SEO + Meta Tags
&lt;/h2&gt;

&lt;p&gt;Since the content is now on a real URL, we inject &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and Open Graph meta tags dynamically on load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;injectMetaTags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; | Recipe Finder`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pageTitle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setMeta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nf"&gt;setMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meta[property='og:title']&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageTitle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meta[name='twitter:title']&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageTitle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;155&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meta[name='description']&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meta[property='og:description']&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meta[property='og:image']&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is called inside a &lt;code&gt;watch&lt;/code&gt; on the recipe computed ref, so it fires on both initial load and when navigating between similar recipes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Async Components Throughout
&lt;/h2&gt;

&lt;p&gt;Every non-critical UI piece is lazy-loaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImportToGroceryList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineAsyncComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/ImportToGroceryList.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PremiumUpgradeDialog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineAsyncComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/PremiumUpgradeDialog.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RecipeScaler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineAsyncComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/RecipeScaler.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RecipeEmbedWatermark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineAsyncComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/RecipeEmbedWatermark.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main recipe content renders immediately. The grocery dialog, scaler, embed widget, and upgrade dialog only load if the user actually interacts with them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Auth Guard on Grocery Import
&lt;/h2&gt;

&lt;p&gt;One regression we caught: clicking "Send to Grocery List" while logged out was calling &lt;code&gt;getAccessTokenSilently()&lt;/code&gt; and throwing an Auth0 missing refresh token error. Fixed by checking auth state before opening the dialog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onDialogToggle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;dialogOpen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;loginWithRedirect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;loadGroceryLists&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;HomePage.vue&lt;/code&gt; Script Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~650 lines&lt;/td&gt;
&lt;td&gt;~390 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recipe URL Shareable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Indexable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mobile Layout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fullscreen modal&lt;/td&gt;
&lt;td&gt;Native page with bottom sheet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth Crash on Grocery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Existed&lt;/td&gt;
&lt;td&gt;✅ Fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI on Mobile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Floating button&lt;/td&gt;
&lt;td&gt;Inline trigger → bottom sheet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The modal still exists for the recipe list view (quick-peek without leaving the page), but the canonical experience is now a proper page. The code is cleaner, the app is faster to load, and every recipe finally has a real URL.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Vue 3, Vuetify 3, TypeScript, and Tailwind CSS. Auth via Auth0.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vue</category>
      <category>webdev</category>
      <category>refactoring</category>
      <category>ux</category>
    </item>
    <item>
      <title>Building a Self-Healing SEO Architecture for a Vue SPA</title>
      <dc:creator>Rusu Ionut</dc:creator>
      <pubDate>Fri, 01 May 2026 18:18:17 +0000</pubDate>
      <link>https://dev.to/johnrusu/building-a-self-healing-seo-architecture-for-a-vue-spa-4lfb</link>
      <guid>https://dev.to/johnrusu/building-a-self-healing-seo-architecture-for-a-vue-spa-4lfb</guid>
      <description>&lt;p&gt;Building a modern Single Page Application (SPA) with Vite and Vue is great for user experience, but it's a minefield for SEO. We faced three major hurdles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Aggressive Bot Protection:&lt;/strong&gt; Our &lt;code&gt;.htaccess&lt;/code&gt; was so tight it was blocking crawlers that we actually wanted.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The "SPA Meta Trap":&lt;/strong&gt; Social media bots (Facebook, WhatsApp) couldn't read our dynamic recipe titles or images because they don't execute JavaScript.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Scale Problem:&lt;/strong&gt; We have access to millions of recipes via the Spoonacular API, but we don't own the full database. How do you tell Google about millions of pages you don't physically store?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Vite + Vue 3 (Hosted on Apache)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Node.js + Express (Hosted on Firebase Functions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; MongoDB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider:&lt;/strong&gt; Spoonacular API&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Solution: A 3-Step "Self-Healing" Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Solving the Social Preview (The Meta Injection)
&lt;/h3&gt;

&lt;p&gt;Since our frontend is on a standard Apache host, we couldn't use edge functions easily. Instead, we optimized our URL structure to include SEO-friendly slugs:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;recipe-finder.org/recipe/644488-german-rhubarb-cake-with-meringue&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We then implemented a backend-driven meta-injection strategy. When a recipe is requested, our Express server pre-fills the Open Graph tags (&lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt;) using the recipe summary, ensuring beautiful previews on social media.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. The "Self-Building" Database
&lt;/h3&gt;

&lt;p&gt;We didn't want to scrape millions of recipes (and get banned). Instead, we created an &lt;strong&gt;Organic Growth Engine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every time a user (guest or authenticated) clicks a recipe, our Express backend performs an &lt;strong&gt;Upsert&lt;/strong&gt; into MongoDB. If it's a new recipe, it's added to our "SEO Index." If it's an existing one, we update the &lt;code&gt;lastViewed&lt;/code&gt; timestamp.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Remove stale entry, then push back to front with a fresh timestamp&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;recipeViewedModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOneAndUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth0Id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$pull&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;recipeViewedModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOneAndUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth0Id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$each&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;viewedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;$position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures our database only grows with high-quality, relevant content that users actually care about.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. The Dynamic Hybrid Sitemap
&lt;/h3&gt;

&lt;p&gt;A static &lt;code&gt;sitemap.xml&lt;/code&gt; was impossible for millions of potential links. We built a &lt;strong&gt;Dynamic Sitemap Index&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;sitemap-main.xml&lt;/strong&gt; — A static file on our hosting server for core pages (Home, Tools, About).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sitemap-recipes-[n].xml&lt;/strong&gt; — Dynamic routes on Express that query MongoDB and generate XML on the fly in 50,000-unit chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Master Index&lt;/strong&gt; — A central &lt;code&gt;sitemap.xml&lt;/code&gt; that bridges the two, served via a silent proxy in &lt;code&gt;.htaccess&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^sitemap\.xml$ [https://your-region-your-project.cloudfunctions.net/api/sitemap.xml](https://your-region-your-project.cloudfunctions.net/api/sitemap.xml) [R=301,L]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^sitemap-recipes-([0-9]+)\.xml$ [https://your-region-your-project.cloudfunctions.net/api/sitemap-recipes-$1.xml](https://your-region-your-project.cloudfunctions.net/api/sitemap-recipes-$1.xml) [R=301,L]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any request for a sitemap is silently routed to the Express API, which assembles the XML from MongoDB on the fly — no static file maintenance required.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Search Console Verified:&lt;/strong&gt; Live URL testing shows Google successfully rendering the SPA and reading the dynamic content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated SEO:&lt;/strong&gt; The more our users cook, the larger our sitemap grows. We don't have to manually add a single link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Maintenance Scaling:&lt;/strong&gt; The system handles 10 recipes or 10 million with the same memory footprint thanks to MongoDB's &lt;code&gt;$group&lt;/code&gt; and &lt;code&gt;$limit&lt;/code&gt; aggregations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaway for CoderLegion
&lt;/h2&gt;

&lt;p&gt;Don't build for millions of pages on Day 1. Build a system that lets your users' activity grow your SEO footprint for you. Work with the bots, not against them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; Rusu Ionut&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;recipe-finder.org&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>vue</category>
      <category>node</category>
    </item>
    <item>
      <title>Get a FREE Month of Premium! 🚀</title>
      <dc:creator>Rusu Ionut</dc:creator>
      <pubDate>Sat, 25 Apr 2026 16:36:01 +0000</pubDate>
      <link>https://dev.to/johnrusu/get-a-free-month-of-premium-4go4</link>
      <guid>https://dev.to/johnrusu/get-a-free-month-of-premium-4go4</guid>
      <description>&lt;p&gt;Hey everyone! 👋&lt;/p&gt;

&lt;p&gt;We are excited to share a brand new way for you to enjoy our Premium features. If you've been loving the platform, why not bring a friend along? &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get a FREE month of Premium!&lt;/strong&gt; The process is incredibly simple: just share your unique invite link, and when a friend upgrades their account, your next month is completely on us. There's no limit to how many friends you can invite!&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Grab your invite link here:&lt;/strong&gt; &lt;a href="https://recipe-finder.org/invite-one-free-month" rel="noopener noreferrer"&gt;https://recipe-finder.org/invite-one-free-month&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank you for being such an awesome part of our community. Happy coding (and cooking)! 💻🍳&lt;/p&gt;

</description>
      <category>recipes</category>
      <category>culinary</category>
      <category>food</category>
      <category>cooking</category>
    </item>
    <item>
      <title>Building Recipe-Finder.org: A Full-Stack Journey with Vue, Express, MongoDB, and Vuetify 🍳</title>
      <dc:creator>Rusu Ionut</dc:creator>
      <pubDate>Sat, 25 Apr 2026 16:30:34 +0000</pubDate>
      <link>https://dev.to/johnrusu/building-recipe-finderorg-a-full-stack-journey-with-vue-express-mongodb-and-vuetify-2k57</link>
      <guid>https://dev.to/johnrusu/building-recipe-finderorg-a-full-stack-journey-with-vue-express-mongodb-and-vuetify-2k57</guid>
      <description>&lt;p&gt;Hello, DEV community! 👋 &lt;/p&gt;

&lt;p&gt;Today, I want to share a project I recently launched: &lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;Recipe-Finder.org&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Like many developers, I often find myself staring into the fridge wondering what to make with the random ingredients I have left. I wanted a fast, clean, and intuitive way to search for recipes, so I decided to build my own solution. &lt;/p&gt;

&lt;p&gt;It was a fantastic opportunity to dive deeper into full-stack development, and I decided to go with a modified MEVN stack. Here is a breakdown of how I built it, the tools I used, and what I learned along the way.&lt;/p&gt;




&lt;h3&gt;
  
  
  🛠️ The Tech Stack
&lt;/h3&gt;

&lt;p&gt;I wanted a stack that allowed for rapid development while keeping the application highly responsive. Here is what powered the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; &lt;strong&gt;Vue.js&lt;/strong&gt;. I love Vue for its approachable learning curve and how easily it handles reactive components. It made building the dynamic search interfaces a breeze.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI Framework:&lt;/strong&gt; &lt;strong&gt;Vuetify&lt;/strong&gt;. To get that polished, Material Design look without writing hundreds of lines of custom CSS, Vuetify was my go-to. It provided out-of-the-box components like cards for the recipes, navigation drawers, and responsive grids.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; &lt;strong&gt;Express.js (Node.js)&lt;/strong&gt;. I kept the backend lightweight. Express handles the API routing, processing search requests from the Vue frontend and communicating with the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; &lt;strong&gt;MongoDB&lt;/strong&gt;. Recipes are inherently document-like (they have arrays of ingredients, arrays of instructions, etc.). A NoSQL database like MongoDB was a perfect fit, allowing me to store recipe data flexibly without strict relational tables.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🏗️ Architecture &amp;amp; How It Works
&lt;/h3&gt;

&lt;p&gt;The architecture is a standard decoupled setup. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The Client:&lt;/strong&gt; The Vue app handles all the state management (using Pinia) and user interactions. When a user types an ingredient or recipe name, Vue triggers an Axios request.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The API:&lt;/strong&gt; The Express server receives this request. It validates the input and constructs a query.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Data:&lt;/strong&gt; The server queries MongoDB, retrieves the matching recipe documents, and sends them back as a JSON response.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Render:&lt;/strong&gt; Vue takes that JSON data and seamlessly updates the Vuetify DOM components to display the delicious results.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  🚧 Biggest Challenges &amp;amp; Lessons Learned
&lt;/h3&gt;

&lt;p&gt;No project is complete without a few bumps in the road. Here are a couple of things that tested my patience and what I learned from them:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Managing Complex Search Queries
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Challenge:&lt;/strong&gt; Users rarely type exact, sanitized ingredient names. Implementing a search that handled both strict array matching (for ingredients) and fuzzy text matching (for recipe titles) was tricky to get right without sacrificing performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Solution:&lt;/strong&gt; I ended up utilizing MongoDB's text search indexes and the &lt;code&gt;$text&lt;/code&gt; operator. For more nuanced ingredient matching, I built out an aggregation pipeline in Express that scores and sorts results based on how many ingredients match the user's input.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Responsive UI with Vuetify
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Challenge:&lt;/strong&gt; Getting the recipe cards to look consistent was surprisingly tough. Recipe images had different aspect ratios, and title lengths varied wildly, which kept breaking my grid layouts on mobile screens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Solution:&lt;/strong&gt; I leveraged Vuetify's &lt;code&gt;v-img&lt;/code&gt; aspect-ratio props to enforce uniformity and used the CSS &lt;code&gt;line-clamp&lt;/code&gt; property for text truncation. I also fully utilized Vuetify's responsive grid system (&lt;code&gt;cols="12" sm="6" md="4"&lt;/code&gt;) to ensure the layout degrades gracefully based on viewport size.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🚀 What's Next?
&lt;/h3&gt;

&lt;p&gt;Getting the core functionality of &lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;Recipe-Finder.org&lt;/a&gt; live was step one. In the future, I plan to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User Accounts:&lt;/strong&gt; So people can save and favorite their go-to recipes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meal Planning:&lt;/strong&gt; A calendar feature to plan the week's dinners in advance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Shopping Lists:&lt;/strong&gt; Automatically compiling missing ingredients from a chosen recipe into an interactive checklist.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Let me know what you think!
&lt;/h3&gt;

&lt;p&gt;Building this was a lot of fun, and seeing it live on the web is incredibly rewarding. I'd love for you to try it out! &lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Check it out here:&lt;/strong&gt; &lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;Recipe-Finder.org&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have any feedback on the UI, the search functionality, or the code structure, please let me know in the comments below. Happy coding! 👨‍💻👩‍💻&lt;/p&gt;

</description>
      <category>vue</category>
      <category>node</category>
      <category>mongodb</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Turn Viral Recipe Videos into Clean Recipe Cards Instantly 🍳</title>
      <dc:creator>Rusu Ionut</dc:creator>
      <pubDate>Mon, 30 Mar 2026 21:07:05 +0000</pubDate>
      <link>https://dev.to/johnrusu/turn-viral-recipe-videos-into-clean-recipe-cards-instantly-p1k</link>
      <guid>https://dev.to/johnrusu/turn-viral-recipe-videos-into-clean-recipe-cards-instantly-p1k</guid>
      <description>&lt;h2&gt;
  
  
  ⚡ 60-Second Video -&amp;gt; 1-Click Cooking
&lt;/h2&gt;

&lt;p&gt;We turn chaotic viral cooking videos into organized recipe cards and standardized grocery lists.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Find:&lt;/strong&gt; Copy a video link (TikTok, Instagram, YouTube).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paste:&lt;/strong&gt; Drop it into our extractor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cook:&lt;/strong&gt; Get an instant clean card.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Try it for FREE:&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Everybody gets &lt;strong&gt;2 FREE AI imports per month&lt;/strong&gt;. No credit card required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://recipe-finder.org/social-recipe-extractor" rel="noopener noreferrer"&gt;👉 Extract Your First Recipe Here&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Go Premium for UNLIMITED Imports:&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;For power users who cook a lot, get seamless, unlimited imports.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://recipe-finder.org/ai-assistant" rel="noopener noreferrer"&gt;👨‍🍳 Go Premium Now&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>tools</category>
      <category>cooking</category>
    </item>
    <item>
      <title>Empty fridge? I built a tool that tells you what to cook 🍳</title>
      <dc:creator>Rusu Ionut</dc:creator>
      <pubDate>Wed, 25 Mar 2026 07:39:12 +0000</pubDate>
      <link>https://dev.to/johnrusu/empty-fridge-i-built-a-tool-that-tells-you-what-to-cook-3a70</link>
      <guid>https://dev.to/johnrusu/empty-fridge-i-built-a-tool-that-tells-you-what-to-cook-3a70</guid>
      <description>&lt;p&gt;Hey everyone! 👋&lt;/p&gt;

&lt;p&gt;Tired of staring at 3 random ingredients in your fridge and just ordering takeout? Me too. So I built an app to solve it. &lt;/p&gt;

&lt;p&gt;Just type in whatever is sitting in your kitchen, and it instantly generates meals you can actually make. &lt;/p&gt;

&lt;h3&gt;
  
  
  🔗 Try it out here:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🍽️ &lt;strong&gt;&lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;Find a Recipe Right Now&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🧅 &lt;strong&gt;&lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;Search by the Ingredients You Have&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;💸 &lt;strong&gt;&lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;Stop Wasting Groceries&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;(Dev note: The site is built for lightning-fast searches using Vue 3, Express, and MongoDB).&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  💬 Let's Talk!
&lt;/h3&gt;

&lt;p&gt;Click the links above, give it a spin, and let me know if it finds you something good. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also: What is your ultimate "clean-out-the-fridge" meal?&lt;/strong&gt; Drop it below! 👇&lt;/p&gt;

&lt;h1&gt;
  
  
  Food #Recipes #Cooking #WebDev #VueJS
&lt;/h1&gt;

</description>
      <category>food</category>
      <category>recipes</category>
      <category>cooking</category>
      <category>lifehacks</category>
    </item>
    <item>
      <title>I built a Recipe Finder using Vue 3, Express.js, and MongoDB 🍳🚀</title>
      <dc:creator>Rusu Ionut</dc:creator>
      <pubDate>Tue, 24 Mar 2026 20:25:05 +0000</pubDate>
      <link>https://dev.to/johnrusu/i-built-a-recipe-finder-using-vue-3-expressjs-and-mongodb-2alg</link>
      <guid>https://dev.to/johnrusu/i-built-a-recipe-finder-using-vue-3-expressjs-and-mongodb-2alg</guid>
      <description>&lt;p&gt;Hey Dev Community! 👋 &lt;/p&gt;

&lt;p&gt;We’ve all been there: you’re staring at a half-empty fridge wondering if you can actually make a meal out of a bell pepper and some leftover rice. To solve my own "what’s for dinner" fatigue, I built &lt;strong&gt;&lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;recipe-finder.org&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  🛠️ The Tech Stack
&lt;/h3&gt;

&lt;p&gt;I wanted this project to be fast, reactive, and easy to scale. Here’s how I put it together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend: Vue 3&lt;/strong&gt; I leaned heavily into Vue’s reactivity system. The goal was a seamless UI where ingredients could be added or removed without any clunky page refreshes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend: Express.js&lt;/strong&gt; I went with Express to keep the API layer robust yet lightweight. It handles the logic between the user's pantry and the recipe database with minimal overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database: MongoDB&lt;/strong&gt; Since recipe data can be pretty unstructured, MongoDB's flexible document schema made it the perfect choice for efficient querying.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🚀 Give it a spin!
&lt;/h3&gt;

&lt;p&gt;I’m officially launching it today and would love for you to check it out:&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://recipe-finder.org" rel="noopener noreferrer"&gt;recipe-finder.org&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  💬 Let's Talk!
&lt;/h3&gt;

&lt;p&gt;I’d love to hear your thoughts on the UI/UX or any features you'd like to see added. Also, I’m curious—&lt;strong&gt;what is your favorite "clean-out-the-fridge" meal?&lt;/strong&gt; 🍲&lt;/p&gt;

&lt;h1&gt;
  
  
  WebDevelopment #VueJS #ExpressJS #MongoDB #FullStack #SoftwareEngineering #ProjectLaunch #RecipeFinder
&lt;/h1&gt;

</description>
      <category>javascript</category>
      <category>food</category>
      <category>recipes</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
