<?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: Francesco Esposito</title>
    <description>The latest articles on Forem by Francesco Esposito (@espfra95).</description>
    <link>https://forem.com/espfra95</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%2F3008250%2Ff86751f4-1164-4454-848f-fa3815bc893c.png</url>
      <title>Forem: Francesco Esposito</title>
      <link>https://forem.com/espfra95</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/espfra95"/>
    <language>en</language>
    <item>
      <title>PaaS vs VPS: Why in 2026 I Use Render, Railway, and Hostinger at the Same Time</title>
      <dc:creator>Francesco Esposito</dc:creator>
      <pubDate>Thu, 19 Mar 2026 14:45:40 +0000</pubDate>
      <link>https://forem.com/espfra95/paas-vs-vps-why-in-2026-i-use-render-railway-and-hostinger-at-the-same-time-23kh</link>
      <guid>https://forem.com/espfra95/paas-vs-vps-why-in-2026-i-use-render-railway-and-hostinger-at-the-same-time-23kh</guid>
      <description>&lt;h2&gt;
  
  
  The modern developer’s dilemma
&lt;/h2&gt;

&lt;p&gt;In the first two months of 2026, I put 3 very different projects into production, but with one thing in common: they were all monoliths.&lt;/p&gt;

&lt;p&gt;At the moment, I’ve decided to manage the deployment of the three projects using three different approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) Laravel + Render + Neon: The "Free Tier" Paradise
&lt;/h2&gt;

&lt;p&gt;Laravel is a fantastic framework, but it can be heavy for free plans. I had built a time tracking app in the last months of 2025 to study  &lt;a href="http://inertia.js/" rel="noopener noreferrer"&gt;inertia.js&lt;/a&gt;, but in a very short time it became an indispensable tool for me, and I decided to put it into production on a public server.&lt;/p&gt;

&lt;p&gt;For my private apps I often use free or almost free services (e.g. Netlify with blobs or serverless db), but we know that a Laravel app is as solid as it is complex and heavy.&lt;/p&gt;

&lt;p&gt;And then Render appeared on my path: an excellent service, available on a free plan.&lt;/p&gt;

&lt;p&gt;Right away I ran into the two real limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the first is the cold start, which could be ‘solved’ by setting up a simple external cron job that pings the URL every few minutes. But I decided not to worry about it because for a personal app it’s not a drama to wait a few seconds at startup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the second is that their free databases expire after 90 days. The solution? Neon.tech (PostgreSQL Serverless). An amazing combo, free and unstoppable. Perfect for a private use only app, but one to which I can delegate very important tasks and work with large amounts of data.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2) Flask + Railway: The Power of Containers (and the Canvas)
&lt;/h2&gt;

&lt;p&gt;I’ve always used Flask to create microservices on the fly. We all know how fast, powerful, and versatile it is. Recently &lt;a href="https://www.tabgenai.xyz" rel="noopener noreferrer"&gt;I built a SaaS&lt;/a&gt; that I really wanted to validate: little time for development, even less for deployment.&lt;/p&gt;

&lt;p&gt;I decided to cut corners where I could, and from the very beginning I used  &lt;a href="http://alpine.js/" rel="noopener noreferrer"&gt;alpine.js&lt;/a&gt;  on the frontend, with the intention of migrating to Vue js. Then after a frontend modularization test of the existing setup using Gemini CLI and Jinja2, I found myself convinced by the monolithic architecture I had just achieved, and I decided to spend time optimizing it.&lt;/p&gt;

&lt;p&gt;And then I asked myself: what about deployment? How do we behave when something that starts as a simple service becomes a monolith?&lt;/p&gt;

&lt;p&gt;Still Railway. The service I’ve always used for my microservices. You connect the repo, it reads the requirements and creates the container. All of this with pressy pay-as-you-go on resource usage.&lt;/p&gt;

&lt;p&gt;All of this while keeping the structure totally granular: if tomorrow I wanted to add a Redis worker to manage message queues in Flask, on Railway I can do it in 2 seconds by adding a "node" to the canvas.&lt;/p&gt;

&lt;h2&gt;
  
  
  3) The Offers Portal on a Hostinger VPS: When You Need Brute Force
&lt;/h2&gt;

&lt;p&gt;Here we shift gears. A complex offers portal with thousands of records and continuous updates cannot run on a free plan or an inexpensive PaaS.&lt;/p&gt;

