<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:blog="https://jonesrussell.github.io/blog/ns"><channel><title>Routing on Web Developer Blog</title><link>https://jonesrussell.github.io/blog/tags/routing/</link><description>Recent content in Routing on Web Developer Blog</description><image><title>Web Developer Blog</title><url>https://jonesrussell.github.io/blog/images/og-default.png</url><link>https://jonesrussell.github.io/blog/images/og-default.png</link></image><generator>Hugo -- 0.168.0</generator><language>en-us</language><lastBuildDate>Fri, 23 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jonesrussell.github.io/blog/tags/routing/feed.xml" rel="self" type="application/rss+xml"/><item><title>Do not let generated content replace your homepage</title><link>https://jonesrussell.github.io/blog/generated-content-homepage-route/</link><pubDate>Fri, 23 Oct 2026 00:00:00 +0000</pubDate><guid>https://jonesrussell.github.io/blog/generated-content-homepage-route/</guid><category>php</category><blog:tag>php</blog:tag><blog:tag>routing</blog:tag><blog:tag>testing</blog:tag><blog:tag>waaseyaa</blog:tag><description>Keep generated collection routes from replacing your application homepage when detail pages live at the root.</description><content:encoded><![CDATA[<p>Ahnii!</p>
<p>A content generator can produce valid routes and still erase the application you started with.</p>
<p>The failure appears when a content type uses a root-level detail route such as <code>/{slug}</code>. If the generator derives the collection route by removing the slug segment, it gets <code>/</code>. Registering that collection at the root can replace the homepage provided by your starter application.</p>
<p>A recent <a href="https://github.com/waaseyaa/framework/commit/17bf3bb78fa494fcd325a5753795d93952ede184">Waaseyaa route-generation repair</a> makes the ownership rule explicit. This post covers the route policy and the tests that keep a generated content index from taking over <code>/</code>.</p>
<h2 id="the-detail-route-is-not-the-collection-route">The detail route is not the collection route</h2>
<p>Consider three content types:</p>
<table>
	<thead>
			<tr>
					<th>Content type</th>
					<th>Detail route</th>
					<th>Expected collection</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Page</td>
					<td><code>/{slug}</code></td>
					<td><code>/page</code></td>
			</tr>
			<tr>
					<td>News</td>
					<td><code>/news/{slug}</code></td>
					<td><code>/news</code></td>
			</tr>
			<tr>
					<td>Archive</td>
					<td><code>/news/archive/{slug}</code></td>
					<td><code>/news/archive</code></td>
			</tr>
	</tbody>
