<?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: Charles Blackwood</title>
    <description>The latest articles on DEV Community by Charles Blackwood (@charles_blackwood).</description>
    <link>https://dev.to/charles_blackwood</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%2F3938905%2Fb831a4d1-997e-4c30-95aa-157850717468.png</url>
      <title>DEV Community: Charles Blackwood</title>
      <link>https://dev.to/charles_blackwood</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/charles_blackwood"/>
    <language>en</language>
    <item>
      <title>I built an SEO interception engine for my side project in one week. Here's the exact system</title>
      <dc:creator>Charles Blackwood</dc:creator>
      <pubDate>Fri, 29 May 2026 10:42:58 +0000</pubDate>
      <link>https://dev.to/charles_blackwood/i-built-an-seo-interception-engine-for-my-side-project-in-one-week-heres-the-exact-system-76h</link>
      <guid>https://dev.to/charles_blackwood/i-built-an-seo-interception-engine-for-my-side-project-in-one-week-heres-the-exact-system-76h</guid>
      <description>&lt;p&gt;Most developers think about SEO the wrong way.&lt;/p&gt;

&lt;p&gt;They write blog posts. They optimise their landing page. They add meta tags and hope Google notices.&lt;/p&gt;

&lt;p&gt;That's broadcasting. You're shouting into the void and waiting.&lt;/p&gt;

&lt;p&gt;There's a better move: &lt;strong&gt;interception&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Find the exact moment a developer is in pain. Meet them there. Solve it. Let the tool speak for itself.&lt;/p&gt;

&lt;p&gt;That's what I spent the last week building for &lt;a href="https://www.pastecheck.co.uk" rel="noopener noreferrer"&gt;PasteCheck&lt;/a&gt; — a mobile-first code linter I built entirely from my Android phone.&lt;/p&gt;

&lt;p&gt;Here's the exact system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core insight
&lt;/h2&gt;

&lt;p&gt;Developers don't search for "code linter." They search for their error.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Cannot read properties of undefined JavaScript"&lt;/li&gt;
&lt;li&gt;"Python indentation error fix"&lt;/li&gt;
&lt;li&gt;"Unexpected token JavaScript"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are high-intent, emotionally charged searches. Someone's code is broken. They're frustrated. They want a fix in the next 30 seconds.&lt;/p&gt;

&lt;p&gt;If you can intercept that search with a page that actually solves the problem — and then naturally introduces your tool — you don't need to advertise. You're already inside the moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;I built a config-driven SEO page system. One template. One data file. Zero duplicated JSX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data file: &lt;code&gt;src/data/fixPages.ts&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every page is a config object:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;How to fix: Cannot read properties of undefined in JavaScript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cannot-read-properties-of-undefined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JavaScript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This error means your code tried to access a property on a value that is undefined...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;whyItHappens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JavaScript allows variables to exist without a value...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;brokenExample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`async function getUser() { ... }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fixedExample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`async function getUser() {
    if (!data.profile) { return; }
    console.log(data.profile.name);
  }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;commonCauses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accessing nested properties before checking the parent exists&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;An API call returned null or an empty object&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="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;primaryCtaCopy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check your code and see the exact error line&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;searchIntent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error-fix&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;&lt;strong&gt;The template: &lt;code&gt;ErrorFixPage.tsx&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One component renders all 20 pages. Title, summary, broken example, fixed example, common causes, CTA — all pulled from config. Change the data, get a new page. No copy-paste, no drift between pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The route: &lt;code&gt;/fix/:slug&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dynamic route via Wouter. One line in &lt;code&gt;App.tsx&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/fix/:slug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ErrorFixPage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SEO: canonical tags via react-helmet-async&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every page gets its own canonical, meta title, and description. No duplicate content risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  20 pages in one sprint
&lt;/h2&gt;

&lt;p&gt;The first 20 pages cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;11 JavaScript errors (the biggest search volume)&lt;/li&gt;
&lt;li&gt;6 Python errors&lt;/li&gt;
&lt;li&gt;2 HTML errors&lt;/li&gt;
&lt;li&gt;1 CSS error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each page took about 20 minutes to write. The template handles everything else.&lt;/p&gt;

