<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Maksim Baranov</title>
    <description>The latest articles on DEV Community by Maksim Baranov (@rsale).</description>
    <link>https://dev.to/rsale</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%2F3686168%2Fc52397e4-eddd-49cf-9e2d-8b8d45f743ba.png</url>
      <title>DEV Community: Maksim Baranov</title>
      <link>https://dev.to/rsale</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rsale"/>
    <language>en</language>
    <item>
      <title>Kako vaš oglas na rsale.net dolazi do Google-a: SEO jednostavnim rečima</title>
      <dc:creator>Maksim Baranov</dc:creator>
      <pubDate>Sun, 03 May 2026 11:31:01 +0000</pubDate>
      <link>https://dev.to/rsale/kako-vas-oglas-na-rsalenet-dolazi-do-google-a-seo-jednostavnim-recima-4a8c</link>
      <guid>https://dev.to/rsale/kako-vas-oglas-na-rsalenet-dolazi-do-google-a-seo-jednostavnim-recima-4a8c</guid>
      <description>&lt;p&gt;Postavili ste oglas. Šta dalje? U idealnom slučaju pronaći će ga ne samo posetioci sajta, već i ljudi koji guglaju „prodaje se iPhone 14 Pro Beograd” ili „iznajmljivanje bicikla Novi Sad”. Da bi to funkcionisalo, nije dovoljno samo objaviti proizvod, potrebno je da ga Google razume, indeksira i prikaže u pravom trenutku.&lt;/p&gt;

&lt;p&gt;U ovom članku, bez tehničkog žargona, objasniću šta tačno &lt;a href="https://rsale.net" rel="noopener noreferrer"&gt;rsale.net&lt;/a&gt; radi za vas kako bi vaši oglasi bili pronađeni u pretrazi, i šta vi sami možete da uradite da biste dobijali više poziva i poruka.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zašto je SEO za oglase uopšte bitan&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kada čovek želi nešto da kupi, retko ide direktno na sajt. Prvo gugla. I od toga na kojoj će se strani rezultata pojaviti vaš oglas direktno zavisi da li će ga uopšte videti. Razlika između prve i druge strane pretrage je razlika između desetina poruka i tišine.&lt;/p&gt;

&lt;p&gt;Dobra vest: na rsale.net mi automatski radimo većinu SEO posla. Loša vest: preostalih 20% rešava sve, a tih 20% zavisi od vas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3djz0x2xt01k4359z372.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3djz0x2xt01k4359z372.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Šta radimo za vas (čak i ako ne razmišljate o tome)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Automatski prevod na tri jezika.&lt;/em&gt; Vi pišete oglas na srpskom, a on se automatski pojavljuje u katalogu na ruskom i engleskom. To znači da vaš proizvod vidi tri puta više ljudi: i lokalni Srbi, i ekspati koji govore engleski, i rusofona dijaspora. Google indeksira svaku jezičku verziju zasebno, kao tri različite strane koje vode ka istom proizvodu.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Razumljive adrese strana.&lt;/em&gt; URL vašeg oglasa ne izgleda kao &lt;code&gt;/listing/12345&lt;/code&gt;, već kao &lt;code&gt;/listing/12345-prodajem-iphone-14-pro-odlicno-stanje&lt;/code&gt;. To je važno iz dva razloga: Google voli čitljive URL-ove i uzima u obzir ključne reči u njima, a čovek u rezultatima pretrage vidi smislen naslov i razume na koji link klikće.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Mapa sajta i hreflang za Google.&lt;/em&gt; To su tehničke stvari, ali ukratko: automatski šaljemo Google-u kompletnu listu svih vaših oglasa sa oznakom „ova strana je na srpskom, ova je na ruskom, ova na engleskom, sve govore o istom proizvodu”. Google se ne zbunjuje i prikazuje korisniku verziju na njegovom jeziku.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Strukturirani podaci o proizvodu.&lt;/em&gt; Svaki oglas označavamo kao &lt;code&gt;Product&lt;/code&gt; po standardu schema.org. To znači da Google ne vidi samo tekst, već strukturiranu karticu: naziv, cenu, fotografiju, stanje (novo/polovno), dostupnost, prodavca. Zahvaljujući tome, vaš oglas u pretrazi može izgledati sa fotografijom, cenom i zvezdicama recenzija, a takav prikaz dobija 2–3 puta više klikova.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Brzina i mobilna optimizacija.&lt;/em&gt; Sajt radi brzo (Lighthouse Performance 95+), a i to je faktor rangiranja: Google snižava spore sajtove u rezultatima. Da se naša početna strana učitava 5 sekundi, vaši oglasi prosto ne bi stizali do vrha.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Šta zavisi od vas (i donosi pravi rezultat)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;1. Naslov je polovina uspeha.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Loše: „Ajfon nov jeftino”&lt;br&gt;
Dobro: „iPhone 14 Pro 256 GB Space Black, nov, garancija Apple Beograd”&lt;/p&gt;

&lt;p&gt;Princip je jednostavan: zamislite šta čovek kuca u Google. On ne piše „ajfon nov jeftino”. On piše „iPhone 14 Pro 256” ili „iPhone 14 Pro Beograd cena”. Što tačnije vaš naslov ponavlja stvarni upit, veće su šanse da uđete u rezultate.&lt;/p&gt;

