<?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: Classifieds24 Network</title>
    <description>The latest articles on DEV Community by Classifieds24 Network (@support_155abb6331703f4f4).</description>
    <link>https://dev.to/support_155abb6331703f4f4</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%2F4208066%2F398489d9-760f-4292-b0b4-5bba4a7f6768.png</url>
      <title>DEV Community: Classifieds24 Network</title>
      <link>https://dev.to/support_155abb6331703f4f4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/support_155abb6331703f4f4"/>
    <language>en</language>
    <item>
      <title>Category Trees in MySQL: Adjacency List vs Closure Table for a Classifieds Site</title>
      <dc:creator>Classifieds24 Network</dc:creator>
      <pubDate>Tue, 27 Oct 2026 12:30:11 +0000</pubDate>
      <link>https://dev.to/support_155abb6331703f4f4/category-trees-in-mysql-adjacency-list-vs-closure-table-for-a-classifieds-site-159n</link>
      <guid>https://dev.to/support_155abb6331703f4f4/category-trees-in-mysql-adjacency-list-vs-closure-table-for-a-classifieds-site-159n</guid>
      <description>&lt;p&gt;Classifieds sites live and die by their categories: "For Sale &amp;gt; Electronics &amp;gt; Phones" needs to load fast, show counts, and support breadcrumbs. I've used three approaches over the years. Here's where each one fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adjacency list: simple, but recursive
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="nb"&gt;UNSIGNED&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="nb"&gt;UNSIGNED&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent_id&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;Easy to insert and move. On MySQL 8 you can fetch a subtree with a recursive CTE:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On MySQL 5.7 or older you're stuck with loops in PHP or a fixed number of self-joins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closure table: fast reads, a bit more bookkeeping
&lt;/h2&gt;

&lt;p&gt;Store every ancestor/descendant pair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;category_paths&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;ancestor&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="nb"&gt;UNSIGNED&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;descendant&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="nb"&gt;UNSIGNED&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="nb"&gt;TINYINT&lt;/span&gt; &lt;span class="nb"&gt;UNSIGNED&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ancestor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;descendant&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;descendant&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;All listings under "Electronics", including subcategories, become one indexed join:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;listings&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;category_paths&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;descendant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ancestor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breadcrumbs are just as easy: select ancestors of the current category ordered by depth descending.&lt;/p&gt;

&lt;p&gt;Inserting a new category means copying the parent's ancestor rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;category_paths&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ancestor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;descendant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ancestor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;category_paths&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;descendant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Nested sets: skip it for classifieds
&lt;/h2&gt;

&lt;p&gt;Nested sets are fast to read but painful to write, and admins reorganize categories more often than you'd think. I wouldn't use them here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counts without counting
&lt;/h2&gt;

&lt;p&gt;Don't &lt;code&gt;COUNT(*)&lt;/code&gt; on every page view. Keep a &lt;code&gt;listing_count&lt;/code&gt; column per category, updated by a nightly job or on listing status changes, and roll it up through the closure table.&lt;/p&gt;

&lt;h2&gt;
  
  
  My pick
&lt;/h2&gt;

&lt;p&gt;Adjacency list for the source of truth, a closure table for reads, both maintained in the same transaction. Category pages stay at one indexed query, and moving a branch is still manageable.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;These patterns power the category pages across our network: &lt;a href="https://www.classifieds24.net/" rel="noopener noreferrer"&gt;Classifieds24.net&lt;/a&gt;, &lt;a href="https://www.classifieds24.info/" rel="noopener noreferrer"&gt;Classifieds24.info&lt;/a&gt;, &lt;a href="https://www.classifieds24.org/" rel="noopener noreferrer"&gt;Classifieds24.org&lt;/a&gt;, &lt;a href="https://www.globalclassifieds.info/" rel="noopener noreferrer"&gt;GlobalClassifieds.info&lt;/a&gt;, &lt;a href="https://www.globalclassifieds.org/" rel="noopener noreferrer"&gt;GlobalClassifieds.org&lt;/a&gt;, &lt;a href="https://www.hirepros.pro/" rel="noopener noreferrer"&gt;HirePros.pro&lt;/a&gt;, &lt;a href="https://realtor.pm/" rel="noopener noreferrer"&gt;Realtor.pm&lt;/a&gt; and &lt;a href="https://linkmarket.app/" rel="noopener noreferrer"&gt;LinkMarket.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mysql</category>
      <category>database</category>
      <category>php</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Running a Network of Classifieds Sites From One PHP Codebase</title>
      <dc:creator>Classifieds24 Network</dc:creator>
      <pubDate>Tue, 27 Oct 2026 12:29:09 +0000</pubDate>
      <link>https://dev.to/support_155abb6331703f4f4/running-a-network-of-classifieds-sites-from-one-php-codebase-2ed5</link>
      <guid>https://dev.to/support_155abb6331703f4f4/running-a-network-of-classifieds-sites-from-one-php-codebase-2ed5</guid>
      <description>&lt;p&gt;I run a handful of classifieds and listing sites: local classifieds, a global version, a services directory, a property site and a marketplace for websites. They look different and target different audiences, but they share one PHP codebase and one database cluster. Here's how that's structured and what I'd do the same way again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolve the site from the host name