&lt;p&gt;When the required resources go beyond  2GB  of RAM and you need many background workers, PaaS costs explode.&lt;br&gt;&lt;br&gt;
On Hostinger, with  10€-15€, I have a machine all to myself. On Railway, the same resource consumption would cost me triple.&lt;br&gt;&lt;br&gt;
Moreover, I have total control: I can install whatever I want, optimize Nginx for SEO, and manage backups however I prefer.&lt;br&gt;&lt;br&gt;
The downside: I have to manage security, operating system updates, and firewall configuration myself. That is the price of freedom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Which One Should You Choose?
&lt;/h2&gt;

&lt;p&gt;Need to validate an idea? Go with Railway.&lt;/p&gt;

&lt;p&gt;Do you have a personal but complex app and want peace of mind? Render.&lt;/p&gt;

&lt;p&gt;Is the project serious, heavy, and you want to optimize costs? Get your hands dirty on a VPS.&lt;/p&gt;

</description>
      <category>vps</category>
      <category>saas</category>
      <category>php</category>
      <category>python</category>
    </item>
    <item>
      <title>Stop Paying for APIs: Build a 100% Local AI Auditor with Python &amp; Llama 3</title>
      <dc:creator>Francesco Esposito</dc:creator>
      <pubDate>Wed, 11 Feb 2026 11:38:55 +0000</pubDate>
      <link>https://forem.com/espfra95/beyond-basic-automation-building-a-web-analyzer-with-python-and-rag-2ecd</link>
      <guid>https://forem.com/espfra95/beyond-basic-automation-building-a-web-analyzer-with-python-and-rag-2ecd</guid>
      <description>&lt;p&gt;If you have worked in tech or digital operations for a few years, you likely know &lt;strong&gt;Python&lt;/strong&gt; as the "Swiss Army Knife" of daily tasks. For a long time, it has been the undisputed king of local automation. We’ve all used it (or seen it used) to merge dozens of CSV files in seconds, batch-rename image archives, scrape documents from the web, or manipulate Excel sheets without ever opening them.&lt;/p&gt;

&lt;p&gt;These scripts are fantastic, but they have a hard limit: they are "blind." They execute rigid instructions. If a file structure changes, the script breaks. Most importantly, they don't understand the &lt;em&gt;content&lt;/em&gt; they are processing.&lt;/p&gt;

&lt;p&gt;Today, thanks to the integration of Generative AI, Python is experiencing a second youth. We are no longer just &lt;em&gt;moving&lt;/em&gt; data; we can now &lt;em&gt;understand&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;Welcome to the world of &lt;strong&gt;RAG&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is RAG and Why it Beats a Standard Chatbot
&lt;/h2&gt;

&lt;p&gt;Imagine asking a standard AI (like ChatGPT or Claude) to analyze your specific website against your company's internal brand guidelines. You will likely get vague answers or "hallucinations" because the model doesn't know your real-time data or your specific private criteria.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;RAG (Retrieval-Augmented Generation)&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;RAG is a technique that combines the linguistic power of AI with the precision of a private library. Instead of relying solely on the model's pre-trained memory (which is often outdated), a RAG system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retrieves&lt;/strong&gt; relevant information from an external source you provide (a PDF, a database, or in this case, a live website).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Augments&lt;/strong&gt; the prompt sent to the AI with this fresh context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generates&lt;/strong&gt; an answer based &lt;em&gt;exclusively&lt;/em&gt; on that data.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The advantage?&lt;/strong&gt; Fewer hallucinations, up-to-date data, and the ability to apply &lt;em&gt;your&lt;/em&gt; business rules to &lt;em&gt;your&lt;/em&gt; data—all potentially running locally on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project: An AI Marketing Auditor
&lt;/h2&gt;

&lt;p&gt;To demonstrate this power, I’ve written a compact Python script. The goal is to create a &lt;strong&gt;Marketing Auditor&lt;/strong&gt;: a tool that reads a website, compares it against our internal marketing criteria (Tone of Voice, SEO, Call to Action), and provides a score with strategic advice.&lt;/p&gt;

&lt;p&gt;We will use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LangChain:&lt;/strong&gt; The framework to orchestrate the workflow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ollama (Llama 3):&lt;/strong&gt; To run the AI locally (total privacy, zero API costs).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ChromaDB:&lt;/strong&gt; To store website data in vector format.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Code
&lt;/h3&gt;