</table>
<p>Removing the terminal slug works for the two prefixed routes. It does not work for the root-level route because the empty parent becomes <code>/</code>.</p>
<p>The original derivation followed that logic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$route <span style="color:#f92672">=</span> <span style="color:#a6e22e">preg_replace</span>(<span style="color:#e6db74">&#39;#/\{[^/]+\}$#&#39;</span>, <span style="color:#e6db74">&#39;&#39;</span>, $detailRoute) <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> $route <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;&#39;</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;/&#39;</span> <span style="color:#f92672">:</span> $route;
</span></span></code></pre></div><p>Both the homepage and the generated page collection then claimed the same literal URL. The router could serve a valid response while still serving the wrong owner.</p>
<p>That distinction matters. An HTTP 200 does not prove your homepage survived generation.</p>
<h2 id="give-the-root-collection-a-stable-name">Give the root collection a stable name</h2>
<p>The repaired recipe passes the bundle identifier into its collection-route function:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">indexRoute</span>(<span style="color:#a6e22e">string</span> $detailRoute, <span style="color:#a6e22e">string</span> $bundle)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    $route <span style="color:#f92672">=</span> <span style="color:#a6e22e">preg_replace</span>(<span style="color:#e6db74">&#39;#/\{[^/]+\}$#&#39;</span>, <span style="color:#e6db74">&#39;&#39;</span>, $detailRoute) <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> $route <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;&#39;</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;/&#39;</span> <span style="color:#f92672">.</span> $bundle <span style="color:#f92672">:</span> $route;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The policy is small:</p>
<ul>
<li>a root-level detail route keeps its authored <code>/{slug}</code> pattern;</li>
<li>its collection moves to <code>/&lt;bundle-id&gt;</code>;</li>
<li>a prefixed detail route keeps its parent as the collection;</li>
<li>the application homepage remains at <code>/</code>.</li>
</ul>
<p>For the default <code>page</code> bundle, individual pages still resolve from paths such as <code>/about</code>, while the collection lives at <code>/page</code>.</p>
<p>This is better than inventing a global <code>/content</code> prefix. The bundle already provides a stable, declared name, and prefixed routes do not change unnecessarily.</p>
<h2 id="reserve-literal-collection-paths-first">Reserve literal collection paths first</h2>
<p>Moving the collection exposes another possible collision. A root-level <code>page</code> bundle now owns <code>/page</code>, so another content type cannot also declare <code>/page/{slug}</code> and derive the same collection.</p>
<p>The recipe records each collection route while rendering definitions:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">isset</span>($indexRoutes[$indexRoute])) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">InvalidArgumentException</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Published content index route collision: </span><span style="color:#e6db74">{</span>$indexRoute<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$indexRoutes[$indexRoute] <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span></code></pre></div><p>This refuses the ambiguous site before publishing generated files. It also reserves the literal collection path ahead of the broad <code>/{slug}</code> detail matcher.</p>
<p>Route ordering can hide a collision during one request. Generation-time refusal makes the contract independent of registration order.</p>
<h2 id="test-ownership-not-only-response-codes">Test ownership, not only response codes</h2>
<p>The focused recipe tests cover the route table directly:</p>
<ul>
<li><code>/{slug}</code> produces <code>/page</code>;</li>
<li><code>/news/{slug}</code> produces <code>/news</code>;</li>
<li><code>/news/archive/{slug}</code> produces <code>/news/archive</code>;</li>
<li>no generated collection may equal <code>/</code>;</li>
<li>a second collection at <code>/page</code> is refused.</li>
</ul>
<p>The stronger integration check verifies the composed application:</p>
<ol>
<li>Generate and install the minimal published-content recipe.</li>
<li>Request <code>/</code> from the real HTTP runtime.</li>
<li>Assert the response contains a marker owned by the application&rsquo;s homepage template.</li>
<li>Request <code>/page</code>.</li>
<li>Assert the response contains the generated <code>page_index</code> listing.</li>
<li>Repeat the homepage request cold, warm, and after restarting the server process.</li>
</ol>
<p>The marker is important. Checking only for status 200 would accept the original bug because the generated listing also returned 200 at <code>/</code>.</p>
<p>You need evidence about <em>which component answered</em>, not just whether something answered.</p>
<h2 id="version-the-generated-contract">Version the generated contract</h2>
<p>Changing generated URLs is observable application behavior. Waaseyaa includes the collection policy in the recipe digest:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#e6db74">&#39;index_routes&#39;</span> <span style="color:#f92672">=&gt;</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;root_detail&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;bundle_path&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;prefixed_detail&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;parent_path&#39;</span>,
</span></span><span style="display:flex;"><span>],
</span></span></code></pre></div><p>That prevents an older generated selection from silently claiming the new output. Existing applications retain their routes until a reviewed regeneration adopts the changed recipe.</p>
<p>This is the right upgrade boundary for code generation:</p>
<ul>
<li>fix future output at the producer;</li>
<li>record the new generation contract;</li>
<li>refuse conflicting routes before writing;</li>
<li>require existing applications to regenerate deliberately.</li>
</ul>
<p>Do not patch generated metadata by hand to make verification pass. The generator, its digest, and the application output should move together.</p>
<h2 id="a-route-generation-checklist">A route-generation checklist</h2>
<p>Before you trust a generated route set, prove these boundaries:</p>
<table>
	<thead>
			<tr>
					<th>Boundary</th>
					<th>Minimum proof</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Homepage ownership</td>
					<td><code>/</code> renders an application-owned marker after generation</td>
			</tr>
			<tr>
					<td>Root detail routes</td>
					<td><code>/{slug}</code> keeps details at the root without creating a root collection</td>
			</tr>
			<tr>
					<td>Prefixed routes</td>
					<td>Parent collection paths remain unchanged</td>
			</tr>
			<tr>
					<td>Literal collisions</td>
					<td>Duplicate collection URLs fail before publication</td>
			</tr>
			<tr>
					<td>Matcher precedence</td>
					<td>Collection literals are reserved ahead of broad slug routes</td>
			</tr>
			<tr>
					<td>Upgrade behavior</td>
					<td>A changed route policy changes the generation contract</td>
			</tr>
	</tbody>
</table>
<p>Generated code is part of your application&rsquo;s public interface. Treat route ownership as a contract, and test the composed application that users will actually reach.</p>
<p>Baamaapii</p>
]]></content:encoded></item></channel></rss>