&lt;p&gt;All 20 submitted to Google Search Console on May 28. Currently discovering. The compounding starts now.&lt;/p&gt;




&lt;h2&gt;
  
  
  What else shipped this week
&lt;/h2&gt;

&lt;p&gt;The SEO engine wasn't the only thing. Here's the full v2.18–v2.33 sprint:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UX:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two-column desktop layout — input left, results right. Mobile untouched.&lt;/li&gt;
&lt;li&gt;First 3 errors expand automatically on check&lt;/li&gt;
&lt;li&gt;"All errors fixed ✓" state — only triggers when previous check had errors&lt;/li&gt;
&lt;li&gt;Ctrl+Enter keyboard shortcut&lt;/li&gt;
&lt;li&gt;First-time onboarding snippet — pre-filled broken JS on first visit&lt;/li&gt;
&lt;li&gt;Check button loading state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixed a critical black screen bug — &lt;code&gt;setHistory&lt;/code&gt; was outside a &lt;code&gt;setTimeout&lt;/code&gt; closure. Vite's minifier renamed the variable reference in production. Silent in dev. Blank page in prod.&lt;/li&gt;
&lt;li&gt;Closed a Pro bypass vulnerability on the &lt;code&gt;/success&lt;/code&gt; page&lt;/li&gt;
&lt;li&gt;Extended Stripe webhook to handle subscription cancellations and failed payments&lt;/li&gt;
&lt;li&gt;Enabled Dependabot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Distribution:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Listed on AlternativeTo — already the highest-quality traffic source in GA (1m 31s avg engagement)&lt;/li&gt;
&lt;li&gt;Full violet rebrand sitewide&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The numbers so far
&lt;/h2&gt;

&lt;p&gt;Two weeks post-launch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;126 total users&lt;/li&gt;
&lt;li&gt;UK users: 2m 43s avg engagement, 84% engagement rate, 81% of all events — the real market&lt;/li&gt;
&lt;li&gt;US users: 1s avg engagement — launch traffic, not retention&lt;/li&gt;
&lt;li&gt;AlternativeTo sending better-quality sessions than Reddit or Dev.to&lt;/li&gt;
&lt;li&gt;Organic search: 94% engagement rate on 18 sessions — early SEO working&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SEO pages haven't indexed yet. That's week 3.&lt;/p&gt;




&lt;h2&gt;
  
  
  The strategic frame
&lt;/h2&gt;

&lt;p&gt;This isn't about gaming Google. It's about being genuinely useful at the exact moment someone needs help.&lt;/p&gt;

&lt;p&gt;Every fix page is a real explanation written for a real error that real developers actually hit. The CTA into PasteCheck is natural — "want to find this exact line in your code?" — because that's what the tool does.&lt;/p&gt;

&lt;p&gt;20 pages compounds. 50 pages compounds heavily. 100 pages becomes a moat.&lt;/p&gt;

&lt;p&gt;Built entirely from an Android phone.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PasteCheck is a free mobile-first code linter. Paste your JavaScript, TypeScript, Python, HTML, or CSS — see your errors instantly, in plain English. &lt;a href="https://www.pastecheck.co.uk" rel="noopener noreferrer"&gt;pastecheck.co.uk&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>seo</category>
    </item>
    <item>
      <title>I Built a Pro Tier with Stripe, Supabase Auth, and Multi-File Mode From My Android Phone</title>
      <dc:creator>Charles Blackwood</dc:creator>
      <pubDate>Sun, 24 May 2026 17:59:06 +0000</pubDate>
      <link>https://dev.to/charles_blackwood/i-built-a-pro-tier-with-stripe-supabase-auth-and-multi-file-mode-from-my-android-phone-1p2i</link>
      <guid>https://dev.to/charles_blackwood/i-built-a-pro-tier-with-stripe-supabase-auth-and-multi-file-mode-from-my-android-phone-1p2i</guid>
      <description>&lt;p&gt;No MacBook. No desk setup. No IDE.&lt;/p&gt;

