<?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: SnapInsta Team</title>
    <description>The latest articles on DEV Community by SnapInsta Team (@snapinstapl).</description>
    <link>https://dev.to/snapinstapl</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4235614%2F98605b00-eb97-4e90-af37-b788d06bea78.png</url>
      <title>DEV Community: SnapInsta Team</title>
      <link>https://dev.to/snapinstapl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/snapinstapl"/>
    <language>en</language>
    <item>
      <title>Building a Multi-Language Instagram Downloader in PHP: Lessons from 21 Locales</title>
      <dc:creator>SnapInsta Team</dc:creator>
      <pubDate>Mon, 09 Nov 2026 17:35:05 +0000</pubDate>
      <link>https://dev.to/snapinstapl/building-a-multi-language-instagram-downloader-in-php-lessons-from-21-locales-4998</link>
      <guid>https://dev.to/snapinstapl/building-a-multi-language-instagram-downloader-in-php-lessons-from-21-locales-4998</guid>
      <description>&lt;p&gt;I recently shipped &lt;a href="https://snapinsta.pl" rel="noopener noreferrer"&gt;snapinsta.pl&lt;/a&gt; — a small Instagram media downloader — in 21 languages, on shared LiteSpeed hosting behind Cloudflare, running on PHP 8.3 with no framework. Zero Composer dependencies. One &lt;code&gt;index.php&lt;/code&gt; front controller, a thin process layer, a signed-download proxy, and a bunch of flat &lt;code&gt;.php&lt;/code&gt; content files.&lt;/p&gt;

&lt;p&gt;A few decisions that I kept bumping into, and what I'd do the same way again.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A front controller, not a router library
&lt;/h2&gt;

&lt;p&gt;The whole site is routed by one file: &lt;code&gt;index.php&lt;/code&gt;. It reads &lt;code&gt;$_SERVER['REQUEST_URI']&lt;/code&gt;, peels off an optional two-letter language prefix (&lt;code&gt;/pl/reels&lt;/code&gt;, &lt;code&gt;/es/story&lt;/code&gt;, etc.), looks the remaining slug up in a map, and includes the matching content file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.php — simplified&lt;/span&gt;
&lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;parse_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REQUEST_URI'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;PHP_URL_PATH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="nv"&gt;$lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'en'&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="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$segments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;LANGS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$segments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_shift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$segments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$segments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'home'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;TOOL_PAGES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="no"&gt;STATIC_PAGES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;include&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/content/404.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;include&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/content/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.php'&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. For a site whose routes are known at write-time, pulling in a routing library is pure ceremony. The map lives in &lt;code&gt;config.php&lt;/code&gt; next to the language list. Adding a new language is one line; adding a new tool page is two.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.htaccess&lt;/code&gt; rewrites everything that isn't a real file into &lt;code&gt;index.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{REQUEST_FILENAME} !-f
&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{REQUEST_FILENAME} !-d
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ index.php [L]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;!-f&lt;/code&gt; matters — it's what lets &lt;code&gt;ads.txt&lt;/code&gt;, &lt;code&gt;robots.txt&lt;/code&gt;, and Google Search Console's verification HTML get served as actual files instead of being swallowed by the router.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Content files as plain PHP arrays
&lt;/h2&gt;

&lt;p&gt;Every blog post and every tool page is a PHP file that returns an array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'meta_title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'How to Download Instagram Reels in HD (2026 Guide)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'h1'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'How to Download Instagram Reels in HD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'sections'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'h2'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Step 1: Copy the link'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'blocks'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'faq'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'q'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'a'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why plain arrays and not Markdown? Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I18n keys inline.&lt;/strong&gt; Every content block can reference &lt;code&gt;{brand}&lt;/code&gt;, &lt;code&gt;{url:reels}&lt;/code&gt;, or any translation placeholder, and the rendering layer resolves them against the current locale. Markdown forces an extra parser and a second syntax for placeholders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured FAQ for schema.org.&lt;/strong&gt; The same array feeds both the HTML template &lt;em&gt;and&lt;/em&gt; the JSON-LD &lt;code&gt;FAQPage&lt;/code&gt; schema. One source of truth, no drift between what the user sees and what Google's rich-result parser sees.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Downside: writing content in PHP arrays is uglier than Markdown. For five tool pages and a handful of blog posts, I'll take ugly-but-structured over pretty-but-two-renderers.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The upstream proxy and the Cloudflare curl fingerprint
&lt;/h2&gt;