&lt;/h2&gt;

&lt;p&gt;Every request starts by working out which site it belongs to. A small &lt;code&gt;sites&lt;/code&gt; table holds the per-site config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sites&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="nb"&gt;UNSIGNED&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;host&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;190&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;default_country&lt;/span&gt; &lt;span class="nb"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;root_category_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="nb"&gt;UNSIGNED&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nv"&gt;$host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^www\./'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_HOST'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&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;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT * FROM sites WHERE host = ?'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FETCH_ASSOC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$site&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Unknown site'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SITE_ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$site&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache that lookup (APCu works well) so it isn't a query on every hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope data with a site column, not separate databases
&lt;/h2&gt;

&lt;p&gt;Listings carry a &lt;code&gt;site_id&lt;/code&gt;, plus an optional many-to-many table for listings that should also appear on sister sites. One schema means one migration path, and cross-posting a listing is just an extra row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;listing_sites&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;listing_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="nb"&gt;UNSIGNED&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;site_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="nb"&gt;UNSIGNED&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listing_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site_id&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;Every listing query joins through &lt;code&gt;listing_sites&lt;/code&gt;, so a site only ever sees what it's allowed to show.&lt;/p&gt;

&lt;h2&gt;
  
  
  Themes as thin template sets
&lt;/h2&gt;

&lt;p&gt;Each site has its own templates folder that overrides a shared base. The lookup falls back to &lt;code&gt;base/&lt;/code&gt; when a site doesn't override a file, so most sites only customize the header, footer and home page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-site SEO, shared logic
&lt;/h2&gt;

&lt;p&gt;Titles, meta descriptions and canonical URLs come from the site config. When a listing appears on more than one site, the canonical points to the site where it was originally posted, which avoids competing with yourself in search.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd watch out for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never trust the Host header blindly.&lt;/strong&gt; Only accept hosts that exist in &lt;code&gt;sites&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep sessions per host.&lt;/strong&gt; Separate cookie domains stop a login on one site leaking to another unintentionally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log the site ID with every error.&lt;/strong&gt; "Search is broken" means nothing until you know which site.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One codebase has kept maintenance sane: a bug fixed once is fixed everywhere.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and run these sites, and listings can appear across the whole network: &lt;a href="https://www.classifieds24.net/" rel="noopener noreferrer"&gt;Classifieds24.net&lt;/a&gt;, &lt;a href="https://www.classifieds24.info/" rel="noopener noreferrer"&gt;Classifieds24.info&lt;/a&gt;, &lt;a href="https://www.classifieds24.org/" rel="noopener noreferrer"&gt;Classifieds24.org&lt;/a&gt;, &lt;a href="https://www.globalclassifieds.info/" rel="noopener noreferrer"&gt;GlobalClassifieds.info&lt;/a&gt;, &lt;a href="https://www.globalclassifieds.org/" rel="noopener noreferrer"&gt;GlobalClassifieds.org&lt;/a&gt;, &lt;a href="https://www.hirepros.pro/" rel="noopener noreferrer"&gt;HirePros.pro&lt;/a&gt;, &lt;a href="https://realtor.pm/" rel="noopener noreferrer"&gt;Realtor.pm&lt;/a&gt; and &lt;a href="https://linkmarket.app/" rel="noopener noreferrer"&gt;LinkMarket.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>mysql</category>
    </item>
    <item>
      <title>A Developer Portfolio That Gets Replies: A Practical Checklist</title>
      <dc:creator>Classifieds24 Network</dc:creator>
      <pubDate>Tue, 27 Oct 2026 09:14:01 +0000</pubDate>
      <link>https://dev.to/support_155abb6331703f4f4/a-developer-portfolio-that-gets-replies-a-practical-checklist-28ii</link>
      <guid>https://dev.to/support_155abb6331703f4f4/a-developer-portfolio-that-gets-replies-a-practical-checklist-28ii</guid>
      <description>&lt;p&gt;Most developer portfolios fail the same way: a grid of project cards, each with a screenshot, a tech-stack badge row and a "View Code" link. Recruiters and hiring managers skim them in under a minute and move on. Below is a checklist I'd use to turn a portfolio from a gallery into evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Lead with the problem, not the stack