&lt;p&gt;Every line of code in PasteCheck was written on an Android phone using SPCK Editor, committed via GitHub mobile, and deployed to Vercel. That includes the free tier launch, the linting engine, and everything I'm about to describe.&lt;/p&gt;

&lt;p&gt;This week I shipped Pro tier. Here's exactly how I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What PasteCheck is (quick context)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.pastecheck.co.uk" rel="noopener noreferrer"&gt;PasteCheck&lt;/a&gt; is a mobile-optimised code linter. You paste code, it detects the language, highlights errors and warnings, and gives you tappable explanations with actionable fix hints. No setup. No signup required for the free tier.&lt;/p&gt;

&lt;p&gt;It supports JavaScript, TypeScript, Python, HTML, and CSS. The linting runs entirely client-side no server round trips, no latency.&lt;/p&gt;

&lt;p&gt;Pro tier adds three things: multi-file mode (up to 5 files at once), shareable check links (permanent URLs anyone can view), and saved collections (cross-device sync via Supabase).&lt;/p&gt;




&lt;h2&gt;
  
  
  The auth flow: magic links vs passwords
&lt;/h2&gt;

&lt;p&gt;The first decision was authentication method. I initially set up Supabase magic links so user enters email, gets a link, clicks it, they're in.&lt;/p&gt;

&lt;p&gt;It broke the user flow immediately. Every session meant going to your email, finding the link, clicking back to the app. For a tool people use repeatedly that's friction that compounds fast. I scrapped it and rebuilt with email and password.&lt;/p&gt;

&lt;p&gt;The Supabase email + password flow in React looks straightforward on paper. In practice the callback routing caught me, the &lt;code&gt;/auth/callback&lt;/code&gt; page needs to handle two completely different scenarios: email confirmation on signup (&lt;code&gt;verifyOtp&lt;/code&gt;) and session restoration on login (&lt;code&gt;getSession&lt;/code&gt;). Getting those two paths clean without one interfering with the other took a few iterations.&lt;/p&gt;

&lt;p&gt;The confirmation email routes through Resend on a custom domain (&lt;code&gt;noreply@pastecheck.co.uk&lt;/code&gt;). 3,000 emails a month free. That was a zero-cost decision that took about 20 minutes to configure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pro gating: keeping free users out of paid features
&lt;/h2&gt;

&lt;p&gt;This was the most important piece to get right. The Pro gate connects three things: Stripe payment confirmation, a Supabase &lt;code&gt;is_pro&lt;/code&gt; boolean on the user record, and localStorage for session persistence.&lt;/p&gt;

&lt;p&gt;The flow on successful payment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stripe redirects to &lt;code&gt;/success&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/success&lt;/code&gt; writes the Pro licence to localStorage immediately (so the UI updates without a round trip)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/success&lt;/code&gt; also writes &lt;code&gt;is_pro: true&lt;/code&gt; to Supabase via a serverless function&lt;/li&gt;
&lt;li&gt;On every subsequent mount, login, and page load the app re-syncs &lt;code&gt;is_pro&lt;/code&gt; from Supabase&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reason for both localStorage and Supabase is speed vs reliability. localStorage gives you instant UI response. Supabase gives you truth that persists across devices and survives a cleared browser. Neither alone is sufficient.&lt;/p&gt;

&lt;p&gt;The gating itself is a check against both the local state and the synced Supabase value before rendering any Pro component. If either is missing or false, the upgrade prompt shows instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stripe: test mode to live mode
&lt;/h2&gt;

&lt;p&gt;I built the entire payment flow in Stripe test mode first. Fake card numbers, test webhooks, the full checkout session. Once it was working reliably I switched to live keys, hit the upgrade button myself, and watched the checkout load with a real &lt;code&gt;cs_live_&lt;/code&gt; session URL, no test banner, real payment form.&lt;/p&gt;

&lt;p&gt;I didn't pay for my own subscription to verify it. I trusted that test mode behaviour maps cleanly to live mode, which it does, by design. The architecture doesn't change between modes. Only the keys change.&lt;/p&gt;