&lt;p&gt;Šta uključiti u naslov:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tačan model i karakteristike (boja, kapacitet memorije, godina)&lt;/li&gt;
&lt;li&gt;Stanje (novo, polovno, kao novo)&lt;/li&gt;
&lt;li&gt;Grad ili kvart, ako je proizvod lokalni&lt;/li&gt;
&lt;li&gt;Brend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Šta izbegavati: emodžije, VELIKA SLOVA, „hitno!!!”, „najbolja cena!!!”. Google to ignoriše ili snižava, a kupci doživljavaju kao spam.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;2. Opis za čoveka, a ne za algoritam.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ranije je radila šema „nabacaj ključne reči i izađi u vrh”. Danas je Google pametniji, i pobeđuje onaj ko opisuje proizvod onako kako bi pravi prodavac ispričao kupcu.&lt;/p&gt;

&lt;p&gt;Dobar opis odgovara na pitanja koja će kupac postaviti pre nego što vam piše:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zašto prodajete?&lt;/li&gt;
&lt;li&gt;Koliko godina/meseci u upotrebi?&lt;/li&gt;
&lt;li&gt;Šta je u kompletu?&lt;/li&gt;
&lt;li&gt;Postoji li račun, garancija, kutija?&lt;/li&gt;
&lt;li&gt;Da li ste spremni na cenkanje, zamenu?&lt;/li&gt;
&lt;li&gt;Da li je moguća dostava?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Što kupac ima manje pitanja nakon čitanja, to više poruka, i to više Google podiže vaš oglas: algoritam vidi da ljudi pročitaju do kraja i kliknu, a ne da odmah odu.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;3. Fotografije su važnije nego što izgleda.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Minimum 5–7 fotografija, prva je najbolja. Slikajte pri dnevnom svetlu, na neutralnoj pozadini, bez filtera. Pokažite proizvod sa svih strana, posebno defekte ako ih ima, posebno komplet. Oglase sa jednom mutnom fotografijom algoritam snižava: niska angažovanost = niske pozicije.&lt;/p&gt;

&lt;p&gt;Bonus: automatski generišemo umanjene verzije različitih veličina (preko &lt;a href="https://sharp.pixelplumbing.com/" rel="noopener noreferrer"&gt;&lt;code&gt;sharp&lt;/code&gt;&lt;/a&gt;) i &lt;code&gt;srcset&lt;/code&gt;, pa se vaše fotografije brzo učitavaju i na mobilnom 4G i na laptopu. To utiče na brzinu strane, a brzina utiče na rangiranje.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;4. Tačna kategorija i grad.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Izbor kategorije nije formalnost. Ako prodajete bicikl i postavite ga u „Sport”, a ne u „Bicikli”, nećete se pojaviti u katalogu &lt;code&gt;/category/bicikli&lt;/code&gt;, a to je posebna strana koju Google indeksira upravo za upit „bicikli Srbija”. Isto sa gradom: oglas u Beogradu se rangira u lokalnoj pretrazi „kupiti ... Beograd”, ali samo ako je grad eksplicitno naveden.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;5. Cena prava, a ne „po dogovoru”.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Oglase bez cene ili sa cenom „1 dinar, zovite” i Google i rsale u katalogu prikazuju niže. I kupci filtriraju po ceni, pa ako je nemate, vas prosto ne vide pri sortiranju.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zašto su tri jezika množenje, a ne sabiranje&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Jednostavan primer. Recimo da prodajete dečija kolica u Novom Sadu. Na srpskoj publici konkurišete sa uslovno 50 drugih oglasa, i šansa za poruku je 1 prema 50. Ali ako se vaš oglas automatski pojavljuje na ruskom i engleskom, ulazite u tri zasebne grupe kupaca:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Srpski roditelji koji traže „dečja kolica Novi Sad”&lt;/li&gt;
&lt;li&gt;Ekspati koji guglaju „baby stroller Novi Sad”&lt;/li&gt;
&lt;li&gt;Rusofoni relokanti koji traže „коляска Нови-Сад”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To nije jedna publika sa prevodom, to su tri različite. Svaka sa svojim Google-om.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kako Google uopšte „razume” vaš oglas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kada objavite proizvod, dešava se otprilike sledeće:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Kartica ulazi u našu bazu.&lt;/li&gt;
&lt;li&gt;AI servis prevodi naslov i opis na druga dva jezika.&lt;/li&gt;
&lt;li&gt;URL oglasa zajedno sa tri jezičke verzije dodaje se u &lt;code&gt;sitemap.xml&lt;/code&gt;, koji čita Google.&lt;/li&gt;
&lt;li&gt;Na strani proizvoda ubacujemo JSON-LD oznake (&lt;code&gt;@type: Product&lt;/code&gt;) sa cenom, stanjem, dostupnošću, prodavcem.&lt;/li&gt;
&lt;li&gt;Google dolazi, vidi strukturiranu stranu, razume: „ovo je proizvod, nije blog, nije članak”, i stavlja u indeks proizvoda.&lt;/li&gt;
&lt;li&gt;Nakon nekoliko dana oglas počinje da se pojavljuje u običnoj pretrazi i u Google Shopping-u.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ovaj ciklus traje od par dana do par nedelja. Zato je nešto skupo i važno bolje prodavati sa rezervom u vremenu, a ne „moram da prodam danas”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Čeklista savršenog oglasa na rsale&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;U naslovu tačan model/naziv i ključna karakteristika&lt;/li&gt;
&lt;li&gt;Naveden grad (ili kvart Beograda, ako je u Beogradu)&lt;/li&gt;
&lt;li&gt;Opis odgovara na 5–7 tipičnih kupčevih pitanja&lt;/li&gt;
&lt;li&gt;Minimum 5 fotografija, prva jasna i pri dnevnom svetlu&lt;/li&gt;
&lt;li&gt;Izabrana tačna kategorija, a ne „Razno”&lt;/li&gt;
&lt;li&gt;Navedena prava cena&lt;/li&gt;
&lt;li&gt;Navedeno stanje (novo / polovno / kao novo)&lt;/li&gt;
&lt;li&gt;Popunjeni kontakt načini (poruke, dostava)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Svaka stavka košta vas 30 sekundi. Svaka preskočena stavka je minus 10–20% vidljivosti.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Česti mitovi&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;„Što češće osvežavam, to sam viši u rezultatima”.&lt;/em&gt; Delom tačno, ali osvežavanje mora biti smisleno: dodati fotografiju, ažurirati cenu, dopisati stavku u opis. Prosto „diži u vrh” dugmetom Google ne uzima u obzir, to radi samo unutar kataloga rsale.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;„Plaćeno promovisanje podiže u Google-u”.&lt;/em&gt; Ne. Plaćeno postavljanje vas podiže u katalogu rsale, ali to Google ne vidi. Da biste bili visoko u Google-u, potrebni su naslov, opis, fotografije i vreme.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;„Ako napišem više ključnih reči, biće više”.&lt;/em&gt; Tako je bilo 2010-ih. Sada Google za to snižava. Opis treba da se čita kao ljudski tekst, sa ključnim rečima prirodno utkanim, a ne nabacanim na gomilu.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zaključak&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SEO za oglase nije magija ni jednokratna radnja. To je zbir pravilno urađenih sitnica: tačan naslov, smislen opis, normalne fotografije, ispravna kategorija, prava cena. Tehnički deo (višejezičnost, sitemap, schema.org, brzina) preuzimamo na sebe. Ostalo ste vi i vaš proizvod.&lt;/p&gt;

