<?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>Forem: Rusu Ionut</title>
    <description>The latest articles on Forem by Rusu Ionut (@johnrusu).</description>
    <link>https://forem.com/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>Forem: Rusu Ionut</title>
      <link>https://forem.com/johnrusu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/johnrusu"/>
    <language>en</language>
    <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://forem.com/johnrusu/how-i-built-a-social-recipe-extractor-that-turns-short-form-video-links-into-structured-recipes-11j6</link>
      <guid>https://forem.com/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://forem.com/johnrusu/recipe-finderorg-is-officially-on-android-4e16</link>
      <guid>https://forem.com/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://forem.com/johnrusu/from-modal-to-full-page-how-we-refactored-a-vue-3-recipe-detail-view-hef</link>
      <guid>https://forem.com/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://forem.com/johnrusu/building-a-self-healing-seo-architecture-for-a-vue-spa-4lfb</link>
      <guid>https://forem.com/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://forem.com/johnrusu/get-a-free-month-of-premium-4go4</link>
      <guid>https://forem.com/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://forem.com/johnrusu/building-recipe-finderorg-a-full-stack-journey-with-vue-express-mongodb-and-vuetify-2k57</link>
      <guid>https://forem.com/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://forem.com/johnrusu/turn-viral-recipe-videos-into-clean-recipe-cards-instantly-p1k</link>
      <guid>https://forem.com/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://forem.com/johnrusu/empty-fridge-i-built-a-tool-that-tells-you-what-to-cook-3a70</link>
      <guid>https://forem.com/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://forem.com/johnrusu/i-built-a-recipe-finder-using-vue-3-expressjs-and-mongodb-2alg</link>
      <guid>https://forem.com/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>