&lt;/h2&gt;

&lt;p&gt;"React + Node + MongoDB" tells a reader nothing about your judgment. Compare:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Inventory sync for a small print shop.&lt;/strong&gt; Their catalog lived in three spreadsheets. I built a PHP/MySQL import that reconciles SKUs nightly and flags conflicts, cutting manual fixes from hours to minutes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One paragraph, and the reader knows the domain, the constraint and the outcome. Put the stack underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Show three projects, not twelve
&lt;/h2&gt;

&lt;p&gt;Pick the three that best show range: one with real users or data, one that shows depth (performance, security, tricky state), and one that shows you can finish and polish. Everything else goes in a "more work" list or stays on GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Write a short case study for each
&lt;/h2&gt;

&lt;p&gt;A good case study fits on one screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context&lt;/strong&gt;: who it was for and what was broken&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraints&lt;/strong&gt;: budget, legacy code, deadlines, hosting limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decisions&lt;/strong&gt;: two or three choices you made and why&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt;: a number if you have one, a sentence if you don't&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What you'd change&lt;/strong&gt;: this is the part reviewers remember&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Make it fast and mobile-first
&lt;/h2&gt;

&lt;p&gt;Your portfolio is itself a project. Check it on a phone. Run Lighthouse. A reviewer opening a 6 MB hero video on hotel Wi-Fi will not wait.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx lighthouse https://your-portfolio.example &lt;span class="nt"&gt;--only-categories&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;performance,accessibility &lt;span class="nt"&gt;--view&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aim for real wins: compressed images (&lt;code&gt;webp&lt;/code&gt;/&lt;code&gt;avif&lt;/code&gt;), no layout shift, readable font sizes, tap targets that don't overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Fix the HTML basics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; per page (your name or the project title), then &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; in order&lt;/li&gt;
&lt;li&gt;A unique &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and meta description for each case study&lt;/li&gt;
&lt;li&gt;Alt text that describes screenshots&lt;/li&gt;
&lt;li&gt;Open Graph tags so links look right when shared&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are small, but they signal that you care about the details a team cares about.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Link to live demos that actually work
&lt;/h2&gt;

&lt;p&gt;Dead demos are worse than no demos. If a project needs a backend you no longer pay for, record a 60-second screen capture instead and say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Make contact effortless
&lt;/h2&gt;

&lt;p&gt;An email address or a short form, visible on every page. If you're open to specific roles or freelance work, say which.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Get it in front of people
&lt;/h2&gt;

&lt;p&gt;A portfolio nobody sees doesn't help. Share case studies as posts, join developer communities and link your portfolio from your profiles.&lt;/p&gt;

&lt;p&gt;If you want to see how other developers lay out case-study-style portfolios, the &lt;a href="https://www.devsocial.net/port/" rel="noopener noreferrer"&gt;DevSocial portfolio showcase&lt;/a&gt; has examples, and &lt;a href="https://www.devsocial.net/" rel="noopener noreferrer"&gt;DevSocial&lt;/a&gt; is a developer community where you can share yours and get feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Three projects, each with a one-screen case study&lt;/li&gt;
&lt;li&gt;[ ] Problem and result before stack&lt;/li&gt;
&lt;li&gt;[ ] Lighthouse performance and accessibility checked&lt;/li&gt;
&lt;li&gt;[ ] Single &lt;code&gt;h1&lt;/code&gt;, ordered headings, unique titles&lt;/li&gt;
&lt;li&gt;[ ] Every demo link works (or has a video)&lt;/li&gt;
&lt;li&gt;[ ] Contact info on every page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's the one thing on your portfolio you'd cut after reading this? I'd love to hear in the comments.&lt;/p&gt;

</description>
      <category>career</category>
      <category>webdev</category>
      <category>portfolio</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
