<?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: Landscapers.city</title>
    <description>The latest articles on DEV Community by Landscapers.city (@landscaperscity).</description>
    <link>https://dev.to/landscaperscity</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%2F4224788%2F4902004c-68e2-4598-9493-ef5fd8e4ef61.png</url>
      <title>DEV Community: Landscapers.city</title>
      <link>https://dev.to/landscaperscity</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/landscaperscity"/>
    <language>en</language>
    <item>
      <title>Structuring a 312-City Local Directory for SEO Without Creating Thin Pages</title>
      <dc:creator>Landscapers.city</dc:creator>
      <pubDate>Tue, 03 Nov 2026 17:50:44 +0000</pubDate>
      <link>https://dev.to/landscaperscity/structuring-a-312-city-local-directory-for-seo-without-creating-thin-pages-5bhe</link>
      <guid>https://dev.to/landscaperscity/structuring-a-312-city-local-directory-for-seo-without-creating-thin-pages-5bhe</guid>
      <description>&lt;h1&gt;
  
  
  Structuring a 312-City Local Directory for SEO Without Creating Thin Pages
&lt;/h1&gt;

&lt;p&gt;Local directories have a reputation problem, and most of it is earned. Many of them are thousands of near-identical pages — "Best Plumbers in {City}" — with a templated paragraph and a list of scraped names. Search engines have been explicit for years that they don't want to rank this kind of content, and users bounce from it instantly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://landscapers.city" rel="noopener noreferrer"&gt;landscapers.city&lt;/a&gt; is a US directory built only for landscaping: 312 cities and more than 3,153 listed companies. This post covers the structural decisions that matter when you build a directory at that scale — URL design, what each city page needs to justify existing, internal linking, sitemaps, and a few mistakes worth avoiding. It's written for developers, because most of these decisions are made in code long before anyone writes a meta description.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Keep the URL model boring and flat
&lt;/h2&gt;

&lt;p&gt;The site uses two levels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/{city}-{state}              e.g. /chicago-il
/{city}-{state}/{business}   e.g. a single company profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus a handful of utility pages: &lt;code&gt;/estimate&lt;/code&gt;, &lt;code&gt;/pricing&lt;/code&gt;, &lt;code&gt;/premium&lt;/code&gt;, &lt;code&gt;/blog&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why flat?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One canonical path per entity.&lt;/strong&gt; A city exists at exactly one URL. No &lt;code&gt;/il/chicago&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;/chicago-il&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-readable and predictable.&lt;/strong&gt; Users (and link builders) can guess the URL for their own city.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disambiguation is built in.&lt;/strong&gt; There are many Springfields; appending the state code solves that without a lookup table in the URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shallow click depth.&lt;/strong&gt; Every business is two clicks from the homepage via its city.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Slug rules worth fixing early: lowercase, ASCII only, hyphens for spaces, strip punctuation (&lt;code&gt;st-louis-mo&lt;/code&gt;, not &lt;code&gt;st.-louis-mo&lt;/code&gt;), and &lt;strong&gt;never change a slug once it's indexed&lt;/strong&gt; without a 301.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A city page must earn its URL
&lt;/h2&gt;

&lt;p&gt;The core question for every programmatic page: &lt;em&gt;if this page didn't exist, would anyone miss it?&lt;/em&gt; For a city page, the honest answer is "yes" only if it contains something a user can act on.&lt;/p&gt;

&lt;p&gt;What a useful city page carries — look at &lt;a href="https://landscapers.city/pittsburgh-pa" rel="noopener noreferrer"&gt;Pittsburgh&lt;/a&gt; or &lt;a href="https://landscapers.city/denver-co" rel="noopener noreferrer"&gt;Denver&lt;/a&gt; for examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real businesses&lt;/strong&gt; with phone, address, hours and service categories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service grouping&lt;/strong&gt; (lawn care, design, hardscaping, irrigation, tree care) so the page answers "who does &lt;em&gt;X&lt;/em&gt; here?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A clear action&lt;/strong&gt;: call, visit a profile, or get a rough price first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local context&lt;/strong&gt; where you actually have it — not invented "fun facts."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And what it should &lt;em&gt;not&lt;/em&gt; do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publish a city with zero or one listing just to fill a sitemap. A thin page is worse than no page; it dilutes crawl budget and trust. Gate city publication behind a minimum-content threshold in code.&lt;/li&gt;
&lt;li&gt;Spin the same paragraph with the city name swapped. If you can't say something specific, say less.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple publication rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_publish_city&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_listings&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;MIN_LISTINGS&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has_verified_contact_data&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;robots_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&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;"&lt;/span&gt;&lt;span class="s"&gt;index,follow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;should_publish_city&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noindex,follow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;noindex,follow&lt;/code&gt; lets you keep the page for users who arrive directly while not asking search engines to rank it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Business profiles: structured data and freshness
&lt;/h2&gt;

&lt;p&gt;Each company profile is the most specific, most useful page on the site. Treat it accordingly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LocalBusiness&lt;/code&gt; schema&lt;/strong&gt; (or a more specific subtype) with name, address, phone, opening hours and &lt;code&gt;areaServed&lt;/code&gt;. Only mark up what's visible on the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent NAP.&lt;/strong&gt; Name, address, phone should match how the business presents itself elsewhere. Inconsistent NAP is one of the most common local-SEO problems, and directories can either help or hurt it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness.&lt;/strong&gt; Free listings on landscapers.city are active for 365 days. An expiry model is a quiet SEO win: listings that nobody renews are often businesses that closed, and pruning them keeps pages accurate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle closures with intent.&lt;/strong&gt; When a listing goes away, either 301 to the city page or return 410. Don't leave soft-404s.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Internal linking as a graph, not an afterthought
&lt;/h2&gt;

