<?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: Kimusta</title>
    <description>The latest articles on DEV Community by Kimusta (@kimusta).</description>
    <link>https://dev.to/kimusta</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%2F4160658%2F763709e5-c5ce-4684-9d16-d1bacd5b93b5.png</url>
      <title>DEV Community: Kimusta</title>
      <link>https://dev.to/kimusta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kimusta"/>
    <language>en</language>
    <item>
      <title>Google Indexed 258 of My 406 Pages. The Sitemap Was Never the Problem.</title>
      <dc:creator>Kimusta</dc:creator>
      <pubDate>Mon, 19 Oct 2026 09:50:49 +0000</pubDate>
      <link>https://dev.to/kimusta/google-indexed-258-of-my-406-pages-the-sitemap-was-never-the-problem-3dhh</link>
      <guid>https://dev.to/kimusta/google-indexed-258-of-my-406-pages-the-sitemap-was-never-the-problem-3dhh</guid>
      <description>&lt;p&gt;I run &lt;a href="https://www.kimusta.com" rel="noopener noreferrer"&gt;Kimusta&lt;/a&gt;, a marketplace for local services in Turkey. 406 URLs in the sitemap, every one returning HTTP 200, every one with its own title, description and canonical.&lt;/p&gt;

&lt;p&gt;Google indexed 258 of them and left 140 in &lt;strong&gt;Discovered — currently not indexed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The obvious suspects were all clean: no crawl errors, no manual actions, robots.txt correct, sitemap read successfully. So I crawled all 406 URLs myself, with JavaScript disabled, and built a link graph from the raw HTML. The answer was embarrassing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The homepage linked to four pages
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Distance from &lt;code&gt;/&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Pages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 or more&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unreachable without JS or the sitemap&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;401&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four links. On a site with 406 pages.&lt;/p&gt;

&lt;p&gt;They pointed at &lt;code&gt;/hizmetler&lt;/code&gt;, &lt;code&gt;/hizmet-verenler&lt;/code&gt;, &lt;code&gt;/iletisim&lt;/code&gt; and &lt;code&gt;/yasal&lt;/code&gt; — and all four were &lt;strong&gt;single-page app shells&lt;/strong&gt;. Their HTML body was empty; the content arrived after the JavaScript bundle did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause: navigation was mounted by JavaScript
&lt;/h2&gt;

&lt;p&gt;Every server-rendered page started like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;header&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"commonSiteHeaderMount"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"site-header"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; ...real content... &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;footer&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"commonSiteFooterMount"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"site-footer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/js/common-site-header.js"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/js/common-site-footer.js"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The content was server-rendered. The navigation was not. Menu and footer links — the most important internal links on any site — only existed after the &lt;code&gt;defer&lt;/code&gt; scripts ran.&lt;/p&gt;

&lt;p&gt;Total internal links across the whole site, as reported by Search Console: &lt;strong&gt;970&lt;/strong&gt;. That is 2.4 per page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catalogue hub had zero links
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/hizmetler&lt;/code&gt; was the worst case. After the SPA rendered, the page had 323 words and 10 links — all of them menu items. &lt;strong&gt;None&lt;/strong&gt; pointed at any of the 119 service pages it was supposed to list.&lt;/p&gt;

&lt;p&gt;The 119 service pages, in turn, received a median of &lt;strong&gt;one&lt;/strong&gt; internal link each.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;p&gt;Three server-side changes. No framework, no build step.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Render the chrome on the server
&lt;/h3&gt;

&lt;p&gt;One PHP partial returns exactly the markup the JavaScript was already producing:&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;// lib/site-chrome.php&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;k_ust_chrome_header&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;return&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;'HTML'
&amp;lt;header class="site-nav"&amp;gt;
  &amp;lt;a class="brand" href="/"&amp;gt;KIMUSTA&amp;lt;/a&amp;gt;
  &amp;lt;nav class="desktop-nav"&amp;gt;
    &amp;lt;a href="/hizmetler"&amp;gt;Hizmetler&amp;lt;/a&amp;gt;
    &amp;lt;a href="/hizmet-veren-basvurusu"&amp;gt;Hizmet Veren Ol&amp;lt;/a&amp;gt;
  &amp;lt;/nav&amp;gt;
&amp;lt;/header&amp;gt;
HTML;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The templates print it straight into the mount:&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="nt"&gt;&amp;lt;header&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"commonSiteHeaderMount"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nf"&gt;k_ust_chrome_header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client script stays. When it runs it writes the same markup into the same element, so there is no hydration mismatch — it only swaps the session-dependent buttons (login / account).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Give the hub a real body
&lt;/h3&gt;

&lt;p&gt;I replaced the &lt;code&gt;/hizmetler&lt;/code&gt; shell with a server-rendered A–Z index: 119 services, each linked, grouped by letter, with its parent category as a label. 46 KB of HTML that works with JavaScript switched off.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Measure the same thing again
&lt;/h3&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;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Links on a city page&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links on &lt;code&gt;/hizmet&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links on &lt;code&gt;/hizmetler&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;142&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The other 140 pages are a different problem
&lt;/h2&gt;