&lt;p&gt;The actual "download" logic proxies to a third-party Instagram scraper API and re-decodes its output. Nothing surprising there — except for one thing that cost me an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare-fronted APIs sometimes 403 PHP's &lt;code&gt;libcurl&lt;/code&gt;, even when a &lt;code&gt;curl&lt;/code&gt; binary on the same machine works fine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is TLS fingerprinting. PHP's &lt;code&gt;libcurl&lt;/code&gt; on Windows (with the OpenSSL build I had) presents a ClientHello that Cloudflare's bot engine doesn't love. The system &lt;code&gt;curl.exe&lt;/code&gt; presents a different one and sails through. On the Linux production host both work, because the libcurl there is a different build.&lt;/p&gt;

&lt;p&gt;The fix was embarrassing in its simplicity — try &lt;code&gt;proc_open&lt;/code&gt; first, fall back to &lt;code&gt;curl_init&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fetch_upstream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&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="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'proc_open'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// System curl binary — argv form, no shell&lt;/span&gt;
        &lt;span class="nv"&gt;$cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'curl'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'--max-time'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'20'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'--data-urlencode'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;run_proc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Shared hosts often disable proc_open — use libcurl there&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;curl_fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&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;Shared hosts commonly disable &lt;code&gt;proc_open&lt;/code&gt; and &lt;code&gt;exec&lt;/code&gt;, so the libcurl path is the only one that works in production for me. Locally the system-curl path takes over. Same code, two TLS fingerprints.&lt;/p&gt;

&lt;p&gt;If I had to do this again I'd probably reach for curl-impersonate on the server, but for a side project, keeping both paths is five lines and zero infra.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. LiteSpeed cache was my worst debugging day
&lt;/h2&gt;

&lt;p&gt;The site went live, I pushed a sitemap fix, hard-reloaded, and still saw the old XML. Cloudflare purge did nothing. cPanel's LiteSpeed cache flush did nothing.&lt;/p&gt;

&lt;p&gt;Turned out LiteSpeed's server-level cache was serving stale bytes regardless of what Cloudflare said. The fix was a single &lt;code&gt;.htaccess&lt;/code&gt; line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="sr"&gt; LiteSpeed&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    CacheLookup &lt;span class="ss"&gt;off&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a downloader where every request is dynamic anyway, there's nothing to cache server-side. Turning it off cost zero performance and ended the ghost-response nightmare. If you're on LiteSpeed shared hosting and your server seems to ignore you, this is the first thing to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. 21 languages, one file each, no gettext
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;lang/&lt;/code&gt; directory has 21 PHP files — &lt;code&gt;en.php&lt;/code&gt;, &lt;code&gt;pl.php&lt;/code&gt;, &lt;code&gt;es.php&lt;/code&gt;, …, each returning a flat associative array. No &lt;code&gt;.po&lt;/code&gt;, no &lt;code&gt;.mo&lt;/code&gt;, no gettext compilation step. A translation helper looks up the current locale's array and falls back to English if a key is missing.&lt;/p&gt;

&lt;p&gt;For a small content site, this is lighter than running gettext on shared hosting (where recompiling &lt;code&gt;.mo&lt;/code&gt; files is a hassle) and makes "edit a translation" literally an edit-the-file operation. Not elegant, but boring in all the right ways.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd skip if I did it again
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing in the admin.&lt;/strong&gt; There is no admin. Everything is code. For a personal-scale site, an admin UI is a liability — one more thing to secure. Content lives in git.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No ORM.&lt;/strong&gt; No database. The whole thing is stateless. State lives at the upstream API and in the user's browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No build step.&lt;/strong&gt; CSS is hand-written. JS is one file, served straight. Deploy is a zip upload to cPanel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site is live at &lt;a href="https://snapinsta.pl" rel="noopener noreferrer"&gt;snapinsta.pl&lt;/a&gt; if you want to poke at it. Boring stack, boring code, bored-into-the-ground reliability. That's the goal.&lt;/p&gt;

&lt;p&gt;If you've got questions about any part of the setup — the router, the decoder, the i18n layer, the Cloudflare quirk — drop a comment. Happy to go deeper.&lt;/p&gt;

</description>
      <category>php</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