&lt;p&gt;Ako se sve uradi kako treba, u prve dve nedelje nakon objavljivanja oglas počinje da se pojavljuje u Google-u za konkretne upite, i pozivi/poruke stižu ne samo od onih koji listaju katalog, već i od onih koji su došli iz pretrage. A to je sasvim druga publika: čovek već zna šta hoće i spreman je da kupi.&lt;/p&gt;

&lt;p&gt;Postavite oglas na &lt;a href="https://rsale.net" rel="noopener noreferrer"&gt;rsale.net&lt;/a&gt; i primenite čeklistu. Ako imate pitanja o konkretnim oglasima, pišite u komentarima, razmotrićemo.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>marketing</category>
      <category>ecommerce</category>
      <category>serbia</category>
    </item>
    <item>
      <title>From Next.js 15 to SvelteKit: Why We Rewrote Rsale.net's Frontend</title>
      <dc:creator>Maksim Baranov</dc:creator>
      <pubDate>Sun, 03 May 2026 10:25:55 +0000</pubDate>
      <link>https://dev.to/rsale/from-nextjs-15-to-sveltekit-why-we-rewrote-rsalenets-frontend-44gh</link>
      <guid>https://dev.to/rsale/from-nextjs-15-to-sveltekit-why-we-rewrote-rsalenets-frontend-44gh</guid>
      <description>&lt;p&gt;Hey dev.to! Maksim here again, author of &lt;a href="https://dev.to/rsale/from-idea-to-mvp-building-a-classified-platform-in-serbia-5gg6"&gt;the previous article "From Idea to MVP: Building a Classified Platform in Serbia"&lt;/a&gt; about &lt;strong&gt;&lt;a href="https://rsale.net" rel="noopener noreferrer"&gt;Rsale.net&lt;/a&gt;&lt;/strong&gt;, a classified platform for Serbia. In that post I described the stack: Next.js 15 + React 19 + ASP.NET Core microservices + AI translation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcagqoxf5l610kdklh3qq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcagqoxf5l610kdklh3qq.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few months later, the frontend has been &lt;strong&gt;fully rewritten on &lt;a href="https://svelte.dev/docs/kit/introduction" rel="noopener noreferrer"&gt;SvelteKit 2&lt;/a&gt; + &lt;a href="https://svelte.dev/docs/svelte/overview" rel="noopener noreferrer"&gt;Svelte 5&lt;/a&gt; (runes)&lt;/strong&gt;. The Next.js codebase is gone. In this article I'll walk through why we did it, what we gained, and where it hurt.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Project status:&lt;/strong&gt; the backend is still being finished, the site shows test data. But the new frontend is already in production-ready shape.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We migrated from Next.js 15 / React 19 to &lt;strong&gt;&lt;a href="https://svelte.dev/docs/kit/introduction" rel="noopener noreferrer"&gt;SvelteKit 2&lt;/a&gt; + &lt;a href="https://svelte.dev/docs/svelte/what-are-runes" rel="noopener noreferrer"&gt;Svelte 5 runes&lt;/a&gt;&lt;/strong&gt;. Bundle size dropped, DX improved, the Server/Client Component dualism disappeared, and i18n became a 50-line file instead of a separate library. No regrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we left Next.js&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next.js 15 is a great framework. But on a real project, a classified board with three languages, ISR, SignalR chat, geolocation, image upload, a sell wizard, three pain points piled up.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;1. The Server/Client Component dualism.&lt;/em&gt; In the previous article I called it the "Server-First + Client Wrapper" pattern. Sounds elegant on a slide. In practice every new component requires a decision: &lt;code&gt;'use client'&lt;/code&gt; or not? Can I import it from a server component? Why did the bundle suddenly grow by 40 kB? Why does this hook break SSR?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Next.js 15, a typical "wrapper" because we need one onClick&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ServerThing&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./server-thing&lt;/span&gt;&lt;span class="dl"&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;function&lt;/span&gt; &lt;span class="nf"&gt;Wrapper&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In SvelteKit there is no such dichotomy. A component is just a component. SSR happens by default, hydration too, and reactivity works the same on the client and the server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8fkux5fnx68h88hze17r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8fkux5fnx68h88hze17r.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;2. Bundle size and React runtime.&lt;/em&gt; React 19 + the Next.js runtime + &lt;code&gt;next-intl&lt;/code&gt; + &lt;code&gt;zustand&lt;/code&gt; + &lt;code&gt;framer-motion&lt;/code&gt;, even with aggressive code-splitting the baseline JS for an empty page was around 99 kB. For a classified board where users open 20–30 listings in a session, that's a real cost on Serbian 4G.&lt;/p&gt;

