<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Durim Hoti]]></title><description><![CDATA[Durim Hoti]]></description><link>https://durimhoti.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 13 Nov 2026 20:05:56 GMT</lastBuildDate><atom:link href="https://durimhoti.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Pitch a Local Business Without Sounding Like Spam]]></title><description><![CDATA[Most local business owners get several "I can improve your website" messages every week. Almost all of them get deleted. If you are a web designer or SEO…]]></description><link>https://durimhoti.hashnode.dev/how-to-pitch-a-local-business-without-sounding-like-spam</link><guid isPermaLink="true">https://durimhoti.hashnode.dev/how-to-pitch-a-local-business-without-sounding-like-spam</guid><dc:creator><![CDATA[Durim]]></dc:creator><pubDate>Thu, 12 Nov 2026 09:47:02 GMT</pubDate><content:encoded><![CDATA[<p>Most local business owners get several "I can improve your website" messages every week. Almost all of them get deleted. If you are a web designer or SEO freelancer, the problem is usually not your skills. It is the pitch.</p>
<p>Here is a simple approach that has worked much better for me.</p>
<h2>1. Only contact businesses with a real, visible gap</h2>
<p>Before writing anything, make sure there is something specific to talk about. Good examples:</p>
<ul>
<li>The business has great reviews but <strong>no website</strong> linked on Google Maps</li>
<li>The website exists but is <strong>broken or not mobile friendly</strong></li>
<li>They have <strong>far fewer reviews</strong> than competitors on the same street</li>
</ul>
<p>If you cannot name the gap in one sentence, do not send the message.</p>
<h2>2. Verify it yourself</h2>
<p>Open their Google Maps listing and website. Check that the business is open, that the gap is real, and that you are not about to pitch a website to someone who launched one last week. Two minutes of checking saves you from looking careless.</p>
<h2>3. Write three short sentences</h2>
<p>A template I use as a starting point:</p>
<blockquote>
<p>Hi [Name], I found [Business] on Google Maps while looking at [niche] in [area]. You have [strong point, e.g. 4.8 stars from 200 reviews], but there is no website linked, so people who want to see your services or prices have nowhere to go. I build simple sites for local [niche] businesses and could show you a quick example for [Business], if that is useful.</p>
</blockquote>
<p>That is it. No long list of services, no "limited offer", no attachments.</p>
<h2>4. Make it easy to say no</h2>
<p>End with something like "If this is not relevant, no worries, I will not follow up." And then actually respect it. In many countries, including the EU under GDPR, you need a clear way for people to opt out of further messages anyway. It also makes you sound like a person, not a sequence.</p>
<h2>5. Follow up once, at most</h2>
<p>One friendly follow-up after a week is fine. Five automated follow-ups are how you get marked as spam.</p>
<h2>Finding the businesses in the first place</h2>
<p>Step 1 is the slow part. Searching Google Maps by hand and checking every listing takes hours. I built <a href="https://findleadly.com">FindLeadly</a> to do that part: search a city and niche, and it lists local businesses with no website, few reviews or a weak online presence, each with an opportunity score and a link to its Google Maps listing.</p>
<p>It deliberately does not scrape phone numbers or emails and does not send messages for you. The outreach part should stay personal. There is a free plan with 20 leads per month if you want to try it on your own niche.</p>
<p>What is the best reply you ever got from a cold pitch to a local business? I would like to hear what worked for you.</p>
]]></content:encoded></item><item><title><![CDATA[How I Built a Python Compiler That Runs in Your Browser, With Real input()]]></title><description><![CDATA[Originally published on Medium. I'm Durim, and I build HTML Suite, a set of free developer tools that run entirely in the browser: an HTML editor, JSON and…]]></description><link>https://durimhoti.hashnode.dev/how-i-built-a-python-compiler-that-runs-in-your-browser-with-real-input</link><guid isPermaLink="true">https://durimhoti.hashnode.dev/how-i-built-a-python-compiler-that-runs-in-your-browser-with-real-input</guid><category><![CDATA[Python]]></category><category><![CDATA[WebAssembly]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Durim]]></dc:creator><pubDate>Thu, 12 Nov 2026 07:06:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6af55dd7c81cc790262fe4f5/676c192d-3b3c-429b-8096-d200aed164da.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Originally published on</em> <a href="https://medium.com/@durimhoti/how-i-built-a-python-compiler-that-runs-in-your-browser-with-real-input-e59547917bf4"><em>Medium</em></a><em>.</em></p>
<p>I'm Durim, and I build <a href="https://htmlsuite.com">HTML Suite</a>, a set of free developer tools that run entirely in the browser: an HTML editor, JSON and YAML formatters, converters and a <a href="https://htmlsuite.com/python-online-compiler">Python online compiler</a>.</p>
<p>The Python compiler runs real Python 3.13 on your own device using Pyodide (CPython compiled to WebAssembly). No server executes your code, nothing is uploaded, and you don't need an account. That part was surprisingly easy. The hard part was one tiny built-in function: <code>input()</code>.</p>
<h2>Why input() breaks in the browser</h2>
<p>In normal Python, <code>input()</code> simply pauses the program until you type a line. In the browser, that is a problem. Pyodide runs inside a Web Worker, and everything in JavaScript is asynchronous. A worker can't just stop and wait for the page to send it some text.</p>
<p>That's why most online Python editors make you type all input into a separate "stdin" box before you press Run. If you forget, or the program asks for one more line than you provided, you get this:</p>
<pre><code class="language-plaintext">EOFError: EOF when reading a line
</code></pre>
<p>My first version did exactly the same, and it was the most common bug people hit. Beginners write <code>name = input("Your name? ")</code> as one of their very first programs, so it had to work properly.</p>
<h2>The fix: SharedArrayBuffer and Atomics.wait</h2>
<p>The trick is to let the worker genuinely block. A <code>SharedArrayBuffer</code> is memory that both the page and the worker can see. Inside a worker, <code>Atomics.wait()</code> puts the thread to sleep until another thread changes a value in that shared memory.</p>
<p>So the flow looks like this:</p>
<ol>
<li>Python calls <code>input()</code>.</li>
<li>The worker tells the page "I need a line" and goes to sleep with <code>Atomics.wait()</code>.</li>
<li>The page shows an input field right below the output.</li>
<li>When you press Enter, the page writes your text into the shared buffer and wakes the worker with <code>Atomics.notify()</code>.</li>
<li>The worker reads the line and hands it back to Python, which continues as if nothing happened.</li>
</ol>
<p>Simplified, the worker side is just this:</p>
<pre><code class="language-js">function readLine() {
  Atomics.store(state, 0, 0);
  postMessage({ type: "input-request" });
  Atomics.wait(state, 0, 0); // sleep until the page answers
  const len = Atomics.load(state, 1);
  return decoder.decode(bytes.slice(0, len));
}
pyodide.setStdin({ stdin: readLine });
</code></pre>
<p>Because the Python interpreter never actually stops running, everything is preserved across the pause: variables, random numbers, open loops. You can ask for five inputs in a row and it behaves like a real terminal. UTF-8 works too, so "Grüße" or emoji come through fine.</p>
<h2>The catch: cross-origin isolation</h2>
<p>Browsers only allow <code>SharedArrayBuffer</code> on pages that are "cross-origin isolated". That means sending two HTTP headers:</p>
<pre><code class="language-plaintext">Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
</code></pre>
<p>These headers are strict: every resource the page loads from another domain has to allow it. Pyodide is loaded from jsDelivr, which already sends the right headers, so that worked. I chose <code>credentialless</code> instead of <code>require-corp</code> so that third-party scripts without special headers still load (just without cookies).</p>
<p>I only set these headers on the Python page and its worker, not on the whole site, so nothing else on HTML Suite is affected.</p>
<p>One more surprise: the site uses Next.js, and when you click an internal link, Next.js swaps the page without a full reload. That means the new headers never apply, and the page isn't isolated. The fix was to make every link to the Python compiler do a full page load.</p>
<h2>A fallback for browsers that can't do it</h2>
<p>Not every browser supports <code>credentialless</code>. In that case the page doesn't crash with <code>EOFError</code>. It shows a clear message and opens the classic stdin box instead, so you can still provide input the old way.</p>
<h2>Try it</h2>
<p>You can try it here: <a href="https://htmlsuite.com/python-online-compiler">htmlsuite.com/python-online-compiler</a>. Paste this and press Run:</p>
<pre><code class="language-python">name = input("What's your name? ")
age = int(input("How old are you? "))
print(f"Hi {name}, in 10 years you'll be {age + 10}.")
</code></pre>
<p>The rest of HTML Suite works the same way: free, in your browser, no signup. There's an <a href="https://htmlsuite.com/">HTML editor with live preview</a>, JSON, XML and YAML tools, Word to HTML, Base64, a JWT decoder, a regex tester and color tools.</p>
<p>If you try it and something doesn't work, or a tool you need is missing, I'd really like to hear about it in the comments.</p>
]]></content:encoded></item></channel></rss>