&lt;p&gt;Those 140 URLs are city pages: 20 categories × 12 cities, generated from one template. Measured across a category, the text overlap is &lt;strong&gt;91%&lt;/strong&gt;. Only the city name changes.&lt;/p&gt;

&lt;p&gt;That is not a crawling bug. It is a content verdict, and no amount of internal linking fixes it.&lt;/p&gt;

&lt;p&gt;What I did &lt;strong&gt;not&lt;/strong&gt; do is delete them. They are linked, they return 200, and the catalogue is still growing — a category that has one service today will have ten next quarter, and its city pages become useful on their own. Deleting URLs to please a crawler is how you lose the pages you will want later.&lt;/p&gt;

&lt;p&gt;The honest plan is slower: write content that actually differs per city (provider counts, price ranges, seasonal demand) and let the thin ones stay unindexed until they earn it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five things I took away
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure crawl depth, not sitemap size.&lt;/strong&gt; A 406-URL sitemap tells you nothing about what Google can reach by following links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If a component is navigation, render it on the server.&lt;/strong&gt; Hydration is for interactivity, not for structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never ship an empty mount for anything you want indexed.&lt;/strong&gt; An SPA shell returns 200 and looks fine in a browser, which is exactly why it survives review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovered — currently not indexed is a quality signal.&lt;/strong&gt; 140 pages said it at once; that was the content model talking, not a technical fault.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programmatic pages are cheap to generate and expensive to earn.&lt;/strong&gt; Generating them took an afternoon. Everything since has been the real work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The site is at &lt;a href="https://www.kimusta.com" rel="noopener noreferrer"&gt;kimusta.com&lt;/a&gt;; the catalogue index that replaced the empty shell is at &lt;a href="https://www.kimusta.com/hizmetler" rel="noopener noreferrer"&gt;kimusta.com/hizmetler&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Running a two-sided marketplace on shared hosting: PHP 7.4, no framework, no cron</title>
      <dc:creator>Kimusta</dc:creator>
      <pubDate>Sun, 04 Oct 2026 00:08:57 +0000</pubDate>
      <link>https://dev.to/kimusta/running-a-two-sided-marketplace-on-shared-hosting-php-74-no-framework-no-cron-521n</link>
      <guid>https://dev.to/kimusta/running-a-two-sided-marketplace-on-shared-hosting-php-74-no-framework-no-cron-521n</guid>
      <description>&lt;p&gt;I run &lt;a href="https://www.kimusta.com" rel="noopener noreferrer"&gt;Kimusta&lt;/a&gt;, a marketplace for local services in Turkey. Someone posts what they need (a move, a deep clean, a broken boiler), vetted service providers send quotes, and the customer compares them.&lt;/p&gt;

&lt;p&gt;The interesting part is not the product. The interesting part is that it runs on &lt;strong&gt;cheap shared hosting&lt;/strong&gt;: PHP 7.4 behind LiteSpeed, MariaDB 10.3, no framework, no Node runtime, no Redis, no queue worker, and no guarantee that cron will actually fire. Here is what that constraint forced me to build, and the bugs it produced.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. No trust in cron → "trigger on request" background jobs
&lt;/h2&gt;

&lt;p&gt;Shared hosting cron is unreliable (and sometimes not available at all). Instead of a scheduler, the app runs background work &lt;strong&gt;inside normal web requests&lt;/strong&gt;, gated so that only roughly 1 in 20 requests actually executes it:&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;// called early in the request lifecycle; cheap gate, no locks needed at this volume&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;random_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;match_auto_tick&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// pair new requests with suitable providers&lt;/span&gt;
    &lt;span class="nf"&gt;davet_hatirlatma_tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// remind providers whose invitations expire soon&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules make this safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every job must be idempotent.&lt;/strong&gt; If the same tick runs twice, nothing changes twice. The reminder job writes a &lt;code&gt;reminder_sent_at&lt;/code&gt; marker so an invitation is reminded exactly once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jobs must be short.&lt;/strong&gt; If a tick takes two seconds, one unlucky user pays for it. All queries are indexed and the batches are small.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Never send email inside a database transaction
&lt;/h2&gt;

&lt;p&gt;This was a real, user-visible bug. Matching wrote invitation rows and updated the request status inside a transaction — and notifications were simply missing from the flow. Providers had invitations sitting in the database that they never saw.&lt;/p&gt;