&lt;p&gt;The Stripe integration runs through a Vercel serverless function (&lt;code&gt;api/create-checkout.ts&lt;/code&gt;). It creates the checkout session server-side so the secret key never touches the client. Standard practice but worth stating explicitly if you're building this for the first time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-file mode
&lt;/h2&gt;

&lt;p&gt;This was the most satisfying feature to build. The UI lets you add up to 5 named files, switch between them, and see per-file linting results alongside a summary bar showing total errors and warnings across all files.&lt;/p&gt;

&lt;p&gt;The architecture is straightforward. each file is an entry in a React state array with its own name, content, and result set. The linter runs on each independently. The summary bar aggregates.&lt;/p&gt;

&lt;p&gt;What makes it feel polished is the tab system. Each file gets a tab. The active tab highlights. Errors on inactive tabs show a red indicator so you can see at a glance which files need attention without switching to them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shareable check links
&lt;/h2&gt;

&lt;p&gt;User hits share, a serverless function writes the current code and results to a Supabase &lt;code&gt;shared_checks&lt;/code&gt; table, returns a unique ID, and the app constructs a &lt;code&gt;/s/[id]&lt;/code&gt; URL. The share page is read-only — anyone with the link can see the code and results without an account.&lt;/p&gt;

&lt;p&gt;The share page includes a CTA to the free tool for users who arrive without an account. Every shared link is a passive acquisition touchpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the stack looks like end to end
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React / TypeScript / Vite / Tailwind&lt;/strong&gt; — client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; — hosting and serverless functions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; — database and auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe&lt;/strong&gt; — payments (£4/month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend&lt;/strong&gt; — transactional email&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SPCK Editor on Android&lt;/strong&gt; — where every line was written&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total infrastructure cost: £0 until revenue arrives. Vercel free tier, Supabase free tier, Resend free tier, Stripe takes a cut only on successful payments.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Android constraint
&lt;/h2&gt;

&lt;p&gt;I want to be direct about this because it changes how you think about the whole build.&lt;/p&gt;

&lt;p&gt;Working from Android means no terminal. No local dev server. No hot reload in a browser you control. Every change is: edit in SPCK → commit via GitHub mobile → wait for Vercel to deploy → test in mobile Chrome.&lt;/p&gt;

&lt;p&gt;The feedback loop is longer. But it forces discipline. You don't make speculative changes because every change costs a deploy cycle. You think the edit through before you make it. You read the code more carefully because you can't run it locally to check.&lt;/p&gt;

&lt;p&gt;I'm not claiming it's better. I'm saying it's a real constraint that produces a specific way of working and that way of working built a production app with live payments in a few weeks from nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it is now
&lt;/h2&gt;

&lt;p&gt;PasteCheck is live at &lt;a href="https://www.pastecheck.co.uk" rel="noopener noreferrer"&gt;pastecheck.co.uk&lt;/a&gt;. Free tier is fully functional with no account required. Pro is £4/month.&lt;/p&gt;

&lt;p&gt;If you paste code regularly for debugging, for learning, for checking before committing give it a try. The mobile experience in particular is something most linters don't prioritise.&lt;/p&gt;

&lt;p&gt;If you're building something solo under constraints, I'd genuinely like to hear what your setup looks like. The Android-only workflow is uncommon enough that I rarely find other people doing it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built solo. Deployed from a phone. Open for business.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>javascript</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>PasteCheck v1.7 + v1.8 — Hints that tell you what to fix, and a nudge panel that tells you where to start</title>
      <dc:creator>Charles Blackwood</dc:creator>
      <pubDate>Fri, 22 May 2026 23:20:14 +0000</pubDate>
      <link>https://dev.to/charles_blackwood/pastecheck-v17-v18-hints-that-tell-you-what-to-fix-and-a-nudge-panel-that-tells-you-where-to-42lc</link>
      <guid>https://dev.to/charles_blackwood/pastecheck-v17-v18-hints-that-tell-you-what-to-fix-and-a-nudge-panel-that-tells-you-where-to-42lc</guid>
      <description>&lt;p&gt;Most linters tell you what's wrong. They stop there.&lt;/p&gt;

&lt;p&gt;PasteCheck has always shown you the error and the line. But "var is discouraged" doesn't tell a beginner what to actually do about it. "Empty catch block" doesn't tell you why it matters or what to replace it with.&lt;/p&gt;

&lt;p&gt;That changes in v1.7.&lt;/p&gt;

&lt;p&gt;Every error and warning message now ends with one actionable sentence. Not a docs link. Not a vague suggestion. One clear fix.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Prefer 'let' or 'const' over 'var' — use 'const' if the value never changes, 'let' if it does&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;'eval()' executes arbitrary code — dangerous and a security risk — remove it and find a safer alternative like JSON.parse() or a lookup object&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Empty catch block — errors are silently swallowed — add at least console.error(e) so failures don't disappear silently&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This covers every language PasteCheck supports — JavaScript, TypeScript, Python, HTML, and CSS. 30 messages updated across the entire linter.&lt;/p&gt;

&lt;p&gt;Two Reddit commenters validated this independently before it was built. Both said the same thing unprompted — knowing what's wrong isn't enough, you need to know what to do next. That's what v1.7 ships.&lt;/p&gt;




&lt;p&gt;v1.8 adds something no other paste tool has.&lt;/p&gt;

&lt;p&gt;When errors or warnings exist, a collapsible "What to do next" panel appears below the results. Not per-error guidance — big picture debugging logic. The kind of thing an experienced developer would tell you if they were sitting next to you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you have errors: fix the first one before the rest. Errors cascade. One missing bracket can cause five false positives below it.&lt;/li&gt;
&lt;li&gt;If you have warnings only: no errors is good — work through warnings one at a time using the fix hints above.&lt;/li&gt;
&lt;li&gt;If you have both: ignore the warnings for now. Fix the errors first. Warnings can wait until your code runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's collapsed by default. It doesn't get in the way. It's there when you need it.&lt;/p&gt;




&lt;p&gt;Both features were designed around the same principle — PasteCheck doesn't just find your mistakes, it helps you understand them.&lt;/p&gt;

&lt;p&gt;Built entirely from an Android phone. Live at pastecheck.co.uk.&lt;/p&gt;




</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>javascript</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I built a syntax linter from my Android phone. Here's what v1.6 taught me about false positives.</title>
      <dc:creator>Charles Blackwood</dc:creator>
      <pubDate>Fri, 22 May 2026 19:33:44 +0000</pubDate>
      <link>https://dev.to/charles_blackwood/i-built-a-syntax-linter-from-my-android-phone-heres-what-v16-taught-me-about-false-positives-45p6</link>
      <guid>https://dev.to/charles_blackwood/i-built-a-syntax-linter-from-my-android-phone-heres-what-v16-taught-me-about-false-positives-45p6</guid>
      <description>&lt;p&gt;A few weeks ago I was debugging a TypeScript file on my phone and pasted it into three different AI tools trying to get a clean syntax check. Every one of them reformatted something. One introduced a line break mid-string. Another quietly changed a variable name.&lt;/p&gt;

&lt;p&gt;I didn't need AI to fix my code. I needed something to &lt;em&gt;read&lt;/em&gt; it and tell me what was wrong. So I built PasteCheck — a dark-themed, mobile-optimised linter that runs entirely in the browser. No backend. No login. No reformatting.&lt;/p&gt;

&lt;p&gt;I built the whole thing from my Android phone using SPCK Editor, Termux, and GitHub. No laptop. No desktop. That constraint turned out to matter more than I expected — but more on that later.&lt;/p&gt;

&lt;p&gt;This is the story of v1.6, and what I learned building it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with being too clever
&lt;/h2&gt;

&lt;p&gt;By v1.5, PasteCheck was linting JavaScript, TypeScript, Python, HTML, and CSS with decent accuracy. Session history was live. The core loop worked.&lt;/p&gt;

&lt;p&gt;Then I started getting false positives.&lt;/p&gt;

&lt;p&gt;The duplicate variable checker I shipped in v1.6 was supposed to catch &lt;code&gt;const&lt;/code&gt; and &lt;code&gt;let&lt;/code&gt; redeclarations — a real, common mistake. But without scope awareness, it was flagging perfectly valid code where the same variable name appeared in two separate functions. Technically the same name. Semantically completely different things.&lt;/p&gt;

&lt;p&gt;The fix was a scope stack — push on function and block entry, pop on exit. Simple in principle. Fiddly to get right. The checker now correctly ignores cross-scope reuse and only flags genuine same-scope redeclarations.&lt;/p&gt;

&lt;p&gt;The lesson: &lt;strong&gt;a linter that's wrong loudly is worse than no linter at all.&lt;/strong&gt; Developers learn to ignore noisy tools. Once you've trained someone to dismiss your warnings, you've lost them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The large file problem
&lt;/h2&gt;

&lt;p&gt;The same issue appeared from a different angle with TypeScript detection.&lt;/p&gt;

&lt;p&gt;PasteCheck uses acorn with an acorn-typescript plugin to parse TS files. For small files, this worked cleanly. Then someone pasted a 974-line &lt;code&gt;linter.ts&lt;/code&gt; file and it misdetected as Python — because the file contained &lt;code&gt;import *&lt;/code&gt; and &lt;code&gt;print&lt;/code&gt;, both of which pattern-match as Python syntax.&lt;/p&gt;

&lt;p&gt;The fix was ordering: TypeScript detection now runs &lt;em&gt;before&lt;/em&gt; Python detection. Obvious in retrospect. But it only surfaced because the file was large enough to contain Python-shaped TS patterns.&lt;/p&gt;

&lt;p&gt;I also capped the duplicate variable checker and TS regex fallbacks at 200 lines. Not because large file support isn't worth building — it is, and it's in the roadmap — but because a wrong answer on a 500-line file does more damage than a "file too large, try a smaller snippet" message. Honest limitations beat confident mistakes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What else shipped in v1.6
&lt;/h2&gt;

&lt;p&gt;Beyond the scope-aware duplicate variable detection and the language detection fix:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python &lt;code&gt;pass&lt;/code&gt; check.&lt;/strong&gt; A &lt;code&gt;pass&lt;/code&gt; statement inside a non-empty block is redundant — the block runs fine without it. v1.6 flags these as warnings by looking ahead for sibling lines at the same or deeper indent level. Small thing, but it catches a specific habit that creeps into refactored Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSS missing units.&lt;/strong&gt; Non-zero numeric values on dimension properties — &lt;code&gt;margin&lt;/code&gt;, &lt;code&gt;padding&lt;/code&gt;, &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, &lt;code&gt;font-size&lt;/code&gt;, &lt;code&gt;border-radius&lt;/code&gt;, and 20+ others — flagged as warnings with a "did you mean &lt;code&gt;16px&lt;/code&gt;?" suggestion. Zero values are correctly ignored. This one came from my own mistakes more than anything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Android constraint
&lt;/h2&gt;

&lt;p&gt;I mentioned building PasteCheck entirely from an Android phone. This isn't a gimmick — it's the actual development environment.&lt;/p&gt;

&lt;p&gt;Every commit, every deploy, every debug session happened on a phone screen. Termux for the terminal. SPCK Editor for the code. GitHub for version control. Vercel for deployment via GitHub sync.&lt;/p&gt;

&lt;p&gt;The constraint has a real effect on what gets built. When your own testing surface &lt;em&gt;is&lt;/em&gt; mobile, mobile UX problems surface immediately. The v1.4 scroll fix — removing a fixed &lt;code&gt;maxHeight&lt;/code&gt; on the results panel that was creating a box-within-a-page scroll nightmare — would have taken much longer to catch on a desktop.&lt;/p&gt;

&lt;p&gt;Building from the same device your users are likely using isn't a limitation. It's a feedback loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where PasteCheck is now
&lt;/h2&gt;

&lt;p&gt;Five languages. Real syntax detection. Session history. 54 users across five countries in the first five days — US, UK, Canada, Türkiye, and others showing up in Analytics.&lt;/p&gt;

&lt;p&gt;The roadmap has a Pro tier on the horizon — GitHub integration as the proposed anchor feature, so PasteCheck runs on every push rather than requiring manual paste. That's a Week 2 research problem. Right now the free tier is the product, and it's earning its keep.&lt;/p&gt;

&lt;p&gt;If you work with code on mobile — or just want a clean, instant paste-and-check tool without an AI trying to improve your work — give it a try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.pastecheck.co.uk" rel="noopener noreferrer"&gt;pastecheck.co.uk&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building in public, solo, from Android. Follow along if that sounds interesting.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>python</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>PasteCheck v1.3 — what I improved after launching and getting real users</title>
      <dc:creator>Charles Blackwood</dc:creator>
      <pubDate>Wed, 20 May 2026 14:20:18 +0000</pubDate>
      <link>https://dev.to/charles_blackwood/pastecheck-v13-what-i-improved-after-launching-and-getting-real-users-2l87</link>
      <guid>https://dev.to/charles_blackwood/pastecheck-v13-what-i-improved-after-launching-and-getting-real-users-2l87</guid>
      <description>&lt;p&gt;A few days ago I launched PasteCheck — a free tool &lt;br&gt;
to paste code and see errors highlighted instantly.&lt;/p&gt;

&lt;p&gt;After launch I found real gaps and shipped v1.3. &lt;br&gt;
Here's what changed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python indentation monitor&lt;/strong&gt;&lt;br&gt;
Now flags mixed tabs and spaces as an error, and &lt;br&gt;
inconsistent indentation style across the file as &lt;br&gt;
a warning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSS property validator&lt;/strong&gt;&lt;br&gt;
Embedded CSS inside HTML now checked for misspelled &lt;br&gt;
properties. Catches things like &lt;code&gt;colour&lt;/code&gt; instead of &lt;br&gt;
&lt;code&gt;color&lt;/code&gt;, &lt;code&gt;font-weigh&lt;/code&gt; instead of &lt;code&gt;font-weight&lt;/code&gt; — &lt;br&gt;
with "did you mean X?" suggestions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aria-label support&lt;/strong&gt;&lt;br&gt;
Previously flagged inputs with aria-label or &lt;br&gt;
aria-labelledby as unlabelled. Fixed — those are &lt;br&gt;
now correctly recognised as labelled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Language detection confidence UI&lt;/strong&gt;&lt;br&gt;
Short snippets under 5 lines now show a subtle &lt;code&gt;?&lt;/code&gt; &lt;br&gt;
badge next to the detected language so users know &lt;br&gt;
detection may be approximate.&lt;/p&gt;

&lt;p&gt;All shipped in a single session, tested, live.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://www.pastecheck.co.uk" rel="noopener noreferrer"&gt;https://www.pastecheck.co.uk&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What would you want to see in v2?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a free mobile-friendly code error checker — PasteCheck</title>
      <dc:creator>Charles Blackwood</dc:creator>
      <pubDate>Mon, 18 May 2026 20:30:15 +0000</pubDate>
      <link>https://dev.to/charles_blackwood/i-built-a-free-mobile-friendly-code-error-checker-pastecheck-4n5k</link>
      <guid>https://dev.to/charles_blackwood/i-built-a-free-mobile-friendly-code-error-checker-pastecheck-4n5k</guid>
      <description>&lt;p&gt;I got frustrated constantly copy-pasting broken code into AI and getting inconsistent results. So I built PasteCheck.&lt;/p&gt;

&lt;p&gt;You paste JavaScript, Python or HTML and every error gets highlighted instantly. Tap any highlight to see what the issue is. No sign up, completely free, works well on mobile.&lt;/p&gt;

&lt;p&gt;Would love feedback from developers: &lt;/p&gt;

&lt;p&gt;(&lt;a href="https://www.pastecheck.co.uk" rel="noopener noreferrer"&gt;https://www.pastecheck.co.uk&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>coding</category>
      <category>management</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