&lt;p&gt;Svelte is a compiler. There is no "framework runtime" shipped to the browser, only the code your components actually need.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;3. RSC, caching and the "magic".&lt;/em&gt; &lt;code&gt;fetch&lt;/code&gt; overrides, &lt;code&gt;revalidate&lt;/code&gt;, &lt;code&gt;unstable_cache&lt;/code&gt;, &lt;code&gt;dynamic = 'force-dynamic'&lt;/code&gt;, ISR with on-demand webhooks, Next.js gives a lot, but every "why isn't this updating" debugging session takes an hour. SvelteKit's model is honest and explicit: the &lt;a href="https://svelte.dev/docs/kit/load" rel="noopener noreferrer"&gt;&lt;code&gt;load&lt;/code&gt;&lt;/a&gt; function, &lt;a href="https://svelte.dev/docs/kit/page-options#prerender" rel="noopener noreferrer"&gt;&lt;code&gt;export const prerender&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://svelte.dev/docs/kit/page-options#ssr" rel="noopener noreferrer"&gt;&lt;code&gt;export const ssr&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://svelte.dev/docs/kit/page-options#csr" rel="noopener noreferrer"&gt;&lt;code&gt;export const csr&lt;/code&gt;&lt;/a&gt;. That's it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why SvelteKit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Compiler instead of runtime.&lt;/em&gt; &lt;a href="https://svelte.dev/docs/svelte/svelte-compiler" rel="noopener noreferrer"&gt;Svelte compiles components&lt;/a&gt; to plain JS that mutates the DOM directly. No virtual DOM, no reconciler, no Fiber. For a content-heavy site, that's a free performance win.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Svelte 5 + runes.&lt;/em&gt; &lt;a href="https://svelte.dev/docs/svelte/what-are-runes" rel="noopener noreferrer"&gt;Runes&lt;/a&gt; (&lt;a href="https://svelte.dev/docs/svelte/$state" rel="noopener noreferrer"&gt;&lt;code&gt;$state&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://svelte.dev/docs/svelte/$derived" rel="noopener noreferrer"&gt;&lt;code&gt;$derived&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://svelte.dev/docs/svelte/$effect" rel="noopener noreferrer"&gt;&lt;code&gt;$effect&lt;/code&gt;&lt;/a&gt;) replaced the magic &lt;code&gt;$:&lt;/code&gt; and stores from Svelte 4. The model is closer to Solid/MobX, explicit, debuggable, and works the same in &lt;code&gt;.svelte&lt;/code&gt; and &lt;code&gt;.svelte.ts&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Here's our auth store. It's a regular &lt;code&gt;.svelte.ts&lt;/code&gt; file, no &lt;code&gt;writable&lt;/code&gt;, no providers, no &lt;code&gt;useContext&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;// src/lib/stores/auth.svelte.ts&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AuthUser&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="kc"&gt;null&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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isAuthenticated&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="p"&gt;},&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;notifyAuthChanged&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;getChannel&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth:changed&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;Any component just imports &lt;code&gt;auth&lt;/code&gt; and reads &lt;code&gt;auth.user&lt;/code&gt;, reactivity is automatic. Compare with React: &lt;code&gt;useContext&lt;/code&gt; + &lt;code&gt;Provider&lt;/code&gt; + &lt;code&gt;useSyncExternalStore&lt;/code&gt; for cross-tab sync.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;File-based routing without &lt;code&gt;[locale]&lt;/code&gt; everywhere.&lt;/em&gt; In Next.js the i18n routing scheme is &lt;code&gt;app/[locale]/...&lt;/code&gt;, every route lives under a dynamic segment. In SvelteKit we handle locale in &lt;a href="https://svelte.dev/docs/kit/hooks#server-hooks" rel="noopener noreferrer"&gt;&lt;code&gt;hooks.server.ts&lt;/code&gt;&lt;/a&gt; via cookie + &lt;code&gt;Accept-Language&lt;/code&gt; and prefix URLs with a helper &lt;code&gt;lp('/category/cars')&lt;/code&gt;. Route folders stay clean: &lt;code&gt;routes/category/[id]&lt;/code&gt;, not &lt;code&gt;routes/[locale]/category/[id]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;i18n in 50 lines instead of a library.&lt;/em&gt; We dropped &lt;code&gt;next-intl&lt;/code&gt; and wrote our own thing. Three dictionaries, one &lt;code&gt;t()&lt;/code&gt; function, runes for reactivity:&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;// src/lib/i18n/index.svelte.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ru&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&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;allTranslations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Translations&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ru&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentLocale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="o"&gt;&amp;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;sr&lt;/span&gt;&lt;span class="dl"&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;function&lt;/span&gt; &lt;span class="nf"&gt;setLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;currentLocale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;locale&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;cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`locale=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; path=/; max-age=31536000; SameSite=Lax`&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;function&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TranslationKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allTranslations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentLocale&lt;/span&gt;&lt;span class="p"&gt;]?.[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;allTranslations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;key&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;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&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="s2"&gt;`{&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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;return&lt;/span&gt; &lt;span class="nx"&gt;text&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;That's it. No ICU MessageFormat (we don't need plural rules in this domain), no providers, no &lt;code&gt;next-intl&lt;/code&gt; config files. Type-safety comes from &lt;code&gt;TranslationKey = keyof typeof sr&lt;/code&gt;, TypeScript yells at any unknown key.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SSR by default, no &lt;code&gt;'use client'&lt;/code&gt;.&lt;/em&gt; Every &lt;a href="https://svelte.dev/docs/kit/routing#page-page-svelte" rel="noopener noreferrer"&gt;&lt;code&gt;+page.svelte&lt;/code&gt;&lt;/a&gt; is rendered on the server, gets hydrated on the client, and reactivity just works. No mental tax on whether to put &lt;code&gt;'use client'&lt;/code&gt; at the top.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- routes/category/[id]/+page.svelte --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&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="nx"&gt;t&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$lib/i18n/index.svelte&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$lib/stores/auth.svelte&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$props&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category.title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;#each&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/listing/{product.id}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;/each&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;#if&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isAuthenticated&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add_favorite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;/if&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This component runs on the server and on the client. No directives, no wrappers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack: before and after&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Was (Next.js 15)&lt;/th&gt;
&lt;th&gt;Now (SvelteKit 2)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 15.4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://svelte.dev/docs/kit/introduction" rel="noopener noreferrer"&gt;SvelteKit 2.50&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;React 19&lt;/td&gt;
&lt;td&gt;&lt;a href="https://svelte.dev/docs/svelte/what-are-runes" rel="noopener noreferrer"&gt;Svelte 5.51 (runes)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Zustand&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://svelte.dev/docs/svelte/$state" rel="noopener noreferrer"&gt;&lt;code&gt;$state&lt;/code&gt;&lt;/a&gt; in &lt;code&gt;.svelte.ts&lt;/code&gt; files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i18n&lt;/td&gt;
&lt;td&gt;next-intl&lt;/td&gt;
&lt;td&gt;own 50-line module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animations&lt;/td&gt;
&lt;td&gt;framer-motion&lt;/td&gt;
&lt;td&gt;CSS + &lt;a href="https://svelte.dev/docs/svelte/transition" rel="noopener noreferrer"&gt;Svelte transitions&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundler&lt;/td&gt;
&lt;td&gt;Webpack/Turbopack&lt;/td&gt;
&lt;td&gt;&lt;a href="https://vite.dev/" rel="noopener noreferrer"&gt;Vite 7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adapter&lt;/td&gt;
&lt;td&gt;Vercel/Node&lt;/td&gt;
&lt;td&gt;&lt;a href="https://svelte.dev/docs/kit/adapter-node" rel="noopener noreferrer"&gt;&lt;code&gt;@sveltejs/adapter-node&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time&lt;/td&gt;
&lt;td&gt;@microsoft/signalr 9&lt;/td&gt;
&lt;td&gt;@microsoft/signalr 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB layer&lt;/td&gt;
&lt;td&gt;(REST to ASP.NET)&lt;/td&gt;
&lt;td&gt;+ Drizzle ORM for FE-side reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Vitest + Playwright&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SignalR for chat stayed, it's a client library, framework-agnostic. The ASP.NET Core backend with microservices and the AI Translation Service didn't change at all, the rewrite was purely frontend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The migration in practice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What was easy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://svelte.dev/docs/kit/routing" rel="noopener noreferrer"&gt;Routing&lt;/a&gt;. &lt;code&gt;app/[locale]/products/[id]/page.tsx&lt;/code&gt; became &lt;code&gt;routes/products/[id]/+page.svelte&lt;/code&gt;. Conceptually 1:1.&lt;/li&gt;
&lt;li&gt;Server-side data loading. Next's &lt;code&gt;async function Page()&lt;/code&gt; became SvelteKit's &lt;a href="https://svelte.dev/docs/kit/load#Page-data" rel="noopener noreferrer"&gt;&lt;code&gt;load()&lt;/code&gt; in &lt;code&gt;+page.server.ts&lt;/code&gt;&lt;/a&gt;. Same idea, cleaner separation.&lt;/li&gt;
&lt;li&gt;API routes. &lt;code&gt;route.ts&lt;/code&gt; with &lt;code&gt;GET&lt;/code&gt;/&lt;code&gt;POST&lt;/code&gt; became &lt;a href="https://svelte.dev/docs/kit/routing#server" rel="noopener noreferrer"&gt;&lt;code&gt;+server.ts&lt;/code&gt;&lt;/a&gt; with the same exports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where we had to think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ISR analog. Next had &lt;code&gt;revalidate: 600&lt;/code&gt;. SvelteKit doesn't have ISR out of the box with &lt;a href="https://svelte.dev/docs/kit/adapter-node" rel="noopener noreferrer"&gt;&lt;code&gt;adapter-node&lt;/code&gt;&lt;/a&gt; (it does with &lt;a href="https://svelte.dev/docs/kit/adapter-vercel#Incremental-Static-Regeneration" rel="noopener noreferrer"&gt;&lt;code&gt;adapter-vercel&lt;/code&gt;&lt;/a&gt; if you host there). For static pages (about, terms, blog index) we use &lt;a href="https://svelte.dev/docs/kit/page-options#prerender" rel="noopener noreferrer"&gt;&lt;code&gt;export const prerender = true&lt;/code&gt;&lt;/a&gt;, for dynamic ones plain SSR-on-demand. With proper &lt;code&gt;Cache-Control&lt;/code&gt; headers and a CDN in front, the practical effect is the same.&lt;/li&gt;
&lt;li&gt;Image optimization. No &lt;code&gt;next/image&lt;/code&gt;. We use &lt;a href="https://sharp.pixelplumbing.com/" rel="noopener noreferrer"&gt;&lt;code&gt;sharp&lt;/code&gt;&lt;/a&gt; at upload time + a small &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; wrapper with &lt;code&gt;srcset&lt;/code&gt;. Less magic, more control.&lt;/li&gt;
&lt;li&gt;Sell wizard state. Our sell wizard was a tangle of Zustand slices. Moving it to a single &lt;code&gt;sell-wizard.svelte.ts&lt;/code&gt; with &lt;code&gt;$state&lt;/code&gt; actually shrank the code by about 40%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What genuinely hurt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smaller ecosystem. No &lt;code&gt;shadcn/ui&lt;/code&gt;, no &lt;code&gt;react-hook-form&lt;/code&gt;. You write more by hand. For us this was a plus, fewer black-box deps, but on a deadline-driven project it would hurt.&lt;/li&gt;
&lt;li&gt;Svelte 5 is fresh. Some libraries (Leaflet wrappers, for instance) hadn't been updated to runes yet. We use vanilla Leaflet directly, works fine.&lt;/li&gt;
&lt;li&gt;Ecosystem search. "How do I do X in SvelteKit" Google results often return Svelte 4 / SvelteKit 1 answers. Always check the version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Numbers (dev environment, same machine)&lt;/strong&gt;&lt;/p&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;Next.js 15&lt;/th&gt;
&lt;th&gt;SvelteKit 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold dev start&lt;/td&gt;
&lt;td&gt;~8 s&lt;/td&gt;
&lt;td&gt;~1.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMR update&lt;/td&gt;
&lt;td&gt;300–800 ms&lt;/td&gt;
&lt;td&gt;30–80 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production build&lt;/td&gt;
&lt;td&gt;~3 min&lt;/td&gt;
&lt;td&gt;~25 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline JS (homepage)&lt;/td&gt;
&lt;td&gt;~99 kB&lt;/td&gt;
&lt;td&gt;~38 kB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse Performance&lt;/td&gt;
&lt;td&gt;90+&lt;/td&gt;
&lt;td&gt;95+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The build-time numbers are the ones that changed daily life the most. &lt;code&gt;vite build&lt;/code&gt; finishes before you alt-tab to the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stayed from the previous architecture&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ASP.NET Core microservices. Untouched. The frontend rewrite didn't ripple into the backend at all, that's the upside of a clean REST contract.&lt;/li&gt;
&lt;li&gt;AI Translation Service. Still translates listings between Serbian, Russian and English on submit.&lt;/li&gt;
&lt;li&gt;SignalR for chat. Library is frontend-agnostic, just a different import path.&lt;/li&gt;
&lt;li&gt;Three-language UX with Cyrillic-first Serbian. That's a market decision, not a tech one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Would I recommend the move?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're starting from scratch in 2026 and your project is content-heavy with SSR/SEO requirements, try SvelteKit first. The DX is genuinely better, the bundle is smaller, the mental model is simpler.&lt;/p&gt;

&lt;p&gt;If you have a working Next.js project that ships features, don't migrate for the sake of migrating. We rewrote because we were still in MVP, there are only two of us (me and my backend friend), and 2–3 weeks of rewriting paid for itself in faster iteration after.&lt;/p&gt;

&lt;p&gt;If your team is large and React-shaped, the hiring market reality is real. SvelteKit devs exist but there are fewer of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's next&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finishing the backend (transition from test data to real listings).&lt;/li&gt;
&lt;li&gt;On-demand cache invalidation: ASP.NET webhook to a SvelteKit endpoint that purges CDN entries.&lt;/li&gt;
&lt;li&gt;PWA and offline-first for mobile.&lt;/li&gt;
&lt;li&gt;Improving AI translation with category context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture lesson from both articles is the same: boring backend, sharp frontend choices. The stack you pick on the frontend is the one users actually feel.&lt;/p&gt;

&lt;p&gt;If you're considering the same migration, drop questions in the comments. The site lives at &lt;a href="https://rsale.net" rel="noopener noreferrer"&gt;rsale.net&lt;/a&gt;, still on test data, but the SvelteKit build is what's serving it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>svelte</category>
      <category>sveltekit</category>
      <category>architecture</category>
    </item>
    <item>
      <title>From Idea to MVP: Building a Classified Platform in Serbia</title>
      <dc:creator>Maksim Baranov</dc:creator>
      <pubDate>Tue, 30 Dec 2025 14:37:55 +0000</pubDate>
      <link>https://dev.to/rsale/from-idea-to-mvp-building-a-classified-platform-in-serbia-5gg6</link>
      <guid>https://dev.to/rsale/from-idea-to-mvp-building-a-classified-platform-in-serbia-5gg6</guid>
      <description>&lt;p&gt;Hey dev.to! My name is Maksim, and I want to share the story of building &lt;a href="https://rsale.net/ru" rel="noopener noreferrer"&gt;Rsale.net&lt;/a&gt; — a classified platform for Serbia. This is a story about technical decisions, architectural trade-offs, and the specifics of the Balkan market.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5weqpgnhamwwx4acb0e.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5weqpgnhamwwx4acb0e.jpg" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Project Status: Backend and frontend are currently under active development. The site currently displays test data to demonstrate functionality.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why Serbia?&lt;/strong&gt;&lt;br&gt;
Serbia is an interesting market for classifieds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;7 million population with a growing e-commerce segment&lt;/li&gt;
&lt;li&gt;Dominated by outdated platforms with 2010s-era UX&lt;/li&gt;
&lt;li&gt;High smartphone penetration rate&lt;/li&gt;
&lt;li&gt;Active Russian-speaking community (relocants)&lt;/li&gt;
&lt;li&gt;We saw an opportunity to create a modern platform that combines products and services with a focus on multilingualism and geolocation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tech Stack: Why These Choices&lt;br&gt;
Architecture: Microservices&lt;/p&gt;

&lt;p&gt;We chose a microservices architecture with clear separation of responsibilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                      Frontend                           │
│                   Next.js 15 + React 19                 │
└─────────────────────┬───────────────────────────────────┘
                      │ REST API
                      ▼
┌─────────────────────────────────────────────────────────┐
│                    API Gateway                          │
│                    ASP.NET Core                         │
└───┬─────────────┬─────────────┬─────────────┬───────────┘
    │             │             │             │
    ▼             ▼             ▼             ▼
┌────────┐  ┌──────────┐  ┌──────────┐  ┌────────────┐
│Products│  │ Services │  │   Auth   │  │Translation │
│Service │  │ Service  │  │ Service  │  │  Service   │
│        │  │          │  │  (OIDC)  │  │   (AI)     │
└────────┘  └──────────┘  └──────────┘  └────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why microservices for an MVP? The common advice is "start with a monolith". But we had reasons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Independent scaling — the translation service is under heavier load than others&lt;/li&gt;
&lt;li&gt;Different teams — frontend and backend are developed in parallel&lt;/li&gt;
&lt;li&gt;AI component isolation — Translation Service can be easily replaced or updated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why ASP.NET Core?&lt;/strong&gt;&lt;br&gt;
Performance — one of the fastest web frameworks (TechEmpower benchmarks)&lt;br&gt;
Type safety — C# catches errors at compile time&lt;br&gt;
Ecosystem — Entity Framework, Identity, SignalR out of the box&lt;br&gt;
Hosting — works great in Docker/Kubernetes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend: Next.js 15 + React 19&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// package.json
{
  "dependencies": {
    "next": "15.4.6",
    "react": "19.1.0",
    "next-intl": "^4.6.1",        // i18n
    "zustand": "^5.0.8",           // State management
    "framer-motion": "^12.23.24",  // Animations
    "@microsoft/signalr": "^9.0.6" // Real-time chat
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why Next.js 15?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;App Router — native Server Components support&lt;/li&gt;
&lt;li&gt;ISR (Incremental Static Regeneration) — critical for classified SEO&lt;/li&gt;
&lt;li&gt;next-intl — best integration for multilingualism&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Frontend + Backend Integration&lt;/strong&gt;&lt;br&gt;
Frontend communicates with ASP.NET Core via REST API with retry logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// lib/server-api-service.ts

  async function fetchWithRetry&amp;lt;T&amp;gt;(endpoint: string): Promise&amp;lt;T&amp;gt; {
  const url = `${API_CONFIG.EXTERNAL_API_URL}${endpoint}`;

  for (let attempt = 1; attempt &amp;lt;= 3; attempt++) {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() =&amp;gt; controller.abort(), 10000);

      const response = await fetch(url, {
        signal: controller.signal,
        headers: { 
          'Content-Type': 'application/json',
          'User-Agent': 'RSALE-Frontend/1.0' 
        }
      });

      clearTimeout(timeoutId);
      return await response.json();

    } catch (error) {
      if (attempt === 3) throw error;
      await new Promise(r =&amp;gt; setTimeout(r, 1000));
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ISR Architecture: The Key to SEO&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For classifieds, SEO is everything. Every listing must be indexed by Google. We use different revalidation intervals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// lib/config.ts
export const ISR_CONFIG = {
  REVALIDATE_MAIN: 300,      // Homepage: 5 min
  REVALIDATE_PRODUCTS: 600,  // Lists: 10 min  
  REVALIDATE_DETAILS: 1200,  // Product pages: 20 min
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Server Components vs Client Components&lt;/strong&gt;&lt;br&gt;
We developed the "Server-First + Client Wrapper" pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// app/[locale]/page.tsx — Server Component
export default async function HomePage({ params }) {
  const { locale } = await params;
  setRequestLocale(locale);
  return (
    &amp;lt;&amp;gt;
      {/* SEO-critical content renders on server */}
      &amp;lt;script type="application/ld+json" 
        dangerouslySetInnerHTML={{ 
          __html: JSON.stringify(structuredData.website()) 
        }} 
      /&amp;gt;

      &amp;lt;Suspense fallback={&amp;lt;CategoriesSkeleton /&amp;gt;}&amp;gt;
        &amp;lt;MainPageContent /&amp;gt;  {/* Server Component */}
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/&amp;gt;
  );
} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule: Everything the search bot sees — server component. Interactivity (modals, forms, auth) — client components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three Languages: Serbian, English, Russian&lt;/strong&gt;&lt;br&gt;
Multilingualism was mandatory from day one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// app/[locale]/products/page.tsx
export default async function ProductsPage({ params }) {
  const { locale } = await params;
  setRequestLocale(locale);

  const t = await getTranslations('navigation');

  return (
    &amp;lt;h1&amp;gt;{t('products')}&amp;lt;/h1&amp;gt;  
    // ru: "Товары", sr: "Производи", en: "Products"
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important lesson: Serbian has two alphabets — Cyrillic and Latin. We chose Cyrillic as more traditional for the older generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Translation of Listings on the Server&lt;/strong&gt;&lt;br&gt;
One of our key features — automatic listing translation using AI. When a user adds a product or service in one language, the Translation Service automatically translates the content into the other two languages:&lt;/p&gt;

&lt;p&gt;User writes in Russian:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Продаю iPhone 14 Pro, отличное состояние, полный комплект"&lt;br&gt;
↓ AI Translation Service ↓&lt;br&gt;
Serbian: "Продајем iPhone 14 Pro, одлично стање, комплетан сет"&lt;br&gt;
English: "Selling iPhone 14 Pro, excellent condition, full set"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Backend implementation (ASP.NET Core):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// TranslationService.cs
public class AITranslationService : ITranslationService
{
    private readonly IAIClient _aiClient;

    public async Task&amp;lt;TranslatedContent&amp;gt; TranslateAsync(
        string title, 
        string description, 
        string sourceLang)
    {
        var targetLanguages = new[] { "ru", "sr", "en" }
            .Where(l =&amp;gt; l != sourceLang);

        var translations = new Dictionary&amp;lt;string, LocalizedContent&amp;gt;();

        foreach (var targetLang in targetLanguages)
        {
            var translated = await _aiClient.TranslateAsync(
                new TranslationRequest
                {
                    Title = title,
                    Description = description,
                    From = sourceLang,
                    To = targetLang
                });

            translations[targetLang] = translated;
        }

        return new TranslatedContent
        {
            Original = new LocalizedContent { Title = title, Description = description },
            SourceLanguage = sourceLang,
            Translations = translations
        };
    }
}

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lower barrier to entry — sellers don't think about translation&lt;/li&gt;
&lt;li&gt;SEO in all languages — listings are indexed in three language versions&lt;/li&gt;
&lt;li&gt;3x audience — a Russian-speaking seller reaches Serbian and English-speaking buyers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Current Development Metrics&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Metric Value
Build Time (frontend)   ~3 sec
Bundle Size 99.7 kB
Lighthouse Performance  90+
SEO Score   9/10
Static Pages    72

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's Next?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We're actively working on:&lt;/li&gt;
&lt;li&gt;Completing the backend — transitioning from test data to production&lt;/li&gt;
&lt;li&gt;PWA — offline mode for mobile&lt;/li&gt;
&lt;li&gt;Real-time chat — SignalR is already connected on both ends&lt;/li&gt;
&lt;li&gt;On-Demand Revalidation — webhook from ASP.NET Core to Next.js when listings change&lt;/li&gt;
&lt;li&gt;Improving AI translation — contextual translation based on product category&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building a classified platform in 2024-2025 isn't rocket science, but it requires thoughtful technical decisions:&lt;/li&gt;
&lt;li&gt;ASP.NET Core for a reliable and fast backend&lt;/li&gt;
&lt;li&gt;Microservices for independent scaling (especially AI translation)&lt;/li&gt;
&lt;li&gt;Next.js 15 with ISR for SEO and performance&lt;/li&gt;
&lt;li&gt;AI translation to remove language barriers&lt;/li&gt;
&lt;li&gt;The project is still in development, but the architecture is already set up right — and that's what matters. If you have questions about our stack or architecture — drop them in the comments!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://rsale.net/" rel="noopener noreferrer"&gt;rsale.net&lt;/a&gt; - check out the demo &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrown2qf3zzpl3wshdly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrown2qf3zzpl3wshdly.png" alt=" " width="163" height="41"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>startup</category>
      <category>nextjs</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