&lt;p&gt;The fix was structural: collect the affected provider IDs inside the transaction, &lt;code&gt;COMMIT&lt;/code&gt;, &lt;strong&gt;then&lt;/strong&gt; notify.&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="nv"&gt;$etkilenenServisler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;beginTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// ... insert invitations, update request status, collect provider ids ...&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;commit&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;$etkilenenServisler&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$servisId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   &lt;span class="c1"&gt;// outside the transaction, on purpose&lt;/span&gt;
    &lt;span class="nf"&gt;davet_bildirimlerini_gonder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$talepId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$servisId&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;If the mail server is slow or down, the data is already committed. The user never waits on SMTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Deliverability: a missing header can put you in spam
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mail()&lt;/code&gt; is disabled on this host, so everything goes through SMTP on &lt;code&gt;localhost&lt;/code&gt;. Mail was being &lt;em&gt;accepted&lt;/em&gt; (&lt;code&gt;250 OK&lt;/code&gt;) — but landing in spam. The spam scanner report was blunt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MISSING_MID 1.60
DOS_BODY_HIGH_NO_MID 4.00
=&amp;gt; score 9.06 (required 5)  → spam
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app never set &lt;code&gt;Message-ID&lt;/code&gt;. Adding &lt;code&gt;Message-ID&lt;/code&gt;, &lt;code&gt;Date&lt;/code&gt; and &lt;code&gt;To&lt;/code&gt; headers dropped the score to &lt;strong&gt;3.461 — not spam&lt;/strong&gt;, and the subject lost its &lt;code&gt;{Spam?}&lt;/code&gt; prefix.&lt;/p&gt;

&lt;p&gt;Deliverability is not "did SMTP return 250". It is "did the message arrive, and does the scanner like it". Test with real external addresses, and read the actual headers.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Single-page apps hand out soft 404s for free
&lt;/h2&gt;

&lt;p&gt;The client-side router meant the fallback rule sent &lt;strong&gt;every unknown URL&lt;/strong&gt; to the app shell with HTTP &lt;code&gt;200&lt;/code&gt;. Google sees an infinite supply of URLs that all return the front page — the classic soft 404.&lt;/p&gt;

&lt;p&gt;The fix was a small server-side router that returns honest status codes, driven by the &lt;strong&gt;same route manifest the client uses&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;known route or matching pattern → &lt;code&gt;200&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;unknown → &lt;code&gt;404&lt;/code&gt; (still rendering the shell, but with the correct status)&lt;/li&gt;
&lt;li&gt;if the manifest cannot be read → fail open, never fail closed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One source of truth for both sides means the server and the SPA cannot drift apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Programmatic pages: volume is easy, uniqueness is the work
&lt;/h2&gt;

&lt;p&gt;The catalogue has 139 service categories, plus city-level pages, which produced a &lt;strong&gt;406-URL sitemap&lt;/strong&gt;. Generating the pages took an afternoon; making them &lt;em&gt;not&lt;/em&gt; thin content is the ongoing part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unique title, description and canonical per page&lt;/li&gt;
&lt;li&gt;breadcrumbs and real internal links between parent/child categories&lt;/li&gt;
&lt;li&gt;city pages only for cities that actually have districts in the database (&lt;a href="https://www.kimusta.com/hizmet/temizlik/istanbul" rel="noopener noreferrer"&gt;example: cleaning services in Istanbul&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;non-existent combinations return &lt;code&gt;404&lt;/code&gt; instead of an empty page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A page that nobody can rank for is not neutral — it dilutes the crawl budget for the pages that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Instant indexing (IndexNow) has one annoying prerequisite
&lt;/h2&gt;

&lt;p&gt;IndexNow is easy once ownership is verified and infuriating before that: submissions fail with &lt;code&gt;403 SiteVerificationNotCompleted&lt;/code&gt; and no further explanation. The key file has to be reachable at the root, and verification has to be completed on each search engine's side first. After that, 406 URLs submitted in one go.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The cold-start bug nobody warns you about
&lt;/h2&gt;

&lt;p&gt;A two-sided marketplace can be technically perfect and still dead, because &lt;strong&gt;the supply side never learns that demand exists&lt;/strong&gt;. Our first real customer request produced three invitations — and zero quotes, because none of the providers ever opened the panel to look.&lt;/p&gt;

&lt;p&gt;Matching is not the feature. &lt;strong&gt;Notifying&lt;/strong&gt; is the feature. The notification has to arrive where the provider already is (email), and it has to say something concrete: what the job is, which district, what time window, when the invitation expires — without leaking personal data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build notifications before the matching engine.&lt;/strong&gt; Data in the database that nobody is told about is worthless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return honest HTTP status codes from day one.&lt;/strong&gt; Retrofitting a router is more work than writing it first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test email deliverability against a real mailbox before launch&lt;/strong&gt;, not after the first customer asks why nothing arrived.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume there is no cron.&lt;/strong&gt; "Trigger on request" is not a hack if you design the jobs to be short and idempotent — it is a deployment model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this needs a framework, a VPS, or a queue. It needs deciding what the constraints are and then designing inside them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Kimusta is live at &lt;a href="https://www.kimusta.com" rel="noopener noreferrer"&gt;kimusta.com&lt;/a&gt;. The whole catalogue — from cleaning to boiler repair — is browsable at &lt;a href="https://www.kimusta.com/hizmetler" rel="noopener noreferrer"&gt;kimusta.com/hizmetler&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>startup</category>
      <category>php</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