&lt;p&gt;Here is the complete code &lt;code&gt;app.py&lt;/code&gt;. To run this, you would need a local folder named &lt;code&gt;criteri&lt;/code&gt; containing &lt;code&gt;.txt&lt;/code&gt; files with your guidelines (e.g., &lt;code&gt;seo_copywriting.txt&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: The comments and prompts have been translated to English for this article).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Python&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os
import time
from tqdm import tqdm
from langchain_community.document_loaders import WebBaseLoader, TextLoader
from langchain_community.vectorstores import Chroma
from langchain_ollama import OllamaEmbeddings, ChatOllama
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA

# --- CONFIGURATION ---
MODEL_NAME = "llama3"
CRITERIA_DIR = "./criteria" # Directory for your txt guidelines

def load_marketing_criteria(directory):
    """Loads the defined marketing txt files."""
    combined_content = ""
    # Example files you might have in your folder
    target_files = ["tone_of_voice.txt", "call_to_action.txt", "seo_copywriting.txt"]

    if not os.path.exists(directory):
        return None

    for filename in target_files:
        path = os.path.join(directory, filename)
        if os.path.exists(path):
            loader = TextLoader(path, encoding='utf-8')
            combined_content += f"\n--- CRITERIA: {filename.upper()} ---\n"
            combined_content += loader.load()[0].page_content
    return combined_content

def run_rag_analysis():
    # User Input
    print("--- RAG Marketing Analyzer ---")
    target_url = input("Enter the URL to analyze: ").strip()

    if not target_url.startswith("http"):
        print("Error: Please enter a valid URL (must start with http or https).")
        return

    # Progress Bar for visual feedback
    pbar = tqdm(total=100, desc="Analysis Progress", bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt}%")

    try:
        # Step 1: Load Criteria (The "Manual")
        criteria = load_marketing_criteria(CRITERIA_DIR)
        if not criteria:
            pbar.close()
            print("Error: Ensure .txt files are in the 'criteria' folder.")
            return
        pbar.update(20)

        # Step 2: Site Scraping
        loader = WebBaseLoader(target_url)
        data = loader.load()
        pbar.update(20)

        # Step 3: Text Chunking
        text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
        all_splits = text_splitter.split_documents(data)
        pbar.update(20)

        # Step 4: Vector Store Creation (Embeddings)
        embeddings = OllamaEmbeddings(model=MODEL_NAME)
        vectorstore = Chroma.from_documents(documents=all_splits, embedding=embeddings)
        pbar.update(20)

        # Step 5: Llama 3 Analysis
        llm = ChatOllama(model=MODEL_NAME, temperature=0.2)

        prompt_str = f"""
        You are a Senior Marketing Auditor. Analyze the provided website using these criteria:
        {criteria}

        Provide a report containing:
        1. An analysis for each category with a score from 1 to 10.
        2. A final average score.
        3. A strategic recommendation.
        """

        qa_chain = RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff",
            retriever=vectorstore.as_retriever()
        )

        result = qa_chain.invoke(prompt_str)
        pbar.update(20)
        pbar.close()

        # Final Output
        print("\n" + "="*60)
        print(f"REPORT FOR: {target_url}")
        print("="*60)
        print(result["result"])
        print("="*60)

    except Exception as e:
        pbar.close()
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    run_rag_analysis()

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How It Works "Under the Hood"
&lt;/h3&gt;

&lt;p&gt;For those who want to build similar tools, here is the breakdown of the logic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context Loading (&lt;code&gt;load_marketing_criteria&lt;/code&gt;):&lt;/strong&gt; Before looking at the website, the script loads the "rules of the game." It reads local text files containing your best practices. This is what differentiates a generic analysis from a custom one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Web Scraping (&lt;code&gt;WebBaseLoader&lt;/code&gt;):&lt;/strong&gt; LangChain downloads the HTML content of the target URL and converts it into raw text.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chunking &amp;amp; Embeddings:&lt;/strong&gt; This is where the magic happens. The website text is split into small, digestible pieces ("chunks") and converted into numerical vectors using &lt;code&gt;OllamaEmbeddings&lt;/code&gt;. These are stored in &lt;code&gt;Chroma&lt;/code&gt;, a vector database that allows the AI to retrieve only the paragraphs semantically relevant to our query.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generation (&lt;code&gt;ChatOllama&lt;/code&gt;):&lt;/strong&gt; Finally, we pass everything to Llama 3. We give it the criteria, we give it the website content retrieved from the database, and we ask it to act as a "Senior Marketing Auditor."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion: A Game Changer for Productivity
&lt;/h2&gt;