&lt;p&gt;At 300+ cities, internal links are your main ranking signal distribution tool. A sensible graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;home ──► state/region hubs or top cities
city ──► every business in that city
city ──► nearby cities (by distance, not alphabet)
business ──► its city + 2–3 similar businesses
blog post ──► relevant cities and tools
tool (/estimate) ──► city pages after a result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Nearby cities" by geography&lt;/strong&gt; is genuinely useful for users on a metro edge (someone in a suburb may be served by companies from the next city over). Compute it with a simple haversine query on city centroids.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap link lists.&lt;/strong&gt; A footer with 312 city links on every page isn't navigation; it's noise. Show the closest or most relevant handful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual links from content.&lt;/strong&gt; A blog post about irrigation in dry climates should link to cities where that's relevant, in the sentence, not in a "related links" block.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Sitemaps and robots: small files, big mistakes
&lt;/h2&gt;

&lt;p&gt;The landscapers.city sitemap lists 1,222 URLs. At this scale one sitemap file is fine (the protocol limit is 50,000 URLs per file), but a few things are easy to get wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only include URLs that return 200 and are indexable.&lt;/strong&gt; A sitemap containing 404s or &lt;code&gt;noindex&lt;/code&gt; pages sends mixed signals. Run a nightly job that requests every sitemap URL and fails the build if any return non-200.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't block the sitemap in robots.txt.&lt;/strong&gt; It sounds obvious; it happens. A rule like &lt;code&gt;Disallow: /sitemap.xml$&lt;/code&gt; can slip in during a refactor. Add a test that parses robots.txt and asserts your sitemap path is allowed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;lastmod&lt;/code&gt; honestly.&lt;/strong&gt; Only bump it when page content actually changed (a new listing, updated hours), not on every deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split by type when it grows&lt;/strong&gt;: &lt;code&gt;sitemap-cities.xml&lt;/code&gt;, &lt;code&gt;sitemap-businesses.xml&lt;/code&gt;, &lt;code&gt;sitemap-blog.xml&lt;/code&gt;. This makes Search Console coverage reports far easier to read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal CI check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/sitemap.xml &lt;span class="se"&gt;\&lt;/span&gt;
 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'(?&amp;lt;=&amp;lt;loc&amp;gt;)[^&amp;lt;]+'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 | xargs &lt;span class="nt"&gt;-n1&lt;/span&gt; &lt;span class="nt"&gt;-P8&lt;/span&gt; curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code} %{url_effective}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'^200'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sitemap clean"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. The next layer: city × service
&lt;/h2&gt;

&lt;p&gt;The current structure is city and business. The obvious next dimension is service type — "hardscaping in Chicago," "irrigation in Henderson." It's also the most dangerous dimension, because it multiplies page count fast: 312 cities × dozens of service types is thousands of potential URLs.&lt;/p&gt;

&lt;p&gt;Rules we'd apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate a city × service page &lt;strong&gt;only when enough businesses in that city actually offer that service.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it something the city page doesn't have: service-specific questions to ask a contractor, what affects price for that job, and a filtered list.&lt;/li&gt;
&lt;li&gt;Canonicalize carefully so the city page and the service page don't compete for the same query.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Content that isn't a listing
&lt;/h2&gt;

&lt;p&gt;Directories that rank long-term usually have &lt;em&gt;something&lt;/em&gt; beyond listings. For landscapers.city that's a blog, a cost guide, and a &lt;a href="https://landscapers.city/estimate" rel="noopener noreferrer"&gt;photo-based estimate tool&lt;/a&gt;. Tools are particularly good for this: they're useful, they attract natural links, and they give every city page a meaningful next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Monetization that doesn't break trust
&lt;/h2&gt;

&lt;p&gt;The business model matters for SEO more than people think, because it shapes what you're tempted to do to pages. landscapers.city keeps free listings free and charges $249/year for Premium + AI Page: featured placement, a badge, analytics and an AI business page whose assistant answers homeowners 24/7 and captures leads. No per-lead fees.&lt;/p&gt;

&lt;p&gt;From a structure standpoint, the important rule is: &lt;strong&gt;paid placement should be labeled and shouldn't hide free listings.&lt;/strong&gt; Featured cards at the top of a city, clearly marked, are fine. Removing non-paying businesses from the page would make the page less useful — and less deserving of ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Flat, stable, state-disambiguated slugs&lt;/li&gt;
&lt;li&gt;[ ] Publication threshold for programmatic pages&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;LocalBusiness&lt;/code&gt; schema matching visible content&lt;/li&gt;
&lt;li&gt;[ ] Listing expiry + proper 301/410 on removal&lt;/li&gt;
&lt;li&gt;[ ] Geographic "nearby" linking, capped&lt;/li&gt;
&lt;li&gt;[ ] Sitemap with only 200/indexable URLs, tested in CI&lt;/li&gt;
&lt;li&gt;[ ] robots.txt test that the sitemap is allowed&lt;/li&gt;
&lt;li&gt;[ ] Clear labeling of paid placement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built a programmatic local site and found a better approach to the thin-page problem, share it in the comments — it's the hardest part of the whole exercise.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
