DEV Community

Cover image for How I Ranked #1 in Uganda by Shipping SEO Nobody Else Ships
Роман Шамагин
Роман Шамагин

Posted on

How I Ranked #1 in Uganda by Shipping SEO Nobody Else Ships

I run a small e-commerce site in Kampala, Uganda called PowerBulb.ug. We sell rechargeable LED emergency bulbs and power-backup equipment to households affected by UEDCL load shedding. Five months after launch, we rank on page 1 of Google for a growing list of commercial-intent queries. This post is the technical write-up of how — and why the playbook is almost embarrassingly boring.

The premise: zero-competition SEO is a real market

Most SEO advice is written for saturated markets. "You need 500 backlinks, a DA of 60, and eighteen months of content marketing." This is true in California. It is not true in Uganda.

I did a competitive audit of the top 10 results for every commercial keyword in my niche: rechargeable bulb uganda, emergency bulb kampala, voltage stabilizer uganda, etc. Here is what I found:

  • No competitor had a valid Product schema.
  • No competitor had LocalBusiness or Place schema.
  • No competitor had hasMerchantReturnPolicy or shippingDetails.
  • Most competitor pages were Jiji.ug listings with broken image gallery, no H1, no metadata.
  • Two had Core Web Vitals LCP > 8 seconds.
  • None had a blog, ever.

This is a market where Google's ranking algorithm is basically yelling for help. Anything halfway-competent ranks. The question is what "halfway-competent" means technically.

Stack

- React 18 + TypeScript + Vite 6
- Tailwind CSS v4
- Framer Motion
- Lucide React (icons — no emoji in production UI)
- Static HTML pages for non-SPA routes (/products/, /kampala/, /blog/)
- Deployed: Kubernetes (Megav Cloud) via Helm + GitHub Actions
- Cloudflare Free plan, geo-block non-UG traffic
Enter fullscreen mode Exit fullscreen mode

Why not Next.js? Honestly because I wanted minimum moving parts for a single-person team. Vite + static HTML for SEO-critical pages is less clever than SSR but infinitely easier to debug at 2am.

The schema strategy

Uganda is not a market where complex schema is needed. It is a market where any valid schema wins. I shipped:

Product schema on every SKU page:

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Rechargeable LED Emergency Bulb 15W E27",
  "offers": {
    "@type": "Offer",
    "priceCurrency": "UGX",
    "price": "30000",
    "availability": "https://schema.org/InStock",
    "hasMerchantReturnPolicy": {
      "@type": "MerchantReturnPolicy",
      "applicableCountry": "UG",
      "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
      "merchantReturnDays": 7,
      "returnFees": "https://schema.org/FreeReturn",
      "returnMethod": "https://schema.org/ReturnAtKiosk"
    },
    "shippingDetails": { /* UG-specific */ }
  }
}
Enter fullscreen mode Exit fullscreen mode

Three things tripped me up:

  1. returnMethod — if you ship MerchantReturnPolicy, Google flags it as a non-critical issue unless you specify. I used ReturnAtKiosk because delivery is by boda-boda (motorcycle taxi) — the rider collects the return in person. ReturnByMail was wrong; Uganda has no household postal pickup.

  2. priceCurrency: "UGX" — Google's Rich Results Test initially refused to show price in SERPs because my shipping rate object was missing a currency field. The fix was adding MonetaryAmount with explicit currency to every rate.

  3. availability — the InStock schema triggers "In stock" rich snippet. Worth every second it takes to configure.

LocalBusiness + Place schema on the homepage and Kampala hub page. Five neighbourhood pages (/kampala/ntinda/, /kampala/naalya/, etc.) each get their own Place schema with latitude/longitude coordinates — this is what makes Uganda-specific "near me" queries surface the site.

BlogPosting + FAQPage schema on blog articles. FAQPage schema is wildly undervalued — it gets you the accordion-style rich snippet below the search result, which eats 30% more SERP real estate.

The hyperlocal pages trick

I noticed that competitors were all targeting generic terms like rechargeable bulb kampala. Nobody targeted neighbourhood queries. So I shipped:

/kampala/           -- hub page
/kampala/ntinda/    -- Ntinda-specific
/kampala/naalya/
/kampala/bukoto/
/kampala/kireka/
/kampala/muyenga/
Enter fullscreen mode Exit fullscreen mode

Each page has:

  • Unique H1 with the neighbourhood name
  • Unique hero image (AI-generated, but authentic-looking — ethnic and architectural detail matter)
  • Local landmarks in the copy (street names, churches, schools — stuff a boda driver would recognise)
  • Specific delivery time to that neighbourhood
  • Local Place schema with coords
  • 600-900 words of unique copy per page
  • Internal links to/from the hub

These rank for rechargeable bulb ntinda, emergency bulb kireka, etc. within days of indexing because there is literally no other English-language page on the internet optimised for those queries. Google has to rank something. It ranks you.

Content cadence that works for a solo team

I cannot write a blog post a week. Nobody in my situation can. The compromise:

One cornerstone article per month, 1,500-2,500 words, targeting a commercial-intent keyword with buying signals. So far I have shipped:

  • "UEDCL vs UMEME: complete Kampala guide to the 2026 transition"
  • "Types of bulbs in Uganda: rechargeable vs LED vs solar (buyer's guide)"

The pattern: the article answers a real question, mentions the product in context (not as a pitch), and internally links to 3-5 product pages. Google treats the blog as topical authority for the product pages, which pulls them up in SERPs. This is the old cornerstone content model and it still works.

What surprised me

GSC indexing is faster than I expected. New pages are indexed in 24-72 hours with Request Indexing in Google Search Console. In a mature market you wait weeks. In Uganda, Google seems happy to index anything because it improves their results.

Bing matters more than I thought. Uganda has a non-trivial Edge/Bing user base in corporate networks. I submit to Bing Webmaster Tools + IndexNow for every new page — same-day indexing, separate traffic stream, almost no effort.

Cloudflare's geo-block improves crawl quality. I block non-UG human traffic but whitelist Googlebot, Bingbot, and a few academic crawlers. This keeps bot traffic clean, reduces server load, and (I suspect) marginally helps locality signals because Google sees the site serving Uganda consistently.

Image authenticity beats image quality. AI-generated hero images work if they look Kampala-specific — correct architectural detail, skin tones, clothing, streetscape. Generic stock photography tanks engagement because Ugandan users spot "this is not here" instantly.

What I would do differently

Ship structured data first, not last. I added hasMerchantReturnPolicy three weeks after launch. Every day I waited was a day I was invisible to Google Shopping intent signals.

Write the blog in the first week. I was too focused on product pages initially. The blog drives organic discovery for top-of-funnel queries that the product page will never rank for.

Build the sitemap generator first. I manually maintained sitemap.xml for two months. Moving to a build-time generator that reads every page and outputs correct <lastmod> dates cut indexing lag by 40%.

The repo

The site is powerbulb.ug. The repo is private but I am considering open-sourcing the SEO-relevant parts (schema generator, sitemap builder, geo-SEO page template) as a standalone starter. If you would find that useful, reply below — I'll bump it up the queue.


TL;DR

Zero-competition SEO markets are real. They reward basic execution (valid schema, unique copy, hyperlocal pages) that would not even register in saturated markets. If you are building for a developing market, the ceiling is higher and the competition is lower than you think. Ship the boring stuff first.


I is a Kampala-based software engineer and the founder of PowerBulb.ug. Follow me on Dev.to for more write-ups on building for underserved markets.

Top comments (0)