&lt;p&gt;This script is just a prototype—a simple example of what can be achieved in under 100 lines of code. However, the implications are massive.&lt;/p&gt;

&lt;p&gt;Imagine scaling this concept. It’s not just for analyzing a website; consider the possibilities for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consultancy:&lt;/strong&gt; Analyzing financial statements or annual reports from 50 different companies and comparing them against your firm's specific investment criteria.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Management:&lt;/strong&gt; Automatically summarizing weekly support tickets to identify recurring trends without reading them one by one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HR:&lt;/strong&gt; comparing hundreds of CVs against a specific job description saved locally.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coupling Python's automation capabilities with RAG models transforms the computer from a simple executor into an &lt;strong&gt;active collaborator&lt;/strong&gt;. It drastically reduces the time spent on repetitive analysis, freeing up professionals, managers, and employees to focus on what truly matters: strategy and creativity.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>I used to hate Laravel. Now it’s my 2026 primary stack</title>
      <dc:creator>Francesco Esposito</dc:creator>
      <pubDate>Fri, 06 Feb 2026 09:15:23 +0000</pubDate>
      <link>https://forem.com/espfra95/i-used-to-hate-laravel-now-its-my-2026-primary-stack-47ff</link>
      <guid>https://forem.com/espfra95/i-used-to-hate-laravel-now-its-my-2026-primary-stack-47ff</guid>
      <description>&lt;p&gt;&lt;strong&gt;Just kidding, it’s not my primary stack... yet.&lt;/strong&gt; But after delivering two successful projects with it and having two more currently in production, the transition is undeniable. Here is how I went from being a hater to (almost) a full-time advocate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Dark" Ages (Late Junior Days)
&lt;/h2&gt;

&lt;p&gt;In 2019, at the end of my junior days, I had my first real encounter with Laravel. I wasn't just a frontend dev looking at the surface; I was already comfortable with &lt;strong&gt;SQL and PHP&lt;/strong&gt;, so I dove deep into the backend architecture.&lt;/p&gt;

&lt;p&gt;At the time, I was also working on a stack involving &lt;strong&gt;Angular for the frontend and .NET with various microservices for the backend.&lt;/strong&gt; Comparing the two felt like night and day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Angular + Microservices&lt;/strong&gt; felt like the future: modular, scalable, and modern.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Laravel&lt;/strong&gt; felt like the past: messy, bloated, and unnecessarily complex.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I honestly didn't see any sense in continuing to invest time in that stack. I walked away for years, moving toward Flask, Firebase, and distributed systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Aha!" Moment: Laravel Breeze
&lt;/h2&gt;

&lt;p&gt;Fast forward to last September. I stumbled upon a YouTube video showcasing &lt;strong&gt;Laravel Breeze&lt;/strong&gt;. It didn't look like the Laravel I remembered. It was clean, minimal, and remarkably modern.&lt;/p&gt;

&lt;p&gt;My curiosity was piqued, but the real breakthrough happened in December. Having a few days off, I decided to give it a serious try.&lt;/p&gt;

&lt;h3&gt;
  
  
  From Zero to Production-Ready in 2 Hours
&lt;/h3&gt;

&lt;p&gt;The efficiency was staggering. In just two hours:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hour 1:&lt;/strong&gt; I read the documentation and dove into &lt;strong&gt;Inertia.js&lt;/strong&gt; and &lt;strong&gt;Livewire&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hour 2:&lt;/strong&gt; I developed a 5-field form with advanced validation using &lt;strong&gt;Inertia + Vue&lt;/strong&gt;, plus a &lt;strong&gt;Livewire&lt;/strong&gt; backend to display and sort records behind an auth wall.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It wasn't just working; it was &lt;em&gt;elegant&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Deep: The Time-Tracking App
&lt;/h2&gt;

&lt;p&gt;To truly test the limits, I built a complete time-tracking application for myself. It features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Detailed Reporting:&lt;/strong&gt; General and client-specific summaries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Management:&lt;/strong&gt; Full CRUD for clients and hours with a daily table view.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Insights:&lt;/strong&gt; A reporting dashboard with charts and a CSV export feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Filtering:&lt;/strong&gt; Every data view allows for 7-day, 30-day, or custom range filtering.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Filament Changed Everything
&lt;/h2&gt;

&lt;p&gt;Then I explored &lt;strong&gt;Filament&lt;/strong&gt; further, and... wow. It’s a game-changing system that accelerates interface development. It allows you to focus on complex backend logic rather than wasting hours aligning buttons and managing state—which is a massive time-sink in admin dashboards.&lt;/p&gt;

&lt;p&gt;I was so convinced that I successfully pitched a stack change for a current project. We moved to &lt;strong&gt;Laravel + Filament + Inertia&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By leveraging &lt;strong&gt;Qwen + Claude Code (local AI - free and secure)&lt;/strong&gt; for the "horizontal" update of the MVC structure, I further reduced boilerplate time. This allowed me to focus 100% on &lt;strong&gt;query optimization, business logic, and application security.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: A New Love Story
&lt;/h2&gt;

&lt;p&gt;I’ve officially fallen in love with Laravel. I’ve completely sidelined Flask, Firebase, and those "tons of microservices" that were becoming a maintenance nightmare.&lt;/p&gt;

&lt;p&gt;My roadmap for the next six months? One major project already in production, and two more in the pipeline, all built with &lt;strong&gt;Laravel, Livewire, and Inertia.js&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you haven't touched PHP or Laravel in years, do yourself a favor: give it one hour. It might just become your favorite tool for 2026.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I Built a 3D Product Configurator using Alpine.js &amp; Three.js (No React required!)</title>
      <dc:creator>Francesco Esposito</dc:creator>
      <pubDate>Fri, 05 Dec 2025 18:23:00 +0000</pubDate>
      <link>https://forem.com/espfra95/how-i-built-a-3d-product-configurator-using-alpinejs-threejs-no-react-required-ah6</link>
      <guid>https://forem.com/espfra95/how-i-built-a-3d-product-configurator-using-alpinejs-threejs-no-react-required-ah6</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Goal&lt;/strong&gt;&lt;br&gt;
I recently took on a challenge to build a real-time 3D product configurator for custom adhesive tapes. The objective was to allow users to customize dimensions (width, length), materials, and graphical elements (text, logos) on a 2D canvas, and see the results instantly projected onto a 3D model.&lt;/p&gt;

&lt;p&gt;Instead of reaching for heavy frameworks like React Three Fiber, I wanted to explore a lightweight, modular approach using Native ES Modules and Alpine.js for state management: a perfect frontend for  WooCommerce plugin development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://3d-config-js.netlify.app/" rel="noopener noreferrer"&gt;https://3d-config-js.netlify.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/Noise1995/3d-config-js" rel="noopener noreferrer"&gt;https://github.com/Noise1995/3d-config-js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architecture:&lt;/strong&gt; A Linear Rendering Pipeline**&lt;br&gt;
The core challenge of this project was not just rendering a 3D model, but synchronizing a 2D user input with a 3D texture in real-time. To manage this, I organized the codebase into a strict data pipeline where each module handles a specific stage of the transformation.&lt;/p&gt;

&lt;p&gt;Here is how the application flows, from the user's click to the final 3D pixel:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Controller (app.js)&lt;/strong&gt;&lt;br&gt;
This is the entry point and the "brain" of the application. It uses Alpine.js to manage the reactive state. It listens for user inputs (like changing width or typing text) and orchestrates the other modules. It does not handle rendering logic directly; it simply tells the other modules when to update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The 2D Designer (editor2d.js)&lt;/strong&gt;&lt;br&gt;
This module wraps Fabric.js. It handles the interactive HTML canvas where users drag and drop logos or edit text. Its only responsibility is to capture user intent and provide a snapshot (Data URL) of the current design when requested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The Compositor (composer.js)&lt;/strong&gt;&lt;br&gt;
This is the middleware. When the state changes, this module acts as a bridge. It takes the snapshot from the 2D Designer and merges it with the base material textures (like the paper grain of the tape). Crucially, it handles Distortion Correction. Since the 3D model scales based on the tape width, a simple texture mapping would result in stretched logos. This module mathematically compensates for that stretch before generating the final texture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The 3D Engine (scene3d.js&lt;/strong&gt;)&lt;br&gt;
This module wraps Three.js. It initializes the WebGL renderer, lights, and the GLB model. It exposes specific methods (like updateTexture or updateModelScale) that the Controller calls. It remains "dumb" regarding the app logic; it only knows how to render what it is given.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation Details&lt;/strong&gt;&lt;br&gt;
Reactive State with Alpine.js&lt;br&gt;
I moved away from vanilla DOM manipulation to Alpine.js to keep the code declarative. The store holds the configuration and watches for changes.&lt;/p&gt;

&lt;p&gt;JavaScript&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// app.js
document.addEventListener('alpine:init', () =&amp;gt; {
    Alpine.data('tapeConfigurator', () =&amp;gt; ({
        config: {
            width: 50,
            length: 66,
            materialId: 'standard'
        },

        init() {
            init3D('preview-3d');

            // Watcher: When width changes, trigger the pipeline
            this.$watch('config.width', () =&amp;gt; {
                this.refreshArtwork();
            });
        },

        refreshArtwork() {
            // Trigger the Composer -&amp;gt; then update the 3D Scene
            updateCompositeArtwork(this.config.width, (textureUrl) =&amp;gt; {
                updateTexture(textureUrl);
                updateModelScale(this.config.width);
            });
        }
    }));
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Compositing Logic&lt;/strong&gt;&lt;br&gt;
The most complex part was mapping the 2D canvas onto the cylinder without distortion. The composer.js creates a hidden canvas to generate the final material map.&lt;/p&gt;

&lt;p&gt;JavaScript&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// composer.js
export function updateCompositeArtwork(widthMm, onResult) {
    // 1. Get raw image from Fabric.js
    const patchData = getSnapshot(); 

    // 2. Calculate aspect ratio compensation
    const stretchFactor = widthMm / CONSTANTS.BASE_3D_WIDTH_MM;
    const compensationY = 1 / stretchFactor;

    // 3. Draw to hidden canvas with inverse scaling
    ctx.scale(1, compensationY);
    ctx.drawImage(imgPatch, x, y, width, height);

    // 4. Return new texture URL
    onResult(tempCanvas.toDataURL('image/png'));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance Considerations&lt;/strong&gt;&lt;br&gt;
Since we are generating textures on the fly, performance is key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debouncing:&lt;/strong&gt; I ensured the texture regeneration doesn't fire on every single keystroke, but batches updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Texture Reuse:&lt;/strong&gt; Three.js materials are resource-heavy. Instead of creating new materials, I swap the .map property and flag needsUpdate = true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialization Delay:&lt;/strong&gt; To prevent layout thrashing or black screens, the 3D engine waits for the CSS layout to compute container dimensions before initializing the WebGL context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
This project demonstrates that you can build complex, interactive 3D applications without the complexity of a build step or a heavy SPA framework. By strictly separating the concerns—State (Alpine), 2D (Fabric), and 3D (Three.js)—the code remains clean, maintainable, and easy to extend.&lt;/p&gt;

&lt;p&gt;Feel free to explore the code structure in the repository.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Smooth Scroll and Fade-in in Vanilla JS: A Portfolio Experiment</title>
      <dc:creator>Francesco Esposito</dc:creator>
      <pubDate>Wed, 03 Dec 2025 12:28:42 +0000</pubDate>
      <link>https://forem.com/espfra95/smooth-scroll-and-fade-in-in-vanilla-js-a-portfolio-experiment-31fd</link>
      <guid>https://forem.com/espfra95/smooth-scroll-and-fade-in-in-vanilla-js-a-portfolio-experiment-31fd</guid>
      <description>&lt;p&gt;In recent months, I've experimented with an alternative approach for smooth scroll and fade-in animations on my personal portfolio site. Instead of relying on libraries like GSAP—which I typically use for client projects—I wanted to test the power of vanilla JavaScript to achieve fluid, performant results without external dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Vanilla JS?&lt;/strong&gt;&lt;br&gt;
The goal was straightforward: reduce reliance on third-party libraries, improve initial page load times, and deepen understanding of native browser APIs. The outcome exceeded expectations—a customizable smooth scroll paired with scroll-triggered fade-ins that deliver a premium user experience without sacrificing performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core Smooth Scroll Implementation&lt;/strong&gt;&lt;br&gt;
This vanilla JS smooth scroll uses requestAnimationFrame for silky animations and direct content positioning control. Here's the essential code—plug it into your project by calling initSmoothScroll(wrapper, content) with your container elements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;window._smoothScrollState = {
  rafId: null,
  handlers: {},
  wrapper: null,
  content: null,
  current: 0,
  target: 0
};

window.initSmoothScroll = function (wrapper, content) {
  if (window._smoothScrollState.rafId) {
    window.destroySmoothScroll();
  }

  const ease = 0.08;
  let touchStart = 0;
  const mobileScrollFactor = 2.5;

  Object.assign(wrapper.style, {
    position: 'fixed',
    width: '100vw',
    height: '100vh',
    overflow: 'hidden',
    top: '0',
    left: '0'
  });

  function setBodyHeight() {
    document.body.style.height = content.scrollHeight + 'px'; 
  }

  const onWheel = function(e) {
    e.preventDefault();
    window._smoothScrollState.target += e.deltaY;
    window._smoothScrollState.target = Math.max(
      0,
      Math.min(
        window._smoothScrollState.target,
        content.scrollHeight - window.innerHeight
      )
    );
  };

  const onTouchStart = function(e) {
    touchStart = e.touches[0].clientY;
  };

  const onTouchMove = function(e) {
    e.preventDefault();
    const touchY = e.touches[0].clientY;
    let delta = touchStart - touchY;
    delta *= mobileScrollFactor;
    window._smoothScrollState.target += delta;
    window._smoothScrollState.target = Math.max(
      0,
      Math.min(
        window._smoothScrollState.target,
        content.scrollHeight - window.innerHeight
      )
    );
    touchStart = touchY;
  };

  const onResize = function() {
    setBodyHeight();
    window._smoothScrollState.target = Math.max(
      0,
      Math.min(
        window._smoothScrollState.target,
        content.scrollHeight - window.innerHeight
      )
    );
  };

  function smoothScroll() {
    let state = window._smoothScrollState;
    state.current += (state.target - state.current) * ease;
    if (Math.abs(state.target - state.current) &amp;lt; 0.1) state.current = state.target;
    content.style.transform = `translateY(${-state.current}px)`;
    state.rafId = requestAnimationFrame(smoothScroll);
  }

  window.addEventListener('wheel', onWheel, { passive: false });
  window.addEventListener('touchstart', onTouchStart, { passive: false });
  window.addEventListener('touchmove', onTouchMove, { passive: false });
  window.addEventListener('resize', onResize);

  window._smoothScrollState.handlers = { onWheel, onTouchStart, onTouchMove, onResize };
  window._smoothScrollState.wrapper = wrapper;
  window._smoothScrollState.content = content;

  setBodyHeight();
  smoothScroll();
};

window.destroySmoothScroll = function() {
  const state = window._smoothScrollState;
  if (!state) return;

  if (state.rafId) {
    cancelAnimationFrame(state.rafId);
    state.rafId = null;
  }

  if (state.handlers) {
    window.removeEventListener('wheel', state.handlers.onWheel);
    window.removeEventListener('touchstart', state.handlers.onTouchStart);
    window.removeEventListener('touchmove', state.handlers.onTouchMove);
    window.removeEventListener('resize', state.handlers.onResize);
    state.handlers = {};
  }

  if (state.content) {
    state.content.style.transform = 'translateY(0px)';
  }

  state.current = 0;
  state.target = 0;

  if (state.wrapper) {
    state.wrapper.style.position = '';
    state.wrapper.style.width = '';
    state.wrapper.style.height = '';
    state.wrapper.style.overflow = '';
    state.wrapper.style.top = '';
    state.wrapper.style.left = '';
  }

  document.body.style.height = '';
  state.wrapper = null;
  state.content = null;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scroll-Triggered Fade-ins&lt;br&gt;
For fade-in effects, IntersectionObserver detects when elements enter the viewport and triggers smooth transitions—no polling or heavy loops needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function initFadeIns() {
  const elements = document.querySelectorAll('.fade-in');
  const observer = new IntersectionObserver((entries) =&amp;gt; {
    entries.forEach(entry =&amp;gt; {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible');
      }
    });
  }, { threshold: 0.1 });

  elements.forEach(el =&amp;gt; observer.observe(el));
}

// Initialize on load
initFadeIns();

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair it with this CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.fade-in {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.fade-in.visible {
  opacity: 1;
  transform: translateY(0);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;br&gt;
This vanilla approach rivals GSAP in smoothness while keeping bundle sizes tiny and control total. Tweak the ease value for faster/slower scrolls, adjust thresholds for fade timing, and scale it across projects. Drop a comment if you implement it—happy to discuss optimizations!&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>ui</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
